Skip to content

Make Semian work in non-main Ractors - #1053

Open
yaroslav-shopify wants to merge 3 commits into
mainfrom
ractorize
Open

yaroslav-shopify wants to merge 3 commits into
mainfrom
ractorize

Conversation

@yaroslav-shopify

@yaroslav-shopify yaroslav-shopify commented Oct 9, 2026 •

Copy link
Copy Markdown

Calling Semian from a non-main Ractor raises Ractor::IsolationError, and on Linux bulkheads raise Ractor::UnsafeError. Two commits:

  • Make Semian work in non-main Ractors: each Ractor gets its own resources and circuit breakers, as each forked process does. The main Ractor works as before. Needs Ruby 3.4.
  • Mark the C extension as Ractor-safe, so bulkheads work there too.

Tested on macOS with Ruby 3.4, 4.0 and master, so the bulkhead changes will first run in CI.

A third commit pins hiredis-client to redis-client 0.30.1 in gemfiles/redis_client.gemfile: redis-rb's master requires that version, so the gemfile no longer resolves.

Non-main Ractors can't write the Semian module's instance variables,
and can only read the ones that hold shareable values. Semian keeps its
resources, logger and memoized settings there, and its subscribers in a
constant, so the first call into Semian from a non-main Ractor raises,
for example:

    Semian#consumers: can not get unshareable values from instance
    variables of classes/modules from non-main Ractors (@reset_mutex
    from Semian) (Ractor::IsolationError)

Each non-main Ractor now keeps its own resources and consumers in
Ractor-local storage, as each forked process has its own, so its
circuit breakers count its own errors. Bulkheads are still shared
through their SysV semaphores. The main Ractor keeps the module's state
as before, and Semian.reset! only resets the current Ractor's.

- Subscribers: non-main Ractors notify the main Ractor's subscribers
  whose names and blocks are shareable, from a frozen copy rebuilt on
  each subscribe and unsubscribe. Other blocks capture state that only
  the main Ractor can use. Non-main Ractors can subscribe for
  themselves too.
- Logger: non-main Ractors use the logger set in the main Ractor if
  it's shareable by the time they log. Otherwise they log to their own
  $stderr, unless they set a logger of their own.
- Configuration: Semian.namespace= stores a frozen copy of a String,
  Semian.thread_safe? no longer memoizes its default, and the
  disabled-semaphores warning is issued once in each Ractor.

This needs Ractor.main? and Ractor-local storage, from Ruby 3.4. On
older Rubies, Semian works as before.

Each test failed without the fix, apart from the shareable logger's.
The redis-client test passes the driver as a class, because
RedisClient.driver(:ruby) reads a class instance variable. It also
turns off the bulkhead, because bulkheads in non-main Ractors need the
extension to be marked as Ractor-safe.
The extension implements bulkheads with SysV semaphores, and isn't
marked as Ractor-safe, so on Linux every bulkhead operation in a
non-main Ractor, starting with registering one, raises
Ractor::UnsafeError ("ractor unsafe method called from not main
ractor").

The extension has no mutable global state. Init_semian sets the
exception classes, two IDs and system_max_semaphore_count, and none of
them changes afterwards. Each Semian::Resource only uses its own
struct, which isn't shareable, so it stays in the Ractor that created
it, and its semaphore set, which the kernel synchronizes across
processes, threads and Ractors alike. Keys are SHA-1 digests into a
local buffer.

Each Ractor that registers a resource counts as a worker of its
semaphore set, as each forked process does, so quotas scale with the
number of Ractors. SEM_UNDO adjustments are per process, so a Ractor's
tickets and worker registration are only undone when the process
exits, as for threads.

The test holds a bulkhead's only ticket in the main Ractor and expects
the same resource to time out in a non-main Ractor, registered as a
second worker. It's skipped where semaphores aren't supported, so it
hasn't run on macOS, where this was written.
redis-rb's master requires redis-client 0.30.1, and hiredis-client from
redis-client's master now requires 0.31.0, so the gemfile no longer
resolves. Use the v0.30.1 tag until redis-rb moves to 0.31.0.

This branch has not been deployed

No deployments
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