identity_cache 0.4.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +92 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +5 -0
  6. data/CAVEATS.md +25 -0
  7. data/CHANGELOG.md +73 -19
  8. data/Gemfile +5 -1
  9. data/LICENSE +1 -1
  10. data/README.md +49 -27
  11. data/Rakefile +14 -5
  12. data/dev.yml +12 -16
  13. data/gemfiles/Gemfile.latest-release +8 -0
  14. data/gemfiles/Gemfile.min-supported +7 -0
  15. data/gemfiles/Gemfile.rails-edge +7 -0
  16. data/identity_cache.gemspec +29 -10
  17. data/lib/identity_cache.rb +78 -51
  18. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  19. data/lib/identity_cache/cache_fetcher.rb +6 -5
  20. data/lib/identity_cache/cache_hash.rb +2 -2
  21. data/lib/identity_cache/cache_invalidation.rb +4 -11
  22. data/lib/identity_cache/cache_key_generation.rb +17 -65
  23. data/lib/identity_cache/cache_key_loader.rb +128 -0
  24. data/lib/identity_cache/cached.rb +7 -0
  25. data/lib/identity_cache/cached/association.rb +87 -0
  26. data/lib/identity_cache/cached/attribute.rb +123 -0
  27. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  28. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  29. data/lib/identity_cache/cached/belongs_to.rb +100 -0
  30. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +61 -0
  32. data/lib/identity_cache/cached/primary_index.rb +96 -0
  33. data/lib/identity_cache/cached/recursive/association.rb +109 -0
  34. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  36. data/lib/identity_cache/cached/reference/association.rb +16 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  38. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  39. data/lib/identity_cache/configuration_dsl.rb +53 -215
  40. data/lib/identity_cache/encoder.rb +95 -0
  41. data/lib/identity_cache/expiry_hook.rb +36 -0
  42. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  43. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  44. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  45. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  46. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  47. data/lib/identity_cache/mem_cache_store_cas.rb +53 -0
  48. data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
  49. data/lib/identity_cache/parent_model_expiration.rb +46 -11
  50. data/lib/identity_cache/query_api.rb +102 -408
  51. data/lib/identity_cache/railtie.rb +8 -0
  52. data/lib/identity_cache/record_not_found.rb +6 -0
  53. data/lib/identity_cache/should_use_cache.rb +1 -0
  54. data/lib/identity_cache/version.rb +3 -2
  55. data/lib/identity_cache/with_primary_index.rb +136 -0
  56. data/lib/identity_cache/without_primary_index.rb +24 -3
  57. data/performance/cache_runner.rb +25 -73
  58. data/performance/cpu.rb +4 -3
  59. data/performance/externals.rb +4 -3
  60. data/performance/profile.rb +6 -5
  61. data/railgun.yml +16 -0
  62. metadata +60 -73
  63. data/.travis.yml +0 -30
  64. data/Gemfile.rails42 +0 -6
  65. data/Gemfile.rails50 +0 -6
  66. data/test/attribute_cache_test.rb +0 -110
  67. data/test/cache_fetch_includes_test.rb +0 -46
  68. data/test/cache_hash_test.rb +0 -14
  69. data/test/cache_invalidation_test.rb +0 -139
  70. data/test/deeply_nested_associated_record_test.rb +0 -19
  71. data/test/denormalized_has_many_test.rb +0 -211
  72. data/test/denormalized_has_one_test.rb +0 -160
  73. data/test/fetch_multi_test.rb +0 -308
  74. data/test/fetch_test.rb +0 -258
  75. data/test/fixtures/serialized_record.mysql2 +0 -0
  76. data/test/fixtures/serialized_record.postgresql +0 -0
  77. data/test/helpers/active_record_objects.rb +0 -106
  78. data/test/helpers/database_connection.rb +0 -72
  79. data/test/helpers/serialization_format.rb +0 -42
  80. data/test/helpers/update_serialization_format.rb +0 -24
  81. data/test/identity_cache_test.rb +0 -29
  82. data/test/index_cache_test.rb +0 -161
  83. data/test/memoized_attributes_test.rb +0 -49
  84. data/test/memoized_cache_proxy_test.rb +0 -107
  85. data/test/normalized_belongs_to_test.rb +0 -107
  86. data/test/normalized_has_many_test.rb +0 -231
  87. data/test/normalized_has_one_test.rb +0 -9
  88. data/test/prefetch_associations_test.rb +0 -364
  89. data/test/readonly_test.rb +0 -109
  90. data/test/recursive_denormalized_has_many_test.rb +0 -131
  91. data/test/save_test.rb +0 -82
  92. data/test/schema_change_test.rb +0 -112
  93. data/test/serialization_format_change_test.rb +0 -16
  94. data/test/test_helper.rb +0 -140
