diff --git a/class.c b/class.c
index a90a0f43de50ed..da22fd3583006a 100644
--- a/class.c
+++ b/class.c
@@ -1885,6 +1885,7 @@ rb_prepend_module(VALUE klass, VALUE module)
if (subs_v) {
struct rb_subclasses *subs = (struct rb_subclasses *)subs_v;
VALUE *entries = rb_imemo_subclasses_entries(subs_v);
+ VALUE new_origins = 0;
for (uint32_t i = 0; i < subs->count; i++) {
const VALUE subclass = entries[i];
if (!subclass) continue;
@@ -1900,10 +1901,20 @@ rb_prepend_module(VALUE klass, VALUE module)
RCLASS_SET_INCLUDER(origin, RCLASS_INCLUDER(subclass));
RCLASS_WRITE_ORIGIN(subclass, origin);
RICLASS_SET_ORIGIN_SHARED_MTBL(origin);
+ if (!new_origins) new_origins = rb_ary_hidden_new(1);
+ rb_ary_push(new_origins, origin);
}
include_modules_at(subclass, subclass, module, FALSE);
}
}
+ /* Register after the loop. Registering during it would visit the
+ * new iclass and prepend module into it a second time. */
+ if (new_origins) {
+ for (long i = 0; i < RARRAY_LEN(new_origins); i++) {
+ rb_module_add_to_subclasses_list(klass, RARRAY_AREF(new_origins, i));
+ }
+ }
+ RB_GC_GUARD(new_origins);
}
}
}
diff --git a/ext/coverage/coverage.c b/ext/coverage/coverage.c
index 95688c309b0316..95c85ca4ab317d 100644
--- a/ext/coverage/coverage.c
+++ b/ext/coverage/coverage.c
@@ -21,6 +21,25 @@ static int current_mode;
static VALUE cme2counter = Qnil;
static VALUE me_set = Qnil;
+/*
+ * Method coverage result template:
+ * method_tmpl_by_path: { path => { key => me or [me, ...] } }
+ * Each peek dups the per-path template and replaces the values with the
+ * call counts. me_set and cme2counter are append-only and iterate in
+ * insertion order, so only the entries after the ones already seen are added.
+ */
+static VALUE method_tmpl_by_path = Qnil;
+static long method_tmpl_n_me_set = 0;
+static long method_tmpl_n_cme2counter = 0;
+
+static void
+method_template_reset(void)
+{
+ method_tmpl_by_path = Qnil;
+ method_tmpl_n_me_set = 0;
+ method_tmpl_n_cme2counter = 0;
+}
+
/*
* call-seq: Coverage.supported?(mode) -> true or false
*
@@ -120,6 +139,7 @@ rb_coverage_setup(int argc, VALUE *argv, VALUE klass)
cme2counter = Qnil;
me_set = Qnil;
}
+ method_template_reset();
coverages = rb_get_coverages();
if (!RTEST(coverages)) {
@@ -186,11 +206,16 @@ struct branch_coverage_result_builder
int id;
VALUE result;
VALUE children;
- VALUE counters;
};
+/*
+ * Branch coverage result template, cached in branches[2]:
+ * { base_key => { target_key => counter_index } }
+ * Each peek dups it and replaces the indexes with the counters, which avoids
+ * hashing the array keys again and again.
+ */
static int
-branch_coverage_ii(VALUE _key, VALUE branch, VALUE v)
+branch_template_ii(VALUE _key, VALUE branch, VALUE v)
{
struct branch_coverage_result_builder *b = (struct branch_coverage_result_builder *) v;
@@ -199,14 +224,16 @@ branch_coverage_ii(VALUE _key, VALUE branch, VALUE v)
VALUE target_first_column = RARRAY_AREF(branch, 2);
VALUE target_last_lineno = RARRAY_AREF(branch, 3);
VALUE target_last_column = RARRAY_AREF(branch, 4);
- long counter_idx = FIX2LONG(RARRAY_AREF(branch, 5));
- rb_hash_aset(b->children, rb_ary_new_from_args(6, target_label, LONG2FIX(b->id++), target_first_lineno, target_first_column, target_last_lineno, target_last_column), RARRAY_AREF(b->counters, counter_idx));
+ VALUE counter_idx = RARRAY_AREF(branch, 5);
+ VALUE key = rb_ary_new_from_args(6, target_label, LONG2FIX(b->id++), target_first_lineno, target_first_column, target_last_lineno, target_last_column);
+ rb_ary_freeze(key);
+ rb_hash_aset(b->children, key, counter_idx);
return ST_CONTINUE;
}
static int
-branch_coverage_i(VALUE _key, VALUE branch_base, VALUE v)
+branch_template_i(VALUE _key, VALUE branch_base, VALUE v)
{
struct branch_coverage_result_builder *b = (struct branch_coverage_result_builder *) v;
@@ -217,52 +244,211 @@ branch_coverage_i(VALUE _key, VALUE branch_base, VALUE v)
VALUE base_last_column = RARRAY_AREF(branch_base, 4);
VALUE branches = RARRAY_AREF(branch_base, 5);
VALUE children = rb_hash_new();
- rb_hash_aset(b->result, rb_ary_new_from_args(6, base_type, LONG2FIX(b->id++), base_first_lineno, base_first_column, base_last_lineno, base_last_column), children);
+ VALUE key = rb_ary_new_from_args(6, base_type, LONG2FIX(b->id++), base_first_lineno, base_first_column, base_last_lineno, base_last_column);
+ rb_ary_freeze(key);
+ rb_hash_aset(b->result, key, children);
b->children = children;
- rb_hash_foreach(branches, branch_coverage_ii, v);
+ rb_hash_foreach(branches, branch_template_ii, v);
return ST_CONTINUE;
}
+/* returns [template, nbases, ntargets] */
static VALUE
-branch_coverage(VALUE branches)
+branch_template(VALUE branches)
{
VALUE structure = RARRAY_AREF(branches, 0);
+ VALUE counters = RARRAY_AREF(branches, 1);
+ long nbases = RHASH_SIZE(structure);
+ long ntargets = RARRAY_LEN(counters);
+ VALUE cache = RARRAY_LEN(branches) > 2 ? RARRAY_AREF(branches, 2) : Qnil;
+
+ if (!NIL_P(cache) &&
+ FIX2LONG(RARRAY_AREF(cache, 1)) == nbases &&
+ FIX2LONG(RARRAY_AREF(cache, 2)) == ntargets) {
+ return RARRAY_AREF(cache, 0);
+ }
+ else {
+ struct branch_coverage_result_builder b;
+ b.id = 0;
+ b.result = rb_hash_new();
+ rb_hash_foreach(structure, branch_template_i, (VALUE)&b);
+ cache = rb_ary_hidden_new(3);
+ rb_ary_push(cache, b.result);
+ rb_ary_push(cache, LONG2FIX(nbases));
+ rb_ary_push(cache, LONG2FIX(ntargets));
+ rb_ary_store(branches, 2, cache);
+ return b.result;
+ }
+}
+
+static int
+branch_fill_check(st_data_t key, st_data_t value, st_data_t argp, int error)
+{
+ return ST_REPLACE;
+}
+
+/* children: {target_key => counter_index} -> {target_key => counter} */
+static int
+branch_fill_counter(st_data_t *key, st_data_t *value, st_data_t argp, int existing)
+{
+ VALUE counters = (VALUE)argp;
+ *value = (st_data_t)RARRAY_AREF(counters, FIX2LONG((VALUE)*value));
+ return ST_CONTINUE;
+}
- struct branch_coverage_result_builder b;
- b.id = 0;
- b.result = rb_hash_new();
- b.counters = RARRAY_AREF(branches, 1);
+struct branch_fill_arg
+{
+ VALUE result;
+ VALUE counters;
+};
- rb_hash_foreach(structure, branch_coverage_i, (VALUE)&b);
+/* result: {base_key => children_template} -> {base_key => filled copy of children} */
+static int
+branch_fill_children(st_data_t *key, st_data_t *value, st_data_t argp, int existing)
+{
+ struct branch_fill_arg *a = (struct branch_fill_arg *)argp;
+ VALUE children = rb_hash_dup((VALUE)*value);
+ rb_hash_stlike_foreach_with_replace(children, branch_fill_check, branch_fill_counter, (st_data_t)a->counters);
+ RB_OBJ_WRITE(a->result, value, children);
+ return ST_CONTINUE;
+}
- return b.result;
+static VALUE
+branch_coverage(VALUE branches)
+{
+ struct branch_fill_arg a;
+ VALUE template = branch_template(branches);
+ a.counters = RARRAY_AREF(branches, 1);
+ a.result = rb_hash_dup(template);
+ rb_hash_stlike_foreach_with_replace(a.result, branch_fill_check, branch_fill_children, (st_data_t)&a);
+ return a.result;
}
+struct method_template_add_arg {
+ long skip;
+ long i;
+ int check_me_set; /* skip the entries in me_set (they are already added) */
+};
+
static void
-method_coverage_i(const struct rb_coverage_method_data *method, void *data)
+method_template_add(VALUE me)
{
- VALUE ncoverages = *(VALUE *)data;
- VALUE ncoverage = rb_hash_aref(ncoverages, method->path);
-
- if (!NIL_P(ncoverage)) {
- VALUE methods = rb_hash_aref(ncoverage, ID2SYM(rb_intern("methods")));
- VALUE key = rb_ary_new_from_args(6, method->owner, method->method_id,
- method->first_lineno, method->first_column,
- method->last_lineno, method->last_column);
- VALUE rcount = method->count;
- VALUE previous = rb_hash_aref(methods, key);
-
- if (NIL_P(rcount)) rcount = LONG2FIX(0);
- if (NIL_P(previous)) previous = LONG2FIX(0);
- if (!POSFIXABLE(FIX2LONG(rcount) + FIX2LONG(previous))) {
- rcount = LONG2FIX(FIXNUM_MAX);
- }
- else {
- rcount = LONG2FIX(FIX2LONG(rcount) + FIX2LONG(previous));
+ struct rb_coverage_method_data d;
+ VALUE tmpl, key, mes;
+
+ if (!rb_coverage_method_data_of(me, Qnil, &d)) return;
+
+ tmpl = rb_hash_lookup(method_tmpl_by_path, d.path);
+ if (NIL_P(tmpl)) {
+ tmpl = rb_hash_new();
+ rb_hash_aset(method_tmpl_by_path, d.path, tmpl);
+ }
+ key = rb_ary_new_from_args(6, d.owner, d.method_id,
+ d.first_lineno, d.first_column,
+ d.last_lineno, d.last_column);
+ rb_ary_freeze(key);
+ mes = rb_hash_lookup(tmpl, key);
+ if (NIL_P(mes)) {
+ rb_hash_aset(tmpl, key, me);
+ }
+ else if (RB_TYPE_P(mes, T_ARRAY)) {
+ rb_ary_push(mes, me);
+ }
+ else {
+ VALUE ary = rb_ary_hidden_new(2);
+ rb_ary_push(ary, mes);
+ rb_ary_push(ary, me);
+ rb_hash_aset(tmpl, key, ary);
+ }
+}
+
+static int
+method_template_add_i(VALUE me, VALUE value, VALUE data)
+{
+ struct method_template_add_arg *arg = (struct method_template_add_arg *)data;
+ if (arg->i++ >= arg->skip) {
+ if (arg->check_me_set && RTEST(rb_hash_lookup2(me_set, me, Qfalse))) return ST_CONTINUE;
+ method_template_add(me);
+ }
+ return ST_CONTINUE;
+}
+
+static void
+method_template_update(void)
+{
+ struct method_template_add_arg arg;
+
+ if (NIL_P(method_tmpl_by_path)) {
+ method_tmpl_by_path = rb_hash_new();
+ method_tmpl_n_me_set = 0;
+ method_tmpl_n_cme2counter = 0;
+ }
+ if (RTEST(me_set) && RHASH_SIZE(me_set) > (size_t)method_tmpl_n_me_set) {
+ arg.skip = method_tmpl_n_me_set;
+ arg.i = 0;
+ arg.check_me_set = 0;
+ rb_hash_foreach(me_set, method_template_add_i, (VALUE)&arg);
+ method_tmpl_n_me_set = arg.i;
+ }
+ if (RTEST(cme2counter) && RHASH_SIZE(cme2counter) > (size_t)method_tmpl_n_cme2counter) {
+ arg.skip = method_tmpl_n_cme2counter;
+ arg.i = 0;
+ arg.check_me_set = RTEST(me_set);
+ rb_hash_foreach(cme2counter, method_template_add_i, (VALUE)&arg);
+ method_tmpl_n_cme2counter = arg.i;
+ }
+}
+
+static int
+method_fill_check(st_data_t key, st_data_t value, st_data_t argp, int error)
+{
+ return ST_REPLACE;
+}
+
+static long
+method_call_count(VALUE me)
+{
+ VALUE c = rb_hash_lookup2(cme2counter, me, Qnil);
+ return FIXNUM_P(c) ? FIX2LONG(c) : 0;
+}
+
+/* methods: {key => me or [me, ...]} -> {key => count}, where count is the
+ * sum of the call counts of all the method entries sharing the key
+ * (methods redefined at the same location) */
+static int
+method_fill_count(st_data_t *key, st_data_t *value, st_data_t argp, int existing)
+{
+ VALUE mes = (VALUE)*value;
+ long count;
+ if (RB_TYPE_P(mes, T_ARRAY)) {
+ long i;
+ count = 0;
+ for (i = 0; i < RARRAY_LEN(mes); i++) {
+ count += method_call_count(RARRAY_AREF(mes, i));
+ if (!POSFIXABLE(count)) count = FIXNUM_MAX;
}
- rb_hash_aset(methods, key, rcount);
}
+ else {
+ count = method_call_count(mes);
+ }
+ *value = (st_data_t)LONG2FIX(count);
+ return ST_CONTINUE;
+}
+
+static VALUE
+method_coverage(VALUE path)
+{
+ VALUE tmpl = rb_hash_lookup(method_tmpl_by_path, path);
+ VALUE methods;
+ if (NIL_P(tmpl)) {
+ methods = rb_hash_new();
+ }
+ else {
+ methods = rb_hash_dup(tmpl);
+ rb_hash_stlike_foreach_with_replace(methods, method_fill_check, method_fill_count, 0);
+ }
+ return methods;
}
static int
@@ -294,7 +480,7 @@ coverage_peek_result_i(st_data_t key, st_data_t val, st_data_t h)
}
if (current_mode & COVERAGE_TARGET_METHODS) {
- rb_hash_aset(h, ID2SYM(rb_intern("methods")), rb_hash_new());
+ rb_hash_aset(h, ID2SYM(rb_intern("methods")), method_coverage(path));
}
coverage = h;
@@ -325,11 +511,10 @@ rb_coverage_peek_result(VALUE klass)
rb_raise(rb_eRuntimeError, "coverage measurement is not enabled");
}
- rb_hash_foreach(coverages, coverage_peek_result_i, ncoverages);
-
if (current_mode & COVERAGE_TARGET_METHODS) {
- rb_coverage_each_method(method_coverage_i, &ncoverages);
+ method_template_update();
}
+ rb_hash_foreach(coverages, coverage_peek_result_i, ncoverages);
rb_hash_freeze(ncoverages);
return ncoverages;
@@ -406,6 +591,7 @@ rb_coverage_result(int argc, VALUE *argv, VALUE klass)
rb_reset_coverages();
cme2counter = Qnil;
me_set = Qnil;
+ method_template_reset();
current_state = IDLE;
}
return ncoverages;
@@ -671,4 +857,5 @@ Init_coverage(void)
rb_define_module_function(rb_mCoverage, "running?", rb_coverage_running, 0);
rb_global_variable(&cme2counter);
rb_global_variable(&me_set);
+ rb_global_variable(&method_tmpl_by_path);
}
diff --git a/file.c b/file.c
index 5ba0d104b4eeb4..ddea32d0e54710 100644
--- a/file.c
+++ b/file.c
@@ -1108,28 +1108,45 @@ static VALUE statx_birthtime(const rb_io_stat_data *st);
/*
* call-seq:
- * atime -> new_time
+ * atime -> time
*
* Returns a new Time object containing the access time
* of the object represented by +self+
* at the time +self+ was created;
- * see {Snapshot}[rdoc-ref:File::Stat@Snapshot]:
+ * see {Snapshot}[rdoc-ref:File::Stat@Snapshot].
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
+ *
+ * Access time for a file is established when it is created,
+ * and may be updated when the file content is read:
*
* filepath = 't.tmp'
- * File.write(filepath, 'foo')
- * file = File.new(filepath, 'w')
- * stat = File::Stat.new(filepath)
- * file.atime # => 2026-03-31 16:26:39.5913207 -0500
- * stat.atime # => 2026-03-31 16:26:39.5913207 -0500
- * File.write(filepath, 'bar')
- * file.atime # => 2026-03-31 16:27:01.4981624 -0500 # Changed by access.
- * stat.atime # => 2026-03-31 16:26:39.5913207 -0500 # Unchanged by access.
- * stat = File::Stat.new(filepath)
- * stat.atime # => 2026-03-31 16:27:01.4981624 -0500 # New access time.
+ * File.exist?(filepath) # => false
+ * file = File.open(filepath, 'w+') # Create by writing; establishes access time.
+ * file.atime # => 2026-08-14 11:55:55.436283939 -0500
+ * stat0 = File::Stat.new(filepath) # Take snapshot.
+ * stat0.atime # => 2026-08-14 11:55:55.436283939 -0500
+ * file.read # Read file content; updates file access time.
+ * file.atime # => 2026-08-14 11:56:22.74241085 -0500
+ * stat0.atime # => 2026-08-14 11:55:55.436283939 -0500 # Not updated.
+ * stat1 = File::Stat.new(filepath) # Take new snapshot.
+ * stat1.atime # => 2026-08-14 11:56:22.74241085 -0500 # Updated.
+ * # Clean up.
* file.close
* File.delete(filepath)
*
- * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
+ * Access time for a directory is established when it is created,
+ * and may be updated when its entries are read:
+ *
+ * dirpath = 'foo'
+ * File.exist?(dirpath) # => false
+ * FileUtils.cp_r('doc', 'foo') # Create directory by copying.
+ * File.atime(dirpath) # => 2026-08-15 14:10:04.832180372 -0500
+ * stat = File::Stat.new(dirpath)
+ * stat.atime # => 2026-08-15 14:10:04.832180372 -0500
+ * # Clean up.
+ * FileUtils.rm_rf(dirpath)
+ * dir.close
+ *
*/
static VALUE
@@ -2524,25 +2541,42 @@ rb_file_s_ftype(VALUE klass, VALUE fname)
/*
* call-seq:
- * File.atime(object) -> new_time
+ * File.atime(object) -> time
*
* Returns a new Time object containing the time of the most recent
- * access (read or write) to the object,
- * which may be a string filepath or dirpath, or a File or Dir object:
+ * access to the given +object+.
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
+ *
+ * Access time for a file is established when it is created,
+ * and may be updated when the file content is read:
*
* filepath = 't.tmp'
- * File.exist?(filepath) # => false
- * File.atime(filepath) # Raises Errno::ENOENT.
- * File.write(filepath, 'foo')
- * File.atime(filepath) # => 2026-03-31 16:39:37.9290772 -0500
- * File.write(filepath, 'bar')
- * File.atime(filepath) # => 2026-03-31 16:39:57.7710876 -0500
- *
- * File.atime('.') # => 2026-03-31 16:47:49.0970483 -0500
+ * File.exist?(filepath) # => false
+ * File.atime(filepath) # Raises Errno::ENOENT.
+ * File.write(filepath, 'foo') # Create by writing; establishes access time.
+ * File.atime(filepath) # => 2026-08-14 10:02:39.721407762 -0500
+ * File.read(filepath) # Read file content; updates access time.
+ * File.atime(filepath) # => 2026-08-14 10:03:02.520494995 -0500
+ * File.delete(filepath) # Clean up.
+ *
+ * Access time for a directory is established when it is created,
+ * and may updated when its entries are read:
+ *
+ * dirpath = 'foo'
+ * File.exist?(dirpath) # => false
+ * File.atime(dirpath) # Raises Errno::ENOENT.
+ * FileUtils.cp_r('doc', 'foo') # Create by copying; establishes access time.
+ * File.atime(dirpath) # => 2026-08-14 10:32:59.229951125 -0500
+ * Dir.entries(dirpath) # Read directory entries; updates access time.
+ * File.atime(dirpath) # => 2026-08-14 10:33:05.679978581 -0500
+ * FileUtils.rm_rf(dirpath) # Clean up.
+ *
+ * Argument +object+ may be a string path (as above),
+ * a File object, or a Dir object:
+ *
* File.atime(File.new('README.md')) # => 2026-03-31 11:15:27.8215934 -0500
* File.atime(Dir.new('.')) # => 2026-03-31 12:39:45.5910591 -0500
*
- * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
@@ -2560,22 +2594,25 @@ rb_file_s_atime(VALUE klass, VALUE fname)
/*
* call-seq:
- * atime -> new_time
+ * atime -> time
*
* Returns a new Time object containing the time of the most recent
- * access (read or write) to the file represented by +self+:
+ * access to +self+.
+ * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
+ *
+ * Access time for a file is established when it is created,
+ * and may be updated when the file content is read:
*
* filepath = 't.tmp'
- * file = File.new(filepath, 'a+')
- * file.atime # => 2026-03-31 17:11:27.7285397 -0500
- * file.write('foo')
- * file.atime # => 2026-03-31 17:11:27.7285397 -0500 # Unchanged; not yet written.
- * file.flush
- * file.atime # => 2026-03-31 17:12:11.3408054 -0500 # Changed; now written.
+ * File.exist?(filepath) # => false
+ * file = File.open(filepath, 'w+') # Create by opening; establishes access time.
+ * file.atime # => 2026-08-14 11:15:48.422773736 -0500
+ * file.read # Read file content; updates access time.
+ * file.atime # => 2026-08-14 11:16:10.697861103 -0500
+ * # Clean up.
* file.close
- * File.delete(filename)
+ * File.delete(filepath)
*
- * See {File System Timestamps}[rdoc-ref:file/timestamps.md].
*/
static VALUE
diff --git a/internal/coverage.h b/internal/coverage.h
index 9c78661671dc23..f07f384dd06ada 100644
--- a/internal/coverage.h
+++ b/internal/coverage.h
@@ -41,6 +41,7 @@ void rb_reset_coverages(void);
void rb_resume_coverages(void);
void rb_suspend_coverages(void);
void rb_coverage_each_method(rb_coverage_method_callback callback, void *data);
+bool rb_coverage_method_data_of(VALUE me, VALUE count, struct rb_coverage_method_data *out);
RUBY_SYMBOL_EXPORT_END
diff --git a/internal/hash.h b/internal/hash.h
index 34fd659384e9f5..acc81978d9004b 100644
--- a/internal/hash.h
+++ b/internal/hash.h
@@ -84,7 +84,6 @@ VALUE rb_hash_rehash(VALUE hash);
int rb_hash_add_new_element(VALUE hash, VALUE key, VALUE val);
VALUE rb_hash_set_pair(VALUE hash, VALUE pair);
int rb_hash_stlike_delete(VALUE hash, st_data_t *pkey, st_data_t *pval);
-int rb_hash_stlike_foreach_with_replace(VALUE hash, st_foreach_check_callback_func *func, st_update_callback_func *replace, st_data_t arg);
int rb_hash_stlike_update(VALUE hash, st_data_t key, st_update_callback_func *func, st_data_t arg);
bool rb_hash_default_unredefined(VALUE hash);
VALUE rb_hash_alloc_fixed_size(VALUE klass, st_index_t size);
@@ -108,6 +107,7 @@ RUBY_SYMBOL_EXPORT_BEGIN
VALUE rb_hash_delete_entry(VALUE hash, VALUE key);
VALUE rb_ident_hash_new(void);
int rb_hash_stlike_foreach(VALUE hash, st_foreach_callback_func *func, st_data_t arg);
+int rb_hash_stlike_foreach_with_replace(VALUE hash, st_foreach_check_callback_func *func, st_update_callback_func *replace, st_data_t arg);
RUBY_SYMBOL_EXPORT_END
VALUE rb_hash_new_with_bulk_insert(long argc, const VALUE *argv);
diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb
index ce6f5fdcf402be..ecaca924205688 100644
--- a/lib/bundler/fetcher.rb
+++ b/lib/bundler/fetcher.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "vendored_persistent"
+require_relative "vendored_net_http"
require_relative "vendored_timeout"
require_relative "vendored_securerandom"
require "zlib"
@@ -10,6 +10,7 @@ module Bundler
class Fetcher
autoload :Base, File.expand_path("fetcher/base", __dir__)
autoload :CompactIndex, File.expand_path("fetcher/compact_index", __dir__)
+ autoload :ConnectionPools, File.expand_path("fetcher/connection_pools", __dir__)
autoload :Downloader, File.expand_path("fetcher/downloader", __dir__)
autoload :Dependency, File.expand_path("fetcher/dependency", __dir__)
autoload :Index, File.expand_path("fetcher/index", __dir__)
@@ -137,7 +138,7 @@ def initialize(remote)
@remote = remote
Socket.do_not_reverse_lookup = true
- connection # create persistent connection
+ connection # set up the connection pools eagerly so SSL support is checked upfront
end
def uri
@@ -237,7 +238,7 @@ def user_agent
end
def http_proxy
- return unless uri = connection.proxy_uri
+ return unless uri = connection.proxy_for(remote_uri)
uri.to_s
end
@@ -307,28 +308,7 @@ def connection
end
end
- con = Gem::Net::HTTP::Persistent.new name: "bundler", proxy: :ENV
- if gem_proxy = Gem.configuration[:http_proxy]
- con.proxy = Gem::URI.parse(gem_proxy) if gem_proxy != :no_proxy
- end
-
- if remote_uri.scheme == "https"
- con.verify_mode = (Bundler.settings[:ssl_verify_mode] ||
- OpenSSL::SSL::VERIFY_PEER)
- con.cert_store = bundler_cert_store
- end
-
- ssl_client_cert = Bundler.settings[:ssl_client_cert] ||
- (Gem.configuration.ssl_client_cert if
- Gem.configuration.respond_to?(:ssl_client_cert))
- if ssl_client_cert
- pem = File.read(ssl_client_cert)
- con.cert = OpenSSL::X509::Certificate.new(pem)
- con.key = OpenSSL::PKey.read(pem)
- end
-
- con.read_timeout = Fetcher.api_timeout
- con.open_timeout = Fetcher.api_timeout
+ con = ConnectionPools.new(size: Bundler.settings.processor_count, timeout: Fetcher.api_timeout)
con.override_headers["User-Agent"] = user_agent
con.override_headers["X-Gemfile-Source"] = @remote.original_uri.to_s if @remote.original_uri
con
@@ -341,25 +321,6 @@ def gemspec_cached_path(spec_file_name)
paths.find {|path| File.file? path }
end
- def bundler_cert_store
- store = OpenSSL::X509::Store.new
- ssl_ca_cert = Bundler.settings[:ssl_ca_cert] ||
- (Gem.configuration.ssl_ca_cert if
- Gem.configuration.respond_to?(:ssl_ca_cert))
- if ssl_ca_cert
- if File.directory? ssl_ca_cert
- store.add_path ssl_ca_cert
- else
- store.add_file ssl_ca_cert
- end
- else
- store.set_default_paths
- require "rubygems/request"
- Gem::Request.get_cert_files.each {|c| store.add_file c }
- end
- store
- end
-
def remote_uri
@remote.uri
end
diff --git a/lib/bundler/fetcher/connection_pools.rb b/lib/bundler/fetcher/connection_pools.rb
new file mode 100644
index 00000000000000..a2e72af74b8020
--- /dev/null
+++ b/lib/bundler/fetcher/connection_pools.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require "rubygems/request"
+require "rubygems/remote_fetcher"
+
+module Bundler
+ class Fetcher
+ # Hands out pools of configured Gem::Net::HTTP connections, compatible
+ # with the checkout/checkin interface that Gem::Request expects.
+ # Connection policy (proxy, SSL, timeouts) follows Bundler settings
+ # first, falling back to RubyGems configuration.
+ class ConnectionPools
+ # A fixed-size pool of connections to a single host, sharing the
+ # interface of Gem::Request::HTTPPool.
+ class Pool
+ attr_reader :cert_files, :proxy_uri
+
+ def initialize(connections, uri, proxy_uri, size)
+ @connections = connections
+ @uri = uri
+ @proxy_uri = proxy_uri
+ @cert_files = connections.cert_files
+ @queue = Thread::SizedQueue.new(size)
+ size.times { @queue.push(nil) }
+ end
+
+ def checkout
+ @queue.pop || @connections.build_connection(@uri, @proxy_uri)
+ end
+
+ def checkin(connection)
+ @queue.push(connection)
+ end
+ end
+
+ attr_reader :override_headers, :cert_files
+
+ def initialize(size:, timeout:)
+ @size = size
+ @timeout = timeout
+ @override_headers = {}
+ @cert_files = Gem::Request.get_cert_files
+ @pools = {}
+ @pool_mutex = Thread::Mutex.new
+ end
+
+ # Performs a GET request through Gem::Request, which handles resetting
+ # and retrying stale connections, and returns the raw response.
+ def request(uri, headers = nil)
+ Gem::Request.new(uri, Gem::Net::HTTP::Get, nil, pool_for(uri)).fetch do |request|
+ @override_headers.each {|key, value| request[key] = value }
+ headers&.each {|key, value| request[key] = value }
+ end
+ end
+
+ def pool_for(uri)
+ key = [uri.scheme, uri.hostname, uri.port]
+ @pool_mutex.synchronize do
+ @pools[key] ||= Pool.new(self, uri, proxy_for(uri), @size)
+ end
+ end
+
+ # The proxy that will be used for +uri+, or nil. RubyGems configuration
+ # takes precedence over the environment, like Gem::RemoteFetcher.
+ # Unlike Gem::Request, an https URI falls back to `http_proxy` when no
+ # https-specific proxy is set, which is what the previous
+ # net-http-persistent based transport did.
+ def proxy_for(uri)
+ proxy = if config_proxy = Gem.configuration[:http_proxy]
+ Gem::Request.proxy_uri(config_proxy)
+ else
+ env_proxy_for(uri)
+ end
+ return unless proxy
+ return unless Gem::URI::Generic.use_proxy?(uri.hostname, nil, uri.port, no_proxy_env)
+ proxy
+ end
+
+ def build_connection(uri, proxy_uri) # :nodoc:
+ args = [uri.hostname, uri.port]
+ args += if proxy_uri
+ [proxy_uri.hostname, proxy_uri.port,
+ Gem::UriFormatter.new(proxy_uri.user).unescape,
+ Gem::UriFormatter.new(proxy_uri.password).unescape]
+ else
+ [nil, nil]
+ end
+
+ connection = Gem::Request::ConnectionPools.client.new(*args)
+ configure_ssl(connection) if uri.scheme == "https"
+ connection.open_timeout = @timeout
+ connection.read_timeout = @timeout
+ connection.start
+ connection
+ end
+
+ private
+
+ def env_proxy_for(uri)
+ proxy = Gem::Request.get_proxy_from_env(uri.scheme)
+ proxy = Gem::Request.get_proxy_from_env("http") if proxy == :no_proxy && uri.scheme == "https"
+ proxy == :no_proxy ? nil : proxy
+ end
+
+ def no_proxy_env
+ ENV["no_proxy"] || ENV["NO_PROXY"] || ""
+ end
+
+ def configure_ssl(connection)
+ connection.use_ssl = true
+ connection.verify_mode = Bundler.settings[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER
+ connection.cert_store = bundler_cert_store
+
+ ssl_client_cert = Bundler.settings[:ssl_client_cert] ||
+ (Gem.configuration.ssl_client_cert if
+ Gem.configuration.respond_to?(:ssl_client_cert))
+ return unless ssl_client_cert
+
+ pem = File.read(ssl_client_cert)
+ connection.cert = OpenSSL::X509::Certificate.new(pem)
+ connection.key = OpenSSL::PKey.read(pem)
+ end
+
+ def bundler_cert_store
+ store = OpenSSL::X509::Store.new
+ ssl_ca_cert = Bundler.settings[:ssl_ca_cert] ||
+ (Gem.configuration.ssl_ca_cert if
+ Gem.configuration.respond_to?(:ssl_ca_cert))
+ if ssl_ca_cert
+ if File.directory? ssl_ca_cert
+ store.add_path ssl_ca_cert
+ else
+ store.add_file ssl_ca_cert
+ end
+ else
+ store.set_default_paths
+ cert_files.each {|c| store.add_file c }
+ end
+ store
+ end
+ end
+ end
+end
diff --git a/lib/bundler/fetcher/downloader.rb b/lib/bundler/fetcher/downloader.rb
index 179eed83401f03..59e54c819de0bd 100644
--- a/lib/bundler/fetcher/downloader.rb
+++ b/lib/bundler/fetcher/downloader.rb
@@ -6,14 +6,19 @@ class Downloader
HTTP_NON_RETRYABLE_ERRORS = [
SocketError,
Errno::EADDRNOTAVAIL,
+ Errno::ECONNREFUSED,
+ Errno::EHOSTDOWN,
+ Errno::EHOSTUNREACH,
Errno::ENETDOWN,
Errno::ENETUNREACH,
- Gem::Net::HTTP::Persistent::Error,
- Errno::EHOSTUNREACH,
].freeze
+ # The vendored net-http raises Gem::Timeout::Error, but when Gem::Net is
+ # the real Net (hosts without a vendored net-http), timeouts are plain
+ # Timeout::Error subclasses instead.
HTTP_RETRYABLE_ERRORS = [
Gem::Timeout::Error,
+ *(::Timeout::Error if defined?(::Timeout::Error)),
EOFError,
Errno::EINVAL,
Errno::ECONNRESET,
@@ -25,11 +30,11 @@ class Downloader
Zlib::BufError,
].freeze
- attr_reader :connection
+ attr_reader :connections
attr_reader :redirect_limit
- def initialize(connection, redirect_limit)
- @connection = connection
+ def initialize(connections, redirect_limit)
+ @connections = connections
@redirect_limit = redirect_limit
end
@@ -79,23 +84,25 @@ def request(uri, headers)
filtered_uri = URICredentialsFilter.credential_filtered_uri(uri)
Bundler.ui.debug "HTTP GET #{filtered_uri}"
- req = Gem::Net::HTTP::Get.new uri.request_uri, headers
- if uri.user
- user = CGI.unescape(uri.user)
- password = uri.password ? CGI.unescape(uri.password) : nil
- req.basic_auth(user, password)
+ connections.request(uri, headers)
+ rescue Gem::RemoteFetcher::FetchError => e
+ Bundler.ui.trace e
+
+ case e.message
+ when /certificate verify failed/
+ raise CertificateFailureError.new(uri)
+ when /host is down|host down/i
+ raise network_down_error(uri, filtered_uri)
+ else
+ raise HTTPError, "Network error while fetching #{filtered_uri}" \
+ " (#{e})"
end
- connection.request(uri, req)
rescue OpenSSL::SSL::SSLError
raise CertificateFailureError.new(uri)
rescue *HTTP_NON_RETRYABLE_ERRORS => e
Bundler.ui.trace e
- host = uri.host
- host_port = "#{host}:#{uri.port}"
- host = host_port if filtered_uri.to_s.include?(host_port)
- raise NetworkDownError, "Could not reach host #{host}. Check your network " \
- "connection and try again."
+ raise network_down_error(uri, filtered_uri)
rescue *HTTP_RETRYABLE_ERRORS => e
Bundler.ui.trace e
@@ -105,6 +112,14 @@ def request(uri, headers)
private
+ def network_down_error(uri, filtered_uri)
+ host = uri.host
+ host_port = "#{host}:#{uri.port}"
+ host = host_port if filtered_uri.to_s.include?(host_port)
+ NetworkDownError.new("Could not reach host #{host}. Check your network " \
+ "connection and try again.")
+ end
+
def validate_uri_scheme!(uri)
return if /\Ahttps?\z/.match?(uri.scheme)
raise InvalidOption,
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool.rb
deleted file mode 100644
index c52694160800d7..00000000000000
--- a/lib/bundler/vendor/connection_pool/lib/connection_pool.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-require_relative "../../../vendored_timeout"
-require_relative "connection_pool/version"
-
-class Bundler::ConnectionPool
- class Error < ::RuntimeError; end
-
- class PoolShuttingDownError < ::Bundler::ConnectionPool::Error; end
-
- class TimeoutError < ::Gem::Timeout::Error; end
-end
-
-# Generic connection pool class for sharing a limited number of objects or network connections
-# among many threads. Note: pool elements are lazily created.
-#
-# Example usage with block (faster):
-#
-# @pool = Bundler::ConnectionPool.new { Redis.new }
-# @pool.with do |redis|
-# redis.lpop('my-list') if redis.llen('my-list') > 0
-# end
-#
-# Using optional timeout override (for that single invocation)
-#
-# @pool.with(timeout: 2.0) do |redis|
-# redis.lpop('my-list') if redis.llen('my-list') > 0
-# end
-#
-# Example usage replacing an existing connection (slower):
-#
-# $redis = Bundler::ConnectionPool.wrap { Redis.new }
-#
-# def do_work
-# $redis.lpop('my-list') if $redis.llen('my-list') > 0
-# end
-#
-# Accepts the following options:
-# - :size - number of connections to pool, defaults to 5
-# - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds
-# - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true
-#
-class Bundler::ConnectionPool
- def self.wrap(**, &)
- Wrapper.new(**, &)
- end
-
- attr_reader :size
-
- def initialize(timeout: 5, size: 5, auto_reload_after_fork: true, name: nil, &)
- raise ArgumentError, "Connection pool requires a block" unless block_given?
-
- @size = Integer(size)
- @timeout = Float(timeout)
- @available = TimedStack.new(size: @size, &)
- @key = :"pool-#{@available.object_id}"
- @key_count = :"pool-#{@available.object_id}-count"
- @discard_key = :"pool-#{@available.object_id}-discard"
- INSTANCES[self] = self if auto_reload_after_fork && INSTANCES
- end
-
- def with(**)
- # We need to manage exception handling manually here in order
- # to work correctly with `Gem::Timeout.timeout` and `Thread#raise`.
- # Otherwise an interrupted Thread can leak connections.
- Thread.handle_interrupt(Exception => :never) do
- conn = checkout(**)
- begin
- Thread.handle_interrupt(Exception => :immediate) do
- yield conn
- end
- ensure
- checkin
- end
- end
- end
- alias_method :then, :with
-
- ##
- # Marks the current thread's checked-out connection for discard.
- #
- # When a connection is marked for discard, it will not be returned to the pool
- # when checked in. Instead, the connection will be discarded.
- # This is useful when a connection has become invalid or corrupted
- # and should not be reused.
- #
- # Takes an optional block that will be called with the connection to be discarded.
- # The block should perform any necessary clean-up on the connection.
- #
- # @yield [conn]
- # @yieldparam conn [Object] The connection to be discarded.
- # @yieldreturn [void]
- #
- #
- # Note: This only affects the connection currently checked out by the calling thread.
- # The connection will be discarded when +checkin+ is called.
- #
- # @return [void]
- #
- # @example
- # pool.with do |conn|
- # begin
- # conn.execute("SELECT 1")
- # rescue SomeConnectionError
- # pool.discard_current_connection # Mark connection as bad
- # raise
- # end
- # end
- def discard_current_connection(&block)
- ::Thread.current[@discard_key] = block || proc { |conn| conn }
- end
-
- def checkout(timeout: @timeout, **)
- if ::Thread.current[@key]
- ::Thread.current[@key_count] += 1
- ::Thread.current[@key]
- else
- conn = @available.pop(timeout:, **)
- ::Thread.current[@key] = conn
- ::Thread.current[@key_count] = 1
- conn
- end
- end
-
- def checkin(force: false)
- if ::Thread.current[@key]
- if ::Thread.current[@key_count] == 1 || force
- if ::Thread.current[@discard_key]
- begin
- @available.decrement_created
- ::Thread.current[@discard_key].call(::Thread.current[@key])
- rescue
- nil
- ensure
- ::Thread.current[@discard_key] = nil
- end
- else
- @available.push(::Thread.current[@key])
- end
- ::Thread.current[@key] = nil
- ::Thread.current[@key_count] = nil
- else
- ::Thread.current[@key_count] -= 1
- end
- elsif !force
- raise Bundler::ConnectionPool::Error, "no connections are checked out"
- end
-
- nil
- end
-
- ##
- # Shuts down the Bundler::ConnectionPool by passing each connection to +block+ and
- # then removing it from the pool. Attempting to checkout a connection after
- # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+.
- def shutdown(&)
- @available.shutdown(&)
- end
-
- ##
- # Reloads the Bundler::ConnectionPool by passing each connection to +block+ and then
- # removing it the pool. Subsequent checkouts will create new connections as
- # needed.
- def reload(&)
- @available.shutdown(reload: true, &)
- end
-
- ## Reaps idle connections that have been idle for over +idle_seconds+.
- # +idle_seconds+ defaults to 60.
- def reap(idle_seconds: 60, &)
- @available.reap(idle_seconds:, &)
- end
-
- # Number of pool entries available for checkout at this instant.
- def available
- @available.length
- end
-
- # Number of pool entries created and idle in the pool.
- def idle
- @available.idle
- end
-end
-
-require_relative "connection_pool/timed_stack"
-require_relative "connection_pool/wrapper"
-require_relative "connection_pool/fork"
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb
deleted file mode 100644
index 69683d16c26800..00000000000000
--- a/lib/bundler/vendor/connection_pool/lib/connection_pool/fork.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-class Bundler::ConnectionPool
- if Process.respond_to?(:fork)
- INSTANCES = ObjectSpace::WeakMap.new
- private_constant :INSTANCES
-
- def self.after_fork
- INSTANCES.each_value do |pool|
- # We're in after_fork, so we know all other threads are dead.
- # All we need to do is ensure the main thread doesn't have a
- # checked out connection
- pool.checkin(force: true)
- pool.reload do |connection|
- # Unfortunately we don't know what method to call to close the connection,
- # so we try the most common one.
- connection.close if connection.respond_to?(:close)
- end
- end
- nil
- end
-
- module ForkTracker
- def _fork
- pid = super
- if pid == 0
- Bundler::ConnectionPool.after_fork
- end
- pid
- end
- end
- Process.singleton_class.prepend(ForkTracker)
- else
- # JRuby, et al
- INSTANCES = nil
- private_constant :INSTANCES
-
- def self.after_fork
- # noop
- end
- end
-end
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb
deleted file mode 100644
index d62a44159b5db7..00000000000000
--- a/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb
+++ /dev/null
@@ -1,236 +0,0 @@
-##
-# The TimedStack manages a pool of homogeneous connections (or any resource
-# you wish to manage). Connections are created lazily up to a given maximum
-# number.
-#
-# Examples:
-#
-# ts = TimedStack.new(size: 1) { MyConnection.new }
-#
-# # fetch a connection
-# conn = ts.pop
-#
-# # return a connection
-# ts.push conn
-#
-# conn = ts.pop
-# ts.pop timeout: 5
-# #=> raises Bundler::ConnectionPool::TimeoutError after 5 seconds
-class Bundler::ConnectionPool::TimedStack
- attr_reader :max
-
- ##
- # Creates a new pool with +size+ connections that are created from the given
- # +block+.
- def initialize(size: 0, &block)
- @create_block = block
- @created = 0
- @que = []
- @max = size
- @mutex = Thread::Mutex.new
- @resource = Thread::ConditionVariable.new
- @shutdown_block = nil
- end
-
- ##
- # Returns +obj+ to the stack. Additional kwargs are ignored in TimedStack but may be
- # used by subclasses that extend TimedStack.
- def push(obj, **)
- @mutex.synchronize do
- if @shutdown_block
- @created -= 1 unless @created == 0
- @shutdown_block.call(obj)
- else
- store_connection obj, **
- end
-
- @resource.broadcast
- end
- end
- alias_method :<<, :push
-
- ##
- # Retrieves a connection from the stack. If a connection is available it is
- # immediately returned. If no connection is available within the given
- # timeout a Bundler::ConnectionPool::TimeoutError is raised.
- #
- # @option options [Float] :timeout (0.5) Wait this many seconds for an available entry
- # @option options [Class] :exception (Bundler::ConnectionPool::TimeoutError) Exception class to raise
- # if an entry was not available within the timeout period. Use `exception: false` to return nil.
- #
- # Other options may be used by subclasses that extend TimedStack.
- def pop(timeout: 0.5, exception: Bundler::ConnectionPool::TimeoutError, **)
- deadline = current_time + timeout
- @mutex.synchronize do
- loop do
- raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
- if (conn = try_fetch_connection(**))
- return conn
- end
-
- connection = try_create(**)
- return connection if connection
-
- to_wait = deadline - current_time
- if to_wait <= 0
- if exception
- raise exception, "Waited #{timeout} sec, #{length}/#{@max} available"
- else
- return nil
- end
- end
- @resource.wait(@mutex, to_wait)
- end
- end
- end
-
- ##
- # Shuts down the TimedStack by passing each connection to +block+ and then
- # removing it from the pool. Attempting to checkout a connection after
- # shutdown will raise +Bundler::ConnectionPool::PoolShuttingDownError+ unless
- # +:reload+ is +true+.
- def shutdown(reload: false, &block)
- raise ArgumentError, "shutdown must receive a block" unless block
-
- @mutex.synchronize do
- @shutdown_block = block
- @resource.broadcast
-
- shutdown_connections
- @shutdown_block = nil if reload
- end
- end
-
- ##
- # Reaps connections that were checked in more than +idle_seconds+ ago.
- def reap(idle_seconds:)
- raise ArgumentError, "reap must receive a block" unless block_given?
- raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric)
- raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
-
- count = idle
- count.times do
- conn = @mutex.synchronize do
- raise Bundler::ConnectionPool::PoolShuttingDownError if @shutdown_block
- reserve_idle_connection(idle_seconds)
- end
- break unless conn
-
- yield conn
- end
- end
-
- ##
- # Returns +true+ if there are no available connections.
- def empty?
- (@created - @que.length) >= @max
- end
-
- ##
- # The number of connections available on the stack.
- def length
- @max - @created + @que.length
- end
-
- ##
- # The number of connections created and available on the stack.
- def idle
- @que.length
- end
-
- ##
- # Reduce the created count
- def decrement_created
- @created -= 1 unless @created == 0
- end
-
- private
-
- def current_time
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method must returns a connection from the stack if one exists. Allows
- # subclasses with expensive match/search algorithms to avoid double-handling
- # their stack.
- def try_fetch_connection(**)
- connection_stored?(**) && fetch_connection(**)
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method must returns true if a connection is available on the stack.
- def connection_stored?(**)
- !@que.empty?
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method must return a connection from the stack.
- def fetch_connection(**)
- @que.pop&.first
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method must shut down all connections on the stack.
- def shutdown_connections(**)
- while (conn = try_fetch_connection(**))
- @created -= 1 unless @created == 0
- @shutdown_block.call(conn)
- end
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method returns the oldest idle connection if it has been idle for more than idle_seconds.
- # This requires that the stack is kept in order of checked in time (oldest first).
- def reserve_idle_connection(idle_seconds)
- return unless idle_connections?(idle_seconds)
-
- @created -= 1 unless @created == 0
-
- # Most active elements are at the tail of the array.
- # Most idle will be at the head so `shift` rather than `pop`.
- @que.shift.first
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # Returns true if the first connection in the stack has been idle for more than idle_seconds
- def idle_connections?(idle_seconds)
- return unless connection_stored?
- # Most idle will be at the head so `first`
- age = (current_time - @que.first.last)
- age > idle_seconds
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method must return +obj+ to the stack.
- def store_connection(obj, **)
- @que.push [obj, current_time]
- end
-
- ##
- # This is an extension point for TimedStack and is called with a mutex.
- #
- # This method must create a connection if and only if the total number of
- # connections allowed has not been met.
- def try_create(**)
- unless @created == @max
- object = @create_block.call
- @created += 1
- object
- end
- end
-end
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb
deleted file mode 100644
index 509d5af2db81fd..00000000000000
--- a/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class Bundler::ConnectionPool
- VERSION = "3.0.2"
-end
diff --git a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb b/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb
deleted file mode 100644
index d11d6c8eb4b129..00000000000000
--- a/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-class Bundler::ConnectionPool
- class Wrapper < ::BasicObject
- METHODS = [:with, :pool_shutdown, :wrapped_pool]
-
- def initialize(**options, &)
- @pool = options.fetch(:pool) { ::Bundler::ConnectionPool.new(**options, &) }
- end
-
- def wrapped_pool
- @pool
- end
-
- def with(**, &)
- @pool.with(**, &)
- end
-
- def pool_shutdown(&)
- @pool.shutdown(&)
- end
-
- def pool_size
- @pool.size
- end
-
- def pool_available
- @pool.available
- end
-
- def respond_to?(id, *, **)
- METHODS.include?(id) || with { |c| c.respond_to?(id, *, **) }
- end
-
- def respond_to_missing?(id, *, **)
- with { |c| c.respond_to?(id, *, **) }
- end
-
- def method_missing(name, *, **, &)
- with do |connection|
- connection.send(name, *, **, &)
- end
- end
- end
-end
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb
deleted file mode 100644
index 00f2bca14ad31f..00000000000000
--- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb
+++ /dev/null
@@ -1,1159 +0,0 @@
-require_relative '../../../../../vendored_net_http'
-require_relative '../../../../../vendored_uri'
-require 'cgi/escape'
-require 'cgi/util' unless defined?(CGI::EscapeExt)
-require_relative '../../../../connection_pool/lib/connection_pool'
-
-autoload :OpenSSL, 'openssl'
-
-##
-# Persistent connections for Gem::Net::HTTP
-#
-# Gem::Net::HTTP::Persistent maintains persistent connections across all the
-# servers you wish to talk to. For each host:port you communicate with a
-# single persistent connection is created.
-#
-# Connections will be shared across threads through a connection pool to
-# increase reuse of connections.
-#
-# You can shut down any remaining HTTP connections when done by calling
-# #shutdown.
-#
-# Example:
-#
-# require 'bundler/vendor/net-http-persistent/lib/net/http/persistent'
-#
-# uri = Gem::URI 'http://example.com/awesome/web/service'
-#
-# http = Gem::Net::HTTP::Persistent.new
-#
-# # perform a GET
-# response = http.request uri
-#
-# # or
-#
-# get = Gem::Net::HTTP::Get.new uri.request_uri
-# response = http.request get
-#
-# # create a POST
-# post_uri = uri + 'create'
-# post = Gem::Net::HTTP::Post.new post_uri.path
-# post.set_form_data 'some' => 'cool data'
-#
-# # perform the POST, the Gem::URI is always required
-# response http.request post_uri, post
-#
-# ⚠ Note that for GET, HEAD and other requests that do not have a body,
-# it uses Gem::URI#request_uri as default to send query params
-#
-# == TLS/SSL
-#
-# TLS connections are automatically created depending upon the scheme of the
-# Gem::URI. TLS connections are automatically verified against the default
-# certificate store for your computer. You can override this by changing
-# verify_mode or by specifying an alternate cert_store.
-#
-# Here are the TLS settings, see the individual methods for documentation:
-#
-# #certificate :: This client's certificate
-# #ca_file :: The certificate-authorities
-# #ca_path :: Directory with certificate-authorities
-# #cert_store :: An SSL certificate store
-# #ciphers :: List of SSl ciphers allowed
-# #extra_chain_cert :: Extra certificates to be added to the certificate chain
-# #private_key :: The client's SSL private key
-# #reuse_ssl_sessions :: Reuse a previously opened SSL session for a new
-# connection
-# #ssl_timeout :: Session lifetime
-# #ssl_version :: Which specific SSL version to use
-# #verify_callback :: For server certificate verification
-# #verify_depth :: Depth of certificate verification
-# #verify_mode :: How connections should be verified
-# #verify_hostname :: Use hostname verification for server certificate
-# during the handshake
-#
-# == Proxies
-#
-# A proxy can be set through #proxy= or at initialization time by providing a
-# second argument to ::new. The proxy may be the Gem::URI of the proxy server or
-# :ENV which will consult environment variables.
-#
-# See #proxy= and #proxy_from_env for details.
-#
-# == Headers
-#
-# Headers may be specified for use in every request. #headers are appended to
-# any headers on the request. #override_headers replace existing headers on
-# the request.
-#
-# The difference between the two can be seen in setting the User-Agent. Using
-# http.headers['User-Agent'] = 'MyUserAgent' will send "Ruby,
-# MyUserAgent" while http.override_headers['User-Agent'] =
-# 'MyUserAgent' will send "MyUserAgent".
-#
-# == Tuning
-#
-# === Segregation
-#
-# Each Gem::Net::HTTP::Persistent instance has its own pool of connections. There
-# is no sharing with other instances (as was true in earlier versions).
-#
-# === Idle Timeout
-#
-# If a connection hasn't been used for this number of seconds it will
-# automatically be reset upon the next use to avoid attempting to send to a
-# closed connection. The default value is 5 seconds. nil means no timeout.
-# Set through #idle_timeout.
-#
-# Reducing this value may help avoid the "too many connection resets" error
-# when sending non-idempotent requests while increasing this value will cause
-# fewer round-trips.
-#
-# === Read Timeout
-#
-# The amount of time allowed between reading two chunks from the socket. Set
-# through #read_timeout
-#
-# === Max Requests
-#
-# The number of requests that should be made before opening a new connection.
-# Typically many keep-alive capable servers tune this to 100 or less, so the
-# 101st request will fail with ECONNRESET. If unset (default), this value has
-# no effect, if set, connections will be reset on the request after
-# max_requests.
-#
-# === Open Timeout
-#
-# The amount of time to wait for a connection to be opened. Set through
-# #open_timeout.
-#
-# === Socket Options
-#
-# Socket options may be set on newly-created connections. See #socket_options
-# for details.
-#
-# === Connection Termination
-#
-# If you are done using the Gem::Net::HTTP::Persistent instance you may shut down
-# all the connections in the current thread with #shutdown. This is not
-# recommended for normal use, it should only be used when it will be several
-# minutes before you make another HTTP request.
-#
-# If you are using multiple threads, call #shutdown in each thread when the
-# thread is done making requests. If you don't call shutdown, that's OK.
-# Ruby will automatically garbage collect and shutdown your HTTP connections
-# when the thread terminates.
-
-class Gem::Net::HTTP::Persistent
-
- ##
- # The beginning of Time
-
- EPOCH = Time.at 0 # :nodoc:
-
- ##
- # Is OpenSSL available? This test works with autoload
-
- HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc:
-
- ##
- # The default connection pool size is 1/4 the allowed open files
- # (ulimit -n) or 256 if your OS does not support file handle
- # limits (typically windows).
-
- if Process.const_defined? :RLIMIT_NOFILE
- open_file_limits = Process.getrlimit(Process::RLIMIT_NOFILE)
-
- # Under JRuby on Windows Process responds to `getrlimit` but returns something that does not match docs
- if open_file_limits.respond_to?(:first)
- DEFAULT_POOL_SIZE = open_file_limits.first / 4
- else
- DEFAULT_POOL_SIZE = 256
- end
- else
- DEFAULT_POOL_SIZE = 256
- end
-
- ##
- # The version of Gem::Net::HTTP::Persistent you are using
-
- VERSION = '4.0.8'
-
- ##
- # Error class for errors raised by Gem::Net::HTTP::Persistent. Various
- # SystemCallErrors are re-raised with a human-readable message under this
- # class.
-
- class Error < StandardError; end
-
- ##
- # Use this method to detect the idle timeout of the host at +uri+. The
- # value returned can be used to configure #idle_timeout. +max+ controls the
- # maximum idle timeout to detect.
- #
- # After
- #
- # Idle timeout detection is performed by creating a connection then
- # performing a HEAD request in a loop until the connection terminates
- # waiting one additional second per loop.
- #
- # NOTE: This may not work on ruby > 1.9.
-
- def self.detect_idle_timeout uri, max = 10
- uri = Gem::URI uri unless Gem::URI::Generic === uri
- uri += '/'
-
- req = Gem::Net::HTTP::Head.new uri.request_uri
-
- http = new 'net-http-persistent detect_idle_timeout'
-
- http.connection_for uri do |connection|
- sleep_time = 0
-
- http = connection.http
-
- loop do
- response = http.request req
-
- $stderr.puts "HEAD #{uri} => #{response.code}" if $DEBUG
-
- unless Gem::Net::HTTPOK === response then
- raise Error, "bad response code #{response.code} detecting idle timeout"
- end
-
- break if sleep_time >= max
-
- sleep_time += 1
-
- $stderr.puts "sleeping #{sleep_time}" if $DEBUG
- sleep sleep_time
- end
- end
- rescue
- # ignore StandardErrors, we've probably found the idle timeout.
- ensure
- return sleep_time unless $!
- end
-
- ##
- # This client's OpenSSL::X509::Certificate
-
- attr_reader :certificate
-
- ##
- # For Gem::Net::HTTP parity
-
- alias cert certificate
-
- ##
- # An SSL certificate authority. Setting this will set verify_mode to
- # VERIFY_PEER.
-
- attr_reader :ca_file
-
- ##
- # A directory of SSL certificates to be used as certificate authorities.
- # Setting this will set verify_mode to VERIFY_PEER.
-
- attr_reader :ca_path
-
- ##
- # An SSL certificate store. Setting this will override the default
- # certificate store. See verify_mode for more information.
-
- attr_reader :cert_store
-
- ##
- # The ciphers allowed for SSL connections
-
- attr_reader :ciphers
-
- ##
- # Extra certificates to be added to the certificate chain
-
- attr_reader :extra_chain_cert
-
- ##
- # Sends debug_output to this IO via Gem::Net::HTTP#set_debug_output.
- #
- # Never use this method in production code, it causes a serious security
- # hole.
-
- attr_accessor :debug_output
-
- ##
- # Current connection generation
-
- attr_reader :generation # :nodoc:
-
- ##
- # Headers that are added to every request using Gem::Net::HTTP#add_field
-
- attr_reader :headers
-
- ##
- # Maps host:port to an HTTP version. This allows us to enable version
- # specific features.
-
- attr_reader :http_versions
-
- ##
- # Maximum time an unused connection can remain idle before being
- # automatically closed.
-
- attr_accessor :idle_timeout
-
- ##
- # Maximum number of requests on a connection before it is considered expired
- # and automatically closed.
-
- attr_accessor :max_requests
-
- ##
- # Number of retries to perform if a request fails.
- #
- # See also #max_retries=, Gem::Net::HTTP#max_retries=.
-
- attr_reader :max_retries
-
- ##
- # The value sent in the Keep-Alive header. Defaults to 30. Not needed for
- # HTTP/1.1 servers.
- #
- # This may not work correctly for HTTP/1.0 servers
- #
- # This method may be removed in a future version as RFC 2616 does not
- # require this header.
-
- attr_accessor :keep_alive
-
- ##
- # The name for this collection of persistent connections.
-
- attr_reader :name
-
- ##
- # Seconds to wait until a connection is opened. See Gem::Net::HTTP#open_timeout
-
- attr_accessor :open_timeout
-
- ##
- # Headers that are added to every request using Gem::Net::HTTP#[]=
-
- attr_reader :override_headers
-
- ##
- # This client's SSL private key
-
- attr_reader :private_key
-
- ##
- # For Gem::Net::HTTP parity
-
- alias key private_key
-
- ##
- # The URL through which requests will be proxied
-
- attr_reader :proxy_uri
-
- ##
- # List of host suffixes which will not be proxied
-
- attr_reader :no_proxy
-
- ##
- # Test-only accessor for the connection pool
-
- attr_reader :pool # :nodoc:
-
- ##
- # Seconds to wait until reading one block. See Gem::Net::HTTP#read_timeout
-
- attr_accessor :read_timeout
-
- ##
- # Seconds to wait until writing one block. See Gem::Net::HTTP#write_timeout
-
- attr_accessor :write_timeout
-
- ##
- # By default SSL sessions are reused to avoid extra SSL handshakes. Set
- # this to false if you have problems communicating with an HTTPS server
- # like:
- #
- # SSL_connect [...] read finished A: unexpected message (OpenSSL::SSL::SSLError)
-
- attr_accessor :reuse_ssl_sessions
-
- ##
- # An array of options for Socket#setsockopt.
- #
- # By default the TCP_NODELAY option is set on sockets.
- #
- # To set additional options append them to this array:
- #
- # http.socket_options << [Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1]
-
- attr_reader :socket_options
-
- ##
- # Current SSL connection generation
-
- attr_reader :ssl_generation # :nodoc:
-
- ##
- # SSL session lifetime
-
- attr_reader :ssl_timeout
-
- ##
- # SSL version to use.
- #
- # By default, the version will be negotiated automatically between client
- # and server. Ruby 1.9 and newer only. Deprecated since Ruby 2.5.
-
- attr_reader :ssl_version
-
- ##
- # Minimum SSL version to use, e.g. :TLS1_1
- #
- # By default, the version will be negotiated automatically between client
- # and server. Ruby 2.5 and newer only.
-
- attr_reader :min_version
-
- ##
- # Maximum SSL version to use, e.g. :TLS1_2
- #
- # By default, the version will be negotiated automatically between client
- # and server. Ruby 2.5 and newer only.
-
- attr_reader :max_version
-
- ##
- # Where this instance's last-use times live in the thread local variables
-
- attr_reader :timeout_key # :nodoc:
-
- ##
- # SSL verification callback. Used when ca_file or ca_path is set.
-
- attr_reader :verify_callback
-
- ##
- # Sets the depth of SSL certificate verification
-
- attr_reader :verify_depth
-
- ##
- # HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER which verifies
- # the server certificate.
- #
- # If no ca_file, ca_path or cert_store is set the default system certificate
- # store is used.
- #
- # You can use +verify_mode+ to override any default values.
-
- attr_reader :verify_mode
-
- ##
- # HTTPS verify_hostname.
- #
- # If a client sets this to true and enables SNI with SSLSocket#hostname=,
- # the hostname verification on the server certificate is performed
- # automatically during the handshake using
- # OpenSSL::SSL.verify_certificate_identity().
- #
- # You can set +verify_hostname+ as true to use hostname verification
- # during the handshake.
- #
- # NOTE: This works with Ruby > 3.0.
-
- attr_reader :verify_hostname
-
-
- ##
- # Sets whether to ignore end-of-file when reading a response body
- # with Content-Length headers.
-
- attr_accessor :ignore_eof
-
- ##
- # Creates a new Gem::Net::HTTP::Persistent.
- #
- # Set a +name+ for fun. Your library name should be good enough, but this
- # otherwise has no purpose.
- #
- # +proxy+ may be set to a Gem::URI::HTTP or :ENV to pick up proxy options from
- # the environment. See proxy_from_env for details.
- #
- # In order to use a Gem::URI for the proxy you may need to do some extra work
- # beyond Gem::URI parsing if the proxy requires a password:
- #
- # proxy = Gem::URI 'http://proxy.example'
- # proxy.user = 'AzureDiamond'
- # proxy.password = 'hunter2'
- #
- # Set +pool_size+ to limit the maximum number of connections allowed.
- # Defaults to 1/4 the number of allowed file handles or 256 if your OS does
- # not support a limit on allowed file handles. You can have no more than
- # this many threads with active HTTP transactions.
-
- def initialize name: nil, proxy: nil, pool_size: DEFAULT_POOL_SIZE
- @name = name
-
- @debug_output = nil
- @proxy_uri = nil
- @no_proxy = []
- @headers = {}
- @override_headers = {}
- @http_versions = {}
- @keep_alive = 30
- @open_timeout = nil
- @read_timeout = nil
- @write_timeout = nil
- @idle_timeout = 5
- @max_requests = nil
- @max_retries = 1
- @socket_options = []
- @ssl_generation = 0 # incremented when SSL session variables change
- @ignore_eof = nil
-
- @socket_options << [Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1] if
- Socket.const_defined? :TCP_NODELAY
-
- @pool = Gem::Net::HTTP::Persistent::Pool.new size: pool_size do |http_args|
- Gem::Net::HTTP::Persistent::Connection.new Gem::Net::HTTP, http_args, @ssl_generation
- end
-
- @certificate = nil
- @ca_file = nil
- @ca_path = nil
- @ciphers = nil
- @private_key = nil
- @ssl_timeout = nil
- @ssl_version = nil
- @min_version = nil
- @max_version = nil
- @verify_callback = nil
- @verify_depth = nil
- @verify_mode = nil
- @verify_hostname = nil
- @cert_store = nil
-
- @generation = 0 # incremented when proxy Gem::URI changes
-
- if HAVE_OPENSSL then
- @verify_mode = OpenSSL::SSL::VERIFY_PEER
- @reuse_ssl_sessions = OpenSSL::SSL.const_defined? :Session
- end
-
- self.proxy = proxy if proxy
- end
-
- ##
- # Sets this client's OpenSSL::X509::Certificate
-
- def certificate= certificate
- @certificate = certificate
-
- reconnect_ssl
- end
-
- # For Gem::Net::HTTP parity
- alias cert= certificate=
-
- ##
- # Sets the SSL certificate authority file.
-
- def ca_file= file
- @ca_file = file
-
- reconnect_ssl
- end
-
- ##
- # Sets the SSL certificate authority path.
-
- def ca_path= path
- @ca_path = path
-
- reconnect_ssl
- end
-
- ##
- # Overrides the default SSL certificate store used for verifying
- # connections.
-
- def cert_store= store
- @cert_store = store
-
- reconnect_ssl
- end
-
- ##
- # The ciphers allowed for SSL connections
-
- def ciphers= ciphers
- @ciphers = ciphers
-
- reconnect_ssl
- end
-
- if Gem::Net::HTTP.method_defined?(:extra_chain_cert=)
- ##
- # Extra certificates to be added to the certificate chain.
- # It is only supported starting from Gem::Net::HTTP version 0.1.1
- def extra_chain_cert= extra_chain_cert
- @extra_chain_cert = extra_chain_cert
-
- reconnect_ssl
- end
- else
- def extra_chain_cert= _extra_chain_cert
- raise "extra_chain_cert= is not supported by this version of Gem::Net::HTTP"
- end
- end
-
- ##
- # Creates a new connection for +uri+
-
- def connection_for uri
- use_ssl = uri.scheme.downcase == 'https'
-
- net_http_args = [uri.hostname, uri.port]
-
- # I'm unsure if uri.host or uri.hostname should be checked against
- # the proxy bypass list.
- if @proxy_uri and not proxy_bypass? uri.host, uri.port then
- net_http_args.concat @proxy_args
- else
- net_http_args.concat [nil, nil, nil, nil]
- end
-
- connection = @pool.checkout net_http_args
-
- begin
- http = connection.http
-
- connection.ressl @ssl_generation if
- connection.ssl_generation != @ssl_generation
-
- if not http.started? then
- ssl http if use_ssl
- start http
- elsif expired? connection then
- reset connection
- end
-
- http.ignore_eof = @ignore_eof if @ignore_eof
- http.keep_alive_timeout = @idle_timeout if @idle_timeout
- http.max_retries = @max_retries if http.respond_to?(:max_retries=)
- http.read_timeout = @read_timeout if @read_timeout
- http.write_timeout = @write_timeout if
- @write_timeout && http.respond_to?(:write_timeout=)
-
- return yield connection
- rescue Errno::ECONNREFUSED
- if http.proxy?
- address = http.proxy_address
- port = http.proxy_port
- else
- address = http.address
- port = http.port
- end
-
- raise Error, "connection refused: #{address}:#{port}"
- rescue Errno::EHOSTDOWN
- if http.proxy?
- address = http.proxy_address
- port = http.proxy_port
- else
- address = http.address
- port = http.port
- end
-
- raise Error, "host down: #{address}:#{port}"
- ensure
- @pool.checkin net_http_args
- end
- end
-
- ##
- # CGI::escape wrapper
-
- def escape str
- CGI.escape str if str
- end
-
- ##
- # CGI::unescape wrapper
-
- def unescape str
- CGI.unescape str if str
- end
-
-
- ##
- # Returns true if the connection should be reset due to an idle timeout, or
- # maximum request count, false otherwise.
-
- def expired? connection
- return true if @max_requests && connection.requests >= @max_requests
- return false unless @idle_timeout
- return true if @idle_timeout.zero?
-
- Time.now - connection.last_use > @idle_timeout
- end
-
- ##
- # Starts the Gem::Net::HTTP +connection+
-
- def start http
- http.set_debug_output @debug_output if @debug_output
- http.open_timeout = @open_timeout if @open_timeout
-
- http.start
-
- socket = http.instance_variable_get :@socket
-
- if socket then # for fakeweb
- @socket_options.each do |option|
- socket.io.setsockopt(*option)
- end
- end
- end
-
- ##
- # Finishes the Gem::Net::HTTP +connection+
-
- def finish connection
- connection.finish
-
- connection.http.instance_variable_set :@last_communicated, nil
- connection.http.instance_variable_set :@ssl_session, nil unless
- @reuse_ssl_sessions
- end
-
- ##
- # Returns the HTTP protocol version for +uri+
-
- def http_version uri
- @http_versions["#{uri.hostname}:#{uri.port}"]
- end
-
- ##
- # Adds "http://" to the String +uri+ if it is missing.
-
- def normalize_uri uri
- (uri =~ /^https?:/) ? uri : "http://#{uri}"
- end
-
- ##
- # Set the maximum number of retries for a request.
- #
- # Defaults to one retry.
- #
- # Set this to 0 to disable retries.
-
- def max_retries= retries
- retries = retries.to_int
-
- raise ArgumentError, "max_retries must be positive" if retries < 0
-
- @max_retries = retries
-
- reconnect
- end
-
- ##
- # Sets this client's SSL private key
-
- def private_key= key
- @private_key = key
-
- reconnect_ssl
- end
-
- # For Gem::Net::HTTP parity
- alias key= private_key=
-
- ##
- # Sets the proxy server. The +proxy+ may be the Gem::URI of the proxy server,
- # the symbol +:ENV+ which will read the proxy from the environment or nil to
- # disable use of a proxy. See #proxy_from_env for details on setting the
- # proxy from the environment.
- #
- # If the proxy Gem::URI is set after requests have been made, the next request
- # will shut-down and re-open all connections.
- #
- # The +no_proxy+ query parameter can be used to specify hosts which shouldn't
- # be reached via proxy; if set it should be a comma separated list of
- # hostname suffixes, optionally with +:port+ appended, for example
- # example.com,some.host:8080.
-
- def proxy= proxy
- @proxy_uri = case proxy
- when :ENV then proxy_from_env
- when Gem::URI::HTTP then proxy
- when nil then # ignore
- else raise ArgumentError, 'proxy must be :ENV or a Gem::URI::HTTP'
- end
-
- @no_proxy.clear
-
- if @proxy_uri then
- @proxy_args = [
- @proxy_uri.hostname,
- @proxy_uri.port,
- unescape(@proxy_uri.user),
- unescape(@proxy_uri.password),
- ]
-
- @proxy_connection_id = [nil, *@proxy_args].join ':'
-
- if @proxy_uri.query then
- @no_proxy = Gem::URI.decode_www_form(@proxy_uri.query).filter_map { |k, v| v if k == 'no_proxy' }.join(',').downcase.split(',').map { |x| x.strip }.reject { |x| x.empty? }
- end
- end
-
- reconnect
- reconnect_ssl
- end
-
- ##
- # Creates a Gem::URI for an HTTP proxy server from ENV variables.
- #
- # If +HTTP_PROXY+ is set a proxy will be returned.
- #
- # If +HTTP_PROXY_USER+ or +HTTP_PROXY_PASS+ are set the Gem::URI is given the
- # indicated user and password unless HTTP_PROXY contains either of these in
- # the Gem::URI.
- #
- # The +NO_PROXY+ ENV variable can be used to specify hosts which shouldn't
- # be reached via proxy; if set it should be a comma separated list of
- # hostname suffixes, optionally with +:port+ appended, for example
- # example.com,some.host:8080. When set to * no proxy will
- # be returned.
- #
- # For Windows users, lowercase ENV variables are preferred over uppercase ENV
- # variables.
-
- def proxy_from_env
- env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
-
- return nil if env_proxy.nil? or env_proxy.empty?
-
- uri = Gem::URI normalize_uri env_proxy
-
- env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY']
-
- # '*' is special case for always bypass
- return nil if env_no_proxy == '*'
-
- if env_no_proxy then
- uri.query = "no_proxy=#{escape(env_no_proxy)}"
- end
-
- unless uri.user or uri.password then
- uri.user = escape ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']
- uri.password = escape ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']
- end
-
- uri
- end
-
- ##
- # Returns true when proxy should by bypassed for host.
-
- def proxy_bypass? host, port
- host = host.downcase
- host_port = [host, port].join ':'
-
- @no_proxy.each do |name|
- return true if host[-name.length, name.length] == name or
- host_port[-name.length, name.length] == name
- end
-
- false
- end
-
- ##
- # Forces reconnection of all HTTP connections, including TLS/SSL
- # connections.
-
- def reconnect
- @generation += 1
- end
-
- ##
- # Forces reconnection of only TLS/SSL connections.
-
- def reconnect_ssl
- @ssl_generation += 1
- end
-
- ##
- # Finishes then restarts the Gem::Net::HTTP +connection+
-
- def reset connection
- http = connection.http
-
- finish connection
-
- start http
- rescue Errno::ECONNREFUSED
- e = Error.new "connection refused: #{http.address}:#{http.port}"
- e.set_backtrace $@
- raise e
- rescue Errno::EHOSTDOWN
- e = Error.new "host down: #{http.address}:#{http.port}"
- e.set_backtrace $@
- raise e
- end
-
- ##
- # Makes a request on +uri+. If +req+ is nil a Gem::Net::HTTP::Get is performed
- # against +uri+.
- #
- # If a block is passed #request behaves like Gem::Net::HTTP#request (the body of
- # the response will not have been read).
- #
- # +req+ must be a Gem::Net::HTTPGenericRequest subclass (see Gem::Net::HTTP for a list).
-
- def request uri, req = nil, &block
- uri = Gem::URI uri
- req = request_setup req || uri
- response = nil
-
- connection_for uri do |connection|
- http = connection.http
-
- begin
- connection.requests += 1
-
- response = http.request req, &block
-
- if req.connection_close? or
- (response.http_version <= '1.0' and
- not response.connection_keep_alive?) or
- response.connection_close? then
- finish connection
- end
- rescue Exception # make sure to close the connection when it was interrupted
- finish connection
-
- raise
- ensure
- connection.last_use = Time.now
- end
- end
-
- @http_versions["#{uri.hostname}:#{uri.port}"] ||= response.http_version
-
- response
- end
-
- ##
- # Creates a GET request if +req_or_uri+ is a Gem::URI and adds headers to the
- # request.
- #
- # Returns the request.
-
- def request_setup req_or_uri # :nodoc:
- req = if req_or_uri.respond_to? 'request_uri' then
- Gem::Net::HTTP::Get.new req_or_uri.request_uri
- else
- req_or_uri
- end
-
- @headers.each do |pair|
- req.add_field(*pair)
- end
-
- @override_headers.each do |name, value|
- req[name] = value
- end
-
- unless req['Connection'] then
- req.add_field 'Connection', 'keep-alive'
- req.add_field 'Keep-Alive', @keep_alive
- end
-
- req
- end
-
- ##
- # Shuts down all connections. Attempting to checkout a connection after
- # shutdown will raise an error.
- #
- # *NOTE*: Calling shutdown for can be dangerous!
- #
- # If any thread is still using a connection it may cause an error! Call
- # #shutdown when you are completely done making requests!
-
- def shutdown
- @pool.shutdown { |http| http.finish }
- end
-
- ##
- # Discard all existing connections. Subsequent checkouts will create
- # new connections as needed.
- #
- # If any thread is still using a connection it may cause an error! Call
- # #reload when you are completely done making requests!
-
- def reload
- @pool.reload { |http| http.finish }
- end
-
- ##
- # Enables SSL on +connection+
-
- def ssl connection
- connection.use_ssl = true
-
- connection.ciphers = @ciphers if @ciphers
- connection.ssl_timeout = @ssl_timeout if @ssl_timeout
- connection.ssl_version = @ssl_version if @ssl_version
- connection.min_version = @min_version if @min_version
- connection.max_version = @max_version if @max_version
-
- connection.verify_depth = @verify_depth
- connection.verify_mode = @verify_mode
- connection.verify_hostname = @verify_hostname if
- @verify_hostname != nil && connection.respond_to?(:verify_hostname=)
-
- if OpenSSL::SSL::VERIFY_PEER == OpenSSL::SSL::VERIFY_NONE and
- not Object.const_defined?(:I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG) then
- warn <<-WARNING
- !!!SECURITY WARNING!!!
-
-The SSL HTTP connection to:
-
- #{connection.address}:#{connection.port}
-
- !!!MAY NOT BE VERIFIED!!!
-
-On your platform your OpenSSL implementation is broken.
-
-There is no difference between the values of VERIFY_NONE and VERIFY_PEER.
-
-This means that attempting to verify the security of SSL connections may not
-work. This exposes you to man-in-the-middle exploits, snooping on the
-contents of your connection and other dangers to the security of your data.
-
-To disable this warning define the following constant at top-level in your
-application:
-
- I_KNOW_THAT_OPENSSL_VERIFY_PEER_EQUALS_VERIFY_NONE_IS_WRONG = nil
-
- WARNING
- end
-
- connection.ca_file = @ca_file if @ca_file
- connection.ca_path = @ca_path if @ca_path
-
- if @ca_file or @ca_path then
- connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
- connection.verify_callback = @verify_callback if @verify_callback
- end
-
- if @certificate and @private_key then
- connection.cert = @certificate
- connection.key = @private_key
- end
-
- if defined?(@extra_chain_cert) and @extra_chain_cert
- connection.extra_chain_cert = @extra_chain_cert
- end
-
- connection.cert_store = if @cert_store then
- @cert_store
- else
- store = OpenSSL::X509::Store.new
- store.set_default_paths
- store
- end
- end
-
- ##
- # SSL session lifetime
-
- def ssl_timeout= ssl_timeout
- @ssl_timeout = ssl_timeout
-
- reconnect_ssl
- end
-
- ##
- # SSL version to use
-
- def ssl_version= ssl_version
- @ssl_version = ssl_version
-
- reconnect_ssl
- end
-
- ##
- # Minimum SSL version to use
-
- def min_version= min_version
- @min_version = min_version
-
- reconnect_ssl
- end
-
- ##
- # maximum SSL version to use
-
- def max_version= max_version
- @max_version = max_version
-
- reconnect_ssl
- end
-
- ##
- # Sets the depth of SSL certificate verification
-
- def verify_depth= verify_depth
- @verify_depth = verify_depth
-
- reconnect_ssl
- end
-
- ##
- # Sets the HTTPS verify mode. Defaults to OpenSSL::SSL::VERIFY_PEER.
- #
- # Setting this to VERIFY_NONE is a VERY BAD IDEA and should NEVER be used.
- # Securely transfer the correct certificate and update the default
- # certificate store or set the ca file instead.
-
- def verify_mode= verify_mode
- @verify_mode = verify_mode
-
- reconnect_ssl
- end
-
- ##
- # Sets the HTTPS verify_hostname.
-
- def verify_hostname= verify_hostname
- @verify_hostname = verify_hostname
-
- reconnect_ssl
- end
-
- ##
- # SSL verification callback.
-
- def verify_callback= callback
- @verify_callback = callback
-
- reconnect_ssl
- end
-end
-
-require_relative 'persistent/connection'
-require_relative 'persistent/pool'
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb
deleted file mode 100644
index 8b9ab5cc785b77..00000000000000
--- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-##
-# A Gem::Net::HTTP connection wrapper that holds extra information for managing the
-# connection's lifetime.
-
-class Gem::Net::HTTP::Persistent::Connection # :nodoc:
-
- attr_accessor :http
-
- attr_accessor :last_use
-
- attr_accessor :requests
-
- attr_accessor :ssl_generation
-
- def initialize http_class, http_args, ssl_generation
- @http = http_class.new(*http_args)
- @ssl_generation = ssl_generation
-
- reset
- end
-
- def finish
- @http.finish
- rescue IOError
- ensure
- reset
- end
- alias_method :close, :finish
-
- def reset
- @last_use = Gem::Net::HTTP::Persistent::EPOCH
- @requests = 0
- end
-
- def ressl ssl_generation
- @ssl_generation = ssl_generation
-
- finish
- end
-
-end
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb
deleted file mode 100644
index 5e45ea77d1afde..00000000000000
--- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-class Gem::Net::HTTP::Persistent::Pool < Bundler::ConnectionPool # :nodoc:
-
- attr_reader :available # :nodoc:
- attr_reader :key # :nodoc:
-
- def initialize(options = {}, &block)
- super(**options, &block)
-
- @available = Gem::Net::HTTP::Persistent::TimedStackMulti.new(@size, &block)
- @key = "current-#{@available.object_id}"
- end
-
- def checkin net_http_args
- if net_http_args.is_a?(Hash) && net_http_args.size == 1 && net_http_args[:force]
- # Bundler::ConnectionPool 2.4+ calls `checkin(force: true)` after fork.
- # When this happens, we should remove all connections from Thread.current
- if stacks = Thread.current[@key]
- stacks.each do |http_args, connections|
- connections.each do |conn|
- @available.push conn, connection_args: http_args
- end
- connections.clear
- end
- end
- else
- stack = Thread.current[@key][net_http_args] ||= []
-
- raise Bundler::ConnectionPool::Error, 'no connections are checked out' if
- stack.empty?
-
- conn = stack.pop
-
- if stack.empty?
- @available.push conn, connection_args: net_http_args
-
- Thread.current[@key].delete(net_http_args)
- Thread.current[@key] = nil if Thread.current[@key].empty?
- end
- end
- nil
- end
-
- def checkout net_http_args
- stacks = Thread.current[@key] ||= {}
- stack = stacks[net_http_args] ||= []
-
- if stack.empty? then
- conn = @available.pop connection_args: net_http_args
- else
- conn = stack.last
- end
-
- stack.push conn
-
- conn
- end
-
- def shutdown
- Thread.current[@key] = nil
- super
- end
-end
-
-require_relative 'timed_stack_multi'
-
diff --git a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb b/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb
deleted file mode 100644
index 57dfefdaae5098..00000000000000
--- a/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-class Gem::Net::HTTP::Persistent::TimedStackMulti < Bundler::ConnectionPool::TimedStack # :nodoc:
-
- ##
- # Detects if Bundler::ConnectionPool 3.0+ is being used (needed for TimedStack subclass compatibility)
-
- CP_USES_KEYWORD_ARGS = Gem::Version.new(Bundler::ConnectionPool::VERSION) >= Gem::Version.new('3.0.0') # :nodoc:
-
- ##
- # Returns a new hash that has arrays for keys
- #
- # Using a class method to limit the bindings referenced by the hash's
- # default_proc
-
- def self.hash_of_arrays # :nodoc:
- Hash.new { |h,k| h[k] = [] }
- end
-
- def initialize(size = 0, &block)
- if CP_USES_KEYWORD_ARGS
- super(size: size, &block)
- else
- super(size, &block)
- end
-
- @enqueued = 0
- @ques = self.class.hash_of_arrays
- @lru = {}
- @key = :"connection_args-#{object_id}"
- end
-
- def empty?
- (@created - @enqueued) >= @max
- end
-
- def length
- @max - @created + @enqueued
- end
-
- private
-
- def connection_stored? options = {} # :nodoc:
- !@ques[options[:connection_args]].empty?
- end
-
- def fetch_connection options = {} # :nodoc:
- connection_args = options[:connection_args]
-
- @enqueued -= 1
- lru_update connection_args
- @ques[connection_args].pop
- end
-
- def lru_update connection_args # :nodoc:
- @lru.delete connection_args
- @lru[connection_args] = true
- end
-
- def shutdown_connections # :nodoc:
- @ques.each_key do |key|
- super connection_args: key
- end
- end
-
- def store_connection obj, options = {} # :nodoc:
- @ques[options[:connection_args]].push obj
- @enqueued += 1
- end
-
- def try_create options = {} # :nodoc:
- connection_args = options[:connection_args]
-
- if @created >= @max && @enqueued >= 1
- oldest, = @lru.first
- @lru.delete oldest
- connection = @ques[oldest].pop
- connection.close if connection.respond_to?(:close)
-
- @created -= 1
- end
-
- if @created < @max
- @created += 1
- lru_update connection_args
- return @create_block.call(connection_args)
- end
- end
-
-end
-
diff --git a/lib/bundler/vendored_persistent.rb b/lib/bundler/vendored_persistent.rb
deleted file mode 100644
index ab985c267f252c..00000000000000
--- a/lib/bundler/vendored_persistent.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Bundler
- module Persistent
- module Net
- module HTTP
- end
- end
- end
-end
-require_relative "vendor/net-http-persistent/lib/net/http/persistent"
diff --git a/pathname_builtin.rb b/pathname_builtin.rb
index 0d359b4ed1fb1f..9deb0c860ef2d6 100644
--- a/pathname_builtin.rb
+++ b/pathname_builtin.rb
@@ -1357,35 +1357,36 @@ def binwrite(...) File.binwrite(@path, ...) end
# :markup: markdown
#
# call-seq:
- # atime -> new_time
+ # atime -> time
#
- # Returns a Time object containing the access time
+ # Returns a new Time object containing the access time
# of the entry represented by `self`, as reported by the filesystem;
- # see {File System Access Time}[rdoc-ref:file/timestamps.md@Access+Time]:
+ # see {File System Access Time}[rdoc-ref:file/timestamps.md@Access+Time].
+ #
+ # For a file, the access time is established when the file is created,
+ # and may be updated with the file content is read:
#
# ```ruby
- # # Pathname for a (non-existent) directory.
- # dir_pn = Pathname('doc/foo') # => #
- # # Create directory; establishes atime for directory.
- # dir_pn.mkdir
- # dir_pn.atime # => 2026-06-17 10:10:20.801115774 -0500
- # # Pathname for a (non-existent) file in the directory.
- # file_pn = dir_pn.join('t.tmp') # => #
- # # Create file; establishes atime for file, updates atime for directory.
- # file_pn.write('foo')
- # file_pn.atime # => 2026-06-17 10:11:40.987171568 -0500
- # dir_pn.atime # => 2026-06-17 10:11:40.96617277 -0500
- # # Write file; updates atime for file,but not directory.
- # file_pn.write('bar')
- # file_pn.atime # => 2026-06-17 10:13:22.062904563 -0500
- # dir_pn.atime # => 2026-06-17 10:11:40.96617277 -0500
- # # Read file; may update atime for file, but not directory.
- # file_pn.read
- # file_pn.atime # => 2026-06-17 10:13:22.062904563 -0500
- # dir_pn.atime # => 2026-06-17 10:11:40.96617277 -0500
- # # Clean up.
- # file_pn.delete
- # dir_pn.rmdir
+ # filepath = 't.tmp'
+ # pn = Pathname(filepath)
+ # pn.exist? # => false
+ # pn.write('foo')
+ # pn.atime # => 2026-08-15 14:30:28.624455747 -0500
+ # pn.delete
+ # ```
+ #
+ # For a directory, the access time is established when the directory is created,
+ # and may be updated when its entries are read:
+ #
+ # ```ruby
+ # dirpath = 'foo'
+ # pn = Pathname(dirpath)
+ # pn.exist? # => false
+ # FileUtils.cp_r('doc', 'foo')
+ # pn.atime # => 2026-08-15 14:36:20.139756073 -0500
+ # pn.entries.take(3) # => [#, #, #]
+ # pn.atime # => 2026-08-15 14:36:32.262779081 -0500
+ # pn.rmtree # Clean up.
# ```
#
def atime() File.atime(@path) end
diff --git a/process.c b/process.c
index af6b95a1af7e37..a94b1b4fced775 100644
--- a/process.c
+++ b/process.c
@@ -591,7 +591,7 @@ static const rb_data_type_t rb_process_status_type = {
.dfree = RUBY_DEFAULT_FREE,
.dsize = NULL,
},
- .flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE,
+ .flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE | RUBY_TYPED_FROZEN_SHAREABLE,
};
static VALUE
diff --git a/ractor.c b/ractor.c
index 9a1ea4b61d631f..1d9659e4aa1c85 100644
--- a/ractor.c
+++ b/ractor.c
@@ -771,6 +771,7 @@ rb_ractor_living_threads_init(rb_ractor_t *r)
ccan_list_head_init(&r->threads.set);
r->threads.cnt = 0;
r->threads.blocking_cnt = 0;
+ r->threads.terminating = false;
}
static void
@@ -893,7 +894,7 @@ void
rb_ractor_receive_parameters(rb_execution_context_t *ec, rb_ractor_t *r, int len, VALUE *ptr)
{
for (int i=0; iport);
+ VALUE results = ractor_port_receive(ec, crr->port, Qnil);
ractor_port_close(ec, crr->port);
VALUE exc = rb_ary_pop(results);
@@ -4000,7 +4001,7 @@ rb_ractor_autoload_load(VALUE module, ID name)
rb_ractor_interrupt_exec(main_r, ractor_autoload_load_func, (void *)crr_obj, rb_interrupt_exec_flag_value_data);
// wait for require done
- VALUE results = ractor_port_receive(ec, crr->port);
+ VALUE results = ractor_port_receive(ec, crr->port, Qnil);
ractor_port_close(ec, crr->port);
VALUE exc = rb_ary_pop(results);
diff --git a/ractor.rb b/ractor.rb
index ee4a6fa73f3241..f0c9cc9aa6968b 100644
--- a/ractor.rb
+++ b/ractor.rb
@@ -265,10 +265,11 @@ def self.count
#
# call-seq:
- # Ractor.select(*ractors_or_ports) -> [ractor or port, obj]
+ # Ractor.select(*ractors_or_ports, timeout: nil) -> [ractor or port, obj] or nil
#
# Blocks the current Thread until one of the given ports has received a message. Returns an
# array of two elements where the first element is the Port and the second is the received object.
+ # With +timeout+ (in seconds) it returns +nil+ instead once the timeout passes.
# This method can also accept Ractor objects themselves, and in that case will wait until one
# has terminated and return a two-element array where the first element is the ractor and the
# second is its termination value.
@@ -307,7 +308,7 @@ def self.count
# values << val
# end
#
- def self.select(*ports)
+ def self.select(*ports, timeout: nil)
raise ArgumentError, 'specify at least one Ractor::Port or Ractor' if ports.empty?
monitors = {} # Ractor::Port => Ractor
@@ -327,7 +328,10 @@ def self.select(*ports)
end
begin
- result_port, obj = __builtin_ractor_select_internal(ports)
+ result = __builtin_ractor_select_internal(ports, timeout)
+ return nil if result.nil? # timed out
+
+ result_port, obj = result
if r = monitors[result_port]
[r, r.value]
@@ -345,11 +349,11 @@ def self.select(*ports)
#
# call-seq:
- # Ractor.receive -> obj
+ # Ractor.receive(timeout: nil) -> obj or nil
#
# Receives a message from the current ractor's default port.
- def self.receive
- Ractor.current.default_port.receive
+ def self.receive(timeout: nil)
+ Ractor.current.default_port.receive(timeout: timeout)
end
class << self
@@ -357,8 +361,8 @@ class << self
end
# same as Ractor.receive
- private def receive
- default_port.receive
+ private def receive(timeout: nil)
+ default_port.receive(timeout: timeout)
end
alias recv receive
@@ -702,7 +706,7 @@ def self.shareable_lambda self: nil
class Port
#
# call-seq:
- # port.receive -> msg
+ # port.receive(timeout: nil) -> msg or nil
#
# Receives a message from the port (which was sent there by Port#send). Only the ractor
# that created the port can receive messages this way.
@@ -743,6 +747,17 @@ class Port
# Still received only one
# Received: message2
#
+ # With +timeout+ (in seconds) the method gives up waiting and returns +nil+
+ # once it passes. A message that arrives just as the timeout expires is still
+ # returned; the timeout bounds the wait, it does not cut delivery off.
+ #
+ # port = Ractor::Port.new
+ # port.receive(timeout: 0.1) #=> nil
+ # port.receive(timeout: 0) #=> nil
+ #
+ # A +timeout+ of 0 never blocks and reads no clock: it takes a message if one
+ # is already there and returns +nil+ otherwise.
+ #
# If the port is closed and there are no more messages in the message queue,
# the method raises Ractor::ClosedError.
#
@@ -750,9 +765,9 @@ class Port
# port.close
# port.receive #=> raise Ractor::ClosedError
#
- def receive
+ def receive(timeout: nil)
__builtin_cexpr! %q{
- ractor_port_receive(ec, self)
+ ractor_port_receive(ec, self, timeout)
}
end
diff --git a/ractor_core.h b/ractor_core.h
index 1649e69fd017d1..99e3a30254a5cb 100644
--- a/ractor_core.h
+++ b/ractor_core.h
@@ -4,6 +4,7 @@
#include "vm_core.h"
#include "id_table.h"
#include "vm_debug.h"
+#include "hrtime.h"
#ifndef RACTOR_CHECK_MODE
#define RACTOR_CHECK_MODE (VM_CHECK_MODE || RUBY_DEBUG) && (SIZEOF_UINT64_T == SIZEOF_VALUE)
@@ -100,6 +101,9 @@ struct rb_ractor_struct {
struct rb_thread_sched sched;
rb_execution_context_t *running_ec;
rb_thread_t *main;
+
+ // `main` is in rb_thread_terminate_all(), waiting for the others to go
+ bool terminating;
} threads;
/* Postponed jobs targeted at this Ractor
@@ -178,6 +182,9 @@ struct ractor_waiter {
rb_thread_t *th;
struct ccan_list_node node;
rb_atomic_t event_serial;
+
+ // absolute deadline for this wait, NULL when there is no timeout
+ const rb_hrtime_t *end;
};
static inline VALUE
diff --git a/ractor_sync.c b/ractor_sync.c
index aafc7b0ccbe9a3..d8205775ba99ec 100644
--- a/ractor_sync.c
+++ b/ractor_sync.c
@@ -13,7 +13,7 @@ ractor_port_id(const struct ractor_port *rp)
static VALUE rb_cRactorPort;
-static VALUE ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp);
+static VALUE ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp, const rb_hrtime_t *end);
static VALUE ractor_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move);
static struct ractor_basket *ractor_basket_new_ref(VALUE shareable);
static void ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, struct ractor_basket *b, bool raise_on_error);
@@ -149,8 +149,10 @@ ractor_port_p(VALUE self)
return rb_typeddata_is_kind_of(self, &ractor_port_data_type);
}
+static const rb_hrtime_t *ractor_timeout_deadline(VALUE timeout, rb_hrtime_t *storage);
+
static VALUE
-ractor_port_receive(rb_execution_context_t *ec, VALUE self)
+ractor_port_receive(rb_execution_context_t *ec, VALUE self, VALUE timeout)
{
const struct ractor_port *rp = ractor_port_ptr_check(self);
@@ -158,9 +160,14 @@ ractor_port_receive(rb_execution_context_t *ec, VALUE self)
rb_raise(rb_eRactorError, "only allowed from the creator Ractor of this port");
}
- VALUE v = ractor_receive(ec, rp);
+ rb_hrtime_t deadline;
+ const rb_hrtime_t *end = ractor_timeout_deadline(timeout, &deadline);
+
+ VALUE v = ractor_receive(ec, rp, end);
RB_GC_GUARD(self);
- return v;
+
+ // no message before the timeout
+ return UNDEF_P(v) ? Qnil : v;
}
static VALUE
@@ -1294,13 +1301,22 @@ basket_type_name(enum ractor_basket_type type)
#else // win32
static void
-ractor_cond_wait(rb_ractor_t *r)
+ractor_cond_wait(rb_ractor_t *r, const rb_hrtime_t *end)
{
#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 (end) {
+ rb_hrtime_t now = rb_hrtime_now();
+ rb_hrtime_t rel = *end > now ? *end - now : 0;
+ // the condvar takes msec: never round a live timeout down to 0
+ unsigned long msec = (unsigned long)(rel / RB_HRTIME_PER_MSEC);
+ rb_native_cond_timedwait(&r->sync.wakeup_cond, &r->sync.lock, msec > 0 ? msec : 1);
+ }
+ else {
+ rb_native_cond_wait(&r->sync.wakeup_cond, &r->sync.lock);
+ }
#if RACTOR_CHECK_MODE > 0
r->sync.locked_by = locked_by;
@@ -1316,7 +1332,7 @@ ractor_wait_no_gvl(void *ptr)
RACTOR_LOCK_SELF(cr);
{
if (waiter->wakeup_status == wakeup_none) {
- ractor_cond_wait(cr);
+ ractor_cond_wait(cr, waiter->end);
}
}
RACTOR_UNLOCK_SELF(cr);
@@ -1406,14 +1422,16 @@ ubf_ractor_wait(void *ptr)
rb_native_mutex_lock(&th->interrupt_lock);
}
+// Waits for an event on cr. `end` is an absolute deadline, NULL to wait forever.
static enum ractor_wakeup_status
-ractor_wait(rb_execution_context_t *ec, rb_ractor_t *cr)
+ractor_wait(rb_execution_context_t *ec, rb_ractor_t *cr, const rb_hrtime_t *end)
{
rb_thread_t *th = rb_ec_thread_ptr(ec);
struct ractor_waiter waiter = {
.wakeup_status = wakeup_none,
.th = th,
+ .end = end,
};
RUBY_DEBUG_LOG("wait%s", "");
@@ -1481,19 +1499,30 @@ ractor_check_received(rb_ractor_t *cr, struct ractor_queue *messages)
return received;
}
-static void
-ractor_wait_receive(rb_execution_context_t *ec, rb_ractor_t *cr)
+// Returns false if the deadline `end` passed with nothing to deliver. Incoming
+// messages are delivered even then, so the caller retries its queue once more.
+static bool
+ractor_wait_receive(rb_execution_context_t *ec, rb_ractor_t *cr, const rb_hrtime_t *end)
{
struct ractor_queue messages;
bool deliverred = false;
+ bool timedout = false;
RACTOR_LOCK_SELF(cr);
{
if (ractor_check_received(cr, &messages)) {
deliverred = true;
}
+ else if (!end) {
+ ractor_wait(ec, cr, NULL); // no timeout: wait until a message arrives
+ }
+ else if (*end == 0) {
+ timedout = true; // `timeout: 0`: over without reading any clock
+ }
else {
- ractor_wait(ec, cr);
+ // only a wakeup nobody claimed can be the deadline, so only then look at
+ // the clock: a send or an interrupt says what woke this thread by itself
+ timedout = ractor_wait(ec, cr, end) == wakeup_none && rb_hrtime_now() >= *end;
}
}
RACTOR_UNLOCK_SELF(cr);
@@ -1506,6 +1535,8 @@ ractor_wait_receive(rb_execution_context_t *ec, rb_ractor_t *cr)
ractor_queue_enq(cr, ractor_get_queue(cr, b->port_id, false), b);
}
}
+
+ return !timedout;
}
static VALUE
@@ -1533,8 +1564,12 @@ ractor_try_receive(rb_execution_context_t *ec, rb_ractor_t *cr, const struct rac
}
}
+// Returns Qundef if the deadline passed first. It bounds how long this blocks; it
+// does not cut delivery off. A message that lands while the timeout is being
+// reported is still returned, as Thread::Queue#pop(timeout:) does. Either way
+// nothing is lost: a basket only leaves the queue when it is returned.
static VALUE
-ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp)
+ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp, const rb_hrtime_t *end)
{
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
VM_ASSERT(cr == rp->r);
@@ -1547,10 +1582,31 @@ ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp)
if (v != Qundef) {
return v;
}
- else {
- ractor_wait_receive(ec, cr);
+ else if (!ractor_wait_receive(ec, cr, end)) {
+ return Qundef;
+ }
+ }
+}
+
+// A timeout argument becomes an absolute deadline, or 0 for `timeout: 0`, which
+// every wait reads as "do not wait". Returns NULL when there is no timeout.
+static const rb_hrtime_t *
+ractor_timeout_deadline(VALUE timeout, rb_hrtime_t *storage)
+{
+ if (NIL_P(timeout)) return NULL;
+
+ if (!(FIXNUM_P(timeout) && FIX2LONG(timeout) == 0)) {
+ struct timeval tv = rb_time_interval(timeout); // raises on a negative timeout
+ rb_hrtime_t rel = rb_timeval2hrtime(&tv);
+
+ if (rel > 0) {
+ *storage = rb_hrtime_add(rb_hrtime_now(), rel);
+ return storage;
}
}
+
+ *storage = 0;
+ return storage;
}
// Ractor#send
@@ -1816,7 +1872,7 @@ ractor_selector_wait_i(st_data_t key, st_data_t val, st_data_t data)
}
static VALUE
-ractor_selector__wait(rb_execution_context_t *ec, VALUE selector)
+ractor_selector__wait(rb_execution_context_t *ec, VALUE selector, const rb_hrtime_t *end)
{
rb_ractor_t *cr = rb_ec_ractor_ptr(ec);
struct ractor_selector *s = RACTOR_SELECTOR_PTR(selector);
@@ -1833,8 +1889,9 @@ ractor_selector__wait(rb_execution_context_t *ec, VALUE selector)
if (data.found) {
return rb_ary_new_from_args(2, data.rpv, data.v);
}
-
- ractor_wait_receive(ec, cr);
+ else if (!ractor_wait_receive(ec, cr, end)) {
+ return Qnil;
+ }
}
}
@@ -1847,7 +1904,7 @@ ractor_selector__wait(rb_execution_context_t *ec, VALUE selector)
static VALUE
ractor_selector_wait(VALUE selector)
{
- return ractor_selector__wait(GET_EC(), selector);
+ return ractor_selector__wait(GET_EC(), selector, NULL);
}
static VALUE
@@ -1863,10 +1920,13 @@ ractor_selector_new(int argc, VALUE *ractors, VALUE klass)
}
static VALUE
-ractor_select_internal(rb_execution_context_t *ec, VALUE self, VALUE ports)
+ractor_select_internal(rb_execution_context_t *ec, VALUE self, VALUE ports, VALUE timeout)
{
+ rb_hrtime_t deadline;
+ const rb_hrtime_t *end = ractor_timeout_deadline(timeout, &deadline);
+
VALUE selector = ractor_selector_new(RARRAY_LENINT(ports), (VALUE *)RARRAY_CONST_PTR(ports), rb_cRactorSelector);
- VALUE result = ractor_selector__wait(ec, selector);
+ VALUE result = ractor_selector__wait(ec, selector, end);
RB_GC_GUARD(selector);
RB_GC_GUARD(ports);
diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb
index edf426328a4d9e..a3390ac5276fc0 100644
--- a/spec/bundler/bundler/fetcher/downloader_spec.rb
+++ b/spec/bundler/bundler/fetcher/downloader_spec.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
+require "rubygems/remote_fetcher"
+
RSpec.describe Bundler::Fetcher::Downloader do
- let(:connection) { double(:connection) }
+ let(:connections) { double(:connections) }
let(:redirect_limit) { 5 }
let(:uri) { Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint") }
let(:options) { double(:options) }
- subject { described_class.new(connection, redirect_limit) }
+ subject { described_class.new(connections, redirect_limit) }
describe "fetch" do
let(:counter) { 0 }
@@ -168,12 +170,10 @@
end
describe "request" do
- let(:net_http_get) { double(:net_http_get) }
- let(:response) { double(:response) }
+ let(:response) { double(:response) }
before do
- allow(Gem::Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get)
- allow(connection).to receive(:request).with(uri, net_http_get).and_return(response)
+ allow(connections).to receive(:request).with(uri, options).and_return(response)
end
it "should log the HTTP GET request to debug" do
@@ -181,55 +181,63 @@
subject.request(uri, options)
end
- context "when there is a user provided in the request" do
- context "and there is also a password provided" do
- context "that contains cgi escaped characters" do
- let(:uri) { Gem::URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") }
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
- it "should request basic authentication with the username and password, and log the HTTP GET request to debug, without the password" do
- expect(net_http_get).to receive(:basic_auth).with("username", "password$")
- expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
- subject.request(uri, options)
- end
- end
+ it "should log the HTTP GET request to debug, without the password" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
- context "that is all unescaped characters" do
- let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
- it "should request basic authentication with the username and proper cgi compliant password, and log the HTTP GET request to debug, without the password" do
- expect(net_http_get).to receive(:basic_auth).with("username", "password")
- expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
- subject.request(uri, options)
- end
- end
+ context "when the request response causes a OpenSSL::SSL::SSLError" do
+ before { allow(connections).to receive(:request).with(uri, options) { raise OpenSSL::SSL::SSLError.new } }
+
+ it "should raise a Bundler::Fetcher::CertificateFailureError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
+ %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint})
+ end
+ end
+
+ context "when the request response causes a Gem::RemoteFetcher::FetchError" do
+ let(:message) { "error about network" }
+ let(:error) { Gem::RemoteFetcher::FetchError.new(message, uri) }
+
+ before do
+ allow(connections).to receive(:request).with(uri, options) { raise error }
+ end
+
+ it "should raise a Bundler::HTTPError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError,
+ %r{Network error while fetching http://www\.uri-to-fetch\.com/api/v2/endpoint \(error about network})
end
- context "and it's used as the authentication token" do
- let(:uri) { Gem::URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") }
+ context "when the error is about a failed certificate verification" do
+ let(:message) { "SSL_connect returned=1 errno=0 peeraddr=127.0.0.1:443 state=error: certificate verify failed" }
- it "should request basic authentication with just the user, and log the HTTP GET request to debug, without the token" do
- expect(net_http_get).to receive(:basic_auth).with("username", nil)
- expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
- subject.request(uri, options)
+ it "should raise a Bundler::Fetcher::CertificateFailureError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
+ %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint})
end
end
- context "and it's used as the authentication token, and contains cgi escaped characters" do
- let(:uri) { Gem::URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") }
+ context "when the error is about the host being down" do
+ let(:message) { "Host is down" }
- it "should request basic authentication with the proper cgi compliant password user, and log the HTTP GET request to debug, without the token" do
- expect(net_http_get).to receive(:basic_auth).with("username$", nil)
- expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
- subject.request(uri, options)
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
end
end
- end
- context "when the request response causes a OpenSSL::SSL::SSLError" do
- before { allow(connection).to receive(:request).with(uri, net_http_get) { raise OpenSSL::SSL::SSLError.new } }
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
- it "should raise a LoadError about openssl" do
- expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
- %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint})
+ it "should raise a Bundler::HTTPError that doesn't contain the password" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError) do |error|
+ expect(error.message).not_to include("password")
+ end
+ end
end
end
@@ -238,7 +246,7 @@
let(:error) { error_class.new(message) }
before do
- allow(connection).to receive(:request).with(uri, net_http_get) { raise error }
+ allow(connections).to receive(:request).with(uri, options) { raise error }
end
context "that it's retryable" do
@@ -257,9 +265,6 @@
context "when there are credentials provided in the request" do
let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
- before do
- allow(net_http_get).to receive(:basic_auth).with("username", "password")
- end
it "should raise a Bundler::HTTPError that doesn't contain the password" do
expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError,
@@ -268,19 +273,8 @@
end
end
- context "when error is about the host being down" do
- let(:error_class) { Gem::Net::HTTP::Persistent::Error }
- let(:message) { "host down: http://www.uri-to-fetch.com" }
-
- it "should raise a Bundler::Fetcher::NetworkDownError" do
- expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
- /Could not reach host www.uri-to-fetch.com/)
- end
- end
-
context "when error is about connection refused" do
- let(:error_class) { Gem::Net::HTTP::Persistent::Error }
- let(:message) { "connection refused down: http://www.uri-to-fetch.com" }
+ let(:error_class) { Errno::ECONNREFUSED }
it "should raise a Bundler::Fetcher::NetworkDownError" do
expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
diff --git a/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
index a06cf0396c66cb..2619491972ee28 100644
--- a/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
+++ b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
@@ -3,7 +3,6 @@
require "rubygems/remote_fetcher"
require "bundler/fetcher/gem_remote_fetcher"
require_relative "../../support/artifice/helpers/artifice"
-require "bundler/vendored_persistent.rb"
RSpec.describe Bundler::Fetcher::GemRemoteFetcher do
describe "#initialize" do
diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb
index 6891a7991de636..87a49ea6fc7865 100644
--- a/spec/bundler/bundler/fetcher_spec.rb
+++ b/spec/bundler/bundler/fetcher_spec.rb
@@ -78,18 +78,22 @@
end
end
it "consider no_proxy" do
- with_env_vars("HTTP_PROXY" => "http://proxy-example4.com", "NO_PROXY" => ".example.com,.example.net") do
- expect(
- fetcher.send(:connection).no_proxy
- ).to eq([".example.com", ".example.net"])
+ with_env_vars("HTTP_PROXY" => "http://proxy-example4.com", "NO_PROXY" => "example.com,.example.net") do
+ expect(fetcher.http_proxy).to be_nil
end
end
end
+ def configured_connection
+ http = Gem::Net::HTTP.new(uri.host, uri.port)
+ fetcher.send(:connection).send(:configure_ssl, http)
+ http
+ end
+
context "when no ssl configuration is set" do
it "no cert" do
- expect(fetcher.send(:connection).cert).to be_nil
- expect(fetcher.send(:connection).key).to be_nil
+ expect(configured_connection.cert).to be_nil
+ expect(configured_connection.key).to be_nil
end
end
@@ -106,8 +110,9 @@
FileUtils.rm File.join(Spec::Path.tmpdir, "cert")
end
it "use bundler configuration" do
- expect(fetcher.send(:connection).cert).to eq("cert")
- expect(fetcher.send(:connection).key).to eq("key")
+ connection = configured_connection
+ expect(connection.cert).to eq("cert")
+ expect(connection.key).to eq("key")
end
end
@@ -126,8 +131,9 @@
expect(OpenSSL::X509::Store).to receive(:new).and_return(store)
end
it "use gem configuration" do
- expect(fetcher.send(:connection).cert).to eq("cert")
- expect(fetcher.send(:connection).key).to eq("key")
+ connection = configured_connection
+ expect(connection.cert).to eq("cert")
+ expect(connection.key).to eq("key")
end
end
end
diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb
index bdfbbe6db34d2f..20d39e885be3d8 100644
--- a/spec/bundler/bundler/installer/parallel_installer_spec.rb
+++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb
@@ -8,11 +8,6 @@
RSpec.describe Bundler::ParallelInstaller do
describe "priority queue" do
before do
- # Anchor the vendored Persistent classes on the real Gem::Net::HTTP
- # before Artifice replaces it, see Artifice.activate_with. Requiring
- # support/artifice/compact_index already activates Artifice, so this
- # must come first.
- require "bundler/vendored_persistent"
require "support/artifice/compact_index"
Artifice.activate_with(CompactIndexAPI)
@@ -103,11 +98,6 @@
skip "This example does not work under a parent make jobserver"
end
- # Anchor the vendored Persistent classes on the real Gem::Net::HTTP
- # before Artifice replaces it, see Artifice.activate_with. Requiring
- # support/artifice/compact_index already activates Artifice, so this
- # must come first.
- require "bundler/vendored_persistent"
require "support/artifice/compact_index"
Artifice.activate_with(CompactIndexAPI)
@@ -142,9 +132,12 @@
Bundler.ui = Bundler::UI::Silent.new
end
+ # The `before` hook can `skip` before it saves anything, so only restore
+ # what was actually captured. Otherwise every skipped example clobbers the
+ # globals with nil.
after do
- Bundler.ui = @old_ui
- Gem::Request::ConnectionPools.client = @previous_client
+ Bundler.ui = @old_ui if @old_ui
+ Gem::Request::ConnectionPools.client = @previous_client if @previous_client
Artifice.deactivate
end
diff --git a/spec/bundler/commands/ssl_spec.rb b/spec/bundler/commands/ssl_spec.rb
index 4220731b697073..54d11c44ceecee 100644
--- a/spec/bundler/commands/ssl_spec.rb
+++ b/spec/bundler/commands/ssl_spec.rb
@@ -4,7 +4,6 @@
require "bundler/cli/doctor"
require "bundler/cli/doctor/ssl"
require_relative "../support/artifice/helpers/artifice"
-require "bundler/vendored_persistent.rb"
RSpec.describe "bundle doctor ssl" do
before(:each) do
diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb
index 41d18280f24cfb..2de60ecee9f961 100644
--- a/spec/bundler/install/gems/compact_index_spec.rb
+++ b/spec/bundler/install/gems/compact_index_spec.rb
@@ -219,30 +219,6 @@
expect(the_bundle).to include_gems "myrack 1.0.0"
end
- it "handles host redirects without Gem::Net::HTTP::Persistent" do
- gemfile <<-G
- source "#{source_uri}"
- gem "myrack"
- G
-
- FileUtils.mkdir_p lib_path
- File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h|
- h.write <<-H
- module Kernel
- alias require_without_disabled_net_http require
- def require(*args)
- raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty?
- require_without_disabled_net_http(*args)
- end
- end
- H
- end
-
- bundle :install, artifice: "compact_index_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")]
- expect(out).to_not match(/Too many redirects/)
- expect(the_bundle).to include_gems "myrack 1.0.0"
- end
-
it "times out when Bundler::Fetcher redirects too much" do
gemfile <<-G
source "#{source_uri}"
diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb
index 9f28bfcebe79b3..e9a2cdf42c1586 100644
--- a/spec/bundler/install/gems/dependency_api_spec.rb
+++ b/spec/bundler/install/gems/dependency_api_spec.rb
@@ -195,30 +195,6 @@
expect(the_bundle).to include_gems "myrack 1.0.0"
end
- it "handles host redirects without Gem::Net::HTTP::Persistent" do
- gemfile <<-G
- source "#{source_uri}"
- gem "myrack"
- G
-
- FileUtils.mkdir_p lib_path
- File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h|
- h.write <<-H
- module Kernel
- alias require_without_disabled_net_http require
- def require(*args)
- raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty?
- require_without_disabled_net_http(*args)
- end
- end
- H
- end
-
- bundle :install, artifice: "endpoint_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")]
- expect(out).to_not match(/Too many redirects/)
- expect(the_bundle).to include_gems "myrack 1.0.0"
- end
-
it "timeouts when Bundler::Fetcher redirects too much" do
gemfile <<-G
source "#{source_uri}"
diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb
index 23501bdf8d5d60..61bac6231be1e7 100644
--- a/spec/bundler/support/artifice/helpers/artifice.rb
+++ b/spec/bundler/support/artifice/helpers/artifice.rb
@@ -9,14 +9,6 @@ module Artifice
# Rack endpoint.
#
# @param [#call] endpoint A valid Rack endpoint
- # In-process users that also deactivate must load bundler/vendored_persistent
- # before activating. If it gets lazily required while Artifice is active, the
- # vendored Persistent classes are defined under the Artifice replacement of
- # Gem::Net::HTTP instead of the real one, and after deactivation
- # Gem::Net::HTTP::Persistent becomes unresolvable, blowing up the
- # connection_pool fork hook on any later Process.fork. Spawned bundler
- # processes are unaffected: they never deactivate, and requiring it here
- # would double-load bundler files in them through mismatched load paths.
def self.activate_with(endpoint)
require_relative "rack_request"
diff --git a/test/coverage/test_coverage.rb b/test/coverage/test_coverage.rb
index 4a3b9690c09411..73e3c0553b8c40 100644
--- a/test/coverage/test_coverage.rb
+++ b/test/coverage/test_coverage.rb
@@ -256,7 +256,7 @@ def test_eval_coverage
end;
end
- def test_branch_coverage_for_eval_repeated
+def test_branch_coverage_for_eval_repeated
assert_in_out_err(["-W0", *ARGV], <<-"end;", ["2", "2", "[[0, 1], [0, 2]]"], [])
Coverage.start(eval: true, branches: true)
@@ -284,6 +284,82 @@ def foo(x)
end;
end
+ def test_peek_result_branches_after_eval_adds_branches
+ assert_in_out_err(ARGV, <<-"end;", ["1", "1", "2", "3", "1", "false"], [])
+ Coverage.start(eval: true, branches: true)
+
+ sum = ->(r) { r.sum {|_, targets| targets.sum {|_, count| count } } }
+
+ eval(<<-RUBY, TOPLEVEL_BINDING, "test.rb", 1)
+ def foo(x)
+ x ? 1 : 2
+ end
+ RUBY
+
+ foo(true)
+ r1 = Coverage.peek_result["test.rb"][:branches]
+ p r1.size
+ p sum[r1]
+
+ # A later eval with the same path adds new branches to the same file,
+ # and the cached result template must be refreshed accordingly
+ eval(<<-RUBY, TOPLEVEL_BINDING, "test.rb", 10)
+ def bar(x)
+ x ? 1 : 2
+ end
+ RUBY
+
+ foo(true)
+ bar(false)
+ r2 = Coverage.peek_result["test.rb"][:branches]
+ p r2.size
+ p sum[r2]
+ # the earlier snapshot must not be affected
+ p sum[r1]
+ p r1.equal?(r2)
+ end;
+ end
+
+ def test_peek_result_methods_after_eval_adds_methods
+ assert_in_out_err(["-W0", *ARGV], <<-"end;", ["1", "2", "1", "3", "1", "false"], [])
+ Coverage.start(eval: true, methods: true)
+
+ eval(<<-RUBY, TOPLEVEL_BINDING, "test.rb", 1)
+ class Foo
+ def foo; end
+ end
+ RUBY
+
+ Foo.new.foo
+ r1 = Coverage.peek_result["test.rb"][:methods]
+ p r1.size
+
+ # A later eval with the same path adds new methods to the same file,
+ # and redefining a method at the same location shares the key
+ eval(<<-RUBY, TOPLEVEL_BINDING, "test.rb", 10)
+ class Foo
+ def bar; end
+ end
+ RUBY
+ eval(<<-RUBY, TOPLEVEL_BINDING, "test.rb", 1)
+ class Foo
+ def foo; end
+ end
+ RUBY
+
+ Foo.new.foo
+ Foo.new.foo
+ Foo.new.bar
+ r2 = Coverage.peek_result["test.rb"][:methods]
+ p r2.size
+ # the earlier snapshot must not be affected
+ p r1.size
+ p r2[[Foo, :foo, 2, 8, 2, 20]]
+ p r2[[Foo, :bar, 11, 8, 11, 20]]
+ p r1.equal?(r2)
+ end;
+ end
+
def test_eval_negative_lineno
assert_in_out_err(ARGV, <<-"end;", ["[1, 1, 1]"], [])
Coverage.start(eval: true, lines: true)
diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb
index 84202c57ccf7f7..c28d1f496564cd 100644
--- a/test/ruby/test_gc.rb
+++ b/test/ruby/test_gc.rb
@@ -302,6 +302,13 @@ def test_stat_heap_constraints
assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots]
assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects]
assert_equal stat[:total_freed_objects], stat_heap_sum[:total_freed_objects]
+ rescue Test::Unit::AssertionFailedError
+ # GC.stat and GC.stat_heap are separate reads of the same counters, so
+ # anything allocated between them makes the two disagree. An accounting
+ # bug disagrees on the retry too.
+ raise if @retried
+ @retried = true
+ retry
end
def test_page_pool_stat_consistency
diff --git a/test/ruby/test_process.rb b/test/ruby/test_process.rb
index 93b3cc7da7e8f2..c87fd8be327f92 100644
--- a/test/ruby/test_process.rb
+++ b/test/ruby/test_process.rb
@@ -1513,6 +1513,14 @@ def test_status
end
end
+ def test_status_frozen_shareable
+ with_tmpchdir do
+ s = run_in_child("exit 1")
+ assert_predicate(s, :frozen?)
+ assert(Ractor.shareable?(s), "a frozen Process::Status should be shareable")
+ end
+ end
+
def test_status_kill
return unless Process.respond_to?(:kill)
return unless Signal.list.include?("KILL")
diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb
index 1b903a72ca20fb..0901757201740d 100644
--- a/test/ruby/test_ractor.rb
+++ b/test/ruby/test_ractor.rb
@@ -166,6 +166,29 @@ def test_default_thread_group
refute_equal main_ractor_id, ractor_id
end;
end
+ def test_ractor_with_live_threads_terminates_without_waiting
+ assert_separately([], __FILE__, __LINE__, <<-'RUBY')
+ Warning[:experimental] = false
+ # A Ractor that ends while a thread of its own is still running used to sit out
+ # the one second poll in rb_thread_terminate_all(), once per Ractor. Measure
+ # against the same Ractors without a live thread, so that a busy machine, which
+ # makes both of them slow, does not decide this.
+ n = 5
+ elapsed = ->(&blk) {
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ n.times { blk.call }
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
+ }
+
+ base = elapsed.call { assert_equal :done, Ractor.new { :done }.value }
+ live = elapsed.call { assert_equal :done, Ractor.new { Thread.new { sleep 10 }; :done }.value }
+
+ # the bug costs a second per Ractor, so #{n} seconds here
+ assert_operator live, :<, base + 2.0,
+ "#{n} Ractors with a live thread took #{live}s, without one #{base}s"
+ RUBY
+ end
+
def test_class_instance_variables
assert_ractor(<<~'RUBY')
@@ -556,6 +579,66 @@ def test_port_new_under_gc_stress
assert_equal 4, ports.size
RUBY
end
+ def test_port_receive_timeout
+ assert_separately([], __FILE__, __LINE__, <<-'RUBY')
+ Warning[:experimental] = false
+ port = Ractor::Port.new
+
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ assert_nil port.receive(timeout: 0.1)
+ assert_operator Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0, :>=, 0.1
+
+ # a message that is already there wins over the timeout
+ port << :a
+ assert_equal :a, port.receive(timeout: 10)
+
+ # timeout: 0 polls
+ assert_nil port.receive(timeout: 0)
+ port << :b
+ assert_equal :b, port.receive(timeout: 0)
+ RUBY
+ end
+
+ def test_receive_timeout_on_mn_thread
+ assert_separately([], __FILE__, __LINE__, <<-'RUBY')
+ Warning[:experimental] = false
+ # a Ractor's thread is an M:N thread: the timeout must not need a native thread
+ r = Ractor.new do
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ [Ractor.receive(timeout: 0.1), Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0]
+ end
+ v, elapsed = r.value
+ assert_nil v
+ assert_operator elapsed, :>=, 0.1
+ RUBY
+ end
+
+ def test_select_timeout
+ assert_separately([], __FILE__, __LINE__, <<-'RUBY')
+ Warning[:experimental] = false
+ p1, p2 = Ractor::Port.new, Ractor::Port.new
+ assert_nil Ractor.select(p1, p2, timeout: 0.1)
+
+ p2 << :b
+ assert_equal [p2, :b], Ractor.select(p1, p2, timeout: 10)
+ RUBY
+ end
+
+ def test_receive_timeout_racing_with_send
+ assert_separately([], __FILE__, __LINE__, <<-'RUBY')
+ Warning[:experimental] = false
+ # the timeout and a send aim at the same instant: both wake the waiter
+ results = []
+ 300.times do
+ port = Ractor::Port.new
+ th = Thread.new(port) {|p| sleep 0.001; p << :msg }
+ results << port.receive(timeout: 0.001)
+ th.join
+ end
+ assert_empty results.uniq - [:msg, nil]
+ RUBY
+ end
+
# Moving a Hash that has Hash keys must not lose entries (regression guard for inserting a
# key before its contents are filled in, which corrupts its hash value).
diff --git a/test/ruby/test_super.rb b/test/ruby/test_super.rb
index 39594d74be38af..4a9ef1a2ea3418 100644
--- a/test/ruby/test_super.rb
+++ b/test/ruby/test_super.rb
@@ -667,6 +667,38 @@ def test
assert_equal(:test, c.new.test)
end
+ def test_super_with_prepended_module_after_include_method_caching
+ m = Module.new do
+ def test
+ :m
+ end
+ end
+
+ c = Class.new { include m }
+
+ m.prepend(Module.new do
+ def test
+ super
+ end
+ end)
+
+ # prime the super call-site cache through the prepended module
+ assert_equal(:m, c.new.test)
+
+ m.class_eval do
+ begin
+ verbose_bak, $VERBOSE = $VERBOSE, nil
+ def test
+ :redefined
+ end
+ ensure
+ $VERBOSE = verbose_bak
+ end
+ end
+
+ assert_equal(:redefined, c.new.test)
+ end
+
class TestFor_super_with_modified_rest_parameter_base
def foo *args
args
diff --git a/thread.c b/thread.c
index 33d45a8c343c67..78c91de8e7df90 100644
--- a/thread.c
+++ b/thread.c
@@ -482,6 +482,10 @@ rb_thread_terminate_all(rb_thread_t *th)
/* unlock all locking mutexes */
rb_threadptr_unlock_all_locking_mutexes(th);
+ // tells the last sub-thread to wake this one out of the sleep below. Nothing
+ // clears it: no thread of this Ractor can run again once this returns.
+ cr->threads.terminating = true;
+
EC_PUSH_TAG(ec);
if (EC_EXEC_TAG() == TAG_NONE) {
retry:
@@ -810,7 +814,7 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start)
(void *)th, th->locking_mutex);
}
- if (ractor_main_th->status == THREAD_KILLED &&
+ if (th->ractor->threads.terminating &&
th->ractor->threads.cnt <= 2 /* main thread and this thread */) {
/* I'm last thread. wake up main thread from rb_thread_terminate_all */
rb_threadptr_interrupt(ractor_main_th);
@@ -6274,27 +6278,38 @@ struct method_coverage_arg {
void *data;
};
-static void
-method_coverage_call(const rb_method_entry_t *me, VALUE count,
- struct method_coverage_arg *arg)
+/* Fills *out for the method entry `me_v` and returns true, or returns false
+ * if the method entry is not a subject of method coverage (aliases,
+ * complemented entries, and methods without a source location). */
+bool
+rb_coverage_method_data_of(VALUE me_v, VALUE count, struct rb_coverage_method_data *out)
{
+ const rb_method_entry_t *me = (const rb_method_entry_t *)me_v;
VALUE location[5];
const rb_method_entry_t *resolved_me = rb_resolve_me_location(me, location);
if (me != resolved_me || RB_TYPE_P(me->owner, T_ICLASS) ||
- FIX2LONG(location[1]) <= 0) return;
-
- struct rb_coverage_method_data method = {
- .owner = me->owner,
- .method_id = ID2SYM(me->def->original_id),
- .path = location[0],
- .first_lineno = location[1],
- .first_column = location[2],
- .last_lineno = location[3],
- .last_column = location[4],
- .count = count,
- };
- arg->callback(&method, arg->data);
+ FIX2LONG(location[1]) <= 0) return false;
+
+ out->owner = me->owner;
+ out->method_id = ID2SYM(me->def->original_id);
+ out->path = location[0];
+ out->first_lineno = location[1];
+ out->first_column = location[2];
+ out->last_lineno = location[3];
+ out->last_column = location[4];
+ out->count = count;
+ return true;
+}
+
+static void
+method_coverage_call(const rb_method_entry_t *me, VALUE count,
+ struct method_coverage_arg *arg)
+{
+ struct rb_coverage_method_data method;
+ if (rb_coverage_method_data_of((VALUE)me, count, &method)) {
+ arg->callback(&method, arg->data);
+ }
}
static int
diff --git a/thread_pthread.c b/thread_pthread.c
index 2500e8998b8d48..4c5fdf74c9c8aa 100644
--- a/thread_pthread.c
+++ b/thread_pthread.c
@@ -334,6 +334,8 @@ static void ractor_sched_enq(rb_vm_t *vm, rb_ractor_t *r);
static void timer_thread_wakeup(void);
static void timer_thread_wakeup_locked(rb_vm_t *vm);
static void timer_thread_wakeup_force(void);
+static bool ractor_sched_timeout_arm(rb_thread_t *th, const rb_hrtime_t *rel);
+static bool ractor_sched_timeout_disarm(rb_thread_t *th);
static void thread_sched_switch(rb_thread_t *cth, rb_thread_t *next_th);
static void ractor_sched_cancel_enq(rb_vm_t *vm, struct rb_thread_sched *sched);
#if USE_MN_THREADS
@@ -863,9 +865,9 @@ thread_sched_to_ready(struct rb_thread_sched *sched, rb_thread_t *th)
thread_sched_unlock(sched, th);
}
-// wait until sched->running is `th`.
+// wait until sched->running is `th`. `end` is an absolute deadline for a dedicated
static void
-thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, bool can_direct_transfer)
+thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, bool can_direct_transfer, const rb_hrtime_t *end)
{
RUBY_DEBUG_LOG("th:%u", rb_th_serial(th));
@@ -922,10 +924,32 @@ thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, b
thread_sched_set_unlocked(sched, th);
{
RUBY_DEBUG_LOG("nt:%d cond:%p", th->nt->serial, &th->nt->cond.readyq);
- rb_native_cond_wait(&th->nt->cond.readyq, &sched->lock_);
+ rb_nativethread_cond_t *cond = &th->nt->cond.readyq;
+
+ if (end) {
+ rb_hrtime_t abs = *end;
+
+ if (!condattr_monotonic) {
+ // the condvar counts in another clock: restate it there
+ rb_hrtime_t now = rb_hrtime_now();
+ abs = native_cond_timeout(cond, *end > now ? *end - now : 0);
+ }
+ native_cond_timedwait(cond, &sched->lock_, &abs);
+ }
+ else {
+ rb_native_cond_wait(cond, &sched->lock_);
+ }
}
thread_sched_set_locked(sched, th);
+ if (end && rb_hrtime_now() >= *end &&
+ sched->running != th && !th->sched.node.is_ready) {
+ // the deadline passed and nobody woke this thread: get back in
+ // line for the running turn, then wait for it without a deadline
+ thread_sched_to_ready_common(sched, th, false, false);
+ end = NULL;
+ }
+
if (sched->runnable_hot_th != NULL && sched->runnable_hot_th_waiting) {
VM_ASSERT(sched->runnable_hot_th != th);
// Give the hot thread a chance to preempt, if it's actively spinning.
@@ -1012,7 +1036,7 @@ thread_sched_to_running_common(struct rb_thread_sched *sched, rb_thread_t *th)
}
// TODO: check SNT number
- thread_sched_wait_running_turn(sched, th, false);
+ thread_sched_wait_running_turn(sched, th, false, NULL);
}
// waiting -> ready -> running
@@ -1220,7 +1244,7 @@ thread_sched_to_waiting_until_wakeup(struct rb_thread_sched *sched, rb_thread_t
bool can_direct_transfer = !th_has_dedicated_nt(th);
// NOTE: th->status is set before and after this sleep outside of this function in `sleep_forever`
thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
- thread_sched_wait_running_turn(sched, th, can_direct_transfer);
+ thread_sched_wait_running_turn(sched, th, can_direct_transfer, NULL);
}
}
thread_sched_unlock(sched, th);
@@ -1242,7 +1266,7 @@ thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th)
thread_sched_wakeup_next_thread(sched, th, !th_has_dedicated_nt(th));
bool can_direct_transfer = !th_has_dedicated_nt(th);
thread_sched_to_ready_common(sched, th, false, can_direct_transfer);
- thread_sched_wait_running_turn(sched, th, can_direct_transfer);
+ thread_sched_wait_running_turn(sched, th, can_direct_transfer, NULL);
th->status = THREAD_RUNNABLE;
}
else {
@@ -1531,15 +1555,49 @@ rb_ractor_sched_wait(rb_execution_context_t *ec, rb_ractor_t *cr, rb_unblock_fun
thread_sched_lock(sched, th);
rb_ractor_unlock_self(cr);
{
- // setup sleep
- bool can_direct_transfer = !th_has_dedicated_nt(th);
- RB_VM_SAVE_MACHINE_CONTEXT(th);
- th->status = THREAD_STOPPED_FOREVER;
- RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
- thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
- // sleep
- thread_sched_wait_running_turn(sched, th, can_direct_transfer);
- th->status = THREAD_RUNNABLE;
+ // A dedicated native thread takes the deadline on the very condvar a wakeup
+ // signals. An M:N thread has no condvar of its own, so its deadline goes to
+ // the timer thread, which then wakes it the way rb_ractor_sched_wakeup() does.
+ bool dedicated = th_has_dedicated_nt(th);
+ const rb_hrtime_t *end_p = NULL;
+ bool armed = false, expired = false;
+
+ if (waiter->end) {
+ if (dedicated) {
+ end_p = waiter->end;
+ }
+ else {
+ // the timer wheel takes a relative timeout
+ rb_hrtime_t now = rb_hrtime_now();
+ rb_hrtime_t rel = *waiter->end > now ? *waiter->end - now : 0;
+
+ armed = ractor_sched_timeout_arm(th, &rel);
+ expired = !armed;
+ }
+ }
+
+ if (expired) {
+ RUBY_DEBUG_LOG("expired before sleep%s", "");
+ }
+ else if (armed && th->sched.waiting_reason.flags == thread_sched_waiting_none) {
+ // the timer thread already took this thread out of the wheel; bump the
+ // serial so that it does not try to wake a thread that never slept
+ th->sched.event_serial++;
+ }
+ else {
+ // setup sleep
+ bool can_direct_transfer = !dedicated;
+ RB_VM_SAVE_MACHINE_CONTEXT(th);
+ th->status = THREAD_STOPPED_FOREVER;
+ RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th);
+ thread_sched_wakeup_next_thread(sched, th, can_direct_transfer);
+ // sleep
+ thread_sched_wait_running_turn(sched, th, can_direct_transfer, end_p);
+ th->status = THREAD_RUNNABLE;
+
+ // whoever woke this thread took the timeout back first
+ VM_ASSERT(th->sched.waiting_reason.flags == thread_sched_waiting_none);
+ }
}
thread_sched_unlock(sched, th);
rb_ractor_lock_self(cr);
@@ -1561,7 +1619,20 @@ rb_ractor_sched_wakeup(rb_ractor_t *r, rb_thread_t *r_th)
{
if (r_th->status == THREAD_STOPPED_FOREVER) {
RUBY_ATOMIC_ADD(r_th->unblock.event_serial, 1);
- thread_sched_to_ready_common(sched, r_th, true, false);
+
+ // r_th must not resume with a wheel entry left behind: take its timeout
+ // back, as ubf_event_waiting() does. Only r_th arms it, and it is
+ // parked here, so reading the flags without the timer lock is safe.
+ if (r_th->sched.waiting_reason.flags != thread_sched_waiting_none) {
+ ractor_sched_timeout_disarm(r_th);
+ }
+
+ // a timeout that fired first may have made r_th runnable already: waking
+ // it twice would put it on the readyq twice
+ if (sched->running != r_th && !r_th->sched.node.is_ready) {
+ r_th->sched.event_serial++; // a timeout still armed must not wake it again
+ thread_sched_to_ready_common(sched, r_th, true, false);
+ }
}
}
thread_sched_unlock(sched, r_th);
@@ -2491,7 +2562,7 @@ nt_start(void *ptr)
if (sched->running == th) {
thread_sched_add_running_thread(sched, th);
}
- thread_sched_wait_running_turn(sched, th, false);
+ thread_sched_wait_running_turn(sched, th, false, NULL);
}
thread_sched_unlock(sched, th);
diff --git a/thread_pthread_mn.c b/thread_pthread_mn.c
index 938ac1001f4b38..d1bbd362c58039 100644
--- a/thread_pthread_mn.c
+++ b/thread_pthread_mn.c
@@ -364,6 +364,24 @@ enum timer_thread_register_result {
static enum timer_thread_register_result
timer_thread_register_waiting(rb_thread_t *th, int fd, enum thread_sched_waiting_flag flags, rb_hrtime_t *rel, uint32_t event_serial);
+// Arm a timeout-only wake on the timer thread for a Ractor wait. Returns false
+// if the deadline has already passed, in which case nothing was registered.
+static bool
+ractor_sched_timeout_arm(rb_thread_t *th, const rb_hrtime_t *rel)
+{
+ rb_hrtime_t rel_copy = *rel;
+
+ return timer_thread_register_waiting(th, -1, thread_sched_waiting_timeout, &rel_copy,
+ ++th->sched.event_serial) == timer_thread_registered;
+}
+
+// Returns true if the timeout was still armed, i.e. it did not fire.
+static bool
+ractor_sched_timeout_disarm(rb_thread_t *th)
+{
+ return timer_thread_cancel_waiting(th);
+}
+
// return how the wait ended; see enum thread_sched_wait_result
static enum thread_sched_wait_result
thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd, enum thread_sched_waiting_flag events, rb_hrtime_t *rel)
@@ -414,7 +432,7 @@ thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd,
enum rb_thread_status prev_status = th->status;
if (prev_status == THREAD_RUNNABLE) th->status = THREAD_STOPPED_FOREVER;
thread_sched_wakeup_next_thread(sched, th, true);
- thread_sched_wait_running_turn(sched, th, true);
+ thread_sched_wait_running_turn(sched, th, true, NULL);
if (prev_status == THREAD_RUNNABLE) th->status = THREAD_RUNNABLE;
RUBY_DEBUG_LOG("wakeup");
@@ -1660,6 +1678,20 @@ thread_sched_wait_events(struct rb_thread_sched *sched, rb_thread_t *th, int fd,
rb_bug("unreachable");
}
+// Without the wheel every thread is dedicated, so a Ractor wait takes its
+// deadline on its own condvar and never reaches these.
+static bool
+ractor_sched_timeout_arm(rb_thread_t *th, const rb_hrtime_t *rel)
+{
+ rb_bug("unreachable");
+}
+
+static bool
+ractor_sched_timeout_disarm(rb_thread_t *th)
+{
+ rb_bug("unreachable");
+}
+
static int
timer_wheel_timeout(int timeout)
{
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs
index b49753635e2b13..2922e9002b8524 100644
--- a/zjit/src/hir.rs
+++ b/zjit/src/hir.rs
@@ -3676,12 +3676,33 @@ impl Function {
let mut reachable = BlockSet::with_capacity(self.blocks.len());
reachable.insert(self.entries_block);
- // Walk the graph, computing types until fixpoint
+ // Repeatedly walk the graph in RPO order, computing types until fixpoint. For each
+ // iteration over the CFG, track the following two attributes to detect the fixpoint:
+ //
+ // 1. if new types were inferred
+ // 2. if back edges were traversed
+ //
+ // For point (1), if no new types were inferred it means no new information is available.
+ // Further repetitions will not change the result.
+ //
+ // For point (2), if the RPO walk does not traverse a back edge, type information can only
+ // be propagated forwards. It follows that a node's type can only be inferred from its
+ // predecessors. RPO ordering ensures all of a node's predecessors have been processed;
+ // therefore, a single walk of the RPO ordering will reach the fixpoint.
let rpo = self.reverse_post_order();
+ // Map BlockId -> rpo index. Used to detect back edge traversal. If an edge targets a block
+ // with rpo index <= the current rpo index it's a back edge. Note that `rpo_order` must be
+ // of size `self.blocks.len()` to support all possible block IDs; however, `rpo` only
+ // includes reachable blocks. Any blocks not present in `rpo` default to `usize::MAX`.
+ let mut rpo_order = vec![usize::MAX; self.blocks.len()];
+ for (idx, &block_id) in rpo.iter().enumerate() {
+ rpo_order[block_id.to_usize()] = idx;
+ }
loop {
let mut changed = false;
+ let mut traversed_back_edge = false;
let mut num_instructions = 0;
- for &block in &rpo {
+ for (rpo_index, &block) in rpo.iter().enumerate() {
if !reachable.get(block) { continue; }
for i in 0..self.blocks[block.to_usize()].insns.len() {
let insn_id = self.blocks[block.to_usize()].insns[i];
@@ -3703,6 +3724,7 @@ impl Function {
let param = self.blocks[if_true.target.to_usize()].params[idx];
changed |= set_type!(param, self.type_of(param).union(arg_type));
}
+ traversed_back_edge |= rpo_order[if_true.target.to_usize()] <= rpo_index;
}
if self.type_of(*val).could_be(Type::from_cbool(false)) {
reachable.insert(if_false.target);
@@ -3711,6 +3733,7 @@ impl Function {
let param = self.blocks[if_false.target.to_usize()].params[idx];
changed |= set_type!(param, self.type_of(param).union(arg_type));
}
+ traversed_back_edge |= rpo_order[if_false.target.to_usize()] <= rpo_index;
}
continue;
}
@@ -3721,6 +3744,7 @@ impl Function {
let param = self.blocks[target.to_usize()].params[idx];
changed |= set_type!(param, self.type_of(param).union(arg_type));
}
+ traversed_back_edge |= rpo_order[target.to_usize()] <= rpo_index;
continue;
}
Insn::Entries { targets } => {
@@ -3735,7 +3759,7 @@ impl Function {
changed |= set_type!(insn_id, insn_type);
}
}
- if !changed {
+ if !changed || !traversed_back_edge {
self.num_instructions = num_instructions;
break;
}