lock_and_cache_msgpack 4.0.7.pre1

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.
@@ -0,0 +1,3 @@
1
+ module LockAndCacheMsgpack
2
+ VERSION = '4.0.7.pre1'
3
+ end
@@ -0,0 +1,16 @@
1
+ class Time
2
+
3
+ def to_msgpack(packer=nil)
4
+ packer ||= MessagePack::Packer.new
5
+ packer.pack self
6
+ end
7
+
8
+ end
9
+
10
+ MessagePack::DefaultFactory.register_type(0x00, Symbol)
11
+
12
+ MessagePack::DefaultFactory.register_type(0x01, Time,
13
+ packer: ->(t){ t.iso8601.to_msgpack },
14
+ unpacker: ->(d){
15
+ Time.parse(MessagePack.unpack(d.force_encoding("ASCII-8BIT")))
16
+ })
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'lock_and_cache_msgpack/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "lock_and_cache_msgpack"
8
+ spec.version = LockAndCacheMsgpack::VERSION
9
+ spec.authors = ["Seamus Abshere", "Matt E. Patterson"]
10
+ spec.email = ["seamus@abshere.net", "mpatterson@skillsengine.com"]
11
+ spec.summary = %q{Lock and cache methods, with MessagePack.}
12
+ spec.description = %q{Lock and cache methods, in case things should only be calculated once across processes.}
13
+ spec.homepage = "https://github.com/c4eo/lock_and_cache_msgpack"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'activesupport'
22
+ spec.add_runtime_dependency 'redis'
23
+ # temporary until https://github.com/leandromoreira/redlock-rb/pull/20 is merged
24
+ spec.add_runtime_dependency 'redlock', '>=0.1.3'
25
+ spec.add_runtime_dependency 'msgpack', '~> 1.1.0'
26
+
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'bundler', '~> 1.6'
29
+ spec.add_development_dependency 'rake', '~> 10.0'
30
+ spec.add_development_dependency 'rspec'
31
+ spec.add_development_dependency 'thread'
32
+ spec.add_development_dependency 'yard'
33
+ spec.add_development_dependency 'redcarpet'
34
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ class KeyTestId
4
+ def id
5
+ 'id'
6
+ end
7
+ end
8
+ class KeyTestLockAndCacheMsgpackKey
9
+ def lock_and_cache_key
10
+ 'lock_and_cache_key'
11
+ end
12
+ end
13
+ class KeyTest1
14
+ def lock_and_cache_key
15
+ KeyTestLockAndCacheMsgpackKey.new
16
+ end
17
+ end
18
+ describe LockAndCacheMsgpack::Key do
19
+ describe 'parts' do
20
+ it "has a known issue differentiating between {a: 1} and [[:a, 1]]" do
21
+ expect(described_class.new(a: 1).send(:parts)).to eq(described_class.new([[:a, 1]]).send(:parts))
22
+ end
23
+
24
+ now = Time.now
25
+ today = Date.today
26
+ {
27
+ [1] => [1],
28
+ ['you'] => ['you'],
29
+ [['you']] => [['you']],
30
+ [['you'], "person"] => [['you'], "person"],
31
+ [['you'], {:silly=>:person}] => [['you'], [[:silly, :person]] ],
32
+ [now] => [now.to_s],
33
+ [[now]] => [[now.to_s]],
34
+ [today] => [today.to_s],
35
+ [[today]] => [[today.to_s]],
36
+ { hi: 'you' } => [[:hi, 'you']],
37
+ { hi: 123 } => [[:hi, 123]],
38
+ { hi: 123.0 } => [[:hi, 123.0]],
39
+ { hi: now } => [[:hi, now.to_s]],
40
+ { hi: today } => [[:hi, today.to_s]],
41
+ [KeyTestId.new] => ['id'],
42
+ [[KeyTestId.new]] => [['id']],
43
+ { a: KeyTestId.new } => [[:a, "id"]],
44
+ [{ a: KeyTestId.new }] => [[[:a, "id"]]],
45
+ [[{ a: KeyTestId.new }]] => [[ [[:a, "id"]] ]],
46
+ [[{ a: [ KeyTestId.new ] }]] => [[[[:a, ["id"]]]]],
47
+ [[{ a: { b: KeyTestId.new } }]] => [[ [[ :a, [[:b, "id"]] ]] ]],
48
+ [[{ a: { b: [ KeyTestId.new ] } }]] => [[ [[ :a, [[:b, ["id"]]] ]] ]],
49
+ [KeyTestLockAndCacheMsgpackKey.new] => ['lock_and_cache_key'],
50
+ [[KeyTestLockAndCacheMsgpackKey.new]] => [['lock_and_cache_key']],
51
+ { a: KeyTestLockAndCacheMsgpackKey.new } => [[:a, "lock_and_cache_key"]],
52
+ [{ a: KeyTestLockAndCacheMsgpackKey.new }] => [[[:a, "lock_and_cache_key"]]],
53
+ [[{ a: KeyTestLockAndCacheMsgpackKey.new }]] => [[ [[:a, "lock_and_cache_key"]] ]],
54
+ [[{ a: [ KeyTestLockAndCacheMsgpackKey.new ] }]] => [[[[:a, ["lock_and_cache_key"]]]]],
55
+ [[{ a: { b: KeyTestLockAndCacheMsgpackKey.new } }]] => [[ [[ :a, [[:b, "lock_and_cache_key"]] ]] ]],
56
+ [[{ a: { b: [ KeyTestLockAndCacheMsgpackKey.new ] } }]] => [[ [[ :a, [[:b, ["lock_and_cache_key"]]] ]] ]],
57
+
58
+ [[{ a: { b: [ KeyTest1.new ] } }]] => [[ [[ :a, [[:b, ["lock_and_cache_key"]]] ]] ]],
59
+ }.each do |i, o|
60
+ it "turns #{i} into #{o}" do
61
+ expect(described_class.new(i).send(:parts)).to eq(o)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,476 @@
1
+ require 'spec_helper'
2
+
3
+ class Foo
4
+ include LockAndCacheMsgpack
5
+
6
+ def initialize(id)
7
+ @id = id
8
+ @count = 0
9
+ @count_exp = 0
10
+ @click_single_hash_arg_as_options = 0
11
+ @click_last_hash_as_options = 0
12
+ end
13
+
14
+ def click
15
+ lock_and_cache do
16
+ @count += 1
17
+ end
18
+ end
19
+
20
+ def cached_rand
21
+ lock_and_cache do
22
+ rand
23
+ end
24
+ end
25
+
26
+ def click_null
27
+ lock_and_cache do
28
+ nil
29
+ end
30
+ end
31
+
32
+ def click_exp
33
+ lock_and_cache(expires: 1) do
34
+ @count_exp += 1
35
+ end
36
+ end
37
+
38
+ # foo will be treated as option, so this is cacheable
39
+ def click_single_hash_arg_as_options
40
+ lock_and_cache(foo: rand, expires: 1) do
41
+ @click_single_hash_arg_as_options += 1
42
+ end
43
+ end
44
+
45
+ # foo will be treated as part of cache key, so this is uncacheable
46
+ def click_last_hash_as_options
47
+ lock_and_cache({foo: rand}, expires: 1) do
48
+ @click_last_hash_as_options += 1
49
+ end
50
+ end
51
+
52
+ def lock_and_cache_key
53
+ @id
54
+ end
55
+ end
56
+
57
+ class FooId
58
+ include LockAndCacheMsgpack
59
+ def click
60
+ lock_and_cache do
61
+ nil
62
+ end
63
+ end
64
+ def id
65
+ @id ||= rand
66
+ end
67
+ end
68
+
69
+ class FooClass
70
+ class << self
71
+ include LockAndCacheMsgpack
72
+ def click
73
+ lock_and_cache do
74
+ nil
75
+ end
76
+ end
77
+ def id
78
+ raise "called id"
79
+ end
80
+ end
81
+ end
82
+
83
+ require 'set'
84
+ $clicking = Set.new
85
+ class Bar
86
+ include LockAndCacheMsgpack
87
+
88
+ def initialize(id)
89
+ @id = id
90
+ @count = 0
91
+ @mutex = Mutex.new
92
+ end
93
+
94
+ def unsafe_click
95
+ @mutex.synchronize do
96
+ # puts "clicking bar #{@id} - #{$clicking.to_a} - #{$clicking.include?(@id)} - #{@id == $clicking.to_a[0]}"
97
+ raise "somebody already clicking Bar #{@id}" if $clicking.include?(@id)
98
+ $clicking << @id
99
+ end
100
+ sleep 1
101
+ @count += 1
102
+ $clicking.delete @id
103
+ @count
104
+ end
105
+
106
+ def click
107
+ lock_and_cache do
108
+ unsafe_click
109
+ end
110
+ end
111
+
112
+ def slow_click
113
+ lock_and_cache do
114
+ sleep 1
115
+ end
116
+ end
117
+
118
+ def lock_and_cache_key
119
+ @id
120
+ end
121
+ end
122
+
123
+ class Sleeper
124
+ include LockAndCacheMsgpack
125
+
126
+ def initialize
127
+ @id = SecureRandom.hex
128
+ end
129
+
130
+ def poke
131
+ lock_and_cache heartbeat_expires: 2 do
132
+ sleep
133
+ end
134
+ end
135
+
136
+ def lock_and_cache_key
137
+ @id
138
+ end
139
+ end
140
+
141
+ describe LockAndCacheMsgpack do
142
+ before do
143
+ LockAndCacheMsgpack.flush
144
+ end
145
+
146
+ it 'has a version number' do
147
+ expect(LockAndCacheMsgpack::VERSION).not_to be nil
148
+ end
149
+
150
+ describe "caching" do
151
+ let(:foo) { Foo.new(rand.to_s) }
152
+ it "works" do
153
+ expect(foo.click).to eq(1)
154
+ expect(foo.click).to eq(1)
155
+ end
156
+
157
+ it "can be cleared" do
158
+ expect(foo.click).to eq(1)
159
+ foo.lock_and_cache_clear :click
160
+ expect(foo.click).to eq(2)
161
+ end
162
+
163
+ it "can be expired" do
164
+ expect(foo.click_exp).to eq(1)
165
+ expect(foo.click_exp).to eq(1)
166
+ sleep 1.5
167
+ expect(foo.click_exp).to eq(2)
168
+ end
169
+
170
+ it "can cache null" do
171
+ expect(foo.click_null).to eq(nil)
172
+ expect(foo.click_null).to eq(nil)
173
+ end
174
+
175
+ it "treats single hash arg as options" do
176
+ expect(foo.click_single_hash_arg_as_options).to eq(1)
177
+ expect(foo.click_single_hash_arg_as_options).to eq(1)
178
+ sleep 1.1
179
+ expect(foo.click_single_hash_arg_as_options).to eq(2)
180
+ end
181
+
182
+ it "treats last hash as options" do
183
+ expect(foo.click_last_hash_as_options).to eq(1)
184
+ expect(foo.click_last_hash_as_options).to eq(2) # it's uncacheable to prove we're not using as part of options
185
+ expect(foo.click_last_hash_as_options).to eq(3)
186
+ end
187
+
188
+ it "calls #lock_and_cache_key" do
189
+ expect(foo).to receive(:lock_and_cache_key)
190
+ foo.click
191
+ end
192
+
193
+ it "calls #lock_and_cache_key to differentiate" do
194
+ a = Foo.new 1
195
+ b = Foo.new 2
196
+ expect(a.cached_rand).not_to eq(b.cached_rand)
197
+ end
198
+ end
199
+
200
+ describe 'self-identification in context mode' do
201
+ it "calls #id for non-class" do
202
+ foo_id = FooId.new
203
+ expect(foo_id).to receive(:id)
204
+ foo_id.click
205
+ end
206
+ it "calls class name for non-class" do
207
+ foo_id = FooId.new
208
+ expect(FooId).to receive(:name)
209
+ foo_id.click
210
+ end
211
+ it "uses class name for class" do
212
+ expect(FooClass).to receive(:name)
213
+ expect(FooClass).not_to receive(:id)
214
+ FooClass.click
215
+ end
216
+ end
217
+
218
+ describe "locking" do
219
+ let(:bar) { Bar.new(rand.to_s) }
220
+
221
+ it "it blows up normally (simple thread)" do
222
+ a = Thread.new do
223
+ bar.unsafe_click
224
+ end
225
+ b = Thread.new do
226
+ bar.unsafe_click
227
+ end
228
+ expect do
229
+ a.join
230
+ b.join
231
+ end.to raise_error(/somebody/)
232
+ end
233
+
234
+ it "it blows up (pre-existing thread pool, more reliable)" do
235
+ pool = Thread.pool 2
236
+ Thread::Pool.abort_on_exception = true
237
+ expect do
238
+ pool.process do
239
+ bar.unsafe_click
240
+ end
241
+ pool.process do
242
+ bar.unsafe_click
243
+ end
244
+ pool.shutdown
245
+ end.to raise_error(/somebody/)
246
+ end
247
+
248
+ it "doesn't blow up if you lock it (simple thread)" do
249
+ a = Thread.new do
250
+ bar.click
251
+ end
252
+ b = Thread.new do
253
+ bar.click
254
+ end
255
+ a.join
256
+ b.join
257
+ end
258
+
259
+ it "doesn't blow up if you lock it (pre-existing thread pool, more reliable)" do
260
+ pool = Thread.pool 2
261
+ Thread::Pool.abort_on_exception = true
262
+ pool.process do
263
+ bar.click
264
+ end
265
+ pool.process do
266
+ bar.click
267
+ end
268
+ pool.shutdown
269
+ end
270
+
271
+ it "can set a wait time" do
272
+ pool = Thread.pool 2
273
+ Thread::Pool.abort_on_exception = true
274
+ begin
275
+ old_max = LockAndCacheMsgpack.max_lock_wait
276
+ LockAndCacheMsgpack.max_lock_wait = 0.5
277
+ expect do
278
+ pool.process do
279
+ bar.slow_click
280
+ end
281
+ pool.process do
282
+ bar.slow_click
283
+ end
284
+ pool.shutdown
285
+ end.to raise_error(LockAndCacheMsgpack::TimeoutWaitingForLock)
286
+ ensure
287
+ LockAndCacheMsgpack.max_lock_wait = old_max
288
+ end
289
+ end
290
+
291
+ it 'unlocks if a process dies' do
292
+ child = nil
293
+ begin
294
+ sleeper = Sleeper.new
295
+ child = fork do
296
+ sleeper.poke
297
+ end
298
+ sleep 0.1
299
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(true) # the other process has it
300
+ Process.kill 'KILL', child
301
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(true) # the other (dead) process still has it
302
+ sleep 2
303
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(false) # but now it should be cleared because no heartbeat
304
+ ensure
305
+ Process.kill('KILL', child) rescue Errno::ESRCH
306
+ end
307
+ end
308
+
309
+ it "pays attention to heartbeats" do
310
+ child = nil
311
+ begin
312
+ sleeper = Sleeper.new
313
+ child = fork do
314
+ sleeper.poke
315
+ end
316
+ sleep 0.1
317
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(true) # the other process has it
318
+ sleep 2
319
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(true) # the other process still has it
320
+ sleep 2
321
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(true) # the other process still has it
322
+ sleep 2
323
+ expect(sleeper.lock_and_cache_locked?(:poke)).to eq(true) # the other process still has it
324
+ ensure
325
+ Process.kill('TERM', child) rescue Errno::ESRCH
326
+ end
327
+ end
328
+
329
+ end
330
+
331
+ describe 'standalone' do
332
+ it 'works like you expect' do
333
+ count = 0
334
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') { count += 1 }).to eq(1)
335
+ expect(count).to eq(1)
336
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') { count += 1 }).to eq(1)
337
+ expect(count).to eq(1)
338
+ end
339
+
340
+ it "can be queried for cached?" do
341
+ expect(LockAndCacheMsgpack.cached?('hello')).to be_falsy
342
+ LockAndCacheMsgpack.lock_and_cache('hello') { nil }
343
+ expect(LockAndCacheMsgpack.cached?('hello')).to be_truthy
344
+ end
345
+
346
+ it 'allows expiry' do
347
+ count = 0
348
+ expect(LockAndCacheMsgpack.lock_and_cache('hello', expires: 1) { count += 1 }).to eq(1)
349
+ expect(count).to eq(1)
350
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') { count += 1 }).to eq(1)
351
+ expect(count).to eq(1)
352
+ sleep 1.1
353
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') { count += 1 }).to eq(2)
354
+ expect(count).to eq(2)
355
+ end
356
+
357
+ it "allows float expiry" do
358
+ expect{LockAndCacheMsgpack.lock_and_cache('hello', expires: 1.5) {}}.not_to raise_error
359
+ end
360
+
361
+ it 'can be nested' do
362
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') do
363
+ LockAndCacheMsgpack.lock_and_cache('world') do
364
+ LockAndCacheMsgpack.lock_and_cache('privyet') do
365
+ 123
366
+ end
367
+ end
368
+ end).to eq(123)
369
+ end
370
+
371
+ it "requires a key" do
372
+ expect do
373
+ LockAndCacheMsgpack.lock_and_cache do
374
+ raise "this won't happen"
375
+ end
376
+ end.to raise_error(/need/)
377
+ end
378
+
379
+ it 'allows checking locks' do
380
+ expect(LockAndCacheMsgpack.locked?(:sleeper)).to be_falsey
381
+ t = Thread.new do
382
+ LockAndCacheMsgpack.lock_and_cache(:sleeper) { sleep 1 }
383
+ end
384
+ sleep 0.2
385
+ expect(LockAndCacheMsgpack.locked?(:sleeper)).to be_truthy
386
+ t.join
387
+ end
388
+
389
+ it 'allows clearing' do
390
+ count = 0
391
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') { count += 1 }).to eq(1)
392
+ expect(count).to eq(1)
393
+ LockAndCacheMsgpack.clear('hello')
394
+ expect(LockAndCacheMsgpack.lock_and_cache('hello') { count += 1 }).to eq(2)
395
+ expect(count).to eq(2)
396
+ end
397
+
398
+ it 'allows clearing (complex keys)' do
399
+ count = 0
400
+ expect(LockAndCacheMsgpack.lock_and_cache('hello', {world: 1}, expires: 100) { count += 1 }).to eq(1)
401
+ expect(count).to eq(1)
402
+ LockAndCacheMsgpack.clear('hello', world: 1)
403
+ expect(LockAndCacheMsgpack.lock_and_cache('hello', {world: 1}, expires: 100) { count += 1 }).to eq(2)
404
+ expect(count).to eq(2)
405
+ end
406
+
407
+ it 'allows multi-part keys' do
408
+ count = 0
409
+ expect(LockAndCacheMsgpack.lock_and_cache(['hello', 1, { target: 'world' }]) { count += 1 }).to eq(1)
410
+ expect(count).to eq(1)
411
+ expect(LockAndCacheMsgpack.lock_and_cache(['hello', 1, { target: 'world' }]) { count += 1 }).to eq(1)
412
+ expect(count).to eq(1)
413
+ end
414
+
415
+ it 'treats a single hash arg as a cache key (not as options)' do
416
+ count = 0
417
+ LockAndCacheMsgpack.lock_and_cache(hello: 'world', expires: 100) { count += 1 }
418
+ expect(count).to eq(1)
419
+ LockAndCacheMsgpack.lock_and_cache(hello: 'world', expires: 100) { count += 1 }
420
+ expect(count).to eq(1)
421
+ LockAndCacheMsgpack.lock_and_cache(hello: 'world', expires: 200) { count += 1 } # expires is being treated as part of cache key
422
+ expect(count).to eq(2)
423
+ end
424
+
425
+ it "correctly identifies options hash" do
426
+ count = 0
427
+ LockAndCacheMsgpack.lock_and_cache({ hello: 'world' }, expires: 1, ignored: rand) { count += 1 }
428
+ expect(count).to eq(1)
429
+ LockAndCacheMsgpack.lock_and_cache({ hello: 'world' }, expires: 1, ignored: rand) { count += 1 } # expires is not being treated as part of cache key
430
+ expect(count).to eq(1)
431
+ sleep 1.1
432
+ LockAndCacheMsgpack.lock_and_cache({ hello: 'world' }) { count += 1 }
433
+ expect(count).to eq(2)
434
+ end
435
+ end
436
+
437
+ describe "shorter expiry for null results" do
438
+ it "optionally caches null for less time" do
439
+ count = 0
440
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; nil }
441
+ expect(count).to eq(1)
442
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; nil }
443
+ expect(count).to eq(1)
444
+ sleep 1.1 # this is enough to expire
445
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; nil }
446
+ expect(count).to eq(2)
447
+ end
448
+
449
+ it "normally caches null for the same amount of time" do
450
+ count = 0
451
+ expect(LockAndCacheMsgpack.lock_and_cache('hello', expires: 1) { count += 1; nil }).to be_nil
452
+ expect(count).to eq(1)
453
+ expect(LockAndCacheMsgpack.lock_and_cache('hello', expires: 1) { count += 1; nil }).to be_nil
454
+ expect(count).to eq(1)
455
+ sleep 1.1
456
+ expect(LockAndCacheMsgpack.lock_and_cache('hello', expires: 1) { count += 1; nil }).to be_nil
457
+ expect(count).to eq(2)
458
+ end
459
+
460
+ it "caches non-null for normal time" do
461
+ count = 0
462
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
463
+ expect(count).to eq(1)
464
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
465
+ expect(count).to eq(1)
466
+ sleep 1.1
467
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
468
+ expect(count).to eq(1)
469
+ sleep 1
470
+ LockAndCacheMsgpack.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
471
+ expect(count).to eq(2)
472
+ end
473
+ end
474
+
475
+
476
+ end