@@ -1,308 +0,0 @@
1
- require "test_helper"
2
-
3
- class FetchMultiTest < IdentityCache::TestCase
4
- NAMESPACE = IdentityCache.cache_namespace
5
-
6
- def setup
7
- super
8
- @bob = Item.create!(:title => 'bob')
9
- @joe = Item.create!(:title => 'joe')
10
- @fred = Item.create!(:title => 'fred')
11
- @bob_blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:1"
12
- @joe_blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:2"
13
- @fred_blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:3"
14
- @tenth_blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:10"
15
- end
16
-
17
- def test_fetch_multi_with_no_records
18
- assert_equal [], Item.fetch_multi
19
- end
20
-
21
- def test_fetch_multi_namespace
22
- Item.send(:include, SwitchNamespace)
23
- bob_blob_key, joe_blob_key, fred_blob_key = [@bob_blob_key, @joe_blob_key, @fred_blob_key].map { |k| "ns:#{k}" }
24
- cache_response = {}
25
- cache_response[bob_blob_key] = cache_response_for(@bob)
26
- cache_response[joe_blob_key] = cache_response_for(@joe)
27
- cache_response[fred_blob_key] = cache_response_for(@fred)
28
- IdentityCache.cache.expects(:fetch_multi).with(bob_blob_key, joe_blob_key, fred_blob_key).returns(cache_response)
29
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
30
- end
31
-
32
- def test_fetch_multi_with_all_hits
33
- cache_response = {}
34
- cache_response[@bob_blob_key] = cache_response_for(@bob)
35
- cache_response[@joe_blob_key] = cache_response_for(@joe)
36
- cache_response[@fred_blob_key] = cache_response_for(@fred)
37
- IdentityCache.cache.expects(:fetch_multi).with(@bob_blob_key, @joe_blob_key, @fred_blob_key).returns(cache_response)
38
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
39
- end
40
-
41
- def test_fetch_multi_with_all_misses
42
- cache_response = {}
43
- cache_response[@bob_blob_key] = nil
44
- cache_response[@joe_blob_key] = nil
45
- cache_response[@fred_blob_key] = nil
46
- fetch_multi = fetch_multi_stub(cache_response)
47
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
48
- assert fetch_multi.has_been_called_with?(@bob_blob_key, @joe_blob_key, @fred_blob_key)
49
- end
50
-
51
- def test_fetch_multi_with_mixed_hits_and_misses
52
- cache_response = {}
53
- cache_response[@bob_blob_key] = cache_response_for(@bob)
54
- cache_response[@joe_blob_key] = nil
55
- cache_response[@fred_blob_key] = cache_response_for(@fred)
56
- fetch_multi = fetch_multi_stub(cache_response)
57
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
58
- assert fetch_multi.has_been_called_with?(@bob_blob_key, @joe_blob_key, @fred_blob_key)
59
- end
60
-
61
- def test_fetch_multi_with_mixed_hits_and_misses_and_responses_in_the_wrong_order
62
- cache_response = {}
63
- cache_response[@bob_blob_key] = nil
64
- cache_response[@joe_blob_key] = nil
65
- cache_response[@fred_blob_key] = cache_response_for(@fred)
66
- fetch_multi = fetch_multi_stub(cache_response)
67
- assert_equal [@bob, @fred, @joe], Item.fetch_multi(@bob.id, @fred.id, @joe.id)
68
- assert fetch_multi.has_been_called_with?(@bob_blob_key, @fred_blob_key, @joe_blob_key)
69
- end
70
-
71
- def test_fetch_multi_with_mixed_hits_and_misses_and_non_existant_keys_1
72
- populate_only_fred
73
-
74
- fetch_multi = fetch_multi_stub(@cache_response)
75
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(10, @bob.id, @joe.id, @fred.id)
76
- assert fetch_multi.has_been_called_with?(@tenth_blob_key, @bob_blob_key, @joe_blob_key, @fred_blob_key)
77
- end
78
-
79
- def test_fetch_multi_with_mixed_hits_and_misses_and_non_existant_keys_2
80
- populate_only_fred
81
-
82
- fetch_multi = fetch_multi_stub(@cache_response)
83
- assert_equal [@fred, @bob, @joe], Item.fetch_multi(@fred.id, @bob.id, 10, @joe.id)
84
- assert fetch_multi.has_been_called_with?(@fred_blob_key, @bob_blob_key, @tenth_blob_key, @joe_blob_key)
85
- end
86
-
87
-
88
- def test_fetch_multi_works_with_nils
89
- cache_result = {1 => IdentityCache::CACHED_NIL, 2 => IdentityCache::CACHED_NIL}
90
- fetch_result = {1 => nil, 2 => nil}
91
-
92
- fetcher.expects(:cas_multi).with([1, 2]).twice.returns(nil, cache_result)
93
- fetcher.expects(:add).with(1, IdentityCache::CACHED_NIL).once
94
- fetcher.expects(:add).with(2, IdentityCache::CACHED_NIL).once
95
-
96
- results = IdentityCache.fetch_multi(1,2) do |keys|
97
- [nil, nil]
98
- end
99
- assert_equal fetch_result, results
100
-
101
- results = IdentityCache.fetch_multi(1,2) do |keys|
102
- flunk "Contents should have been fetched from cache successfully"
103
- end
104
-
105
- assert_equal fetch_result, results
106
- end
107
-
108
- def test_fetch_multi_works_with_blanks
109
- cache_result = {1 => false, 2 => ' '}
110
-
111
- fetcher.expects(:fetch_multi).with([1,2]).returns(cache_result)
112
-
113
- results = IdentityCache.fetch_multi(1,2) do |keys|
114
- flunk "Contents should have been fetched from cache successfully"
115
- end
116
-
117
- assert_equal cache_result, results
118
- end
119
-
120
- def test_fetch_multi_duplicate_ids
121
- assert_equal [@joe, @bob, @joe], Item.fetch_multi(@joe.id, @bob.id, @joe.id)
122
- end
123
-
124
- def test_fetch_multi_with_duplicate_ids_hits_backend_once_per_id
125
- cache_response = {
126
- @joe_blob_key => cache_response_for(@joe),
127
- @bob_blob_key => cache_response_for(@bob),
128
- }
129
-
130
- fetcher.expects(:fetch_multi).with([@joe_blob_key, @bob_blob_key]).returns(cache_response)
131
- result = Item.fetch_multi(@joe.id, @bob.id, @joe.id)
132
-
133
- assert_equal [@joe, @bob, @joe], result
134
- end
135
-
136
- def test_fetch_multi_with_open_transactions_hits_the_database
137
- Item.connection.expects(:open_transactions).at_least_once.returns(1)
138
- fetcher.expects(:fetch_multi).never
139
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
140
- end
141
-
142
- def test_fetch_multi_with_open_transactions_returns_results_in_the_order_of_the_passed_ids
143
- Item.connection.expects(:open_transactions).at_least_once.returns(1)
144
- assert_equal [@joe, @bob, @fred], Item.fetch_multi(@joe.id, @bob.id, @fred.id)
145
- end
146
-
147
- def test_fetch_multi_with_open_transactions_should_compacts_returned_array
148
- Item.connection.expects(:open_transactions).at_least_once.returns(1)
149
- assert_equal [@joe, @fred], Item.fetch_multi(@joe.id, 0, @fred.id)
150
- end
151
-
152
- def test_fetch_multi_with_duplicate_ids_in_transaction_returns_results_in_the_order_of_the_passed_ids
153
- Item.connection.expects(:open_transactions).at_least_once.returns(1)
154
- assert_equal [@joe, @bob, @joe], Item.fetch_multi(@joe.id, @bob.id, @joe.id)
155
- end
156
-
157
- def test_find_batch_coerces_ids_to_primary_key_type
158
- mock_relation = mock("ActiveRecord::Relation")
159
- Item.expects(:where).returns(mock_relation)
160
- mock_relation.expects(:includes).returns(stub(:to_a => [@bob, @joe, @fred]))
161
-
162
- Item.send(:find_batch, [@bob, @joe, @fred].map(&:id).map(&:to_s))
163
- end
164
-
165
- def test_fetch_multi_doesnt_freeze_keys
166
- cache_response = {}
167
- cache_response[@bob_blob_key] = cache_response_for(@bob)
168
- cache_response[@joe_blob_key] = cache_response_for(@fred)
169
-
170
- IdentityCache.expects(:fetch_multi).with{ |*args| args.none?(&:frozen?) }.returns(cache_response)
171
-
172
- Item.fetch_multi(@bob.id, @joe.id)
173
- end
174
-
175
- def test_fetch_multi_array
176
- assert_equal [@joe, @bob], Item.fetch_multi([@joe.id, @bob.id])
177
- end
178
-
179
- def test_fetch_multi_reads_in_batches
180
- cache_response = {}
181
- cache_response[@bob_blob_key] = cache_response_for(@bob)
182
- cache_response[@joe_blob_key] = cache_response_for(@joe)
183
-
184
- with_batch_size 1 do
185
- fetcher.expects(:fetch_multi).with([@bob_blob_key]).returns(cache_response).once
186
- fetcher.expects(:fetch_multi).with([@joe_blob_key]).returns(cache_response).once
187
- assert_equal [@bob, @joe], Item.fetch_multi(@bob.id, @joe.id)
188
- end
189
- end
190
-
191
- def test_fetch_multi_max_stack_level
192
- cache_response = { @fred_blob_key => cache_response_for(@fred) }
193
- fetcher.stubs(:fetch_multi).returns(cache_response)
194
- assert_nothing_raised { Item.fetch_multi([@fred.id] * 200_000) }
195
- end
196
-
197
- def test_fetch_multi_with_non_id_primary_key
198
- fixture = KeyedRecord.create!(:value => "a") { |r| r.hashed_key = 123 }
199
- assert_equal [fixture], KeyedRecord.fetch_multi(123, 456)
200
- end
201
-
202
- def test_fetch_multi_after_expiring_a_record
203
- Item.fetch_multi(@joe.id, @fred.id)
204
- @bob.send(:expire_cache)
205
- assert_equal IdentityCache::DELETED, backend.read(@bob.primary_cache_index_key)
206
-
207
- add = Spy.on(IdentityCache.cache.cache_fetcher, :add).and_call_through
208
-
209
- assert_equal [@bob, @joe, @fred], Item.fetch_multi(@bob.id, @joe.id, @fred.id)
210
- refute add.has_been_called?
211
- assert_equal cache_response_for(Item.find(@bob.id)), backend.read(@bob.primary_cache_index_key)
212
- end
213
-
214
- def test_fetch_multi_raises_when_called_on_a_scope
215
- assert_raises(IdentityCache::UnsupportedScopeError) do
216
- Item.where(updated_at: nil).fetch_multi(@bob.id, @joe.id, @fred.id)
217
- end
218
- end
219
-
220
- def test_fetch_multi_on_derived_model_raises
221
- assert_raises(IdentityCache::DerivedModelError) do
222
- StiRecordTypeA.fetch_multi(1, 2)
223
- end
224
- end
225
-
226
- def test_fetch_multi_with_mixed_hits_and_misses_returns_only_readonly_records
227
- IdentityCache.with_fetch_read_only_records do
228
- cache_response = {}
229
- cache_response[@bob_blob_key] = cache_response_for(@bob)
230
- cache_response[@joe_blob_key] = nil
231
- cache_response[@fred_blob_key] = cache_response_for(@fred)
232
- fetch_multi = fetch_multi_stub(cache_response)
233
-
234
- response = Item.fetch_multi(@bob.id, @joe.id, @fred.id)
235
- assert fetch_multi.has_been_called_with?(@bob_blob_key, @joe_blob_key, @fred_blob_key)
236
- assert_equal [@bob, @joe, @fred], response
237
-
238
- assert response.all?(&:readonly?)
239
- end
240
- end
241
-
242
- def test_fetch_multi_with_mixed_hits_and_misses_and_responses_in_the_wrong_order_returns_readonly
243
- IdentityCache.with_fetch_read_only_records do
244
- cache_response = {}
245
- cache_response[@bob_blob_key] = nil
246
- cache_response[@joe_blob_key] = nil
247
- cache_response[@fred_blob_key] = cache_response_for(@fred)
248
- fetch_multi = fetch_multi_stub(cache_response)
249
-
250
- response = Item.fetch_multi(@bob.id, @joe.id, @fred.id)
251
- assert fetch_multi.has_been_called_with?(@bob_blob_key, @joe_blob_key, @fred_blob_key)
252
- assert_equal [@bob, @joe, @fred], response
253
-
254
- assert response.all?(&:readonly?)
255
- end
256
- end
257
-
258
- def test_fetch_multi_with_open_transactions_returns_non_readonly_records
259
- IdentityCache.with_fetch_read_only_records do
260
- Item.transaction do
261
- assert_equal IdentityCache.should_use_cache?, false
262
- IdentityCache.cache.expects(:fetch_multi).never
263
- assert Item.fetch_multi(@bob.id, @joe.id, @fred.id).none?(&:readonly?)
264
- end
265
- end
266
- end
267
-
268
- def test_fetch_multi_with_no_keys_does_not_query_when_cache_is_disabled
269
- Item.stubs(:should_use_cache?).returns(false)
270
-
271
- assert_queries(0) do
272
- assert_memcache_operations(0) do
273
- Item.fetch_multi
274
- end
275
- end
276
- end
277
-
278
- private
279
-
280
- def populate_only_fred
281
- @cache_response = {}
282
- @cache_response[@bob_blob_key] = nil
283
- @cache_response[@joe_blob_key] = nil
284
- @cache_response[@tenth_blob_key] = nil
285
- @cache_response[@fred_blob_key] = cache_response_for(@fred)
286
- end
287
-
288
- def cache_response_for(record)
289
- {class: record.class, attributes: record.attributes_before_type_cast}
290
- end
291
-
292
- def with_batch_size(size)
293
- previous_batch_size = IdentityCache::BATCH_SIZE
294
- IdentityCache.send(:remove_const, :BATCH_SIZE)
295
- IdentityCache.const_set(:BATCH_SIZE, size)
296
- yield
297
- ensure
298
- IdentityCache.send(:remove_const, :BATCH_SIZE)
299
- IdentityCache.const_set(:BATCH_SIZE, previous_batch_size)
300
- end
301
-
302
- def fetch_multi_stub(cache_response)
303
- Spy.on(IdentityCache.cache, :fetch_multi).and_return do |*args, &block|
304
- nil_keys = cache_response.select {|_, v| v.nil? }.keys
305
- cache_response.merge(Hash[nil_keys.zip(block.call(nil_keys))])
306
- end
307
- end
308
- end
@@ -1,258 +0,0 @@
1
- require "test_helper"
2
-
3
- class FetchTest < IdentityCache::TestCase
4
- NAMESPACE = IdentityCache::CacheKeyGeneration::DEFAULT_NAMESPACE
5
-
6
- def setup
7
- super
8
- Item.cache_index :title, :unique => true
9
- Item.cache_index :id, :title, :unique => true
10
-
11
- @record = Item.new
12
- @record.id = 1
13
- @record.title = 'bob'
14
- @cached_value = {class: @record.class, attributes: @record.attributes_before_type_cast}
15
- @blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:1"
16
- @index_key = "#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}"
17
- end
18
-
19
- def test_fetch_with_garbage_input
20
- assert_nil Item.fetch_by_id('garbage')
21
- end
22
-
23
- def test_fetch_cache_hit
24
- IdentityCache.cache.expects(:fetch).with(@blob_key).returns(@cached_value)
25
-
26
- assert_equal @record, Item.fetch(1)
27
- end
28
-
29
- def test_fetch_hit_cache_namespace
30
- old_ns = IdentityCache.cache_namespace
31
- IdentityCache.cache_namespace = proc { |model| "#{model.table_name}:#{old_ns}" }
32
-
33
- new_blob_key = "items:#{@blob_key}"
34
- IdentityCache.cache.expects(:fetch).with(new_blob_key).returns(@cached_value)
35
-
36
- assert_equal @record, Item.fetch(1)
37
- ensure
38
- IdentityCache.cache_namespace = old_ns
39
- end
40
-
41
- def test_exists_with_identity_cache_when_cache_hit
42
- IdentityCache.cache.expects(:fetch).with(@blob_key).returns(@cached_value)
43
-
44
- assert Item.exists_with_identity_cache?(1)
45
- end
46
-
47
- def test_exists_with_identity_cache_when_cache_miss_and_in_db
48
- fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
49
- Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
50
-
51
- assert Item.exists_with_identity_cache?(1)
52
- assert fetch.has_been_called_with?(@blob_key)
53
- end
54
-
55
- def test_exists_with_identity_cache_when_cache_miss_and_not_in_db
56
- fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
57
- Item.expects(:resolve_cache_miss).with(1).once.returns(nil)
58
-
59
- assert !Item.exists_with_identity_cache?(1)
60
- assert fetch.has_been_called_with?(@blob_key)
61
- end
62
-
63
- def test_fetch_miss
64
- Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
65
-
66
- results = []
67
- fetch = Spy.on(IdentityCache.cache, :fetch).and_return {|_, &block| block.call.tap {|result| results << result}}
68
-
69
- assert_equal @record, Item.fetch(1)
70
- assert fetch.has_been_called_with?(@blob_key)
71
- assert_equal [@cached_value], results
72
- end
73
-
74
- def test_fetch_miss_with_non_id_primary_key
75
- hashed_key = Zlib::crc32("foo") % (2 ** 30 - 1)
76
- fixture = KeyedRecord.create!(:value => "foo") { |r| r.hashed_key = hashed_key }
77
- assert_equal fixture, KeyedRecord.fetch(hashed_key)
78
- end
79
-
80
- def test_fetch_conflict
81
- resolve_cache_miss = Spy.on(Item, :resolve_cache_miss).and_return do
82
- @record.send(:expire_cache)
83
- @record
84
- end
85
- add = Spy.on(fetcher, :add).and_call_through
86
-
87
- assert_equal @record, Item.fetch(1)
88
- assert resolve_cache_miss.has_been_called_with?(1)
89
- assert add.has_been_called_with?(@blob_key, @cached_value)
90
- assert_equal IdentityCache::DELETED, backend.read(@record.primary_cache_index_key)
91
- end
92
-
93
- def test_fetch_conflict_after_delete
94
- @record.send(:expire_cache)
95
- assert_equal IdentityCache::DELETED, backend.read(@record.primary_cache_index_key)
96
-
97
- resolve_cache_miss = Spy.on(Item, :resolve_cache_miss).and_return do
98
- @record.send(:expire_cache)
99
- @record
100
- end
101
- add = Spy.on(IdentityCache.cache.cache_fetcher, :add).and_call_through
102
-
103
- assert_equal @record, Item.fetch(1)
104
- assert resolve_cache_miss.has_been_called_with?(1)
105
- refute add.has_been_called?
106
- assert_equal IdentityCache::DELETED, backend.read(@record.primary_cache_index_key)
107
- end
108
-
109
- def test_fetch_by_id_not_found_should_return_nil
110
- nonexistent_record_id = 10
111
- fetcher.expects(:add).with(@blob_key + '0', IdentityCache::CACHED_NIL)
112
-
113
- assert_nil Item.fetch_by_id(nonexistent_record_id)
114
- end
115
-
116
- def test_fetch_not_found_should_raise
117
- nonexistent_record_id = 10
118
- fetcher.expects(:add).with(@blob_key + '0', IdentityCache::CACHED_NIL)
119
-
120
- assert_raises(ActiveRecord::RecordNotFound) { Item.fetch(nonexistent_record_id) }
121
- end
122
-
123
- def test_cached_nil_expiry_on_record_creation
124
- key = @record.primary_cache_index_key
125
-
126
- assert_nil Item.fetch_by_id(@record.id)
127
- assert_equal IdentityCache::CACHED_NIL, backend.read(key)
128
-
129
- @record.save!
130
- assert_equal IdentityCache::DELETED, backend.read(key)
131
- end
132
-
133
- def test_fetch_by_title_hit
134
- values = []
135
- fetch = Spy.on(IdentityCache.cache, :fetch).and_return do |key, &block|
136
- case key
137
- # Read record with title bob
138
- when @index_key then block.call.tap {|val| values << val}
139
- # got id, do memcache lookup on that, hit -> done
140
- when @blob_key then @cached_value
141
- end
142
- end
143
-
144
- # Id not found, use sql, SELECT id FROM records WHERE title = '...' LIMIT 1"
145
- Item.connection.expects(:exec_query).returns(ActiveRecord::Result.new(['id'], [[1]]))
146
-
147
- assert_equal @record, Item.fetch_by_title('bob')
148
- assert_equal [1], values
149
- assert fetch.has_been_called_with?(@index_key)
150
- assert fetch.has_been_called_with?(@blob_key)
151
- end
152
-
153
- def test_fetch_by_title_cache_namespace
154
- Item.send(:include, SwitchNamespace)
155
- IdentityCache.cache.expects(:fetch).with("ns:#{@index_key}").returns(1)
156
- IdentityCache.cache.expects(:fetch).with("ns:#{@blob_key}").returns(@cached_value)
157
-
158
- assert_equal @record, Item.fetch_by_title('bob')
159
- end
160
-
161
- def test_fetch_by_title_stores_idcnil
162
- Item.connection.expects(:exec_query).once.returns(ActiveRecord::Result.new([], []))
163
- add = Spy.on(fetcher, :add).and_call_through
164
- fetch = Spy.on(fetcher, :fetch).and_call_through
165
- assert_nil Item.fetch_by_title('bob') # exec_query => nil
166
-
167
- assert_nil Item.fetch_by_title('bob') # returns cached nil
168
- assert_nil Item.fetch_by_title('bob') # returns cached nil
169
-
170
- assert add.has_been_called_with?(@index_key, IdentityCache::CACHED_NIL)
171
- assert_equal 3, fetch.calls.length
172
- end
173
-
174
- def test_fetch_by_bang_method
175
- Item.connection.expects(:exec_query).returns(ActiveRecord::Result.new([], []))
176
- assert_raises ActiveRecord::RecordNotFound do
177
- Item.fetch_by_title!('bob')
178
- end
179
- end
180
-
181
- def test_fetch_does_not_communicate_to_cache_with_nil_id
182
- fetcher.expects(:fetch).never
183
- fetcher.expects(:add).never
184
- assert_raises(ActiveRecord::RecordNotFound) { Item.fetch(nil) }
185
- end
186
-
187
- def test_fetch_cache_hit_does_not_checkout_database_connection
188
- @record.save!
189
- record = Item.fetch(@record.id)
190
-
191
- ActiveRecord::Base.clear_active_connections!
192
-
193
- assert_equal record, Item.fetch(@record.id)
194
-
195
- assert_equal false, ActiveRecord::Base.connection_handler.active_connections?
196
- end
197
-
198
- def test_fetch_raises_when_called_on_a_scope
199
- assert_raises(IdentityCache::UnsupportedScopeError) do
200
- Item.where(updated_at: nil).fetch(1)
201
- end
202
- end
203
-
204
- def test_fetch_by_raises_when_called_on_a_scope
205
- assert_raises(IdentityCache::UnsupportedScopeError) do
206
- Item.where(updated_at: nil).fetch_by_id(1)
207
- end
208
- end
209
-
210
- def test_fetch_on_derived_model_raises
211
- assert_raises(IdentityCache::DerivedModelError) do
212
- StiRecordTypeA.fetch(1)
213
- end
214
- end
215
-
216
- def test_returned_records_are_readonly_on_cache_hit
217
- IdentityCache.with_fetch_read_only_records do
218
- IdentityCache.cache.expects(:fetch).with(@blob_key).returns(@cached_value)
219
- assert Item.fetch(1).readonly?
220
- end
221
- end
222
-
223
- def test_returned_records_are_readonly_on_cache_miss
224
- IdentityCache.with_fetch_read_only_records do
225
- fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
226
- Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
227
-
228
- assert Item.exists_with_identity_cache?(1)
229
- assert fetch.has_been_called_with?(@blob_key)
230
- assert Item.fetch(1).readonly?
231
- end
232
- end
233
-
234
- def test_returned_records_are_not_readonly_with_open_transactions
235
- IdentityCache.with_fetch_read_only_records do
236
-
237
- @record.transaction do
238
- fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
239
- Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
240
-
241
- refute IdentityCache.should_use_cache?
242
- refute fetch.has_been_called_with?(@blob_key)
243
- refute Item.fetch(1).readonly?, "Fetched item was read-only"
244
- end
245
- end
246
- end
247
-
248
- def test_respects_should_use_cache_on_record
249
- @record.save
250
- Item.stubs(:should_use_cache?).returns(false)
251
-
252
- assert_memcache_operations(0) do
253
- assert_queries(1) do
254
- Item.fetch_by_id(@record.id)
255
- end
256
- end
257
- end
258
- end