Repository navigation
Conversation
…pass The START handler held the global LOCKID_SOKOVAN_TARGET_CREATING lock across create_kernels, so a slow kernel creation made every later START pass wait, and a lost lock let a second pass start the same session again. Each pass now claims its sessions in one transaction (PREPARED sessions and their PREPARED kernels to CREATING) and launches only what it claimed; the handler takes no lock. START's success transition leaves a session that moved on during the pass (promoted to RUNNING, terminated) alone. A pass that fails before the launch moves the sessions back to PREPARED.
…ved on during START START's success transition now skips a session promoted to RUNNING or terminated while the pass waited for create_kernels. update_with_history returns the ids it moved and records history only for them, and the coordinator broadcasts only those.
…that pass START's expired and give_up transitions now move a session only while it is still PREPARED; a session another pass claimed again after its release is CREATING and is skipped. The coordinator resets kernels to PENDING only for the sessions a transition moved, so a skipped session's kernels are not reset and their agents not cleared while it is being created, and it logs the sessions a guarded transition left in their status.
hhoikoo
force-pushed
the
fix/start-sessions-claim
branch
from
October 22, 2026 09:09
719ce6f to
8a7cd73
Compare
This was referenced Oct 22, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
resolves #NNN (BA-XXXX)
BA-XXXXand#NNNare placeholders until the ticket exists.The sokovan START handler no longer holds the global
LOCKID_SOKOVAN_TARGET_CREATINGlock acrosscreate_kernels. Each START pass claims its PREPARED sessions in one transaction before the RPC and launches only the sessions it claimed, so two passes cannot start the same session and a pass does not wait for another pass's kernel creation.Problem
StartSessionsLifecycleHandler.lock_idreturnsLOCKID_SOKOVAN_TARGET_CREATING.coordinator._process_lifecycle_handler_scheduletakes it once per pass and holds it whileasyncio.gatherruns every resource group'shandler.execute(), which awaitscreate_kernelson the agents (START_SESSION_TIMEOUT_SECper session).execute()returns;update_kernel_status_preparing/_creatingdo not match a PREPARED kernel).create_kernelsin flight returns. With Docker kernels this is seconds. Where the create RPC is slow (a large image pulled inside the create, a VM-based runtime), it is minutes, and waiters hitlock_timeoutand retry every cycle.create_kernelsa second time for the same PREPARED session. A network blip or a PostgreSQL failover can lose the lock the same way.Change
ScheduleDBSource.claim_sessions_for_start(session_ids, reason)(repositories/scheduler/db_source/db_source.py): one READ COMMITTED transaction moves the sessions that are still PREPARED to CREATING (UPDATE sessions ... WHERE status = PREPARED RETURNING id) and their PREPARED kernels to CREATING. The conditional session update is the claim: a second pass blocks on the session row until the first commits, rechecks it, finds it CREATING and leaves it out. Kernels move only for the winner, in the same transaction, so a cluster session is claimed all or nothing. The kernel UPDATE is shared with the existingupdate_kernels_to_creating_for_sessionsthrough a private_update_kernels_to_creating.ScheduleDBSource.release_sessions_for_start(session_ids): moves sessions that are still CREATING, and their CREATING kernels, back to PREPARED. A session that moved on (for example, terminated) keeps its status.StartSessionsLifecycleHandler.execute: claims first, drops the sessions another pass claimed (not reported, so the coordinator records nothing for them in this pass), queries and launches only the claimed ones, and reports them as successes as before. If the session query fails or the pass is cancelled before the launch, it releases the claim and re-raises.lock_idreturnsNone.LOCKID_SOKOVAN_TARGET_CREATINGstays indefs.pyunused, like the other retired lock ids there.TransitionStatusgainsexcept_session_statuses(default empty, so every other transition is unchanged), passed toSessionStatusBatchUpdater.except_statuses. START's success transition usesfrozenset(SessionStatus) - {CREATING}; its expired and give_up transitions usefrozenset(SessionStatus) - {PREPARED}: a failed session has had its claim released to PREPARED, and if another pass claimed it again meanwhile it is CREATING and belongs to that pass._apply_transitionresets kernels to PENDING only for the sessions the update moved, so a skipped session's kernels are not reset and their agents not cleared while it is being created; the sessions a guarded transition left in their status are logged at info. Reason: the session is CREATING during the RPC now, sopromote-to-running(CHECK_CREATING_PROGRESS) can promote it to RUNNING as soon as its kernels report started, while the pass still waits for other sessions. The unguarded write would put it back to CREATING; a session terminated during the RPC would also be written back (that part already happened on main from PREPARED).update_with_historyreturns the IDs of the sessions it moved and records history only for those (update, then history, in onewrite_opstransaction, asmark_sessions_statusdoes)._apply_transitionreturns the moved session infos, and_handle_resultbroadcasts the success event only for them, so a session promoted during the pass gets no late CREATING event (the event is cached per session and re-sent to new subscribers) and no CREATING history row after its RUNNING row.SchedulerRepositorygains the two pass-through methods; the scheduler README's start phase describes the claim.Provisional (design choices of this PR, not decided elsewhere)
promote-to-runningpicks it up once the kernels start.execute().Not changed / known gaps
start_sessions_for_handleritself raises or is cancelled after the claim (for example at shutdown): the claim is released only for a failure before the launch. There is no CREATING sweeper.status_dataand the session stays CREATING as before.release_sessions_for_start; thestatus_transitionsdocstring says so). Main reports none; a hook that rejects a kernel before the RPC would add them.wait_foraround the RPC, not a sweeper.create_kernelsbatch in flight at once; the remaining bounds are the agents' creation limits and the agent client pool. No other handler used this lock.CHECK_CREATING_PROGRESSmark).Test
tests/unit/manager/repositories/scheduler/test_start_claim.py(real PostgreSQL):update_with_history: a claimed CREATING session gets its history row; a session moved to RUNNING or TERMINATING keeps its status,[]is returned, nostart-sessionshistory row.update_with_history: a session still PREPARED moves (PENDING / TERMINATING) with one history row; a session claimed again (CREATING) is not updated and gets no history row.test_lifecycle_handlers.pySC-ST-006: two passes racing for one PREPARED session, the launcher is awaited once. SC-ST-007 (error andCancelledError): a pass that fails before the launch releases the claim and does not launch.test_coordinator.pySC-CO-019d: START's expired and give_up transitions exclude CREATING but not PREPARED; a skipped session is not returned and its kernels are not reset; a moved one is (kernel reset on expired only). SC-CO-019e: a guarded PENDING transition resets the kernels of the moved session only. SC-CO-026: a START pass whose launcher is blocked on an event for session A, then a second START pass for session B with a process-local lock factory; the second pass finishes within 1 s while the first is still blocked. SC-CO-019c: START's success updater excludes every status but CREATING, and a skipped session is not returned for the broadcast.assert <SessionStatus.CREATING> in frozenset(), SC-CO-019eexpected await not found(reset called with both sessions), DB failure testsassert [SessionID(...)] == [](the CREATING session was moved). After: pass.Expected start_sessions_for_handler to have been awaited once. Awaited 2 times.; SC-ST-007Expected release_sessions_for_start to have been awaited once. Awaited 0 times.; SC-CO-026TimeoutError(the second pass waits on the lock); SC-CO-019c fails on the updater's emptyexcept_statuses; the DB tests fail withAttributeError(no claim method). After: all pass.pants test tests/unit/manager/sokovan:: tests/unit/manager/repositories/scheduler:: tests/unit/manager/repositories/scheduling_history::passes (108 targets);pants lint check --changed-since=origin/main --changed-dependents=directclean (ruff, visibility, mypy).Related
Checklist: (if applicable)
fix:PR goes to every version in.github/maintained-versions.ymlautomatically.A
Backport:line here names the targets instead — some of them, other ones, ornone.ai.backend.testdocsdirectory