redis_queued_locks 0.0.7 → 0.0.9
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.md +14 -0
- data/README.md +109 -14
- data/lib/redis_queued_locks/acquier/try.rb +7 -1
- data/lib/redis_queued_locks/acquier.rb +135 -3
- data/lib/redis_queued_locks/client.rb +62 -23
- data/lib/redis_queued_locks/resource.rb +33 -2
- data/lib/redis_queued_locks/version.rb +2 -2
- data/lib/redis_queued_locks.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 463b0613174c7a3806aef19ce962bbe5ed91d342796bec84ea47b2aa79cf61fb
|
4
|
+
data.tar.gz: 4d483db40896d77049e3e6d324a206606225c9c8e8b592072b1f501563fb7ec8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f829f8afc264f8a44d0edd1469c5ada76c7257f15ef6945c3f154fe2908b29d5ace4ce0c11365a30af1cdbb6d3b4aeec30227b84500da65b720f22247ed96053
|
7
|
+
data.tar.gz: 1eee0e4c2a665b57ee6d2c2c2d6488c065bbcdd586cfe06f2890a6b412a3a009675dd26e39383f2fad54434288d4c3c7660fba2faaa3013da98ab27c484b08b0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.9] - 2024-02-27
|
4
|
+
### Changed
|
5
|
+
- The lock acquier identifier (`acq_id`) now includes the fiber id, the ractor id and an unique per-process
|
6
|
+
10 byte string. It is added in order to prevent collisions between different processes/pods
|
7
|
+
that will have the same procjet id / thread id identifiers (cuz it is an object_id integers) that can lead
|
8
|
+
to the same position with the same `acq_id` for different processes/pods in the lock request queue.
|
9
|
+
|
10
|
+
## [0.0.8] - 2024-02-27
|
11
|
+
### Added
|
12
|
+
- `RedisQueuedLock::Client#locked?`
|
13
|
+
- `RedisQueuedLock::Client#queued?`
|
14
|
+
- `RedisQueuedLock::Client#lock_info`
|
15
|
+
- `RedisQueuedLock::Client#queue_info`
|
16
|
+
|
3
17
|
## [0.0.7] - 2024-02-27
|
4
18
|
### Changed
|
5
19
|
- Minor documentation updates;
|
data/README.md
CHANGED
@@ -15,11 +15,15 @@ Each lock request is put into a request queue and processed in order of their pr
|
|
15
15
|
- [Usage](#usage)
|
16
16
|
- [lock](#lock---obtain-a-lock)
|
17
17
|
- [lock!](#lock---exeptional-lock-obtaining)
|
18
|
+
- [lock_info](#lock_info)
|
19
|
+
- [queue_info](#queue_info)
|
20
|
+
- [locked?](#locked)
|
21
|
+
- [queued?](#queued)
|
18
22
|
- [unlock](#unlock---release-a-lock)
|
19
23
|
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
20
24
|
- [Instrumentation](#instrumentation)
|
21
25
|
- [Instrumentation Events](#instrumentation-events)
|
22
|
-
- [
|
26
|
+
- [Roadmap](#roadmap)
|
23
27
|
- [Contributing](#contributing)
|
24
28
|
- [License](#license)
|
25
29
|
- [Authors](#authors)
|
@@ -56,7 +60,7 @@ require 'redis_queued_locks'
|
|
56
60
|
require 'redis_queued_locks'
|
57
61
|
|
58
62
|
# Step 1: initialize RedisClient instance
|
59
|
-
redis_clinet = RedisClient.config.new_pool # NOTE: provide your
|
63
|
+
redis_clinet = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
|
60
64
|
|
61
65
|
# Step 2: initialize RedisQueuedLock::Client instance
|
62
66
|
rq_lock_client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
@@ -115,6 +119,13 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
115
119
|
# - payload: <hash> requried;
|
116
120
|
# - disabled by default via VoidNotifier;
|
117
121
|
config.instrumenter = RedisQueuedLocks::Instrument::ActiveSupport
|
122
|
+
|
123
|
+
# (default: -> { RedisQueuedLocks::Resource.calc_uniq_identity })
|
124
|
+
# - uniqude idenfitier that is uniq per process/pod;
|
125
|
+
# - prevents potential lock-acquirement collisions bettween different process/pods
|
126
|
+
# that have identical process_id/thread_id/fiber_id/ractor_id (identivcal acquier ids);
|
127
|
+
# - it is calculated once per `RedisQueudLocks::Client` instance;
|
128
|
+
config.uniq_identifier = -> { RedisQueuedLocks::Resource.calc_uniq_identity }
|
118
129
|
end
|
119
130
|
```
|
120
131
|
|
@@ -124,6 +135,10 @@ end
|
|
124
135
|
|
125
136
|
- [lock](#lock---obtain-a-lock)
|
126
137
|
- [lock!](#lock---exeptional-lock-obtaining)
|
138
|
+
- [lock_info](#lock_info)
|
139
|
+
- [queue_info](#queue_info)
|
140
|
+
- [locked?](#locked)
|
141
|
+
- [queued?](#queued)
|
127
142
|
- [unlock](#unlock---release-a-lock)
|
128
143
|
- [clear_locks](#clear_locks---release-all-locks-and-lock-queues)
|
129
144
|
|
@@ -134,8 +149,6 @@ end
|
|
134
149
|
```ruby
|
135
150
|
def lock(
|
136
151
|
lock_name,
|
137
|
-
process_id: RedisQueuedLocks::Resource.get_process_id,
|
138
|
-
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
139
152
|
ttl: config[:default_lock_ttl],
|
140
153
|
queue_ttl: config[:default_queue_ttl],
|
141
154
|
timeout: config[:try_to_lock_timeout],
|
@@ -143,16 +156,13 @@ def lock(
|
|
143
156
|
retry_delay: config[:retry_delay],
|
144
157
|
retry_jitter: config[:retry_jitter],
|
145
158
|
raise_errors: false,
|
159
|
+
identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config[:uniq_identifier] proc;
|
146
160
|
&block
|
147
161
|
)
|
148
162
|
```
|
149
163
|
|
150
164
|
- `lock_name` - `[String]`
|
151
165
|
- Lock name to be obtained.
|
152
|
-
- `process_id` - `[Integer,String]`
|
153
|
-
- The process that want to acquire the lock.
|
154
|
-
- `thread_id` - `[Integer,String]`
|
155
|
-
- The process's thread that want to acquire the lock.
|
156
166
|
- `ttl` [Integer]
|
157
167
|
- Lock's time to live (in milliseconds).
|
158
168
|
- `queue_ttl` - `[Integer]`
|
@@ -169,6 +179,12 @@ def lock(
|
|
169
179
|
- See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
170
180
|
- `raise_errors` - `[Boolean]`
|
171
181
|
- Raise errors on library-related limits such as timeout or retry count limit.
|
182
|
+
- `identity` - `[String]`
|
183
|
+
- An unique string that is unique per `RedisQueuedLock::Client` instance. Resolves the
|
184
|
+
collisions between the same process_id/thread_id/fiber_id/ractor_id identifiers on different
|
185
|
+
pods or/and nodes of your application;
|
186
|
+
- It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
|
187
|
+
ivar (accessed via `uniq_dentity` accessor method);
|
172
188
|
- `block` - `[Block]`
|
173
189
|
- A block of code that should be executed after the successfully acquired lock.
|
174
190
|
- If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
|
@@ -206,14 +222,13 @@ Return value:
|
|
206
222
|
```ruby
|
207
223
|
def lock!(
|
208
224
|
lock_name,
|
209
|
-
process_id: RedisQueuedLocks::Resource.get_process_id,
|
210
|
-
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
211
225
|
ttl: config[:default_lock_ttl],
|
212
226
|
queue_ttl: config[:default_queue_ttl],
|
213
227
|
timeout: config[:default_timeout],
|
214
228
|
retry_count: config[:retry_count],
|
215
229
|
retry_delay: config[:retry_delay],
|
216
230
|
retry_jitter: config[:retry_jitter],
|
231
|
+
identity: uniq_identity,
|
217
232
|
&block
|
218
233
|
)
|
219
234
|
```
|
@@ -222,6 +237,83 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
|
|
222
237
|
|
223
238
|
---
|
224
239
|
|
240
|
+
#### #lock_info
|
241
|
+
|
242
|
+
- get the lock information;
|
243
|
+
- returns `nil` if lock does not exist;
|
244
|
+
- lock data (`Hash<Symbol,String|Integer>`):
|
245
|
+
- `lock_key` - `string` - lock key in redis;
|
246
|
+
- `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
|
247
|
+
- `ts` - `integer`/`epoch` - the time lock was obtained;
|
248
|
+
- `init_ttl` - `integer` - (milliseconds) initial lock key ttl;
|
249
|
+
- `rem_ttl` - `integer` - (milliseconds) remaining lock key ttl;
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
rql.lock_info("your_lock_name")
|
253
|
+
|
254
|
+
# =>
|
255
|
+
{
|
256
|
+
lock_key: "rql:lock:your_lock_name",
|
257
|
+
acq_id: "rql:acq:123/456",
|
258
|
+
ts: 123456789,
|
259
|
+
ini_ttl: 123456789,
|
260
|
+
rem_ttl: 123456789
|
261
|
+
}
|
262
|
+
```
|
263
|
+
|
264
|
+
---
|
265
|
+
|
266
|
+
#### #queue_info
|
267
|
+
|
268
|
+
- get the lock queue information;
|
269
|
+
- queue represents the ordered set of lock key reqests:
|
270
|
+
- set is ordered by score in ASC manner (inside the Redis Set);
|
271
|
+
- score is represented as a timestamp when the lock request was made;
|
272
|
+
- represents the acquier identifier and their score as an array of hashes;
|
273
|
+
- returns `nil` if lock queue does not exist;
|
274
|
+
- lock queue data (`Hash<Symbol,String|Array<Hash<Symbol,String|Numeric>>`):
|
275
|
+
- `lock_queue` - `string` - lock queue key in redis;
|
276
|
+
- `queue` - `array` - an array of lock requests (array of hashes):
|
277
|
+
- `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
|
278
|
+
- `score` - `float`/`epoch` - time when the lock request was made (epoch);
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
rql.queue_info("your_lock_name")
|
282
|
+
|
283
|
+
# =>
|
284
|
+
{
|
285
|
+
lock_queue: "rql:lock_queue:your_lock_name",
|
286
|
+
queue: [
|
287
|
+
{ acq_id: "rql:acq:123/456/567/678/fa76df9cc2", score: 1},
|
288
|
+
{ acq_id: "rql:acq:123/567/456/679/c7bfcaf4f9", score: 2},
|
289
|
+
{ acq_id: "rql:acq:555/329/523/127/7329553b11", score: 3},
|
290
|
+
# ...etc
|
291
|
+
]
|
292
|
+
}
|
293
|
+
```
|
294
|
+
|
295
|
+
---
|
296
|
+
|
297
|
+
#### #locked?
|
298
|
+
|
299
|
+
- is the lock obtaied or not?
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
rql.locked?("your_lock_name") # => true/false
|
303
|
+
```
|
304
|
+
|
305
|
+
---
|
306
|
+
|
307
|
+
#### #queued?
|
308
|
+
|
309
|
+
- is the lock queued for obtain / has requests for obtain?
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
rql.queued?("your_lock_name") # => true/false
|
313
|
+
```
|
314
|
+
|
315
|
+
---
|
316
|
+
|
225
317
|
#### #unlock - release a lock
|
226
318
|
|
227
319
|
- release the concrete lock with lock request queue;
|
@@ -341,11 +433,14 @@ Detalized event semantics and payload structure:
|
|
341
433
|
|
342
434
|
---
|
343
435
|
|
344
|
-
##
|
436
|
+
## Roadmap
|
345
437
|
|
346
|
-
-
|
347
|
-
-
|
348
|
-
-
|
438
|
+
- **Major**
|
439
|
+
- Semantic Error objects for unexpected Redis errors;
|
440
|
+
- `100%` test coverage;
|
441
|
+
- **Minor**
|
442
|
+
- `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
|
443
|
+
- better code stylization and interesting refactorings;
|
349
444
|
|
350
445
|
---
|
351
446
|
|
@@ -95,7 +95,13 @@ module RedisQueuedLocks::Acquier::Try
|
|
95
95
|
)
|
96
96
|
|
97
97
|
# Step 6.2: acquire a lock and store an info about the acquier
|
98
|
-
transact.call(
|
98
|
+
transact.call(
|
99
|
+
'HSET',
|
100
|
+
lock_key,
|
101
|
+
'acq_id', acquier_id,
|
102
|
+
'ts', (timestamp = Time.now.to_i),
|
103
|
+
'ini_ttl', ttl
|
104
|
+
)
|
99
105
|
|
100
106
|
# Step 6.3: set the lock expiration time in order to prevent "infinite locks"
|
101
107
|
transact.call('PEXPIRE', lock_key, ttl) # NOTE: in seconds
|
@@ -34,9 +34,13 @@ module RedisQueuedLocks::Acquier
|
|
34
34
|
# @param lock_name [String]
|
35
35
|
# Lock name to be acquier.
|
36
36
|
# @option process_id [Integer,String]
|
37
|
-
# The process that want to acquire
|
37
|
+
# The process that want to acquire a lock.
|
38
38
|
# @option thread_id [Integer,String]
|
39
|
-
# The process's thread that want to acquire
|
39
|
+
# The process's thread that want to acquire a lock.
|
40
|
+
# @option fiber_id [Integer,String]
|
41
|
+
# A current fiber that want to acquire a lock.
|
42
|
+
# @option ractor_id [Integer,String]
|
43
|
+
# The current ractor that want to acquire a lock.
|
40
44
|
# @option ttl [Integer,NilClass]
|
41
45
|
# Lock's time to live (in milliseconds). Nil means "without timeout".
|
42
46
|
# @option queue_ttl [Integer]
|
@@ -53,6 +57,10 @@ module RedisQueuedLocks::Acquier
|
|
53
57
|
# Raise errors on exceptional cases.
|
54
58
|
# @option instrumenter [#notify]
|
55
59
|
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
60
|
+
# @option identity [String]
|
61
|
+
# Unique acquire identifier that is also should be unique between processes and pods
|
62
|
+
# on different machines. By default the uniq identity string is
|
63
|
+
# represented as 10 bytes hexstr.
|
56
64
|
# @param [Block]
|
57
65
|
# A block of code that should be executed after the successfully acquired lock.
|
58
66
|
# @return [Hash<Symbol,Any>]
|
@@ -66,6 +74,8 @@ module RedisQueuedLocks::Acquier
|
|
66
74
|
lock_name,
|
67
75
|
process_id:,
|
68
76
|
thread_id:,
|
77
|
+
fiber_id:,
|
78
|
+
ractor_id:,
|
69
79
|
ttl:,
|
70
80
|
queue_ttl:,
|
71
81
|
timeout:,
|
@@ -74,10 +84,17 @@ module RedisQueuedLocks::Acquier
|
|
74
84
|
retry_jitter:,
|
75
85
|
raise_errors:,
|
76
86
|
instrumenter:,
|
87
|
+
identity:,
|
77
88
|
&block
|
78
89
|
)
|
79
90
|
# Step 1: prepare lock requirements (generate lock name, calc lock ttl, etc).
|
80
|
-
acquier_id = RedisQueuedLocks::Resource.acquier_identifier(
|
91
|
+
acquier_id = RedisQueuedLocks::Resource.acquier_identifier(
|
92
|
+
process_id,
|
93
|
+
thread_id,
|
94
|
+
fiber_id,
|
95
|
+
ractor_id,
|
96
|
+
identity
|
97
|
+
)
|
81
98
|
lock_ttl = ttl + REDIS_EXPIRATION_DEVIATION
|
82
99
|
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
83
100
|
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
@@ -271,6 +288,121 @@ module RedisQueuedLocks::Acquier
|
|
271
288
|
{ ok: true, result: { rel_key_cnt: result[:rel_keys], rel_time: rel_time } }
|
272
289
|
end
|
273
290
|
|
291
|
+
# @param redis_client [RedisClient]
|
292
|
+
# @param lock_name [String]
|
293
|
+
# @return [Boolean]
|
294
|
+
#
|
295
|
+
# @api private
|
296
|
+
# @since 0.1.0
|
297
|
+
def locked?(redis_client, lock_name)
|
298
|
+
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
299
|
+
redis_client.call('EXISTS', lock_key) == 1
|
300
|
+
end
|
301
|
+
|
302
|
+
# @param redis_client [RedisClient]
|
303
|
+
# @param lock_name [String]
|
304
|
+
# @return [Boolean]
|
305
|
+
#
|
306
|
+
# @api private
|
307
|
+
# @since 0.1.0
|
308
|
+
def queued?(redis_client, lock_name)
|
309
|
+
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
310
|
+
redis_client.call('EXISTS', lock_key_queue) == 1
|
311
|
+
end
|
312
|
+
|
313
|
+
# @param redis_client [RedisClient]
|
314
|
+
# @param lock_name [String]
|
315
|
+
# @return [Hash<Symbol,String|Numeric>,NilClass]
|
316
|
+
# - `nil` is returned when lock key does not exist or expired;
|
317
|
+
# - result format: {
|
318
|
+
# lock_key: "rql:lock:your_lockname", # acquired lock key
|
319
|
+
# acq_id: "rql:acq:process_id/thread_id", # lock acquier identifier
|
320
|
+
# ts: 123456789, # <locked at> time stamp (epoch)
|
321
|
+
# ini_ttl: 123456789, # initial lock key ttl (milliseconds),
|
322
|
+
# rem_ttl: 123456789, # remaining lock key ttl (milliseconds)
|
323
|
+
# }
|
324
|
+
#
|
325
|
+
# @api private
|
326
|
+
# @since 0.1.0
|
327
|
+
def lock_info(redis_client, lock_name)
|
328
|
+
lock_key = RedisQueuedLocks::Resource.prepare_lock_key(lock_name)
|
329
|
+
|
330
|
+
result = redis_client.multi(watch: [lock_key]) do |transact|
|
331
|
+
transact.call('HGETALL', lock_key)
|
332
|
+
transact.call('PTTL', lock_key)
|
333
|
+
end
|
334
|
+
|
335
|
+
if result == nil
|
336
|
+
# NOTE:
|
337
|
+
# - nil result means that during transaction invocation the lock is changed (CAS):
|
338
|
+
# - lock is expired;
|
339
|
+
# - lock is released;
|
340
|
+
# - lock is expired + re-obtained;
|
341
|
+
nil
|
342
|
+
else
|
343
|
+
hget_cmd_res = result[0]
|
344
|
+
pttl_cmd_res = result[1]
|
345
|
+
|
346
|
+
if hget_cmd_res == {} || pttl_cmd_res == -2 # NOTE: key does not exist
|
347
|
+
nil
|
348
|
+
else
|
349
|
+
# NOTE: the result of MULTI-command is an array of results of each internal command
|
350
|
+
# - result[0] (HGETALL) (Hash<String,String>)
|
351
|
+
# - result[1] (PTTL) (Integer)
|
352
|
+
{
|
353
|
+
lock_key: lock_key,
|
354
|
+
acq_id: hget_cmd_res['acq_id'],
|
355
|
+
ts: Integer(hget_cmd_res['ts']),
|
356
|
+
ini_ttl: Integer(hget_cmd_res['ini_ttl']),
|
357
|
+
rem_ttl: ((pttl_cmd_res == -1) ? Infinity : pttl_cmd_res)
|
358
|
+
}
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Returns an information about the required lock queue by the lock name. The result
|
364
|
+
# represnts the ordered lock request queue that is ordered by score (Redis sets) and shows
|
365
|
+
# lock acquirers and their position in queue. Async nature with redis communcation can lead
|
366
|
+
# the sitation when the queue becomes empty during the queue data extraction. So sometimes
|
367
|
+
# you can receive the lock queue info with empty queue.
|
368
|
+
#
|
369
|
+
# @param redis_client [RedisClient]
|
370
|
+
# @param lock_name [String]
|
371
|
+
# @return [Hash<Symbol,String|Array<Hash<Symbol,String|Float>>,NilClass]
|
372
|
+
# - `nil` is returned when lock queue does not exist;
|
373
|
+
# - result format: {
|
374
|
+
# lock_queue: "rql:lock_queue:your_lock_name", # lock queue key in redis,
|
375
|
+
# queue: [
|
376
|
+
# { acq_id: "rql:acq:process_id/thread_id", score: 123 },
|
377
|
+
# { acq_id: "rql:acq:process_id/thread_id", score: 456 },
|
378
|
+
# ] # ordered set (by score) with information about an acquier and their position in queue
|
379
|
+
# }
|
380
|
+
#
|
381
|
+
# @api private
|
382
|
+
# @since 0.1.0
|
383
|
+
def queue_info(redis_client, lock_name)
|
384
|
+
lock_key_queue = RedisQueuedLocks::Resource.prepare_lock_queue(lock_name)
|
385
|
+
|
386
|
+
result = redis_client.pipelined do |pipeline|
|
387
|
+
pipeline.call('EXISTS', lock_key_queue)
|
388
|
+
pipeline.call('ZRANGE', lock_key_queue, '0', '-1', 'WITHSCORES')
|
389
|
+
end
|
390
|
+
|
391
|
+
exists_cmd_res = result[0]
|
392
|
+
zrange_cmd_res = result[1]
|
393
|
+
|
394
|
+
if exists_cmd_res == 1
|
395
|
+
# NOTE: queue existed during the piepline invocation
|
396
|
+
{
|
397
|
+
lock_queue: lock_key_queue,
|
398
|
+
queue: zrange_cmd_res.map { |val| { acq_id: val[0], score: val[1] } }
|
399
|
+
}
|
400
|
+
else
|
401
|
+
# NOTE: queue did not exist during the pipeline invocation
|
402
|
+
nil
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
274
406
|
private
|
275
407
|
|
276
408
|
# @param timeout [NilClass,Integer]
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
# @api public
|
4
4
|
# @since 0.1.0
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
5
6
|
class RedisQueuedLocks::Client
|
6
7
|
# @since 0.1.0
|
7
8
|
include Qonfig::Configurable
|
@@ -16,6 +17,7 @@ class RedisQueuedLocks::Client
|
|
16
17
|
setting :default_queue_ttl, 15 # NOTE: seconds
|
17
18
|
setting :lock_release_batch_size, 100
|
18
19
|
setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
|
20
|
+
setting :uniq_identifier, -> { RedisQueuedLocks::Resource.calc_uniq_identity }
|
19
21
|
|
20
22
|
# TODO: setting :logger, Logger.new(IO::NULL)
|
21
23
|
# TODO: setting :debug, true/false
|
@@ -29,6 +31,7 @@ class RedisQueuedLocks::Client
|
|
29
31
|
validate('default_queue_ttl', :integer)
|
30
32
|
validate('lock_release_batch_size', :integer)
|
31
33
|
validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
|
34
|
+
validate('uniq_identifier', :proc)
|
32
35
|
end
|
33
36
|
|
34
37
|
# @return [RedisClient]
|
@@ -37,6 +40,12 @@ class RedisQueuedLocks::Client
|
|
37
40
|
# @since 0.1.0
|
38
41
|
attr_reader :redis_client
|
39
42
|
|
43
|
+
# @return [String]
|
44
|
+
#
|
45
|
+
# @api private
|
46
|
+
# @since 0.1.0
|
47
|
+
attr_accessor :uniq_identity
|
48
|
+
|
40
49
|
# @param redis_client [RedisClient]
|
41
50
|
# Redis connection manager, which will be used for the lock acquierment and distribution.
|
42
51
|
# It should be an instance of RedisClient.
|
@@ -48,15 +57,12 @@ class RedisQueuedLocks::Client
|
|
48
57
|
# @since 0.1.0
|
49
58
|
def initialize(redis_client, &configs)
|
50
59
|
configure(&configs)
|
60
|
+
@uniq_identity = config[:uniq_identifier].call
|
51
61
|
@redis_client = redis_client
|
52
62
|
end
|
53
63
|
|
54
64
|
# @param lock_name [String]
|
55
65
|
# Lock name to be obtained.
|
56
|
-
# @option process_id [Integer,String]
|
57
|
-
# The process that want to acquire the lock.
|
58
|
-
# @option thread_id [Integer,String]
|
59
|
-
# The process's thread that want to acquire the lock.
|
60
66
|
# @option ttl [Integer]
|
61
67
|
# Lock's time to live (in milliseconds).
|
62
68
|
# @option queue_ttl [Integer]
|
@@ -73,17 +79,19 @@ class RedisQueuedLocks::Client
|
|
73
79
|
# See RedisQueuedLocks::Instrument::ActiveSupport for example.
|
74
80
|
# @option raise_errors [Boolean]
|
75
81
|
# Raise errors on library-related limits such as timeout or failed lock obtain.
|
82
|
+
# @option identity [String]
|
83
|
+
# Unique acquire identifier that is also should be unique between processes and pods
|
84
|
+
# on different machines. By default the uniq identity string is
|
85
|
+
# represented as 10 bytes hexstr.
|
76
86
|
# @param block [Block]
|
77
87
|
# A block of code that should be executed after the successfully acquired lock.
|
78
|
-
# @return [Hash<Symbol,Any
|
88
|
+
# @return [Hash<Symbol,Any>,yield]
|
79
89
|
# Format: { ok: true/false, result: Symbol/Hash }.
|
80
90
|
#
|
81
91
|
# @api public
|
82
92
|
# @since 0.1.0
|
83
93
|
def lock(
|
84
94
|
lock_name,
|
85
|
-
process_id: RedisQueuedLocks::Resource.get_process_id,
|
86
|
-
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
87
95
|
ttl: config[:default_lock_ttl],
|
88
96
|
queue_ttl: config[:default_queue_ttl],
|
89
97
|
timeout: config[:try_to_lock_timeout],
|
@@ -91,13 +99,16 @@ class RedisQueuedLocks::Client
|
|
91
99
|
retry_delay: config[:retry_delay],
|
92
100
|
retry_jitter: config[:retry_jitter],
|
93
101
|
raise_errors: false,
|
102
|
+
identity: uniq_identity,
|
94
103
|
&block
|
95
104
|
)
|
96
105
|
RedisQueuedLocks::Acquier.acquire_lock!(
|
97
106
|
redis_client,
|
98
107
|
lock_name,
|
99
|
-
process_id
|
100
|
-
thread_id
|
108
|
+
process_id: RedisQueuedLocks::Resource.get_process_id,
|
109
|
+
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
110
|
+
fiber_id: RedisQueuedLocks::Resource.get_fiber_id,
|
111
|
+
ractor_id: RedisQueuedLocks::Resource.get_ractor_id,
|
101
112
|
ttl:,
|
102
113
|
queue_ttl:,
|
103
114
|
timeout:,
|
@@ -106,6 +117,7 @@ class RedisQueuedLocks::Client
|
|
106
117
|
retry_jitter:,
|
107
118
|
raise_errors:,
|
108
119
|
instrumenter: config[:instrumenter],
|
120
|
+
identity:,
|
109
121
|
&block
|
110
122
|
)
|
111
123
|
end
|
@@ -116,20 +128,17 @@ class RedisQueuedLocks::Client
|
|
116
128
|
# @since 0.1.0
|
117
129
|
def lock!(
|
118
130
|
lock_name,
|
119
|
-
process_id: RedisQueuedLocks::Resource.get_process_id,
|
120
|
-
thread_id: RedisQueuedLocks::Resource.get_thread_id,
|
121
131
|
ttl: config[:default_lock_ttl],
|
122
132
|
queue_ttl: config[:default_queue_ttl],
|
123
133
|
timeout: config[:default_timeout],
|
124
134
|
retry_count: config[:retry_count],
|
125
135
|
retry_delay: config[:retry_delay],
|
126
136
|
retry_jitter: config[:retry_jitter],
|
137
|
+
identity: uniq_identity,
|
127
138
|
&block
|
128
139
|
)
|
129
140
|
lock(
|
130
141
|
lock_name,
|
131
|
-
process_id:,
|
132
|
-
thread_id:,
|
133
142
|
ttl:,
|
134
143
|
queue_ttl:,
|
135
144
|
timeout:,
|
@@ -137,6 +146,7 @@ class RedisQueuedLocks::Client
|
|
137
146
|
retry_delay:,
|
138
147
|
retry_jitter:,
|
139
148
|
raise_errors: true,
|
149
|
+
identity:,
|
140
150
|
&block
|
141
151
|
)
|
142
152
|
end
|
@@ -147,11 +157,43 @@ class RedisQueuedLocks::Client
|
|
147
157
|
# @api public
|
148
158
|
# @since 0.1.0
|
149
159
|
def unlock(lock_name)
|
150
|
-
RedisQueuedLocks::Acquier.release_lock!(
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
160
|
+
RedisQueuedLocks::Acquier.release_lock!(redis_client, lock_name, config[:instrumenter])
|
161
|
+
end
|
162
|
+
|
163
|
+
# @param lock_name [String]
|
164
|
+
# @return [Boolean]
|
165
|
+
#
|
166
|
+
# @api public
|
167
|
+
# @since 0.1.0
|
168
|
+
def locked?(lock_name)
|
169
|
+
RedisQueuedLocks::Acquier.locked?(redis_client, lock_name)
|
170
|
+
end
|
171
|
+
|
172
|
+
# @param lock_name [String]
|
173
|
+
# @return [Boolean]
|
174
|
+
#
|
175
|
+
# @api public
|
176
|
+
# @since 0.1.0
|
177
|
+
def queued?(lock_name)
|
178
|
+
RedisQueuedLocks::Acquier.queued?(redis_client, lock_name)
|
179
|
+
end
|
180
|
+
|
181
|
+
# @param lock_name [String]
|
182
|
+
# @return [Hash,NilClass]
|
183
|
+
#
|
184
|
+
# @api public
|
185
|
+
# @since 0.1.0
|
186
|
+
def lock_info(lock_name)
|
187
|
+
RedisQueuedLocks::Acquier.lock_info(redis_client, lock_name)
|
188
|
+
end
|
189
|
+
|
190
|
+
# @param lock_name [String]
|
191
|
+
# @return [Hash,NilClass]
|
192
|
+
#
|
193
|
+
# @api public
|
194
|
+
# @since 0.1.0
|
195
|
+
def queue_info(lock_name)
|
196
|
+
RedisQueuedLocks::Acquier.queue_info(redis_client, lock_name)
|
155
197
|
end
|
156
198
|
|
157
199
|
# @option batch_size [Integer]
|
@@ -160,10 +202,7 @@ class RedisQueuedLocks::Client
|
|
160
202
|
# @api public
|
161
203
|
# @since 0.1.0
|
162
204
|
def clear_locks(batch_size: config[:lock_release_batch_size])
|
163
|
-
RedisQueuedLocks::Acquier.release_all_locks!(
|
164
|
-
redis_client,
|
165
|
-
batch_size,
|
166
|
-
config[:instrumenter]
|
167
|
-
)
|
205
|
+
RedisQueuedLocks::Acquier.release_all_locks!(redis_client, batch_size, config[:instrumenter])
|
168
206
|
end
|
169
207
|
end
|
208
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -16,14 +16,29 @@ module RedisQueuedLocks::Resource
|
|
16
16
|
LOCK_QUEUE_PATTERN = 'rql:lock_queue:*'
|
17
17
|
|
18
18
|
class << self
|
19
|
+
# Returns 10-byte unique identifier. It is used for uniquely
|
20
|
+
# identify current process between different nodes/pods of your application
|
21
|
+
# during the lock obtaining and self-identifying in the lock queue.
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
# @since 0.1.0
|
27
|
+
def calc_uniq_identity
|
28
|
+
SecureRandom.hex(5)
|
29
|
+
end
|
30
|
+
|
19
31
|
# @param process_id [Integer,String]
|
20
32
|
# @param thread_id [Integer,String]
|
33
|
+
# @param fiber_id [Integer,String]
|
34
|
+
# @param ractor_id [Integer,String]
|
35
|
+
# @param identity [String]
|
21
36
|
# @return [String]
|
22
37
|
#
|
23
38
|
# @api private
|
24
39
|
# @since 0.1.0
|
25
|
-
def acquier_identifier(process_id
|
26
|
-
"rql:acq:#{process_id}/#{thread_id}"
|
40
|
+
def acquier_identifier(process_id, thread_id, fiber_id, ractor_id, identity)
|
41
|
+
"rql:acq:#{process_id}/#{thread_id}/#{fiber_id}/#{ractor_id}/#{identity}"
|
27
42
|
end
|
28
43
|
|
29
44
|
# @param lock_name [String]
|
@@ -79,6 +94,22 @@ module RedisQueuedLocks::Resource
|
|
79
94
|
::Thread.current.object_id
|
80
95
|
end
|
81
96
|
|
97
|
+
# @return [Integer]
|
98
|
+
#
|
99
|
+
# @api private
|
100
|
+
# @since 0.1.0
|
101
|
+
def get_fiber_id
|
102
|
+
::Fiber.current.object_id
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [Integer]
|
106
|
+
#
|
107
|
+
# @api private
|
108
|
+
# @since 0.1.0
|
109
|
+
def get_ractor_id
|
110
|
+
::Ractor.current.object_id
|
111
|
+
end
|
112
|
+
|
82
113
|
# @return [Integer]
|
83
114
|
#
|
84
115
|
# @api private
|
data/lib/redis_queued_locks.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis_queued_locks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rustam Ibragimov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-02-
|
11
|
+
date: 2024-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|