Skip to content

Fix loader for kwargs mutation ruby4#207

Open
AlfonsoUceda wants to merge 3 commits intoShopify:mainfrom
AlfonsoUceda:fix-loader-for-kwargs-mutation-ruby4
Open

Fix loader for kwargs mutation ruby4#207
AlfonsoUceda wants to merge 3 commits intoShopify:mainfrom
AlfonsoUceda:fix-loader-for-kwargs-mutation-ruby4

Conversation

@AlfonsoUceda
Copy link

Hello!

I'm creating this PR because I've found a problem using this gem with Ruby 4 and a specific way of using loaders. The problem seems with the use of ... operator forwarding in ruby 4 and the loader class.

On Ruby 4.0, when kwargs are destructured by a caller method and re-passed to Loader.for(...), the ... forwarding shares the internal kwargs hash across all expansion sites. When new(...) destructures them in initialize, it empties the same hash already stored in the executor's loader key, breaking loader reuse.

This only manifests when .for() is called through an intermediary that captures kwargs as named parameters and re-passes the, a common pattern in subclasses:

  def self.load(model_class, id, key: :id, preload: true, **conditions)
    self.for(model_class, key: key, preload: preload, **conditions).load(id)
  end

Direct .for(key: 'value') calls are unaffected, which is why the bug is subtle.

  1. loader_key_for(...) captures kwargs into the key: [self, {key: :id, ...}, [model]]
  2. new(...) in the block shares the same kwargs hash - initialize destructures it, emptying the hash
  3. The stored key becomes [self, {}, [model]]
  4. Next lookup with {key: :id, ...} doesn't match {} - a new loader is created.

I've made thanks to Claude that helped me to identify this problem in our codebase and pointed out to this.

Ruby 4.0 shares the internal kwargs hash when `...` is forwarded
to multiple call sites in the same method. In `Loader.for(...)`,
the kwargs captured by `loader_key_for(...)` are emptied when
`new(...)` destructures them in `initialize`, corrupting the
stored loader key and breaking batch grouping.

Explicit `*group_args, **group_kwargs, &block` ensures each call
site receives an independent copy of the kwargs hash.
@AlfonsoUceda
Copy link
Author

I have signed the CLA!

Reproduces a bug where kwargs forwarded through ... get mutated
when destructured and re-assembled by an intermediary method
(e.g. RecordLoader.load → .for(model, key: key, ...)).
Direct .for() calls don't trigger this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant