redis_locks 0.0.3 → 0.1.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: 9c5d583e881ddb03eb3a27a20ec98600b66d756d
4
- data.tar.gz: c2553b07d1bc3e33918ec0084410f4adeffc666d
3
+ metadata.gz: 9d50bb61032babb4eb127f5f4cf8a4b8804d84c0
4
+ data.tar.gz: 658610bbd101c4bce5004415c08517a44b23dd35
5
5
  SHA512:
6
- metadata.gz: 04ba8f31d89668b2c872de19c14ee8896288db1f14de23301fa2b74bd9a27ea2305055b6f9105f4e3cabb304c3739c39dcf8a430a96d8a1661f96820636b9531
7
- data.tar.gz: 240bdc28a11410e5a894687e28910d37d14f872fbfc2d762b56b9b80f8235134554d58151c52eb6186c97d0ac5afba5c3c36d31d84ec88c8881aa06b86f9e1fc
6
+ metadata.gz: b512b7584dc46896361d1c622a1c183ba6efc3ec5c22144e1162e614f47bc0bbe0c7e9e93bcb8323a1103982ceb26cd97df538d0f375b92a1b66eef13f675a60
7
+ data.tar.gz: 461ba90c3336dc56296053475a24141a1f0454e6cbb96cfe2d16c590eee7b7080147559a369a5713020f2af334495b7c7bb522737358930bffcc90242cca7619
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis_locks (0.0.3)
4
+ redis_locks (0.1.0)
5
+ connection_pool
5
6
  redis
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
11
  coderay (1.1.1)
12
+ connection_pool (2.2.0)
11
13
  diff-lcs (1.2.5)
12
14
  method_source (0.8.2)
13
15
  pry (0.10.3)
