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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cont.c
Original file line number Diff line number Diff line change
Expand Up @@ -2201,7 +2201,7 @@ rb_fiber_storage_set(VALUE self, VALUE value)
* Returns the value of the fiber storage variable identified by +key+.
*
* The +key+ must be a symbol, and the value is set by Fiber#[]= or
* Fiber#store.
* Fiber#storage.
*
* See also Fiber::[]=.
*/
Expand Down
2 changes: 1 addition & 1 deletion ext/psych/lib/psych/versions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ module Psych
VERSION = '5.3.0'

if RUBY_ENGINE == 'jruby'
DEFAULT_SNAKEYAML_VERSION = '2.9'.freeze
DEFAULT_SNAKEYAML_VERSION = '2.10'.freeze
end
end
1 change: 1 addition & 0 deletions gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -2061,6 +2061,7 @@ obj_free_object_id(VALUE obj)
void
rb_gc_obj_free_vm_weak_references(VALUE obj)
{
ASSUME(!RB_SPECIAL_CONST_P(obj));
obj_free_object_id(obj);

if (rb_obj_gen_fields_p(obj)) {
Expand Down
4 changes: 2 additions & 2 deletions internal/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -658,8 +658,8 @@ RCLASS_SET_REFINED_CLASS(VALUE klass, VALUE refined)
static inline rb_alloc_func_t
RCLASS_ALLOCATOR(VALUE klass)
{
RUBY_ASSERT(RB_TYPE_P(klass, T_CLASS) || RB_TYPE_P(klass, T_ICLASS));
if (RCLASS_SINGLETON_P(klass) || RB_TYPE_P(klass, T_ICLASS)) {
RBIMPL_ASSERT_TYPE(klass, T_CLASS);
if (RCLASS_SINGLETON_P(klass)) {
return 0;
}
return RCLASS_EXT_PRIME(klass)->as.class.allocator;
Expand Down
149 changes: 111 additions & 38 deletions lib/timeout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module Timeout
# The version
VERSION = "0.5.0"

# Internal error raised to when a timeout is triggered.
# Internal exception raised to when a timeout is triggered.
class ExitException < Exception
def exception(*) # :nodoc:
self
Expand Down Expand Up @@ -54,8 +54,6 @@ def self.handle_timeout(message) # :nodoc:
private_constant :GET_TIME

class State
attr_reader :condvar, :queue, :queue_mutex # shared with Timeout.timeout()

def initialize
@condvar = ConditionVariable.new
@queue = Queue.new
Expand Down Expand Up @@ -83,36 +81,40 @@ def self.instance
end

def create_timeout_thread
watcher = Thread.new do
requests = []
while true
until @queue.empty? and !requests.empty? # wait to have at least one request
req = @queue.pop
requests << req unless req.done?
end
closest_deadline = requests.min_by(&:deadline).deadline
# Threads unexpectedly inherit the interrupt mask: https://github.com/ruby/timeout/issues/41
# So reset the interrupt mask to the default one for the timeout thread
Thread.handle_interrupt(Object => :immediate) do
watcher = Thread.new do
requests = []
while true
until @queue.empty? and !requests.empty? # wait to have at least one request
req = @queue.pop
requests << req unless req.done?
end
closest_deadline = requests.min_by(&:deadline).deadline

now = 0.0
@queue_mutex.synchronize do
while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and @queue.empty?
@condvar.wait(@queue_mutex, closest_deadline - now)
now = 0.0
@queue_mutex.synchronize do
while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and @queue.empty?
@condvar.wait(@queue_mutex, closest_deadline - now)
end
end
end

requests.each do |req|
req.interrupt if req.expired?(now)
requests.each do |req|
req.interrupt if req.expired?(now)
end
requests.reject!(&:done?)
end
requests.reject!(&:done?)
end
end

if !watcher.group.enclosed? && (!defined?(Ractor.main?) || Ractor.main?)
ThreadGroup::Default.add(watcher)
end
if !watcher.group.enclosed? && (!defined?(Ractor.main?) || Ractor.main?)
ThreadGroup::Default.add(watcher)
end

watcher.name = "Timeout stdlib thread"
watcher.thread_variable_set(:"\0__detached_thread__", true)
watcher
watcher.name = "Timeout stdlib thread"
watcher.thread_variable_set(:"\0__detached_thread__", true)
watcher
end
end

def ensure_timeout_thread_created
Expand All @@ -121,13 +123,20 @@ def ensure_timeout_thread_created
# In that case, just return and let the main thread create the Timeout thread.
return if @timeout_thread_mutex.owned?

@timeout_thread_mutex.synchronize do
Sync.synchronize @timeout_thread_mutex do
unless @timeout_thread&.alive?
@timeout_thread = create_timeout_thread
end
end
end
end

def add_request(request)
Sync.synchronize @queue_mutex do
@queue << request
@condvar.signal
end
end
end
private_constant :State

Expand All @@ -144,6 +153,7 @@ def initialize(thread, timeout, exception_class, message)
@done = false # protected by @mutex
end

# Only called by the timeout thread, so does not need Sync.synchronize
def done?
@mutex.synchronize do
@done
Expand All @@ -154,6 +164,7 @@ def expired?(now)
now >= @deadline
end

# Only called by the timeout thread, so does not need Sync.synchronize
def interrupt
@mutex.synchronize do
unless @done
Expand All @@ -164,16 +175,36 @@ def interrupt
end

def finished
@mutex.synchronize do
Sync.synchronize @mutex do
@done = true
end
end
end
private_constant :Request

module Sync
# Calls mutex.synchronize(&block) but if that fails on CRuby due to being in a trap handler,
# run mutex.synchronize(&block) in a separate Thread instead.
def self.synchronize(mutex, &block)
begin
mutex.synchronize(&block)
rescue ThreadError => e
raise e unless e.message == "can't be called from trap context"
# Workaround CRuby issue https://bugs.ruby-lang.org/issues/19473
# which raises on Mutex#synchronize in trap handler.
# It's expensive to create a Thread just for this,
# but better than failing.
Thread.new {
mutex.synchronize(&block)
}.join
end
end
end
private_constant :Sync

# :startdoc:

# Perform an operation in a block, raising an error if it takes longer than
# Perform an operation in a block, raising an exception if it takes longer than
# +sec+ seconds to complete.
#
# +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number
Expand All @@ -186,19 +217,64 @@ def finished
# Omitting will use the default, "execution expired"
#
# Returns the result of the block *if* the block completed before
# +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
# +sec+ seconds, otherwise raises an exception, based on the value of +klass+.
#
# The exception thrown to terminate the given block cannot be rescued inside
# the block unless +klass+ is given explicitly. However, the block can use
# ensure to prevent the handling of the exception. For that reason, this
# method cannot be relied on to enforce timeouts for untrusted blocks.
# The exception raised to terminate the given block is the given +klass+, or
# Timeout::ExitException if +klass+ is not given. The reason for that behavior
# is that Timeout::Error inherits from RuntimeError and might be caught unexpectedly by `rescue`.
# Timeout::ExitException inherits from Exception so it will only be rescued by `rescue Exception`.
# Note that the Timeout::ExitException is translated to a Timeout::Error once it reaches the Timeout.timeout call,
# so outside that call it will be a Timeout::Error.
#
# In general, be aware that the code block may rescue the exception, and in such a case not respect the timeout.
# Also, the block can use +ensure+ to prevent the handling of the exception.
# For those reasons, this method cannot be relied on to enforce timeouts for untrusted blocks.
#
# If a scheduler is defined, it will be used to handle the timeout by invoking
# Scheduler#timeout_after.
#
# Note that this is both a method of module Timeout, so you can <tt>include
# Timeout</tt> into your classes so they have a #timeout method, as well as
# a module method, so you can call it directly as Timeout.timeout().
#
# ==== Ensuring the exception does not fire inside ensure blocks
#
# When using Timeout.timeout it can be desirable to ensure the timeout exception does not fire inside an +ensure+ block.
# The simplest and best way to do so it to put the Timeout.timeout call inside the body of the begin/ensure/end:
#
# begin
# Timeout.timeout(sec) { some_long_operation }
# ensure
# cleanup # safe, cannot be interrupt by timeout
# end
#
# If that is not feasible, e.g. if there are +ensure+ blocks inside +some_long_operation+,
# they need to not be interrupted by timeout, and it's not possible to move these ensure blocks outside,
# one can use Thread.handle_interrupt to delay the timeout exception like so:
#
# Thread.handle_interrupt(Timeout::Error => :never) {
# Timeout.timeout(sec, Timeout::Error) do
# setup # timeout cannot happen here, no matter how long it takes
# Thread.handle_interrupt(Timeout::Error => :immediate) {
# some_long_operation # timeout can happen here
# }
# ensure
# cleanup # timeout cannot happen here, no matter how long it takes
# end
# }
#
# An important thing to note is the need to pass an exception klass to Timeout.timeout,
# otherwise it does not work. Specifically, using +Thread.handle_interrupt(Timeout::ExitException => ...)+
# is unsupported and causes subtle errors like raising the wrong exception outside the block, do not use that.
#
# Note that Thread.handle_interrupt is somewhat dangerous because if setup or cleanup hangs
# then the current thread will hang too and the timeout will never fire.
# Also note the block might run for longer than +sec+ seconds:
# e.g. some_long_operation executes for +sec+ seconds + whatever time cleanup takes.
#
# If you want the timeout to only happen on blocking operations one can use :on_blocking
# instead of :immediate. However, that means if the block uses no blocking operations after +sec+ seconds,
# the block will not be interrupted.
def self.timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
return yield(sec) if sec == nil or sec.zero?
raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec
Expand All @@ -214,10 +290,7 @@ def self.timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+

perform = Proc.new do |exc|
request = Request.new(Thread.current, sec, exc, message)
state.queue_mutex.synchronize do
state.queue << request
state.condvar.signal
end
state.add_request(request)
begin
return yield(sec)
ensure
Expand Down
6 changes: 4 additions & 2 deletions object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2238,8 +2238,10 @@ class_call_alloc_func(rb_alloc_func_t allocator, VALUE klass)

obj = (*allocator)(klass);

if (rb_obj_class(obj) != rb_class_real(klass)) {
rb_raise(rb_eTypeError, "wrong instance allocation");
if (UNLIKELY(RBASIC_CLASS(obj) != klass)) {
if (rb_obj_class(obj) != rb_class_real(klass)) {
rb_raise(rb_eTypeError, "wrong instance allocation");
}
}
return obj;
}
Expand Down
12 changes: 7 additions & 5 deletions set.c
Original file line number Diff line number Diff line change
Expand Up @@ -1291,15 +1291,17 @@ set_xor_i(st_data_t key, st_data_t data)
static VALUE
set_i_xor(VALUE set, VALUE other)
{
VALUE new_set;
VALUE new_set = rb_obj_dup(set);

if (rb_obj_is_kind_of(other, rb_cSet)) {
new_set = other;
set_iter(other, set_xor_i, (st_data_t)new_set);
}
else {
new_set = set_s_alloc(rb_obj_class(set));
set_merge_enum_into(new_set, other);
VALUE tmp = set_s_alloc(rb_cSet);
set_merge_enum_into(tmp, other);
set_iter(tmp, set_xor_i, (st_data_t)new_set);
}
set_iter(set, set_xor_i, (st_data_t)new_set);

return new_set;
}

Expand Down
1 change: 0 additions & 1 deletion test/json/json_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,6 @@ def test_parse_whitespace_after_newline

def test_frozen
parser_config = JSON::Parser::Config.new({}).freeze
omit "JRuby failure in CI" if RUBY_ENGINE == "jruby"
assert_raise FrozenError do
parser_config.send(:initialize, {})
end
Expand Down
11 changes: 11 additions & 0 deletions test/ruby/test_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,17 @@ def test_xor
}
end

def test_xor_does_not_mutate_other_set
a = Set[1]
b = Set[1, 2]
original_b = b.dup

result = a ^ b

assert_equal(original_b, b)
assert_equal(Set[2], result)
end

def test_eq
set1 = Set[2,3,1]
set2 = Set[1,2,3]
Expand Down
Loading