Skip to content

Commit abd534b

Browse files
committed
Fix a race condition in TestMessageHandler
Not entirely sure what introduced the race condition. Found by thread sanitizer. Probably wouldn’t have been an issue if we had migrated this code to strict concurrency checking.
1 parent cbb5693 commit abd534b

File tree

4 files changed

+51
-41
lines changed

4 files changed

+51
-41
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ let package = Package(
155155
dependencies: [
156156
"LanguageServerProtocol",
157157
"LanguageServerProtocolJSONRPC",
158+
"SKSupport",
158159
]
159160
),
160161

Sources/LSPTestSupport/TestJSONRPCConnection.swift

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import LanguageServerProtocol
1414
import LanguageServerProtocolJSONRPC
15+
import SKSupport
1516
import XCTest
1617

1718
import class Foundation.Pipe
@@ -24,7 +25,7 @@ public final class TestJSONRPCConnection {
2425
public let server: TestServer
2526
public let serverConnection: JSONRPCConnection
2627

27-
public init() {
28+
public init(allowUnexpectedNotification: Bool = true) {
2829
clientConnection = JSONRPCConnection(
2930
name: "client",
3031
protocol: testMessageRegistry,
@@ -39,7 +40,7 @@ public final class TestJSONRPCConnection {
3940
outFD: serverToClient.fileHandleForWriting
4041
)
4142

42-
client = TestMessageHandler(server: clientConnection)
43+
client = TestMessageHandler(server: clientConnection, allowUnexpectedNotification: allowUnexpectedNotification)
4344
server = TestServer(client: serverConnection)
4445

4546
clientConnection.start(receiveHandler: client) {
@@ -66,8 +67,8 @@ public struct TestLocalConnection {
6667
public let server: TestServer
6768
public let serverConnection: LocalConnection = .init()
6869

69-
public init() {
70-
client = TestMessageHandler(server: serverConnection)
70+
public init(allowUnexpectedNotification: Bool = true) {
71+
client = TestMessageHandler(server: serverConnection, allowUnexpectedNotification: allowUnexpectedNotification)
7172
server = TestServer(client: clientConnection)
7273

7374
clientConnection.start(handler: client)
@@ -80,17 +81,20 @@ public struct TestLocalConnection {
8081
}
8182
}
8283

83-
public final class TestMessageHandler: MessageHandler {
84+
public actor TestMessageHandler: MessageHandler {
8485
/// The connection to the language client.
8586
public let server: Connection
8687

87-
public init(server: Connection) {
88-
self.server = server
89-
}
88+
private let messageHandlingQueue = AsyncQueue<Serial>()
9089

91-
var oneShotNotificationHandlers: [((Any) -> Void)] = []
90+
private var oneShotNotificationHandlers: [((Any) -> Void)] = []
9291

93-
public var allowUnexpectedNotification: Bool = true
92+
private let allowUnexpectedNotification: Bool
93+
94+
public init(server: Connection, allowUnexpectedNotification: Bool = true) {
95+
self.server = server
96+
self.allowUnexpectedNotification = allowUnexpectedNotification
97+
}
9498

9599
public func appendOneShotNotificationHandler<N: NotificationType>(_ handler: @escaping (N) -> Void) {
96100
oneShotNotificationHandlers.append({ anyNote in
@@ -101,7 +105,14 @@ public final class TestMessageHandler: MessageHandler {
101105
})
102106
}
103107

104-
public func handle(_ notification: some NotificationType, from clientID: ObjectIdentifier) {
108+
/// The LSP server sent a notification to the client. Handle it.
109+
public nonisolated func handle(_ notification: some NotificationType, from clientID: ObjectIdentifier) {
110+
messageHandlingQueue.async {
111+
await self.handleNotificationImpl(notification)
112+
}
113+
}
114+
115+
public func handleNotificationImpl(_ notification: some NotificationType) {
105116
guard !oneShotNotificationHandlers.isEmpty else {
106117
if allowUnexpectedNotification { return }
107118
fatalError("unexpected notification \(notification)")
@@ -110,25 +121,25 @@ public final class TestMessageHandler: MessageHandler {
110121
handler(notification)
111122
}
112123

113-
public func handle<R: RequestType>(
114-
_ params: R,
124+
/// The LSP server sent a request to the client. Handle it.
125+
public nonisolated func handle<Request: RequestType>(
126+
_ request: Request,
115127
id: RequestID,
116128
from clientID: ObjectIdentifier,
117-
reply: @escaping (LSPResult<R.Response>) -> Void
129+
reply: @escaping (LSPResult<Request.Response>) -> Void
118130
) {
119-
reply(.failure(.methodNotFound(R.method)))
131+
reply(.failure(.methodNotFound(Request.method)))
120132
}
121133
}
122134

123135
extension TestMessageHandler: Connection {
124-
125-
/// Send a notification to the language server.
126-
public func send(_ notification: some NotificationType) {
136+
/// Send a notification to the LSP server.
137+
public nonisolated func send(_ notification: some NotificationType) {
127138
server.send(notification)
128139
}
129140

130-
/// Send a request to the language server and (asynchronously) receive a reply.
131-
public func send<Request: RequestType>(
141+
/// Send a request to the LSP server and (asynchronously) receive a reply.
142+
public nonisolated func send<Request: RequestType>(
132143
_ request: Request,
133144
reply: @escaping (LSPResult<Request.Response>) -> Void
134145
) -> RequestID {

Tests/LanguageServerProtocolJSONRPCTests/ConnectionTests.swift

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ class ConnectionTests: XCTestCase {
2424
var connection: TestJSONRPCConnection! = nil
2525

2626
override func setUp() {
27-
connection = TestJSONRPCConnection()
28-
connection.client.allowUnexpectedNotification = false
27+
connection = TestJSONRPCConnection(allowUnexpectedNotification: false)
2928
}
3029

3130
override func tearDown() {
@@ -52,12 +51,12 @@ class ConnectionTests: XCTestCase {
5251
waitForExpectations(timeout: defaultTimeout)
5352
}
5453

55-
func testMessageBuffer() throws {
54+
func testMessageBuffer() async throws {
5655
let client = connection.client
5756
let clientConnection = connection.clientConnection
5857
let expectation = self.expectation(description: "note received")
5958

60-
client.appendOneShotNotificationHandler { (note: EchoNotification) in
59+
await client.appendOneShotNotificationHandler { (note: EchoNotification) in
6160
XCTAssertEqual(note.string, "hello!")
6261
expectation.fulfill()
6362
}
@@ -76,11 +75,11 @@ class ConnectionTests: XCTestCase {
7675
_rawData: [note1Str.utf8.last!, note2Str.utf8.first!].withUnsafeBytes { DispatchData(bytes: $0) }
7776
)
7877

79-
waitForExpectations(timeout: defaultTimeout)
78+
try await fulfillmentOfOrThrow([expectation])
8079

8180
let expectation2 = self.expectation(description: "note received")
8281

83-
client.appendOneShotNotificationHandler { (note: EchoNotification) in
82+
await client.appendOneShotNotificationHandler { (note: EchoNotification) in
8483
XCTAssertEqual(note.string, "no way!")
8584
expectation2.fulfill()
8685
}
@@ -89,7 +88,7 @@ class ConnectionTests: XCTestCase {
8988
clientConnection.send(_rawData: [b].withUnsafeBytes { DispatchData(bytes: $0) })
9089
}
9190

92-
waitForExpectations(timeout: defaultTimeout)
91+
try await fulfillmentOfOrThrow([expectation2])
9392

9493
// Close the connection before accessing _requestBuffer, which ensures we don't race.
9594
connection.serverConnection.close()
@@ -118,18 +117,18 @@ class ConnectionTests: XCTestCase {
118117
waitForExpectations(timeout: defaultTimeout)
119118
}
120119

121-
func testEchoNote() {
120+
func testEchoNote() async throws {
122121
let client = connection.client
123122
let expectation = self.expectation(description: "note received")
124123

125-
client.appendOneShotNotificationHandler { (note: EchoNotification) in
124+
await client.appendOneShotNotificationHandler { (note: EchoNotification) in
126125
XCTAssertEqual(note.string, "hello!")
127126
expectation.fulfill()
128127
}
129128

130129
client.send(EchoNotification(string: "hello!"))
131130

132-
waitForExpectations(timeout: defaultTimeout)
131+
try await fulfillmentOfOrThrow([expectation])
133132
}
134133

135134
func testUnknownRequest() {
@@ -210,33 +209,33 @@ class ConnectionTests: XCTestCase {
210209
waitForExpectations(timeout: defaultTimeout)
211210
}
212211

213-
func testSendBeforeClose() {
212+
func testSendBeforeClose() async throws {
214213
let client = connection.client
215214
let server = connection.server
216215

217216
let expectation = self.expectation(description: "received notification")
218-
client.appendOneShotNotificationHandler { (note: EchoNotification) in
217+
await client.appendOneShotNotificationHandler { (note: EchoNotification) in
219218
expectation.fulfill()
220219
}
221220

222221
server.client.send(EchoNotification(string: "about to close!"))
223222
connection.serverConnection.close()
224223

225-
waitForExpectations(timeout: defaultTimeout)
224+
try await fulfillmentOfOrThrow([expectation])
226225
}
227226

228-
func testSendSynchronouslyBeforeClose() {
227+
func testSendSynchronouslyBeforeClose() async throws {
229228
let client = connection.client
230229

231230
let expectation = self.expectation(description: "received notification")
232-
client.appendOneShotNotificationHandler { (note: EchoNotification) in
231+
await client.appendOneShotNotificationHandler { (note: EchoNotification) in
233232
expectation.fulfill()
234233
}
235234
let notification = EchoNotification(string: "about to close!")
236235
connection.serverConnection._send(.notification(notification), async: false)
237236
connection.serverConnection.close()
238237

239-
waitForExpectations(timeout: defaultTimeout)
238+
try await fulfillmentOfOrThrow([expectation])
240239
}
241240

242241
/// We can explicitly close a connection, but the connection also

Tests/LanguageServerProtocolTests/ConnectionTests.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ class ConnectionTests: XCTestCase {
1919
var connection: TestLocalConnection! = nil
2020

2121
override func setUp() {
22-
connection = TestLocalConnection()
23-
connection.client.allowUnexpectedNotification = false
22+
connection = TestLocalConnection(allowUnexpectedNotification: false)
2423
}
2524

2625
override func tearDown() {
@@ -61,17 +60,17 @@ class ConnectionTests: XCTestCase {
6160
waitForExpectations(timeout: defaultTimeout)
6261
}
6362

64-
func testEchoNote() {
63+
func testEchoNote() async throws {
6564
let client = connection.client
6665
let expectation = self.expectation(description: "note received")
6766

68-
client.appendOneShotNotificationHandler { (note: EchoNotification) in
67+
await client.appendOneShotNotificationHandler { (note: EchoNotification) in
6968
XCTAssertEqual(note.string, "hello!")
7069
expectation.fulfill()
7170
}
7271

7372
client.send(EchoNotification(string: "hello!"))
7473

75-
waitForExpectations(timeout: defaultTimeout)
74+
try await fulfillmentOfOrThrow([expectation])
7675
}
7776
}

0 commit comments

Comments
 (0)