Files
ruby/ractor_sync.c
Koichi SasadaandClaude Fable 5 e768a87a17 Ractor: the dying thread collects its own objspace post-mortem
The retire GC at notify_exit could not reclaim the terminating
thread's scaffolding -- the Thread and Fiber wrappers, the ThreadGroup,
the Ractor-local stdio, whatever the machine stack still references --
because at that point they were all still rooted.  Those ~11 objects
kept ~2 heap pages alive per dead Ractor, merged into the inheritor to
wait for its next major GC, so both a spawn-and-join loop and a
fire-and-forget loop grew the heap by two pages per Ractor with
nothing reclaiming them.

Instead, run the final collection at the tail of thread_start_func_2,
just before the scheduler handoff.  The thread still holds the GVL, so
the collection runs lock-free like any local GC (a concurrent global
GC waits for its safepoint), and vm_remove_ractor keeps its light
lock-only section.  r->postmortem makes rb_ractor_mark_local_roots
root only what must survive the death -- the join value
(r->sync.legacy) and the rb_gc_register_mark_object pins -- and the
zombie_objspaces entry recorded later measures exactly the pages the
joiner will inherit.  While the Ractor is still in vm->ractor.set,
rb_vm_mark and ractor_mark keep the Ractor object itself and its
default port alive as before; everything they mark is foreign to or
pinned by this collection.

Two of the swept wrappers are the dying thread's own: its Thread (the
rest of the collection resolves GET_EC through ec->thread_ptr) and its
root Fiber (whose struct embeds that ec).  thread_free and fiber_free
recognize "the struct I am standing on" (ec == the current ec), keep
the struct, and leave an in-band mark (th->self = 0; cont.self = 0,
which already means "no wrapper" on the never-wrapped path).  Found as
a 1/30 heap corruption, pinned by ASAN.  The collecting thread
captures those two pointers right after the collection, while its GVL
still excludes any global GC, and frees exactly the captured structs
at its very last step -- after vm_remove_ractor unlinks the Ractor,
anything it did not sweep may already be freed by a concurrent global
GC, so the epilogue reads no shared memory at all.  Those frees would
resolve their objspace through the TLS ec -- which sits inside the
fiber struct being freed and leads to the retired Ractor -- so
rb_ractor_postmortem_free severs the TLS ec first (the MN epilogue
already had) and they fall back to the main objspace.

While a thread-event hook is registered the postmortem flag stays off
and the collection keeps the ordinary retire roots: callbacks may
retain any Ractor's Thread object (rb_internal_thread_event_hook_t),
and such references are invisible to the reduced roots.  The check
takes the hook rwlock, so it happens-after any completed registration;
a hook registered after it gets no event from this thread --
rb_thread_execute_hooks skips a thread whose wrapper is gone
(th->self == 0) -- so it never receives what the collection swept.
On platforms without hook support (win32, none) the check reports
false.

