Drop unused **opts from Process.spawn wrapper signature#5774
Conversation
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: 6677dcf | Docs | Datadog PR Page | Give us feedback! |
BenchmarksBenchmark execution time: 2026-05-19 18:16:21 Comparing candidate commit 6677dcf in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 46 metrics, 0 unstable metrics.
|
Two issues caught by CI: - standard/lint: hash-literal spacing and `Lint/ConstantDefinitionInBlock`. Auto-fixed spacing via `rake standard:fix`; converted the three describe- block constants (LINEAGE_VAR / LINEAGE_VAL / PROBE_CMD) into `let` blocks. - Ruby 2.5/2.6/2.7: the `options_with_integer_fd_keys` test failed because the wrapper's `def spawn(*args, **opts)` partially extracts a positional Hash with mixed Symbol + Integer keys into `**opts` on Ruby 2.x. That is a distinct bug from the constant shadow this PR fixes — it is the bug that PR #5774 (drop `**opts`) addresses. The test (and its proof) belong with that PR. Removed from this spec with a comment pointing to #5774.
Typing analysisNote: Ignored files are excluded from the next sections.
|
I call AI hallucination! Although OTOH it's frequent to have docs bugs. I think at least in theory the safest would be to use TLDR: I think the fix is good, but it's not only cosmetic, it changes if the callee receives kwargs or not, it happens that |
Agreed. There are even varying behaviours depending on the call arguments, which can have subtle changes across Ruby versions that don't usually matter for 99% of Ruby code out there but suddenly do when one does this kind of generic delegation. Using Best bet is to split the |
Address review feedback (#5774): keep `def spawn(*args, **opts)` on Ruby 3+ so caller kwargs continue flowing to `Process.spawn` as kwargs through `super`; use `def spawn(*args)` on Ruby 2.5/2.6/2.7 to avoid the mixed-keys options Hash split that raises `TypeError` there. Matches the existing per-version split pattern in `lib/datadog/core/utils/forking.rb`. - lib/datadog/core/utils/spawn_monkey_patch.rb: per-version conditional - sig/datadog/core/utils/spawn_monkey_patch.rbs: stays at `(*untyped args)` (matches forking.rbs; narrow sig, ignored on Ruby 3+ branch) All 5 specs in spec/datadog/core/utils/spawn_monkey_patch_spec.rb pass. Steep clean. Co-Authored-By: Claude <[email protected]>
|
You're right — calling it cosmetic was wrong. With Restructured in 6f93283 as a per-Ruby-version split (matching the pattern already in |
|
Thanks for the graft-rb pointer — applied the per-version split in 6f93283. Ruby 3+ keeps |
| def spawn(*args) | ||
| args.replace(SpawnMonkeyPatch.inject_lineage_envs(args)) | ||
| super | ||
| end |
There was a problem hiding this comment.
| def spawn(*args) | |
| args.replace(SpawnMonkeyPatch.inject_lineage_envs(args)) | |
| super | |
| end | |
| def spawn(*args) | |
| args.replace(SpawnMonkeyPatch.inject_lineage_envs(args)) | |
| super | |
| end | |
| ruby2_keywords :spawn if respond_to?(:ruby2_keywords, true) |
On 2.7 (and only 2.7) this could be problematic:
https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/#other-minor-changes-empty-hash
Though in this case an empty Hash disappearing should be no problem since the semantics of empty Hash is the same as passing no Hash for spawn.
But still, I think it's good to be consistent and use the correct pattern everywhere,
so I would suggest adding that change.
https://eregon.me/blog/2021/02/13/correct-delegation-in-ruby-2-27-3.html documents that pattern, here we also use *args, **kwargs for 3+ which is fine and avoids using ruby2_keywords longer than necessary.
There was a problem hiding this comment.
Another point is just (*args) without ruby2_keywords warns on 2.7 and we wouldn't want to emit extra deprecation warnings for users (only visible if they enable deprecation warnings but that's common because test harnesses often enable them).
There was a problem hiding this comment.
We should be able to close this PR in favor of #5634. I made this change here: https://github.com/DataDog/dd-trace-rb/pull/5634/changes#r3268075567.
There was a problem hiding this comment.
#5634 does def spawn(*args) which is mostly fine and fixes the Hash splitting bug, except it will convert any kwargs to positional args.
Process.spawn currently doesn't expect kwargs so it works fine, but it could change: #5774 (comment)
There is still a potential issue if another monkey-patch of Process.spawn would expect kwargs and our monkey-patch is earlier in the call chain: then we lose kwargs and pass them as positional args and potentially break the other monkey patch.
I'll make a PR for that.
…ns Hash bug Master's wrapper signature `def spawn(*args, **opts)` is unchanged from before. On Ruby 3+ this works fine. On Ruby 2.5/2.6/2.7 `**opts` auto-extracts the Symbol-keyed entries from a mixed-keys positional options Hash (e.g. childprocess's `options[writer.fileno] = :close` on duplex pipes), splitting the Hash and raising `TypeError: no implicit conversion of Hash into String` inside `Process.spawn`. Use a per-Ruby-version signature (matching `lib/datadog/core/utils/forking.rb`): - Ruby 3+: `def spawn(*args, **opts)` — preserves caller kwargs as kwargs through bare `super` - Ruby 2.5/2.6/2.7: `def spawn(*args)` — keeps the options Hash positional and intact regardless of key shape Adds a regression test (`spawn(cmd, options_hash_with_mixed_symbol_and_integer_keys)`) under the existing `Process.spawn argument forms (issue #5621 regression)` describe block, using the shared `run_spawn` helper added in #5773. The test triggers `TypeError` on master against Ruby 2.5/2.6/2.7 and passes on every Ruby with the per-version split. No env-Hash is passed, so the test is independent of the constant-shadow fix in #5773. Sig stays at `(*untyped args)` to match the 2.x form (RBS doesn't support runtime conditionals); the Ruby 3+ branch uses an inline `# steep:ignore DifferentMethodParameterKind` directive. All 13 spec/datadog/core/utils/spawn_monkey_patch_spec.rb examples pass. Steep clean. Co-Authored-By: Claude <[email protected]>
6f93283 to
6677dcf
Compare
…e-on-exec shape 5634 fixes the Ruby 2.5/2.6/2.7 mixed-keys bug by dropping `**opts` from the wrapper signature, but the test suite has no regression coverage for this specific call shape — only a NOTE block deferring to PR DataDog#5774. Add an `it` block under the existing `Process.spawn argument forms (issue DataDog#5621 regression)` describe that exercises the exact childprocess close-on-exec form: an options Hash with `IO.pipe` filenos (Integer keys) mixed with Symbol keys (`:pgroup => true`, `fileno => :close`). On Ruby 2.5/2.6/2.7 this triggers `TypeError: no implicit conversion of Hash into String` against the pre-fix wrapper; with `**opts` dropped, the options Hash stays positional and intact across all supported Rubies. Uses the shared `run_spawn` helper and `env_provider:` API already established in the surrounding describe block. Replaces the now-stale NOTE that pointed at PR DataDog#5774 — 5634 fixes the same bug, so no deferral is needed. 20 examples pass locally. Co-Authored-By: Claude <[email protected]>
What does this PR do?
Restructures
Datadog::Core::Utils::SpawnMonkeyPatch::ProcessSpawnPatch#spawnto use a per-Ruby-version signature (matching the existing pattern inlib/datadog/core/utils/forking.rb):def spawn(*args, **opts)— unchanged from master. With baresuper, kwargs at the call site flow toProcess.spawnas kwargs.def spawn(*args)— drops**opts. This is the bug fix.Motivation:
Master's wrapper signature was
def spawn(*args, **opts)for every Ruby. The method body never readsopts. On Ruby 3+ the signature works fine — kwargs flow tosuperas kwargs andProcess.spawnaccepts either form. On Ruby 2.5/2.6/2.7 the**optsactively breaks one call shape:childprocess'soptions[writer.fileno] = :closeon duplex pipes.**optswhile leaving the Integer-keyed entries behind in the trailing positional Hash.superthen forwards the broken split.Process.spawninterprets the partially-stripped Hash as a command argument and raisesTypeError: no implicit conversion of Hash into String.Ruby 3+ removed positional-Hash auto-extraction into kwargs, so this bug is invisible there.
Dropping
**optson Ruby 2.x keeps the entire options Hash positional regardless of key shape, andProcess.spawnreceives it intact. Keeping**optson Ruby 3+ preserves the original kwargs-forwarding behaviour (see #5774 (review)).This is independent from the constant-shadow bug fixed in #5773: that one fires whenever the caller passes an env-Hash (any Ruby), this one fires whenever the caller passes a mixed-keys options Hash without an env-Hash (Ruby 2.x only). With both PRs landed, both shapes work on every Ruby.
Change log entry
Yes. Fix
TypeError: no implicit conversion of Hash into Stringraised byProcess.spawnon Ruby 2.5/2.6/2.7 when the caller passes an options Hash with mixed Symbol + Integer keys (e.g.childprocesson duplex pipes).Additional Notes:
One of two independent splits from #5634:
Hash→::Hash) for #5621, all RubiesMarco's #5634 bundles these along with a
lineage_envs_provider→env_providerrename, an idempotency guard inapply!, doc edits, and aexecute_in_forkdoc tweak. I deliberately did not split those out — the rename is cosmetic and breaks the API, the idempotency guard has no observable effect (Module#prependis already idempotent), and the doc edits are unrelated.How to test the change?
Three regression specs in
spec/datadog/core/utils/spawn_monkey_patch_spec.rbunderdescribe 'option-syntax compatibility':kwargs-syntax options work—Process.spawn(cmd, kw: value)positional Hash options work—opts = {...}; Process.spawn(cmd, opts)positional options Hash with mixed Symbol + Integer keys (childprocess close-on-exec form)— the falsifiable test for the bug above. TriggersTypeErroron master against Ruby 2.5/2.6/2.7; passes on every Ruby with the per-version split.The third test does not pass an env-Hash argument, so it is independent of the constant-shadow fix in #5773 — confirmed by reproducing on master's lib with
::Hashstill bare.🤖 Generated with Claude Code