Skip to content

Commit 8891ea1

Browse files
committed
Use fetch_multi instead of fetch
This will improve the performance since we are going to hit the backend just once to read all keys, as long the cache adapter implements fetch multi support like dalli. For example: builder.cache! :x do |cache| cache.x true end builder.cache! :y do |cache| cache.y true end builder.cache! :z do |cache| cache.z true end This example was hitting the memcached 6 times on cache miss: 1. read x 2. write x 3. read y 4. write y 5. read z 6. write z And 3 times on cache hit: 1. read x 2. read y 3. read z After this change, 4 times on cache miss: 1. read multi x,y,z 2. write x 3. write y 4. write z And 1 time on cache hit: 1. read multi x,y,z Note that in the case of different options, one read multi will be made per each options, i.e.: builder.cache! :x do |cache| cache.x true end builder.cache! :y do |cache| cache.y true end builder.cache! :z, expires_in: 10.minutes do |cache| cache.z true end builder.cache! :w, expires_in: 10.minutes do |cache| cache.w true end In the case of cache miss: 1. read multi x,y 2. write x 3. write y 4. read multi z,w 5. write z 5. write w In the case of cache hit: 1. read multi x,y 2. read multi z,w That's because Rails.cache.fetch_multi signature is limited to use the same options for all given keys. And for last, nested cache calls are allowed and will follow recursively to accomplish the same behavior, i.e.: builder.cache! :x do |cache_x| cache_x.x true cache_x.cache! :y do |cache_y| cache_y.y true end cache_x.cache! :z do |cache_z| cache_z.z true end end builder.cache! :w do |cache_w| cache_w.w true end In the case of cache miss: 1. read multi x,w 2. read multi y,z 3. write y 4. write z 5. write x 6. write w In the case of cache hit: 1. read multi x,w The same rule of options will be applied, if you have different options, one hit per options. Pretty much the same of rails/jbuilder#421.
1 parent 8e4d0bf commit 8891ea1

File tree

6 files changed

+240
-35
lines changed

6 files changed

+240
-35
lines changed

lib/abstract_builder.rb

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'abstract_builder/null_cache'
2+
require 'abstract_builder/lazy_cache'
23

34
class AbstractBuilder
45
@@format_key = nil
@@ -21,6 +22,7 @@ def initialize
2122
@format_key = @@format_key
2223
@ignore_value = @@ignore_value
2324
@cache_store = @@cache_store
25+
@lazy_cache = LazyCache.new(@@cache_store)
2426
@stack = []
2527
end
2628

@@ -34,6 +36,7 @@ def ignore_value!(&block)
3436

3537
def cache_store!(cache_store)
3638
@cache_store = cache_store
39+
@lazy_cache = LazyCache.new(cache_store)
3740
end
3841

3942
def set!(key, value)
@@ -70,14 +73,14 @@ def array!(key, collection, &block)
7073
set! key, values
7174
end
7275

73-
def cache!(key, options = nil, &block)
74-
value = @cache_store.fetch([:abstract_builder, :v1, *key], options) do
76+
def cache!(cache_key, options = {}, &block)
77+
cache_key = _compute_cache_key(cache_key)
78+
79+
@lazy_cache.add(cache_key, options) do
7580
builder = _inherit
7681
block.call(builder)
7782
builder.data!
7883
end
79-
80-
merge! value
8184
end
8285

8386
def data!
@@ -88,6 +91,10 @@ def data!
8891
data[key] = value unless _ignore_value?(value)
8992
end
9093

94+
@lazy_cache.resolve.each do |value|
95+
data.merge!(value)
96+
end
97+
9198
data
9299
end
93100

@@ -133,6 +140,10 @@ def _inherit
133140
builder
134141
end
135142

143+
def _compute_cache_key(key)
144+
[:abstract_builder, :v1, *key].join("/".freeze)
145+
end
146+
136147
def _ignore_value?(value)
137148
@ignore_value && @ignore_value.call(value)
138149
end

lib/abstract_builder/lazy_cache.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
class AbstractBuilder
2+
class LazyCache
3+
def initialize(driver)
4+
@cache = Hash.new { |h, k| h[k] = {} }
5+
@driver = driver
6+
end
7+
8+
def add(key, options, &block)
9+
cache[options][key] = block
10+
end
11+
12+
def resolve
13+
resolved = []
14+
15+
# Fail-fast if there is no items to be computed.
16+
return resolved if cache.empty?
17+
18+
# We can't add new items during interation, so iterate through a clone
19+
# that will allow us to add new items.
20+
previous = cache.clone
21+
cache.clear
22+
23+
# Keys are grouped by options and because of that, fetch_multi will use
24+
# the same options for the same group of keys.
25+
previous.each do |options, group|
26+
result = driver.fetch_multi(*group.keys, options) do |group_key|
27+
[group[group_key].call, *resolve]
28+
end
29+
30+
# Since the fetch_multi returns { cache_key => value }, we need to
31+
# discard the cache key and merge only the values.
32+
resolved.concat result.values.flatten(1)
33+
end
34+
35+
resolved
36+
end
37+
38+
private
39+
40+
attr_reader :cache, :driver
41+
end
42+
end

