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; }