Repository navigation
Conversation
…sitions (issue agentic-ai-java#215) start/stop composed their lifecycle from independent volatile started/stopped flags, so the transitions were not atomic: two concurrent starts could both pass the guard and publish two schedules, a stop racing with the schedule publication could miss the future published afterwards, a stop followed by start resurrected the stopped task, and a failed start left the registration behind. The lifecycle is now a single CAS-able state (NEW, STARTING, RUNNING, STOPPING, STOPPED, FAILED). start() claims NEW->STARTING up front, so a second start is rejected while the first is still publishing; a stop winning the STARTING window makes the late future cancelled instead of live; a scheduling failure releases the registration and lands in FAILED. stop() is idempotent through a single winner transition. The derived isStarted/isStopped keep their previous observable semantics, and all existing lifecycle/ownership tests pass unchanged. Deterministic interleaving tests gate the scheduler publication with latches: double start schedules once, start after stop is rejected, failed start unregisters, stop during start cancels the late future.
konlue
force-pushed
the
fix/issue215-scheduled-task-atomic-lifecycle
branch
from
November 15, 2026 03:39
00523e4 to
31be3f3
Compare
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.
Describe what this PR does / why we need it
Issue #215 reports that
ScheduledAgentTask.start/stopcompose their lifecycle from two independent volatile flags with no atomic transition. This PR replaces the flag pair with a single CAS-able lifecycle state and closes all four windows it exposes, with deterministic latch-driven regression tests.Root cause
Four concrete defects fall out of the flag pair. Two concurrent starts can both pass the
startedguard and each publish a schedule, leaving two live schedulings for one task. A stop racing with an in-flight start can finish before the future is published, so the late future is never cancelled and keeps triggering. A stop followed by a start succeeds:stoppedplays no role in the start guard, resurrecting a stopped task. And a start whose scheduling throws leaves the task registered forever, since the registration happens in the constructor but nothing releases it on failure.The fix
The task now holds one
AtomicReferenceover a package-privateScheduleState(NEW, STARTING, RUNNING, STOPPING, STOPPED, FAILED).start()claimsNEW → STARTINGup front, so a concurrent or repeated start is rejected while the first is still publishing the schedule. After the schedule is published,STARTING → RUNNINGmust win; ifstop()took the state during the window, the just-published future is cancelled instead of left live and the start fails. A scheduling failure moves to FAILED, releases the registration, and propagates the original exception, so a failed start strands nothing.stop()funnels through a single-winner transition to STOPPING — concurrent and repeated stops are no-ops — then cancels the future, unregisters, notifies, and lands on STOPPED. The stop semantics are unchanged:cancel(false)only, so in-flight executions are still not interrupted. No new listener events and no public API growth;isStarted/isStoppedkeep their previous observable semantics (started history is retained after stop).Testing
The new
ScheduledAgentTaskAtomicStartStopTestgates the scheduler publication with latches for deterministic interleaving: a first start blocked inside the publication window makes a concurrent second start fail with exactly one schedule published (red before: two schedules, no exception); a start after stop is rejected with no new schedule (red before: it scheduled again); a start whose scheduling throws unregisters and propagates (red before: registration left behind); a stop during the STARTING window cancels the late-published future and ends STOPPED (red before: the future stayed live and start succeeded); and the full state walk NEW → STARTING → RUNNING → STOPPING → STOPPED is asserted through a package-private observation point. The scheduling package is 19/19 green with the #214/#216 regressions untouched, checkstyle and spring-javaformat are clean. The module's 8 pre-existing failures on this baseline (skills prompt template, file store Windows path, prompt template line endings) are unrelated and unchanged.Fixes #215