lib/abstract_builder/null_cache.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,17 @@ class NullCache
33
def fetch(key, _options = nil, &block)
44
block.call
55
end
6+
7+
def fetch_multi(*keys, options, &block)
8+
result = {}
9+
10+
keys.each do |key|
11+
result[key] = fetch(key, options) do
12+
block.call(key)
13+
end
14+
end
15+
16+
result
17+
end
618
end
719
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
RSpec.describe AbstractBuilder::LazyCache do
2+
it "resolves all entries at once using fetch multi" do
3+
driver = NaiveCache.new
4+
5+
expect(driver).to receive(:fetch_multi).with("x", "y", "z", {}).and_call_original.exactly(3).times
6+
7+
3.times do
8+
lazy_cache = described_class.new(driver)
9+
lazy_cache.add("x", {}) { { x: true } }
10+
lazy_cache.add("y", {}) { { y: true } }
11+
lazy_cache.add("z", {}) { { z: true } }
12+
13+
expect(lazy_cache.resolve).to eq [
14+
{ x: true },
15+
{ y: true },
16+
{ z: true }
17+
]
18+
end
19+
end
20+
21+
it "resolves all entries at once per options using fetch multi" do
22+
driver = NaiveCache.new
23+
24+
expect(driver).to receive(:fetch_multi).with("x", { option: false }).and_call_original.exactly(3).times
25+
expect(driver).to receive(:fetch_multi).with("y", "z", { option: true }).and_call_original.exactly(3).times
26+
27+
3.times do
28+
lazy_cache = described_class.new(driver)
29+
lazy_cache.add("x", { option: false }) { { x: true } }
30+
lazy_cache.add("y", { option: true }) { { y: true } }
31+
lazy_cache.add("z", { option: true }) { { z: true } }
32+
33+
expect(lazy_cache.resolve).to eq [
34+
{ x: true },
35+
{ y: true },
36+
{ z: true }
37+
]
38+
end
39+
end
40+
41+
it 'resolves nested entries at once using fetch multi' do
42+
driver = NaiveCache.new
43+
44+
expect(driver).to receive(:fetch_multi).with("x", {}).and_call_original.exactly(3).times
45+
expect(driver).to receive(:fetch_multi).with("y", "z", { option: false }).and_call_original.exactly(1).times
46+
expect(driver).to receive(:fetch_multi).with("w", { option: true }).and_call_original.exactly(1).times
47+
48+
3.times do
49+
lazy_cache = described_class.new(driver)
50+
51+
lazy_cache.add("x", {}) do
52+
lazy_cache.add("y", { option: false }) do
53+
{ y: true }
54+
end
55+
56+
lazy_cache.add("z", { option: false }) do
57+
{ z: true }
58+
end
59+
60+
lazy_cache.add("w", { option: true }) do
61+
{ w: true }
62+
end
63+
64+
{ x: true }
65+
end
66+
67+
expect(lazy_cache.resolve).to eq [
68+
{ x: true },
69+
{ y: true },
70+
{ z: true },
71+
{ w: true }
72+
]
73+
end
74+
end
75+
end

spec/abstract_builder_spec.rb

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -52,32 +52,38 @@
5252
it "caches using the given cache store" do
5353
subject.cache_store! cache_store
5454

55-
expect(cache_store).to receive(:fetch).with([:abstract_builder, :v1, :cache_key], nil).and_call_original
55+
expect(cache_store).to receive(:fetch_multi).with("abstract_builder/v1/cache_key", {}).and_call_original
5656

5757
subject.cache! :cache_key do |builder|
5858
builder.cache "hit"
5959
end
60+
61+
subject.data!
6062
end
6163

6264
it "caches using the given options" do
6365
subject.cache_store! cache_store
6466

65-
expect(cache_store).to receive(:fetch).with([:abstract_builder, :v1, :cache_key], option: true).and_call_original
67+
expect(cache_store).to receive(:fetch_multi).with("abstract_builder/v1/cache_key", option: true).and_call_original
6668

6769
subject.cache! :cache_key, option: true do |builder|
6870
builder.cache "hit"
6971
end
72+
73+
subject.data!
7074
end
7175

7276
it "inherits the global ignore value by default" do
7377
begin
7478
AbstractBuilder.cache_store! cache_store
7579

76-
expect(cache_store).to receive(:fetch).with([:abstract_builder, :v1, :cache_key], nil).and_call_original
80+
expect(cache_store).to receive(:fetch_multi).with("abstract_builder/v1/cache_key", {}).and_call_original
7781

