lock_and_cache_msgpack 4.0.7.pre1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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