redis_queued_locks 1.18.0 → 1.20.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 +4 -4
- data/.claude/project-overview.md +65 -13
- data/.claude/rules/acquirer.md +29 -11
- data/.claude/rules/arguments.md +3 -2
- data/.claude/rules/git.md +3 -2
- data/.claude/rules/logic.md +4 -3
- data/.claude/rules/swarm.md +12 -7
- data/.claude/rules/tests.md +6 -0
- data/.claude/rules/type-checking.md +2 -1
- data/.claude/rules/visitors.md +1 -1
- data/.claude/skills/rql-code-review/SKILL.md +372 -0
- data/.claude/skills/rql-code-review/templates/report.en.md +31 -0
- data/.claude/skills/rql-code-review/templates/report.ru.md +31 -0
- data/CHANGELOG.md +74 -0
- data/CLAUDE.md +4 -2
- data/README.md +338 -24
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +684 -128
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +3 -1
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +24 -3
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +145 -18
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +71 -10
- data/lib/redis_queued_locks/acquirer/keys.rb +142 -7
- data/lib/redis_queued_locks/acquirer/lock_info.rb +10 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +26 -7
- data/lib/redis_queued_locks/acquirer/locks.rb +195 -83
- data/lib/redis_queued_locks/acquirer/queue_info.rb +12 -18
- data/lib/redis_queued_locks/acquirer/queues.rb +114 -50
- data/lib/redis_queued_locks/acquirer/reindex_locks.rb +140 -0
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +147 -58
- data/lib/redis_queued_locks/acquirer/release_lock.rb +52 -19
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +235 -66
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +116 -16
- data/lib/redis_queued_locks/config.rb +5 -1
- data/lib/redis_queued_locks/resource.rb +99 -2
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +332 -61
- data/lib/redis_queued_locks/swarm/zombie_info.rb +203 -53
- data/lib/redis_queued_locks/swarm.rb +15 -4
- data/lib/redis_queued_locks/version.rb +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +107 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +2 -0
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +5 -2
- data/sig/redis_queued_locks/acquirer/keys.rbs +11 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +15 -6
- data/sig/redis_queued_locks/acquirer/queues.rbs +9 -2
- data/sig/redis_queued_locks/acquirer/reindex_locks.rbs +31 -0
- data/sig/redis_queued_locks/acquirer/release_all_locks.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +3 -1
- data/sig/redis_queued_locks/acquirer/release_locks_of.rbs +20 -1
- data/sig/redis_queued_locks/client.rbs +9 -0
- data/sig/redis_queued_locks/resource.rbs +6 -0
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +51 -1
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +58 -7
- data/sig/vendor/redis_client.rbs +1 -0
- metadata +6 -1
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@ In addition to the classic `queued` (FIFO) strategy RQL supports `random` (RANDO
|
|
|
10
10
|
|
|
11
11
|
Supports `Read`/`Write` locks and `Reentrant` locks.
|
|
12
12
|
|
|
13
|
-
Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), **zombie locks elimination**, logging and instrumentation (and much more).
|
|
13
|
+
Provides flexible invocation flow, parametrized limits (lock request ttl, lock ttl, queue ttl, lock attempts limit, fast failing, etc), **zombie locks elimination**, **[lock indexing](#lock-index)**, logging and instrumentation (and much more).
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -64,6 +64,9 @@ Provides flexible invocation flow, parametrized limits (lock request ttl, lock t
|
|
|
64
64
|
- [zombie_locks](#zombie_locks)
|
|
65
65
|
- [zombie_hosts](#zombie_hosts)
|
|
66
66
|
- [zombie_acquirers](#zombie_acquirers)
|
|
67
|
+
- [Lock Index](#lock-index)
|
|
68
|
+
- [Lock Index Performance](#lock-index-performance)
|
|
69
|
+
- [Lock Index Size](#lock-index-size)
|
|
67
70
|
- [Lock Access Strategies](#lock-access-strategies)
|
|
68
71
|
- [queued](#lock-access-strategies)
|
|
69
72
|
- [random](#lock-access-strategies)
|
|
@@ -196,6 +199,8 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
|
196
199
|
|
|
197
200
|
# (seconds) (default: 15)
|
|
198
201
|
# - lock request timeout. after this timeout your lock request in queue will be requeued with new position (at the end of the queue);
|
|
202
|
+
# - (the enabled lock index) the lock request queue lives as long as its latest lock request (the queue TTL is refreshed with
|
|
203
|
+
# the queue ttl on each lock request): queues of dead acquirers expire by themselves;
|
|
199
204
|
config['default_queue_ttl'] = 15
|
|
200
205
|
|
|
201
206
|
# (boolean) (default: false)
|
|
@@ -252,23 +257,30 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
|
252
257
|
|
|
253
258
|
# (default: 100)
|
|
254
259
|
# - how many items will be released at a time in #clear_locks and in #clear_dead_requests (uses SCAN);
|
|
260
|
+
# - (the enabled lock index) how many locks (lock names of the lock index) will be processed at a time
|
|
261
|
+
# (the ZSCAN count of the lock index pass);
|
|
255
262
|
# - affects the performance of your Redis and Ruby Application (configure thoughtfully);
|
|
256
263
|
config['lock_release_batch_size'] = 100
|
|
257
264
|
|
|
258
265
|
# (default: 300)
|
|
259
266
|
# - how many items will be released at a time in #clear_locks_of and in #clear_current_locks methods (uses SCAN for batch extraction and DEL for batch deletion);
|
|
267
|
+
# - (the enabled lock index) how many locks (lock names of the lock index) will be processed at a time
|
|
268
|
+
# (the ZSCAN count of the lock index pass, DEL for batch deletion);
|
|
260
269
|
# - affects the ruby's memory (cuz this batch will be stored in Set object in first, and then will be splatted to the DEL redis method invocation) (configure thoughtfully);
|
|
261
270
|
# - affects the performance of your Redis (configure thoughtfully);
|
|
262
271
|
config['clear_locks_of__lock_scan_size'] = 300
|
|
263
272
|
|
|
264
273
|
# (default: 300)
|
|
265
|
-
# - how many queues will be
|
|
274
|
+
# - how many lock queues will be processed in one round trip in #clear_locks_of and in #clear_current_locks methods
|
|
275
|
+
# (lock queues are found by the same SCAN pass as locks or derived from the lock names of the same lock index pass);
|
|
266
276
|
# - affects the performance of your Redis (configure thoughtfully);
|
|
267
277
|
config['clear_locks_of__queue_scan_size'] = 300
|
|
268
278
|
|
|
269
279
|
# (default: 500)
|
|
270
280
|
# - how many items should be extracted from redis during the #locks, #queues, #keys
|
|
271
281
|
# #locks_info, and #queues_info operations (uses SCAN);
|
|
282
|
+
# - (the enabled lock index) how many locks (lock names of the lock index) should be processed at a time
|
|
283
|
+
# (the ZSCAN count of the lock index pass);
|
|
272
284
|
# - affects the performance of your Redis and Ruby Application (configure thoughtfully);
|
|
273
285
|
config['key_extraction_batch_size'] = 500
|
|
274
286
|
|
|
@@ -277,6 +289,22 @@ client = RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
|
277
289
|
# - used for `#clear_dead_requests` as default vaule of `:dead_ttl` option;
|
|
278
290
|
config['dead_request_ttl'] = (1 * 24 * 60 * 60 * 1000) # one day in milliseconds
|
|
279
291
|
|
|
292
|
+
# (boolean) (default: false)
|
|
293
|
+
# - find locks via the lock index (`rql:index:locks`) instead of the full Redis keyspace `SCAN` in all lock-iterating operations
|
|
294
|
+
# (the swarm, #locks, #queues, #keys, #clear_locks, #clear_locks_of, #clear_dead_requests, ...);
|
|
295
|
+
# - the lock index is maintained by the lock acquirement itself only when it is enabled;
|
|
296
|
+
# - the client-wide mode (not a per-call option): use the same value on all your processes that share the Redis database;
|
|
297
|
+
# after enabling invoke `#reindex_locks` once (see the "Lock Index" documentation section);
|
|
298
|
+
config['lock_index_enabled'] = false
|
|
299
|
+
|
|
300
|
+
# (milliseconds) (default: 60_000)
|
|
301
|
+
# - the lock index grace period: the lock name stays in the lock index (`rql:index:locks`) for this period
|
|
302
|
+
# after the expiration of all its keys (see the "Lock Index" documentation section);
|
|
303
|
+
# - tolerance of the client clock skew between your hosts and of the network latency: should be greater than
|
|
304
|
+
# the maximal clock skew between your hosts plus the network latency;
|
|
305
|
+
# - used only with the enabled lock index (`config['lock_index_enabled']`);
|
|
306
|
+
config['lock_index_grace_period'] = 60_000
|
|
307
|
+
|
|
280
308
|
# (default: RedisQueuedLocks::Instrument::VoidNotifier)
|
|
281
309
|
# - instrumentation layer;
|
|
282
310
|
# - you can provide your own instrumenter that should realize `#notify(event, payload = {})` interface:
|
|
@@ -397,6 +425,7 @@ end
|
|
|
397
425
|
- [locks_info](#locks_info---get-list-of-locks-with-their-info)
|
|
398
426
|
- [queues_info](#queues_info---get-list-of-queues-with-their-info)
|
|
399
427
|
- [clear_dead_requests](#clear_dead_requests)
|
|
428
|
+
- [reindex_locks](#reindex_locks)
|
|
400
429
|
- [current_acquirer_id](#current_acquirer_id)
|
|
401
430
|
- [current_host_id](#current_host_id)
|
|
402
431
|
- [possible_host_ids](#possible_host_ids)
|
|
@@ -411,6 +440,10 @@ end
|
|
|
411
440
|
- If block is passed:
|
|
412
441
|
- the obtained lock will be released after the block execution or the lock's ttl (what will happen first);
|
|
413
442
|
- if you want to timeout (fail with timeout) the block execution with lock's TTL use `timed: true` option;
|
|
443
|
+
- (limitation) the write lock is released after the block without an owner check: if the block (without `timed: true`)
|
|
444
|
+
runs longer than the lock's ttl, the expired lock can be already obtained by another acquirer and the release
|
|
445
|
+
deletes that lock (the same for the ttl decrease of `:extendable_work_through` reentrant locks); use `timed: true`
|
|
446
|
+
or extend the lock (`#extend_lock_ttl`) for long blocks (read locks are released per acquirer and are not affected);
|
|
414
447
|
- the block's result will be returned;
|
|
415
448
|
- If block is not passed:
|
|
416
449
|
- the obtained lock will be released after lock's ttl;
|
|
@@ -421,6 +454,7 @@ def lock(
|
|
|
421
454
|
lock_name,
|
|
422
455
|
ttl: config['default_lock_ttl'],
|
|
423
456
|
queue_ttl: config['default_queue_ttl'],
|
|
457
|
+
lock_index_grace_period: config['lock_index_grace_period'],
|
|
424
458
|
timeout: config['try_to_lock_timeout'],
|
|
425
459
|
timed: config['is_timed_by_default'],
|
|
426
460
|
retry_count: config['retry_count'],
|
|
@@ -457,7 +491,15 @@ def lock(
|
|
|
457
491
|
- pre-configured in `config['default_lock_ttl']`;
|
|
458
492
|
- `queue_ttl` - (optional) `[Integer]`
|
|
459
493
|
- Lifetime of the acuier's lock request. In seconds.
|
|
494
|
+
- (the enabled lock index) the lock request queue lives as long as its latest lock request (the queue TTL is refreshed
|
|
495
|
+
on each lock request);
|
|
460
496
|
- pre-configured in `config['default_queue_ttl']`;
|
|
497
|
+
- `lock_index_grace_period` - (optional) `[Integer]`
|
|
498
|
+
- The lock index grace period (in milliseconds): the lock name stays in the lock index for this period after the expiration
|
|
499
|
+
of all its keys (tolerance of the client clock skew between your hosts and of the network latency);
|
|
500
|
+
- used only with the enabled lock index (`config['lock_index_enabled']`, the client-wide mode);
|
|
501
|
+
- see the [Lock Index](#lock-index) documentation section;
|
|
502
|
+
- pre-configured in `config['lock_index_grace_period']`;
|
|
461
503
|
- `timeout` - (optional) `[Integer,NilClass]`
|
|
462
504
|
- Time period a client should try to acquire the lock (in seconds). Nil means "without timeout".
|
|
463
505
|
- pre-configured in `config['try_to_lock_timeout']`;
|
|
@@ -842,6 +884,7 @@ def lock!(
|
|
|
842
884
|
lock_name,
|
|
843
885
|
ttl: config['default_lock_ttl'],
|
|
844
886
|
queue_ttl: config['default_queue_ttl'],
|
|
887
|
+
lock_index_grace_period: config['lock_index_grace_period'],
|
|
845
888
|
timeout: config['try_to_lock_timeout'],
|
|
846
889
|
timed: config['is_timed_by_default'],
|
|
847
890
|
retry_count: config['retry_count'],
|
|
@@ -1094,6 +1137,64 @@ rql.lock_info("your_lock_name")
|
|
|
1094
1137
|
}
|
|
1095
1138
|
```
|
|
1096
1139
|
|
|
1140
|
+
```ruby
|
|
1141
|
+
# <for read/write locks>
|
|
1142
|
+
# (see `read_write_mode:` kwarg attribute of #lock/#lock! methods and "Read/Write Locks" documentation section)
|
|
1143
|
+
# (lock requests are not included: use #queue_info to see the queued readers and writers)
|
|
1144
|
+
|
|
1145
|
+
# -> case 1: the lock is held by readers, writers and readers are waiting in the lock queues;
|
|
1146
|
+
|
|
1147
|
+
# process#1 and process#2 (readers, work in parallel):
|
|
1148
|
+
rql.lock("your_lock_name", read_write_mode: :read, ttl: 5_000, meta: { "kek" => "pek" }) { ... }
|
|
1149
|
+
rql.lock("your_lock_name", read_write_mode: :read, ttl: 3_000) { ... }
|
|
1150
|
+
# process#3 (writer, waits for all readers in the write lock queue):
|
|
1151
|
+
rql.lock("your_lock_name", read_write_mode: :write) { ... }
|
|
1152
|
+
# process#4 (reader, waits for the earlier writer in the read lock queue):
|
|
1153
|
+
rql.lock("your_lock_name", read_write_mode: :read) { ... }
|
|
1154
|
+
|
|
1155
|
+
rql.lock_info("your_lock_name")
|
|
1156
|
+
|
|
1157
|
+
# => (read locks info: process#1 and process#2)
|
|
1158
|
+
{
|
|
1159
|
+
"lock_key" => "rql:lock:your_lock_name",
|
|
1160
|
+
"rw_mode" => "read",
|
|
1161
|
+
"rem_ttl" => 4_985, # remaining ttl of the longest read lock
|
|
1162
|
+
"readers" => [
|
|
1163
|
+
{
|
|
1164
|
+
"acq_id" => "rql:acq:123/456/567/678/374dd74324",
|
|
1165
|
+
"hst_id" => "rql:hst:123/456/678/374dd74324",
|
|
1166
|
+
"ts" => 123456789.12345,
|
|
1167
|
+
"ini_ttl" => 5_000,
|
|
1168
|
+
"rem_ttl" => 4_985,
|
|
1169
|
+
"kek" => "pek" # custom metadata of the read lock
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
"acq_id" => "rql:acq:124/457/568/679/a83cb4f9d1",
|
|
1173
|
+
"hst_id" => "rql:hst:124/457/679/a83cb4f9d1",
|
|
1174
|
+
"ts" => 123456789.54321,
|
|
1175
|
+
"ini_ttl" => 3_000,
|
|
1176
|
+
"rem_ttl" => 2_412
|
|
1177
|
+
}
|
|
1178
|
+
]
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
# -> case 2: (some time later) readers are released, the lock is held by the writer (process#3),
|
|
1182
|
+
# the reader (process#4) is still waiting in the read lock queue;
|
|
1183
|
+
|
|
1184
|
+
rql.lock_info("your_lock_name")
|
|
1185
|
+
|
|
1186
|
+
# => (write lock info: process#3, read lock requests are not included)
|
|
1187
|
+
{
|
|
1188
|
+
"lock_key" => "rql:lock:your_lock_name",
|
|
1189
|
+
"rw_mode" => "write",
|
|
1190
|
+
"acq_id" => "rql:acq:125/458/569/680/c7bfcaf4f9",
|
|
1191
|
+
"hst_id" => "rql:hst:125/458/680/c7bfcaf4f9",
|
|
1192
|
+
"ts" => 123456795.12345,
|
|
1193
|
+
"ini_ttl" => 5_000,
|
|
1194
|
+
"rem_ttl" => 4_870
|
|
1195
|
+
}
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1097
1198
|
---
|
|
1098
1199
|
|
|
1099
1200
|
#### #queue_info
|
|
@@ -1102,9 +1203,8 @@ rql.lock_info("your_lock_name")
|
|
|
1102
1203
|
|
|
1103
1204
|
Returns an information about the required lock queue by the lock name. The result
|
|
1104
1205
|
represnts the ordered lock request queue that is ordered by score (Redis Sets) and shows
|
|
1105
|
-
lock acquirers and their position in queue.
|
|
1106
|
-
|
|
1107
|
-
you can receive the lock queue info with empty queue value (an empty array).
|
|
1206
|
+
lock acquirers and their position in queue. The queue of write lock requests is an empty array
|
|
1207
|
+
(`"queue" => []`) when only the queue of read lock requests exists.
|
|
1108
1208
|
|
|
1109
1209
|
- get the lock queue information;
|
|
1110
1210
|
- queue represents the ordered set of lock key reqests:
|
|
@@ -1174,6 +1274,7 @@ rql.queued?("your_lock_name") # => true/false
|
|
|
1174
1274
|
|
|
1175
1275
|
- release the concrete lock with lock request queue;
|
|
1176
1276
|
- queue will be relased first;
|
|
1277
|
+
- (the enabled lock index) the lock name is dropped from the [Lock Index](#lock-index) in the same transaction;
|
|
1177
1278
|
- has an alias: `#release_lock`;
|
|
1178
1279
|
- accepts:
|
|
1179
1280
|
- `lock_name` - (required) `[String]` - the lock name that should be released.
|
|
@@ -1253,10 +1354,15 @@ rql.unlock("your_lock_name")
|
|
|
1253
1354
|
- suitable for read locks obtained without a block of code (`lock(..., read_write_mode: :read)` without a block);
|
|
1254
1355
|
- the current acquirer is calculated in the same way as for `#lock` (`#current_acquirer_id`: process, thread, fiber, ractor, identity),
|
|
1255
1356
|
so call it from the same thread/fiber that obtained the read lock;
|
|
1357
|
+
- the read lock of a different acquirer can be released via `:acquirer_id` (for example, extracted from `#lock_info`/`#locks_info`);
|
|
1358
|
+
`:identity` is ignored in this case (the acquirer id already contains its own identity);
|
|
1256
1359
|
- has an alias: `#release_read_lock`;
|
|
1257
1360
|
- accepts:
|
|
1258
1361
|
- `lock_name` - (required) `[String]` - the lock name whose read lock should be released;
|
|
1362
|
+
- `:acquirer_id` - (optional) `[String, NilClass]` - the acquirer whose read lock should be released
|
|
1363
|
+
(`nil` by default: the current acquirer is used);
|
|
1259
1364
|
- `:identity` - (optional) `[String]` - the same identity as for `#lock` (`uniq_identity` by default);
|
|
1365
|
+
ignored when `:acquirer_id` is passed: the acquirer id already contains its own identity;
|
|
1260
1366
|
- `:logger`, `:instrumenter`, `:instrument`, `:log_sampling_enabled`, `:log_sampling_percent`, `:log_sampler`, `:log_sample_this`,
|
|
1261
1367
|
`:instr_sampling_enabled`, `:instr_sampling_percent`, `:instr_sampler`, `:instr_sample_this` - (optional) - the same as for `#unlock`;
|
|
1262
1368
|
- returns `{ ok: true, result: { rel_time:, rel_key:, rel_acq_id:, lock_res: } }` (`lock_res` - `:released` or `:nothing_to_release`);
|
|
@@ -1278,6 +1384,22 @@ rql.unlock_read('report')
|
|
|
1278
1384
|
}
|
|
1279
1385
|
```
|
|
1280
1386
|
|
|
1387
|
+
```ruby
|
|
1388
|
+
# release the read lock of a different acquirer
|
|
1389
|
+
reader = rql.lock_info('report')['readers'].first
|
|
1390
|
+
rql.unlock_read('report', acquirer_id: reader['acq_id'])
|
|
1391
|
+
# =>
|
|
1392
|
+
{
|
|
1393
|
+
ok: true,
|
|
1394
|
+
result: {
|
|
1395
|
+
rel_time: 0.29, # milliseconds
|
|
1396
|
+
rel_key: 'rql:lock:report',
|
|
1397
|
+
rel_acq_id: 'rql:acq:17580/2260/2380/2280/3f16b93973612580', # reader['acq_id']
|
|
1398
|
+
lock_res: :released # or :nothing_to_release
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1281
1403
|
---
|
|
1282
1404
|
|
|
1283
1405
|
#### #clear_locks - release all locks and lock queues
|
|
@@ -1285,11 +1407,17 @@ rql.unlock_read('report')
|
|
|
1285
1407
|
<sup>\[[back to top](#usage)\]</sup>
|
|
1286
1408
|
|
|
1287
1409
|
- release all obtained locks and related lock request queues;
|
|
1288
|
-
- queues
|
|
1410
|
+
- all lock keys (locks, read locks and lock queues) are found in one `SCAN` pass and deleted during it with one `DEL` per batch;
|
|
1411
|
+
- (the enabled [Lock Index](#lock-index)) all locks are found in one `ZSCAN` pass over the lock index (the keyspace is never
|
|
1412
|
+
scanned): all the keys of each batch of locks (locks, read locks and lock queues) are deleted with one `DEL` in one transaction
|
|
1413
|
+
with the removal of their lock names from the lock index; keys without the lock index entry (keys of older RQL versions) are not
|
|
1414
|
+
found (see [#reindex_locks](#reindex_locks));
|
|
1289
1415
|
- has an alias: `#release_locks`;
|
|
1290
1416
|
- accepts:
|
|
1291
1417
|
- `:batch_size` - (optional) `[Integer]`
|
|
1292
|
-
- the size of batch of locks and lock queus that should be cleared under the one
|
|
1418
|
+
- the size of batch of locks and lock queus that should be cleared under the one `DEL` redis command at once (and the `SCAN` count);
|
|
1419
|
+
- (the enabled lock index) the number of locks (lock names) whose keys should be cleared under the one `DEL` redis command
|
|
1420
|
+
at once (and the `ZSCAN` count);
|
|
1293
1421
|
- pre-configured in `config['lock_release_batch_size']`;
|
|
1294
1422
|
- `:logger` - (optional) `[::Logger,#debug]`
|
|
1295
1423
|
- custom logger object;
|
|
@@ -1366,10 +1494,13 @@ rql.clear_locks
|
|
|
1366
1494
|
- `:acquirer_id` - (required) `[String]`
|
|
1367
1495
|
- acquirer indentifier whose locks we want to release and which we want to remove from all locks queues;
|
|
1368
1496
|
- `:lock_scan_size` - (optional) `[Integer]`
|
|
1369
|
-
- how many items will be released at a time (uses `SCAN
|
|
1497
|
+
- how many items will be released at a time (uses one `SCAN` pass for batch extraction and `DEL` for batch deletion);
|
|
1498
|
+
- (the enabled [Lock Index](#lock-index)) how many locks will be processed at a time (one `ZSCAN` pass over the lock index
|
|
1499
|
+
for batch extraction and `DEL` for batch deletion);
|
|
1370
1500
|
- pre-configuerd value in `config['clear_locks_of__lock_scan_size']`;
|
|
1371
1501
|
- `:queue_scan_size` - (optional) `[Integer]`
|
|
1372
|
-
- how many queues will be
|
|
1502
|
+
- how many lock queues will be processed in one round trip (lock queues are found by the same `SCAN` pass as locks or
|
|
1503
|
+
derived from the lock names of the same lock index pass);
|
|
1373
1504
|
- pre-configuerd value in `config['clear_locks_of__queue_scan_size']`;
|
|
1374
1505
|
- `:logger` - (optional) `[::Logger,#debug]`
|
|
1375
1506
|
- custom logger object;
|
|
@@ -1471,10 +1602,13 @@ rql.clear_current_locks
|
|
|
1471
1602
|
```
|
|
1472
1603
|
- produces `"redis_queued_locks.release_locks_of"` instrumentation event;
|
|
1473
1604
|
- `:lock_scan_size` - (optional) `[Integer]`
|
|
1474
|
-
- how many items will be released at a time (uses `SCAN
|
|
1605
|
+
- how many items will be released at a time (uses one `SCAN` pass for batch extraction and `DEL` for batch deletion);
|
|
1606
|
+
- (the enabled [Lock Index](#lock-index)) how many locks will be processed at a time (one `ZSCAN` pass over the lock index
|
|
1607
|
+
for batch extraction and `DEL` for batch deletion);
|
|
1475
1608
|
- pre-configuerd value in `config['clear_locks_of__lock_scan_size']`;
|
|
1476
1609
|
- `:queue_scan_size` - (optional) `[Integer]`
|
|
1477
|
-
- how many queues will be
|
|
1610
|
+
- how many lock queues will be processed in one round trip (lock queues are found by the same `SCAN` pass as locks or
|
|
1611
|
+
derived from the lock names of the same lock index pass);
|
|
1478
1612
|
- pre-configuerd value in `config['clear_locks_of__queue_scan_size']`;
|
|
1479
1613
|
- `:logger` - (optional) `[::Logger,#debug]`
|
|
1480
1614
|
- custom logger object;
|
|
@@ -1543,6 +1677,8 @@ rql.release_locks_of(host_id: rql.current_host_id, acquirer_id: rql.current_acqu
|
|
|
1543
1677
|
|
|
1544
1678
|
- extends lock ttl by the required number of milliseconds;
|
|
1545
1679
|
- expects the lock name and the number of milliseconds;
|
|
1680
|
+
- (the enabled lock index) the lock index score of the lock is increased by the same number of milliseconds
|
|
1681
|
+
(see [Lock Index](#lock-index));
|
|
1546
1682
|
- accepts:
|
|
1547
1683
|
- `lock_name` - (required) `[String]`
|
|
1548
1684
|
- the lock name which ttl should be extended;
|
|
@@ -1638,8 +1774,11 @@ rql.extend_lock_ttl("my_lock", 5_000) # NOTE: add 5_000 milliseconds
|
|
|
1638
1774
|
|
|
1639
1775
|
- get list of obtained locks;
|
|
1640
1776
|
- uses redis `SCAN` under the hood;
|
|
1777
|
+
- (the enabled [Lock Index](#lock-index)) uses the lock index under the hood (one `ZSCAN` pass, the keyspace is never scanned):
|
|
1778
|
+
keys without the lock index entry (keys of older RQL versions) are not found (see [#reindex_locks](#reindex_locks));
|
|
1641
1779
|
- accepts:
|
|
1642
|
-
- `:scan_size` - `Integer` - (`config['key_extraction_batch_size']` by default)
|
|
1780
|
+
- `:scan_size` - `Integer` - (`config['key_extraction_batch_size']` by default) - the `SCAN` count (or the `ZSCAN` count and
|
|
1781
|
+
the batch size of locks with the enabled lock index);
|
|
1643
1782
|
- `:with_info` - `Boolean` - `false` by default (for details see [#locks_info](#locks_info---get-list-of-locks-with-their-info));
|
|
1644
1783
|
- returns:
|
|
1645
1784
|
- `Set<String>` (for `with_info: false`);
|
|
@@ -1671,8 +1810,11 @@ rql.locks # or rql.locks(scan_size: 123)
|
|
|
1671
1810
|
|
|
1672
1811
|
- get list of lock request queues;
|
|
1673
1812
|
- uses redis `SCAN` under the hood;
|
|
1813
|
+
- (the enabled [Lock Index](#lock-index)) uses the lock index under the hood (one `ZSCAN` pass, the keyspace is never scanned):
|
|
1814
|
+
keys without the lock index entry (keys of older RQL versions) are not found (see [#reindex_locks](#reindex_locks));
|
|
1674
1815
|
- accepts
|
|
1675
|
-
- `:scan_size` - `Integer` - (`config['key_extraction_batch_size']` by default)
|
|
1816
|
+
- `:scan_size` - `Integer` - (`config['key_extraction_batch_size']` by default) - the `SCAN` count (or the `ZSCAN` count and
|
|
1817
|
+
the batch size of locks with the enabled lock index);
|
|
1676
1818
|
- `:with_info` - `Boolean` - `false` by default (for details see [#queues_info](#queues_info---get-list-of-queues-with-their-info));
|
|
1677
1819
|
- returns:
|
|
1678
1820
|
- `Set<String>` (for `with_info: false`);
|
|
@@ -1702,10 +1844,15 @@ rql.queues # or rql.queues(scan_size: 123)
|
|
|
1702
1844
|
|
|
1703
1845
|
<sup>\[[back to top](#usage)\]</sup>
|
|
1704
1846
|
|
|
1705
|
-
- get list of taken locks and queues
|
|
1706
|
-
|
|
1847
|
+
- get list of taken locks and queues (all the existing keys of the locks: locks, lock queues, read locks registries,
|
|
1848
|
+
read lock queues, read lock data);
|
|
1849
|
+
- uses redis `SCAN` under the hood (all `rql:lock*` keys);
|
|
1850
|
+
- (the enabled [Lock Index](#lock-index)) uses the lock index under the hood (one `ZSCAN` pass, the keyspace is never scanned):
|
|
1851
|
+
keys without the lock index entry (keys of older RQL versions) and other `rql:lock*` keys are not listed
|
|
1852
|
+
(see [#reindex_locks](#reindex_locks));
|
|
1707
1853
|
- accepts:
|
|
1708
|
-
`:scan_size` - `Integer` - (`config['key_extraction_batch_size']` by default)
|
|
1854
|
+
`:scan_size` - `Integer` - (`config['key_extraction_batch_size']` by default) - the `SCAN` count (or the `ZSCAN` count and
|
|
1855
|
+
the batch size of locks with the enabled lock index);
|
|
1709
1856
|
- returns: `Set<String>`
|
|
1710
1857
|
|
|
1711
1858
|
```ruby
|
|
@@ -1737,7 +1884,8 @@ rql.keys # or rql.keys(scan_size: 123)
|
|
|
1737
1884
|
<sup>\[[back to top](#usage)\]</sup>
|
|
1738
1885
|
|
|
1739
1886
|
- get list of locks with their info;
|
|
1740
|
-
- uses redis `SCAN` under the hod
|
|
1887
|
+
- uses redis `SCAN` under the hod (one pass) or the [Lock Index](#lock-index) (one `ZSCAN` pass, the keyspace is never scanned:
|
|
1888
|
+
the enabled lock index), lock info is extracted in batches of `scan_size` locks (two round trips per batch);
|
|
1741
1889
|
- accepts `scan_size:`/`Integer` option (`config['key_extraction_batch_size']` by default);
|
|
1742
1890
|
- returns `Set<Hash<Symbol,Any>>` (see [#lock_info](#lock_info) and examples below for details).
|
|
1743
1891
|
- contained data: `{ lock: String, status: Symbol, info: Hash<String,Any> }`;
|
|
@@ -1775,7 +1923,8 @@ rql.locks_info # or rql.locks_info(scan_size: 123)
|
|
|
1775
1923
|
<sup>\[[back to top](#usage)\]</sup>
|
|
1776
1924
|
|
|
1777
1925
|
- get list of queues with their info;
|
|
1778
|
-
- uses redis `SCAN` under the hod
|
|
1926
|
+
- uses redis `SCAN` under the hod (one pass) or the [Lock Index](#lock-index) (one `ZSCAN` pass, the keyspace is never scanned:
|
|
1927
|
+
the enabled lock index), queue info is extracted in batches of `scan_size` queues (one round trip per batch);
|
|
1779
1928
|
- accepts `scan_size:`/`Integer` option (`config['key_extraction_batch_size']` by default);
|
|
1780
1929
|
- returns `Set<Hash<Symbol,Any>>` (see [#queue_info](#queue_info) and examples below for details).
|
|
1781
1930
|
- contained data: `{ queue: String, requests: Array<Hash<String,Any>> }`
|
|
@@ -1810,8 +1959,14 @@ before the lock acquire moment occurs and when no any other process does not nee
|
|
|
1810
1959
|
For this case your lock reuquest will be cleared only when any process will try
|
|
1811
1960
|
to acquire this lock again (cuz lock acquirement triggers the removement of expired requests).
|
|
1812
1961
|
|
|
1962
|
+
NOTE: with the enabled [Lock Index](#lock-index) the lock request queue lives as long as its latest lock request (the queue TTL
|
|
1963
|
+
is refreshed with the request `queue_ttl` on each lock request), so the queue of dead requests expires by itself (queues of older
|
|
1964
|
+
RQL versions and queues created with the disabled lock index have no TTL: see [#reindex_locks](#reindex_locks)).
|
|
1965
|
+
|
|
1813
1966
|
In order to help with these dead requests you may periodically call `#clear_dead_requests`
|
|
1814
1967
|
with corresponding `:dead_ttl` option, that is pre-configured by default via `config['dead_request_ttl']`.
|
|
1968
|
+
Lock queues are found via the redis `SCAN` (one pass) or via the [Lock Index](#lock-index) (one `ZSCAN` pass, the keyspace is
|
|
1969
|
+
never scanned: the enabled lock index).
|
|
1815
1970
|
|
|
1816
1971
|
`:dead_ttl` option is required because of it is no any **fast** and **resource-free** way to understand which request
|
|
1817
1972
|
is dead now and is it really dead cuz each request queue can host their requests with
|
|
@@ -1823,6 +1978,8 @@ Accepts:
|
|
|
1823
1978
|
- has a preconfigured value in `config['dead_request_ttl']` (1 day by default);
|
|
1824
1979
|
- `:scan_size` - (optional) `[Integer]`
|
|
1825
1980
|
- the batch of scanned keys for Redis'es SCAN command;
|
|
1981
|
+
- (the enabled lock index) the `ZSCAN` count of the [Lock Index](#lock-index) pass and the number of locks (both lock queues
|
|
1982
|
+
of each lock) processed in one round trip;
|
|
1826
1983
|
- has a preconfigured valie in `config['lock_release_batch_size']`;
|
|
1827
1984
|
- `:logger` - (optional) `[::Logger,#debug]`
|
|
1828
1985
|
- custom logger object;
|
|
@@ -1879,6 +2036,42 @@ rql.clear_dead_requests(dead_ttl: 60 * 60 * 1000) # 1 hour in milliseconds
|
|
|
1879
2036
|
|
|
1880
2037
|
---
|
|
1881
2038
|
|
|
2039
|
+
#### #reindex_locks
|
|
2040
|
+
|
|
2041
|
+
<sup>\[[back to top](#usage)\]</sup>
|
|
2042
|
+
|
|
2043
|
+
- adds the lock keys without the lock index entry (keys of older RQL versions and keys created with the disabled lock index)
|
|
2044
|
+
to the lock index (see [Lock Index](#lock-index));
|
|
2045
|
+
- invoke it once after the lock index is enabled (`config['lock_index_enabled'] = true`) on **all** your processes: with the
|
|
2046
|
+
enabled lock index all lock-iterating operations (the swarm, `#locks`, `#queues`, `#keys`, `#clear_locks`, `#clear_locks_of`,
|
|
2047
|
+
`#clear_dead_requests`, ...) find locks via the lock index only;
|
|
2048
|
+
- works regardless of `config['lock_index_enabled']` (it is not needed while the lock index is disabled: the lock index is
|
|
2049
|
+
neither used nor maintained then);
|
|
2050
|
+
- one `SCAN` pass over the RQL keys (`rql:lock*`) with pipelined batches (two round trips per batch);
|
|
2051
|
+
- write locks and read locks registries are indexed with their remaining TTL (+ the grace period), lock queues without TTL
|
|
2052
|
+
(older RQL versions) get the `queue_ttl` TTL first; read lock data is not indexed (it is found via the read locks registry);
|
|
2053
|
+
- idempotent: the lock index scores are never decreased (it can be invoked at any time);
|
|
2054
|
+
- accepts:
|
|
2055
|
+
- `:scan_size` - (optional) `[Integer]`
|
|
2056
|
+
- the batch of scanned keys for Redis'es SCAN command;
|
|
2057
|
+
- pre-configured in `config['lock_release_batch_size']`;
|
|
2058
|
+
- `:queue_ttl` - (optional) `[Integer]`
|
|
2059
|
+
- TTL of the lock queues without TTL (in seconds);
|
|
2060
|
+
- pre-configured in `config['default_queue_ttl']`;
|
|
2061
|
+
- `:lock_index_grace_period` - (optional) `[Integer]`
|
|
2062
|
+
- the lock index grace period (in milliseconds);
|
|
2063
|
+
- pre-configured in `config['lock_index_grace_period']`;
|
|
2064
|
+
- returns `{ ok: true, result: { idx_key_cnt: Integer } }` (the number of indexed keys);
|
|
2065
|
+
|
|
2066
|
+
```ruby
|
|
2067
|
+
rql.reindex_locks
|
|
2068
|
+
|
|
2069
|
+
# =>
|
|
2070
|
+
{ ok: true, result: { idx_key_cnt: 123 } }
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
---
|
|
2074
|
+
|
|
1882
2075
|
#### #current_acquirer_id
|
|
1883
2076
|
|
|
1884
2077
|
<sup>\[[back to top](#usage)\]</sup>
|
|
@@ -2005,6 +2198,12 @@ rql.possible_host_ids
|
|
|
2005
2198
|
|
|
2006
2199
|
**This documentation section is in progress!** (see the changelog and the usage preview for details at this moment)
|
|
2007
2200
|
|
|
2201
|
+
- zombie locks and zombie lock requests are found in one `SCAN` pass over the RQL keys (`rql:lock*`) or, with the enabled
|
|
2202
|
+
[Lock Index](#lock-index) (`config['lock_index_enabled']`), in one `ZSCAN` pass over the lock names: the swarm never scans
|
|
2203
|
+
the whole Redis keyspace then (keys of your other applications are never visited);
|
|
2204
|
+
- after enabling the lock index invoke [#reindex_locks](#reindex_locks) once (when all your processes use the lock index):
|
|
2205
|
+
locks created without the lock index are not indexed and are not found by the swarm until then;
|
|
2206
|
+
|
|
2008
2207
|
[(work and usage preview (temporary example-based docs))](#work-and-usage-preview-temporary-example-based-docs)
|
|
2009
2208
|
|
|
2010
2209
|
- [How to Swarm](#how-to-swarm)
|
|
@@ -2052,8 +2251,8 @@ rql.possible_host_ids
|
|
|
2052
2251
|
config['swarm.flush_zombies.enabled_for_swarm'] = true # NOTE: run zombie flushing or not
|
|
2053
2252
|
config['swarm.flush_zombies.zombie_flush_period'] = 10 # NOTE: (in seconds) period of time when the zombie flusher is triggered
|
|
2054
2253
|
config['swarm.flush_zombies.zombie_ttl'] = 15_000 # NOTE: (in milliseconds) when the lock/host/acquirer is considered a zombie
|
|
2055
|
-
config['swarm.flush_zombies.zombie_lock_scan_size'] = 500 # NOTE:
|
|
2056
|
-
config['swarm.flush_zombies.zombie_queue_scan_size'] = 500 # NOTE:
|
|
2254
|
+
config['swarm.flush_zombies.zombie_lock_scan_size'] = 500 # NOTE: SCAN count (or ZSCAN count with the enabled lock index) and the batch size of locks during zombie flushing
|
|
2255
|
+
config['swarm.flush_zombies.zombie_queue_scan_size'] = 500 # NOTE: the batch size of lock queues during zombie flushing (one round trip per batch)
|
|
2057
2256
|
# (flush_zombies) individual redis config
|
|
2058
2257
|
config['swarm.flush_zombies.redis_config.sentinel'] = false # NOTE: individual redis config
|
|
2059
2258
|
config['swarm.flush_zombies.redis_config.pooled'] = false # NOTE: individual redis config
|
|
@@ -2188,6 +2387,120 @@ rql.possible_host_ids
|
|
|
2188
2387
|
```
|
|
2189
2388
|
</details>
|
|
2190
2389
|
|
|
2390
|
+
<details>
|
|
2391
|
+
<summary>lock index key in Redis (the enabled lock index only)</summary>
|
|
2392
|
+
|
|
2393
|
+
```ruby
|
|
2394
|
+
"rql:index:locks"
|
|
2395
|
+
```
|
|
2396
|
+
</details>
|
|
2397
|
+
|
|
2398
|
+
---
|
|
2399
|
+
|
|
2400
|
+
## Lock Index
|
|
2401
|
+
|
|
2402
|
+
<sup>\[[back to top](#table-of-contents)\]</sup>
|
|
2403
|
+
|
|
2404
|
+
> Find locks without scanning the whole Redis keyspace (opt-in: `config['lock_index_enabled'] = true`).
|
|
2405
|
+
|
|
2406
|
+
> [!IMPORTANT]
|
|
2407
|
+
> Your locks should work either **always with** the lock index or **always without** it (the same
|
|
2408
|
+
> `config['lock_index_enabled']` value on all your processes that share the Redis database): locks obtained without the lock
|
|
2409
|
+
> index are not indexed and are invisible to the lock-iterating operations of the processes with the enabled lock index
|
|
2410
|
+
> (the swarm, `#locks`, `#queues`, `#keys`, `#clear_locks`, ...). So every time you enable the lock index after working without it
|
|
2411
|
+
> (including the upgrade from older RQL versions) invoke [#reindex_locks](#reindex_locks) once all your processes use the lock index.
|
|
2412
|
+
|
|
2413
|
+
- **disabled by default**: lock-iterating operations scan the Redis keyspace (`SCAN MATCH rql:lock*`, the classic way),
|
|
2414
|
+
no lock index commands are sent and lock request queues have no TTL; enable the lock index via config:
|
|
2415
|
+
|
|
2416
|
+
```ruby
|
|
2417
|
+
RedisQueuedLocks::Client.new(redis_client) do |config|
|
|
2418
|
+
config['lock_index_enabled'] = true
|
|
2419
|
+
# (milliseconds) (default: 60_000)
|
|
2420
|
+
# - the lock name stays in the lock index for this period after the expiration of all its keys;
|
|
2421
|
+
# - why: the lock index scores use the client clock, so this margin protects the index from the clock skew
|
|
2422
|
+
# between your hosts and from the network latency (otherwise a live lock can drop out of the index and
|
|
2423
|
+
# the swarm/lock-iterating operations will not see it);
|
|
2424
|
+
# - keep it greater than the maximal clock skew between your hosts plus the network latency;
|
|
2425
|
+
config['lock_index_grace_period'] = 60_000
|
|
2426
|
+
end
|
|
2427
|
+
```
|
|
2428
|
+
- the client-wide mode (not a per-call option): the lock acquirement maintains the lock index and all lock-iterating operations
|
|
2429
|
+
read it only when it is enabled; use the same mode on **all** your processes that share the Redis database (locks of
|
|
2430
|
+
the processes without the lock index are not indexed: lock-iterating operations of the processes with the lock index do not
|
|
2431
|
+
see them); the swarm reads the mode when it is started;
|
|
2432
|
+
- the lock index is a sorted set `rql:index:locks`: the member is the lock name, the score is the upper bound of the lifetime
|
|
2433
|
+
of all lock keys (write lock, read locks registry, lock request queues): `client time (epoch, ms) + key ttl + grace period`;
|
|
2434
|
+
- all lock-iterating operations iterate over the lock index (`ZSCAN`) and derive lock keys from the lock names instead of
|
|
2435
|
+
the full keyspace `SCAN`: their cost depends on the number of your locks only (not on the size of your Redis database):
|
|
2436
|
+
- the swarm: `#flush_zombies`, the zombie flusher, `#zombie_locks`, `#zombie_acquirers`, `#zombies_info`;
|
|
2437
|
+
- `#locks`/`#locks_info`, `#queues`/`#queues_info`, `#keys`, `#clear_locks`, `#clear_locks_of`/`#clear_current_locks`,
|
|
2438
|
+
`#clear_dead_requests`;
|
|
2439
|
+
- only [#reindex_locks](#reindex_locks) scans the keyspace (it adds the keys without the lock index entries to the lock index);
|
|
2440
|
+
- the index is maintained by the lock acquirement itself (no background jobs, no additional Redis round trips):
|
|
2441
|
+
- each lock request adds the lock name to the index (`ZADD GT` with the queue ttl), sets the lock request queue TTL
|
|
2442
|
+
(the queue lives as long as its latest lock request) and drops the expired lock names from the index (`ZREMRANGEBYSCORE`):
|
|
2443
|
+
4 commands in the existing queueing pipeline;
|
|
2444
|
+
- each lock obtaining (write and read) adds the lock name with the lock ttl in the same transaction (1 command);
|
|
2445
|
+
- each lock extension (`#extend_lock_ttl`, `:extendable_work_through` reentrant locks) increases the score by the extension;
|
|
2446
|
+
- the lock name is dropped from the index when all its keys are expired and the grace period is over, by `#unlock` (in the
|
|
2447
|
+
release transaction) and by `#clear_locks` (in the transaction that deletes the keys of the lock);
|
|
2448
|
+
- `config['lock_index_grace_period']` (`60_000` milliseconds by default) and `lock_index_grace_period:` option of
|
|
2449
|
+
`#lock`/`#lock!`/`#lock_series`/`#lock_series!`: the lock name stays in the index for this period after the expiration of
|
|
2450
|
+
all its keys; it should be greater than the maximal client clock skew between your hosts plus the duration of one lock
|
|
2451
|
+
acquirement attempt (network latency) (otherwise a lock can be dropped from the index before its expiration and the swarm
|
|
2452
|
+
will not see it);
|
|
2453
|
+
- (enabling) locks of older RQL versions and locks created with the disabled lock index have no lock index entries (they are not
|
|
2454
|
+
found by lock-iterating operations till then): enable the lock index on **all** your processes, then invoke
|
|
2455
|
+
[#reindex_locks](#reindex_locks) once;
|
|
2456
|
+
- (disabling) lock-iterating operations scan the keyspace again; the lock index key is neither used nor maintained anymore
|
|
2457
|
+
(you can delete it: `DEL rql:index:locks`); invoke [#reindex_locks](#reindex_locks) again after the next enabling;
|
|
2458
|
+
- (limitation) the lock index is one Redis key written on each lock request (a "hot" key): fine for a standalone Redis
|
|
2459
|
+
(and Sentinel); Redis Cluster is not supported (RQL transactions use several keys of each lock without hash tags);
|
|
2460
|
+
|
|
2461
|
+
#### Lock Index Performance
|
|
2462
|
+
|
|
2463
|
+
<sup>\[[back to top](#lock-index)\]</sup>
|
|
2464
|
+
|
|
2465
|
+
- measured on a local Redis with 1 million keys of other applications in the same database and ~1 200 locks (+100 lock queues
|
|
2466
|
+
for the lock-iterating operations, a zombie host for `#flush_zombies`): wall time and Redis CPU time of one call with
|
|
2467
|
+
the keyspace `SCAN` (`config['lock_index_enabled'] = false`) and with the lock index (`config['lock_index_enabled'] = true`):
|
|
2468
|
+
|
|
2469
|
+
| Operation | Keyspace `SCAN` (wall / Redis CPU) | Lock index (wall / Redis CPU) | Speedup (wall / Redis CPU) |
|
|
2470
|
+
|---|---|---|---|
|
|
2471
|
+
| `#flush_zombies` (the zombie flusher) | 263 ms / 181 ms | 20 ms / 2.4 ms | **x13** / **x75** |
|
|
2472
|
+
| `#locks` | 269 ms / 192 ms | 7.7 ms / 1.4 ms | **x35** / **x137** |
|
|
2473
|
+
| `#locks_info` | 288 ms / 194 ms | 23 ms / 2.3 ms | **x12.5** / **x84** |
|
|
2474
|
+
| `#queues` / `#queues_info` | 260-280 ms / 190 ms | 8 ms / 1-2 ms | **x32-35** / **x95-190** |
|
|
2475
|
+
| `#keys` | 272 ms / 193 ms | 17 ms / 2.6 ms | **x16** / **x74** |
|
|
2476
|
+
| `#clear_dead_requests` | 574 ms / 224 ms | 18 ms / 2.2 ms | **x32** / **x102** |
|
|
2477
|
+
| `#clear_locks_of` / `#clear_current_locks` | 322 ms / 200 ms | 14 ms / 1.9 ms | **x23** / **x105** |
|
|
2478
|
+
| `#clear_locks` | 574 ms / 225 ms | 14 ms / 5.1 ms | **x41** / **x44** |
|
|
2479
|
+
|
|
2480
|
+
- the speedup grows with the number of keys in your Redis database: the keyspace `SCAN` visits all keys of the database (keys
|
|
2481
|
+
of your other applications too), the lock index visits the lock names only (the cost depends on the number of your locks and
|
|
2482
|
+
on the lock index size, see [Lock Index Size](#lock-index-size));
|
|
2483
|
+
- the zombie flusher runs in every process with the swarm (every `zombie_flush_period`): the saved Redis CPU is multiplied by
|
|
2484
|
+
the number of your processes;
|
|
2485
|
+
- the price is paid by the lock acquirement (it maintains the lock index without new round trips: 4 more commands in
|
|
2486
|
+
the queueing pipeline and 1 more command in the obtaining transaction): ~10% fewer locks per second in a single-thread
|
|
2487
|
+
`lock {}` loop on a local Redis, 280 -> 348 allocations per `lock {}`;
|
|
2488
|
+
|
|
2489
|
+
#### Lock Index Size
|
|
2490
|
+
|
|
2491
|
+
<sup>\[[back to top](#lock-index)\]</sup>
|
|
2492
|
+
|
|
2493
|
+
- the lock index size is about `<the number of distinct lock names per second> * (max(queue_ttl, ttl) + grace period)`
|
|
2494
|
+
(`~75` seconds of lock names with the default settings: `config['default_queue_ttl']` (15 seconds) +
|
|
2495
|
+
`config['lock_index_grace_period']` (60 seconds));
|
|
2496
|
+
- with very diverse lock names (for example, `order:#{id}`) lock-iterating operations (the swarm, `#locks`, `#queues`, `#keys`,
|
|
2497
|
+
`#clear_locks`, ...) visit the dead lock names of this window too (lock names whose keys are already expired);
|
|
2498
|
+
- if it becomes noticeable, there are two levers:
|
|
2499
|
+
- reduce `config['lock_index_grace_period']` (keep it greater than the maximal client clock skew between your hosts plus
|
|
2500
|
+
the network latency);
|
|
2501
|
+
- **TODO**: a cheap existence check of the lock name (`EXISTS` of its keys) before reading the lock details in
|
|
2502
|
+
lock-iterating operations;
|
|
2503
|
+
|
|
2191
2504
|
---
|
|
2192
2505
|
|
|
2193
2506
|
## Lock Access Strategies
|
|
@@ -2235,10 +2548,11 @@ rql.lock('report') { rebuild_report } # (write mode is the default mode)
|
|
|
2235
2548
|
- `ttl` should be a positive `Integer` (`RedisQueuedLocks::ArgumentError`);
|
|
2236
2549
|
- each read lock has its own data with the same structure as the write lock data: acquirer, host, timestamp,
|
|
2237
2550
|
initial ttl, `:meta` (with the same reserved keys) and reentrant lock data (`spc_cnt`, `spc_ext_ttl`, ...);
|
|
2238
|
-
the read lock data lives as long as the read lock and is returned by `#lock_info`/`#locks_info` for each reader
|
|
2551
|
+
the read lock data lives as long as the read lock and is returned by `#lock_info`/`#locks_info` for each reader
|
|
2552
|
+
(acquirer and host are not stored: both are derived from the acquirer id);
|
|
2239
2553
|
- `#unlock` releases the lock entirely: the write lock, all read locks (with their data) and all lock requests;
|
|
2240
|
-
- `#unlock_read` (aka `#release_read_lock`) releases the read lock of the current acquirer
|
|
2241
|
-
(suitable for read locks obtained without a block)
|
|
2554
|
+
- `#unlock_read` (aka `#release_read_lock`) releases the read lock of the current acquirer
|
|
2555
|
+
(suitable for read locks obtained without a block), or the read lock of the acquirer passed via `acquirer_id:`;
|
|
2242
2556
|
- `#extend_lock_ttl(lock_name, milliseconds, read_write_mode: :read)` extends the read lock of the current acquirer only;
|
|
2243
2557
|
`all_read_locks: true` extends all live read locks of the lock;
|
|
2244
2558
|
- `#lock_series` with `read_write_mode: :read` obtains a series of read locks (the mode is the same for all locks of the series):
|
|
@@ -2284,7 +2598,7 @@ rql.lock('report') { rebuild_report } # (write mode is the default mode)
|
|
|
2284
2598
|
- `rql:lock_queue:<lock_name>` - write lock requests (`ZSET`, the classic RQL lock queue);
|
|
2285
2599
|
- `rql:lock_readers:<lock_name>` - read locks (`ZSET`: acquirer id => read lock expiration time in Redis time, milliseconds);
|
|
2286
2600
|
- `rql:lock_read_queue:<lock_name>` - read lock requests (`ZSET`);
|
|
2287
|
-
- `rql:lock_reader:<lock_name>:<acquirer_id>` - read lock data (`HASH`, the same structure as the write lock, read lock ttl);
|
|
2601
|
+
- `rql:lock_reader:<lock_name>:<acquirer_id>` - read lock data (`HASH`, the same structure as the write lock without acquirer and host ids, read lock ttl);
|
|
2288
2602
|
- implementation notes:
|
|
2289
2603
|
- optimistic transactions only (`WATCH`/`MULTI`): readers watch the write lock key, writers watch the write lock key and the read locks key,
|
|
2290
2604
|
so readers never invalidate transactions of each other;
|