redis_locks 0.0.3 → 0.1.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: 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