data/README.md CHANGED
@@ -92,3 +92,13 @@ A [token-bucket](https://en.wikipedia.org/wiki/Token_bucket) rate limiter implem
92
92
 
93
93
  limiter.take # true
94
94
  ```
95
+
96
+ # Configuration
97
+
98
+ We use a connection pool, because a connection shared among multiple threads
99
+ will result in contention when using blocking Redis operations.
100
+
101
+ `RedisLocks.redis=` and the `redis:` option in the initializer for each utility
102
+ can each be passed either a plain Redis object or an object responding to `with`
103
+ (which will be expected to accept a block and yield a Redis connection, e.g.
104
+ an instance of [ConnectionPool](https://github.com/mperham/connection_pool)).
data/lib/redis_locks.rb CHANGED
@@ -4,11 +4,12 @@ require 'redis_locks/evalsha_or_eval'
4
4
  require 'redis_locks/mutex'
5
5
  require 'redis_locks/semaphore'
6
6
  require 'redis_locks/token_bucket'
7
+ require 'redis_locks/connections'
7
8
 
8
9
  module RedisLocks
9
10
 
10
11
  def self.redis=(redis)
11
- @redis = redis
12
+ @redis = Connections.ensure_pool(redis)
12
13
  end
13
14
 
14
15
  def self.redis
@@ -0,0 +1,15 @@
1
+ require 'connection_pool'
2
+
3
+ module RedisLocks
4
+ module Connections
5
+
6
+ def self.ensure_pool(redis)
7
+ if redis.respond_to?(:with)
8
+ redis
9
+ else
10
+ ConnectionPool.new { redis.dup }
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -2,11 +2,11 @@ module RedisLocks
2
2
 
3
3
  # This ensures that each Lua script is evaluated at most once; after it has
4
4
  # been evaluated, we will be able to call it just by passing its digest.
5
- def self.evalsha_or_eval(redis:, script:, digest:, keys: [], args: [])
6
- redis.evalsha digest, keys, args
5
+ def self.evalsha_or_eval(conn:, script:, digest:, keys: [], args: [])
6
+ conn.evalsha digest, keys, args
7
7
  rescue Redis::CommandError => e
8
8
  if e.message.start_with?('NOSCRIPT')
9
- redis.eval script, keys, args
9
+ conn.eval script, keys, args
10
10
  else
11
11
  raise
12
12
  end
@@ -11,7 +11,7 @@ module RedisLocks
11
11
 
12
12
  def initialize(key, expires_in: 86400, redis: RedisLocks.redis)
13
13
  @key = "#{NAMESPACE}:#{key}"
14
- @redis = redis
14
+ @redis = Connections.ensure_pool(redis)
15
15
  @expires_in = expires_in.to_i
16
16
 
17
17
  raise ArgumentError.new("Invalid expires_in: #{expires_in}") unless expires_in > 0
@@ -27,16 +27,18 @@ module RedisLocks
27
27
  expires_at = now + @expires_in
28
28
  end
29
29
 
30
- if @redis.setnx(@key, expires_at)
31
- @redis.expire(@key, expires_at - now)
32
- @expires_at = expires_at
33
- locked = true
34
- else # it was locked
35
- if (old_value = @redis.get(@key)).to_i <= now
36
- # lock has expired
37
- if @redis.getset(@key, expires_at) == old_value
38
- @expires_at = expires_at
39
- locked = true
30
+ @redis.with do |conn|
31
+ if conn.setnx(@key, expires_at)
32
+ conn.expire(@key, expires_at - now)
33
+ @expires_at = expires_at
34
+ locked = true
35
+ else # it was locked
36
+ if (old_value = conn.get(@key)).to_i <= now
37
+ # lock has expired
38
+ if conn.getset(@key, expires_at) == old_value
39
+ @expires_at = expires_at
40
+ locked = true
41
+ end
40
42
  end
41
43
  end
42
44
  end
@@ -58,7 +60,7 @@ module RedisLocks
58
60
  # To prevent deleting a lock acquired from another process, only delete
59
61
  # the key if it's still valid, and will be for another 2 seconds
60
62
  if Time.now.utc.to_i - 2 < @expires_at
61
- @redis.del(@key)
63
+ @redis.with { |conn| conn.del(@key) }
62
64
  end
63
65
 
64
66
  @expires_at = nil
@@ -66,7 +66,7 @@ module RedisLocks
66
66
  @key = key
67
67
  @resource_count = resources.to_i
68
68
  @stale_client_timeout = stale_client_timeout.to_f
69
- @redis = redis
69
+ @redis = Connections.ensure_pool(redis)
70
70
  @tokens = []
71
71
 
72
72
  raise ArgumentError.new("Lock key is required") if @key.nil? || @key.empty?
@@ -76,8 +76,11 @@ module RedisLocks
76
76
 
77
77
  # Forcefully clear the lock. Be careful!
78
78
  def delete!
79
- @redis.del(available_key)
80
- @redis.del(grabbed_key)
79
+ @redis.with do |conn|
80
+ conn.del(available_key)
81
+ conn.del(grabbed_key)
82
+ end
83
+
81
84
  @tokens = []
82
85
  end
83
86
 
@@ -101,18 +104,21 @@ module RedisLocks
101
104
  def lock(timeout: nil, &block)
102
105
  ensure_exists_and_release_stale_locks!
103
106
 
104
- success =
107
+ success = @redis.with do |conn|
105
108
  if timeout
106
- !@redis.blpop(available_key, timeout.to_i).nil?
109
+ !conn.blpop(available_key, timeout.to_i).nil?
107
110
  else
108
- !@redis.lpop(available_key).nil?
111
+ !conn.lpop(available_key).nil?
109
112
  end
113
+ end
110
114
 
111
115
  return false unless success
112
116
 
113
117
  token = SecureRandom.hex(16)
114
118
  @tokens.push(token)
115
- @redis.zadd(grabbed_key, epoch_f, token)
119
+ @redis.with do |conn|
120
+ conn.zadd(grabbed_key, epoch_f(conn), token)
121
+ end
116
122
 
117
123
  return_or_yield(token, &block)
118
124
  end
@@ -141,9 +147,13 @@ module RedisLocks
141
147
  def unlock(token = @tokens.pop)
142
148
  return unless token
143
149
 
144
- removed = @redis.zrem grabbed_key, token
145
- if removed
146
- @redis.lpush available_key, 1
150
+ removed = false
151
+
152
+ @redis.with do |conn|
153
+ removed = conn.zrem grabbed_key, token
154
+ if removed
155
+ conn.lpush available_key, 1
156
+ end
147
157
  end
148
158
 
149
159
  removed
@@ -165,13 +175,15 @@ module RedisLocks
165
175
  end
166
176
 
167
177
  def ensure_exists_and_release_stale_locks!
168
- RedisLocks.evalsha_or_eval(
169
- redis: @redis,
170
- script: SETUP_SCRIPT,
171
- digest: SETUP_DIGEST,
172
- keys: [available_key, grabbed_key],
173
- args: [@resource_count, stale_before]
174
- )
178
+ @redis.with do |conn|
179
+ RedisLocks.evalsha_or_eval(
180
+ conn: conn,
181
+ script: SETUP_SCRIPT,
182
+ digest: SETUP_DIGEST,
183
+ keys: [available_key, grabbed_key],
184
+ args: [@resource_count, stale_before(conn)]
185
+ )
186
+ end
175
187
  end
176
188
 
177
189
  def namespaced_key(variable)
@@ -186,12 +198,12 @@ module RedisLocks
186
198
  @grabbed_key ||= namespaced_key('GRABBED')
187
199
  end
188
200
 
189
- def stale_before
190
- epoch_f - @stale_client_timeout
201
+ def stale_before(conn)
202
+ epoch_f(conn) - @stale_client_timeout
191
203
  end
192
204
 
193
- def epoch_f
194
- epoch_i, microseconds = @redis.time
205
+ def epoch_f(conn)
206
+ epoch_i, microseconds = conn.time
195
207
  epoch_i + microseconds.to_f / 1_000_000
196
208
  end
197
209
 
@@ -45,19 +45,20 @@ module RedisLocks
45
45
  @key = "#{NAMESPACE}:#{key}".freeze
46
46
  @rps = number.to_f / period.to_i
47
47
  @burst = number.to_i
48
- @redis = redis
48
+ @redis = Connections.ensure_pool(redis)
49
49
  end
50
50
 
51
51
  def take
52
- epoch_i, microseconds = @redis.time
53
- epoch_f = epoch_i + (microseconds.to_f/1_000_000)
54
- took = RedisLocks.evalsha_or_eval(
55
- redis: @redis,
56
- script: SCRIPT,
57
- digest: DIGEST,
58
- keys: [@key],
59
- args: [epoch_f, @rps, @burst]
60
- )
52
+ took = @redis.with do |conn|
53
+ RedisLocks.evalsha_or_eval(
54
+ conn: conn,
55
+ script: SCRIPT,
56
+ digest: DIGEST,
57
+ keys: [@key],
58
+ args: [epoch_f(conn), @rps, @burst]
59
+ )
60
+ end
61
+
61
62
  took == 1
62
63
  end
63
64
 
@@ -65,5 +66,12 @@ module RedisLocks
65
66
  raise RateLimitExceeded.new(@key, @rps) unless take
66
67
  end
67
68
 
69
+ private
70
+
71
+ def epoch_f(conn)
72
+ epoch_i, microseconds = conn.time
73
+ epoch_i + (microseconds.to_f/1_000_000)
74
+ end
75
+
68
76
  end
69
77
  end
@@ -1,3 +1,3 @@
1
1
  module RedisLocks
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1.0'
3
3
  end
data/redis_locks.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  s.add_dependency "redis"
23
+ s.add_dependency "connection_pool"
23
24
 
24
25
  s.add_development_dependency "rspec"
25
26
  s.add_development_dependency "rake"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Academia.edu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-16 00:00:00.000000000 Z
11
+ date: 2016-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rspec
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -67,6 +81,7 @@ files:
67
81
  - README.md
68
82
  - Rakefile
69
83
  - lib/redis_locks.rb
84
+ - lib/redis_locks/connections.rb
70
85
  - lib/redis_locks/evalsha_or_eval.rb
71
86
  - lib/redis_locks/mutex.rb
72
87
  - lib/redis_locks/resource_unavailable.rb