lock_and_cache 4.0.6 → 5.0.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.
- 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.
|