Skip to content

Prevent native selector waits with pending signal exceptions#204

Merged
samuel-williams-shopify merged 1 commit into
mainfrom
fix-pending-interrupt-fallback
Jul 21, 2026
Merged

Prevent native selector waits with pending signal exceptions#204
samuel-williams-shopify merged 1 commit into
mainfrom
fix-pending-interrupt-fallback

Conversation

@samuel-williams-shopify

@samuel-williams-shopify samuel-williams-shopify commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • centralize native selector no-GVL waits behind a shared blocking-operation helper
  • use RB_NOGVL_PENDING_INTR_FAIL when Ruby provides it
  • on older Rubies, refresh pending-interrupt state with a C-backed Thread.handle_interrupt(SignalException => :never) block immediately before rb_thread_call_without_gvl2
  • remove the racy Thread.pending_interrupt? pre-check from URing, EPoll, and KQueue
  • preserve selector cleanup when an exception unwinds the compatibility helper

Why this works

A signal exception can be queued under an outer Thread.handle_interrupt(... => :never) mask after the selector checks Thread.pending_interrupt? but before it releases the GVL. Ruby can then mark the queue as checked and clear the VM interrupt flag, so rb_thread_call_without_gvl2 enters the native wait without an interrupt to observe and without an installed UBF to wake it.

Pushing a Thread.handle_interrupt mask while the pending queue is non-empty resets pending_interrupt_queue_checked and raises the VM interrupt flag. The fallback invokes rb_thread_call_without_gvl2 directly from a C-backed block, leaving no Ruby safepoint between that refresh and unblock_function_set. Ruby then either observes the interrupt before blocking or installs the UBF while holding interrupt_lock, closing the transition race.

The fallback uses SignalException => :never: it prevents the native wait from being stranded without changing the caller's exception-delivery timing. It is compiled out when RB_NOGVL_PENDING_INTR_FAIL is available.

Tests

  • Ruby 4.0.5: 279 passed, 9 skipped, 901 assertions
  • Ruby HEAD: extension compiles through the native flag path; fallback symbols are absent
  • existing black-box selector tests cover interrupts pending before and arriving during native waits
  • Falcon shutdown reproduction: 2,000 iterations, 2 workers per iteration, zero delayed worker dumps or shutdown failures

Performance

On Ruby 4.0.5, 7 alternating 100,000-iteration KQueue runs with a positive near-zero timeout measured 27.152 us/wait for the baseline and 27.128 us/wait for the fallback (-0.09%, within noise). The compatibility path allocates one internal T_IMEMO per genuinely blocking wait. Nonblocking/immediately-ready waits do not enter it, and Rubies with RB_NOGVL_PENDING_INTR_FAIL retain the direct native path.

@samuel-williams-shopify
samuel-williams-shopify force-pushed the fix-pending-interrupt-fallback branch 2 times, most recently from 76b9b61 to 2094ba5 Compare July 20, 2026 23:53
@samuel-williams-shopify samuel-williams-shopify changed the title Avoid stranding pending interrupts in native selectors Prevent native selector waits with pending signal exceptions Jul 21, 2026
@samuel-williams-shopify
samuel-williams-shopify force-pushed the fix-pending-interrupt-fallback branch from 2094ba5 to 077e4e3 Compare July 21, 2026 00:25
@samuel-williams-shopify
samuel-williams-shopify force-pushed the fix-pending-interrupt-fallback branch from 077e4e3 to 79a7244 Compare July 21, 2026 00:26
@samuel-williams-shopify
samuel-williams-shopify merged commit a90dcb3 into main Jul 21, 2026
55 of 59 checks passed
@samuel-williams-shopify
samuel-williams-shopify deleted the fix-pending-interrupt-fallback branch July 21, 2026 00:31
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.

1 participant