Files
cleveragents-core/docs/reference/lsp_lifecycle_restart.md

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:

  1. health_check is not blocked while restart_server is in progress — uses a threading.Barrier to synchronise two threads at the exact moment the lock is released in Phase 2.

  2. restart_server does not hold the lock during client.initialize — inspects _lock state from within the mock initialize() side-effect using acquire(blocking=False).

  3. 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