Skip to content

Linux: aps watch never honors --timeout, polling stalls, and swift test intermittently deadlocks (RunLoop timeout never fires) #97

Description

@0xLeif

Found during an adversarial dogfooding pass on main @ 6a40258 (v1.0.0+).

Environment

  • Debian 12 (glibc 2.36), kernel 6.6.69-cube, x86_64
  • Stock swift.org toolchains: reproduced on both 6.0.3 (ubuntu22.04 build) and 6.1.2 (debian12 build)
  • Not reproducible on ubuntu-latest CI (linux-smoke is green on main), so this may be distro/kernel specific. Filing because it fully blocks fledge lanes run verify for a Linux contributor in this environment.

Symptoms

  1. aps watch counter --timeout 1 --jsonl never stops; it has to be killed. Expected: exit 124 after ~1s (Scripts/smoke.sh line 149 asserts exactly this, so smoke hangs here).
  2. aps watch note --jsonl prints the initial value, then never polls: external writes to note.json go unreported until a signal arrives. Sending SIGINT wakes the loop, flushes one late event (intermediate values missed), then exits 130.
  3. swift test intermittently deadlocks mid-suite (about 2 of 5 runs here, at different tests each time, including a synchronous test's async setUp). lldb on the hung bundle shows the main thread parked in XCTWaiter.wait -> RunLoop.run -> ppoll with only the dispatch manager thread alive; the async test's MainActor hop (e.g. await MainActor.run in setUp) never resumes.

Minimal repro (no aps code)

import Foundation
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05))
print("returned") // never prints in this environment

Control experiment: DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) DOES fire while the main thread sits inside RunLoop.run(until:), so main-queue draining works. Only the CFRunLoop limit-date timeout (and CFRunLoopTimer) wakeups never arrive.

Code reading (swift-corelibs-foundation 6.0.3, Sources/CoreFoundation/CFRunLoop.c): on Linux the wait is __CFRunLoopServiceFileDescriptors(waitSet, CFPORT_NULL, poll ? 0 : TIMEOUT_INFINITY, ...), and the limit-date timeout is armed as a dispatch timer whose handler calls CFRunLoopWakeUp. That wakeup never lands here, so the epoll wait sleeps forever.

Impact on aps

  • Scripts/smoke.sh hangs at the --timeout 1 assertion, so fledge lanes run verify cannot complete (verified: verify dies at the test step here, and smoke independently at the watch step).
  • watchBlocking / watchStatsBlocking drive their polling cadence with RunLoop.current.run(until:) slices (Sources/aps/StateStore.swift), so all time-based watch behavior is dead on this platform: no poll interval, no --timeout, signal handling only works because signal delivery itself wakes the loop.
  • Async XCTest cases deadlock whenever the waiter run loop stops waking (same plumbing).

Possible directions

  • Replace the polling slices with Thread.sleep + a zero-timeout run-loop drain, or add a watchdog path for --timeout/signals that does not depend on CFRunLoop timeouts.
  • Consider making the Linux test harness less dependent on async MainActor hops (the CLI's synchronous MainActor.assumeIsolated pattern has been reliable here).
  • Track upstream: this looks like a corelibs-foundation/libdispatch platform issue rather than aps logic, but aps currently has no workaround.

Happy to run further diagnostics (lldb backtraces and repro scripts preserved).

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:codexTicket claimed by CodexbugSomething isn't workingdogfoodExercises an AppState feature surface

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions