Skip to content

Fix bindSession ConflictException crashing control-plane#21

Merged
ajit-zer07 merged 2 commits intomainfrom
subscribe-session
Apr 22, 2026
Merged

Fix bindSession ConflictException crashing control-plane#21
ajit-zer07 merged 2 commits intomainfrom
subscribe-session

Conversation

@ajit-zer07
Copy link
Copy Markdown
Contributor

Summary

  • Harden RunManagerService.bindSession against races where the run has
    already advanced past binding_session (e.g., SessionDiscoveryService
    and RunExecutorService both processing the same session). The method
    now catches ConflictException from transitionTo, logs a warning, and
    returns the current run — avoiding duplicate event emission and session
    upserts.
  • Stop SessionDiscoveryService.handleSessionCreated from firing three
    unawaited void state transitions. They are now awaited sequentially
    inside a try/catch, so a transition failure on one discovered session
    no longer kills the WatchSessions loop for every other session.

Why

Previously, void this.runManager.bindSession(...) in
session-discovery.service.ts:98 would surface any ConflictException
from the state-machine guard (run.repository.ts:102) as an unhandled
promise rejection — crashing the control-plane. This was observed from
the UIConsole as a control-plane crash during session binding.

Root cause:

  1. transitionTo throws ConflictException when the current status is
    not a valid predecessor for the target (e.g., run already running).
  2. Three void calls (markStarted, bindSession, markRunning) run
    concurrently and out of order relative to the state machine.
  3. Unhandled rejection → process exit.

Changes

  • src/runs/run-manager.service.ts
    • Import ConflictException from @nestjs/common.
    • In bindSession, wrap markBindingSession in try/catch. On
      ConflictException, fetch the current run, log a warning with the
      current status, and return it. Throws NotFoundException only if the
      run genuinely disappeared.
  • src/runs/session-discovery.service.ts
    • Replace the three void state transitions with sequential awaits
      inside a try/catch.
    • Errors are logged but swallowed so subsequent subscribeSession +
      streamConsumer.start still execute — observation is the whole
      point of session discovery — and the outer WatchSessions loop
      stays alive for other sessions.

Test plan

  • npx tsc --noEmit -p tsconfig.json — clean.
  • npx jest src/runs/run-manager.service.spec.ts — 19 / 19 pass.
  • npx jest src/runs/session-discovery.service.spec.ts — 9 / 9 pass.
  • npx jest src/runs/run-executor.service.spec.ts — 24 / 24 pass.
  • npx jest (full suite) — 593 / 593 pass across 46 suites.
  • Manual: run control-plane against runtime, trigger a session twice
    (RunExecutor + SessionDiscovery on the same sessionId), confirm no
    crash and a single session.bound event is emitted.

Out of scope / follow-ups

  • markStarted and markRunning still throw ConflictException on
    racing callers. They are now caught by the new try/catch in
    session-discovery.service.ts, but could be made idempotent in
    RunManagerService for consistency with markCompleted /
    markFailed / markCancelled.
  • void this.streamConsumer.start(...) in handleSessionCreated is
    still unawaited; it's long-running by design, but a .catch handler
    would make rejections observable rather than unhandled.

  RunManagerService.bindSession let ConflictException from transitionTo
  propagate unchanged. SessionDiscoveryService fires bindSession (and its
  sibling transitions markStarted / markRunning) as unawaited `void` calls,
  so any race — e.g., RunExecutor also advancing the run, or a re-emitted
  session.created — surfaced as an unhandled promise rejection and killed
  the Node process.

  - run-manager.service.ts: catch ConflictException in bindSession, log,
    and return the current run. Side-effects (session upsert, event emit)
    already fired on the first successful bind, so they are skipped.
  - session-discovery.service.ts: sequence markStarted / bindSession /
    markRunning with await inside a try/catch. Any remaining conflict is
    logged; subscribeSession + streamConsumer.start still run so the
    WatchSessions loop keeps observing subsequent sessions.
@ajit-zer07 ajit-zer07 merged commit a36f48c into main Apr 22, 2026
7 of 8 checks passed
@ajit-zer07 ajit-zer07 deleted the subscribe-session branch April 22, 2026 21:39
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.

1 participant