7882
subject.cache! :cache_key do |builder|
7983
builder.cache "hit"
8084
end
85+
86+
subject.data!
8187
ensure
8288
AbstractBuilder.cache_store! AbstractBuilder::NullCache.new
8389
end
@@ -220,27 +226,44 @@
220226

221227
context "using cache store" do
222228
it "inherits the parent cache store" do
223-
subject.cache_store! NaiveCache.new
229+
driver = NaiveCache.new
224230

225-
subject.cache! :outside_cache_key do |builder|
226-
builder.outside_cache "hit"
227-
end
231+
# cache miss
232+
builder = described_class.new
233+
builder.cache_store! driver
228234

229-
subject.cache! :outside_cache_key do |builder|
230-
builder.outside_cache "miss"
235+
builder.cache! :outside_cache_key do |builder|
236+
builder.outside_cache "hit"
231237
end
232238

233-
subject.block! :meta do |meta|
239+
builder.block! :meta do |meta|
234240
meta.cache! :inside_cache_key do |builder|
235241
builder.inside_cache "hit"
236242
end
243+
end
237244

245+
expect(builder.data!).to eq(
246+
outside_cache: "hit",
247+
meta: {
248+
inside_cache: "hit"
249+
}
250+
)
251+
252+
# cache hit
253+
builder = described_class.new
254+
builder.cache_store! driver
255+
256+
builder.cache! :outside_cache_key do |builder|
257+
builder.outside_cache "miss"
258+
end
259+
260+
builder.block! :meta do |meta|
238261
meta.cache! :inside_cache_key do |builder|
239262
builder.inside_cache "miss"
240263
end
241264
end
242265

243-
expect(subject.data!).to eq(
266+
expect(builder.data!).to eq(
244267
outside_cache: "hit",
245268
meta: {
246269
inside_cache: "hit"
@@ -249,33 +272,53 @@
249272
end
250273

251274
it "do not leaks the ignore value to the parent" do
252-
subject.cache_store! AbstractBuilder::NullCache.new
253-
254-
subject.cache! :outside_cache_key do |builder|
255-
builder.outside_cache "hit"
256-
end
275+
null_driver = AbstractBuilder::NullCache.new
276+
naive_driver = NaiveCache.new
257277

258-
subject.cache! :outside_cache_key do |builder|
259-
builder.outside_cache "miss"
260-
end
278+
# cache miss
279+
builder = described_class.new
280+
builder.cache_store! null_driver
261281

262-
subject.block! :meta do |meta|
263-
meta.cache_store! NaiveCache.new
282+
builder.block! :meta do |meta|
283+
meta.cache_store! naive_driver
264284

265285
meta.cache! :inside_cache_key do |builder|
266286
builder.inside_cache "hit"
267287
end
288+
end
289+
290+
builder.cache! :outside_cache_key do |builder|
291+
builder.outside_cache "hit"
292+
end
293+
294+
expect(builder.data!).to eq(
295+
meta: {
296+
inside_cache: "hit"
297+
},
298+
outside_cache: "hit"
299+
)
300+
301+
# cache hit
302+
builder = described_class.new
303+
builder.cache_store! null_driver
304+
305+
builder.block! :meta do |meta|
306+
meta.cache_store! naive_driver
268307

269308
meta.cache! :inside_cache_key do |builder|
270309
builder.inside_cache "miss"
271310
end
272311
end
273312

274-
expect(subject.data!).to eq(
275-
outside_cache: "miss",
313+
builder.cache! :outside_cache_key do |builder|
314+
builder.outside_cache "miss"
315+
end
316+
317+
expect(builder.data!).to eq(
276318
meta: {
277319
inside_cache: "hit"
278-
}
320+
},
321+
outside_cache: "miss"
279322
)
280323
end
281324
end
@@ -303,17 +346,27 @@
303346

304347
describe "#cache!" do
305348
it "caches the given block" do
306-
subject.cache_store! NaiveCache.new
349+
driver = NaiveCache.new
307350

308-
subject.cache! :cache_key do |cache|
309-
cache.cache "miss"
310-
end
351+
# cache miss
352+
builder = described_class.new
353+
builder.cache_store! driver
311354

312-
subject.cache! :cache_key do |cache|
355+
builder.cache! :cache_key do |cache|
313356
cache.cache "hit"
314357
end
315358

316-
expect(subject.data!).to eq(cache: "miss")
359+
expect(builder.data!).to eq(cache: "hit")
360+
361+
# cache hit
362+
builder = described_class.new
363+
builder.cache_store! driver
364+
365+
builder.cache! :cache_key do |cache|
366+
cache.cache "miss"
367+
end
368+
369+
expect(builder.data!).to eq(cache: "hit")
317370
end
318371
end
319372

0 commit comments

Comments
 (0)