Skip to content

Commit d043dc0

Browse files
authored
Merge pull request #860 from ahoppen/ahoppen/cancellation
Implement request cancellation
2 parents 875a876 + f29c97f commit d043dc0

17 files changed

+278
-89
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ let package = Package(
103103
name: "LanguageServerProtocol",
104104
dependencies: [
105105
"LSPLogging",
106+
"SKSupport",
106107
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
107108
],
108109
exclude: ["CMakeLists.txt"]

Sources/LSPTestSupport/Assertions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public func assertNoThrow<T>(
3636
}
3737
}
3838

39-
/// Same as `XCTAssertThrows` but executes the trailing closure.
39+
/// Same as `XCTAssertThrows` but allows the expression to be async
4040
public func assertThrowsError<T>(
4141
_ expression: @autoclosure () async throws -> T,
4242
_ message: @autoclosure () -> String = "",

Sources/LSPTestSupport/TestJSONRPCConnection.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ public final class TestMessageHandler: MessageHandler {
129129
from clientID: ObjectIdentifier,
130130
reply: @escaping (LSPResult<R.Response>) -> Void
131131
) {
132-
let cancellationToken = CancellationToken()
133-
134-
let request = Request(params, id: id, clientID: clientID, cancellation: cancellationToken, reply: reply)
132+
let request = Request(params, id: id, clientID: clientID, reply: reply)
135133

136134
guard !oneShotRequestHandlers.isEmpty else {
137135
fatalError("unexpected request \(request)")
@@ -179,14 +177,11 @@ public final class TestServer: MessageHandler {
179177
from clientID: ObjectIdentifier,
180178
reply: @escaping (LSPResult<R.Response>) -> Void
181179
) {
182-
let cancellationToken = CancellationToken()
183-
184180
if let params = params as? EchoRequest {
185181
let req = Request(
186182
params,
187183
id: id,
188184
clientID: clientID,
189-
cancellation: cancellationToken,
190185
reply: { result in
191186
reply(result.map({ $0 as! R.Response }))
192187
}
@@ -197,7 +192,6 @@ public final class TestServer: MessageHandler {
197192
params,
198193
id: id,
199194
clientID: clientID,
200-
cancellation: cancellationToken,
201195
reply: { result in
202196
reply(result.map({ $0 as! R.Response }))
203197
}

Sources/LanguageServerProtocol/AsyncQueue.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker> {
8888
let throwingTask = asyncThrowing(priority: priority, metadata: metadata, operation: operation)
8989
return Task {
9090
do {
91-
return try await throwingTask.value
91+
return try await throwingTask.valuePropagatingCancellation
9292
} catch {
9393
// We know this can never happen because `operation` does not throw.
9494
preconditionFailure("Executing a task threw an error even though the operation did not throw")
@@ -141,7 +141,7 @@ public final class AsyncQueue<TaskMetadata: DependencyTracker> {
141141

142142
/// Convenience overloads for serial queues.
143143
extension AsyncQueue where TaskMetadata == Serial {
144-
/// Same as ``async(priority:operation:)`` but specialized for serial queues
144+
/// Same as ``async(priority:operation:)`` but specialized for serial queues
145145
/// that don't specify any metadata.
146146
@discardableResult
147147
public func async<Success: Sendable>(
@@ -151,7 +151,7 @@ extension AsyncQueue where TaskMetadata == Serial {
151151
return self.async(priority: priority, metadata: Serial(), operation: operation)
152152
}
153153

154-
/// Same as ``asyncThrowing(priority:metadata:operation:)`` but specialized
154+
/// Same as ``asyncThrowing(priority:metadata:operation:)`` but specialized
155155
/// for serial queues that don't specify any metadata.
156156
public func asyncThrowing<Success: Sendable>(
157157
priority: TaskPriority? = nil,

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
add_library(LanguageServerProtocol STATIC
22
AsyncQueue.swift
3-
Cancellation.swift
43
Connection.swift
54
CustomCodable.swift
65
Error.swift
@@ -138,10 +137,8 @@ add_library(LanguageServerProtocol STATIC
138137
set_target_properties(LanguageServerProtocol PROPERTIES
139138
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
140139
target_link_libraries(LanguageServerProtocol PUBLIC
140+
LSPLogging
141+
SKSupport
141142
TSCBasic
142143
$<$<NOT:$<PLATFORM_ID:Darwin>>:swiftDispatch>
143144
$<$<NOT:$<PLATFORM_ID:Darwin>>:Foundation>)
144-
145-
target_link_libraries(LanguageServerProtocol PUBLIC
146-
LSPLogging
147-
)

Sources/LanguageServerProtocol/Cancellation.swift

Lines changed: 0 additions & 27 deletions
This file was deleted.

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Dispatch
14+
import SKSupport
1415

1516
/// An abstract connection, allow messages to be sent to a (potentially remote) `MessageHandler`.
1617
public protocol Connection: AnyObject {
@@ -144,3 +145,23 @@ extension LocalConnection: Connection {
144145
return id
145146
}
146147
}
148+
149+
extension Connection {
150+
/// Send the given request to the connection and await its result.
151+
///
152+
/// This method automatically sends a `CancelRequestNotification` to the
153+
/// connection if the task it is executing in is being cancelled.
154+
///
155+
/// - Warning: Because this message is `async`, it does not provide any ordering
156+
/// guarantees. If you need to gurantee that messages are sent in-order
157+
/// use the version with a completion handler.
158+
public func send<R: RequestType>(_ request: R) async throws -> R.Response {
159+
return try await withCancellableCheckedThrowingContinuation { continuation in
160+
return self.send(request) { result in
161+
continuation.resume(with: result)
162+
}
163+
} cancel: { requestID in
164+
self.send(CancelRequestNotification(id: requestID))
165+
}
166+
}
167+
}

Sources/LanguageServerProtocol/Request.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,15 @@ public final class Request<R: RequestType> {
3838
}
3939
}
4040

41-
/// The request's cancellation state.
42-
public let cancellationToken: CancellationToken
43-
4441
public init(
4542
_ request: Params,
4643
id: RequestID,
4744
clientID: ObjectIdentifier,
48-
cancellation: CancellationToken,
4945
reply: @escaping (LSPResult<Response>) -> Void
5046
) {
5147
self.id = id
5248
self.clientID = clientID
5349
self.params = request
54-
self.cancellationToken = cancellation
5550
self.replyBlock = reply
5651
}
5752

@@ -71,9 +66,6 @@ public final class Request<R: RequestType> {
7166
public func reply(_ result: Response) {
7267
reply(.success(result))
7368
}
74-
75-
/// Whether the result has been cancelled.
76-
public var isCancelled: Bool { return cancellationToken.isCancelled }
7769
}
7870

7971
/// A request object, wrapping the parameters of a `NotificationType`.

Sources/SKSupport/AsyncUtils.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
public extension Task {
14+
/// Awaits the value of the result.
15+
///
16+
/// If the current task is cancelled, this will cancel the subtask as well.
17+
var valuePropagatingCancellation: Success {
18+
get async throws {
19+
try await withTaskCancellationHandler {
20+
return try await self.value
21+
} onCancel: {
22+
self.cancel()
23+
}
24+
}
25+
}
26+
}
27+
28+
/// Allows the execution of a cancellable operation that returns the results
29+
/// via a completion handler.
30+
///
31+
/// `operation` must invoke the continuation's `resume` method exactly once.
32+
///
33+
/// If the task executing `withCancellableCheckedThrowingContinuation` gets
34+
/// cancelled, `cancel` is invoked with the handle that `operation` provided.
35+
public func withCancellableCheckedThrowingContinuation<Handle, Result>(
36+
_ operation: (_ continuation: CheckedContinuation<Result, any Error>) -> Handle,
37+
cancel: (Handle) -> Void
38+
) async throws -> Result {
39+
let handleWrapper = ThreadSafeBox<Handle?>(initialValue: nil)
40+
41+
@Sendable
42+
func callCancel() {
43+
/// Take the request ID out of the box. This ensures that we only send the
44+
/// cancel notification once in case the `Task.isCancelled` and the
45+
/// `onCancel` check race.
46+
if let handle = handleWrapper.takeValue() {
47+
cancel(handle)
48+
}
49+
}
50+
51+
return try await withTaskCancellationHandler(operation: {
52+
try Task.checkCancellation()
53+
return try await withCheckedThrowingContinuation { continuation in
54+
handleWrapper.value = operation(continuation)
55+
56+
// Check if the task was cancelled. This ensures we send a
57+
// CancelNotification even if the task gets cancelled after we register
58+
// the cancellation handler but before we set the `requestID`.
59+
if Task.isCancelled {
60+
callCancel()
61+
}
62+
}
63+
}, onCancel: callCancel)
64+
}

Sources/SKSupport/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11

22
add_library(SKSupport STATIC
3+
AsyncUtils.swift
34
BuildConfiguration.swift
45
ByteString.swift
6+
dlopen.swift
57
FileSystem.swift
68
LineTable.swift
79
Random.swift
810
Result.swift
9-
dlopen.swift)
11+
ThreadSafeBox.swift
12+
)
1013
set_target_properties(SKSupport PROPERTIES
1114
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})
1215
target_link_libraries(SKSupport PRIVATE

Sources/SKSupport/ThreadSafeBox.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Foundation
14+
15+
extension NSLock {
16+
/// NOTE: Keep in sync with SwiftPM's 'Sources/Basics/NSLock+Extensions.swift'
17+
fileprivate func withLock<T>(_ body: () throws -> T) rethrows -> T {
18+
lock()
19+
defer { unlock() }
20+
return try body()
21+
}
22+
}
23+
24+
/// A thread safe container that contains a value of type `T`.
25+
public class ThreadSafeBox<T> {
26+
/// Lock guarding `_value`.
27+
private let lock = NSLock()
28+
29+
private var _value: T
30+
31+
public var value: T {
32+
get {
33+
return lock.withLock {
34+
return _value
35+
}
36+
}
37+
set {
38+
lock.withLock {
39+
_value = newValue
40+
}
41+
}
42+
}
43+
44+
public init(initialValue: T) {
45+
_value = initialValue
46+
}
47+
48+
/// If the value in the box is an optional, return it and reset it to `nil`
49+
/// in an atomic operation.
50+
public func takeValue<U>() -> T where U? == T {
51+
lock.withLock {
52+
guard let value = self._value else { return nil }
53+
self._value = nil
54+
return value
55+
}
56+
}
57+
}

Sources/SourceKitD/SourceKitD.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,26 +83,23 @@ extension SourceKitD {
8383
public func send(_ req: SKDRequestDictionary) async throws -> SKDResponseDictionary {
8484
logRequest(req)
8585

86-
let sourcekitdResponse: SKDResponse = await withCheckedContinuation { continuation in
86+
let sourcekitdResponse: SKDResponse = try await withCancellableCheckedThrowingContinuation { continuation in
8787
var handle: sourcekitd_request_handle_t? = nil
88-
8988
api.send_request(req.dict, &handle) { _resp in
9089
continuation.resume(returning: SKDResponse(_resp, sourcekitd: self))
9190
}
91+
return handle
92+
} cancel: { handle in
93+
api.cancel_request(handle)
9294
}
95+
9396
logResponse(sourcekitdResponse)
9497

9598
guard let dict = sourcekitdResponse.value else {
9699
throw sourcekitdResponse.error!
97100
}
98101

99102
return dict
100-
101-
// FIXME: (async) Cancellation
102-
}
103-
104-
public func cancel(_ handle: sourcekitd_request_handle_t) {
105-
api.cancel_request(handle)
106103
}
107104
}
108105

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,6 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
298298
params,
299299
id: id,
300300
clientID: clientID,
301-
cancellation: CancellationToken(),
302301
reply: { result in
303302
reply(result)
304303
}
@@ -327,17 +326,7 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
327326
///
328327
/// The response of the request is returned asynchronously as the return value.
329328
func forwardRequestToClangd<R: RequestType>(_ request: R) async throws -> R.Response {
330-
try await withCheckedThrowingContinuation { continuation in
331-
_ = clangd.send(request) { result in
332-
switch result {
333-
case .success(let response):
334-
continuation.resume(returning: response)
335-
case .failure(let error):
336-
continuation.resume(throwing: error)
337-
}
338-
}
339-
}
340-
// FIXME: (async) Cancellation
329+
return try await clangd.send(request)
341330
}
342331

343332
func _crash() {

0 commit comments

Comments
 (0)