Repository navigation
Conversation
…own (issue agentic-ai-java#216) registerTask checked the shutdown flag before acquiring the write lock, so a registration that passed the check could be interleaved after shutdown() completed and stranded inside a shut-down manager's activeTasks forever. The rejection is now re-checked inside the write lock, which is the same boundary shutdown() uses to stop and clear tasks and set the flag, making check-and-register atomic with shutdown. The rejection contract is documented on the ScheduledAgentManager interface. A package-private constructor injects a test scheduler for deterministic interleaving tests: a registration passing the pre-lock check while shutdown holds the write lock in the task stop callback must be rejected by the in-lock re-check and leave no stranded record. Concurrent stop/shutdown stays idempotent without residue.
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 #216 reports that
DefaultScheduledAgentManager.registerTaskcan strand a task inside a shut-down manager: the shutdown check sits outside the write lock, so a registration that passes it can be interleaved aftershutdown()has fully completed — the task lands inactiveTasksof a manager whose scheduler is closed, is never stopped, and leaves the active count non-zero forever. This PR makes check-and-register atomic with shutdown, documents the rejection contract on the interface, and adds deterministic interleaving regression tests.Root cause
registerTaskchecked the volatile shutdown flag before acquiring the write lock.shutdown()stops every active task, clearsactiveTasks, closes the scheduler, and sets the flag — all under the same write lock. A registrant passing the flag check can be preempted before locking; when it finally acquires the lock, it inserts into a manager that is already shut down. The pre-lock check is not synchronized with the flag write, so the two operations race.The fix
The same rejection is re-checked inside the write lock critical section (the pre-lock fast-fail check stays, so the uncontended path does not pay for the lock). Because
shutdown()sets the flag under that very lock, the in-lock re-check closes the interleaving window: any registration that wins the write lock after shutdown has started sees the flag and throwsIllegalStateExceptioninstead of inserting a stranded record. The lock type is deliberately unchanged —shutdown()callstask.stop()while holding the write lock andstop()re-enters the manager's write lock throughunregisterTask, which only works becauseReentrantReadWriteLockis reentrant.The rejection contract is now stated on the
ScheduledAgentManagerinterface: registration after shutdown is rejected, including a registration racing with an in-progress shutdown. A package-private constructor onDefaultScheduledAgentManagerlets tests inject a scheduler, since the eager singleton is not resurrectable and its real pool cannot drive deterministic interleaving.Testing
The new
ScheduledAgentManagerShutdownMutexTestreproduces the race deterministically, not by stress loops: shutdown holds the write lock inside a blockedSTOPPEDlistener callback while a registration thread passes the pre-lock check and blocks on the lock (the gap between check and lock is observed through a latch-gatedgetName()hook, whichregisterTaskcalls exactly there). Releasing the shutdown lets the manager finish; before the fix the registration then inserts a stranded record (red: count 1, no exception), after the fix the in-lock re-check rejects it (count 0,IllegalStateException). Two further cases pin the contract: a registration after a completed shutdown is rejected with no residue, and concurrent stop/shutdown threads remain idempotent with no leftover records. The scheduling package is 17/17 green (including the #214 ownership regressions), checkstyle and spring-javaformat are clean. The module has 8 pre-existing failures on this baseline (skills prompt template, file store Windows path, prompt template line endings) that this PR does not touch.Fixes #216