Commit graph

143 commits

Author SHA1 Message Date
Jean Boussier
547f111b5b Refactor vm_lookup_cc to allow lock-free lookups in RClass.cc_tbl
In multi-ractor mode, the `cc_tbl` mutations use the RCU pattern,
which allow lock-less reads.

Based on the assumption that invalidations and misses should be
increasingly rare as the process ages, locking on modification
isn't a big concern.
2025-08-01 10:42:04 +02:00
Luke Gruber
be58cd4d7d Ractor: lock around global variable get/set
There's a global id_table `rb_global_tbl` that needs a lock (I used VM lock). In the future, we might use a lock-free rb_id_table if we create such a data structure.

Reproduction script that might crash or behave strangely:

```ruby
100.times do
  Ractor.new do
    1_000_000.times do
      $stderr
      $stdout
      $stdin
      $VERBOSE
      $stderr
      $stdout
      $stdin
      $VERBOSE
      $stderr
      $stdout
      $stdin
      $VERBOSE
    end
  end
end

$myglobal0 = nil;
$myglobal1 = nil;
  # ... vim macros to the rescue
$myglobal100000 = nil;
```
2025-07-21 15:57:44 +02:00
Luke Gruber
815eb58fce Fix btest in ractor_test.rb that can lead timeout of the test
It could also potentially lead to an out of memory error.
2025-07-17 11:21:01 -07:00
John Hawthorn
6c66458070 Fix rb_eSystemExit raised in Ractor
[Bug #21505]

Previously `Ractor.new { exit }.join` would hang because SystemExit was
special cased.

This commit updates this to take the same path as other exceptions,
which wraps the exception in a Ractor::RemoteError and does not end up
exiting the main Ractor. I don't know if that's what this should do, but
I think it's a reasonable behaviour as calling exit() in a Ractor is
odd.

    in 'Ractor#join': thrown by remote Ractor. (Ractor::RemoteError)
       from -e:1:in '<main>'
    in 'Kernel#exit': exit (SystemExit)
            from -e:1:in 'block in <main>'
2025-07-10 15:57:08 -07:00
John Hawthorn
365317f6ba Fix wrong GENIV WB on too_complex Ractor traversal
WBCHECK ERROR: Missed write barrier detected!
      Parent object: 0x7c4a5f1f66c0 (wb_protected: true)
        rb_obj_info_dump: 0x00007c4a5f1f66c0 T_IMEMO/<fields>
      Reference counts - snapshot: 2, writebarrier: 0, current: 2, missed: 1
      Missing reference to: 0x7b6a5f2f7010
        rb_obj_info_dump: 0x00007b6a5f2f7010 T_ARRAY/Array [E ] len: 1 (embed)
2025-07-04 14:54:49 -07:00
Takashi Kokubun
57f4460f0c
ZJIT: Skip a hanging ractor test (#13774) 2025-07-03 09:22:45 -07:00
Koichi Sasada
8070d5d97d Ractor#take and warn
`Ractor#take` was deprecated but some libraries can use it as
an alias for `Ractor#value` (i.e., to wait for a Ractor's
temrination and retrieve its result).
Therefore `Ractor#take` is simply an alias for `Ractor#value`.

This method will remain available until the end of August 2025,
unless there is further discussion.
2025-06-04 19:18:16 +09:00
Koichi Sasada
ef2bb61018 Ractor::Port
* Added `Ractor::Port`
  * `Ractor::Port#receive` (support multi-threads)
  * `Rcator::Port#close`
  * `Ractor::Port#closed?`
* Added some methods
  * `Ractor#join`
  * `Ractor#value`
  * `Ractor#monitor`
  * `Ractor#unmonitor`
* Removed some methods
  * `Ractor#take`
  * `Ractor.yield`
* Change the spec
  * `Racotr.select`

You can wait for multiple sequences of messages with `Ractor::Port`.

```ruby
ports = 3.times.map{ Ractor::Port.new }
ports.map.with_index do |port, ri|
  Ractor.new port,ri do |port, ri|
    3.times{|i| port << "r#{ri}-#{i}"}
  end
end

p ports.each{|port| pp 3.times.map{port.receive}}

```

In this example, we use 3 ports, and 3 Ractors send messages to them respectively.
We can receive a series of messages from each port.

You can use `Ractor#value` to get the last value of a Ractor's block:

```ruby
result = Ractor.new do
  heavy_task()
end.value
```

You can wait for the termination of a Ractor with `Ractor#join` like this:

```ruby
Ractor.new do
  some_task()
end.join
```

`#value` and `#join` are similar to `Thread#value` and `Thread#join`.

To implement `#join`, `Ractor#monitor` (and `Ractor#unmonitor`) is introduced.

This commit changes `Ractor.select()` method.
It now only accepts ports or Ractors, and returns when a port receives a message or a Ractor terminates.

We removes `Ractor.yield` and `Ractor#take` because:
* `Ractor::Port` supports most of similar use cases in a simpler manner.
* Removing them significantly simplifies the code.

We also change the internal thread scheduler code (thread_pthread.c):
* During barrier synchronization, we keep the `ractor_sched` lock to avoid deadlocks.
  This lock is released by `rb_ractor_sched_barrier_end()`
  which is called at the end of operations that require the barrier.
* fix potential deadlock issues by checking interrupts just before setting UBF.

https://bugs.ruby-lang.org/issues/21262
2025-05-31 04:01:33 +09:00
Peter Zhu
386f874816 Don't copy FL_PROMOTED to new object in Ractor move
We should not copy the FL_PROMOTED flag when we move an object in Ractor#send(move: true)
because the newly created object may not be old.
2025-05-26 15:04:00 -04:00
Peter Zhu
746d7fef92 Fix moving old objects between Ractors
The FL_PROMOTED flag was not copied when moving objects, causing assertions
to fail when an old object is moved:

    gc/default/default.c:834: Assertion Failed: RVALUE_AGE_SET:age <= RVALUE_OLD_AGE

Co-Authored-By: Luke Gruber <luke.gruber@shopify.com>
2025-05-23 11:06:53 -04:00
Luke Gruber
966fcb77e4 lock vm around rb_free_generic_ivar
Currently, this can be reproduced by:

r = Ractor.new do
    a = [1, 2, 3]
    a.object_id
    a.dup # this frees the generic ivar for `object_id` on the copied object
    :done
end
r.take

In debug builds, this hits an assertion failure without this fix.
2025-05-23 18:20:35 +09:00
Luke Gruber
f6cbf499bc Fix Symbol#to_proc (rb_sym_to_proc) to be ractor safe
In non-main ractors, don't use `sym_proc_cache`. It is not thread-safe
to add to this array without a lock and also it leaks procs from one
ractor to another. Instead, we create a new proc each time. If this
results in poor performance we can come up with a solution later.

Fixes [Bug #21354]
2025-05-21 08:12:18 +02:00
Luke Gruber
1d4822a175 Get ractor message passing working with > 1 thread sending/receiving values in same ractor
Rework ractors so that any ractor action (Ractor.receive, Ractor#send, Ractor.yield, Ractor#take,
Ractor.select) will operate on the thread that called the action. It will put that thread to sleep if
it's a blocking function and it needs to put it to sleep, and the awakening action (Ractor.yield,
Ractor#send) will wake up the blocked thread.

Before this change every blocking ractor action was associated with the ractor struct and its fields.
If a ractor called Ractor.receive, its wait status was wait_receiving, and when another ractor calls
r.send on it, it will look for that status in the ractor struct fields and wake it up. The problem was that
what if 2 threads call blocking ractor actions in the same ractor. Imagine if 1 thread has called Ractor.receive
and another r.take. Then, when a different ractor calls r.send on it, it doesn't know which ruby thread is associated
to which ractor action, so what ruby thread should it schedule? This change moves some fields onto the ruby thread
itself so that ruby threads are the ones that have ractor blocking statuses, and threads are then specifically scheduled
when unblocked.

Fixes [#17624]
Fixes [#21037]
2025-05-13 13:23:57 -07:00
lukeg
c941fced21 Throw RuntimeError if getting/setting ractor local storage for non-main ractor
[Bug #19367]
2025-05-13 13:18:10 +02:00
Peter Zhu
f30f0f0a22 Fix crash when instantiating classes in Ractors
[Bug #18119]

When we create classes, it pushes the class to the subclass list of the
superclass. This access needs to be synchronized because multiple Ractors
may be creating classes with the same superclass, which would cause race
conditions and cause the linked list to be corrupted.

For example, we can reproduce with this script crashing:

    workers = (0...8).map do
      Ractor.new do
        loop do
          100.times.map { Class.new }
          Ractor.yield nil
        end
      end
    end

    100.times { Ractor.select(*workers) }

With ASAN enabled, we can see that there are use-after-free errors:

    ==176013==ERROR: AddressSanitizer: heap-use-after-free on address 0x5030000974f0 at pc 0x62f9e56f892d bp 0x7a503f1ffd90 sp 0x7a503f1ffd88
    WRITE of size 8 at 0x5030000974f0 thread T4
        #0 0x62f9e56f892c in rb_class_remove_from_super_subclasses class.c:149:24
        #1 0x62f9e58c9dd2 in rb_gc_obj_free gc.c:1262:9
        #2 0x62f9e58f6e19 in gc_sweep_plane gc/default/default.c:3450:21
        #3 0x62f9e58f686a in gc_sweep_page gc/default/default.c:3535:13
        #4 0x62f9e58f12b4 in gc_sweep_step gc/default/default.c:3810:9
        #5 0x62f9e58ed2a7 in gc_sweep gc/default/default.c:4058:13
        #6 0x62f9e58fac93 in gc_start gc/default/default.c:6402:13
        #7 0x62f9e58e8b69 in heap_prepare gc/default/default.c:2032:13
        #8 0x62f9e58e8b69 in heap_next_free_page gc/default/default.c:2255:9
        #9 0x62f9e58e8b69 in newobj_cache_miss gc/default/default.c:2362:38
    ...
    0x5030000974f0 is located 16 bytes inside of 24-byte region [0x5030000974e0,0x5030000974f8)
    freed by thread T4 here:
        #0 0x62f9e562f28a in free (miniruby+0x1fd28a) (BuildId: 5ad6d9e7cec8318df6726ea5ce34d3c76d0d0233)
        #1 0x62f9e58ca2ab in rb_gc_impl_free gc/default/default.c:8102:9
        #2 0x62f9e58ca2ab in ruby_sized_xfree gc.c:5029:13
        #3 0x62f9e58ca2ab in ruby_xfree gc.c:5040:5
        #4 0x62f9e56f88e6 in rb_class_remove_from_super_subclasses class.c:152:9
        #5 0x62f9e58c9dd2 in rb_gc_obj_free gc.c:1262:9
        #6 0x62f9e58f6e19 in gc_sweep_plane gc/default/default.c:3450:21
        #7 0x62f9e58f686a in gc_sweep_page gc/default/default.c:3535:13
        #8 0x62f9e58f12b4 in gc_sweep_step gc/default/default.c:3810:9
        #9 0x62f9e58ed2a7 in gc_sweep gc/default/default.c:4058:13
    ...
    previously allocated by thread T5 here:
        #0 0x62f9e562f70d in calloc (miniruby+0x1fd70d) (BuildId: 5ad6d9e7cec8318df6726ea5ce34d3c76d0d0233)
        #1 0x62f9e58c8e1a in calloc1 gc/default/default.c:1472:12
        #2 0x62f9e58c8e1a in rb_gc_impl_calloc gc/default/default.c:8138:5
        #3 0x62f9e58c8e1a in ruby_xcalloc_body gc.c:4964:12
        #4 0x62f9e58c8e1a in ruby_xcalloc gc.c:4958:34
        #5 0x62f9e56f906e in push_subclass_entry_to_list class.c:88:13
        #6 0x62f9e56f906e in rb_class_subclass_add class.c:111:38
        #7 0x62f9e56f906e in RCLASS_SET_SUPER internal/class.h:257:9
        #8 0x62f9e56fca7a in make_metaclass class.c:786:5
        #9 0x62f9e59db982 in rb_class_initialize object.c:2101:5
2025-05-09 10:24:38 -04:00
Aaron Patterson
e3452cfad2 Raise error on take/send for Ractors in child processes
Ractor objects that are available in a child process should raise a
`Ractor::ClosedError` exception when called with `send` or `take`

Co-authored-by: John Hawthorn <john@hawthorn.email>
2025-05-08 10:53:28 -07:00
Aaron Patterson
f7ff380998 Clean up Ractor cache after fork
Ractors created in a parent process should be properly shut down in the
child process.  They need their cache cleared and status set to
"terminated"

Co-authored-by: John Hawthorn <john@hawthorn.email>
2025-05-08 10:53:28 -07:00
Jean Boussier
0ea210d1ea Rename ivptr -> fields, next_iv_index -> next_field_index
Ivars will longer be the only thing stored inline
via shapes, so keeping the `iv_index` and `ivptr` names
would be confusing.

Instance variables won't be the only thing stored inline
via shapes, so keeping the `ivptr` name would be confusing.

`field` encompass anything that can be stored in a VALUE array.

Similarly, `gen_ivtbl` becomes `gen_fields_tbl`.
2025-05-08 07:58:05 +02:00
John Hawthorn
57b6a7503f Lock-free hash set for fstrings [Feature #21268]
This implements a hash set which is wait-free for lookup and lock-free
for insert (unless resizing) to use for fstring de-duplication.

As highlighted in https://bugs.ruby-lang.org/issues/19288, heavy use of
fstrings (frozen interned strings) can significantly reduce the
parallelism of Ractors.

I tried a few other approaches first: using an RWLock, striping a series
of RWlocks (partitioning the hash N-ways to reduce lock contention), and
putting a cache in front of it. All of these improved the situation, but
were unsatisfying as all still required locks for writes (and granular
locks are awkward, since we run the risk of needing to reach a vm
barrier) and this table is somewhat write-heavy.

My main reference for this was Cliff Click's talk on a lock free
hash-table for java https://www.youtube.com/watch?v=HJ-719EGIts. It
turns out this lock-free hash set is made easier to implement by a few
properties:

 * We only need a hash set rather than a hash table (we only need keys,
   not values), and so the full entry can be written as a single VALUE
 * As a set we only need lookup/insert/delete, no update
 * Delete is only run inside GC so does not need to be atomic (It could
   be made concurrent)
 * I use rb_vm_barrier for the (rare) table rebuilds (It could be made
   concurrent) We VM lock (but don't require other threads to stop) for
   table rebuilds, as those are rare
 * The conservative garbage collector makes deferred replication easy,
   using a T_DATA object

Another benefits of having a table specific to fstrings is that we
compare by value on lookup/insert, but by identity on delete, as we only
want to remove the exact string which is being freed. This is faster and
provides a second way to avoid the race condition in
https://bugs.ruby-lang.org/issues/21172.

This is a pretty standard open-addressing hash table with quadratic
probing. Similar to our existing st_table or id_table. Deletes (which
happen on GC) replace existing keys with a tombstone, which is the only
type of update which can occur. Tombstones are only cleared out on
resize.

Unlike st_table, the VALUEs are stored in the hash table itself
(st_table's bins) rather than as a compact index. This avoids an extra
pointer dereference and is possible because we don't need to preserve
insertion order. The table targets a load factor of 2 (it is enlarged
once it is half full).
2025-04-18 13:03:54 +09:00
Luke Gruber
72dc16aa65 Add a test for moving composite object parts 2025-04-15 16:34:22 +09:00
Hiroshi SHIBATA
f70bf78403 Fixed wrong condition to avoid flaky ractor_test.rb 2025-04-07 15:10:58 +09:00
Naoto Ono
b5ac483d95
Fix the if condition to skip test_ractor.rb correctly (#13067)
Follow-up for a2b03ba7cb
2025-04-06 09:38:58 +09:00
Jean Boussier
085cc6e434 Ractor: revert to moving object bytes, but size pool aware
Using `rb_obj_clone` introduce other problems, such as `initialize_*`
callbacks invocation in the context of the parent ractor.

So we can revert back to copy the content of the object slots,
but in a way that is aware of size pools.
2025-04-04 16:26:29 +02:00
Jean Boussier
7db0e07134 Don't preserve object_id when moving object to another Ractor
That seemed like the logical thing to do to me, but ko1 disagree.
2025-03-31 12:01:55 +02:00
Jean Boussier
0350290262 Ractor: Fix moving embedded objects
[Bug #20271]
[Bug #20267]
[Bug #20255]

`rb_obj_alloc(RBASIC_CLASS(obj))` will always allocate from the basic
40B pool, so if `obj` is larger than `40B`, we'll create a corrupted
object when we later copy the shape_id.

Instead we can use the same logic than ractor copy, which is
to use `rb_obj_clone`, and later ask the GC to free the original
object.

We then must turn it into a `T_OBJECT`, because otherwise
just changing its class to `RactorMoved` leaves a lot of
ways to keep using the object, e.g.:

```
a = [1, 2, 3]
Ractor.new{}.send(a, move: true)
[].concat(a) # Should raise, but wasn't.
```

If it turns out that `rb_obj_clone` isn't performant enough
for some uses, we can always have carefully crafted specialized
paths for the types that would benefit from it.
2025-03-31 12:01:55 +02:00
Hiroshi SHIBATA
a2b03ba7cb Skip test_ractor.rb with ModGC workflow because this test is flaky 2025-03-31 15:08:58 +09:00
lukeg
d80f3a287c Ractor.make_shareable(proc_obj) makes inner structure shareable
Proc objects are now traversed like other objects when making them
shareable.

Fixes [Bug #19372]
Fixes [Bug #19374]
2025-03-26 16:05:02 -07:00
John Hawthorn
bfe6068417 Use atomic for method reference count [Bug #20934]
This changes reference_count on rb_method_definition_struct into an
atomic.

Ractors can create additional references as part of `bind_call` or
(presumably) similar. Because this can be done inside Ractors, we should
use a lock or atomics so that we don't race and avoid incrementing.

Co-authored-by: wanabe <s.wanabe@gmail.com>
2025-03-20 13:09:40 -07:00
Nobuyoshi Nakada
4a67ef09cc
[Feature #21116] Extract RJIT as a third-party gem 2025-02-13 18:01:03 +09:00
Peter Zhu
cfee3d9f4b Revert "[MMTk/CI] Skip Ractor btests with MMTk"
This reverts commit 58b4e249ed.

The bug that it encountered was fixed in f76d40789d.
2025-01-10 10:17:16 -05:00
Peter Zhu
f1049aa55d Don't check for presence of ENV['GITHUB_WORKFLOW']
We already check whether `ENV['GITHUB_WORKFLOW']` is equal to `Compilations`,
so we don't need to check that it's not nil.
2025-01-10 10:17:16 -05:00
Hiroshi SHIBATA
3a1414a70b Rewrite Benchmark to Tempfile on bootstraptest/test_ractor.rb 2025-01-10 10:19:39 +09:00
Luke Gruber
38af38edcb Fix ractor move of unshareable frozen objects
These objects didn't retain their frozen status after the move

Bug [#19408]
2024-12-24 11:38:44 +09:00
Koichi Sasada
0bdb38ba6b Ractor.set_if_absent(key)
to initialize ractor local storage in thread-safety.
[Feature #20875]
2024-12-13 06:22:13 +09:00
Matt Valentine-House
58b4e249ed [MMTk/CI] Skip Ractor btests with MMTk
currently these are flaky, so until we can make them more robust, we'll
skip them for MMTk CI
2024-12-06 09:48:30 +00:00
Koichi Sasada
78064d0770 skip SystemStackError
with -O0 build, prism parser consumes a lot of machine stack and
it doesn't work with minimum machine stack for threads, which
specified with `RUBY_THREAD_MACHINE_STACK_SIZE=1`.

So simply ignore `SystemStackError` for btest.
2024-11-08 18:02:46 +09:00
Koichi Sasada
aa63699d10 support require in non-main Ractors
Many libraries should be loaded on the main ractor because of
setting constants with unshareable objects and so on.

This patch allows to call `requore` on non-main Ractors by
asking the main ractor to call `require` on it. The calling ractor
waits for the result of `require` from the main ractor.

If the `require` call failed with some reasons, an exception
objects will be deliverred from the main ractor to the calling ractor
if it is copy-able.

Same on `require_relative` and `require` by `autoload`.

Now `Ractor.new{pp obj}` works well (the first call of `pp` requires
`pp` library implicitly).

[Feature #20627]
2024-11-08 18:02:46 +09:00
tompng
f4e548924e Update bootstraptest test for colon-style hash inspect 2024-10-03 18:47:09 +09:00
Koichi Sasada
43aee3393d fix defined?(@ivar) with Ractors
`defined?(@ivar)` on the non main Ractor has two issues:

1. raising an exception

```ruby
class C
  @iv1 = []
  def self.defined_iv1 = defined?(@iv1)
end

Ractor.new{
  p C.defined_iv1
  #=> can not get unshareable values from instance variables of classes/modules from non-main Ractors (Ractor::IsolationError)
}.take
```

-> Do not raise an exception but return `"instance-variable"` because
it is defined.

2. returning `"instance-variable"` if there is not defined.

```
class C
  # @iv2 is not defined
  def self.defined_iv2 = defined?(@iv2)
end

Ractor.new{
  p C.defined_iv2 #=> "instance-variable"
}.take
```

-> returns `nil`
2024-07-12 04:43:14 +09:00
Luke Gruber
6747fbe77d
Fix interrupts during Ractor.select
Fixes [Bug #20168]
2024-05-05 15:14:53 +00:00
Takashi Kokubun
a1db69f0c9 Skip a flaky Ractor test for YJIT
2431063188
2430763832

This seems like an existing, separate issue from what we're currently
investigating. The `iseq->body->jit_entry` setup is not Ractor-safe?
Let me suppress this for now and revisit this after resolving the
ongoing issue.
2024-04-26 11:11:30 -07:00
Étienne Barrié
12be40ae6b Implement chilled strings
[Feature #20205]

As a path toward enabling frozen string literals by default in the future,
this commit introduce "chilled strings". From a user perspective chilled
strings pretend to be frozen, but on the first attempt to mutate them,
they lose their frozen status and emit a warning rather than to raise a
`FrozenError`.

Implementation wise, `rb_compile_option_struct.frozen_string_literal` is
no longer a boolean but a tri-state of `enabled/disabled/unset`.

When code is compiled with frozen string literals neither explictly enabled
or disabled, string literals are compiled with a new `putchilledstring`
instruction. This instruction is identical to `putstring` except it marks
the String with the `STR_CHILLED (FL_USER3)` and `FL_FREEZE` flags.

Chilled strings have the `FL_FREEZE` flag as to minimize the need to check
for chilled strings across the codebase, and to improve compatibility with
C extensions.

Notes:
  - `String#freeze`: clears the chilled flag.
  - `String#-@`: acts as if the string was mutable.
  - `String#+@`: acts as if the string was mutable.
  - `String#clone`: copies the chilled flag.

Co-authored-by: Jean Boussier <byroot@ruby-lang.org>
2024-03-19 09:26:49 +01:00
Jean Boussier
91bf7eb274 Refactor frozen_string_literal check during compilation
In preparation for https://bugs.ruby-lang.org/issues/20205.

The `frozen_string_literal` compilation option will no longer
be a boolean but a tri-state: `on/off/default`.
2024-03-15 15:52:33 +01:00
Jean Boussier
09d8c99cdc Ensure test suite is compatible with --frozen-string-literal
As preparation for https://bugs.ruby-lang.org/issues/20205
making sure the test suite is compatible with frozen string
literals is making things easier.
2024-03-14 17:56:15 +01:00
Takashi Kokubun
2918e43dee Skip a flaky Ractor test for YJIT
`[BUG] pthread_mutex_lock: Invalid argument (EINVAL)` doesn't seem like
a fault of YJIT?

2073675497
2073957248
2024-01-22 10:31:21 -08:00
Luke Gruber
32c4b0125f Set Ractor moved object's shape to original object's shape
Fixes [Bug #19409]
2024-01-02 08:10:59 +09:00
Koichi Sasada
054f56fd3e moved object should not have a shape ID
fix [Bug #19917]
2023-12-20 07:04:32 +09:00
Koichi Sasada
c9a9b8036c remove Ractor::Selector from Ruby level
`Ractor::Selector` is not approved by Matz so remove it from
Ruby-level.

The implementation is used by `Ractor.select` so most of implementation
was remaind and calling `rb_init_ractor_selector()`, `Ractor::Selector`
will be defined. I will provide `ractor-selector` gem to try it.
2023-12-16 01:00:01 +09:00
Nobuyoshi Nakada
d3bcff0158
Fix a typo [ci skip] 2023-07-17 00:15:05 +09:00
Nobuyoshi Nakada
ce47ee00ae Fix indirect counter increment
`*pcnt++` just dereferences `pcnt` then increments the local variable,
but has no side effect.
2023-03-15 13:59:11 +09:00