Internals¶
How RactorQueue is built — useful if you're contributing, debugging, or just curious.
C++ Foundation: atomic_queue¶
The queue is backed by max0x7ba/atomic_queue — a C++14 header-only library that implements cache-line-aligned, lock-free MPMC queues using atomic compare-and-swap (CAS) operations.
RactorQueue uses AtomicQueueB2<VALUE>, the dynamically-sized variant. VALUE is MRI Ruby's opaque pointer type for Ruby objects — every Ruby object is represented as a VALUE (a uintptr_t).
The AtomicQueueB2 template keeps two separate cache-line-aligned arrays for head/tail indices and for the ring buffer, minimizing false sharing between producers and consumers.
Rice 4.x Bindings¶
The C++ class is exposed to Ruby via Rice 4.x:
Data_Type<StandardQueue> rb_cRQ = define_class<StandardQueue>("RactorQueue")
.define_constructor(Constructor<StandardQueue, unsigned>())
.define_method("c_try_push", &StandardQueue::try_push,
Arg("v").setValue())
.define_method("c_try_pop", &StandardQueue::try_pop,
Return().setValue())
.define_method("capacity", &StandardQueue::capacity)
.define_method("was_size", &StandardQueue::was_size)
.define_method("was_empty", &StandardQueue::was_empty)
.define_method("was_full", &StandardQueue::was_full);
Key details:
Arg("v").setValue()— tells Rice to pass the argument as a rawVALUE(no type conversion); the C++ method receives the Ruby object pointer directlyReturn().setValue()— tells Rice to return the rawVALUEwithout conversion- Without
.setValue(), Rice would attempt to convertVALUEusing its type-mapping system, which would incorrectly treatVALUEas an integer
EMPTY_SENTINEL¶
The C extension needs a way to signal "the queue is empty" from c_try_pop without conflating it with a legitimately pushed nil (Ruby's Qnil, which is a valid VALUE).
The solution: a permanent, unique, frozen Object instance registered as a GC root:
g_empty_sentinel = rb_obj_alloc(rb_cObject);
rb_obj_freeze(g_empty_sentinel);
rb_gc_register_mark_object(g_empty_sentinel); // pin — never GC'd
rb_define_const(rb_cRQ, "EMPTY_SENTINEL", g_empty_sentinel);
c_try_pop returns this sentinel when the queue is empty. The Ruby-level try_pop checks for it with result.equal?(RactorQueue::EMPTY_SENTINEL) (identity comparison, not ==), then converts it to nil for the caller.
The blocking blocking_pop checks for the sentinel in its spin loop: if it gets the sentinel, the queue is empty and it retries; any other value (including nil) is returned as-is.
Ractor Shareability¶
Two things are needed to make a Rice-wrapped object Ractor.shareable?:
1. RUBY_TYPED_FROZEN_SHAREABLE flag
Rice sets RUBY_TYPED_FREE_IMMEDIATELY on the rb_data_type_t it creates. We OR-in RUBY_TYPED_FROZEN_SHAREABLE so that Ractor.make_shareable(instance) succeeds when the Ruby wrapper is frozen:
2. rb_ext_ractor_safe(true)
This marks all methods registered by subsequent rb_define_method calls (which Rice uses internally) as safe to call from within a Ractor. Without this, Ruby raises an error when a Ractor attempts to call the C method:
rb_ext_ractor_safe(true);
// ... define_class and define_method calls ...
rb_ext_ractor_safe(false);
3. Ractor.make_shareable in the Ruby factory
The Ruby-level RactorQueue.new calls Ractor.make_shareable(instance) after setting instance variables. This deep-freezes the Ruby wrapper object. The C++ AtomicQueueB2 buffer is not a Ruby object and is unaffected by Ruby's freeze.
File Layout¶
ractor_queue/
├── ext/
│ └── ractor_queue/
│ ├── extconf.rb # mkmf config; finds Rice headers
│ ├── ractor_queue.cpp # Rice bindings + Init_ractor_queue
│ └── standard_queue.h # C++ wrapper around AtomicQueueB2<VALUE>
├── lib/
│ └── ractor_queue/
│ ├── ractor_queue.rb # RactorQueue.new factory (capacity + make_shareable)
│ ├── interface.rb # Ruby-level spin loops, try_push/pop, validate_shareable
│ ├── errors.rb # Error, TimeoutError, NotShareableError
│ └── version.rb # VERSION constant
├── vendor/
│ └── atomic_queue/
│ └── include/
│ └── atomic_queue/
│ └── atomic_queue.h # vendored header (MIT)
└── test/
├── test_helper.rb
├── test_ractor_queue.rb # Core API tests
├── test_ractor_safety.rb # Ractor shareability and cross-Ractor round-trips
└── test_timeout.rb # Timeout behavior
Ruby-Level Spin Loop¶
The blocking operations (push, pop) live entirely in Ruby (lib/ractor_queue/interface.rb). The C extension provides only the non-blocking c_try_push / c_try_pop primitives.
This keeps the GVL release/acquire cycle outside the hot path: the C methods are brief atomic operations that don't need to release the GVL, and the Ruby spin loop has access to full Ruby interrupt semantics (Thread.pass, sleep, Thread#raise).
SPIN_THRESHOLD = 16
SLEEP_INTERVAL = 0.0001 # 100 µs
def blocking_push(obj, timeout)
deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil
spins = 0
loop do
return self if c_try_push(obj)
raise TimeoutError if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
if spins < SPIN_THRESHOLD
spins += 1
Thread.pass
else
sleep(SLEEP_INTERVAL)
end
end
end