Skip to content

THRIFT-6439: Isolate nested Ruby struct defaults - #4030

Open
kpumuk wants to merge 1 commit into
apache:masterfrom
kpumuk:rb-isolated-defaults
Open

kpumuk wants to merge 1 commit into
apache:masterfrom
kpumuk:rb-isolated-defaults

Conversation

@kpumuk

@kpumuk kpumuk commented Oct 20, 2026

Copy link
Copy Markdown
Member

Ruby currently duplicates only the outer object when assigning a generated field default. Two independently created structs can therefore share nested lists, maps, sets, or mutable fields of a default struct or union. Changing one instance can also change the template used by future instances. Deserialization repeats the same shallow copying when restoring omitted defaults.

Copy the supported mutable Thrift values recursively, both during initialization and when restoring defaults in Ruby or the native extension. Nested structs and unions are duplicated without rerunning their constructors; immutable scalars are retained. Explicitly supplied arguments keep their existing ownership semantics.

Benchmarks

Ruby 4.0.7 on aarch64 Linux, five warmed process trials per revision, pinned to CPU 0, without YJIT. The baseline is master 7e288b753; the relevant Ruby runtime sources are unchanged at ae74fc8e8.

Protocol measurements used the repository harness with 125,000 small reads:

taskset -c 0 ruby test/rb/benchmarks/protocol_benchmark.rb \
  --small-runs 125000 \
  --scenarios rb-bin-read-small,c-bin-read-small,rb-cmp-read-small --json

Pure-Ruby runs disabled the extension, set THRIFT_BENCHMARK_SKIP_NATIVE=1, and omitted the accelerated Binary scenario. Native runs verified that Struct's read/write methods were C implementations.

Times are milliseconds: median [minimum–maximum].

Read workload Master Proposed Median change
Binary, native Struct 933.58 [904.25–963.36] 915.29 [908.89–919.63] −1.96%
Accelerated Binary 648.07 [636.74–700.97] 642.78 [634.07–645.17] −0.82%
Compact, native 710.25 [688.38–743.50] 695.51 [691.02–700.02] −2.08%
Binary, pure Ruby 2303.30 [2281.01–2309.62] 2290.07 [2279.17–2305.86] −0.57%
Compact, pure Ruby 2208.57 [2178.23–2247.65] 2215.52 [2190.66–2246.21] +0.31%

The ranges overlap: these results do not establish a meaningful speedup. The harness's OneOfEach fixture has no defaults, so these workloads are a control, not a measurement of deep-copy costs.

Separate construction probes measured those costs:

Construction workload Master Proposed Median change
25,000 generated Foo instances 45.25 [44.79–46.36] 87.31 [86.95–110.08] +92.95%
2,000 instances with eight nested default structs 1.35 [1.21–1.40] 13.72 [13.44–22.27] 10.19×

Allocations increased from 300,007 to 375,007 for Foo, and from 10,001 to 42,001 for the synthetic nested-default chain. Independent mutable defaults cost more than sharing their contents: approximately 1.68 additional microseconds per Foo instance in this environment. The nested-chain result is a synthetic depth probe, not an application throughput estimate.

  • Did you create an Apache Jira ticket? THRIFT-6439
  • If a ticket exists: Does your pull request title follow the pattern "THRIFT-NNNN: describe my issue"?
  • Did you squash your changes to a single commit? (not required, but preferred)
  • Did you do your best to avoid breaking changes? If one was needed, did you label the Jira ticket with "Breaking-Change"?
  • If your change does not involve any code, include a CI-skip marker in the commit message to free up build resources. (Not applicable: this change includes code.)

Copilot AI balanced review requested due to automatic review settings October 20, 2026 22:19
@mergeable mergeable Bot added the ruby Pull requests that update Ruby code label Oct 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

3 open issues
What changed in this PR

This PR fixes Ruby generated struct defaults so nested mutable values (containers, nested structs/unions, strings) are deep-copied per instance, preventing cross-instance sharing and mutation of the default template (including during deserialization when defaults are restored).

Changes:

  • Add new spec fixtures and RSpec coverage for nested/default isolation, union defaults, and constructor behavior.
  • Implement recursive default copying in Ruby (Struct.copy_default_value) and use it during initialization and default restoration after reads.
  • Update the native extension to delegate non-scalar default copying to the Ruby helper for consistent behavior.
File Description
lib/​rb/​spec/​struct_spec.rb Adds specs validating deep-copy behavior for nested defaults and read-time default restoration.
lib/​rb/​spec/​ThriftSpec.thrift Introduces new Thrift structs/unions used as fixtures for nested default behaviors.
lib/​rb/​lib/​thrift/​struct.rb Replaces shallow dup with recursive copying via Struct.copy_default_value.
lib/​rb/​ext/​struct.c Changes C default restoration to avoid shallow copying and defer deep-copying to Ruby helper.

Review effort: Lite

Comment on lines +242 to +244
else
value
end
Comment on lines +120 to +123
defaults = SpecNamespace::NestedDefaults::FIELDS
defaults[7][:default].first.freeze
defaults[7][:default].freeze
defaults[5][:default].freeze
Comment on lines +224 to +227
when Hash
value.each_with_object({}) do |(key, element), copy|
copy[copy_default_value(key)] = copy_default_value(element)
end
Client: rb

Co-Authored-By: OpenAI Codex (gpt-6.1-sol) <codex@openai.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 02:18
@kpumuk
kpumuk force-pushed the rb-isolated-defaults branch from 55f6dea to e0f1685 Compare October 21, 2026 02:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +222 to +229
when Array
value.map { |element| copy_default_value(element) }
when Hash
value.each_with_object({}) do |(key, element), copy|
copy[copy_default_value(key)] = copy_default_value(element)
end
when Set
value.each_with_object(Set.new) { |element, copy| copy.add(copy_default_value(element)) }
Comment on lines +120 to +123
defaults = SpecNamespace::NestedDefaults::FIELDS
defaults[7][:default].first.freeze
defaults[7][:default].freeze
defaults[5][:default].freeze
Comment thread lib/rb/ext/struct.c
Comment on lines +504 to +511
VALUE value;
if (RB_TYPE_P(default_value, T_STRING)) {
value = rb_obj_dup(default_value);
} else if (RB_SPECIAL_CONST_P(default_value) || RB_TYPE_P(default_value, T_FLOAT) || RB_TYPE_P(default_value, T_BIGNUM)) {
value = default_value;
} else {
value = rb_funcall(thrift_struct_module, copy_default_value_method_id, 1, default_value);
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ruby Pull requests that update Ruby code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants