fix(threading): guard the pool cancellation flag behind a lock - #1102
Conversation
The shared cancel flag was a raw PBoolean read and written across threads with no synchronization — disassembly shows a bare ldrb/strb with no barriers — so a worker could observe a stale False after the first failure and keep draining the queue. It becomes a small lock-owning TGocciaCancellationFlag built on the unit's existing critical-section idiom: the Boolean is private and reachable only through three locked methods, so no future access site can bypass it, and all seven sites (worker read/write, pool init, RunAll reset, Cancel, the Cancelled property, worker handoff) go through it. That also closes a use-after-free the pointer form allowed — an abandoned watchdog-stalled worker dereferenced into the freed pool; the flag now follows the queue's leak rule instead. Stress evidence: 400 files, failure at index 8, --exit-on-first-failure — pre-fix the post-cancel count wobbled (9-16), post-fix it pins at the analytic minimum 8+jobs across 25/25 runs per job count. Two threading regressions added, one bounding executed files so a missed cancel blows the assert. Also makes the end-of-input documentation platform-neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
📝 WalkthroughWalkthroughThe thread pool now uses a synchronized shared cancellation flag. It resets the flag per run, cancels on errors, and preserves it for abandoned workers. New tests verify lifecycle and bounded cancellation. CLI documentation now describes platform-specific end-of-input keys. ChangesThread-pool cancellation
Platform-specific CLI input documentation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ThreadPool
participant Worker
participant CancellationFlag
participant WorkerCallback
ThreadPool->>CancellationFlag: Reset before RunAll
ThreadPool->>Worker: Start with shared flag
Worker->>WorkerCallback: Process file
WorkerCallback-->>Worker: Return error for fail.js
Worker->>CancellationFlag: Cancel on error
Worker->>CancellationFlag: Check IsCancelled
ThreadPool->>CancellationFlag: Read Cancelled state
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@source/units/Goccia.Threading.pas`:
- Around line 641-649: Update the RunAll lifecycle around FCancelFlag and
FCancelFlagLeaked so an abandoned run’s flag is never reused: before dispatching
a later run, create and own a new TGocciaCancellationFlag when FCancelFlagLeaked
is set, then clear the leak marker after replacement. Preserve the abandoned
flag for existing workers, and add a regression test covering a stalled worker
that resumes and fails during a subsequent run without cancelling that later
run.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6e5435ef-af4e-4096-82a9-5c2ecfb2751f
📒 Files selected for processing (4)
docs/contributing/cli-conventions.mdsource/app/Goccia.CLI.Application.passource/units/Goccia.Threading.Test.passource/units/Goccia.Threading.pas
Summary
PBooleanread and written across threads with no synchronization (disassembly: bareldrb/strb, no barriers), so a worker could observe a staleFalseafter the first failure and keep draining the queue.TGocciaCancellationFlagon the unit's existing critical-section idiom — the Boolean is private, reachable only through three locked methods, so no future access site can bypass it. All seven sites are covered (worker read/write, pool init,RunAllreset,Cancel, theCancelledproperty getter, worker handoff).@FCancelledpointed into the pool object, so an abandoned watchdog-stalled worker kept dereferencing it afterPool.Free— a use-after-free every caller could hit. The flag now follows the work queue's established leak rule.--exit-on-first-failure— pre-fix the post-cancel file count wobbled (jobs=4: 9–12; jobs=8: 10–16), post-fix it pins at the analytic minimum8 + jobsacross 25/25 runs per job count. Two new threading regressions, one bounding executed files so a missed cancel blows the assert.FLastActivityNs,FCurrentFileIndex) are flagged, not silently widened into scope.Stack #1083 layer 26.
Testing
Goccia.Threading.Test(14/14); CLI harness, differential harness (0 divergences), format, all doc validators exit 0Pool CancelOnError bounds work across workers(8 workers, 2000 files, 10 repeats)🤖 Generated with Claude Code