Skip to content

fix(threading): guard the pool cancellation flag behind a lock - #1102

Merged
frostney merged 1 commit into
fix/coderabbit-round-2from
fix/coderabbit-round-3
Aug 9, 2026
Merged

fix(threading): guard the pool cancellation flag behind a lock#1102
frostney merged 1 commit into
fix/coderabbit-round-2from
fix/coderabbit-round-3

Conversation

@frostney

@frostney frostney commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes both findings from fix(review): CodeRabbit round-2 findings across the stack #1101's review, one of them a data race the round-2 layer introduced: the pool's shared cancel flag was a raw PBoolean read and written across threads with no synchronization (disassembly: bare ldrb/strb, 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 on 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, RunAll reset, Cancel, the Cancelled property getter, worker handoff).
  • Bonus defect closed: the old @FCancelled pointed into the pool object, so an abandoned watchdog-stalled worker kept dereferencing it after Pool.Free — a use-after-free every caller could hit. The flag now follows the work queue's established leak rule.
  • Evidence, since a passing suite proves nothing about races: 400 stress files with a failure at index 8 under --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 minimum 8 + jobs across 25/25 runs per job count. Two new threading regressions, one bounding executed files so a missed cancel blows the assert.
  • Audited the rest of the round-2 layer's cross-thread surface: no second instance. Two pre-existing best-effort watchdog fields (FLastActivityNs, FCurrentFileIndex) are flagged, not silently widened into scope.
  • Minor: end-of-input docs are platform-neutral (Ctrl-D / Ctrl-Z+Enter), matching the runtime strings, plus a stale code comment.

Stack #1083 layer 26.

Testing

  • Verified in end-to-end tests — full suite 12,087/12,087 both modes; all 66 Pascal test programs exit 0 including Goccia.Threading.Test (14/14); CLI harness, differential harness (0 divergences), format, all doc validators exit 0
  • Updated documentation — cli-conventions.md end-of-input guidance
  • Optional: native Pascal tests — flag state-machine test + Pool CancelOnError bounds work across workers (8 workers, 2000 files, 10 repeats)
  • Optional: benchmarks — n/a (lock is on the per-file boundary, not a hot path)

🤖 Generated with Claude Code

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>
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gocciascript-homepage Ready Ready Preview Aug 7, 2026 2:31pm

Request Review

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

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

Changes

Thread-pool cancellation

Layer / File(s) Summary
Shared cancellation flag contract
source/units/Goccia.Threading.pas
Added TGocciaCancellationFlag and changed workers and the pool to use its synchronized state.
Pool and worker cancellation flow
source/units/Goccia.Threading.pas
Workers check the shared flag. Runs reset it. Explicit cancellation and CancelOnError invoke Cancel. Abandoned workers retain safe access to the flag.
Cancellation lifecycle and bounded-work tests
source/units/Goccia.Threading.Test.pas
Added tests for flag transitions and bounded callback execution across repeated multi-worker runs.

Platform-specific CLI input documentation

Layer / File(s) Summary
End-of-input key documentation
docs/contributing/cli-conventions.md, source/app/Goccia.CLI.Application.pas
Updated documentation and comments to refer to platform-specific EndOfInputKeys values instead of only Ctrl-D.

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
Loading

Suggested labels: bug, internal

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description includes a complete Summary and Testing section, documents implementation constraints, links issue context, and reports test and documentation results.
Title check ✅ Passed The title clearly and concisely describes the main change: protecting the thread pool cancellation flag with synchronization.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@frostney

frostney commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added bug Something isn't working internal Refactoring, CI, tooling, cleanup labels Aug 7, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b16f39a and 659c6b1.

📒 Files selected for processing (4)
  • docs/contributing/cli-conventions.md
  • source/app/Goccia.CLI.Application.pas
  • source/units/Goccia.Threading.Test.pas
  • source/units/Goccia.Threading.pas

Comment thread source/units/Goccia.Threading.pas
@frostney
frostney merged commit 6ad4037 into main Aug 9, 2026
80 checks passed
@frostney
frostney deleted the fix/coderabbit-round-3 branch August 9, 2026 18:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working internal Refactoring, CI, tooling, cleanup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant