Skip to content

Skip SourceKit requests once sourcekitd is known unavailable#6762

Open
Brett-Best wants to merge 1 commit into
realm:adopt-swift-testingfrom
Brett-Best:fix/sourcekit-unavailable-hang-latch
Open

Skip SourceKit requests once sourcekitd is known unavailable#6762
Brett-Best wants to merge 1 commit into
realm:adopt-swift-testingfrom
Brett-Best:fix/sourcekit-unavailable-hang-latch

Conversation

@Brett-Best

Copy link
Copy Markdown
Contributor

Targets the adopt-swift-testing branch (#6048).

Problem

On adopt-swift-testing the macOS CI test jobs hang and are cancelled at the 10‑minute timeout (Linux/Windows pass). In the cancelled run the whole suite prints started in one burst, then sourcekitd has failed at ~T+2s, then zero tests complete for 8+ minutes.

Root cause: Swift Testing runs every test as a concurrent task on a bounded cooperative executor (XCTest used its own per-test threads). Each linted file issues a synchronous, blocking sourcekitd request. When sourcekitd is unavailable, each request fails fast (~2s) but there is no global short‑circuit, so all ~1000 files independently re‑issue their own doomed request. Across the few cooperative threads that is >10 minutes of nothing-but-failing-requests, which starves the executor so no other test can make progress.

Fix

Add a process‑global SourceKitStatus latch, enforced centrally in Request.sendIfNotDisabled() (so it covers editorOpen, index, and cursorInfo uniformly):

  • Once a request fails and sourcekitd has never produced a successful response in this process, isUnavailable latches true and subsequent requests throw SourceKitUnavailableError immediately instead of blocking. The cascade collapses to a single failed request.
  • The "never succeeded" guard is what keeps this safe for normal linting. SwiftLint deliberately handles per‑file sourcekitd crashes (SwiftLintFile.sourcekitdFailed / assertHandler); such a crash happens after sourcekitd has already answered other files, so everSucceeded is already set and the global latch never engages. On a healthy machine the first request succeeds, so the latch is inert — no behaviour change.

Also routes UnusedDeclarationRule's index request through sendIfNotDisabled() (it previously called send() directly, bypassing the latch).

Testing

  • New deterministic regression test (SourceKitCrashTests.sourceKitUnavailableShortCircuitsWithoutIssuingRequest): forces the unavailable state for one task tree (isolated from parallel tests via a @TaskLocal) and asserts the central choke point throws without issuing a request. It fails before the fix and passes after.
  • Full local suite green (1067 tests / 370 suites), swiftlint lint --strict clean.

Notes / caveats

  • The latch only auto‑engages when sourcekitd never came up (the CI failure mode). I could not reproduce that exact mode locally — under a CPU‑throttled container sourcekitd instead works but is glacially slow, a different bottleneck this change doesn't target — so the auto‑latch trigger is validated by the regression test and the macOS CI log rather than an end‑to‑end local repro. macOS CI on this PR is the real confirmation.
  • One edge: if the very first sourcekitd request (the SwiftVersion startup probe) fails transiently on an otherwise‑healthy machine, the latch trips for the run. In practice this is rare, and it is actually beneficial on CI (trips early, before file processing).

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 6, 2026 21:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a process-wide “SourceKit unavailable” latch to avoid Swift Testing hangs when sourcekitd is down, and updates a rule path and tests to exercise the new short-circuit behavior.

Changes:

  • Introduces SourceKitStatus + SourceKitUnavailableError to short-circuit SourceKit requests once sourcekitd is deemed unavailable.
  • Wraps Request.sendIfNotDisabled() to record success/failure and skip future requests when latched unavailable.
  • Adds a regression test and updates UnusedDeclarationRule indexing to use sendIfNotDisabled().

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
Tests/FileSystemAccessTests/SourceKitCrashTests.swift Adds a regression test ensuring SourceKit requests fail fast when forced unavailable.
Source/SwiftLintCore/Extensions/Request+SwiftLint.swift Adds SourceKitStatus latch + short-circuit error to prevent executor starvation/hangs.
Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift Routes indexing request through sendIfNotDisabled() so it participates in the new behavior.

Comment on lines +116 to +126
guard !SourceKitStatus.isUnavailable else {
throw SourceKitUnavailableError()
}
do {
let response = try send()
SourceKitStatus.markSucceeded()
return response
} catch {
SourceKitStatus.markFailed()
throw error
}
Comment on lines +52 to +58
/// Records a failed sourcekitd request. This only latches sourcekitd as globally unavailable if
/// no request has ever succeeded — i.e. sourcekitd never came up, rather than a one-off crash.
public static func markFailed() {
lock.lock()
defer { lock.unlock() }
everFailed = true
}
Comment on lines +23 to +24
nonisolated(unsafe) private static var everSucceeded = false
nonisolated(unsafe) private static var everFailed = false
@SwiftLintBot

SwiftLintBot commented Jun 6, 2026

Copy link
Copy Markdown
1 Warning
⚠️ If this is a user-facing change, please include a CHANGELOG entry to credit yourself!
You can find it at CHANGELOG.md.
19 Messages
📖 Building this branch resulted in a binary size of 27786.52 KiB vs 27536.88 KiB when built on main (0% larger).
📖 Linting Aerial with this PR took 0.66 s vs 0.65 s on main (1% slower).
📖 Linting Alamofire with this PR took 0.96 s vs 0.93 s on main (3% slower).
📖 Linting Brave with this PR took 6.13 s vs 6.08 s on main (0% slower).
📖 Linting DuckDuckGo with this PR took 25.1 s vs 24.95 s on main (0% slower).
📖 Linting Firefox with this PR took 10.42 s vs 10.41 s on main (0% slower).
📖 Linting Kickstarter with this PR took 7.47 s vs 7.57 s on main (1% faster).
📖 Linting Moya with this PR took 0.38 s vs 0.36 s on main (5% slower).
📖 Linting NetNewsWire with this PR took 2.34 s vs 2.35 s on main (0% faster).
📖 Linting Nimble with this PR took 0.57 s vs 0.58 s on main (1% faster).
📖 Linting PocketCasts with this PR took 7.12 s vs 7.18 s on main (0% faster).
📖 Linting Quick with this PR took 0.35 s vs 0.35 s on main (0% slower).
📖 Linting Realm with this PR took 2.52 s vs 2.55 s on main (1% faster).
📖 Linting Sourcery with this PR took 1.54 s vs 1.58 s on main (2% faster).
📖 Linting Swift with this PR took 4.23 s vs 4.23 s on main (0% slower).
📖 Linting SwiftLintPerformanceTests with this PR took 0.17 s vs 0.16 s on main (6% slower).
📖 Linting VLC with this PR took 1.04 s vs 1.04 s on main (0% slower).
📖 Linting Wire with this PR took 15.19 s vs 15.16 s on main (0% slower).
📖 Linting WordPress with this PR took 10.54 s vs 10.53 s on main (0% slower).

Here's an example of your CHANGELOG entry:

* Skip SourceKit requests once sourcekitd is known unavailable.  
  [Brett-Best](https://github.com/Brett-Best)
  [#issue_number](https://github.com/realm/SwiftLint/issues/issue_number)

note: There are two invisible spaces after the entry's text.

Generated by 🚫 Danger

…ging

Under Swift Testing the suite runs as many concurrent tasks on a small
cooperative executor. Each linted file issues a synchronous, blocking sourcekitd
request; if the daemon wedges — as it does on the macOS CI runners — every
concurrent caller blocks on it and the whole run hangs until the job times out,
with zero tests completing.

Run each request through `Request.sendIfNotDisabled()` with a 30s timeout: a
wedged request now frees its cooperative-executor thread instead of blocking
forever, and the first time a request times out `SourceKitStatus` latches so the
remaining requests skip sourcekitd rather than each paying the timeout. A healthy
daemon answers immediately, so the latch never trips and behaviour is unchanged.

`UnusedDeclarationRule`'s index request previously called `send()` directly,
bypassing the timeout; route it through `sendIfNotDisabled()` like the others.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Brett-Best Brett-Best force-pushed the fix/sourcekit-unavailable-hang-latch branch from 54e0f30 to 9610daa Compare June 7, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants