Skip to content

Commit 9b6fb11

Browse files
committed
Do not block initialization of the build server when build server is unresponsive in returning the list of test files
We were blocking the initialization response on `self.buildSystemManager.testFiles`, which requires the list of test files to be determined. Make that operation asynchronous so that a slow build server can’t take down all of SourceKit-LSP.
1 parent b512488 commit 9b6fb11

File tree

4 files changed

+99
-18
lines changed

4 files changed

+99
-18
lines changed

Sources/SKTestSupport/TestSourceKitLSPClient.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,20 @@ package final class TestSourceKitLSPClient: MessageHandler, Sendable {
204204
}
205205
preInitialization?(self)
206206
if initialize {
207-
_ = try await self.send(
208-
InitializeRequest(
209-
processId: nil,
210-
rootPath: nil,
211-
rootURI: nil,
212-
initializationOptions: initializationOptions,
213-
capabilities: capabilities,
214-
trace: .off,
215-
workspaceFolders: workspaceFolders
207+
let capabilities = capabilities
208+
try await withTimeout(.seconds(defaultTimeout)) {
209+
_ = try await self.send(
210+
InitializeRequest(
211+
processId: nil,
212+
rootPath: nil,
213+
rootURI: nil,
214+
initializationOptions: initializationOptions,
215+
capabilities: capabilities,
216+
trace: .off,
217+
workspaceFolders: workspaceFolders
218+
)
216219
)
217-
)
220+
}
218221
}
219222
}
220223

Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,28 @@ import SwiftExtensions
1818

