Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/actions/setup/directories/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,20 @@ runs:
fetch-depth: ${{ inputs.fetch-depth }}
persist-credentials: false

- id: gems-key
shell: bash
run: |
echo "hash=$hash" >> "$GITHUB_OUTPUT"
env:
hash: ${{ hashFiles(format('{0}/gems/bundled_gems', inputs.srcdir)) }}

- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ inputs.srcdir }}/.downloaded-cache
key: ${{ runner.os }}-${{ runner.arch }}-downloaded-cache
key: downloaded-cache-${{ steps.gems-key.outputs.hash }}
restore-keys: |
downloaded-cache-
${{ runner.os }}-${{ runner.arch }}-downloaded-cache

# Cache cloned bundled gem sources so a transient DNS/network failure
# while cloning from github.com (e.g. "Could not resolve host") does not
Expand All @@ -130,8 +140,9 @@ runs:
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ inputs.srcdir }}/gems/src
key: ${{ runner.os }}-${{ runner.arch }}-bundled-gems-src-${{ hashFiles(format('{0}/gems/bundled_gems', inputs.srcdir)) }}
key: bundled-gems-src-${{ steps.gems-key.outputs.hash }}
restore-keys: |
bundled-gems-src-
${{ runner.os }}-${{ runner.arch }}-bundled-gems-src-

- if: steps.which.outputs.autoreconf
Expand Down
17 changes: 12 additions & 5 deletions bootstraptest/test_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -627,8 +627,12 @@ def inspect
if !can_limit
'ok' # cannot make thread creation fail on this platform; nothing to test
else
warm = 2.times.map { Ractor.new { nil until Ractor.receive == :quit } }
sleep 0.3 # the pool now has shared native threads parked for the warm ractors
# One warm ractor parks one shared native thread in the pool. Exactly one:
# the pool is widened only while snt_cnt < max_cpu, so with two parked
# threads a 2-CPU host would never attempt pthread_create below and the
# rlimit would go unnoticed.
warm = Ractor.new { nil until Ractor.receive == :quit }
sleep 0.3 # the pool now has a shared native thread parked for the warm ractor
Process.setrlimit(:NPROC, 1)
# RLIMIT_NPROC binds neither root (CI containers) nor macOS threads;
# probe that thread creation actually fails before asserting on it.
Expand All @@ -651,10 +655,13 @@ def inspect
end
end
sleep 0.5 # a wrongly-published thread would be served and die about now
errs == 20 ? 'ok' : "#{errs} of 20 raised"
# On a single-CPU host the pool is already at max_cpu, widening is never
# attempted and nothing raises; everywhere else every attempt must fail.
# A mixed count means a failed attempt was not rolled back cleanly.
(errs == 20 || errs == 0) ? 'ok' : "#{errs} of 20 raised"
end
warm.each { |r| r.send(:quit) }
warm.each(&:value)
warm.send(:quit)
warm.value
GC.start
result
end
Expand Down
2 changes: 1 addition & 1 deletion ractor_sync.c
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,7 @@ ractor_basket_new(rb_execution_context_t *ec, VALUE obj, enum ractor_basket_type
/* 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;
volatile VALUE v = Qfalse;
bool marshaled = false;
struct rb_ractor_move_courier *courier = NULL;

Expand Down
45 changes: 33 additions & 12 deletions thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -842,12 +842,25 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start)
#endif

if (th->invoke_type == thread_invoke_type_ractor_proc) {
// The postmortem epilogue below runs after this Ractor is unlinked and no
// longer counted, with the GVL already released, and it frees through
// VM-global state (the jit_cont list and its mutex, the fiber pool, the
// main objspace's malloc accounting). Nothing else holds the main Ractor
// back at that point, so count it like a coroutine epilogue: then
// ruby_vm_destruct waits for it (rb_thread_sched_wait_winding) instead of
// tearing that state down underneath. th is freed by the epilogue, so
// keep the VM pointer.
rb_vm_t *const vm = th->vm;
rb_thread_sched_winding_begin(vm);

// after rb_ractor_living_threads_remove()
// GC will happen anytime and this ractor can be collected (and destroy GVL).
// So gvl_release() should be before it.
thread_sched_to_dead(TH_SCHED(th), th);
rb_ractor_living_threads_remove(th->ractor, th);
rb_ractor_postmortem_free(&pf);

rb_thread_sched_winding_end(vm);
}
else {
rb_ractor_living_threads_remove(th->ractor, th);
Expand Down Expand Up @@ -1099,22 +1112,13 @@ rb_thread_create(VALUE (*fn)(void *), void *arg)
return thread_create_core(rb_thread_alloc(rb_cThread), &params);
}

VALUE
rb_thread_create_ractor(rb_ractor_t *r, VALUE args, VALUE proc)
static VALUE
create_ractor_alloc_thread(rb_ractor_t *r, rb_ractor_t *cr, rb_execution_context_t *ec)
{
struct thread_create_params params = {
.type = thread_invoke_type_ractor_proc,
.g = r,
.args = args,
.proc = proc,
};

/* Allocate the child's main Thread and root Fiber wrappers directly in the child's
* objspace, so the thread is built of objects it owns. Whole-VM walks read
* cr->objspace: swap it under the VM lock, unobservable to others. */
VALUE thval = Qundef;
rb_ractor_t *cr = GET_RACTOR();
rb_execution_context_t *ec = GET_EC();
volatile VALUE thval = Qundef;
const bool multi_objspace = rb_gc_multi_objspace_p();
enum ruby_tag_type alloc_state = TAG_NONE;
RB_VM_LOCKING() {
Expand Down Expand Up @@ -1154,6 +1158,23 @@ rb_thread_create_ractor(rb_ractor_t *r, VALUE args, VALUE proc)
}
EC_JUMP_TAG(ec, alloc_state);
}
return thval;
}

