Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Priming the cache #168

Merged
merged 1 commit into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ def product(id:)
end
```

### Priming the Cache

You can prime the loader cache with a specific value, which can be useful in certain situations.

```ruby
def liked_products
liked_products = Product.where(liked: true).load
liked_products.each do |product|
RecordLoader.for(Product).prime(product.id, product)
end
end
```

Priming will add key/value to the loader cache only if it didn't exist before.


## Unit Testing

Your loaders can be tested outside of a GraphQL query by doing the
Expand Down
4 changes: 4 additions & 0 deletions lib/graphql/batch/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ def load_many(keys)
::Promise.all(keys.map { |key| load(key) })
end

def prime(key, value)
cache[cache_key(key)] ||= ::Promise.resolve(value).tap { |p| p.source = self }
end

def resolve #:nodoc:
return if resolved?
load_keys = queue
Expand Down
36 changes: 36 additions & 0 deletions test/loader_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,40 @@ def test_loader_with_failing_perform
promise = ExplodingLoader.load([1]).then(nil, ->(err) { error_message = err.message } ).sync
assert_equal 'perform failed', error_message
end

def test_prime
loader = EchoLoader.for
loader.prime(:a, :prepared_value)

assert_equal :prepared_value, loader.load(:a).sync
assert_equal [:prepared_value, :b, :c], loader.load_many([:a, :b, :c]).sync
end

def test_will_not_call_perform_if_fully_primed
loader = ExplodingLoader.for
loader.prime(:a, 1)
loader.prime(:b, 2)

assert_equal [1, 2], loader.load_many([:a, :b]).sync
end

def test_priming_a_key_already_in_queue_does_nothing
loader = EchoLoader.for

promise = loader.load(:a)

loader.prime(:a, :not_a)

assert_equal :a, promise.sync
end

def test_prime_will_not_replace_already_cached_value
loader = EchoLoader.for

assert_equal :a, loader.load(:a).sync

loader.prime(:a, :not_a)

assert_equal :a, loader.load(:a).sync
end
Comment on lines +245 to +253

Choose a reason for hiding this comment

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

comment: This seems like it potentially is a footgun for surprising behavior. Particularly with ActiveRecord's lazy loading of associations, what if you try to prime with a model that has preloaded an association that you need (N.B. if you're using GraphQL::Batch, you probably shouldn't be loading associations like this, but stranger things have happened), but it has already been fulfilled with a version that does not have that association preloaded?

question: Should we emit a warning or something by default when attempting to prime an already-fulfilled promise? One could then opt in to allow either overriding or ignoring the warning.

This could very well be overkill but it's the first thing that came to mind when I saw the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this is indeed not meant to be used to populate the loader with values that are in a certain state, for some kind of perf reasons. The values you put in should behave the same as the values the loader would have loaded on its own.

I'm not sure there's a good and simple way to protect against this, except in documenting its intended use.

Given this, I don't think we should emit a warning when priming a value for a key that already exists in the loader cache. That is meant to be a no-op - loading or priming should result in equivalent results - so it shouldn't seem to the developer that they are doing something wrong. Given the resolution of GraphQL it can be very hard to reason or know/ensure that you'd be priming keys that won't exist.

The behaviour here (priming inserts a key in the cache only if the key didn't exist) is also the behaviour of the other data loader libraries I found.

What do you think?

end
Loading