redis_queued_locks 0.0.8 → 0.0.9

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
  SHA256:
3
- metadata.gz: 4846c7a27c9ce2108903a9b9b9250a38aa18ae34be6da85b4055bfe9c20371bb
4
- data.tar.gz: 0346d8a0c4d43490ea0fcf79c7048e70c480285b18f26f84b06ad24fe42dc9fc
3
+ metadata.gz: 463b0613174c7a3806aef19ce962bbe5ed91d342796bec84ea47b2aa79cf61fb
4
+ data.tar.gz: 4d483db40896d77049e3e6d324a206606225c9c8e8b592072b1f501563fb7ec8
5
5
  SHA512:
6
- metadata.gz: cbf5bd49d6c3b912ff8e3b8061cae4e2b1eaea00f913c8f217058168e7008f3c4daaeef0888ca22bce22d318742db1890c8524293e20734b4dea506fc3eeae10
7
- data.tar.gz: 744519a43e3f1eb98527cbcb5495700b16a6ea94de95732baf8027debc14df7e5fa0394f804a9360383eb9168d3975794b46c4a0a6c45d1b769fd0c94e1700b5
6
+ metadata.gz: f829f8afc264f8a44d0edd1469c5ada76c7257f15ef6945c3f154fe2908b29d5ace4ce0c11365a30af1cdbb6d3b4aeec30227b84500da65b720f22247ed96053
7
+ data.tar.gz: 1eee0e4c2a665b57ee6d2c2c2d6488c065bbcdd586cfe06f2890a6b412a3a009675dd26e39383f2fad54434288d4c3c7660fba2faaa3013da98ab27c484b08b0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.0.8] - 2024-02-27
4
11
  ### Added
5
12
  - `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_clinet = 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,13 @@ 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
+ config.uniq_identifier = -> { RedisQueuedLocks::Resource.calc_uniq_identity }
122
129
  end
123
130
  ```
124
131
 
@@ -142,8 +149,6 @@ end
142
149
  ```ruby
143
150
  def lock(
144
151
  lock_name,
145
- process_id: RedisQueuedLocks::Resource.get_process_id,
146
- thread_id: RedisQueuedLocks::Resource.get_thread_id,
147
152
  ttl: config[:default_lock_ttl],
148
153
  queue_ttl: config[:default_queue_ttl],
149
154
  timeout: config[:try_to_lock_timeout],
@@ -151,16 +156,13 @@ def lock(
151
156
  retry_delay: config[:retry_delay],
152
157
  retry_jitter: config[:retry_jitter],
153
158
  raise_errors: false,
159
+ identity: uniq_identity, # (attr_accessor) calculated during client instantiation via config[:uniq_identifier] proc;
154
160
  &block
155
161
  )
156
162
  ```
157
163
 
158
164
  - `lock_name` - `[String]`
159
165
  - 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
166
  - `ttl` [Integer]
165
167
  - Lock's time to live (in milliseconds).
166
168
  - `queue_ttl` - `[Integer]`
@@ -177,6 +179,12 @@ def lock(
177
179
  - See RedisQueuedLocks::Instrument::ActiveSupport for example.
178
180
  - `raise_errors` - `[Boolean]`
179
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);
180
188
  - `block` - `[Block]`
181
189
  - A block of code that should be executed after the successfully acquired lock.
182
190
  - If block is **passed** the obtained lock will be released after the block execution or it's ttl (what will happen first);
@@ -214,14 +222,13 @@ Return value:
214
222
  ```ruby
215
223
  def lock!(
216
224
  lock_name,
217
- process_id: RedisQueuedLocks::Resource.get_process_id,
218
- thread_id: RedisQueuedLocks::Resource.get_thread_id,
219
225
  ttl: config[:default_lock_ttl],
220
226
  queue_ttl: config[:default_queue_ttl],
221
227
  timeout: config[:default_timeout],
222
228
  retry_count: config[:retry_count],
223
229
  retry_delay: config[:retry_delay],
224
230
  retry_jitter: config[:retry_jitter],
231
+ identity: uniq_identity,
225
232
  &block
226
233
  )
227
234
  ```
@@ -236,7 +243,7 @@ See `#lock` method [documentation](#lock---obtain-a-lock).
236
243
  - returns `nil` if lock does not exist;
237
244
  - lock data (`Hash<Symbol,String|Integer>`):
238
245
  - `lock_key` - `string` - lock key in redis;
239
- - `acq_id` - `string` - acquier identifier (process_id/thread_id by default);
246
+ - `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
240
247
  - `ts` - `integer`/`epoch` - the time lock was obtained;
241
248
  - `init_ttl` - `integer` - (milliseconds) initial lock key ttl;
242
249
  - `rem_ttl` - `integer` - (milliseconds) remaining lock key ttl;
@@ -267,7 +274,7 @@ rql.lock_info("your_lock_name")
267
274
  - lock queue data (`Hash<Symbol,String|Array<Hash<Symbol,String|Numeric>>`):
268
275
  - `lock_queue` - `string` - lock queue key in redis;
269
276
  - `queue` - `array` - an array of lock requests (array of hashes):
270
- - `acq_id` - `string` - acquier identifier (process_id/thread_id by default);
277
+ - `acq_id` - `string` - acquier identifier (process_id/thread_id/fiber_id/ractor_id/identity by default);
271
278
  - `score` - `float`/`epoch` - time when the lock request was made (epoch);
272
279
 
273
280
  ```ruby
@@ -277,9 +284,9 @@ rql.queue_info("your_lock_name")
277
284
  {
278
285
  lock_queue: "rql:lock_queue:your_lock_name",
279
286
  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},
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},
283
290
  # ...etc
284
291
  ]
285
292
  }
@@ -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)
@@ -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]
@@ -197,11 +202,7 @@ class RedisQueuedLocks::Client
197
202
  # @api public
198
203
  # @since 0.1.0
199
204
  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
- )
205
+ RedisQueuedLocks::Acquier.release_all_locks!(redis_client, batch_size, config[:instrumenter])
205
206
  end
206
207
  end
207
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 = 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.9
9
+ VERSION = '0.0.9'
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.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rustam Ibragimov