fix(TaskQueue): append groups instead of replacing on re-entrant enqueue - #10
Conversation
A concurrent call to `enqueuePriorityMounts` or `enqueuePriorityUnmounts` while a sequence was already draining would overwrite `pendingMountGroups` / `pendingUnmountGroups`, stranding any not-yet-started groups and leaking their placeholder tasks as permanent in-flight identities. The unconditional `_startNextMountGroup` call also caused a new group to start concurrently with the running one, breaking the sequential priority guarantee. Groups are now appended rather than replaced, and a new group is started only when the queue is idle (`mountGroupRemaining == 0`). An in-progress sequence picks up appended groups naturally via the existing completion chain.
📝 WalkthroughWalkthroughThis PR makes ChangesRe-entrant priority enqueuing
Sequence DiagramsequenceDiagram
participant Caller1 as First enqueue
participant Caller2 as Second enqueue
participant Queue as TaskQueue
participant Groups as pendingGroups
participant Counter as Counter (idle?)
participant Start as _startNext...
Caller1->>Queue: enqueuePriority(groups)
Queue->>Counter: check if idle (== 0)
Counter-->>Queue: idle=true
Queue->>Groups: append groups
Queue->>Start: _startNextMountGroup()
Note over Start: Draining, counter > 0
Caller2->>Queue: enqueuePriority(groups)
Queue->>Counter: check if idle (== 0)
Counter-->>Queue: idle=false
Queue->>Groups: append groups
Note over Queue: Skip _startNext, already draining
Start->>Start: complete current group
Start->>Counter: decrement, check idle
Counter-->>Start: idle=false (more pending)
Start->>Start: _startNextMountGroup() via completion
Note over Start: Continue draining appended groups
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes The implementation change is straightforward (append instead of replace, conditional start), but the concurrent test infrastructure and three interrelated regression cases require careful understanding of the deterministic gating mechanism and assertions around race conditions and pending work guarantees.
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Summary
enqueuePriorityMounts/enqueuePriorityUnmountsreplacedpendingMountGroups(and unconditionally kicked off the next group) on every call. That is only correct when the queue is idle. A second enqueue that lands while an earlier priority sequence is still draining — which happens routinely, sincetick()can fire again before the previous wave's groups finish — corrupted the in-flight queue.This PR makes both methods re-entrant: groups are appended, never replaced, and a new group is started only when the queue is idle. An already-draining sequence picks up the newly appended groups through the existing completion chain (
mountTaskCompleted→_startNextMountGroup).The bug
Two distinct failure modes when a second call lands mid-drain:
Stranded pending work / leaked placeholders. Each enqueue first registers every identity as an in-flight placeholder in
tasks, then queues its groups. ReplacingpendingMountGroupsdropped any not-yet-started groups from the earlier sequence — but their placeholders stayed intasksforever. Those nodes never mounted, andinFlightIdentities()never drained, so the engine believed work was still running indefinitely.Broken priority sequentiality. The unconditional
_startNextMountGroup()overwrotemountGroupRemainingand launched a fresh group concurrently with the group already running, violating the "priority groups execute sequentially" contract and corrupting the remaining-count bookkeeping that drives the completion chain.The fix
mountTaskCompleted→_startNextMountGrouponce the running group finishes.Identity dedup is unchanged: work already in-flight (placeholdered by an earlier group) is still filtered out — re-convergence remains
loop()'s job, not the queue's.Changes
Sources/Astrolabe/Engine/TaskQueue.swiftenqueuePriorityMounts/enqueuePriorityUnmountsappend groups and start the next one only when*GroupRemaining == 0. Doc comments spell out the append/idle-start re-entrancy contract.Tests/AstrolabeTests/AstrolabeTests.swiftMountGaterendezvous holds a node mid-mount so a second enqueue lands while the first sequence is still draining. Covers stranded-pending + leaked-placeholder (mount and unmount) and sequential execution of mid-flight enqueues.Test plan
swift test --filter priorityMount/--filter priorityUnmount— the three new tests passswift test— full suite greenreplace/unconditional-start code (confirms it's a real regression guard)🤖 Generated with Claude Code