Skip to content

Commit 9747012

Browse files
committed
Implement request cancellation
When receiving a `CancellationNotification`, we cancel the task that handles the request with that ID. This will cause `cancel_notification` to be sent to sourcekitd or a `CancellationNotification` to be sent to `clangd`, which ultimately cancels the request.
1 parent fbd0cd1 commit 9747012

File tree

15 files changed

+226
-48
lines changed

15 files changed

+226
-48
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ let package = Package(
198198
.target(
199199
name: "LanguageServerProtocol",
200200
dependencies: [
201+
"SKSupport",
201202
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
202203
],
203204
exclude: ["CMakeLists.txt"]),

Sources/LanguageServerProtocol/AsyncQueue.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public final class AsyncQueue {
8787
let throwingTask = asyncThrowing(priority: priority, barrier: isBarrier, operation: operation)
8888
return Task {
8989
do {
90-
return try await throwingTask.value
90+
return try await throwingTask.valuePropagatingCancellation
9191
} catch {
9292
// We know this can never happen because `operation` does not throw.
9393
preconditionFailure("Executing a task threw an error even though the operation did not throw")

Sources/LanguageServerProtocol/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,7 @@ target_link_libraries(LanguageServerProtocol PUBLIC
141141
TSCBasic
142142
$<$<NOT:$<PLATFORM_ID:Darwin>>:swiftDispatch>
143143
$<$<NOT:$<PLATFORM_ID:Darwin>>:Foundation>)
144+
145+
target_link_libraries(LanguageServerProtocol PRIVATE
146+
SKSupport
147+
)

Sources/LanguageServerProtocol/Connection.swift

Lines changed: 28 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 {
@@ -136,3 +137,30 @@ extension LocalConnection: Connection {
136137
return id
137138
}
138139
}
140+
141+
extension Connection {
142+
/// Send the given request to the connection and await its result.
143+
///
144+
/// This method automatically sends a `CancelRequestNotification` to the
145+
/// connection if the task it is executing in is being cancelled.
146+
///
147+
/// - Warning: Because this message is `async`, it does not provide any ordering
148+
/// guarantees. If you need to gurantee that messages are sent in-order
149+
/// use the version with a completion handler.
150+
public func send<R: RequestType>(_ request: R) async throws -> R.Response {
151+
let requestIDWrapper = ThreadSafeBox<RequestID?>(initialValue: nil)
152+
try Task.checkCancellation()
153+
return try await withTaskCancellationHandler {
154+
try await withCheckedThrowingContinuation { continuation in
155+
let requestID = self.send(request) { result in
156+
continuation.resume(with: result)
157+
}
158+
requestIDWrapper.value = requestID
159+
}
160+
} onCancel: {
161+
if let requestID = requestIDWrapper.value {
162+
self.send(CancelRequestNotification(id: requestID))
163+
}
164+
}
165+
}
166+
}

Sources/SKSupport/CMakeLists.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
add_library(SKSupport STATIC
33
BuildConfiguration.swift
44
ByteString.swift
5+
dlopen.swift
56
FileSystem.swift
67
LineTable.swift
78
Random.swift
89
Result.swift
9-
dlopen.swift)
10+
Task+ValuePropagatingCancellation.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
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
}

Sources/SKSupport/ThreadSafeBox.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class Foo {
2+
func slow(x: Invalid1, y: Invalid2) {
3+
x / y / x / y / x / y / x / y . /*slowLoc*/
4+
}
5+
6+
struct Foo {
7+
let fooMember: String
8+
}
9+
10+
func fast(a: Foo) {
11+
a . /*fastLoc*/
12+
}
13+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"sources": ["main.swift"]}

Sources/SourceKitD/SourceKitD.swift

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,31 +84,34 @@ extension SourceKitD {
8484
public func send(_ req: SKDRequestDictionary) async throws -> SKDResponseDictionary {
8585
logRequest(req)
8686

87-
return try await withCheckedThrowingContinuation { continuation in
88-
var handle: sourcekitd_request_handle_t? = nil
87+
let handleWrapper = ThreadSafeBox<sourcekitd_request_handle_t?>(initialValue: nil)
8988

90-
api.send_request(req.dict, &handle) { [weak self] _resp in
91-
guard let self = self else { return }
89+
return try await withTaskCancellationHandler {
90+
try Task.checkCancellation()
91+
return try await withCheckedThrowingContinuation { continuation in
92+
var handle: sourcekitd_request_handle_t?
93+
api.send_request(req.dict, &handle) { [weak self] _resp in
94+
guard let self = self else { return }
9295

93-
let resp = SKDResponse(_resp, sourcekitd: self)
96+
let resp = SKDResponse(_resp, sourcekitd: self)
9497

95-
logResponse(resp)
98+
logResponse(resp)
9699

97-
guard let dict = resp.value else {
98-
continuation.resume(throwing: resp.error!)
99-
return
100-
}
100+
guard let dict = resp.value else {
101+
continuation.resume(throwing: resp.error!)
102+
return
103+
}
101104

102-
continuation.resume(returning: dict)
105+
continuation.resume(returning: dict)
106+
}
107+
handleWrapper.value = handle
108+
}
109+
} onCancel: {
110+
if let handle = handleWrapper.value {
111+
api.cancel_request(handle)
103112
}
104-
105-
// FIXME: (async) Cancellation
106113
}
107114
}
108-
109-
public func cancel(_ handle: sourcekitd_request_handle_t) {
110-
api.cancel_request(handle)
111-
}
112115
}
113116

114117
private func logRequest(_ request: SKDRequestDictionary) {

Sources/SourceKitLSP/Clang/ClangLanguageServer.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -312,17 +312,7 @@ actor ClangLanguageServerShim: ToolchainLanguageServer, MessageHandler {
312312
///
313313
/// The response of the request is returned asynchronously as the return value.
314314
func forwardRequestToClangd<R: RequestType>(_ request: R) async throws -> R.Response {
315-
try await withCheckedThrowingContinuation { continuation in
316-
_ = clangd.send(request) { result in
317-
switch result {
318-
case .success(let response):
319-
continuation.resume(returning: response)
320-
case .failure(let error):
321-
continuation.resume(throwing: error)
322-
}
323-
}
324-
}
325-
// FIXME: (async) Cancellation
315+
return try await clangd.send(request)
326316
}
327317

328318
func _crash() {

Sources/SourceKitLSP/SourceKitServer.swift

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ public actor SourceKitServer {
144144
/// The actual semantic handling of the message happens off this queue.
145145
private let messageHandlingQueue = AsyncQueue(.concurrent)
146146

147+
/// The queue on which we start and stop keeping track of cancellation.
148+
///
149+
/// Having a queue for this ensures that we started keeping track of a
150+
/// request's task before handling any cancellation request for it.
151+
private let cancellationMessageHandlingQueue = AsyncQueue(.serial)
152+
147153
/// The connection to the editor.
148154
public let client: Connection
149155

@@ -184,6 +190,17 @@ public actor SourceKitServer {
184190
}
185191
}
186192

193+
/// The requests that we are currently handling.
194+
///
195+
/// Used to cancel the tasks if the client requests cancellation.
196+
private var inProgressRequests: [RequestID: Task<(), Never>] = [:]
197+
198+
/// - Note: Needed so we can set an in-progress request from a different
199+
/// isolation context.
200+
private func setInProgressRequest(for id: RequestID, task: Task<(), Never>?) {
201+
self.inProgressRequests[id] = task
202+
}
203+
187204
let fs: FileSystem
188205

189206
var onExit: () -> Void
@@ -275,12 +292,7 @@ public actor SourceKitServer {
275292

276293
/// Send the given request to the editor.
277294
public func sendRequestToClient<R: RequestType>(_ request: R) async throws -> R.Response {
278-
try await withCheckedThrowingContinuation { continuation in
279-
_ = client.send(request) { result in
280-
continuation.resume(with: result)
281-
}
282-
// FIXME: (async) Handle cancellation
283-
}
295+
return try await client.send(request)
284296
}
285297

286298
func toolchain(for uri: DocumentURI, _ language: Language) -> Toolchain? {
@@ -450,28 +462,32 @@ public actor SourceKitServer {
450462

451463
extension SourceKitServer: MessageHandler {
452464
public nonisolated func handle(_ params: some NotificationType, from clientID: ObjectIdentifier) {
453-
// All of the notifications sourcekit-lsp currently handles might modify the
465+
if let params = params as? CancelRequestNotification {
466+
// Request cancellation needs to be able to overtake any other message we
467+
// are currently handling. Ordering is not important here. We thus don't
468+
// need to execute it on `messageHandlingQueue`.
469+
self.cancelRequest(params)
470+
}
471+
472+
// All of the other notifications sourcekit-lsp currently handles might modify the
454473
// global state (eg. whether a document is open or its contents) in a way
455474
// that changes the results of requsts before and after.
456475
// We thus need to ensure that we handle the notifications in order, so they
457476
// need to be dispatch barriers.
458477
//
459478
// Technically, we could optimize this further by having an `AsyncQueue` for
460479
// each file, because edits on one file should not block requests on another
461-
// file from executing but, at least in Swift, this would get us any real
480+
// file from executing but, at least in Swift, this would get us any real
462481
// benefits at the moment because sourcekitd only has a single, global queue,
463482
// instead of a queue per file.
464483
// Additionally, usually you are editing one file in a source editor, which
465484
// means that concurrent requests to multiple files tend to be rare.
466485
messageHandlingQueue.async(barrier: true) {
467-
let notification = Notification(params, clientID: clientID)
468-
await self._logNotification(notification)
486+
await self._logNotification(Notification(params, clientID: clientID))
469487

470-
switch notification.params {
488+
switch params {
471489
case let notification as InitializedNotification:
472490
await self.clientInitialized(notification)
473-
case let notification as CancelRequestNotification:
474-
await self.cancelRequest(notification)
475491
case let notification as ExitNotification:
476492
await self.exit(notification)
477493
case let notification as DidOpenTextDocumentNotification:
@@ -503,7 +519,7 @@ extension SourceKitServer: MessageHandler {
503519
// completion session but it only makes sense for the client to request
504520
// more results for this completion session after it has received the
505521
// initial results.
506-
messageHandlingQueue.async(barrier: false) {
522+
let task = messageHandlingQueue.async(barrier: false) {
507523
let cancellationToken = CancellationToken()
508524

509525
let request = Request(params, id: id, clientID: clientID, cancellation: cancellationToken, reply: { [weak self] result in
@@ -581,6 +597,16 @@ extension SourceKitServer: MessageHandler {
581597
default:
582598
reply(.failure(ResponseError.methodNotFound(R.method)))
583599
}
600+
// We have handled the request and can't cancel it anymore.
601+
// Stop keeping track of it to free the memory.
602+
self.cancellationMessageHandlingQueue.async(priority: .background) {
603+
await self.setInProgressRequest(for: id, task: nil)
604+
}
605+
}
606+
// Start tracking the request with a high priority to minimize the chance
607+
// of cancellation
608+
cancellationMessageHandlingQueue.async(priority: .background) {
609+
await self.setInProgressRequest(for: id, task: task)
584610
}
585611
}
586612

@@ -910,8 +936,16 @@ extension SourceKitServer {
910936
// Nothing to do.
911937
}
912938

913-
func cancelRequest(_ notification: CancelRequestNotification) {
914-
// TODO: Implement cancellation
939+
nonisolated func cancelRequest(_ notification: CancelRequestNotification) {
940+
// Since the request is very cheap to execute and stops other requests
941+
// from performing more work, we execute it with a high priority.
942+
cancellationMessageHandlingQueue.async(priority: .high) {
943+
guard let task = await self.inProgressRequests[notification.id] else {
944+
log("Cannot cancel task because it hasn't been scheduled for execution yet", level: .error)
945+
return
946+
}
947+
task.cancel()
948+
}
915949
}
916950

917951
/// The server is about to exit, and the server should flush any buffered state.

Sources/SourceKitLSP/Swift/CodeCompletionSession.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ actor CodeCompletionSession {
8181
return try await self.updateImpl(filterText: filterText, position: position, in: snapshot, options: options)
8282
}
8383
}
84-
return try await task.value
84+
return try await task.valuePropagatingCancellation
8585
}
8686

8787
private func open(

Sources/SourceKitLSP/Swift/SourceKitD+ResponseError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension ResponseError {
1717
public init(_ value: SKDError) {
1818
switch value {
1919
case .requestCancelled:
20-
self = .serverCancelled
20+
self = .cancelled
2121
case .requestFailed(let desc):
2222
self = .unknown("sourcekitd request failed: \(desc)")
2323
case .requestInvalid(let desc):

0 commit comments

Comments
 (0)