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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 486140a35280062778f6f70680406c74798a8a68
4
- data.tar.gz: 262bc8de8f1990818ed04b4e964fe46c4366256a
3
+ metadata.gz: 186a1eea8f6cc7f0d72ed2ccd5cc1c8f19f3c605
4
+ data.tar.gz: 80d956322883072ea8e23f58df3710bc0bc6571c
5
5
  SHA512:
6
- metadata.gz: caf0ac825c8b69ce6d15f4af75aa1f6a7a8e0902184c80dafb04a0de1b9dcebf05ec6018b75c27d4b845cc7d7f5e00f3ca48c7daee1c6a5edb90078561638058
7
- data.tar.gz: 77ed0de21b2fafbc9c36b50ce5b390e1d8090c4c1956c079f83ef6fd6a26d3d1db7c71ead492c06328d9c68808706807f377de1ec92651cea8d3fe08e3a521b0
6
+ metadata.gz: ff3c3bde1ab6eddc04217ee3c64d1877d75f19c24155a110cae23d5019468cae2bf7fc58e695ab5627277c1087c49aaf18f2cf952b109c375eeeaf122738fabf
7
+ data.tar.gz: af5b4021a8d5e5e8ab6fbc7fe80464f9f6d0e2e1ddeeb5f7a97371406414000d464b574a0a01813f9a0b3d11bdb105c0730aa1fe8ffd9861bd9af6297f879378
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ 5.0.0
2
+
3
+ * Enhancements / breaking changes
4
+
5
+ * Propagate errors to all waiters up to 1 second after the error
6
+ * Stop using redlock, just use plain single-node Redis locking
7
+
1
8
  4.0.6
2
9
 
3
10
  * ?
data/README.md CHANGED
@@ -27,9 +27,9 @@ end
27
27
 
28
28
  ## Sponsor
29
29
 
30
- <p><a href="http://faraday.io"><img src="http://cdn2.hubspot.net/hubfs/515497/img/logo.svg" alt="Faraday logo"/></a></p>
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 [data-driven marketing at Faraday](http://faraday.io).
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
- Based on [antirez's Redlock algorithm](http://redis.io/topics/distlock).
87
+ Just uses Redis naive locking with NX.
86
88
 
87
- Above and beyond Redlock, a 32-second heartbeat is used that will clear the lock if a process is killed. This is implemented using lock extensions.
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 and redlock extends).
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
 
@@ -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 ::Marshal.load(existing)
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
- lock_manager = LockAndCache.lock_manager
49
- lock_info = nil
59
+ lock_secret = SecureRandom.hex 16
60
+ acquired = false
50
61
  begin
51
62
  Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do
52
- until lock_info = lock_manager.lock(lock_digest, heartbeat_expires*1000)
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 = ::Marshal.load existing
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
- lock_manager.lock lock_digest, heartbeat_expires*1000, extend: lock_info
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
- retval = blk.call
77
- retval.nil? ? set_nil : set_non_nil(retval)
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
- lock_manager.unlock lock_info if lock_info
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.setex digest, nil_expires, NIL
115
+ storage.set digest, NIL, ex: nil_expires
93
116
  elsif expires
94
- storage.setex digest, expires, NIL
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.setex digest, expires, ::Marshal.dump(retval)
126
+ storage.set digest, ::Marshal.dump(retval), ex: expires
104
127
  else
105
128
  storage.set digest, ::Marshal.dump(retval)
106
129
  end
@@ -1,3 +1,3 @@
1
1
  module LockAndCache
2
- VERSION = '4.0.6'
2
+ VERSION = '5.0.0'
3
3
  end
@@ -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'
@@ -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.0.6
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: 2017-04-26 00:00:00.000000000 Z
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.8
185
+ rubygems_version: 2.6.13
200
186
  signing_key:
201
187
  specification_version: 4
202
188
  summary: Lock and cache methods.