Skip to content

Fix Interrupt robustness. - #191

Merged
ioquatix merged 1 commit into
mainfrom
interrupt-fix
Jun 24, 2026
Merged

Fix Interrupt robustness.#191
ioquatix merged 1 commit into
mainfrom
interrupt-fix

Conversation

@samuel-williams-shopify

@samuel-williams-shopify samuel-williams-shopify commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

It turns out that Interrupt#signal is problematic... I don't know why, but if an IOError is triggered during it's activation during Thread#join, Ruby's blocking_operation list seems to get messed up.

@samuel-williams-shopify samuel-williams-shopify changed the title Document interrupt signal wakeup path Fix Interrupt robustness. Jun 23, 2026
@samuel-williams-shopify
samuel-williams-shopify force-pushed the interrupt-fix branch 4 times, most recently from 3346442 to cd5b12f Compare June 23, 2026 21:39
`IOError` can be raised in response to `@output.close`. `unblock` defers it and this can kill the scheduler.

In addition normal `write` can block, or raise errors, etc. That's also a problem.
@ioquatix
ioquatix merged commit ffcc5c6 into main Jun 24, 2026
56 of 60 checks passed
@ioquatix
ioquatix deleted the interrupt-fix branch June 24, 2026 01:51
@samuel-williams-shopify

Copy link
Copy Markdown
Contributor Author

For context on why rescue IOError/rescue nil around IO#write was not sufficient here:

The confusing part is that the IOError was causally related to @output.write("."), but it was not delivered during the dynamic extent of that Ruby call.

The failing path was roughly:

rb_fiber_scheduler_unblock
  TestScheduler#unblock
    Select#wakeup
      Interrupt#signal
        @output.write(".") rescue nil

rb_fiber_scheduler_unblock deliberately masks pending interrupts while it invokes scheduler Ruby code:

ec->interrupt_mask |= PENDING_INTERRUPT_MASK;
rb_funcall(scheduler, id_unblock, ...);
ec->interrupt_mask = saved_interrupt_mask;
RUBY_VM_CHECK_INTS(ec);

While @output.write(".") was in progress, closing the pipe saw the active CRuby blocking operation and queued ruby_error_stream_closed as a pending interrupt on the writing thread. However, because PENDING_INTERRUPT_MASK was set, the RUBY_VM_CHECK_INTS_BLOCKING inside IO#write could not observe/deliver that pending exception.

So IO#write returned past the local rescue. Then rb_fiber_scheduler_unblock restored the interrupt mask and ran its trailing interrupt check, at which point the queued stream closed in another thread IOError was finally delivered outside the write rescue scope.

That is why write_nonblock fixes this particular issue: the wakeup byte is best-effort and should not enter CRuby's blocking IO operation machinery from inside scheduler unblock/wakeup paths. It avoids creating a blocking operation that can later receive a deferred stream-closed pending interrupt.

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