The collection runs after thread_cleanup_func, so the dying thread's
machine stack is already torn down: the root scan skips its machine
context (rb_gc_impl_during_postmortem_p), which holds nothing the
reduced roots do not already cover and faults when scanned on some
platforms (windows-arm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 03:33:40 +09:00

1910 lines
53 KiB
C

// this file is included by ractor.c
struct ractor_port {
rb_ractor_t *r;
st_data_t id_;
};
static st_data_t
ractor_port_id(const struct ractor_port *rp)
{
return rp->id_;
}
static VALUE rb_cRactorPort;
static VALUE ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp);
static VALUE ractor_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move);
static VALUE ractor_try_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move);
static void ractor_add_port(rb_ractor_t *r, st_data_t id);
// The off-heap courier used for moves. It is defined in ractor.c.
struct rb_ractor_move_courier *rb_ractor_move_courier_build(VALUE obj);
VALUE rb_ractor_move_courier_materialize(struct rb_ractor_move_courier *c);
void rb_ractor_move_courier_free(struct rb_ractor_move_courier *c);
static void
ractor_port_mark(void *ptr)
{
const struct ractor_port *rp = (struct ractor_port *)ptr;
if (rp->r) {
rb_gc_mark(rp->r->pub.self);
}
}
static const rb_data_type_t ractor_port_data_type = {
"ractor/port",
{
ractor_port_mark,
RUBY_TYPED_DEFAULT_FREE,
NULL, // memsize
NULL, // update
},
0, 0, RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
};
static st_data_t
ractor_genid_for_port(rb_ractor_t *cr)
{
// TODO: enough?
return cr->sync.next_port_id++;
}
static struct ractor_port *
RACTOR_PORT_PTR(VALUE self)
{
VM_ASSERT(rb_typeddata_is_kind_of(self, &ractor_port_data_type));
return RTYPEDDATA_GET_DATA(self);
}
// r is NULL between Ractor::Port.allocate and ractor_port_init()
static struct ractor_port *
ractor_port_ptr_check(VALUE self)
{
struct ractor_port *rp = RACTOR_PORT_PTR(self);
if (UNLIKELY(rp->r == NULL)) {
rb_raise(rb_eTypeError, "uninitialized %"PRIsVALUE, rb_obj_class(self));
}
return rp;
}
static VALUE
ractor_port_alloc(VALUE klass)
{
struct ractor_port *rp;
VALUE rpv = TypedData_Make_Struct(klass, struct ractor_port, &ractor_port_data_type, rp);
rb_obj_freeze(rpv);
return rpv;
}
static VALUE
ractor_port_init(VALUE rpv, rb_ractor_t *r)
{
struct ractor_port *rp = RACTOR_PORT_PTR(rpv);
rp->r = r;
RB_OBJ_WRITTEN(rpv, Qundef, r->pub.self);
rp->id_ = ractor_genid_for_port(r);
ractor_add_port(r, ractor_port_id(rp));
rb_obj_freeze(rpv);
return rpv;
}
/*
* call-seq:
* Ractor::Port.new -> new_port
*
* Returns a new Ractor::Port object.
*/
static VALUE
ractor_port_initialize(VALUE self)
{
return ractor_port_init(self, GET_RACTOR());
}
/* :nodoc: */
static VALUE
ractor_port_initialize_copy(VALUE self, VALUE orig)
{
struct ractor_port *dst = RACTOR_PORT_PTR(self); // uninitialized by definition
struct ractor_port *src = ractor_port_ptr_check(orig);
dst->r = src->r;
RB_OBJ_WRITTEN(self, Qundef, dst->r->pub.self);
dst->id_ = ractor_port_id(src);
return self;
}
static VALUE
ractor_port_new(rb_ractor_t *r)
{
VALUE rpv = ractor_port_alloc(rb_cRactorPort);
ractor_port_init(rpv, r);
return rpv;
}
static bool
ractor_port_p(VALUE self)
{
return rb_typeddata_is_kind_of(self, &ractor_port_data_type);
}
static VALUE
ractor_port_receive(rb_execution_context_t *ec, VALUE self)
{
const struct ractor_port *rp = ractor_port_ptr_check(self);
if (rp->r != rb_ec_ractor_ptr(ec)) {
rb_raise(rb_eRactorError, "only allowed from the creator Ractor of this port");
}
VALUE v = ractor_receive(ec, rp);
RB_GC_GUARD(self);
return v;
}
static VALUE
ractor_port_send(rb_execution_context_t *ec, VALUE self, VALUE obj, VALUE move)
{
const struct ractor_port *rp = ractor_port_ptr_check(self);
ractor_send(ec, rp, obj, RTEST(move));
RB_GC_GUARD(self);
return self;
}
static bool ractor_closed_port_p(rb_execution_context_t *ec, rb_ractor_t *r, const struct ractor_port *rp);
static bool ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *r, const struct ractor_port *rp);
static VALUE
ractor_port_closed_p(rb_execution_context_t *ec, VALUE self)
{
const struct ractor_port *rp = ractor_port_ptr_check(self);
rb_ractor_t *r = rp->r;
bool closed;
if (rb_ec_ractor_ptr(ec) == r) {
/* The owner's threads are serialized by the ractor GVL, so the ports
* table can't change under this lookup. */
closed = ractor_closed_port_p(ec, r, rp);
}
else {
/* A foreign Ractor races the owner's st_insert/st_delete on the ports
* table; take the lock like every other foreign reader. ractor_closed_port_p
* asserts the lock is held for foreign access, and Port#closed? was the
* only path reaching it without the lock. */
RACTOR_LOCK(r);
{
closed = ractor_closed_port_p(ec, r, rp);
}
RACTOR_UNLOCK(r);
}
return closed ? Qtrue : Qfalse;
}
static VALUE
ractor_port_close(rb_execution_context_t *ec, VALUE self)
{
const struct ractor_port *rp = ractor_port_ptr_check(self);
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
if (cr != rp->r) {
rb_raise(rb_eRactorError, "closing port by other ractors is not allowed");
}
ractor_close_port(ec, cr, rp);
return self;
}
// ractor-internal
// ractor-internal - ractor_basket
enum ractor_basket_type {
// basket is empty
basket_type_none,
// value is available
basket_type_ref,
basket_type_copy,
basket_type_move,
};
struct ractor_basket {
enum ractor_basket_type type;
VALUE sender;
st_data_t port_id;
struct {
VALUE v;
bool exception;
/* True when v held a type the native copier does not support and became a
* Marshal byte String. The receiver rebuilds it with Marshal.load instead
* of walking it natively. */
bool marshaled;
/* The off-heap (xmalloc) courier of a basket_type_move. A move basket does
* not use v. */
struct rb_ractor_move_courier *move_courier;
/* Every node of a native copy snapshot, collected while building it (raw
* malloc). The global GC's re-pin walks this list, since traversing the graph
* in-GC would need generic-ivar lookups. NULL: only the root (p.v) is pinned. */
VALUE *pinned;
size_t pinned_cnt;
} p; // payload
struct ccan_list_node node;
};
#if 0
static inline bool
ractor_basket_type_p(const struct ractor_basket *b, enum ractor_basket_type type)
{
return b->type == type;
}
static inline bool
ractor_basket_none_p(const struct ractor_basket *b)
{
return ractor_basket_type_p(b, basket_type_none);
}
#endif
static void
ractor_basket_mark(const struct ractor_basket *b)
{
/* A move courier lives off-heap, and the shareable REFs it carries are marked and
* pinned as a global GC root by the in-flight registry (ractor.c). Nothing to do
* here. */
if (b->type != basket_type_move) {
rb_gc_mark(b->p.v);
}
}
static void
ractor_basket_free(struct ractor_basket *b)
{
/* A basket that dies before being enqueued clears the sender's re-pin slot; a
* free by the Ractor tearing the queue down does not match and is a no-op. */
rb_ractor_t *cr = rb_current_ractor_raw(false);
if (cr != NULL && cr->sending_basket == b) {
cr->sending_basket = NULL;
}
free(b->p.pinned);
b->p.pinned = NULL;
b->p.pinned_cnt = 0;
if (b->type == basket_type_move && b->p.move_courier) {
/* A move courier that was never consumed (a queue being torn down, say). */
rb_ractor_move_courier_free(b->p.move_courier);
b->p.move_courier = NULL;
}
SIZED_FREE(b);
}
static struct ractor_basket *
ractor_basket_alloc(void)
{
struct ractor_basket *b = ALLOC(struct ractor_basket);
return b;
}
// ractor-internal - ractor_queue
struct ractor_queue {
struct ccan_list_head set;
bool closed;
};
static void
ractor_queue_init(struct ractor_queue *rq)
{
ccan_list_head_init(&rq->set);
rq->closed = false;
}
static struct ractor_queue *
ractor_queue_new(void)
{
struct ractor_queue *rq = ALLOC(struct ractor_queue);
ractor_queue_init(rq);
return rq;
}
static void
ractor_queue_mark(const struct ractor_queue *rq)
{
const struct ractor_basket *b;
ccan_list_for_each(&rq->set, b, node) {
ractor_basket_mark(b);
}
}
static void
ractor_queue_free(struct ractor_queue *rq)
{
struct ractor_basket *b, *nxt;
ccan_list_for_each_safe(&rq->set, b, nxt, node) {
ccan_list_del_init(&b->node);
ractor_basket_free(b);
}
VM_ASSERT(ccan_list_empty(&rq->set));
SIZED_FREE(rq);
}
RBIMPL_ATTR_MAYBE_UNUSED()
static size_t
ractor_queue_size(const struct ractor_queue *rq)
{
size_t size = 0;
const struct ractor_basket *b;
ccan_list_for_each(&rq->set, b, node) {
size++;
}
return size;
}
static void
ractor_queue_close(struct ractor_queue *rq)
{
rq->closed = true;
}
static void
ractor_queue_move(struct ractor_queue *dst_rq, struct ractor_queue *src_rq)
{
struct ccan_list_head *src = &src_rq->set;
struct ccan_list_head *dst = &dst_rq->set;
dst->n.next = src->n.next;
dst->n.prev = src->n.prev;
dst->n.next->prev = &dst->n;
dst->n.prev->next = &dst->n;
ccan_list_head_init(src);
}
#if 0
static struct ractor_basket *
ractor_queue_head(rb_ractor_t *r, struct ractor_queue *rq)
{
return ccan_list_top(&rq->set, struct ractor_basket, node);
}
#endif
static bool
ractor_queue_empty_p(rb_ractor_t *r, const struct ractor_queue *rq)
{
return ccan_list_empty(&rq->set);
}
static struct ractor_basket *
ractor_queue_deq(rb_ractor_t *r, struct ractor_queue *rq)
{
VM_ASSERT(GET_RACTOR() == r);
return ccan_list_pop(&rq->set, struct ractor_basket, node);
}
static void
ractor_queue_enq(rb_ractor_t *r, struct ractor_queue *rq, struct ractor_basket *basket)
{
ccan_list_add_tail(&rq->set, &basket->node);
}
#if 0
static void
rq_dump(const struct ractor_queue *rq)
{
int i=0;
struct ractor_basket *b;
ccan_list_for_each(&rq->set, b, node) {
fprintf(stderr, "%d type:%s %p\n", i, basket_type_name(b->type), (void *)b);
i++;
}
}
#endif
static void ractor_delete_port(rb_ractor_t *cr, st_data_t id, bool locked);
static struct ractor_queue *
ractor_get_queue(rb_ractor_t *cr, st_data_t id, bool locked)
{
VM_ASSERT(cr == GET_RACTOR());
struct ractor_queue *rq;
if (cr->sync.ports && st_lookup(cr->sync.ports, id, (st_data_t *)&rq)) {
if (rq->closed && ractor_queue_empty_p(cr, rq)) {
ractor_delete_port(cr, id, locked);
return NULL;
}
else {
return rq;
}
}
else {
return NULL;
}
}
// ractor-internal - ports
static void
ractor_add_port(rb_ractor_t *r, st_data_t id)
{
struct ractor_queue *rq = ractor_queue_new();
ASSERT_ractor_unlocking(r);
RUBY_DEBUG_LOG("id:%u", (unsigned int)id);
RACTOR_LOCK(r);
{
st_insert(r->sync.ports, id, (st_data_t)rq);
}
RACTOR_UNLOCK(r);
}
static void
ractor_delete_port_locked(rb_ractor_t *cr, st_data_t id)
{
ASSERT_ractor_locking(cr);
RUBY_DEBUG_LOG("id:%u", (unsigned int)id);
struct ractor_queue *rq;
if (st_delete(cr->sync.ports, &id, (st_data_t *)&rq)) {
ractor_queue_free(rq);
}
else {
VM_ASSERT(0);
}
}
static void
ractor_delete_port(rb_ractor_t *cr, st_data_t id, bool locked)
{
if (locked) {
ractor_delete_port_locked(cr, id);
}
else {
RACTOR_LOCK_SELF(cr);
{
ractor_delete_port_locked(cr, id);
}
RACTOR_UNLOCK_SELF(cr);
}
}
static const struct ractor_port *
ractor_default_port(rb_ractor_t *r)
{
return RACTOR_PORT_PTR(r->sync.default_port_value);
}
static VALUE
ractor_default_port_value(rb_ractor_t *r)
{
return r->sync.default_port_value;
}
static bool
ractor_closed_port_p(rb_execution_context_t *ec, rb_ractor_t *r, const struct ractor_port *rp)
{
VM_ASSERT(rb_ec_ractor_ptr(ec) == rp->r ? 1 : (ASSERT_ractor_locking(rp->r), 1));
const struct ractor_queue *rq;
if (rp->r->sync.ports && st_lookup(rp->r->sync.ports, ractor_port_id(rp), (st_data_t *)&rq)) {
return rq->closed;
}
else {
return true;
}
}
static void ractor_deliver_incoming_messages(rb_execution_context_t *ec, rb_ractor_t *cr);
static bool ractor_queue_empty_p(rb_ractor_t *r, const struct ractor_queue *rq);
static bool
ractor_close_port(rb_execution_context_t *ec, rb_ractor_t *cr, const struct ractor_port *rp)
{
VM_ASSERT(cr == rp->r);
struct ractor_queue *rq = NULL;
RACTOR_LOCK_SELF(cr);
{
ractor_deliver_incoming_messages(ec, cr); // check incoming messages
if (st_lookup(rp->r->sync.ports, ractor_port_id(rp), (st_data_t *)&rq)) {
ractor_queue_close(rq);
if (ractor_queue_empty_p(cr, rq)) {
// delete from the table
ractor_delete_port(cr, ractor_port_id(rp), true);
}
// TODO: free rq
}
}
RACTOR_UNLOCK_SELF(cr);
return rq != NULL;
}
static int
ractor_free_all_ports_i(st_data_t port_id, st_data_t val, st_data_t dat)
{
struct ractor_queue *rq = (struct ractor_queue *)val;
// rb_ractor_t *cr = (rb_ractor_t *)dat;
ractor_queue_free(rq);
return ST_CONTINUE;
}
static void
ractor_free_all_ports(rb_ractor_t *cr)
{
if (cr->sync.ports) {
st_foreach(cr->sync.ports, ractor_free_all_ports_i, (st_data_t)cr);
st_free_table(cr->sync.ports);
cr->sync.ports = NULL;
}
if (cr->sync.recv_queue) {
ractor_queue_free(cr->sync.recv_queue);
cr->sync.recv_queue = NULL;
}
}
#if defined(HAVE_WORKING_FORK)
static void
ractor_sync_terminate_atfork(rb_vm_t *vm, rb_ractor_t *r)
{
ractor_free_all_ports(r);
r->sync.legacy = Qnil;
}
#endif
// Ractor#monitor
struct ractor_monitor {
struct ractor_port port;
struct ccan_list_node node;
};
/* Mark the Ractors monitoring r. ractor_notify_exit sends the exit token through each
* entry's port, so the monitoring Ractor's struct must outlive r, and its wrapper is
* what keeps it alive. */
static void
ractor_mark_monitors(rb_ractor_t *r)
{
const struct ractor_monitor *rm;
ccan_list_for_each(&r->sync.monitors, rm, node) {
rb_gc_mark(rm->port.r->pub.self);
}
}
static VALUE
ractor_exit_token(bool exc)
{
if (exc) {
RUBY_DEBUG_LOG("aborted");
return ID2SYM(idAborted);
}
else {
RUBY_DEBUG_LOG("exited");
return ID2SYM(idExited);
}
}
static VALUE
ractor_monitor(rb_execution_context_t *ec, VALUE self, VALUE port)
{
rb_ractor_t *r = RACTOR_PTR(self);
bool terminated = false;
const struct ractor_port *rp = ractor_port_ptr_check(port);
struct ractor_monitor *rm = ALLOC(struct ractor_monitor);
rm->port = *rp; // copy port information
RACTOR_LOCK(r);
{
if (UNDEF_P(r->sync.legacy)) { // not terminated
RUBY_DEBUG_LOG("OK/r:%u -> port:%u@r%u", (unsigned int)rb_ractor_id(r), (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
ccan_list_add_tail(&r->sync.monitors, &rm->node);
}
else {
RUBY_DEBUG_LOG("NG/r:%u -> port:%u@r%u", (unsigned int)rb_ractor_id(r), (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
terminated = true;
}
}
RACTOR_UNLOCK(r);
if (terminated) {
SIZED_FREE(rm);
ractor_port_send(ec, port, ractor_exit_token(r->sync.legacy_exc), Qfalse);
return Qfalse;
}
else {
return Qtrue;
}
}
static VALUE
ractor_unmonitor(rb_execution_context_t *ec, VALUE self, VALUE port)
{
rb_ractor_t *r = RACTOR_PTR(self);
const struct ractor_port *rp = ractor_port_ptr_check(port);
RACTOR_LOCK(r);
{
if (UNDEF_P(r->sync.legacy)) { // not terminated
struct ractor_monitor *rm, *nxt;
ccan_list_for_each_safe(&r->sync.monitors, rm, nxt, node) {
if (rm->port.r == rp->r && ractor_port_id(&rm->port) == ractor_port_id(rp)) {
RUBY_DEBUG_LOG("r:%u -> port:%u@r%u",
(unsigned int)rb_ractor_id(r),
(unsigned int)ractor_port_id(&rm->port),
(unsigned int)rb_ractor_id(rm->port.r));
ccan_list_del(&rm->node);
SIZED_FREE(rm);
}
}
}
}
RACTOR_UNLOCK(r);
return self;
}
static void
ractor_notify_exit(rb_execution_context_t *ec, rb_ractor_t *cr, VALUE legacy, bool exc)
{
RUBY_DEBUG_LOG("exc:%d", exc);
VM_ASSERT(!UNDEF_P(legacy));
VM_ASSERT(cr->sync.legacy == Qundef);
RACTOR_LOCK_SELF(cr);
{
ractor_free_all_ports(cr);
cr->sync.legacy = legacy;
cr->sync.legacy_exc = exc;
}
RACTOR_UNLOCK_SELF(cr);
// send token
VALUE token = ractor_exit_token(exc);
struct ractor_monitor *rm, *nxt;
ccan_list_for_each_safe(&cr->sync.monitors, rm, nxt, node)
{
RUBY_DEBUG_LOG("port:%u@r%u", (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
ractor_try_send(ec, &rm->port, token, false);
ccan_list_del(&rm->node);
SIZED_FREE(rm);
}
VM_ASSERT(ccan_list_empty(&cr->sync.monitors));
}
// ractor-internal - initialize, mark, free, memsize
static int
ractor_mark_ports_i(st_data_t key, st_data_t val, st_data_t data)
{
// id -> ractor_queue
const struct ractor_queue *rq = (struct ractor_queue *)val;
ractor_queue_mark(rq);
return ST_CONTINUE;
}
static void
ractor_sync_mark(rb_ractor_t *r)
{
/* The owner rewrites the queues, the port table and the monitor list under its sync
* lock, so only the owner itself or the stopped world may walk them. */
const bool world_stopped = rb_gc_during_global_gc_p();
VM_ASSERT(world_stopped || r == rb_current_ractor_raw(false));
rb_gc_mark(r->sync.default_port_value);
/* (A copy snapshot being materialized is not marked here: each EC's frame
* chain roots it in rb_execution_context_mark, which also re-pins it.) */
/* Until the value is absorbed this is its only reliable root (Qundef while the
* Ractor still runs); after Ractor#value returns it, the Ruby side roots it. */
rb_gc_mark(r->sync.legacy);
/* ractor_sync_init builds the rest, and a root scan reaches the main Ractor before
* that: ports is what tells the two apart (the lock and the list heads are still
* zeroed, and walking those crashes). Lock out foreign senders while walking them
* (self-lock: not recursive, and a held Ractor lock disables malloc-GC, so no GC
* nests); a stopped world needs no lock. */
if (r->sync.ports) {
if (!world_stopped) RACTOR_LOCK_SELF(r);
{
ractor_queue_mark(r->sync.recv_queue);
st_foreach(r->sync.ports, ractor_mark_ports_i, 0);
ractor_mark_monitors(r);
}
if (!world_stopped) RACTOR_UNLOCK_SELF(r);
}
}
/* Re-pin a copy basket's payload: the root and every collected node. */
static void
ractor_basket_repin_in_flight(const struct ractor_basket *b)
{
if (b->type != basket_type_copy) return;
rb_gc_pin_in_flight_message(b->p.v);
for (size_t i = 0; i < b->p.pinned_cnt; i++) {
rb_gc_pin_in_flight_message(b->p.pinned[i]);
}
}
static void
ractor_queue_repin_in_flight(const struct ractor_queue *rq)
{
const struct ractor_basket *b;
ccan_list_for_each(&rq->set, b, node) {
/* A move basket carries an off-heap courier, so it has no shref to re-pin;
* ractor_basket_mark marks the shareable VALUEs it carries instead. */
ractor_basket_repin_in_flight(b);
}
}
static int
ractor_repin_ports_i(st_data_t key, st_data_t val, st_data_t data)
{
ractor_queue_repin_in_flight((struct ractor_queue *)val);
return ST_CONTINUE;
}
/* A global GC clears every shref bit, so all in-flight payloads have to be re-pinned
* before the unified mark. Runs on the driver, under the barrier. */
void
rb_ractor_repin_in_flight(rb_ractor_t *r)
{
if (r->sync.ports) {
ractor_queue_repin_in_flight(r->sync.recv_queue);
st_foreach(r->sync.ports, ractor_repin_ports_i, 0);
}
/* Baskets already built but not enqueued yet (in flight on the send path). */
if (r->sending_basket != NULL) {
ractor_basket_repin_in_flight(r->sending_basket);
}
/* A snapshot still being built (from prepare_payload's walk until it moves into
* the basket). */
for (size_t i = 0; i < r->pin_capture_cnt; i++) {
rb_gc_pin_in_flight_message(r->pin_capture[i]);
}
/* Snapshots being materialized are re-pinned from the EC frame chains instead
* (rb_execution_context_mark, which also covers a suspended fiber's EC). */
}
/* A single-objspace impl (mmtk) has no pin or shref bits and no zombie_objspaces, so
* plain marking from the wrapper keeps these alive; the default GC covers the same set
* with its pins and its zombie scan. */
void
rb_ractor_mark_in_flight_for_single_objspace(rb_ractor_t *r)
{
rb_gc_mark(r->sync.legacy);
if (r->sending_basket != NULL) {
ractor_basket_mark(r->sending_basket);
}
for (size_t i = 0; i < r->pin_capture_cnt; i++) {
rb_gc_mark(r->pin_capture[i]);
}
}
static int
ractor_sync_free_ports_i(st_data_t _key, st_data_t val, st_data_t _args)
{
struct ractor_queue *queue = (struct ractor_queue *)val;
ractor_queue_free(queue);
return ST_CONTINUE;
}
static void
ractor_sync_free(rb_ractor_t *r)
{
if (r->sync.recv_queue) {
ractor_queue_free(r->sync.recv_queue);
}
// maybe NULL
if (r->sync.ports) {
st_foreach(r->sync.ports, ractor_sync_free_ports_i, 0);
st_free_table(r->sync.ports);
r->sync.ports = NULL;
}
}
static size_t
ractor_sync_memsize(const rb_ractor_t *r)
{
if (r->sync.ports) {
return st_memsize(r->sync.ports);
}
else {
return 0;
}
}
static void
ractor_sync_init(rb_ractor_t *r)
{
// lock
rb_native_mutex_initialize(&r->sync.lock);
// monitors
ccan_list_head_init(&r->sync.monitors);
// waiters
ccan_list_head_init(&r->sync.waiters);
// receiving queue
r->sync.recv_queue = ractor_queue_new();
// ports
r->sync.ports = st_init_numtable();
/* ractor_setup_default_port creates it only after the Ractor joins
* vm->ractor.set, so a global GC cannot free the rootless port in between. */
r->sync.default_port_value = Qfalse;
// legacy
r->sync.legacy = Qundef;
// no receive is rebuilding a payload yet
r->sync.materializing_copies = 0;
#ifndef RUBY_THREAD_PTHREAD_H
rb_native_cond_initialize(&r->sync.wakeup_cond);
#endif
}
/* Create the default port. Call only after the Ractor joined vm->ractor.set, so the
* root scan can mark the shareable port from creation onwards. */
void
rb_ractor_setup_default_port(rb_ractor_t *r)
{
VM_ASSERT(r->sync.default_port_value == Qfalse);
r->sync.default_port_value = ractor_port_new(r);
FL_SET_RAW(r->sync.default_port_value, RUBY_FL_SHAREABLE); // only default ports are shareable
rb_gc_obj_became_shareable(r->sync.default_port_value);
}
// Ractor#value
static rb_ractor_t *
ractor_set_successor_once(rb_ractor_t *r, rb_ractor_t *cr)
{
if (r->sync.successor == NULL) {
rb_ractor_t *successor = ATOMIC_PTR_CAS(r->sync.successor, NULL, cr);
return successor == NULL ? cr : successor;
}
return r->sync.successor;
}
static VALUE
ractor_make_remote_exception(VALUE cause, VALUE sender)
{
VALUE err = rb_exc_new_cstr(rb_eRactorRemoteError, "thrown by remote Ractor.");
rb_ivar_set(err, rb_intern("@ractor"), sender);
rb_ec_setup_exception(NULL, err, cause);
return err;
}
static VALUE
ractor_value(rb_execution_context_t *ec, VALUE self)
{
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
rb_ractor_t *r = RACTOR_PTR(self);
rb_ractor_t *sr = ractor_set_successor_once(r, cr);
if (sr == cr) {
if (r->sync.legacy_taken) {
rb_raise(rb_eRactorError, "The value was already taken");
}
/* The value is returned by reference: inherit the dead Ractor's objspace first,
* making it our own object (containment without a copy). Wait for
* ractor_terminated: a monitor-port wakeup arrives before the dying thread
* finishes teardown (vm_remove_ractor still touches the objspace). */
while (!rb_ractor_status_p(r, ractor_terminated)) {
rb_thread_schedule();
}
/* The wait above yields the GVL, so another thread of this Ractor can take the
* value first: re-check. */
if (r->sync.legacy_taken) {
rb_raise(rb_eRactorError, "The value was already taken");
}
/* Move r's rb_gc_register_mark_object pins to the joiner before the merge
* below sweeps r's objspace, or the objects pinned there lose their root. */
rb_ractor_absorb_registered_marks(GET_RACTOR(), r);
rb_gc_objspace_absorb_into_current(&r->objspace);
/* Keep legacy alive in a C local until it is returned: after the absorb only
* the C struct reaches it, so let the conservative machine-stack mark find it. */
volatile VALUE legacy_keep = r->sync.legacy;
/* A dead Ractor's local storage is unreachable from Ruby (Ractor#[] only works
* from inside), so let the values die and keep ractor_mark and ractor_free from
* walking a stale table later. */
ractor_local_storage_free(r);
r->local_storage = NULL;
r->idkey_local_storage = NULL;
/* The value is returned to the caller and rooted from Ruby afterwards. Drop it
* from the C struct: keeping it would leave a C-only reference into the
* successor's objspace, needing marking and a pin against compaction. */
VALUE legacy = r->sync.legacy;
r->sync.legacy = Qnil;
r->sync.legacy_taken = true;
RB_GC_GUARD(legacy_keep);
if (r->sync.legacy_exc) {
rb_exc_raise(ractor_make_remote_exception(legacy, self));
}
return legacy;
}
else {
rb_raise(rb_eRactorError, "Only the successor ractor can take a value");
}
}
static VALUE ractor_copy_native_try(VALUE obj); // in ractor.c
static VALUE
ractor_marshal_dump_body(VALUE obj)
{
return rb_marshal_dump(obj, Qnil);
}
static VALUE
ractor_marshal_dump_rescue(VALUE obj, VALUE errinfo)
{
rb_raise(rb_eRactorError, "can not copy %"PRIsVALUE" object.", rb_class_of(obj));
UNREACHABLE_RETURN(Qnil);
}
static VALUE
ractor_prepare_payload(rb_execution_context_t *ec, VALUE obj, enum ractor_basket_type *ptype, bool *pmarshaled)
{
switch (*ptype) {
case basket_type_ref:
return obj;
default:
if (rb_ractor_shareable_p(obj)) {
*ptype = basket_type_ref;
return obj;
}
else {
/* Snapshot the object on the sender side without calling the user-visible
* #clone: core types are deep-copied natively and anything else is
* marshaled here, so its user hooks run on the sender. */
*ptype = basket_type_copy;
/* During a native copy, copy_enter collects every snapshot node into the
* pin list that covers construction, enqueue and materialization. */
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
VM_ASSERT(!cr->gen_fields_capturing);
cr->gen_fields_capturing = true;
VALUE snapshot = Qundef;
/* A native copy can raise (allocation, async interrupt). Leaving the
* capturing flag set would fail the next send's assert and leak a stale
* pin_capture list into that basket. */
enum ruby_tag_type state;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
snapshot = ractor_copy_native_try(obj);
}
EC_POP_TAG();
cr->gen_fields_capturing = false;
if (state != TAG_NONE) {
cr->pin_capture_cnt = 0;
EC_JUMP_TAG(ec, state);
}
if (UNDEF_P(snapshot)) {
cr->pin_capture_cnt = 0;
snapshot = rb_rescue2(ractor_marshal_dump_body, obj,
ractor_marshal_dump_rescue, obj,
rb_eTypeError, (VALUE)0);
*pmarshaled = true;
}
return snapshot;
}
}
}
static struct ractor_basket *
ractor_basket_new(rb_execution_context_t *ec, VALUE obj, enum ractor_basket_type type, bool exc)
{
/* A copy payload's preparation can raise (an uncopyable object), so it runs before
* the basket is allocated and cannot leak one; the move branch allocates first,
* since an alloc raise must not orphan an already built courier. */
VALUE v = Qfalse;
bool marshaled = false;
struct rb_ractor_move_courier *courier = NULL;
struct ractor_basket *b;
if (type == basket_type_move) {
/* Allocate the basket first: its xmalloc can raise NoMemoryError, and a courier
* already built (sources destroyed, registry entry live) would be orphaned. */
b = ractor_basket_alloc();
enum ruby_tag_type state;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
/* Serialize the graph into an off-heap courier; the sources become
* RactorMovedObject. While in flight there is no GC object left for the
* sender's GC to mark, sweep or move. */
courier = rb_ractor_move_courier_build(obj);
}
EC_POP_TAG();
if (state != TAG_NONE) {
SIZED_FREE(b);
EC_JUMP_TAG(ec, state);
}
}
else {
v = ractor_prepare_payload(ec, obj, &type, &marshaled);
enum ruby_tag_type state;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
b = ractor_basket_alloc();
}
EC_POP_TAG();
if (state != TAG_NONE) {
/* Drop the pin list, or every global GC re-pins the dead snapshot from it
* forever (rb_ractor_repin_in_flight walks it unconditionally). The nodes
* stay shref-pinned only until the next global GC clears the bits. */
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
free(cr->pin_capture);
cr->pin_capture = NULL;
cr->pin_capture_cnt = cr->pin_capture_capa = 0;
EC_JUMP_TAG(ec, state);
}
/* copy_enter pinned every node at construction with cr->pin_capture as the
* re-pin source; hand it to the basket only after basket_alloc (which may GC)
* so the cover never lapses. A marshaled String is pinned here, after the
* alloc, so an alloc raise leaves no stale pin. */
if (type == basket_type_copy && marshaled) {
rb_gc_pin_in_flight_message(v);
}
}
b->type = type;
b->p.exception = exc;
b->p.v = v;
b->p.marshaled = marshaled;
b->p.move_courier = courier;
b->p.pinned = NULL;
b->p.pinned_cnt = 0;
if (type == basket_type_copy) {
/* Hand the pin list to the basket, moving the re-pin cover from
* cr->pin_capture to cr->sending_basket with no safepoint in between. */
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
b->p.pinned = cr->pin_capture;
b->p.pinned_cnt = cr->pin_capture_cnt;
VM_ASSERT(cr->sending_basket == NULL);
cr->sending_basket = b;
cr->pin_capture = NULL;
cr->pin_capture_cnt = cr->pin_capture_capa = 0;
}
return b;
}
/* True while this Ractor materializes an arriving copy: the half-built result
* legitimately points at the sender-resident (pinned) snapshot, so a local GC's
* verifier must not report containment violations, and the copy's own allocations can
* start that GC. */
bool
rb_ractor_materializing_p(void)
{
const rb_ractor_t *cr = rb_current_ractor_raw(false);
if (cr == NULL) return false;
/* Only a COPY materialization sets this: move shells reference other shells in
* this objspace, never the sender's graph. The count is per Ractor, so a fiber
* switch keeps it exact. */
return cr->sync.materializing_copies > 0;
}
static VALUE
ractor_basket_value(struct ractor_basket *b)
{
switch (b->type) {
case basket_type_ref:
break;
case basket_type_copy: {
/* Materialize the sender's snapshot into the receiving Ractor's objspace.
* Passing the sender-resident graph by reference would create an unshareable
* cross-objspace edge that neither local GC can follow. The snapshot stays
* pinned in the sender's objspace and becomes garbage there once this copy
* finishes. Marshal.load allocates through this Ractor's normal newobj and
* write-barrier paths.
*
* Rebuilding can raise (marshal load hooks and autoload run user code and an
* async interrupt can arrive anywhere), and those hooks can run a nested
* Ractor.receive. The frame is pushed on the machine stack and popped under a
* TAG, so the chain never leaks a dead materialization or drops an outer one. */
rb_execution_context_t *ec = rb_current_ec_noinline();
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
struct ractor_materialize_frame frame = {
.snapshot = b->p.v, .pinned = b->p.pinned, .pinned_cnt = b->p.pinned_cnt,
.prev = ec->materialize_frames,
};
ec->materialize_frames = &frame;
cr->sync.materializing_copies++;
VALUE result = Qundef;
enum ruby_tag_type state;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
if (b->p.marshaled) {
result = rb_marshal_load(b->p.v);
}
else {
result = ractor_copy_native_try(b->p.v);
if (UNDEF_P(result)) rb_bug("ractor_basket_value: native snapshot not natively copyable");
}
}
EC_POP_TAG();
ec->materialize_frames = frame.prev;
cr->sync.materializing_copies--;
/* rb_copy_generic_ivar left the sender-resident snapshot host and fields_obj in
* this EC's gen_fields_cache; the snapshot is garbage on the sender now, and a
* stale cache hit on a reused address would deref a freed foreign fields_obj.
* Invalidate (the raise path resets it the same way). */
ec->gen_fields_cache.obj = Qundef;
ec->gen_fields_cache.fields_obj = Qundef;
if (state != TAG_NONE) {
/* The basket left the queue and has no other owner, and a raise skips
* accept, so free it here before propagating. */
ractor_basket_free(b);
EC_JUMP_TAG(ec, state);
}
/* keep rooting result from the stack after the frame is popped */
b->p.v = result;
RB_GC_GUARD(result);
break;
}
case basket_type_move: {
/* Rebuild the moved graph from the off-heap courier into this Ractor's
* objspace. The sources are already RactorMovedObject (set when the courier
* was built), so move's snapshot semantics hold. The courier is xmalloc'd
* rather than a GC object, so the sender's concurrent local GC never touches
* it; the VALUEs it carries are shareable or immediates, marked and pinned as
* a global GC root by the in-flight registry (ractor.c).
*
* Rebuilding can raise here too (rb_hash_aset on a moved key with a custom
* #hash runs user code, and an async interrupt can arrive). On a raise the
* courier is still owned by the basket, whose teardown frees it. */
rb_execution_context_t *ec = rb_current_ec_noinline();
struct rb_ractor_move_courier *courier = b->p.move_courier;
/* Keep the materialized graph on the machine stack (result): it is the only
* root until it reaches the caller. courier_free below runs a long loop, and
* only the malloc'd basket's p.v holding it would give a concurrent global GC a
* wide window. */
VALUE result = Qundef;
enum ruby_tag_type state;
EC_PUSH_TAG(ec);
if ((state = EC_EXEC_TAG()) == TAG_NONE) {
result = rb_ractor_move_courier_materialize(courier);
}
EC_POP_TAG();
if (state != TAG_NONE) {
/* An unconsumed courier stays in b->p.move_courier; basket_free frees it. */
ractor_basket_free(b);
EC_JUMP_TAG(ec, state);
}
rb_ractor_move_courier_free(courier);
b->p.move_courier = NULL;
b->p.v = result;
RB_GC_GUARD(result);
break;
}
default:
VM_ASSERT(0); // unreachable
}
VM_ASSERT(!RB_TYPE_P(b->p.v, T_NONE));
return b->p.v;
}
static VALUE
ractor_basket_accept(struct ractor_basket *b)
{
VALUE v = ractor_basket_value(b);
if (b->p.exception) {
VALUE err = ractor_make_remote_exception(v, b->sender);
ractor_basket_free(b);
rb_exc_raise(err);
}
ractor_basket_free(b);
return v;
}
// Ractor blocking by receive
#if VM_CHECK_MODE > 0
static bool
ractor_waiter_included(rb_ractor_t *cr, rb_thread_t *th)
{
ASSERT_ractor_locking(cr);
struct ractor_waiter *w;
ccan_list_for_each(&cr->sync.waiters, w, node) {
if (w->th == th) {
return true;
}
}
return false;
}
#endif
#if USE_RUBY_DEBUG_LOG
static const char *
wakeup_status_str(enum ractor_wakeup_status wakeup_status)
{
switch (wakeup_status) {
case wakeup_none: return "none";
case wakeup_by_send: return "by_send";
case wakeup_by_interrupt: return "by_interrupt";
// case wakeup_by_close: return "by_close";
}
rb_bug("unreachable");
}
static const char *
basket_type_name(enum ractor_basket_type type)
{
switch (type) {
case basket_type_none: return "none";
case basket_type_ref: return "ref";
case basket_type_copy: return "copy";
case basket_type_move: return "move";
}
VM_ASSERT(0);
return NULL;
}
#endif // USE_RUBY_DEBUG_LOG
#ifdef RUBY_THREAD_PTHREAD_H
//
#else // win32
static void
ractor_cond_wait(rb_ractor_t *r)
{
#if RACTOR_CHECK_MODE > 0
VALUE locked_by = r->sync.locked_by;
r->sync.locked_by = Qnil;
#endif
rb_native_cond_wait(&r->sync.wakeup_cond, &r->sync.lock);
#if RACTOR_CHECK_MODE > 0
r->sync.locked_by = locked_by;
#endif
}
static void *
ractor_wait_no_gvl(void *ptr)
{
struct ractor_waiter *waiter = (struct ractor_waiter *)ptr;
rb_ractor_t *cr = waiter->th->ractor;
RACTOR_LOCK_SELF(cr);
{
if (waiter->wakeup_status == wakeup_none) {
ractor_cond_wait(cr);
}
}
RACTOR_UNLOCK_SELF(cr);
return NULL;
}
static void
rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_function_t *ubf, void *ptr)
{
struct ractor_waiter *waiter = (struct ractor_waiter *)ptr;
RACTOR_UNLOCK(cr);
{
rb_nogvl(ractor_wait_no_gvl, waiter,
ubf, waiter,
RB_NOGVL_UBF_ASYNC_SAFE | RB_NOGVL_INTR_FAIL);
}
RACTOR_LOCK(cr);
}
static void
rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *th)
{
// ractor lock is acquired
rb_native_cond_broadcast(&r->sync.wakeup_cond);
}
#endif
static bool
ractor_wakeup_all(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status)
{
ASSERT_ractor_unlocking(r);
RUBY_DEBUG_LOG("r:%u wakeup:%s", rb_ractor_id(r), wakeup_status_str(wakeup_status));
bool wakeup_p = false;
RACTOR_LOCK(r);
while (1) {
struct ractor_waiter *waiter = ccan_list_pop(&r->sync.waiters, struct ractor_waiter, node);
if (waiter) {
VM_ASSERT(waiter->wakeup_status == wakeup_none);
waiter->wakeup_status = wakeup_status;
rb_ractor_sched_wakeup(r, waiter->th);
wakeup_p = true;
}
else {
break;
}
}
RACTOR_UNLOCK(r);
return wakeup_p;
}
static void
ubf_ractor_wait(void *ptr)
{
struct ractor_waiter *waiter = (struct ractor_waiter *)ptr;
rb_thread_t *th = waiter->th;
rb_ractor_t *r = th->ractor;
rb_atomic_t event_serial = waiter->event_serial;
// clear ubf and nobody can kick UBF
th->unblock.func = NULL;
th->unblock.arg = NULL;
rb_native_mutex_unlock(&th->interrupt_lock);
{
RACTOR_LOCK(r);
{
if (RUBY_ATOMIC_LOAD(th->unblock.event_serial) == event_serial && waiter->wakeup_status == wakeup_none) {
RUBY_DEBUG_LOG("waiter:%p", (void *)waiter);
waiter->wakeup_status = wakeup_by_interrupt;
ccan_list_del(&waiter->node);
rb_ractor_sched_wakeup(r, waiter->th);
}
}
RACTOR_UNLOCK(r);
}
rb_native_mutex_lock(&th->interrupt_lock);
}
static enum ractor_wakeup_status
ractor_wait(rb_execution_context_t *ec, rb_ractor_t *cr)
{
rb_thread_t *th = rb_ec_thread_ptr(ec);
struct ractor_waiter waiter = {
.wakeup_status = wakeup_none,
.th = th,
};
RUBY_DEBUG_LOG("wait%s", "");
ASSERT_ractor_locking(cr);
VM_ASSERT(GET_RACTOR() == cr);
VM_ASSERT(!ractor_waiter_included(cr, th));
ccan_list_add_tail(&cr->sync.waiters, &waiter.node);
// resume another ready thread and wait for an event
rb_ractor_sched_wait(ec, cr, ubf_ractor_wait, &waiter);
if (waiter.wakeup_status == wakeup_none) {
ccan_list_del(&waiter.node);
}
RUBY_DEBUG_LOG("wakeup_status:%s", wakeup_status_str(waiter.wakeup_status));
RACTOR_UNLOCK_SELF(cr);
{
rb_ec_check_ints(ec);
}
RACTOR_LOCK_SELF(cr);
VM_ASSERT(!ractor_waiter_included(cr, th));
return waiter.wakeup_status;
}
static void
ractor_deliver_incoming_messages(rb_execution_context_t *ec, rb_ractor_t *cr)
{
ASSERT_ractor_locking(cr);
struct ractor_queue *recv_q = cr->sync.recv_queue;
struct ractor_basket *b;
while ((b = ractor_queue_deq(cr, recv_q)) != NULL) {
ractor_queue_enq(cr, ractor_get_queue(cr, b->port_id, true), b);
}
}
static bool
ractor_check_received(rb_ractor_t *cr, struct ractor_queue *messages)
{
struct ractor_queue *received_queue = cr->sync.recv_queue;
bool received = false;
ASSERT_ractor_locking(cr);
if (ractor_queue_empty_p(cr, received_queue)) {
RUBY_DEBUG_LOG("empty");
}
else {
received = true;
// messages <- incoming
ractor_queue_init(messages);
ractor_queue_move(messages, received_queue);
}
VM_ASSERT(ractor_queue_empty_p(cr, received_queue));
RUBY_DEBUG_LOG("received:%d", received);
return received;
}
static void
ractor_wait_receive(rb_execution_context_t *ec, rb_ractor_t *cr)
{
struct ractor_queue messages;
bool deliverred = false;
RACTOR_LOCK_SELF(cr);
{
if (ractor_check_received(cr, &messages)) {
deliverred = true;
}
else {
ractor_wait(ec, cr);
}
}
RACTOR_UNLOCK_SELF(cr);
if (deliverred) {
VM_ASSERT(!ractor_queue_empty_p(cr, &messages));
struct ractor_basket *b;
while ((b = ractor_queue_deq(cr, &messages)) != NULL) {
ractor_queue_enq(cr, ractor_get_queue(cr, b->port_id, false), b);
}
}
}
static VALUE
ractor_try_receive(rb_execution_context_t *ec, rb_ractor_t *cr, const struct ractor_port *rp)
{
struct ractor_queue *rq = ractor_get_queue(cr, ractor_port_id(rp), false);
if (rq == NULL) {
rb_raise(rb_eRactorClosedError, "The port was already closed");
}
struct ractor_basket *b = ractor_queue_deq(cr, rq);
if (rq->closed && ractor_queue_empty_p(cr, rq)) {
ractor_delete_port(cr, ractor_port_id(rp), false);
}
if (b) {
return ractor_basket_accept(b);
}
else {
return Qundef;
}
}
static VALUE
ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp)
{
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
VM_ASSERT(cr == rp->r);
RUBY_DEBUG_LOG("port:%u", (unsigned int)ractor_port_id(rp));
while (1) {
VALUE v = ractor_try_receive(ec, cr, rp);
if (v != Qundef) {
return v;
}
else {
ractor_wait_receive(ec, cr);
}
}
}
// Ractor#send
static void
ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, struct ractor_basket *b, bool raise_on_error)
{
bool closed = false;
RUBY_DEBUG_LOG("port:%u@r%u b:%s v:%p", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r), basket_type_name(b->type), (void *)b->p.v);
RACTOR_LOCK(rp->r);
{
if (ractor_closed_port_p(ec, rp->r, rp)) {
closed = true;
}
else {
b->port_id = ractor_port_id(rp);
ractor_queue_enq(rp->r, rp->r->sync.recv_queue, b);
/* From basket_new to the enqueue the sender's sending_basket slot covers
* the re-pin; from here the queue walk does, so drop the slot (no safepoint
* or malloc-triggered GC inside the lock, so the cover never lapses). */
if (b->type == basket_type_copy) {
rb_ractor_t *scr = rb_current_ractor_raw(false);
if (scr != NULL && scr->sending_basket == b) {
scr->sending_basket = NULL;
}
}
}
}
RACTOR_UNLOCK(rp->r);
// NOTE: ref r -> b->p.v is created, but Ractor is unprotected object, so no problem on that.
if (!closed) {
ractor_wakeup_all(rp->r, wakeup_by_send);
}
else {
RUBY_DEBUG_LOG("closed:%u@r%u", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r));
if (raise_on_error) {
ractor_basket_free(b);
rb_raise(rb_eRactorClosedError, "The port was already closed");
}
}
}
static VALUE
ractor_send0(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move, bool raise_on_error)
{
struct ractor_basket *b = ractor_basket_new(ec, obj, RTEST(move) ? basket_type_move : basket_type_none, false);
ractor_send_basket(ec, rp, b, raise_on_error);
RB_GC_GUARD(obj);
return rp->r->pub.self;
}
static VALUE
ractor_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move)
{
return ractor_send0(ec, rp, obj, move, true);
}
static VALUE
ractor_try_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move)
{
return ractor_send0(ec, rp, obj, move, false);
}
// Ractor::Selector
struct ractor_selector {
struct st_table *ports; // rpv -> rp
};
static int
ractor_selector_mark_i(st_data_t key, st_data_t val, st_data_t dmy)
{
rb_gc_mark((VALUE)key); // rpv
return ST_CONTINUE;
}
static void
ractor_selector_mark(void *ptr)
{
struct ractor_selector *s = ptr;
if (s->ports) {
st_foreach(s->ports, ractor_selector_mark_i, 0);
}
}
static void
ractor_selector_free(void *ptr)
{
struct ractor_selector *s = ptr;
st_free_table(s->ports);
SIZED_FREE(s);
}
static size_t
ractor_selector_memsize(const void *ptr)
{
const struct ractor_selector *s = ptr;
size_t size = sizeof(struct ractor_selector);
if (s->ports) {
size += st_memsize(s->ports);
}
return size;
}
static const rb_data_type_t ractor_selector_data_type = {
"ractor/selector",
{
ractor_selector_mark,
ractor_selector_free,
ractor_selector_memsize,
NULL, // update
},
0, 0, RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED,
};
static struct ractor_selector *
RACTOR_SELECTOR_PTR(VALUE selv)
{
VM_ASSERT(rb_typeddata_is_kind_of(selv, &ractor_selector_data_type));
return (struct ractor_selector *)DATA_PTR(selv);
}
// Ractor::Selector.new
static VALUE
ractor_selector_create(VALUE klass)
{
struct ractor_selector *s;
VALUE selv = TypedData_Make_Struct(klass, struct ractor_selector, &ractor_selector_data_type, s);
s->ports = st_init_numtable(); // TODO
return selv;
}
// Ractor::Selector#add(r)
/*
* call-seq:
* add(ractor) -> ractor
*
* Adds _ractor_ to +self+. Raises an exception if _ractor_ is already added.
* Returns _ractor_.
*/
static VALUE
ractor_selector_add(VALUE selv, VALUE rpv)
{
if (!ractor_port_p(rpv)) {
rb_raise(rb_eArgError, "Not a Ractor::Port object");
}
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
const struct ractor_port *rp = ractor_port_ptr_check(rpv);
if (st_lookup(s->ports, (st_data_t)rpv, NULL)) {
rb_raise(rb_eArgError, "already added");
}
st_insert(s->ports, (st_data_t)rpv, (st_data_t)rp);
RB_OBJ_WRITTEN(selv, Qundef, rpv);
return selv;
}
// Ractor::Selector#remove(r)
/* call-seq:
* remove(ractor) -> ractor
*
* Removes _ractor_ from +self+. Raises an exception if _ractor_ is not added.
* Returns the removed _ractor_.
*/
static VALUE
ractor_selector_remove(VALUE selv, VALUE rpv)
{
if (!ractor_port_p(rpv)) {
rb_raise(rb_eArgError, "Not a Ractor::Port object");
}
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
if (!st_lookup(s->ports, (st_data_t)rpv, NULL)) {
rb_raise(rb_eArgError, "not added yet");
}
st_delete(s->ports, (st_data_t *)&rpv, NULL);
return selv;
}
// Ractor::Selector#clear
/*
* call-seq:
* clear -> self
*
* Removes all ractors from +self+. Raises +self+.
*/
static VALUE
ractor_selector_clear(VALUE selv)
{
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
st_clear(s->ports);
return selv;
}
/*
* call-seq:
* empty? -> true or false
*
* Returns +true+ if no ractor is added.
*/
static VALUE
ractor_selector_empty_p(VALUE selv)
{
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selv);
return s->ports->num_entries == 0 ? Qtrue : Qfalse;
}
// Ractor::Selector#wait
struct ractor_selector_wait_data {
rb_ractor_t *cr;
rb_execution_context_t *ec;
bool found;
VALUE v;
VALUE rpv;
};
static int
ractor_selector_wait_i(st_data_t key, st_data_t val, st_data_t data)
{
struct ractor_selector_wait_data *p = (struct ractor_selector_wait_data *)data;
const struct ractor_port *rp = (const struct ractor_port *)val;
VALUE v = ractor_try_receive(p->ec, p->cr, rp);
if (v != Qundef) {
p->found = true;
p->v = v;
p->rpv = (VALUE)key;
return ST_STOP;
}
else {
return ST_CONTINUE;
}
}
static VALUE
ractor_selector__wait(rb_execution_context_t *ec, VALUE selector)
{
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selector);
struct ractor_selector_wait_data data = {
.ec = ec,
.cr = cr,
.found = false,
};
while (1) {
st_foreach(s->ports, ractor_selector_wait_i, (st_data_t)&data);
if (data.found) {
return rb_ary_new_from_args(2, data.rpv, data.v);
}
ractor_wait_receive(ec, cr);
}
}
/*
* call-seq:
* wait(receive: false, yield_value: undef, move: false) -> [ractor, value]
*
* Waits until any ractor in _selector_ can be active.
*/
static VALUE
ractor_selector_wait(VALUE selector)
{
return ractor_selector__wait(GET_EC(), selector);
}
static VALUE
ractor_selector_new(int argc, VALUE *ractors, VALUE klass)
{
VALUE selector = ractor_selector_create(klass);
for (int i=0; i<argc; i++) {
ractor_selector_add(selector, ractors[i]);
}
return selector;
}
static VALUE
ractor_select_internal(rb_execution_context_t *ec, VALUE self, VALUE ports)
{
VALUE selector = ractor_selector_new(RARRAY_LENINT(ports), (VALUE *)RARRAY_CONST_PTR(ports), rb_cRactorSelector);
VALUE result = ractor_selector__wait(ec, selector);
RB_GC_GUARD(selector);
RB_GC_GUARD(ports);
return result;
}
#ifndef USE_RACTOR_SELECTOR
#define USE_RACTOR_SELECTOR 0
#endif
RUBY_SYMBOL_EXPORT_BEGIN
void rb_init_ractor_selector(void);
RUBY_SYMBOL_EXPORT_END
/*
* Document-class: Ractor::Selector
* :nodoc: currently
*
* Selects multiple Ractors to be activated.
*/
void
rb_init_ractor_selector(void)
{
rb_cRactorSelector = rb_define_class_under(rb_cRactor, "Selector", rb_cObject);
rb_undef_alloc_func(rb_cRactorSelector);
rb_define_singleton_method(rb_cRactorSelector, "new", ractor_selector_new , -1);
rb_define_method(rb_cRactorSelector, "add", ractor_selector_add, 1);
rb_define_method(rb_cRactorSelector, "remove", ractor_selector_remove, 1);
rb_define_method(rb_cRactorSelector, "clear", ractor_selector_clear, 0);
rb_define_method(rb_cRactorSelector, "empty?", ractor_selector_empty_p, 0);
rb_define_method(rb_cRactorSelector, "wait", ractor_selector_wait, 0);
}
static void
Init_RactorPort(void)
{
rb_cRactorPort = rb_define_class_under(rb_cRactor, "Port", rb_cObject);
rb_define_alloc_func(rb_cRactorPort, ractor_port_alloc);
rb_define_method(rb_cRactorPort, "initialize", ractor_port_initialize, 0);
rb_define_method(rb_cRactorPort, "initialize_copy", ractor_port_initialize_copy, 1);
#if USE_RACTOR_SELECTOR
rb_init_ractor_selector();
#endif
}