VALUE
rb_thread_create_ractor(rb_ractor_t *r, VALUE args, VALUE proc)
{
struct thread_create_params params = {
.type = thread_invoke_type_ractor_proc,
.g = r,
.args = args,
.proc = proc,
};

rb_ractor_t *cr = GET_RACTOR();
rb_execution_context_t *ec = GET_EC();

VALUE thval = create_ractor_alloc_thread(r, cr, ec);

/* Creation can still fail before vm_insert_ractor (an IsolationError, say), and a
* left-over cover would enumerate the dead child's objspace twice and dangle after
Expand Down
13 changes: 13 additions & 0 deletions thread_none.c
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,19 @@ rb_thread_event_hooks_registered_p(void)

#endif /* THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION */

void
rb_thread_sched_winding_begin(rb_vm_t *vm)
{
// nothing to count: rb_thread_sched_wait_winding below never waits
(void)vm;
}

void
rb_thread_sched_winding_end(rb_vm_t *vm)
{
(void)vm;
}

void
rb_thread_sched_wait_winding(rb_vm_t *vm)
{
Expand Down
17 changes: 17 additions & 0 deletions thread_pthread.c
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,23 @@ grq_size(rb_vm_t *vm, rb_ractor_t *cr)
}
#endif

// A native thread enters/leaves an epilogue that outlives its Ractor: from
// the increment until the decrement, ruby_vm_destruct waits for it below.
// The increment must happen while the VM still counts the thread's Ractor,
// so that the two never look absent at the same time.
void
rb_thread_sched_winding_begin(rb_vm_t *vm)
{
RUBY_ATOMIC_INC(vm->ractor.sched.winding_cnt);
}

void
rb_thread_sched_winding_end(rb_vm_t *vm)
{
VM_ASSERT(RUBY_ATOMIC_LOAD(vm->ractor.sched.winding_cnt) > 0);
RUBY_ATOMIC_DEC(vm->ractor.sched.winding_cnt);
}

// ruby_vm_destruct: wait until no native thread is between a coroutine
// epilogue and its reclaim -- past that point the reclaim frees through the
// (about to be destroyed) objspace and reads the (about to be unset) VM.
Expand Down
13 changes: 13 additions & 0 deletions thread_win32.c
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,19 @@ rb_thread_malloc_stack_set(rb_thread_t *th, void *stack, size_t stack_size)

#endif /* THREAD_SYSTEM_DEPENDENT_IMPLEMENTATION */

void
rb_thread_sched_winding_begin(rb_vm_t *vm)
{
// nothing to count: rb_thread_sched_wait_winding below never waits
(void)vm;
}

void
rb_thread_sched_winding_end(rb_vm_t *vm)
{
(void)vm;
}

void
rb_thread_sched_wait_winding(rb_vm_t *vm)
{
Expand Down