lock_and_cache 4.0.6 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +7 -0
- data/README.md +7 -6
- data/lib/lock_and_cache.rb +0 -7
- data/lib/lock_and_cache/action.rb +35 -12
- data/lib/lock_and_cache/version.rb +1 -1
- data/lock_and_cache.gemspec +0 -2
- data/spec/lock_and_cache_spec.rb +22 -0
- metadata +3 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 186a1eea8f6cc7f0d72ed2ccd5cc1c8f19f3c605
|
4
|
+
data.tar.gz: 80d956322883072ea8e23f58df3710bc0bc6571c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff3c3bde1ab6eddc04217ee3c64d1877d75f19c24155a110cae23d5019468cae2bf7fc58e695ab5627277c1087c49aaf18f2cf952b109c375eeeaf122738fabf
|
7
|
+
data.tar.gz: af5b4021a8d5e5e8ab6fbc7fe80464f9f6d0e2e1ddeeb5f7a97371406414000d464b574a0a01813f9a0b3d11bdb105c0730aa1fe8ffd9861bd9af6297f879378
|
data/CHANGELOG
CHANGED
data/README.md
CHANGED
@@ -27,9 +27,9 @@ end
|
|
27
27
|
|
28
28
|
## Sponsor
|
29
29
|
|
30
|
-
<p><a href="
|
30
|
+
<p><a href="https://www.faraday.io"><img src="https://s3.amazonaws.com/faraday-assets/files/img/logo.svg" alt="Faraday logo"/></a></p>
|
31
31
|
|
32
|
-
We use [`lock_and_cache`](https://github.com/seamusabshere/lock_and_cache) for [
|
32
|
+
We use [`lock_and_cache`](https://github.com/seamusabshere/lock_and_cache) for [B2C customer intelligence at Faraday](https://www.faraday.io).
|
33
33
|
|
34
34
|
## TOC
|
35
35
|
|
@@ -70,6 +70,8 @@ We use [`lock_and_cache`](https://github.com/seamusabshere/lock_and_cache) for [
|
|
70
70
|
|
71
71
|
As you can see, most caching libraries only take care of (1) and (4) (well, and (5) of course).
|
72
72
|
|
73
|
+
If an error is raised during calculation, that error is propagated to all waiters for 1 second.
|
74
|
+
|
73
75
|
## Practice
|
74
76
|
|
75
77
|
### Setup
|
@@ -82,9 +84,9 @@ It will use this redis for both locking and storing cached values.
|
|
82
84
|
|
83
85
|
### Locking
|
84
86
|
|
85
|
-
|
87
|
+
Just uses Redis naive locking with NX.
|
86
88
|
|
87
|
-
|
89
|
+
A 32-second heartbeat is used that will clear the lock if a process is killed.
|
88
90
|
|
89
91
|
### Caching
|
90
92
|
|
@@ -189,7 +191,7 @@ Most caching libraries don't do locking, meaning that >1 process can be calculat
|
|
189
191
|
|
190
192
|
### Heartbeat
|
191
193
|
|
192
|
-
If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats
|
194
|
+
If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats).
|
193
195
|
|
194
196
|
### Context mode
|
195
197
|
|
@@ -210,7 +212,6 @@ You can expire nil values with a different timeout (`nil_expires`) than other va
|
|
210
212
|
|
211
213
|
* [activesupport](https://rubygems.org/gems/activesupport) (come on, it's the bomb)
|
212
214
|
* [redis](https://github.com/redis/redis-rb)
|
213
|
-
* [redlock](https://github.com/leandromoreira/redlock-rb)
|
214
215
|
|
215
216
|
## Known issues
|
216
217
|
|
data/lib/lock_and_cache.rb
CHANGED
@@ -3,7 +3,6 @@ require 'timeout'
|
|
3
3
|
require 'digest/sha1'
|
4
4
|
require 'base64'
|
5
5
|
require 'redis'
|
6
|
-
require 'redlock'
|
7
6
|
require 'active_support'
|
8
7
|
require 'active_support/core_ext'
|
9
8
|
|
@@ -25,7 +24,6 @@ module LockAndCache
|
|
25
24
|
def LockAndCache.storage=(redis_connection)
|
26
25
|
raise "only redis for now" unless redis_connection.class.to_s == 'Redis'
|
27
26
|
@storage = redis_connection
|
28
|
-
@lock_manager = Redlock::Client.new [redis_connection], retry_count: 1
|
29
27
|
end
|
30
28
|
|
31
29
|
# @return [Redis] The redis connection used for lock and cached value storage
|
@@ -117,11 +115,6 @@ module LockAndCache
|
|
117
115
|
@heartbeat_expires || DEFAULT_HEARTBEAT_EXPIRES
|
118
116
|
end
|
119
117
|
|
120
|
-
# @private
|
121
|
-
def LockAndCache.lock_manager
|
122
|
-
@lock_manager
|
123
|
-
end
|
124
|
-
|
125
118
|
# Check if a method is locked on an object.
|
126
119
|
#
|
127
120
|
# @note Subject mode - this is expected to be called on an object whose class has LockAndCache mixed in. See also standalone mode.
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module LockAndCache
|
2
2
|
# @private
|
3
3
|
class Action
|
4
|
+
ERROR_MAGIC_KEY = :lock_and_cache_error
|
5
|
+
|
4
6
|
attr_reader :key
|
5
7
|
attr_reader :options
|
6
8
|
attr_reader :blk
|
@@ -34,6 +36,15 @@ module LockAndCache
|
|
34
36
|
@storage ||= LockAndCache.storage or raise("must set LockAndCache.storage=[Redis]")
|
35
37
|
end
|
36
38
|
|
39
|
+
def load_existing(existing)
|
40
|
+
v = ::Marshal.load(existing)
|
41
|
+
if v.is_a?(::Hash) and (founderr = v[ERROR_MAGIC_KEY])
|
42
|
+
raise "Another LockAndCache process raised #{founderr}"
|
43
|
+
else
|
44
|
+
v
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
37
48
|
def perform
|
38
49
|
max_lock_wait = options.fetch 'max_lock_wait', LockAndCache.max_lock_wait
|
39
50
|
heartbeat_expires = options.fetch('heartbeat_expires', LockAndCache.heartbeat_expires).to_f.ceil
|
@@ -41,23 +52,24 @@ module LockAndCache
|
|
41
52
|
heartbeat_frequency = (heartbeat_expires / 2).ceil
|
42
53
|
LockAndCache.logger.debug { "[lock_and_cache] A1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
43
54
|
if storage.exists(digest) and (existing = storage.get(digest)).is_a?(String)
|
44
|
-
return
|
55
|
+
return load_existing(existing)
|
45
56
|
end
|
46
57
|
LockAndCache.logger.debug { "[lock_and_cache] B1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
47
58
|
retval = nil
|
48
|
-
|
49
|
-
|
59
|
+
lock_secret = SecureRandom.hex 16
|
60
|
+
acquired = false
|
50
61
|
begin
|
51
62
|
Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do
|
52
|
-
until
|
63
|
+
until storage.set(lock_digest, lock_secret, nx: true, ex: heartbeat_expires)
|
53
64
|
LockAndCache.logger.debug { "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
54
65
|
sleep rand
|
55
66
|
end
|
67
|
+
acquired = true
|
56
68
|
end
|
57
69
|
LockAndCache.logger.debug { "[lock_and_cache] D1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
58
70
|
if storage.exists(digest) and (existing = storage.get(digest)).is_a?(String)
|
59
71
|
LockAndCache.logger.debug { "[lock_and_cache] E1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
60
|
-
retval =
|
72
|
+
retval = load_existing existing
|
61
73
|
end
|
62
74
|
unless retval
|
63
75
|
LockAndCache.logger.debug { "[lock_and_cache] F1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
@@ -70,28 +82,39 @@ module LockAndCache
|
|
70
82
|
sleep heartbeat_frequency
|
71
83
|
break if done
|
72
84
|
LockAndCache.logger.debug { "[lock_and_cache] heartbeat2 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
|
73
|
-
|
85
|
+
# FIXME use lua to check the value
|
86
|
+
raise "unexpectedly lost lock for #{key.debug}" unless storage.get(lock_digest) == lock_secret
|
87
|
+
storage.set lock_digest, lock_secret, xx: true, ex: heartbeat_expires
|
74
88
|
end
|
75
89
|
end
|
76
|
-
|
77
|
-
|
90
|
+
begin
|
91
|
+
retval = blk.call
|
92
|
+
retval.nil? ? set_nil : set_non_nil(retval)
|
93
|
+
rescue
|
94
|
+
set_error $!
|
95
|
+
raise
|
96
|
+
end
|
78
97
|
ensure
|
79
98
|
done = true
|
80
99
|
lock_extender.join if lock_extender.status.nil?
|
81
100
|
end
|
82
101
|
end
|
83
102
|
ensure
|
84
|
-
|
103
|
+
storage.del lock_digest if acquired
|
85
104
|
end
|
86
105
|
retval
|
87
106
|
end
|
88
107
|
|
108
|
+
def set_error(exception)
|
109
|
+
storage.set digest, ::Marshal.dump(ERROR_MAGIC_KEY => exception.message), ex: 1
|
110
|
+
end
|
111
|
+
|
89
112
|
NIL = Marshal.dump nil
|
90
113
|
def set_nil
|
91
114
|
if nil_expires
|
92
|
-
storage.
|
115
|
+
storage.set digest, NIL, ex: nil_expires
|
93
116
|
elsif expires
|
94
|
-
storage.
|
117
|
+
storage.set digest, NIL, ex: expires
|
95
118
|
else
|
96
119
|
storage.set digest, NIL
|
97
120
|
end
|
@@ -100,7 +123,7 @@ module LockAndCache
|
|
100
123
|
def set_non_nil(retval)
|
101
124
|
raise "expected not null #{retval.inspect}" if retval.nil?
|
102
125
|
if expires
|
103
|
-
storage.
|
126
|
+
storage.set digest, ::Marshal.dump(retval), ex: expires
|
104
127
|
else
|
105
128
|
storage.set digest, ::Marshal.dump(retval)
|
106
129
|
end
|
data/lock_and_cache.gemspec
CHANGED
@@ -20,8 +20,6 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_runtime_dependency 'activesupport'
|
22
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
23
|
|
26
24
|
spec.add_development_dependency 'pry'
|
27
25
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
data/spec/lock_and_cache_spec.rb
CHANGED
@@ -337,6 +337,28 @@ describe LockAndCache do
|
|
337
337
|
expect(count).to eq(1)
|
338
338
|
end
|
339
339
|
|
340
|
+
it 'really caches' do
|
341
|
+
expect(LockAndCache.lock_and_cache('hello') { :red }).to eq(:red)
|
342
|
+
expect(LockAndCache.lock_and_cache('hello') { raise(Exception.new("stop")) }).to eq(:red)
|
343
|
+
end
|
344
|
+
|
345
|
+
it 'caches errors (briefly)' do
|
346
|
+
count = 0
|
347
|
+
expect {
|
348
|
+
LockAndCache.lock_and_cache('hello') { count += 1; raise("stop") }
|
349
|
+
}.to raise_error(/stop/)
|
350
|
+
expect(count).to eq(1)
|
351
|
+
expect {
|
352
|
+
LockAndCache.lock_and_cache('hello') { count += 1; raise("no no not me") }
|
353
|
+
}.to raise_error(/LockAndCache.*stop/)
|
354
|
+
expect(count).to eq(1)
|
355
|
+
sleep 1
|
356
|
+
expect {
|
357
|
+
LockAndCache.lock_and_cache('hello') { count += 1; raise("retrying") }
|
358
|
+
}.to raise_error(/retrying/)
|
359
|
+
expect(count).to eq(2)
|
360
|
+
end
|
361
|
+
|
340
362
|
it "can be queried for cached?" do
|
341
363
|
expect(LockAndCache.cached?('hello')).to be_falsy
|
342
364
|
LockAndCache.lock_and_cache('hello') { nil }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lock_and_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seamus Abshere
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-09-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -38,20 +38,6 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: redlock
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 0.1.3
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 0.1.3
|
55
41
|
- !ruby/object:Gem::Dependency
|
56
42
|
name: pry
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -196,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
196
182
|
version: '0'
|
197
183
|
requirements: []
|
198
184
|
rubyforge_project:
|
199
|
-
rubygems_version: 2.6.
|
185
|
+
rubygems_version: 2.6.13
|
200
186
|
signing_key:
|
201
187
|
specification_version: 4
|
202
188
|
summary: Lock and cache methods.
|