redis_queued_locks 0.0.8 → 0.0.10

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
  SHA256:
3
- metadata.gz: 4846c7a27c9ce2108903a9b9b9250a38aa18ae34be6da85b4055bfe9c20371bb
4
- data.tar.gz: 0346d8a0c4d43490ea0fcf79c7048e70c480285b18f26f84b06ad24fe42dc9fc
3
+ metadata.gz: 28714dd149659f7c58634868219b3e020df6304253db120fda241304a82ce781
4
+ data.tar.gz: 52e9193aa4945598f7da31d202cfc445f2b5b63009f5f09e260a732b02e3ac64
5
5
  SHA512:
6
- metadata.gz: cbf5bd49d6c3b912ff8e3b8061cae4e2b1eaea00f913c8f217058168e7008f3c4daaeef0888ca22bce22d318742db1890c8524293e20734b4dea506fc3eeae10
7
- data.tar.gz: 744519a43e3f1eb98527cbcb5495700b16a6ea94de95732baf8027debc14df7e5fa0394f804a9360383eb9168d3975794b46c4a0a6c45d1b769fd0c94e1700b5
6
+ metadata.gz: b0f02df991559f21667288ebf690695909d65766cb2c9dd809d18d62a34e722d35d898e227ef38cf12d5acf96d271b7c0a078b1aa8ce47b93645fdd27b36d043
7
+ data.tar.gz: 5f60948e47cebdc9ff2de6a594aa68155c39ee262fd0662afbe293b672ce811c74a5182f8793be7d3860a8361560213108f25fd00d8a1891cd72a353d3103b25
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.10] - 2024-02-27
4
+ ### Changed
5
+ - Minor documentation updates;
6
+
7
+ ## [0.0.9] - 2024-02-27
8
+ ### Changed
9
+ - The lock acquier identifier (`acq_id`) now includes the fiber id, the ractor id and an unique per-process
10
+ 10 byte string. It is added in order to prevent collisions between different processes/pods
11
+ that will have the same procjet id / thread id identifiers (cuz it is an object_id integers) that can lead
12
+ to the same position with the same `acq_id` for different processes/pods in the lock request queue.
13
+
3
14
  ## [0.0.8] - 2024-02-27
4
15
  ### Added
5
16
  - `RedisQueuedLock::Client#locked?`
data/README.md CHANGED
@@ -60,7 +60,7 @@ require 'redis_queued_locks'
60
60
  require 'redis_queued_locks'
61
61
 
62
62
  # Step 1: initialize RedisClient instance
63
- redis_clinet = RedisClient.config.new_pool # NOTE: provide your ounw RedisClient instance
63
+ redis_client = RedisClient.config.new_pool # NOTE: provide your own RedisClient instance
64
64
 
65
65
  # Step 2: initialize RedisQueuedLock::Client instance
66
66
  rq_lock_client = RedisQueuedLocks::Client.new(redis_client) do |config|
@@ -119,6 +119,14 @@ clinet = RedisQueuedLocks::Client.new(redis_client) do |config|
119
119
  # - payload: <hash> requried;
120
120
  # - disabled by default via VoidNotifier;
121
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
+ # - expects the proc object;
129
+ config.uniq_identifier = -> { RedisQueuedLocks::Resource.calc_uniq_identity }
122
130
  end
123
131
  ```
124
132
 
@@ -142,8 +150,6 @@ end
142
150
  ```ruby
