diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml
index a7363e3a9ab39f..2c1d35899fb722 100644
--- a/.github/workflows/annocheck.yml
+++ b/.github/workflows/annocheck.yml
@@ -73,7 +73,7 @@ jobs:
builddir: build
makeup: true
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml
index bcbdec54dae635..72611269a40eca 100644
--- a/.github/workflows/auto_review_pr.yml
+++ b/.github/workflows/auto_review_pr.yml
@@ -29,7 +29,7 @@ jobs:
with:
persist-credentials: false
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.4'
bundler: none
diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml
index 27f98d2d50479a..695c468153be36 100644
--- a/.github/workflows/baseruby.yml
+++ b/.github/workflows/baseruby.yml
@@ -48,7 +48,7 @@ jobs:
- ruby-3.3
steps:
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: ${{ matrix.ruby }}
bundler: none
diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml
index fe485d2bfbfa10..464a44dfceb9dd 100644
--- a/.github/workflows/bundled_gems.yml
+++ b/.github/workflows/bundled_gems.yml
@@ -38,7 +38,7 @@ jobs:
with:
token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }}
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: 4.0
diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml
index 6bd21df6faa1aa..56eea4001fd274 100644
--- a/.github/workflows/check_dependencies.yml
+++ b/.github/workflows/check_dependencies.yml
@@ -42,7 +42,7 @@ jobs:
- uses: ./.github/actions/setup/directories
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml
index 0879d72b24ed39..55238ffba504d9 100644
--- a/.github/workflows/check_misc.yml
+++ b/.github/workflows/check_misc.yml
@@ -23,7 +23,7 @@ jobs:
token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }}
persist-credentials: false
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: head
diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml
index 75b0c7f896a69d..537432bd407920 100644
--- a/.github/workflows/check_sast.yml
+++ b/.github/workflows/check_sast.yml
@@ -78,14 +78,14 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
- uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+ uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
languages: ${{ matrix.language }}
build-mode: none
config-file: .github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+ uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
category: '/language:${{ matrix.language }}'
upload: False
@@ -127,7 +127,7 @@ jobs:
continue-on-error: true
- name: Upload SARIF
- uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+ uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
sarif_file: sarif-results/${{ matrix.language }}.sarif
continue-on-error: true
diff --git a/.github/workflows/dependabot_automerge.yml b/.github/workflows/dependabot_automerge.yml
index de408c97cbeb47..b331f888279bc0 100644
--- a/.github/workflows/dependabot_automerge.yml
+++ b/.github/workflows/dependabot_automerge.yml
@@ -17,7 +17,7 @@ jobs:
id: metadata
- name: Wait for status checks
- uses: lewagon/wait-on-check-action@96d9100b431964d10e0136aff8b9ccb92470505e # v1.8.0
+ uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 # v1.8.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml
index 8dce73ab16134f..b0fe1c827f51a8 100644
--- a/.github/workflows/modgc.yml
+++ b/.github/workflows/modgc.yml
@@ -67,7 +67,7 @@ jobs:
uses: ./.github/actions/setup/ubuntu
if: ${{ contains(matrix.os, 'ubuntu') }}
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml
index 026fb28c9fe30b..0e51c852335355 100644
--- a/.github/workflows/parse_y.yml
+++ b/.github/workflows/parse_y.yml
@@ -59,7 +59,7 @@ jobs:
- uses: ./.github/actions/setup/ubuntu
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8dc18ca9f472d5..bea2aba1012685 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -22,7 +22,7 @@ jobs:
with:
persist-credentials: false
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: 3.3.4
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 86a39eb0ab0d89..e63a22e5d262b9 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -73,6 +73,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
+ uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
with:
sarif_file: results.sarif
diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml
index 8b4137f935404c..bf6b6f1d0155ec 100644
--- a/.github/workflows/spec_guards.yml
+++ b/.github/workflows/spec_guards.yml
@@ -49,7 +49,7 @@ jobs:
with:
persist-credentials: false
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: ${{ matrix.ruby }}
bundler: none
diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml
index f4f176b356c341..98957ab01acb29 100644
--- a/.github/workflows/sync_default_gems.yml
+++ b/.github/workflows/sync_default_gems.yml
@@ -39,7 +39,7 @@ jobs:
with:
token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }}
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.4'
bundler: none
diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml
index 932ed4a8298b05..d0d4e5ba575d66 100644
--- a/.github/workflows/tarball-ubuntu.yml
+++ b/.github/workflows/tarball-ubuntu.yml
@@ -43,7 +43,7 @@ jobs:
set -x
sudo apt-get update -q
sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev bison- autoconf-
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.2'
# test-bundled-gems requires executable host ruby
diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml
index c13ba7b0d0cc45..85fe1c073f199a 100644
--- a/.github/workflows/tarball-windows.yml
+++ b/.github/workflows/tarball-windows.yml
@@ -47,7 +47,7 @@ jobs:
- run: md build
working-directory:
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.2'
bundler: none
diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index 1b0371bb5f314d..6518e77d72b07b 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -79,7 +79,7 @@ jobs:
with:
arch: ${{ matrix.arch }}
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml
index 458676137a79e2..75344395bb4155 100644
--- a/.github/workflows/wasm.yml
+++ b/.github/workflows/wasm.yml
@@ -65,7 +65,7 @@ jobs:
sparse-checkout: /.github
persist-credentials: false
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
index 90e2a93ebe37d2..9a9c2be282a000 100644
--- a/.github/workflows/windows.yml
+++ b/.github/workflows/windows.yml
@@ -58,7 +58,7 @@ jobs:
- run: md build
working-directory:
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
# windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head
ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }}
diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml
index 04740c03037ff3..793f93d6d10f44 100644
--- a/.github/workflows/yjit-ubuntu.yml
+++ b/.github/workflows/yjit-ubuntu.yml
@@ -138,7 +138,7 @@ jobs:
- uses: ./.github/actions/setup/ubuntu
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml
index 7dd8e252b79235..a38ad2f0ce5b7d 100644
--- a/.github/workflows/zjit-ubuntu.yml
+++ b/.github/workflows/zjit-ubuntu.yml
@@ -136,7 +136,7 @@ jobs:
- uses: ./.github/actions/setup/ubuntu
- - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
+ - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: '3.1'
bundler: none
diff --git a/common.mk b/common.mk
index 29fd0f5bcf6655..07ece19ee98373 100644
--- a/common.mk
+++ b/common.mk
@@ -1716,10 +1716,13 @@ yes-test-bundler-parallel: $(PREPARE_BUNDLER)
$(gnumake_recursive)$(XRUBY) \
-r./$(arch)-fake \
-r$(tooldir)/lib/_tmpdir \
+ -r$(tooldir)/lib/bundler_runtime_grouping \
-I$(srcdir)/spec/bundler \
-e "ruby = ENV['RUBY']" \
-e "ARGV[-1] = File.expand_path(ARGV[-1])" \
-e "ENV['RSPEC_EXECUTABLE'] = ruby + ARGV.shift" \
+ -e "require 'support/setup'" \
+ -e "BundlerRuntimeGrouping.install!" \
-e "load ARGV.shift" \
-s -- -no-report-tmpdir -- \
" -C $(srcdir) -Ispec/bundler -Ispec/lib .bundle/bin/rspec -r spec_helper" \
diff --git a/configure.ac b/configure.ac
index 8d020771c6d505..562d9262f837f1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3820,9 +3820,6 @@ AS_CASE(["${enable_dtrace}"],
], [
rb_cv_dtrace_available=no
])
-AS_CASE(["$target_os"],[freebsd*],[
- rb_cv_dtrace_available=no
- ])
AS_IF([test "${enable_dtrace}" = yes], [dnl
AS_IF([test -z "$DTRACE"], [dnl
AC_MSG_ERROR([dtrace(1) is missing])
diff --git a/object.c b/object.c
index cbafd558f9fc9d..0552e7cf44019a 100644
--- a/object.c
+++ b/object.c
@@ -453,13 +453,13 @@ rb_immutable_obj_clone(int argc, VALUE *argv, VALUE obj)
VALUE
rb_get_freeze_opt(int argc, VALUE *argv)
{
- static ID keyword_ids[1];
+ /* idFreeze (== :freeze) is preinterned before any Ruby code runs, so use it
+ * directly instead of lazily initializing a shared static, which races when
+ * Ractors run this concurrently. */
+ const ID keyword_ids[1] = { idFreeze };
VALUE opt;
VALUE kwfreeze = Qnil;
- if (!keyword_ids[0]) {
- CONST_ID(keyword_ids[0], "freeze");
- }
rb_scan_args(argc, argv, "0:", &opt);
if (!NIL_P(opt)) {
rb_get_kwargs(opt, keyword_ids, 0, 1, &kwfreeze);
@@ -478,6 +478,29 @@ immutable_obj_clone(VALUE obj, VALUE kwfreeze)
return obj;
}
+/* Cache of the `{freeze: true/false}` keyword hash passed to #initialize_clone.
+ * Ractors may reach this concurrently, so build a fully populated, frozen and
+ * pinned hash locally and publish it with a single atomic CAS: any value another
+ * thread can observe in the static is already complete, and a builder that loses
+ * the CAS just discards its hash. (The old lazy init published an empty hash that
+ * a second thread could read and freeze before the first finished filling it.) */
+static VALUE freeze_true_hash, freeze_false_hash;
+
+static VALUE
+clone_freeze_kwarg_hash(VALUE *cache, VALUE freeze_value)
+{
+ VALUE h = RUBY_ATOMIC_VALUE_LOAD(*cache);
+ if (!h) {
+ h = rb_hash_new();
+ rb_hash_aset(h, ID2SYM(idFreeze), freeze_value);
+ rb_obj_freeze(h);
+ rb_vm_register_global_object(h); /* pin before publishing */
+ VALUE prev = RUBY_ATOMIC_VALUE_CAS(*cache, 0, h);
+ if (prev) h = prev; /* lost the race; our h becomes garbage */
+ }
+ return h;
+}
+
VALUE
rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze)
{
@@ -506,31 +529,15 @@ rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze)
}
break;
case Qtrue: {
- static VALUE freeze_true_hash;
- if (!freeze_true_hash) {
- freeze_true_hash = rb_hash_new();
- rb_vm_register_global_object(freeze_true_hash);
- rb_hash_aset(freeze_true_hash, ID2SYM(idFreeze), Qtrue);
- rb_obj_freeze(freeze_true_hash);
- }
-
argv[0] = obj;
- argv[1] = freeze_true_hash;
+ argv[1] = clone_freeze_kwarg_hash(&freeze_true_hash, Qtrue);
rb_funcallv_kw(clone, id_init_clone, 2, argv, RB_PASS_KEYWORDS);
OBJ_FREEZE(clone);
break;
}
case Qfalse: {
- static VALUE freeze_false_hash;
- if (!freeze_false_hash) {
- freeze_false_hash = rb_hash_new();
- rb_vm_register_global_object(freeze_false_hash);
- rb_hash_aset(freeze_false_hash, ID2SYM(idFreeze), Qfalse);
- rb_obj_freeze(freeze_false_hash);
- }
-
argv[0] = obj;
- argv[1] = freeze_false_hash;
+ argv[1] = clone_freeze_kwarg_hash(&freeze_false_hash, Qfalse);
rb_funcallv_kw(clone, id_init_clone, 2, argv, RB_PASS_KEYWORDS);
break;
}
diff --git a/pathname_builtin.rb b/pathname_builtin.rb
index beaff62de2acfa..ade839e2bc94bf 100644
--- a/pathname_builtin.rb
+++ b/pathname_builtin.rb
@@ -1895,7 +1895,26 @@ def pipe?() FileTest.pipe?(@path) end
# See FileTest.socket?.
def socket?() FileTest.socket?(@path) end
- # See FileTest.owned?.
+ # :markup: markdown
+ #
+ # call-seq:
+ # owned? -> true or false
+ #
+ # Returns whether the entry at the path represented by `self`
+ # exists and is owned by the user of the current process:
+ #
+ # ```ruby
+ # pn = Pathname('doc/t.tmp')
+ # pn.write('foo')
+ # pn.owned? # => true
+ # pn.delete
+ # pn = Pathname('doc/tmp')
+ # pn.mkdir
+ # pn.owned? # => true
+ # pn.rmdir
+ # Pathname('/etc').owned? # => false
+ # ```
+ #
def owned?() FileTest.owned?(@path) end
# See FileTest.readable?.
diff --git a/ractor_sync.c b/ractor_sync.c
index 5eeb6e20db9f28..5dfb86e3bff130 100644
--- a/ractor_sync.c
+++ b/ractor_sync.c
@@ -144,13 +144,27 @@ static VALUE
ractor_port_closed_p(rb_execution_context_t *ec, VALUE self)
{
const struct ractor_port *rp = RACTOR_PORT_PTR(self);
+ rb_ractor_t *r = rp->r;
+ bool closed;
- if (ractor_closed_port_p(ec, rp->r, rp)) {
- return Qtrue;
+ 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 {
- return Qfalse;
+ /* 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
diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb
index 24b0e71020ccdd..27ddc6a77125a2 100644
--- a/spec/bundler/spec_helper.rb
+++ b/spec/bundler/spec_helper.rb
@@ -175,26 +175,6 @@ def self.ruby=(ruby)
reset!
end
- # Opt-in per-example runtime log (set BUNDLER_SPEC_RUNTIME_LOG to a file path).
- # Each parallel worker appends one "\t" line per example to its
- # own "." file (a single shared file would hit Windows
- # cross-process sharing violations), so the heaviest specs can be found after a
- # full run. turbo_tests only writes its own --runtime-log when invoked with the
- # bare "spec" path, which the build never does, so it produces no log otherwise.
- if (runtime_log = ENV["BUNDLER_SPEC_RUNTIME_LOG"])
- worker = ENV["TEST_ENV_NUMBER"].to_s
- worker = "1" if worker.empty?
- runtime_log = "#{runtime_log}.#{worker}"
- config.before(:each) { @__runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) }
- config.after(:each) do |example|
- next unless @__runtime_start
- dt = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @__runtime_start
- File.write(runtime_log, "#{format("%.4f", dt)}\t#{example.metadata[:file_path]}\n", mode: "a")
- rescue StandardError
- # never let runtime logging break a test run
- end
- end
-
Spec::Shards::EXAMPLE_MAPPINGS.each do |tag, file_paths|
file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") })
diff --git a/thread.c b/thread.c
index 2a89582a274e15..22f0fe8e0f73de 100644
--- a/thread.c
+++ b/thread.c
@@ -680,6 +680,13 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start)
r->r_stdin = rb_io_prep_stdin();
r->r_stdout = rb_io_prep_stdout();
r->r_stderr = rb_io_prep_stderr();
+
+ /* Build the interrupt queue and mask stack here, on the new Ractor's
+ * own main thread, instead of carrying over the ones the creating
+ * thread made. The mask stack starts empty so a new Ractor does not
+ * inherit the creating thread's Thread.handle_interrupt state. */
+ th->pending_interrupt_queue = rb_ary_hidden_new(0);
+ th->pending_interrupt_mask_stack = rb_ary_hidden_new(0);
}
RB_VM_UNLOCK();
}
@@ -845,7 +852,12 @@ thread_create_core(VALUE thval, struct thread_create_params *params)
"can't start a new thread (frozen ThreadGroup)");
}
- rb_fiber_inherit_storage(ec, th->ec->fiber_ptr);
+ /* A new Ractor must not inherit the creating thread's fiber storage: its
+ * entries may be objects owned by the creating Ractor. Only threads created
+ * within the same Ractor inherit it. */
+ if (params->type != thread_invoke_type_ractor_proc) {
+ rb_fiber_inherit_storage(ec, th->ec->fiber_ptr);
+ }
switch (params->type) {
case thread_invoke_type_proc:
@@ -882,10 +894,19 @@ thread_create_core(VALUE thval, struct thread_create_params *params)
th->priority = current_th->priority;
th->thgroup = current_th->thgroup;
- th->pending_interrupt_queue = rb_ary_hidden_new(0);
- th->pending_interrupt_queue_checked = 0;
- th->pending_interrupt_mask_stack = rb_ary_dup(current_th->pending_interrupt_mask_stack);
- RBASIC_CLEAR_CLASS(th->pending_interrupt_mask_stack);
+ if (th->invoke_type == thread_invoke_type_ractor_proc) {
+ /* A new Ractor's main thread builds these on start
+ * (thread_start_func_2); leave them unset until then. */
+ th->pending_interrupt_queue = 0;
+ th->pending_interrupt_mask_stack = 0;
+ th->pending_interrupt_queue_checked = 0;
+ }
+ else {
+ th->pending_interrupt_queue = rb_ary_hidden_new(0);
+ th->pending_interrupt_queue_checked = 0;
+ th->pending_interrupt_mask_stack = rb_ary_dup(current_th->pending_interrupt_mask_stack);
+ RBASIC_CLEAR_CLASS(th->pending_interrupt_mask_stack);
+ }
rb_native_mutex_initialize(&th->interrupt_lock);
diff --git a/tool/lib/bundler_runtime_grouping.rb b/tool/lib/bundler_runtime_grouping.rb
new file mode 100644
index 00000000000000..c1c55810146e76
--- /dev/null
+++ b/tool/lib/bundler_runtime_grouping.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+# Distribute the parallel bundler-spec workers by measured per-file runtime
+# instead of file size, with no change to turbo_tests or parallel_tests and
+# without touching the synced spec/bin/parallel_rspec. The test-bundler-parallel
+# recipe requires this file and calls install! just before loading the stock
+# parallel_rspec; install! prepends the grouper (parent) and appends the recorder
+# to RSPEC_EXECUTABLE so each worker records (see bundler_runtime_recorder.rb).
+# The grouping/recording logic lives under tool/, which sync-default-gems never
+# copies, so the rubygems side stays untouched.
+#
+# turbo_tests only feeds a runtime log to the grouper when invoked with the bare
+# "spec" path (TurboTests::CLI passes files: ["spec"] only when given no
+# positional arg), which the build never does: the recipe passes an absolute
+# spec/bundler path, so grouping falls back to file size and the single heaviest
+# file becomes the long pole. We reopen the one grouper hook that size path uses,
+# ParallelTests::RSpec::Runner.sort_by_filesize, and return runtime weights when
+# the previous run left us data. Its argument is the already-expanded file list,
+# so nothing here re-globs or re-reads it.
+
+module BundlerRuntimeGrouping
+ WORKER_LOGS = ".[0-9]*"
+
+ module_function
+
+ # Per-worker log base. Under the (gitignored) source tmp so it survives between
+ # runs; overridable via BUNDLER_SPEC_RUNTIME_LOG.
+ def log_base
+ ENV["BUNDLER_SPEC_RUNTIME_LOG"] ||
+ File.expand_path("../../tmp/bundler_runtime_rspec.log", __dir__)
+ end
+
+ # Append one "\t" line for a finished example to this worker's
+ # own log ("."); a single shared file would hit Windows
+ # cross-process sharing violations. Called from the worker (see recorder).
+ def record(example, seconds)
+ worker = ENV["TEST_ENV_NUMBER"].to_s
+ worker = "1" if worker.empty?
+ File.write("#{log_base}.#{worker}",
+ "#{format("%.4f", seconds)}\t#{example.metadata[:file_path]}\n", mode: "a")
+ rescue StandardError
+ # never let runtime logging break a test run
+ end
+
+ # {canonical_path => seconds} summed across the previous run's worker logs,
+ # memoized before this run overwrites them. Empty on the first run.
+ def runtimes
+ @runtimes ||= Dir.glob(log_base + WORKER_LOGS).each_with_object(Hash.new(0.0)) do |path, sums|
+ File.foreach(path) do |line|
+ seconds, tab, file = line.chomp.partition("\t")
+ sums[canonical(file)] += seconds.to_f unless tab.empty?
+ end
+ rescue StandardError
+ sums
+ end
+ end
+
+ # cwd-independent key: from "spec/bundler/" on, forward slashes. Recorded paths
+ # are rspec-relative ("./spec/bundler/..."); grouped paths are absolute.
+ def canonical(path)
+ normalized = path.tr("\\", "/")
+ i = normalized.index("spec/bundler/")
+ i ? normalized[i..] : normalized
+ end
+
+ # Called once in the parent before parallel_rspec loads (spec/bundler/support
+ # must already be required so parallel_tests is on the load path). Reads the
+ # previous run, clears the logs, prepends the grouper, and arranges for each
+ # worker to record by appending the recorder to RSPEC_EXECUTABLE.
+ def install!
+ require "fileutils"
+ require "parallel_tests/rspec/runner"
+ FileUtils.mkdir_p(File.dirname(log_base))
+ runtimes # capture the previous run before clearing
+ Dir.glob(log_base + WORKER_LOGS).each { |f| File.delete(f) rescue nil }
+ ParallelTests::RSpec::Runner.singleton_class.prepend(SortHook)
+ if ENV["RSPEC_EXECUTABLE"]
+ recorder = File.expand_path("bundler_runtime_recorder.rb", __dir__)
+ ENV["RSPEC_EXECUTABLE"] = "#{ENV["RSPEC_EXECUTABLE"]} -r#{recorder}"
+ end
+ rescue StandardError, LoadError => e
+ warn "parallel_rspec: runtime-based grouping disabled (#{e.class}: #{e.message})"
+ end
+
+ module SortHook
+ def sort_by_filesize(tests)
+ rt = BundlerRuntimeGrouping.runtimes
+ return super if rt.empty?
+ tests.sort!
+ known = tests.map { |t| rt[BundlerRuntimeGrouping.canonical(t)] }.select(&:positive?)
+ return super unless known.size * 1.5 > tests.size # parallel_tests' own threshold
+ average = known.sum / known.size
+ tests.map! do |t|
+ seconds = rt[BundlerRuntimeGrouping.canonical(t)]
+ [t, seconds.positive? ? seconds : average]
+ end
+ end
+ end
+end
diff --git a/tool/lib/bundler_runtime_recorder.rb b/tool/lib/bundler_runtime_recorder.rb
new file mode 100644
index 00000000000000..1c5b110beeb7e0
--- /dev/null
+++ b/tool/lib/bundler_runtime_recorder.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# Loaded into each parallel worker (via -r appended to RSPEC_EXECUTABLE by
+# BundlerRuntimeGrouping.install!) to record per-example runtimes that the
+# next run groups by. Kept in tool/ so recording does not depend on
+# spec/bundler/spec_helper.rb (which is synced from rubygems/rubygems).
+
+require_relative "bundler_runtime_grouping"
+
+RSpec.configure do |config|
+ config.before(:each) { @__runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) }
+ config.after(:each) do |example|
+ next unless @__runtime_start
+ BundlerRuntimeGrouping.record(example, Process.clock_gettime(Process::CLOCK_MONOTONIC) - @__runtime_start)
+ end
+end
diff --git a/vm.c b/vm.c
index a5ab2a153013e1..c30e750d9fee00 100644
--- a/vm.c
+++ b/vm.c
@@ -2292,7 +2292,7 @@ short ruby_vm_redefined_flag[BOP_LAST_];
static st_table *vm_opt_method_def_table = 0;
static st_table *vm_opt_mid_table = 0;
-void
+static void
rb_free_vm_opt_tables(void)
{
st_free_table(vm_opt_method_def_table);