2.4 KiB
LSP Lifecycle Manager — restart_server() Lock Pattern
Overview
LspLifecycleManager.restart_server() uses a 3-phase lock pattern to
prevent deadlocks under concurrent access. This matches the pattern already
used by start_server().
The Problem (pre-fix)
Prior to PR #3165, restart_server() held self._lock across all blocking
I/O operations:
acquire lock
transport.stop() ← blocking I/O (up to 60s)
transport.start() ← blocking I/O
client.initialize() ← blocking I/O (up to 60s)
release lock
Any concurrent caller of health_check(), list_running(), or stop_server()
would block for up to 60 seconds waiting for the lock.
The Fix — 3-Phase Lock Pattern
Phase 1 (lock held — short):
Read current state, snapshot fields, remove old entry from _servers
Phase 2 (lock RELEASED — blocking I/O):
old_transport.stop()
StdioTransport(...).start()
client.initialize()
Phase 3 (lock held — short):
Insert new _ManagedServer into _servers, preserving ref_count
Removing the old entry in Phase 1 ensures concurrent callers see the server
as absent during the restart window (consistent with the start_server
contract). The ref_count from the original managed server is carried
forward to the new one.
Concurrency Guarantees
| Concurrent operation | Behaviour during restart |
|---|---|
health_check() |
Returns immediately (server absent in Phase 2) |
list_running() |
Returns immediately (server absent in Phase 2) |
stop_server() |
Returns immediately (server absent in Phase 2) |
start_server() |
Proceeds normally (no lock contention in Phase 2) |
BDD Coverage
Three scenarios in features/lsp_lifecycle_coverage.feature verify the fix:
-
health_check is not blocked while restart_server is in progress— uses athreading.Barrierto synchronise two threads at the exact moment the lock is released in Phase 2. -
restart_server does not hold the lock during client.initialize— inspects_lockstate from within the mockinitialize()side-effect usingacquire(blocking=False). -
restart_server preserves the ref_count of the managed server— starts the server twice (ref_count=2), restarts it, asserts ref_count=2.
References
- PR #3165 — fix implementation
- Issue #3026 — original bug report
src/cleveragents/lsp/lifecycle.py— implementation