[ruby-core:125830] [Ruby Feature#22128] C API: Expose RB_OBJ_SET_FROZEN_SHAREABLE
Issue #22128 has been reported by byroot (Jean Boussier). ---------------------------------------- Feature #22128: C API: Expose RB_OBJ_SET_FROZEN_SHAREABLE https://bugs.ruby-lang.org/issues/22128 * Author: byroot (Jean Boussier) * Status: Open ---------------------------------------- ### Context I'm trying to experiment with adapting Active Record for a Ractor architecture. Since database connections can't possibly be Ractor shareable, the idea is to warp each connection inside its own ractor, and then send SQL queries and responses through a port. But for this to perform well, I'd like to directly build the query response as a fully shareable object, so that it can be pushed into the port for free, instead of having Ruby need to recursively walk the potentially large response to mark objects as shareable. Here's an example of how it would work in trilogy: https://github.com/byroot/trilogy/commit/5f58200b398995d2fc0d4673a5a6bbccd0d... ### Problem Unfortunately, the necessary API isn't currently exposed in the C API: - `RB_OBJ_SET_FROZEN_SHAREABLE` - `RB_OBJ_SET_SHAREABLE` / `rb_obj_set_shareable` I understand that this API could potentially be misused, but given it's a C API, I believe it's acceptable to require care from the caller. -- https://bugs.ruby-lang.org/
Issue #22128 has been updated by jhawthorn (John Hawthorn). I agree we need to find some way to make up that performance, but since `RB_OBJ_SET_FROZEN_SHAREABLE` is shallow and doesn't verify that referenced objects are shareable, this seems very hard to use safely. Even in the example Trilogy PR it's hard to be confident because ex. Date/Time can reference other objects, and even though those should be `Ractor.shareable?`, they may not have the FL_SHAREABLE flag set which could violate the "FL_SHAREABLE only references other FL_SHAREABLE" invariant (I don't know if we can hit that from values out of MySQL, but it seems iffy). ---------------------------------------- Feature #22128: C API: Expose RB_OBJ_SET_FROZEN_SHAREABLE https://bugs.ruby-lang.org/issues/22128#change-117752 * Author: byroot (Jean Boussier) * Status: Open ---------------------------------------- ### Context I'm trying to experiment with adapting Active Record for a Ractor architecture. Since database connections can't possibly be Ractor shareable, the idea is to warp each connection inside its own ractor, and then send SQL queries and responses through a port. But for this to perform well, I'd like to directly build the query response as a fully shareable object, so that it can be pushed into the port for free, instead of having Ruby need to recursively walk the potentially large response to mark objects as shareable. Here's an example of how it would work in trilogy: https://github.com/trilogy-libraries/trilogy/pull/299 ### Problem Unfortunately, the necessary API isn't currently exposed in the C API: - `RB_OBJ_SET_FROZEN_SHAREABLE` - `RB_OBJ_SET_SHAREABLE` / `rb_obj_set_shareable` I understand that this API could potentially be misused, but given it's a C API, I believe it's acceptable to require care from the caller. ### Benchmark ```ruby # frozen_string_literal: true require 'trilogy' require 'benchmark/ips' baseline = Trilogy.new(database: "test") shareable = Trilogy.new(database: "test", shareable: true) values = { null_test: "test", bit_test: "test", single_bit_test: 1, tiny_int_test: 2, bool_cast_test: true, small_int_test: 4, medium_int_test: 23434, int_test: 324234, big_int_test: 234234, unsigned_big_int_test: 23423423, float_test: 234234, float_zero_test: 213.23, double_test: 23123.12323, decimal_test: 123213.12312, decimal_zero_test: 213213.21323, date_test: "2026-01-30", date_time_test: "2026-01-30 14:03:56", date_time_with_precision_test: "2026-01-30 14:03:56.12", time_with_precision_test: "2026-01-30 14:03:56.12", timestamp_test: "2026-01-30 14:03:56.12", varchar_test: "VARCHAR" } baseline.query("DELETE FROM trilogy_test") insert = "INSERT INTO trilogy_test(#{values.keys.join(", ")}) VALUES (#{values.values.map(&:inspect).join(", ")})" 1000.times do baseline.query(insert) end p shareable.query("SELECT * FROM test.trilogy_test").to_a.size Benchmark.ips do |x| x.report("baseline") { Ractor.make_shareable(baseline.query("SELECT * FROM trilogy_test")) } x.report("shareable") { Ractor.make_shareable(shareable.query("SELECT * FROM trilogy_test")) } x.compare!(order: :baseline) end ``` ``` ruby 4.1.0dev (2026-06-24T13:17:28Z expose-ractor-set-.. f9d7dd50cd) +PRISM [arm64-darwin25] Warming up -------------------------------------- baseline 42.000 i/100ms shareable 66.000 i/100ms Calculating ------------------------------------- baseline 220.024 (±43.2%) i/s (4.54 ms/i) - 1.134k in 5.153976s shareable 677.487 (± 2.4%) i/s (1.48 ms/i) - 3.432k in 5.065782s Comparison: baseline: 220.0 i/s shareable: 677.5 i/s - 3.08x faster ``` -- https://bugs.ruby-lang.org/
Issue #22128 has been updated by byroot (Jean Boussier).
since RB_OBJ_SET_FROZEN_SHAREABLE is shallow and doesn't verify that referenced objects are shareable, this seems very hard to use safely.
I understand your reservation, I kinda have the same, but I think this being a C-level API is OK to be a sharp knife. There is a million other way you can crash the VM from a C extension, not even involving Ruby APIs. In addition, it does check that the invariant is respected when running against `-DRUBY_DEBUG=1`, which I know not a lot of gems do, but isn't that hard to put in place with GHA `setup-ruby`. Also on the very hard ot use safely part, I think this is a good fit for database clients, parsers etc. Could very well see `JSON.parse(..., shareable: true)`. In these situation you are mostly dealing with Ruby primitives (String, Array, Hash) that you've just recursively built yourself, so it's easy to know there's no unaccounted references. But you're right that with things like `Time`, `Date` etc, they're fine today, as they have no reference, but it could no longer be true at some point. ---------------------------------------- Feature #22128: C API: Expose RB_OBJ_SET_FROZEN_SHAREABLE https://bugs.ruby-lang.org/issues/22128#change-117754 * Author: byroot (Jean Boussier) * Status: Open ---------------------------------------- ### Context I'm trying to experiment with adapting Active Record for a Ractor architecture. Since database connections can't possibly be Ractor shareable, the idea is to warp each connection inside its own ractor, and then send SQL queries and responses through a port. But for this to perform well, I'd like to directly build the query response as a fully shareable object, so that it can be pushed into the port for free, instead of having Ruby need to recursively walk the potentially large response to mark objects as shareable. Here's an example of how it would work in trilogy: https://github.com/trilogy-libraries/trilogy/pull/299 ### Problem Unfortunately, the necessary API isn't currently exposed in the C API: - `RB_OBJ_SET_FROZEN_SHAREABLE` - `RB_OBJ_SET_SHAREABLE` / `rb_obj_set_shareable` I understand that this API could potentially be misused, but given it's a C API, I believe it's acceptable to require care from the caller. ### Benchmark ```ruby # frozen_string_literal: true require 'trilogy' require 'benchmark/ips' baseline = Trilogy.new(database: "test") shareable = Trilogy.new(database: "test", shareable: true) values = { null_test: "test", bit_test: "test", single_bit_test: 1, tiny_int_test: 2, bool_cast_test: true, small_int_test: 4, medium_int_test: 23434, int_test: 324234, big_int_test: 234234, unsigned_big_int_test: 23423423, float_test: 234234, float_zero_test: 213.23, double_test: 23123.12323, decimal_test: 123213.12312, decimal_zero_test: 213213.21323, date_test: "2026-01-30", date_time_test: "2026-01-30 14:03:56", date_time_with_precision_test: "2026-01-30 14:03:56.12", time_with_precision_test: "2026-01-30 14:03:56.12", timestamp_test: "2026-01-30 14:03:56.12", varchar_test: "VARCHAR" } baseline.query("DELETE FROM trilogy_test") insert = "INSERT INTO trilogy_test(#{values.keys.join(", ")}) VALUES (#{values.values.map(&:inspect).join(", ")})" 1000.times do baseline.query(insert) end p shareable.query("SELECT * FROM test.trilogy_test").to_a.size Benchmark.ips do |x| x.report("baseline") { Ractor.make_shareable(baseline.query("SELECT * FROM trilogy_test")) } x.report("shareable") { Ractor.make_shareable(shareable.query("SELECT * FROM trilogy_test")) } x.compare!(order: :baseline) end ``` ``` ruby 4.1.0dev (2026-06-24T13:17:28Z expose-ractor-set-.. f9d7dd50cd) +PRISM [arm64-darwin25] Warming up -------------------------------------- baseline 42.000 i/100ms shareable 66.000 i/100ms Calculating ------------------------------------- baseline 220.024 (±43.2%) i/s (4.54 ms/i) - 1.134k in 5.153976s shareable 677.487 (± 2.4%) i/s (1.48 ms/i) - 3.432k in 5.065782s Comparison: baseline: 220.0 i/s shareable: 677.5 i/s - 3.08x faster ``` -- https://bugs.ruby-lang.org/
Issue #22128 has been updated by ianks (Ian Ker-Seymer). I came to a similar conclusion when trying the send `Time` across Ractor ports: ``` c /* before: walk each moved object's refs under the VM lock, on every move */ for (size_t i = 0; i < obj->ref_count; i++) if (!rb_ractor_shareable_p(obj->refs[i])) return false; /* after: the type vouches for its refs once, at definition time */ if (RTYPEDDATA_TYPE(obj)->flags & RUBY_TYPED_ONLY_SHAREABLE_REFS) return true; ``` Which works in the happy path, but as John mentioned, falls apart with Time.zone and arbitrary ivar_set. ---------------------------------------- Feature #22128: C API: Expose RB_OBJ_SET_FROZEN_SHAREABLE https://bugs.ruby-lang.org/issues/22128#change-117758 * Author: byroot (Jean Boussier) * Status: Open ---------------------------------------- ### Context I'm trying to experiment with adapting Active Record for a Ractor architecture. Since database connections can't possibly be Ractor shareable, the idea is to warp each connection inside its own ractor, and then send SQL queries and responses through a port. But for this to perform well, I'd like to directly build the query response as a fully shareable object, so that it can be pushed into the port for free, instead of having Ruby need to recursively walk the potentially large response to mark objects as shareable. Here's an example of how it would work in trilogy: https://github.com/trilogy-libraries/trilogy/pull/299 ### Problem Unfortunately, the necessary API isn't currently exposed in the C API: - `RB_OBJ_SET_FROZEN_SHAREABLE` - `RB_OBJ_SET_SHAREABLE` / `rb_obj_set_shareable` I understand that this API could potentially be misused, but given it's a C API, I believe it's acceptable to require care from the caller. ### Benchmark ```ruby # frozen_string_literal: true require 'trilogy' require 'benchmark/ips' baseline = Trilogy.new(database: "test") shareable = Trilogy.new(database: "test", shareable: true) values = { null_test: "test", bit_test: "test", single_bit_test: 1, tiny_int_test: 2, bool_cast_test: true, small_int_test: 4, medium_int_test: 23434, int_test: 324234, big_int_test: 234234, unsigned_big_int_test: 23423423, float_test: 234234, float_zero_test: 213.23, double_test: 23123.12323, decimal_test: 123213.12312, decimal_zero_test: 213213.21323, date_test: "2026-01-30", date_time_test: "2026-01-30 14:03:56", date_time_with_precision_test: "2026-01-30 14:03:56.12", time_with_precision_test: "2026-01-30 14:03:56.12", timestamp_test: "2026-01-30 14:03:56.12", varchar_test: "VARCHAR" } baseline.query("DELETE FROM trilogy_test") insert = "INSERT INTO trilogy_test(#{values.keys.join(", ")}) VALUES (#{values.values.map(&:inspect).join(", ")})" 1000.times do baseline.query(insert) end p shareable.query("SELECT * FROM test.trilogy_test").to_a.size Benchmark.ips do |x| x.report("baseline") { Ractor.make_shareable(baseline.query("SELECT * FROM trilogy_test")) } x.report("shareable") { Ractor.make_shareable(shareable.query("SELECT * FROM trilogy_test")) } x.compare!(order: :baseline) end ``` ``` ruby 4.1.0dev (2026-06-24T13:17:28Z expose-ractor-set-.. f9d7dd50cd) +PRISM [arm64-darwin25] Warming up -------------------------------------- baseline 42.000 i/100ms shareable 66.000 i/100ms Calculating ------------------------------------- baseline 220.024 (±43.2%) i/s (4.54 ms/i) - 1.134k in 5.153976s shareable 677.487 (± 2.4%) i/s (1.48 ms/i) - 3.432k in 5.065782s Comparison: baseline: 220.0 i/s shareable: 677.5 i/s - 3.08x faster ``` -- https://bugs.ruby-lang.org/
participants (3)
-
byroot (Jean Boussier) -
ianks (Ian Ker-Seymer) -
jhawthorn (John Hawthorn)