redis_queued_locks 1.16.2 → 1.18.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 +281 -0
- data/.claude/rules/acquirer.md +81 -0
- data/.claude/rules/arguments.md +60 -0
- data/.claude/rules/git.md +46 -0
- data/.claude/rules/logic.md +63 -0
- data/.claude/rules/swarm.md +120 -0
- data/.claude/rules/tests.md +48 -0
- data/.claude/rules/type-checking.md +50 -0
- data/.claude/rules/visitors.md +69 -0
- data/.rubocop.rbs.yml +31 -0
- data/.rubocop.yml +3 -3
- data/.ruby-version +1 -1
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +36 -0
- data/README.md +367 -82
- data/Rakefile +31 -2
- data/github_ci/.keep +0 -0
- data/lib/redis_queued_locks/acquirer/acquire_lock/delay_execution.rb +2 -2
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +6 -3
- data/lib/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rb +9 -7
- data/lib/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rb +20 -10
- data/lib/redis_queued_locks/acquirer/acquire_lock/log_visitor.rb +34 -16
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rb +240 -28
- data/lib/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rb +398 -132
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire/log_visitor.rb +12 -6
- data/lib/redis_queued_locks/acquirer/acquire_lock/yield_expire.rb +33 -9
- data/lib/redis_queued_locks/acquirer/acquire_lock.rb +73 -24
- data/lib/redis_queued_locks/acquirer/clear_dead_requests.rb +9 -1
- data/lib/redis_queued_locks/acquirer/extend_lock_ttl.rb +182 -3
- data/lib/redis_queued_locks/acquirer/is_locked.rb +19 -2
- data/lib/redis_queued_locks/acquirer/is_queued.rb +6 -2
- data/lib/redis_queued_locks/acquirer/lock_info.rb +96 -6
- data/lib/redis_queued_locks/acquirer/lock_series_poc/instr_visitor.rb +8 -4
- data/lib/redis_queued_locks/acquirer/lock_series_poc/log_visitor.rb +18 -7
- data/lib/redis_queued_locks/acquirer/lock_series_poc.rb +126 -29
- data/lib/redis_queued_locks/acquirer/locks.rb +103 -6
- data/lib/redis_queued_locks/acquirer/queue_info.rb +38 -9
- data/lib/redis_queued_locks/acquirer/queues.rb +28 -4
- data/lib/redis_queued_locks/acquirer/release_all_locks.rb +35 -1
- data/lib/redis_queued_locks/acquirer/release_lock.rb +40 -9
- data/lib/redis_queued_locks/acquirer/release_locks_of.rb +43 -3
- data/lib/redis_queued_locks/acquirer/release_read_lock.rb +143 -0
- data/lib/redis_queued_locks/acquirer.rb +1 -0
- data/lib/redis_queued_locks/client.rb +274 -158
- data/lib/redis_queued_locks/config/dsl.rb +4 -10
- data/lib/redis_queued_locks/resource.rb +117 -9
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +46 -6
- data/lib/redis_queued_locks/swarm/supervisor.rb +1 -1
- data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +166 -44
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +45 -47
- data/lib/redis_queued_locks/swarm/zombie_info.rb +42 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/rbs_collection.lock.yaml +10 -2
- data/rbs_collection.yaml +0 -2
- data/redis_queued_locks.gemspec +1 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue/log_visitor.rbs +2 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/dequeue_from_lock_queue.rbs +0 -1
- data/sig/redis_queued_locks/acquirer/acquire_lock/instr_visitor.rbs +5 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/log_visitor.rbs +12 -6
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock/log_visitor.rbs +74 -7
- data/sig/redis_queued_locks/acquirer/acquire_lock/try_to_lock.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_expire.rbs +3 -0
- data/sig/redis_queued_locks/acquirer/acquire_lock/yield_with_expire/log_visitor.rbs +4 -2
- data/sig/redis_queued_locks/acquirer/extend_lock_ttl.rbs +19 -1
- data/sig/redis_queued_locks/acquirer/lock_info.rbs +9 -1
- data/sig/redis_queued_locks/acquirer/locks.rbs +10 -1
- data/sig/redis_queued_locks/acquirer/queue_info.rbs +2 -4
- data/sig/redis_queued_locks/acquirer/queues.rbs +1 -1
- data/sig/redis_queued_locks/acquirer/release_lock.rbs +8 -1
- data/sig/redis_queued_locks/acquirer/release_read_lock.rbs +44 -0
- data/sig/redis_queued_locks/client.rbs +22 -2
- data/sig/redis_queued_locks/resource.rbs +9 -2
- data/sig/redis_queued_locks/swarm/flush_zombies.rbs +1 -1
- data/sig/redis_queued_locks/swarm/swarm_element/isolated.rbs +13 -5
- data/sig/redis_queued_locks/swarm/swarm_element/threaded.rbs +6 -6
- data/sig/redis_queued_locks/swarm/zombie_info.rbs +5 -0
- metadata +17 -5
- data/github_ci/ruby3.3.gemfile +0 -17
- data/github_ci/ruby3.3.gemfile.lock +0 -216
|
@@ -6,29 +6,23 @@ module RedisQueuedLocks::Config::DSL
|
|
|
6
6
|
# @api private
|
|
7
7
|
# @since 1.13.0
|
|
8
8
|
module ClassMethods
|
|
9
|
-
# NOTE:
|
|
10
|
-
# 1. Style/DefWithParentheses rubocop's cop incorrectly drops `()` from method definition
|
|
11
|
-
# and breaks ruby code syntax. So this cop is disabled here;
|
|
12
|
-
# 2. attr_reader is not used cuz `steep` can't understand it form inside the class where
|
|
9
|
+
# NOTE: attr_reader is not used cuz `steep` can't understand it form inside the class where
|
|
13
10
|
# the current module is mixed;
|
|
14
11
|
#
|
|
15
12
|
# @return [Hash<String,Block>]
|
|
16
13
|
#
|
|
17
14
|
# @api private
|
|
18
15
|
# @since 1.13.0
|
|
19
|
-
def config_setters
|
|
16
|
+
def config_setters = @config_setters
|
|
20
17
|
|
|
21
|
-
# NOTE:
|
|
22
|
-
# 1. Style/DefWithParentheses rubocop's cop incorrectly drops `()` from method definition
|
|
23
|
-
# and breaks ruby code syntax. So this cop is disabled here;
|
|
24
|
-
# 2. attr_reader is not used cuz `steep` can't understand it form inside the class where
|
|
18
|
+
# NOTE: attr_reader is not used cuz `steep` can't understand it form inside the class where
|
|
25
19
|
# the current module is mixed;
|
|
26
20
|
#
|
|
27
21
|
# @return [Hash<String,Block>]
|
|
28
22
|
#
|
|
29
23
|
# @api private
|
|
30
24
|
# @since 1.13.0
|
|
31
|
-
def config_validators
|
|
25
|
+
def config_validators = @config_validators
|
|
32
26
|
|
|
33
27
|
# @param config_key [String]
|
|
34
28
|
# @param validator [Block]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.0.0
|
|
5
|
+
# rubocop:disable Metrics/ModuleLength
|
|
5
6
|
module RedisQueuedLocks::Resource
|
|
6
7
|
# @return [String]
|
|
7
8
|
#
|
|
@@ -21,17 +22,30 @@ module RedisQueuedLocks::Resource
|
|
|
21
22
|
# @since 1.0.0
|
|
22
23
|
LOCK_QUEUE_PATTERN = 'rql:lock_queue:*'
|
|
23
24
|
|
|
25
|
+
# NOTE:
|
|
26
|
+
# - read/write keys use their own prefixes (not suffixes of the lock name) cuz the lock name
|
|
27
|
+
# is an arbitrary string: `rql:lock_queue:<name>:read` collides with the queue of a lock
|
|
28
|
+
# named "<name>:read";
|
|
29
|
+
# - new prefixes are not matched by LOCK_PATTERN/LOCK_QUEUE_PATTERN
|
|
30
|
+
# (`rql:lock_` vs `rql:lock:`) but are matched by KEY_PATTERN;
|
|
31
|
+
# - write lock requests use the classic lock queue (LOCK_QUEUE_PATTERN);
|
|
24
32
|
# @return [String]
|
|
25
33
|
#
|
|
26
34
|
# @api private
|
|
27
|
-
# @since
|
|
28
|
-
READ_LOCK_QUEUE_PATTERN = 'rql:
|
|
35
|
+
# @since 1.18.0
|
|
36
|
+
READ_LOCK_QUEUE_PATTERN = 'rql:lock_read_queue:*'
|
|
29
37
|
|
|
30
38
|
# @return [String]
|
|
31
39
|
#
|
|
32
40
|
# @api private
|
|
33
|
-
# @since
|
|
34
|
-
|
|
41
|
+
# @since 1.18.0
|
|
42
|
+
LOCK_READERS_PATTERN = 'rql:lock_readers:*'
|
|
43
|
+
|
|
44
|
+
# @return [String]
|
|
45
|
+
#
|
|
46
|
+
# @api private
|
|
47
|
+
# @since 1.18.0
|
|
48
|
+
READ_LOCK_PATTERN = 'rql:lock_reader:*'
|
|
35
49
|
|
|
36
50
|
# @return [String]
|
|
37
51
|
#
|
|
@@ -105,22 +119,115 @@ module RedisQueuedLocks::Resource
|
|
|
105
119
|
"rql:lock_queue:#{lock_name}"
|
|
106
120
|
end
|
|
107
121
|
|
|
122
|
+
# Queue of read lock requests (write lock requests use the classic lock queue).
|
|
123
|
+
#
|
|
108
124
|
# @param lock_name [String]
|
|
109
125
|
# @return [String]
|
|
110
126
|
#
|
|
111
127
|
# @api private
|
|
112
|
-
# @
|
|
128
|
+
# @since 1.18.0
|
|
113
129
|
def prepare_read_lock_queue(lock_name)
|
|
114
|
-
"rql:
|
|
130
|
+
"rql:lock_read_queue:#{lock_name}"
|
|
115
131
|
end
|
|
116
132
|
|
|
133
|
+
# Registry of read lock holders: a sorted set where the member is an acquirer id and
|
|
134
|
+
# the score is the read lock expiration time (redis server time, epoch milliseconds).
|
|
135
|
+
#
|
|
117
136
|
# @param lock_name [String]
|
|
118
137
|
# @return [String]
|
|
119
138
|
#
|
|
120
139
|
# @api private
|
|
121
|
-
# @
|
|
122
|
-
def
|
|
123
|
-
"rql:
|
|
140
|
+
# @since 1.18.0
|
|
141
|
+
def prepare_lock_readers(lock_name)
|
|
142
|
+
"rql:lock_readers:#{lock_name}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Read lock data of the concrete acquirer: a hash with the same structure as the write lock
|
|
146
|
+
# (acquirer, host, timestamp, ttl, meta, reentrant lock data). It is a data carrier only:
|
|
147
|
+
# the readers registry is the source of truth for the read lock existence.
|
|
148
|
+
#
|
|
149
|
+
# @param lock_name [String]
|
|
150
|
+
# @param acquirer_id [String]
|
|
151
|
+
# @return [String]
|
|
152
|
+
#
|
|
153
|
+
# @api private
|
|
154
|
+
# @since 1.18.0
|
|
155
|
+
def prepare_read_lock_key(lock_name, acquirer_id)
|
|
156
|
+
"rql:lock_reader:#{lock_name}:#{acquirer_id}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Mode of the lock requests stored in the given lock queue.
|
|
160
|
+
#
|
|
161
|
+
# @param lock_queue [String]
|
|
162
|
+
# Write lock queue (`rql:lock_queue:*`) or read lock queue (`rql:lock_read_queue:*`).
|
|
163
|
+
# @return [String,NilClass]
|
|
164
|
+
# - `'write'` for the write lock queue, `'read'` for the read lock queue;
|
|
165
|
+
# - `nil` for any other key (unreachable: lock queues are always passed consistently);
|
|
166
|
+
#
|
|
167
|
+
# @api private
|
|
168
|
+
# @since 1.18.0
|
|
169
|
+
def lock_queue_rw_mode(lock_queue)
|
|
170
|
+
# NOTE:
|
|
171
|
+
# - `start_with?` instead of regexp `when`-matchers: no MatchData allocations;
|
|
172
|
+
# - write lock queues are checked first: they are met more often than read lock queues;
|
|
173
|
+
case
|
|
174
|
+
when lock_queue.start_with?('rql:lock_queue:') then 'write'
|
|
175
|
+
when lock_queue.start_with?('rql:lock_read_queue:') then 'read'
|
|
176
|
+
else
|
|
177
|
+
# NOTE: unreachable: the logic always passes a consistent lock queue (read or write)
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @param lock_readers_key [String]
|
|
183
|
+
# @return [String]
|
|
184
|
+
#
|
|
185
|
+
# @api private
|
|
186
|
+
# @since 1.18.0
|
|
187
|
+
def lock_name_from_readers(lock_readers_key)
|
|
188
|
+
lock_readers_key.delete_prefix('rql:lock_readers:')
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @param lock_readers_key [String]
|
|
192
|
+
# @return [String]
|
|
193
|
+
#
|
|
194
|
+
# @api private
|
|
195
|
+
# @since 1.18.0
|
|
196
|
+
def lock_key_from_readers(lock_readers_key)
|
|
197
|
+
"rql:lock:#{lock_readers_key.delete_prefix('rql:lock_readers:')}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Host identifier of the given acquirer
|
|
201
|
+
# (acquirer and host identifiers are built from the same process/thread/ractor/identity data):
|
|
202
|
+
# - acquirer id: `rql:acq:<pid>/<thread>/<fiber>/<ractor>/<identity>`;
|
|
203
|
+
# - host id: `rql:hst:<pid>/<thread>/<ractor>/<identity>` (the same as `host_identifier`);
|
|
204
|
+
#
|
|
205
|
+
# @param acquirer_id [String]
|
|
206
|
+
# @return [String]
|
|
207
|
+
#
|
|
208
|
+
# @api private
|
|
209
|
+
# @since 1.18.0
|
|
210
|
+
def host_identifier_from_acquirer(acquirer_id)
|
|
211
|
+
# NOTE:
|
|
212
|
+
# - the host id is the acquirer id with another prefix and without the fiber part, so it is
|
|
213
|
+
# built in place from the acquirer id copy (one allocation: no intermediate strings and
|
|
214
|
+
# arrays of `delete_prefix`/`split`, no substrings);
|
|
215
|
+
# - 8 is the size of the `rql:acq:` prefix;
|
|
216
|
+
# - byte positions are used cuz identity can contain any (multibyte) symbols;
|
|
217
|
+
thread_end = acquirer_id.byteindex('/', acquirer_id.byteindex('/', 8).to_i + 1).to_i
|
|
218
|
+
fiber_end = acquirer_id.byteindex('/', thread_end + 1).to_i
|
|
219
|
+
host_id = acquirer_id.dup
|
|
220
|
+
host_id.bytesplice(thread_end, fiber_end - thread_end, '') # NOTE: drop "/<fiber>"
|
|
221
|
+
host_id.bytesplice(0, 8, 'rql:hst:')
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# @param redis_time [Array<String>] Result of the redis `TIME` command (seconds, microseconds).
|
|
225
|
+
# @return [Integer] Redis server time (epoch, in milliseconds).
|
|
226
|
+
#
|
|
227
|
+
# @api private
|
|
228
|
+
# @since 1.18.0
|
|
229
|
+
def redis_time_ms(redis_time)
|
|
230
|
+
(Integer(redis_time[0]) * 1_000) + (Integer(redis_time[1]) / 1_000)
|
|
124
231
|
end
|
|
125
232
|
|
|
126
233
|
# @return [Float] Redis's <Set> score that is calculated from the time (epoch) as a float.
|
|
@@ -241,3 +348,4 @@ module RedisQueuedLocks::Resource
|
|
|
241
348
|
end
|
|
242
349
|
end
|
|
243
350
|
end
|
|
351
|
+
# rubocop:enable Metrics/ModuleLength
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# @api private
|
|
4
4
|
# @since 1.9.0
|
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
|
5
6
|
class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
6
7
|
class << self
|
|
7
8
|
# @param redis_client [RedisClient]
|
|
@@ -18,6 +19,7 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
18
19
|
#
|
|
19
20
|
# @api private
|
|
20
21
|
# @since 1.9.0
|
|
22
|
+
# @version 1.18.0
|
|
21
23
|
# rubocop:disable Metrics/MethodLength
|
|
22
24
|
def flush_zombies(
|
|
23
25
|
redis_client,
|
|
@@ -65,6 +67,32 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
65
67
|
# NOTE: (steep) steep can't use <Set>s for splats
|
|
66
68
|
rconn.call('DEL', *zombie_locks) if zombie_locks.any? # steep:ignore
|
|
67
69
|
|
|
70
|
+
# Step 3 (RW): find zombie read locks (read locks of zombie hosts) and drop them
|
|
71
|
+
# - read locks store acquirer ids only (the host is a part of the acquirer id);
|
|
72
|
+
# - readers registry is reported as a zombie lock if any of its read locks is dropped;
|
|
73
|
+
rconn.scan(
|
|
74
|
+
'MATCH', RedisQueuedLocks::Resource::LOCK_READERS_PATTERN, count: lock_scan_size
|
|
75
|
+
) do |lock_readers_key|
|
|
76
|
+
# @type var read_lock_acquirers: Array[String]
|
|
77
|
+
read_lock_acquirers = rconn.call('ZRANGE', lock_readers_key, '0', '-1')
|
|
78
|
+
zombie_readers = read_lock_acquirers.select do |acquirer_id|
|
|
79
|
+
zombie_hosts.include?(RedisQueuedLocks::Resource.host_identifier_from_acquirer(acquirer_id))
|
|
80
|
+
end
|
|
81
|
+
next if zombie_readers.empty?
|
|
82
|
+
|
|
83
|
+
rconn.call('ZREM', lock_readers_key, *zombie_readers)
|
|
84
|
+
# NOTE: drop the zombie read lock data
|
|
85
|
+
lock_name = RedisQueuedLocks::Resource.lock_name_from_readers(lock_readers_key)
|
|
86
|
+
rconn.call(
|
|
87
|
+
'DEL',
|
|
88
|
+
*zombie_readers.map do |acquirer_id|
|
|
89
|
+
RedisQueuedLocks::Resource.prepare_read_lock_key(lock_name, acquirer_id)
|
|
90
|
+
end
|
|
91
|
+
)
|
|
92
|
+
zombie_locks << lock_readers_key
|
|
93
|
+
zombie_acquirers.merge(zombie_readers)
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
# Step 4: find zombie requests => and drop them
|
|
69
97
|
# TODO: indexing (in order to prevent full database scan);
|
|
70
98
|
# NOTE: original redis does not support indexing so we need to use
|
|
@@ -77,6 +105,15 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
77
105
|
end
|
|
78
106
|
end
|
|
79
107
|
|
|
108
|
+
# Step 4 (RW): drop zombie requests from the queues of read lock requests
|
|
109
|
+
rconn.scan(
|
|
110
|
+
'MATCH', RedisQueuedLocks::Resource::READ_LOCK_QUEUE_PATTERN, count: queue_scan_size
|
|
111
|
+
) do |read_lock_queue|
|
|
112
|
+
zombie_acquirers.each do |zombie_acquirer|
|
|
113
|
+
rconn.call('ZREM', read_lock_queue, zombie_acquirer)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
80
117
|
# Step 5: drop zombies from the swarm
|
|
81
118
|
rconn.call('HDEL', RedisQueuedLocks::Resource::SWARM_KEY, *zombie_hosts)
|
|
82
119
|
|
|
@@ -100,19 +137,21 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
100
137
|
rql_client.config['swarm.flush_zombies.enabled_for_swarm']
|
|
101
138
|
end
|
|
102
139
|
|
|
103
|
-
# @
|
|
140
|
+
# @param swarm_element_results_port [Ractor::Port] Results port of the main Ractor.
|
|
141
|
+
# @return [Ractor]
|
|
104
142
|
#
|
|
105
143
|
# @api private
|
|
106
|
-
# @since 1.
|
|
107
|
-
def
|
|
108
|
-
|
|
144
|
+
# @since 1.17.0
|
|
145
|
+
def spawn_swarm_element!(swarm_element_results_port)
|
|
146
|
+
Ractor.new(
|
|
147
|
+
swarm_element_results_port,
|
|
109
148
|
rql_client.config.slice('swarm.flush_zombies.redis_config'),
|
|
110
149
|
rql_client.config['swarm.flush_zombies.zombie_ttl'],
|
|
111
150
|
rql_client.config['swarm.flush_zombies.zombie_lock_scan_size'],
|
|
112
151
|
rql_client.config['swarm.flush_zombies.zombie_queue_scan_size'],
|
|
113
152
|
rql_client.config['swarm.flush_zombies.zombie_flush_period']
|
|
114
|
-
) do |rc, z_ttl, z_lss, z_qss, z_fl_prd|
|
|
115
|
-
RedisQueuedLocks::Swarm::FlushZombies.swarm_loop do
|
|
153
|
+
) do |r_res_p, rc, z_ttl, z_lss, z_qss, z_fl_prd|
|
|
154
|
+
RedisQueuedLocks::Swarm::FlushZombies.swarm_loop(r_res_p) do
|
|
116
155
|
Thread.new do
|
|
117
156
|
redis_client = RedisQueuedLocks::Swarm::RedisClientBuilder.build(
|
|
118
157
|
pooled: rc['pooled'],
|
|
@@ -132,3 +171,4 @@ class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElem
|
|
|
132
171
|
end
|
|
133
172
|
end
|
|
134
173
|
end
|
|
174
|
+
# rubocop:enable Metrics/ClassLength
|
|
@@ -89,7 +89,7 @@ class RedisQueuedLocks::Swarm::Supervisor
|
|
|
89
89
|
|
|
90
90
|
{
|
|
91
91
|
running: running?,
|
|
92
|
-
state: (visor == nil) ? 'non_initialized' : thread_state(visor),
|
|
92
|
+
state: (visor == nil) ? 'non_initialized' : thread_state(visor),
|
|
93
93
|
observable: (observable == nil) ? 'non_initialized' : 'initialized'
|
|
94
94
|
}
|
|
95
95
|
end
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Swarm element isolated inside its own Ractor. Communication works via `Ractor::Port`s:
|
|
4
|
+
# - `swarm_element_results_port`: created in the main Ractor (where the Swarm and its Supervisor
|
|
5
|
+
# live), receives element replies and the element ractor termination notice (`:exited`/`:aborted`,
|
|
6
|
+
# registered via `Ractor#monitor`);
|
|
7
|
+
# - `swarm_element_commands_port`: created inside the element ractor (only the creator ractor can
|
|
8
|
+
# receive from a port) and handed over to the main Ractor during the element startup;
|
|
9
|
+
# Each element instance owns its own pair of ports, so any number of isolated elements can work
|
|
10
|
+
# side by side.
|
|
11
|
+
#
|
|
3
12
|
# @api private
|
|
4
13
|
# @since 1.9.0
|
|
14
|
+
# @version 1.17.0
|
|
5
15
|
# rubocop:disable Metrics/ClassLength
|
|
6
16
|
class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
7
17
|
# @since 1.9.0
|
|
@@ -19,6 +29,22 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
|
19
29
|
# @since 1.9.0
|
|
20
30
|
attr_reader :swarm_element
|
|
21
31
|
|
|
32
|
+
# Port of the element ractor that receives commands (created inside the element ractor).
|
|
33
|
+
#
|
|
34
|
+
# @return [Ractor::Port,NilClass]
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
# @since 1.17.0
|
|
38
|
+
attr_reader :swarm_element_commands_port
|
|
39
|
+
|
|
40
|
+
# Port of the main Ractor that receives element replies and the element termination notice.
|
|
41
|
+
#
|
|
42
|
+
# @return [Ractor::Port,NilClass]
|
|
43
|
+
#
|
|
44
|
+
# @api private
|
|
45
|
+
# @since 1.17.0
|
|
46
|
+
attr_reader :swarm_element_results_port
|
|
47
|
+
|
|
22
48
|
# @return [RedisQueuedLocks::Utilities::Lock]
|
|
23
49
|
#
|
|
24
50
|
# @api private
|
|
@@ -30,9 +56,12 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
|
30
56
|
#
|
|
31
57
|
# @api private
|
|
32
58
|
# @since 1.9.0
|
|
59
|
+
# @version 1.17.0
|
|
33
60
|
def initialize(rql_client)
|
|
34
61
|
@rql_client = rql_client
|
|
35
62
|
@swarm_element = nil
|
|
63
|
+
@swarm_element_commands_port = nil
|
|
64
|
+
@swarm_element_results_port = nil
|
|
36
65
|
@sync = RedisQueuedLocks::Utilities::Lock.new
|
|
37
66
|
end
|
|
38
67
|
|
|
@@ -100,19 +129,18 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
|
100
129
|
#
|
|
101
130
|
# @api private
|
|
102
131
|
# @since 1.9.0
|
|
132
|
+
# @version 1.17.0
|
|
103
133
|
def status
|
|
104
134
|
sync.synchronize do
|
|
105
135
|
ractor_running = swarmed__alive?
|
|
106
136
|
ractor_state = swarmed? ? ractor_status(swarm_element) : 'non_initialized' # steep:ignore
|
|
107
137
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
rescue Ractor::ClosedError
|
|
115
|
-
# NOTE: it can happend when you running RedisQueuedLocks::Swarm#deswarm!
|
|
138
|
+
# NOTE: `nil` when the element ractor is not alive (or has died during the request);
|
|
139
|
+
loop_status = swarm_loop__status
|
|
140
|
+
if loop_status && loop_status[:alive]
|
|
141
|
+
main_loop_running = true
|
|
142
|
+
main_loop_state = loop_status[:state]
|
|
143
|
+
else
|
|
116
144
|
main_loop_running = false
|
|
117
145
|
main_loop_state = 'non_initialized'
|
|
118
146
|
end
|
|
@@ -134,38 +162,79 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
|
134
162
|
private
|
|
135
163
|
|
|
136
164
|
# Swarm element lifecycle have the following scheme:
|
|
137
|
-
# => 1) init (swarm!): create a ractor
|
|
138
|
-
#
|
|
139
|
-
# =>
|
|
140
|
-
# =>
|
|
165
|
+
# => 1) init (swarm!): create a results port and a ractor (main loop is not started),
|
|
166
|
+
# receive the ractor's command port;
|
|
167
|
+
# => 2) start (swarm_loop__start): run the main loop inside the ractor;
|
|
168
|
+
# => 3) stop (swarm_loop__stop): stop the main loop inside the ractor;
|
|
169
|
+
# => 4) kill (swarm_loop__kill): kill the main loop inside the ractor and finish the ractor;
|
|
141
170
|
#
|
|
142
171
|
# @return [void]
|
|
143
172
|
#
|
|
144
173
|
# @api private
|
|
145
174
|
# @since 1.9.0
|
|
175
|
+
# @version 1.17.0
|
|
146
176
|
def swarm!
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
# NOTE: the results port is created in the current (main) Ractor: only it can receive from it;
|
|
178
|
+
results = Ractor::Port.new
|
|
179
|
+
element = spawn_swarm_element!(results)
|
|
180
|
+
@swarm_element_results_port = results
|
|
181
|
+
@swarm_element = element
|
|
182
|
+
# NOTE: `:exited`/`:aborted` will be sent to the results port when the ractor is finished
|
|
183
|
+
# (immediately if it is already finished), so no request can wait for a reply forever;
|
|
184
|
+
element.monitor(results)
|
|
185
|
+
# NOTE: the first message from the element ractor is its command port (see .swarm_loop),
|
|
186
|
+
# any other message is the termination notice (the ractor has died during the startup);
|
|
187
|
+
handshake = results.receive
|
|
188
|
+
@swarm_element_commands_port = handshake.is_a?(Ractor::Port) ? handshake : nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @param swarm_element_results_port [Ractor::Port] Results port of the main Ractor.
|
|
192
|
+
# @return [Ractor]
|
|
193
|
+
#
|
|
194
|
+
# @api private
|
|
195
|
+
# @since 1.17.0
|
|
196
|
+
def spawn_swarm_element!(swarm_element_results_port)
|
|
197
|
+
# IMPORTANT №1: create and return a Ractor here (pass `swarm_element_results_port` and all
|
|
198
|
+
# required configs into `Ractor.new` as shareable/copyable values);
|
|
199
|
+
# IMPORTANT №2: your Ractor should invoke .swarm_loop(swarm_element_results_port) inside
|
|
200
|
+
# (see below);
|
|
201
|
+
# IMPORTANT №3: you should pass the main loop logic as a block to .swarm_loop
|
|
202
|
+
# (the block should return a Thread that wraps the looped logic);
|
|
203
|
+
raise NotImplementedError, "#{self.class}#spawn_swarm_element! is not implemented"
|
|
150
204
|
end
|
|
151
205
|
|
|
206
|
+
# Internal protocol (bare scalars/primitives, no `{ ok:, result: }` wrapper):
|
|
207
|
+
# - handshake: the command port itself (the first message on the results port);
|
|
208
|
+
# - `:status` => `{ alive: <Boolean>, state: <String> }`;
|
|
209
|
+
# - `:is_active` => `true`/`false`;
|
|
210
|
+
# - `:start`, `:stop`, `:kill` => `true` (ack);
|
|
211
|
+
# - Symbol messages on the results port are reserved for the Ractor#monitor notice;
|
|
212
|
+
#
|
|
213
|
+
# @param swarm_element_results_port [Ractor::Port] Results port of the main Ractor.
|
|
152
214
|
# @param main_loop_spawner [Block]
|
|
153
215
|
# @return [void]
|
|
154
216
|
#
|
|
155
217
|
# @api private
|
|
156
218
|
# @since 1.9.0
|
|
219
|
+
# @version 1.17.0
|
|
157
220
|
# rubocop:disable Layout/ClassStructure, Lint/IneffectiveAccessModifier, Metrics/MethodLength
|
|
158
|
-
def self.swarm_loop(&main_loop_spawner)
|
|
221
|
+
def self.swarm_loop(swarm_element_results_port, &main_loop_spawner)
|
|
159
222
|
# NOTE:
|
|
160
223
|
# This self.-related part of code is placed in the middle of class in order
|
|
161
224
|
# to provide better code readability (it is placed next to the method inside
|
|
162
|
-
# wich it should be called (see #
|
|
225
|
+
# wich it should be called (see #spawn_swarm_element!)). That's why some rubocop
|
|
226
|
+
# cops are disabled.
|
|
227
|
+
|
|
228
|
+
# NOTE: the command port should be created inside the element ractor (only the creator
|
|
229
|
+
# ractor can receive from the port), so it is handed over to the main Ractor at startup;
|
|
230
|
+
swarm_element_commands_port = Ractor::Port.new
|
|
231
|
+
swarm_element_results_port << swarm_element_commands_port
|
|
163
232
|
|
|
164
233
|
# @type var main_loop: Thread?
|
|
165
234
|
main_loop = nil
|
|
166
235
|
|
|
167
236
|
loop do
|
|
168
|
-
command =
|
|
237
|
+
command = swarm_element_commands_port.receive
|
|
169
238
|
|
|
170
239
|
case command
|
|
171
240
|
when :status
|
|
@@ -177,25 +246,46 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
|
177
246
|
# @type var main_loop: Thread
|
|
178
247
|
RedisQueuedLocks::Utilities.thread_state(main_loop)
|
|
179
248
|
end
|
|
180
|
-
|
|
181
|
-
Ractor.yield({ # steep:ignore
|
|
182
|
-
main_loop: { alive: main_loop_alive, state: main_loop_state }
|
|
183
|
-
})
|
|
249
|
+
swarm_element_results_port << { alive: main_loop_alive, state: main_loop_state }
|
|
184
250
|
when :is_active
|
|
185
|
-
|
|
251
|
+
swarm_element_results_port << (main_loop != nil && main_loop.alive?) # steep:ignore
|
|
186
252
|
when :start
|
|
187
|
-
main_loop
|
|
188
|
-
|
|
253
|
+
terminate_thread(main_loop)
|
|
254
|
+
# REFERENCE: `main_loop_spawner.call`
|
|
255
|
+
main_loop = yield.tap { |thread| thread.abort_on_exception = false }
|
|
256
|
+
swarm_element_results_port << true
|
|
189
257
|
when :stop
|
|
190
|
-
main_loop
|
|
258
|
+
terminate_thread(main_loop)
|
|
259
|
+
swarm_element_results_port << true
|
|
191
260
|
when :kill
|
|
192
|
-
|
|
193
|
-
|
|
261
|
+
# NOTE: terminate the main loop and all auxiliary threads it may have left behind (for
|
|
262
|
+
# example, socket connection helper threads when the loop is killed during connection):
|
|
263
|
+
# the ractor stays alive while it has unfinished threads;
|
|
264
|
+
(::Thread.list - [::Thread.current]).each { |thread| terminate_thread(thread) }
|
|
265
|
+
swarm_element_results_port << true
|
|
266
|
+
break
|
|
194
267
|
end
|
|
195
268
|
end
|
|
196
269
|
end
|
|
197
270
|
# rubocop:enable Layout/ClassStructure, Lint/IneffectiveAccessModifier, Metrics/MethodLength
|
|
198
271
|
|
|
272
|
+
# Kills the thread and waits for its termination: the ractor can not finish
|
|
273
|
+
# (and its status stays "running") while it has unfinished threads.
|
|
274
|
+
#
|
|
275
|
+
# @param thread [Thread,NilClass]
|
|
276
|
+
# @return [void]
|
|
277
|
+
#
|
|
278
|
+
# @api private
|
|
279
|
+
# @since 1.17.0
|
|
280
|
+
# rubocop:disable Lint/IneffectiveAccessModifier
|
|
281
|
+
def self.terminate_thread(thread)
|
|
282
|
+
return if thread == nil
|
|
283
|
+
thread.kill
|
|
284
|
+
# NOTE: join re-raises the exception that has failed the thread: it is not our case here;
|
|
285
|
+
thread.join rescue nil
|
|
286
|
+
end
|
|
287
|
+
# rubocop:enable Lint/IneffectiveAccessModifier
|
|
288
|
+
|
|
199
289
|
# @return [Boolean]
|
|
200
290
|
#
|
|
201
291
|
# @api private
|
|
@@ -232,61 +322,93 @@ class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
|
|
232
322
|
#
|
|
233
323
|
# @api private
|
|
234
324
|
# @since 1.9.0
|
|
325
|
+
# @version 1.17.0
|
|
235
326
|
def swarmed__running?
|
|
236
|
-
|
|
327
|
+
swarmed__alive? && swarm_loop__is_active == true
|
|
237
328
|
end
|
|
238
329
|
|
|
239
330
|
# @return [Boolean]
|
|
240
331
|
#
|
|
241
332
|
# @api private
|
|
242
333
|
# @since 1.9.0
|
|
334
|
+
# @version 1.17.0
|
|
243
335
|
def swarmed__stopped?
|
|
244
|
-
|
|
336
|
+
# NOTE: `nil` (the element ractor has died during the request) is neither active nor stopped;
|
|
337
|
+
swarmed__alive? && swarm_loop__is_active == false
|
|
245
338
|
end
|
|
246
339
|
|
|
247
|
-
# @return [Boolean]
|
|
340
|
+
# @return [Boolean,NilClass] Is the main loop alive (`nil` when the element is not available).
|
|
248
341
|
#
|
|
249
342
|
# @api private
|
|
250
343
|
# @since 1.9.0
|
|
344
|
+
# @version 1.17.0
|
|
251
345
|
def swarm_loop__is_active
|
|
252
|
-
|
|
253
|
-
sync.synchronize { swarm_element.send(:is_active).take }
|
|
346
|
+
swarm_loop__send_command(:is_active) #: bool?
|
|
254
347
|
end
|
|
255
348
|
|
|
256
|
-
# @return [Hash]
|
|
349
|
+
# @return [Hash<Symbol,Boolean|String>,NilClass]
|
|
350
|
+
# Format: `{ alive: <Boolean>, state: <String> }` (`nil` when the element is not available).
|
|
257
351
|
#
|
|
258
352
|
# @api private
|
|
259
353
|
# @since 1.9.0
|
|
354
|
+
# @version 1.17.0
|
|
260
355
|
def swarm_loop__status
|
|
261
|
-
|
|
262
|
-
sync.synchronize { swarm_element.send(:status).take }
|
|
356
|
+
swarm_loop__send_command(:status) #: swarmLoopStatus?
|
|
263
357
|
end
|
|
264
358
|
|
|
265
359
|
# @return [void]
|
|
266
360
|
#
|
|
267
361
|
# @api private
|
|
268
362
|
# @since 1.9.0
|
|
363
|
+
# @version 1.17.0
|
|
269
364
|
def swarm_loop__start
|
|
270
|
-
|
|
271
|
-
sync.synchronize { swarm_element.send(:start) }
|
|
365
|
+
swarm_loop__send_command(:start)
|
|
272
366
|
end
|
|
273
367
|
|
|
274
368
|
# @return [void]
|
|
275
369
|
#
|
|
276
370
|
# @api private
|
|
277
|
-
# @since 1.
|
|
278
|
-
def
|
|
279
|
-
|
|
280
|
-
sync.synchronize { swarm_element.send(:stop) }
|
|
371
|
+
# @since 1.17.0
|
|
372
|
+
def swarm_loop__stop
|
|
373
|
+
swarm_loop__send_command(:stop)
|
|
281
374
|
end
|
|
282
375
|
|
|
283
376
|
# @return [void]
|
|
284
377
|
#
|
|
285
378
|
# @api private
|
|
286
379
|
# @since 1.9.0
|
|
380
|
+
# @version 1.17.0
|
|
287
381
|
def swarm_loop__kill
|
|
288
|
-
|
|
289
|
-
|
|
382
|
+
sync.synchronize do
|
|
383
|
+
# NOTE: wait for the ractor finish only when it has confirmed the kill command;
|
|
384
|
+
killed = swarm_loop__send_command(:kill) == true
|
|
385
|
+
@swarm_element_commands_port = nil
|
|
386
|
+
swarm_element.join if killed # steep:ignore
|
|
387
|
+
end
|
|
388
|
+
rescue Ractor::RemoteError
|
|
389
|
+
# NOTE: the element ractor has been finished with an exception: it is dead anyway;
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Sends the command to the element ractor and waits for its reply. Replies are received
|
|
393
|
+
# strictly one by one under the lock, so each reply belongs to the sent command.
|
|
394
|
+
#
|
|
395
|
+
# @param command [Symbol]
|
|
396
|
+
# @return [Boolean,Hash<Symbol,Boolean|String>,NilClass]
|
|
397
|
+
# The bare reply value (see .swarm_loop) or `nil` if the element is dead.
|
|
398
|
+
#
|
|
399
|
+
# @api private
|
|
400
|
+
# @since 1.17.0
|
|
401
|
+
def swarm_loop__send_command(command)
|
|
402
|
+
return if idle? || swarmed__dead? || swarm_element_commands_port == nil
|
|
403
|
+
sync.synchronize do
|
|
404
|
+
swarm_element_commands_port << command
|
|
405
|
+
reply = swarm_element_results_port.receive # steep:ignore
|
|
406
|
+
# NOTE: Symbol replies are reserved for the Ractor#monitor notice (`:exited`/`:aborted`);
|
|
407
|
+
reply.is_a?(Symbol) ? nil : reply
|
|
408
|
+
end
|
|
409
|
+
rescue Ractor::ClosedError
|
|
410
|
+
# NOTE: the command port is closed together with the finished element ractor;
|
|
411
|
+
nil
|
|
290
412
|
end
|
|
291
413
|
end
|
|
292
414
|
# rubocop:enable Metrics/ClassLength
|