Skip to content

fix(scheduling): make task registration mutually exclusive with shutdown (issue #216) - #369

Open
konlue wants to merge 1 commit into
agentic-ai-java:mainfrom
konlue:fix/issue216-scheduler-register-shutdown-mutex
Open

konlue wants to merge 1 commit into
agentic-ai-java:mainfrom
konlue:fix/issue216-scheduler-register-shutdown-mutex

Conversation

@konlue

@konlue konlue commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Describe what this PR does / why we need it

Issue #216 reports that DefaultScheduledAgentManager.registerTask can 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 after shutdown() has fully completed — the task lands in activeTasks of 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

registerTask checked the volatile shutdown flag before acquiring the write lock. shutdown() stops every active task, clears activeTasks, 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 throws IllegalStateException instead of inserting a stranded record. The lock type is deliberately unchanged — shutdown() calls task.stop() while holding the write lock and stop() re-enters the manager's write lock through unregisterTask, which only works because ReentrantReadWriteLock is reentrant.

The rejection contract is now stated on the ScheduledAgentManager interface: registration after shutdown is rejected, including a registration racing with an in-progress shutdown. A package-private constructor on DefaultScheduledAgentManager lets tests inject a scheduler, since the eager singleton is not resurrectable and its real pool cannot drive deterministic interleaving.

Testing

The new ScheduledAgentManagerShutdownMutexTest reproduces the race deterministically, not by stress loops: shutdown holds the write lock inside a blocked STOPPED listener 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-gated getName() hook, which registerTask calls 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

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

[BUG][OPT-25] 调度管理器注册应与 shutdown 原子互斥

1 participant