Skip to content

Commit 29e2894

Browse files
Fix specs, update changelog, cleanup rbnext (#143)
1 parent 348469b commit 29e2894

7 files changed

Lines changed: 73 additions & 11 deletions

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
/tmp/
99
Gemfile.lock
1010
.DS_Store
11-
.rbnext/
1211
*.sqlite3
1312
*.sqlite3-*
1413
spec/internal/log/*.log

.rubocop.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ AllCops:
1818
- 'Rakefile'
1919
- 'Gemfile'
2020
- '*.gemspec'
21-
- 'lib/.rbnext/**/*'
2221

2322
Lint/DuplicateMethods:
2423
Exclude:

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## master
44

5+
- [PR#99](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/99) Fix for race condition when reading from cache
6+
([@gsdean][])
7+
58
## 1.22.1 (2025-06-07)
69

710
- [PR#137](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/137) Remove new_connections? check for gql > 2.3.10 in Tracer ([@DmitryTsepelev][])
@@ -226,3 +229,4 @@
226229
[@Drowze]: https://github.com/Drowze
227230
[@danielhartnell]: https://github.com/danielhartnell
228231
[@mgruner]: https://github.com/mgruner
232+
[@gsdean]: https://github.com/gsdean

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
source "https://rubygems.org"
22

33
gem "pry-byebug", platform: :mri
4+
# byebug requires "readline", which is a bundled gem on Ruby 3.5+/4.0 and must
5+
# be declared explicitly so it loads under `bundle exec` (e.g. via Combustion).
6+
gem "readline", platform: :mri
47

58
eval_gemfile "gemfiles/rubocop.gemfile"
69

graphql-ruby-fragment_cache.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
1717
spec.metadata["source_code_uri"] = spec.homepage
1818
spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
1919

20-
spec.files = Dir.glob("lib/**/*") + Dir.glob("lib/.rbnext/**/*") + Dir.glob("bin/**/*") + %w[README.md LICENSE.txt CHANGELOG.md]
20+
spec.files = Dir.glob("lib/**/*") + Dir.glob("bin/**/*") + %w[README.md LICENSE.txt CHANGELOG.md]
2121
spec.require_paths = ["lib"]
2222

2323
spec.required_ruby_version = ">= 3.1"

spec/graphql/fragment_cache/fragment_spec.rb

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,61 @@ def fragment_doubles_factory(count:, ctx: {})
5252
end
5353
end
5454
end
55+
56+
describe "#read" do
57+
let(:cache_key) { "fragment-cache-key" }
58+
let(:cache_store) { double("cache_store") }
59+
60+
let(:fragment) do
61+
context = double("context")
62+
allow(context).to receive(:namespace).with(:interpreter).and_return(current_path: ["post"])
63+
allow(context).to receive(:[]).with(:renew_cache).and_return(nil)
64+
65+
described_class.new(context).tap do |fragment|
66+
allow(fragment).to receive(:cache_key).and_return(cache_key)
67+
end
68+
end
69+
70+
before do
71+
allow(GraphQL::FragmentCache).to receive(:cache_store).and_return(cache_store)
72+
end
73+
74+
# Regression test for the race condition fixed in #99: existence is checked
75+
# with #exist? *before* #read, so a key that is absent at lookup time is
76+
# reported as a cache miss without being read — instead of being read first
77+
# and mistaken for a cached nil if another request populates it in between.
78+
context "when the cache key does not exist" do
79+
before do
80+
allow(cache_store).to receive(:exist?).with(cache_key).and_return(false)
81+
allow(cache_store).to receive(:read).with(cache_key)
82+
end
83+
84+
it "returns a cache miss without reading the absent key" do
85+
expect(fragment.read).to be_nil
86+
expect(cache_store).not_to have_received(:read)
87+
end
88+
end
89+
90+
context "when a nil value is stored in the cache" do
91+
before do
92+
allow(cache_store).to receive(:exist?).with(cache_key).and_return(true)
93+
allow(cache_store).to receive(:read).with(cache_key).and_return(nil)
94+
end
95+
96+
it "returns the sentinel for a cached nil" do
97+
expect(fragment.read).to be(described_class::NIL_IN_CACHE)
98+
end
99+
end
100+
101+
context "when a value is stored in the cache" do
102+
before do
103+
allow(cache_store).to receive(:exist?).with(cache_key).and_return(true)
104+
allow(cache_store).to receive(:read).with(cache_key).and_return("cached value")
105+
end
106+
107+
it "returns the cached value" do
108+
expect(fragment.read).to eq("cached value")
109+
end
110+
end
111+
end
55112
end

spec/graphql/fragment_cache/object_helpers_spec.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -700,13 +700,13 @@ def post(id:, expires_in: nil)
700700
end
701701

702702
it "calls #read for each entry" do
703-
# warmup calls
704-
expect(GraphQL::FragmentCache.cache_store).to have_received(:read).exactly(3)
703+
# cache was cold during warmup, so #exist? short-circuits before #read
704+
expect(GraphQL::FragmentCache.cache_store).not_to have_received(:read)
705705

706706
execute_query
707707

708-
# read key once
709-
expect(GraphQL::FragmentCache.cache_store).to have_received(:read).exactly(6)
708+
# cache is warm now: one #read per entry
709+
expect(GraphQL::FragmentCache.cache_store).to have_received(:read).exactly(3)
710710
end
711711

712712
context "when keep_in_context is true" do
@@ -717,13 +717,13 @@ def post(id:, expires_in: nil)
717717
end
718718

719719
it "calls #read once" do
720-
# warmup calls
721-
expect(GraphQL::FragmentCache.cache_store).to have_received(:read).exactly(3)
720+
# cache was cold during warmup, so #exist? short-circuits before #read
721+
expect(GraphQL::FragmentCache.cache_store).not_to have_received(:read)
722722

723723
execute_query
724724

725-
# read key once
726-
expect(GraphQL::FragmentCache.cache_store).to have_received(:read).exactly(4)
725+
# cache is warm now: read once, then reused from context for the other entries
726+
expect(GraphQL::FragmentCache.cache_store).to have_received(:read).exactly(1)
727727
end
728728
end
729729
end

0 commit comments

Comments
 (0)