Skip to content

Prevent worker-pool re-entry with Fiber.blocking - #232

Merged
samuel-williams-shopify merged 2 commits into
mainfrom
fix-worker-pool-blocking-wait
Oct 18, 2026
Merged

samuel-williams-shopify merged 2 commits into
mainfrom
fix-worker-pool-blocking-wait

Conversation

@samuel-williams-shopify

Copy link
Copy Markdown
Contributor

GC finalizers can run inside the scheduler's block hook and start another blocking operation on the same fiber. The nested worker-pool wait can consume the outer operation's completion notification, cancelling the nested work and leaving the outer wait stuck.

This is an alternative to #230: prevent implicit re-entry into the worker pool instead of tracking nested operations and replaying wakeups.

  • Capture the scheduler, then enter Fiber.blocking before submitting work. Keep waiting, cancellation, and draining inside that scope.
  • Operations triggered by scheduler callbacks or finalizers execute synchronously on the current thread. Explicit fiber transfers still work, and other fibers retain their nonblocking behavior.
  • Ignore spurious wakeups; cancellation remains tied to exceptions and other non-local exits.

The implementation uses the existing Fiber.blocking API and requires no Ruby changes. Its tradeoff is that an incidental nested operation blocks the event-loop thread until it finishes, including delaying scheduler-driven timeouts during that operation. The change applies to WorkerPool#call; it does not change Ruby's scheduler-hook contract globally.

The original regression-test commit from #229/#230 is retained with attribution. The helper classes are at file scope, and the regression now also verifies that only the outer operation reaches the pool. Additional checks cover independent fibers, synchronous operations during cancellation cleanup, restoration of blocking state after normal completion, exceptions and throw, and spurious wakeups.

Validation on macOS with Ruby 4.0.6:

  • Original regression reproduced before the fix: the nested operation was cancelled and the outer wait timed out. It passes with the guard.
  • Full io-event suite: 320 passed, 21 skipped (341 total, 1034 assertions).
  • Worker-pool suite with both KQueue and Select: 18 passed each (103 assertions).
  • Async scheduler and task tests with ASYNC_SCHEDULER_WORKER_POOL=true, using Async 2.46.0 and the local io-event extension: 101 passed (219 assertions).
  • Async integration check forcing GC inside block: all 20 Tempfile finalizers removed their files, 20 file writes completed, and maximum blocking-operation hook depth stayed at 1.
  • RuboCop: no offenses. Linux validation is left to CI.

ekmartin and others added 2 commits October 17, 2026 16:39
…block`.

If the scheduler's `block` hook performs a nested `WorkerPool#call` on the same fiber before suspending (as a GC finalizer can, e.g. `Tempfile` unlinking its file), the outer operation's wakeup resumes the nested wait instead. The nested operation is cancelled and the outer wait never wakes again.

Also record whether the `busy` test operation actually executed, since an operation cancelled before it starts is otherwise reported as completed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@samuel-williams-shopify
samuel-williams-shopify merged commit 77e0c72 into main Oct 18, 2026
60 of 64 checks passed
@samuel-williams-shopify
samuel-williams-shopify deleted the fix-worker-pool-blocking-wait branch October 18, 2026 02:24
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.

2 participants