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 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.