143
151
  def lock(
144
152
  lock_name,
145
- process_id: RedisQueuedLocks::Resource.get_process_id,
146
- thread_id: RedisQueuedLocks::Resource.get_thread_id,
147
153
  ttl: config[:default_lock_ttl],
148
154
  queue_ttl: config[:default_queue_ttl],
149
155
  timeout: config[:try_to_lock_timeout],
@@ -151,16 +157,13 @@ def lock(
151
157
  retry_delay: config[:retry_delay],
152
158
  retry_jitter: config[:retry_jitter],
153
159
  raise_errors: false,
160
+ identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config[:uniq_identifier] proc;
154
161
  &block
155
162
  )
156
163
  ```
157
164
 
158
165
  - `lock_name` - `[String]`
159
166
  - Lock name to be obtained.
160
- - `process_id` - `[Integer,String]`
161
- - The process that want to acquire the lock.
162
- - `thread_id` - `[Integer,String]`
163
- - The process's thread that want to acquire the lock.
164
167
  - `ttl` [Integer]
165
168
  - Lock's time to live (in milliseconds).
166
169
  - `queue_ttl` - `[Integer]`
@@ -177,6 +180,12 @@ def lock(
177
180
  - See RedisQueuedLocks::Instrument::ActiveSupport for example.
178
181
  - `raise_errors` - `[Boolean]`
179
182
  - Raise errors on library-related limits such as timeout or retry count limit.
183
+ - `identity` - `[String]`
184
+ - An unique string that is unique per `RedisQueuedLock::Client` instance. Resolves the
185
+ collisions between the same process_id/thread_id/fiber_id/ractor_id identifiers on different
186
+ pods or/and nodes of your application;
187
+ - It is calculated once during `RedisQueuedLock::Client` instantiation and stored in `@uniq_identity`
188
+ ivar (accessed via `uniq_dentity` accessor method);
180
189
  - `block` - `[Block]`
181
190
  - A block of code that should be executed after the successfully acquired lock.
182
191
  - If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
@@ -190,7 +199,7 @@ Return value:
190
199
  ok: true,
191
200
  result: {
192
201
  lock_key: String, # acquierd lock key ("rql:lock:your_lock_name")
193
- acq_id: String, # acquier identifier ("your_process_id/your_thread_id")
202
+ acq_id: String, # acquier identifier ("process_id/thread_id/fiber_id/ractor_id/identity")
194
203
  ts: Integer, # time (epoch) when lock was obtained (integer)
195
204
  ttl: Integer # lock's time to live in milliseconds (integer)
196
205
  }
@@ -214,14 +223,13 @@ Return value:
214
223
  ```ruby
215
224
  def lock!(
216
225
  lock_name,
217
- process_id: RedisQueuedLocks::Resource.get_process_id,
218
- thread_id: RedisQueuedLocks::Resource.get_thread_id,
219
226
  ttl: config[:default_lock_ttl],
220
227
  queue_ttl: config[:default_queue_ttl],
221
228
  timeout: config[:default_timeout],
222
229
  retry_count: config[:retry_count],
223
230
  retry_delay: config[:retry_delay],
224
231
  retry_jitter: config[:retry_jitter],
232
+ identity: uniq_identity,
225
233
  &block
226
234
  )
227
235
  ```
@@ -236,7 +244,7 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
236
244
  - returns `nil` if lock does not exist;
237
245
  - lock data (`Hash<Symbol,String|Integer>`):
238
246
  - `lock_key` - `string` - lock key in redis;
239
- - `acq_id` - `string` - acquier identifier (process_id/thread_id by default);
247
+ - `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity);
240
248
  - `ts` - `integer`/`epoch` - the time lock was obtained;
241
249
  - `init_ttl` - `integer` - (milliseconds) initial lock key ttl;
242
250
  - `rem_ttl` - `integer` - (milliseconds) remaining lock key ttl;
@@ -247,7 +255,7 @@ rql.lock_info("your_lock_name")
247
255
  # =>
248
256
  {
249
257
  lock_key: "rql:lock:your_lock_name",
250
- acq_id: "rql:acq:123/456",
258
+ acq_id: "rql:acq:123/456/567/678/374dd74324",
251
259
  ts: 123456789,
252
260
  ini_ttl: 123456789,
253
261
  rem_ttl: 123456789
@@ -267,7 +275,7 @@ rql.lock_info("your_lock_name")
267
275
  - lock queue data (`Hash<Symbol,String|Array<Hash<Symbol,String|Numeric>>`):
268
276
  - `lock_queue` - `string` - lock queue key in redis;
269
277
  - `queue` - `array` - an array of lock requests (array of hashes):
270
- - `acq_id` - `string` - acquier identifier (process_id/thread_id by default);
278
+ - `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
271
279
  - `score` - `float`/`epoch` - time when the lock request was made (epoch);
272
280
 
273
281
  ```ruby
@@ -277,9 +285,9 @@ rql.queue_info("your_lock_name")
277
285
  {
278
286
  lock_queue: "rql:lock_queue:your_lock_name",
279
287
  queue: [
280
- { acq_id: "rql:acq:123/456", score: 1},
281
- { acq_id: "rql:acq:123/567", score: 2},
282
- { acq_id: "rql:acq:555/329", score: 3},
288
+ { acq_id: "rql:acq:123/456/567/678/fa76df9cc2", score: 1},
289
+ { acq_id: "rql:acq:123/567/456/679/c7bfcaf4f9", score: 2},
290
+ { acq_id: "rql:acq:555/329/523/127/7329553b11", score: 3},
283
291
  # ...etc
284
292
  ]
285
293
  }
@@ -422,7 +430,7 @@ Detalized event semantics and payload structure:
422
430
  - payload:
423
431
  - `rel_time` - `float`/`milliseconds` - time spent on "realese all locks" operation;
424
432
  - `at` - `integer`/`epoch` - the time when the operation has ended;
425
- - `rel_keys` - `integer` - released redis keys count (`released queu keys` + `released lock keys`);
433
+ - `rel_keys` - `integer` - released redis keys count (`released queue keys` + `released lock keys`);
426
434
 
427
435
  ---
428
436
 
@@ -431,6 +439,7 @@ Detalized event semantics and payload structure:
431
439
  - **Major**
432
440
  - Semantic Error objects for unexpected Redis errors;
433
441
  - `100%` test coverage;
442
+ - sidecar `Ractor` object and `in progress queue` in RedisDB that will extend an acquired lock for long-running blocks of code (that invoked "under" the lock);
434
443
  - **Minor**
435
444
  - `RedisQueuedLocks::Acquier::Try.try_to_lock` - detailed successful result analization;
436
445
  - better code stylization and interesting refactorings;
@@ -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 the lock.
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 the lock.
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(process_id, thread_id)
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)
@@ -386,6 +403,17 @@ module RedisQueuedLocks::Acquier
386
403
  end