1919
/// Task metadata for `SyntacticTestIndexer.indexingQueue`
2020
fileprivate enum TaskMetadata: DependencyTracker, Equatable {
21-
case read
21+
/// Determine the list of test files from the build system and scan them for tests. Only created when the
22+
/// `SyntacticTestIndex` is created
23+
case initialPopulation
24+
25+
/// Index the files in the given set for tests
2226
case index(Set<DocumentURI>)
2327

28+
/// Retrieve information about syntactically discovered tests from the index.
29+
case read
30+
2431
/// Reads can be concurrent and files can be indexed concurrently. But we need to wait for all files to finish
2532
/// indexing before reading the index.
2633
func isDependency(of other: TaskMetadata) -> Bool {
2734
switch (self, other) {
35+
case (.initialPopulation, _):
36+
// The initial population need to finish before we can do anything with the task.
37+
return true
38+
case (_, .initialPopulation):
39+
// Should never happen because the initial population should only be scheduled once before any other operations
40+
// on the test index. But be conservative in case we do get an `initialPopulation` somewhere in between and use it
41+
// as a full blocker on the queue.
42+
return true
2843
case (.read, .read):
2944
// We allow concurrent reads
3045
return false
@@ -109,7 +124,23 @@ actor SyntacticTestIndex {
109124
/// indexing tasks to finish.
110125
private let indexingQueue = AsyncQueue<TaskMetadata>()
111126

112-
init() {}
127+
init(determineTestFiles: @Sendable @escaping () async -> [DocumentURI]) {
128+
indexingQueue.async(priority: .low, metadata: .initialPopulation) {
129+
let testFiles = await determineTestFiles()
130+
131+
// Divide the files into multiple batches. This is more efficient than spawning a new task for every file, mostly
132+
// because it keeps the number of pending items in `indexingQueue` low and adding a new task to `indexingQueue` is
133+
// in O(number of pending tasks), since we need to scan for dependency edges to add, which would make scanning files
134+
// be O(number of files).
135+
// Over-subscribe the processor count in case one batch finishes more quickly than another.
136+
let batches = testFiles.partition(intoNumberOfBatches: ProcessInfo.processInfo.processorCount * 4)
137+
await batches.concurrentForEach { filesInBatch in
138+
for uri in filesInBatch {
139+
await self.rescanFileAssumingOnQueue(uri)
140+
}
141+
}
142+
}
143+
}
113144

114145
private func removeFilesFromIndex(_ removedFiles: Set<DocumentURI>) {
115146
self.removedFiles.formUnion(removedFiles)

Sources/SourceKitLSP/Workspace.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
127127
}
128128

129129
/// The index that syntactically scans the workspace for tests.
130-
let syntacticTestIndex = SyntacticTestIndex()
130+
let syntacticTestIndex: SyntacticTestIndex
131131

132132
/// Language service for an open document, if available.
133133
private let documentService: ThreadSafeBox<[DocumentURI: LanguageService]> = ThreadSafeBox(initialValue: [:])
@@ -180,13 +180,15 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
180180
} else {
181181
self.semanticIndexManager = nil
182182
}
183+
// Trigger an initial population of `syntacticTestIndex`.
184+
self.syntacticTestIndex = SyntacticTestIndex(determineTestFiles: {
185+
await orLog("Getting list of test files for initial syntactic index population") {
186+
try await buildSystemManager.testFiles()
187+
} ?? []
188+
})
183189
await indexDelegate?.addMainFileChangedCallback { [weak self] in
184190
await self?.buildSystemManager.mainFilesChanged()
185191
}
186-
// Trigger an initial population of `syntacticTestIndex`.
187-
if let testFiles = await orLog("Getting initial test files", { try await self.buildSystemManager.testFiles() }) {
188-
await syntacticTestIndex.listOfTestFilesDidChange(testFiles)
189-
}
190192
if let semanticIndexManager {
191193
await semanticIndexManager.scheduleBuildGraphGenerationAndBackgroundIndexAllFiles(
192194
filesToIndex: nil,
@@ -223,7 +225,7 @@ package final class Workspace: Sendable, BuildSystemManagerDelegate {
223225
// `SourceKitLSPServer` has been destructed. We are tearing down the
224226
// language server. Nothing left to do.
225227
logger.error(
226-
"Ignoring notificaiton \(type(of: notification).method) because connection to editor has been closed"
228+
"Ignoring notification \(type(of: notification).method) because connection to editor has been closed"
227229
)
228230
return
229231
}

Tests/BuildSystemIntegrationTests/BuildServerBuildSystemTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import Foundation
1616
import LanguageServerProtocol
1717
import LanguageServerProtocolExtensions
1818
import SKTestSupport
19+
import SourceKitLSP
20+
import SwiftExtensions
1921
import TSCBasic
2022
import XCTest
2123

@@ -369,4 +371,47 @@ final class BuildServerBuildSystemTests: XCTestCase {
369371
return diags.fullReport?.items.map(\.message) == ["DEBUG SET"]
370372
}
371373
}
374+
375+
func testDontBlockBuildServerInitializationIfBuildSystemIsUnresponsive() async throws {
376+
// A build server that responds to the initialize request but not to any other requests.
377+
final class UnresponsiveBuildServer: MessageHandler {
378+
func handle(_ notification: some LanguageServerProtocol.NotificationType) {}
379+
380+
func handle<Request: RequestType>(
381+
_ request: Request,
382+
id: RequestID,
383+
reply: @escaping @Sendable (LSPResult<Request.Response>) -> Void
384+
) {
385+
switch request {
386+
case is InitializeBuildRequest:
387+
reply(
388+
.success(
389+
InitializeBuildResponse(
390+
displayName: "UnresponsiveBuildServer",
391+
version: "",
392+
bspVersion: "2.2.0",
393+
capabilities: BuildServerCapabilities()
394+
) as! Request.Response
395+
)
396+
)
397+
default:
398+
sleep(UInt32(defaultTimeout * 10))
399+
break
400+
}
401+
}
402+
}
403+
404+
// Creating the `MultiFileTestProject` waits for the initialize response and times out if it doesn't receive one.
405+
// Make sure that we get that response back.
406+
_ = try await MultiFileTestProject(
407+
files: ["Test.swift": ""],
408+
hooks: Hooks(
409+
buildSystemHooks: BuildSystemHooks(injectBuildServer: { _, _ in
410+
let connection = LocalConnection(receiverName: "Unresponsive build system")
411+
connection.start(handler: UnresponsiveBuildServer())
412+
return connection
413+
})
414+
)
415+
)
416+
}
372417
}

0 commit comments

Comments
 (0)