redis_queued_locks 1.21.0 → 1.23.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 +294 -51
- data/.claude/rules/acquirer.md +16 -11
- data/.claude/rules/arguments.md +2 -2
- data/.claude/rules/changelog.md +27 -0
- data/.claude/rules/git.md +1 -1
- data/.claude/rules/logic.md +14 -15
- data/.claude/rules/swarm.md +3 -4
- data/.claude/rules/tests.md +9 -9
- data/.claude/rules/type-checking.md +14 -16
- data/.claude/rules/visitors.md +11 -9
- data/.claude/skills/rql-code-review/SKILL.md +129 -47
- data/.ruby-version +1 -1
- data/CHANGELOG.md +122 -245
- data/CLAUDE.md +12 -10
- data/README.md +516 -175
- data/Rakefile +11 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +4 -4
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +28 -12
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +38 -16
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +92 -15
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +945 -272
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +82 -32
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +240 -45
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +15 -13
- data/lib/redis_queued_locks/acquirer/keys.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_series/instr_visitor.rb +243 -0
- data/lib/redis_queued_locks/acquirer/lock_series/log_visitor.rb +472 -0
- data/lib/redis_queued_locks/acquirer/lock_series/release_lock_series.rb +181 -0
- data/lib/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rb +1051 -0
- data/lib/redis_queued_locks/acquirer/lock_series.rb +948 -0
- data/lib/redis_queued_locks/acquirer/locks.rb +6 -2
- data/lib/redis_queued_locks/acquirer/queue_info.rb +30 -7
- data/lib/redis_queued_locks/acquirer/queues.rb +17 -6
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +6 -2
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +6 -2
- data/lib/redis_queued_locks/acquirer.rb +1 -1
- data/lib/redis_queued_locks/client.rb +116 -44
- data/lib/redis_queued_locks/config.rb +93 -1
- data/lib/redis_queued_locks/resource.rb +124 -9
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +6 -5
- data/lib/redis_queued_locks/swarm/zombie_info.rb +6 -2
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +4 -0
- data/rbs_collection.yaml +2 -1
- data/redis_queued_locks.gemspec +21 -14
- data/sig/manifest.yml +1 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +12 -6
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +21 -14
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +61 -40
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +160 -11
- data/sig/redis_queued_locks/acquirer/acquire_lock/{yield_with_expire → yield_expire}/log_visitor.rbs +4 -4
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock.rbs +5 -3
- data/sig/redis_queued_locks/acquirer/clear_dead_requests.rbs +14 -1
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +2 -2
- data/sig/redis_queued_locks/acquirer/lock_series/instr_visitor.rbs +102 -0
- data/sig/redis_queued_locks/acquirer/lock_series/log_visitor.rbs +154 -0
- data/sig/redis_queued_locks/acquirer/lock_series/release_lock_series.rbs +37 -0
- data/sig/redis_queued_locks/acquirer/lock_series/try_to_lock_series.rbs +198 -0
- data/sig/redis_queued_locks/acquirer/lock_series.rbs +106 -0
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/client.rbs +77 -7
- data/sig/redis_queued_locks/config.rbs +54 -1
- data/sig/redis_queued_locks/instrument/sampler.rbs +3 -1
- data/sig/redis_queued_locks/instrument/void_notifier.rbs +3 -1
- data/sig/redis_queued_locks/logging/sampler.rbs +3 -1
- data/sig/redis_queued_locks/resource.rbs +22 -12
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +1 -1
- data/sig/redis_queued_locks/utilities.rbs +2 -1
- data/sig/redis_queued_locks.rbs +7 -0
- metadata +35 -13
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +0 -56
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +0 -95
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +0 -484
- /data/sig/redis_queued_locks/{acquier.rbs → acquirer.rbs} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
-
# @version 1.
|
|
5
|
+
# @version 1.23.0
|
|
6
6
|
# rubocop:disable Metrics/ModuleLength
|
|
7
7
|
# rubocop:disable Metrics/MethodLength
|
|
8
8
|
# rubocop:disable Metrics/ClassLength
|
|
@@ -106,7 +106,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
106
106
|
# - `:extendable_work_through`;
|
|
107
107
|
# - `:wait_for_lock`;
|
|
108
108
|
# - `:dead_locking`;
|
|
109
|
-
# @option
|
|
109
|
+
# @option rw_mode [Symbol]
|
|
110
110
|
# - `:write` (default) - exclusive lock: waits for the write lock and for all read locks;
|
|
111
111
|
# - `:read` - shared lock: waits for the write lock only (read locks do not block each other);
|
|
112
112
|
# - `:queued` access strategy orders read and write requests in FIFO between modes
|
|
@@ -126,6 +126,22 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
126
126
|
# (but with checking the limist, retries, timeouts and so on). if lock is
|
|
127
127
|
# free to obtain - it will be obtained;
|
|
128
128
|
# - pre-configured in `config['default_access_strategy']`;
|
|
129
|
+
# @option priority [Integer,Symbol]
|
|
130
|
+
# - The lock request priority: from `RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY` (`-100`)
|
|
131
|
+
# to `RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY` (`100`), `0` by default, or a priority
|
|
132
|
+
# name of `lock_priorities` (resolved to its Integer priority: the lock request queues,
|
|
133
|
+
# the logs and the instrumentation use the Integer priority);
|
|
134
|
+
# - `:queued` access strategy: the lock request is placed in the lock request queue after all
|
|
135
|
+
# requests of the same or higher priority and before all requests of the lower priority
|
|
136
|
+
# (strict priority, FIFO inside the same priority; between modes too: the read and write
|
|
137
|
+
# lock request queues use the same positions);
|
|
138
|
+
# - `:random` access strategy: the lock request does not wait for its turn (it ignores the
|
|
139
|
+
# order of the lock request queues and the priorities of other requests), but it is queued
|
|
140
|
+
# at the position of its priority like a `:queued` one: `:queued` lock requests wait for it;
|
|
141
|
+
# - the obtained lock is never taken away: the priority orders the waiting lock requests only;
|
|
142
|
+
# - pre-configured in `config['default_lock_priority']`;
|
|
143
|
+
# @option lock_priorities [Hash<Symbol,Integer>]
|
|
144
|
+
# - The priority names (`config['lock_priorities']`): a priority name => its Integer priority;
|
|
129
145
|
# @option log_sampling_enabled [Boolean]
|
|
130
146
|
# - enables <log sampling>: only the configured percent of RQL cases will be logged;
|
|
131
147
|
# - disabled by default;
|
|
@@ -177,7 +193,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
177
193
|
#
|
|
178
194
|
# @api private
|
|
179
195
|
# @since 1.0.0
|
|
180
|
-
# @version 1.
|
|
196
|
+
# @version 1.23.0
|
|
181
197
|
def acquire_lock(
|
|
182
198
|
redis,
|
|
183
199
|
lock_name,
|
|
@@ -204,8 +220,10 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
204
220
|
logger:,
|
|
205
221
|
log_lock_try:,
|
|
206
222
|
conflict_strategy:,
|
|
207
|
-
|
|
223
|
+
rw_mode:,
|
|
208
224
|
access_strategy:,
|
|
225
|
+
priority:,
|
|
226
|
+
lock_priorities:,
|
|
209
227
|
log_sampling_enabled:,
|
|
210
228
|
log_sampling_percent:,
|
|
211
229
|
log_sampler:,
|
|
@@ -227,12 +245,14 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
227
245
|
)
|
|
228
246
|
end
|
|
229
247
|
|
|
230
|
-
# Step 0.1 (RW): prevent :
|
|
231
|
-
|
|
248
|
+
# Step 0.1 (RW): prevent :rw_mode incompatabilities
|
|
249
|
+
case rw_mode # NOTE: (steep) case/when narrows `Symbol` to `RQL::readWriteMode` below
|
|
250
|
+
when :read, :write then nil
|
|
251
|
+
else
|
|
232
252
|
raise(
|
|
233
253
|
RedisQueuedLocks::ArgumentError,
|
|
234
|
-
"`:
|
|
235
|
-
"got #{
|
|
254
|
+
"`:rw_mode` argument should be `:read` or `:write`, " \
|
|
255
|
+
"got #{rw_mode.inspect}."
|
|
236
256
|
)
|
|
237
257
|
end
|
|
238
258
|
|
|
@@ -292,6 +312,27 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
292
312
|
)
|
|
293
313
|
end
|
|
294
314
|
|
|
315
|
+
# Step 0.1: prevent :priority incompatibilities (the same range as its config key)
|
|
316
|
+
# - a Symbol is a priority name of `config['lock_priorities']`: it is resolved to its
|
|
317
|
+
# Integer priority (the lock request queues, the logs and the instrumentation use the
|
|
318
|
+
# Integer priority; an unknown name stays a Symbol and fails the check);
|
|
319
|
+
# - each priority has its own band of positions in the lock request queues: the range
|
|
320
|
+
# bounds the shift of the positions (float scores lose precision with the shift: the FIFO
|
|
321
|
+
# order inside a priority keeps ~61 µs for -100..100, see `LOCK_PRIORITY_BAND`);
|
|
322
|
+
priority = lock_priorities.fetch(priority, priority) if priority.is_a?(::Symbol)
|
|
323
|
+
if !priority.is_a?(::Integer) ||
|
|
324
|
+
priority < RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY ||
|
|
325
|
+
priority > RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY
|
|
326
|
+
raise(
|
|
327
|
+
RedisQueuedLocks::ArgumentError,
|
|
328
|
+
"`:priority` argument should be an Integer from " \
|
|
329
|
+
"#{RedisQueuedLocks::Resource::MIN_LOCK_PRIORITY} to " \
|
|
330
|
+
"#{RedisQueuedLocks::Resource::MAX_LOCK_PRIORITY} or a priority name of " \
|
|
331
|
+
"`config['lock_priorities']` (#{lock_priorities.keys.map(&:inspect).join(', ')}), " \
|
|
332
|
+
"got #{priority.inspect}."
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
295
336
|
# Step 0.2: prevent :meta incompatabiltiies (structure)
|
|
296
337
|
# - Symbol keys are written to the lock data as Strings (`Symbol#name`, as redis-client
|
|
297
338
|
# does it) and would overwrite the reserved lock data fields too;
|
|
@@ -342,11 +383,13 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
342
383
|
# NOTE: (RW) read lock data key is required by read lock requests only (write lock requests
|
|
343
384
|
# never obtain read locks: read-to-write lock upgrade is not supported);
|
|
344
385
|
read_lock_key =
|
|
345
|
-
if
|
|
386
|
+
if rw_mode == :read
|
|
346
387
|
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
347
388
|
end
|
|
348
389
|
|
|
349
|
-
|
|
390
|
+
# NOTE: the position in the lock request queue: the enqueue time shifted by the priority band
|
|
391
|
+
# (see `RedisQueuedLocks::Resource::LOCK_PRIORITY_BAND`);
|
|
392
|
+
acquirer_position = RedisQueuedLocks::Resource.calc_initial_acquirer_position(priority)
|
|
350
393
|
|
|
351
394
|
log_sampled = RedisQueuedLocks::Logging.should_log?(
|
|
352
395
|
log_sampling_enabled,
|
|
@@ -379,7 +422,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
379
422
|
redis,
|
|
380
423
|
logger,
|
|
381
424
|
lock_key,
|
|
382
|
-
|
|
425
|
+
rw_mode,
|
|
383
426
|
lock_key_queue,
|
|
384
427
|
read_lock_key_queue,
|
|
385
428
|
queue_ttl,
|
|
@@ -393,7 +436,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
393
436
|
|
|
394
437
|
LogVisitor.start_lock_obtaining(
|
|
395
438
|
logger, log_sampled, lock_key,
|
|
396
|
-
queue_ttl, acquirer_id, host_id, access_strategy,
|
|
439
|
+
queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
|
|
397
440
|
)
|
|
398
441
|
|
|
399
442
|
# Step 2: try to lock with timeout
|
|
@@ -414,17 +457,18 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
414
457
|
|
|
415
458
|
LogVisitor.start_try_to_lock_cycle(
|
|
416
459
|
logger, log_sampled, lock_key,
|
|
417
|
-
queue_ttl, acquirer_id, host_id, access_strategy,
|
|
460
|
+
queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
|
|
418
461
|
)
|
|
419
462
|
|
|
420
463
|
# Step 2.X: check the actual score: is it in queue ttl limit or not?
|
|
421
|
-
if RedisQueuedLocks::Resource.dead_score_reached?(acquirer_position, queue_ttl)
|
|
422
|
-
# Step 2.X.X: dead score reached => re-queue the lock request with the new score
|
|
423
|
-
|
|
464
|
+
if RedisQueuedLocks::Resource.dead_score_reached?(acquirer_position, priority, queue_ttl)
|
|
465
|
+
# Step 2.X.X: dead score reached => re-queue the lock request with the new score
|
|
466
|
+
# (the end of the requests of the same priority);
|
|
467
|
+
acquirer_position = RedisQueuedLocks::Resource.calc_initial_acquirer_position(priority)
|
|
424
468
|
|
|
425
469
|
LogVisitor.dead_score_reached__reset_acquirer_position(
|
|
426
470
|
logger, log_sampled, lock_key,
|
|
427
|
-
queue_ttl, acquirer_id, host_id, access_strategy,
|
|
471
|
+
queue_ttl, acquirer_id, host_id, access_strategy, rw_mode, priority
|
|
428
472
|
)
|
|
429
473
|
end
|
|
430
474
|
|
|
@@ -435,7 +479,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
435
479
|
logger,
|
|
436
480
|
log_lock_try,
|
|
437
481
|
lock_key,
|
|
438
|
-
|
|
482
|
+
rw_mode,
|
|
439
483
|
lock_key_queue,
|
|
440
484
|
read_lock_key_queue,
|
|
441
485
|
lock_readers_key,
|
|
@@ -444,6 +488,7 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
444
488
|
acquirer_id,
|
|
445
489
|
host_id,
|
|
446
490
|
acquirer_position,
|
|
491
|
+
priority,
|
|
447
492
|
lock_ttl,
|
|
448
493
|
queue_ttl,
|
|
449
494
|
lock_index_enabled,
|
|
@@ -477,10 +522,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
477
522
|
# instrumetnation: (reentrant lock with ttl extension)
|
|
478
523
|
LogVisitor.extendable_reentrant_lock_obtained(
|
|
479
524
|
logger, log_sampled, result[:lock_key],
|
|
480
|
-
queue_ttl, acquirer_id, host_id, acq_time, access_strategy,
|
|
525
|
+
queue_ttl, acquirer_id, host_id, acq_time, access_strategy, rw_mode,
|
|
526
|
+
priority
|
|
481
527
|
)
|
|
482
528
|
InstrVisitor.extendable_reentrant_lock_obtained(
|
|
483
|
-
instrumenter, instr_sampled, result[:lock_key],
|
|
529
|
+
instrumenter, instr_sampled, result[:lock_key], rw_mode, priority,
|
|
484
530
|
result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
|
|
485
531
|
instrument
|
|
486
532
|
)
|
|
@@ -488,10 +534,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
488
534
|
# instrumetnation: (reentrant lock without ttl extension)
|
|
489
535
|
LogVisitor.reentrant_lock_obtained(
|
|
490
536
|
logger, log_sampled, result[:lock_key],
|
|
491
|
-
queue_ttl, acquirer_id, host_id, acq_time, access_strategy,
|
|
537
|
+
queue_ttl, acquirer_id, host_id, acq_time, access_strategy, rw_mode,
|
|
538
|
+
priority
|
|
492
539
|
)
|
|
493
540
|
InstrVisitor.reentrant_lock_obtained(
|
|
494
|
-
instrumenter, instr_sampled, result[:lock_key],
|
|
541
|
+
instrumenter, instr_sampled, result[:lock_key], rw_mode, priority,
|
|
495
542
|
result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
|
|
496
543
|
instrument
|
|
497
544
|
)
|
|
@@ -500,10 +547,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
500
547
|
# NOTE: classic is: acq_process[:result][:process] == :lock_obtaining
|
|
501
548
|
LogVisitor.lock_obtained(
|
|
502
549
|
logger, log_sampled, result[:lock_key],
|
|
503
|
-
queue_ttl, acquirer_id, host_id, acq_time, access_strategy,
|
|
550
|
+
queue_ttl, acquirer_id, host_id, acq_time, access_strategy, rw_mode,
|
|
551
|
+
priority
|
|
504
552
|
)
|
|
505
553
|
InstrVisitor.lock_obtained(
|
|
506
|
-
instrumenter, instr_sampled, result[:lock_key],
|
|
554
|
+
instrumenter, instr_sampled, result[:lock_key], rw_mode, priority,
|
|
507
555
|
result[:ttl], result[:acq_id], result[:hst_id], result[:ts], acq_time,
|
|
508
556
|
instrument
|
|
509
557
|
)
|
|
@@ -660,7 +708,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
660
708
|
instrumenter,
|
|
661
709
|
instr_sampled,
|
|
662
710
|
acq_process[:lock_info][:lock_key],
|
|
663
|
-
|
|
711
|
+
rw_mode,
|
|
712
|
+
priority,
|
|
664
713
|
acq_process[:lock_info][:ttl],
|
|
665
714
|
acq_process[:lock_info][:acq_id],
|
|
666
715
|
acq_process[:lock_info][:hst_id],
|
|
@@ -675,7 +724,8 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
675
724
|
instrumenter,
|
|
676
725
|
instr_sampled,
|
|
677
726
|
acq_process[:lock_info][:lock_key],
|
|
678
|
-
|
|
727
|
+
rw_mode,
|
|
728
|
+
priority,
|
|
679
729
|
acq_process[:lock_info][:ttl],
|
|
680
730
|
acq_process[:lock_info][:acq_id],
|
|
681
731
|
acq_process[:lock_info][:hst_id],
|
|
@@ -705,11 +755,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
705
755
|
acq_time = ((clock_gettime - acq_start_time) / 1_000.0).ceil(2)
|
|
706
756
|
LogVisitor.lock_obtaining_failed(
|
|
707
757
|
logger, log_sampled, lock_key, queue_ttl, acquirer_id, host_id,
|
|
708
|
-
acq_time, access_strategy,
|
|
758
|
+
acq_time, access_strategy, rw_mode, priority, acq_process[:result]
|
|
709
759
|
)
|
|
710
760
|
InstrVisitor.lock_obtaining_failed(
|
|
711
|
-
instrumenter, instr_sampled, lock_key,
|
|
712
|
-
Time.now.to_f, acq_time, acq_process[:result], instrument
|
|
761
|
+
instrumenter, instr_sampled, lock_key, rw_mode, priority, lock_ttl, acquirer_id,
|
|
762
|
+
host_id, Time.now.to_f, acq_time, acq_process[:result], instrument
|
|
713
763
|
)
|
|
714
764
|
{ ok: false, result: acq_process[:result] } #: { ok: bool, result: Symbol }
|
|
715
765
|
end
|
|
@@ -730,11 +780,11 @@ module RedisQueuedLocks::Acquirer::AcquireLock
|
|
|
730
780
|
acq_time = ((clock_gettime - acq_start_time) / 1_000.0).ceil(2)
|
|
731
781
|
LogVisitor.lock_obtaining_failed(
|
|
732
782
|
logger, log_sampled, lock_key, queue_ttl, acquirer_id, host_id,
|
|
733
|
-
acq_time, access_strategy,
|
|
783
|
+
acq_time, access_strategy, rw_mode, priority, acq_process[:result]
|
|
734
784
|
)
|
|
735
785
|
InstrVisitor.lock_obtaining_failed(
|
|
736
|
-
instrumenter, instr_sampled, lock_key,
|
|
737
|
-
Time.now.to_f, acq_time, acq_process[:result], instrument
|
|
786
|
+
instrumenter, instr_sampled, lock_key, rw_mode, priority, lock_ttl, acquirer_id,
|
|
787
|
+
host_id, Time.now.to_f, acq_time, acq_process[:result], instrument
|
|
738
788
|
)
|
|
739
789
|
end
|
|
740
790
|
# steep:ignore:end
|