387
404
  end
388
405
 
406
+ # @param redis_client [RedisClient]
407
+ # @param lock_name [String]
408
+ # @param milliseconds [Integer]
409
+ # @return [?]
410
+ #
411
+ # @api private
412
+ # @since 0.1.0
413
+ def extend_lock_ttl(redis_client, lock_name, milliseconds)
414
+
415
+ end
416
+
389
417
  private
390
418
 
391
419
  # @param timeout [NilClass,Integer]
@@ -17,6 +17,7 @@ class RedisQueuedLocks::Client
17
17
  setting :default_queue_ttl, 15 # NOTE: seconds
18
18
  setting :lock_release_batch_size, 100
19
19
  setting :instrumenter, RedisQueuedLocks::Instrument::VoidNotifier
20
+ setting :uniq_identifier, -> { RedisQueuedLocks::Resource.calc_uniq_identity }
20
21
 
21
22
  # TODO: setting :logger, Logger.new(IO::NULL)
22
23
  # TODO: setting :debug, true/false
@@ -30,6 +31,7 @@ class RedisQueuedLocks::Client
30
31
  validate('default_queue_ttl', :integer)
31
32
  validate('lock_release_batch_size', :integer)
32
33
  validate('instrumenter') { |val| RedisQueuedLocks::Instrument.valid_interface?(val) }
34
+ validate('uniq_identifier', :proc)
33
35
  end
34
36
 
35
37
  # @return [RedisClient]
@@ -38,6 +40,12 @@ class RedisQueuedLocks::Client
38
40
  # @since 0.1.0
39
41
  attr_reader :redis_client
40
42
 
43
+ # @return [String]
44
+ #
45
+ # @api private
46
+ # @since 0.1.0
47
+ attr_accessor :uniq_identity
48
+
41
49
  # @param redis_client [RedisClient]
42
50
  # Redis connection manager, which will be used for the lock acquierment and distribution.
43
51
  # It should be an instance of RedisClient.
@@ -49,15 +57,12 @@ class RedisQueuedLocks::Client
49
57
  # @since 0.1.0
50
58
  def initialize(redis_client, &configs)
51
59
  configure(&configs)
60
+ @uniq_identity = config[:uniq_identifier].call
52
61
  @redis_client = redis_client
53
62
  end
54
63
 
55
64
  # @param lock_name [String]
56
65
  # Lock name to be obtained.
57
- # @option process_id [Integer,String]
58
- # The process that want to acquire the lock.
59
- # @option thread_id [Integer,String]
60
- # The process's thread that want to acquire the lock.
61
66
  # @option ttl [Integer]
62
67
  # Lock's time to live (in milliseconds).
63
68
  # @option queue_ttl [Integer]
@@ -74,17 +79,19 @@ class RedisQueuedLocks::Client
74
79
  # See RedisQueuedLocks::Instrument::ActiveSupport for example.
75
80
  # @option raise_errors [Boolean]
76
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.
77
86
  # @param block [Block]
78
87
  # A block of code that should be executed after the successfully acquired lock.
79
- # @return [Hash<Symbol,Any>]
88
+ # @return [Hash<Symbol,Any>,yield]
80
89
  # Format: { ok: true/false, result: Symbol/Hash }.
81
90
  #
82
91
  # @api public
83
92
  # @since 0.1.0
