Skip to content

Add Linux IO::Event::Futex support - #210

Merged
samuel-williams-shopify merged 22 commits into
mainfrom
add-futex
Sep 24, 2026
Merged

samuel-williams-shopify merged 22 commits into
mainfrom
add-futex

Conversation

@samuel-williams-shopify

@samuel-williams-shopify samuel-williams-shopify commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Add IO::Event::Futex on Linux with Ruby 4.1 or later, backed by an aligned 32-bit word in an IO::Buffer. The platform primitive lives in futex.c/futex.h; uring.c contains the optional asynchronous integration. Single and vector waits use separate argument structs and setup, result-handling, and cleanup callbacks, while sharing the existing io_uring completion and cancellation machinery.

The API provides atomic value access, increment, decrement, compare-and-exchange, wake, increment-and-wake signalling, single waits, and vector waits over up to Futex::WAITV_LIMIT words. Vector support is conditional on the build headers. Wake-ups are notifications: callers must recheck their shared state in a loop. wait(expected) requires an explicit expected value, captured before checking application state; there is no implicit snapshot that could miss a notification between the state check and the wait. The Futex guide explains this race and the required snapshot/check/wait ordering. Both wake(0) and signal(0) are no-ops: neither changes the word nor wakes waiters; they return zero and the current word value respectively. Closed or uninitialized instances still raise IOError.

Without a fiber scheduler, waits release the GVL and use the Linux futex syscalls. With a scheduler, they delegate to the optional futex_wait or futex_waitv hooks; an unsupported scheduler raises NotImplementedError rather than blocking its reactor thread. The URing selector exposes the hooks only when both liburing and the running kernel support the corresponding operations. The Futex class itself does not require io_uring.

Buffer lifetime

  • Each Futex acquires one counted allocation lock when bound and retains it until explicitly closed or finalized. Multiple futex words can independently retain the same allocation, and slices lock their root allocation. This requires Ruby 4.1's counted locks; the class is not defined on older Rubies or non-Linux systems.
  • close is idempotent, closed? reports release, and operations on closed or uninitialized instances raise IOError. Futexes cannot be copied or reinitialized.
  • An independent C-backed Ruby finalizer retains the buffer and releases the lock. Neither native dfree callback dereferences or unlocks another Ruby object. Explicit close remains preferable for deterministic release.
  • Pending waits prevent closing. Cancellation drains the original kernel operation before releasing the futex references or stack-backed wait vector. Argument conversion and partially completed setup are exception-safe, and vector waits retain a private snapshot of their futex references.

Externally managed storage still requires its external owner to keep it alive. Application references attached to the buffer must not retain the Futex indirectly through its finalizer. The dedicated Futex Notifications guide covers capability detection, shared-memory setup, atomic operations, wait/recheck patterns, vector waits, lifetime, cancellation, and portable IPC fallbacks. It is linked from getting-started and the main readme.

Motivation

Provide a low-overhead shared-memory notification path for processes that already exchange state through IO::Buffer. Vector waits allow a coordinator such as Fantail to wait for worker permit changes without polling every worker. Consumers can detect the class and scheduler capabilities before selecting this mechanism or a portable IPC fallback.

Testing

  • Ubuntu 26.04/Ruby-head CI: 470 passed, 29 skipped across the full suite (1,231 assertions), with packaged liburing 2.14 and both Futex helpers detected.
  • Futex tests after the zero-count changes: 66 passed (116 assertions), including actual io_uring single and vector waits. The two new zero-count notification tests both failed against the old extension and pass after rebuilding.
  • Linux/Ruby-head full suite after the zero-count changes: 478 passed, 29 skipped (1,234 assertions). The first run hit the existing sleep-based blocking-wait test's timing race (signal arriving before wait); the full rerun passed. Coverage also includes a single-entry vector returning index zero and an out-of-band vector resume returning nil after cancellation cleanup.
  • Regression coverage includes independent allocation locks, slice roots, explicit close, GC/finalizers, compaction, rejected copies, blocking thread interruption, asynchronous cancellation, out-of-band resumes, argument-conversion failures, and mutated wait-vector inputs.
  • RuboCop: 63 files inspected, no offenses.
  • macOS/Ruby 4.0.7: native build and smoke check confirm the Futex class is absent.
  • The regular Test workflow runs its Linux/Ruby-head matrix entry on Ubuntu 26.04 with packaged liburing-dev, using bundle exec bake test to build the extension and run the full suite including Futex tests. There is no separate Futex job or Bake task. Ruby head retains its existing experimental status, and tests remain conditional on the available APIs.

@samuel-williams-shopify samuel-williams-shopify changed the title Add IO::Event::Futex using io_uring Add Linux IO::Event::Futex support Aug 21, 2026
Comment thread .github/workflows/test-futex.yaml Outdated
@samuel-williams-shopify
samuel-williams-shopify merged commit 3f745d3 into main Sep 24, 2026
60 of 64 checks passed
@samuel-williams-shopify
samuel-williams-shopify deleted the add-futex branch September 24, 2026 21:27
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