84
93
  def lock(
85
94
  lock_name,
86
- process_id: RedisQueuedLocks::Resource.get_process_id,
87
- thread_id: RedisQueuedLocks::Resource.get_thread_id,
88
95
  ttl: config[:default_lock_ttl],
89
96
  queue_ttl: config[:default_queue_ttl],
90
97
  timeout: config[:try_to_lock_timeout],
@@ -92,13 +99,16 @@ class RedisQueuedLocks::Client
92
99
  retry_delay: config[:retry_delay],
93
100
  retry_jitter: config[:retry_jitter],
94
101
  raise_errors: false,
102
+ identity: uniq_identity,
95
103
  &block
96
104
  )
97
105
  RedisQueuedLocks::Acquier.acquire_lock!(
98
106
  redis_client,
99
107
  lock_name,
100
- process_id:,
101
- 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,
102
112
  ttl:,
103
113
  queue_ttl:,
104
114
  timeout:,
@@ -107,6 +117,7 @@ class RedisQueuedLocks::Client
107
117
  retry_jitter:,
108
118
  raise_errors:,
109
119
  instrumenter: config[:instrumenter],
120
+ identity:,
110
121
  &block
111
122
  )
112
123
  end
@@ -117,20 +128,17 @@ class RedisQueuedLocks::Client
117
128
  # @since 0.1.0
118
129
  def lock!(
119
130
  lock_name,
120
- process_id: RedisQueuedLocks::Resource.get_process_id,
121
- thread_id: RedisQueuedLocks::Resource.get_thread_id,
122
131
  ttl: config[:default_lock_ttl],
123
132
  queue_ttl: config[:default_queue_ttl],
124
133
  timeout: config[:default_timeout],
125
134
  retry_count: config[:retry_count],
126
135
  retry_delay: config[:retry_delay],
127
136
  retry_jitter: config[:retry_jitter],
137
+ identity: uniq_identity,
128
138
  &block
129
139
  )
130
140
  lock(
131
141
  lock_name,
132
- process_id:,
133
- thread_id:,
134
142
  ttl:,
135
143
  queue_ttl:,
136
144
  timeout:,
@@ -138,6 +146,7 @@ class RedisQueuedLocks::Client
138
146
  retry_delay:,
139
147
  retry_jitter:,
140
148
  raise_errors: true,
149
+ identity:,
141
150
  &block
142
151
  )
143
152
  end
@@ -148,11 +157,7 @@ class RedisQueuedLocks::Client
148
157
  # @api public
149
158
  # @since 0.1.0
150
159
  def unlock(lock_name)
151
- RedisQueuedLocks::Acquier.release_lock!(
152
- redis_client,
153
- lock_name,
154
- config[:instrumenter]
155
- )
160
+ RedisQueuedLocks::Acquier.release_lock!(redis_client, lock_name, config[:instrumenter])
156
161
  end
157
162
 
158
163
  # @param lock_name [String]
@@ -191,17 +196,23 @@ class RedisQueuedLocks::Client
191
196
  RedisQueuedLocks::Acquier.queue_info(redis_client, lock_name)
192
197
  end
193
198
 
199
+ # @param lock_name [String]
200
+ # @param milliseconds [Integer] How many milliseconds should be added.
201
+ # @return [?]
202
+ #
203
+ # @api public
204
+ # @since 0.1.0
205
+ def extend_lock_ttl(lock_name, milliseconds)
206
+ RedisQueuedLocks::Acquier.extend_lock_ttl(redis_client, lock_name)
207
+ end
208
+
194
209
  # @option batch_size [Integer]
195
210
  # @return [Hash<Symbol,Any>] Format: { ok: true/false, result: Symbol/Hash }.
196
211
  #
197
212
  # @api public
198
213
  # @since 0.1.0
199
214
  def clear_locks(batch_size: config[:lock_release_batch_size])
200
- RedisQueuedLocks::Acquier.release_all_locks!(
201
- redis_client,
202
- batch_size,
203
- config[:instrumenter]
204
- )
215
+ RedisQueuedLocks::Acquier.release_all_locks!(redis_client, batch_size, config[:instrumenter])
205
216
  end
206
217
  end
207
218
  # 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 = get_process_id, thread_id = get_thread_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
@@ -5,6 +5,6 @@ module RedisQueuedLocks
5
5
  #
6
6
  # @api public
7
7
  # @since 0.0.1
8
- # @version 0.0.8
9
- VERSION = '0.0.8'
8
+ # @version 0.0.10
9
+ VERSION = '0.0.10'
10
10
  end
@@ -3,6 +3,7 @@
3
3
  require 'redis-client'
4
4
  require 'qonfig'
5
5
  require 'timeout'
6
+ require 'securerandom'
6
7
 
7
8
  # @api public
8
9
  # @since 0.1.0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis_queued_locks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov