redis_queued_locks 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +51 -0
- data/README.md +402 -46
- data/lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +4 -0
- data/lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue.rb +4 -1
- data/lib/redis_queued_locks/acquier/acquire_lock/instr_visitor.rb +20 -5
- data/lib/redis_queued_locks/acquier/acquire_lock/log_visitor.rb +24 -0
- data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock/log_visitor.rb +56 -0
- data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb +37 -30
- data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire/log_visitor.rb +8 -0
- data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire.rb +13 -9
- data/lib/redis_queued_locks/acquier/acquire_lock.rb +44 -20
- data/lib/redis_queued_locks/acquier/clear_dead_requests.rb +5 -1
- data/lib/redis_queued_locks/acquier/extend_lock_ttl.rb +5 -1
- data/lib/redis_queued_locks/acquier/lock_info.rb +4 -3
- data/lib/redis_queued_locks/acquier/locks.rb +2 -2
- data/lib/redis_queued_locks/acquier/queue_info.rb +2 -2
- data/lib/redis_queued_locks/acquier/release_all_locks.rb +12 -2
- data/lib/redis_queued_locks/acquier/release_lock.rb +12 -2
- data/lib/redis_queued_locks/client.rb +284 -34
- data/lib/redis_queued_locks/errors.rb +8 -0
- data/lib/redis_queued_locks/instrument.rb +8 -1
- data/lib/redis_queued_locks/logging.rb +8 -1
- data/lib/redis_queued_locks/resource.rb +59 -1
- data/lib/redis_queued_locks/swarm/acquirers.rb +44 -0
- data/lib/redis_queued_locks/swarm/flush_zombies.rb +133 -0
- data/lib/redis_queued_locks/swarm/probe_hosts.rb +69 -0
- data/lib/redis_queued_locks/swarm/redis_client_builder.rb +67 -0
- data/lib/redis_queued_locks/swarm/supervisor.rb +83 -0
- data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +287 -0
- data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +351 -0
- data/lib/redis_queued_locks/swarm/swarm_element.rb +8 -0
- data/lib/redis_queued_locks/swarm/zombie_info.rb +145 -0
- data/lib/redis_queued_locks/swarm.rb +241 -0
- data/lib/redis_queued_locks/utilities/lock.rb +22 -0
- data/lib/redis_queued_locks/utilities.rb +75 -0
- data/lib/redis_queued_locks/version.rb +2 -2
- data/lib/redis_queued_locks.rb +2 -0
- metadata +14 -4
- data/lib/redis_queued_locks/watcher.rb +0 -1
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 1.9.0
|
5
|
+
module RedisQueuedLocks::Swarm::Acquirers
|
6
|
+
class << self
|
7
|
+
# Returns the list of swarm acquirers stored as HASH.
|
8
|
+
# Format:
|
9
|
+
# {
|
10
|
+
# <acquirer id #1> => {
|
11
|
+
# zombie: <Boolean>,
|
12
|
+
# last_probe_time: <Time>,
|
13
|
+
# last_probe_score: <Numeric>
|
14
|
+
# },
|
15
|
+
# <acquirer id #2> => {
|
16
|
+
# zombie: <Boolean>,
|
17
|
+
# last_probe_time: <Time>,
|
18
|
+
# last_probe_score: <Numeric>
|
19
|
+
# },
|
20
|
+
# ...
|
21
|
+
# }
|
22
|
+
# Liveness probe time is represented as a float value (Time.now.to_f initially).
|
23
|
+
#
|
24
|
+
# @param redis_client [RedisClient]
|
25
|
+
# @param zombie_ttl [Integer]
|
26
|
+
# @return [Hash<String,Hash<Symbol,Float|Time>>]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
# @since 1.9.0
|
30
|
+
def acquirers(redis_client, zombie_ttl)
|
31
|
+
redis_client.with do |rconn|
|
32
|
+
rconn.call('HGETALL', RedisQueuedLocks::Resource::SWARM_KEY).tap do |swarm_acqs|
|
33
|
+
swarm_acqs.transform_values! do |last_probe|
|
34
|
+
last_probe_score = last_probe.to_f
|
35
|
+
last_probe_time = Time.at(last_probe_score)
|
36
|
+
zombie_score = RedisQueuedLocks::Resource.calc_zombie_score(zombie_ttl / 1_000)
|
37
|
+
is_zombie = last_probe_score < zombie_score
|
38
|
+
{ zombie: is_zombie, last_probe_time:, last_probe_score: }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 1.9.0
|
5
|
+
class RedisQueuedLocks::Swarm::FlushZombies < RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
6
|
+
class << self
|
7
|
+
# @param redis_client [RedisClient]
|
8
|
+
# @parma zombie_ttl [Integer]
|
9
|
+
# @param lock_scan_size [Integer]
|
10
|
+
# @param queue_scan_size [Integer]
|
11
|
+
# @return [
|
12
|
+
# RedisQueuedLocks::Data[
|
13
|
+
# ok: <Boolean>,
|
14
|
+
# deleted_zombie_hosts: <Set<String>>,
|
15
|
+
# deleted_zombie_acquiers: <Set<String>>,
|
16
|
+
# deleted_zombie_locks: <Set<String>>
|
17
|
+
# ]
|
18
|
+
# ]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
# @since 1.9.0
|
22
|
+
# rubocop:disable Metrics/MethodLength
|
23
|
+
def flush_zombies(
|
24
|
+
redis_client,
|
25
|
+
zombie_ttl,
|
26
|
+
lock_scan_size,
|
27
|
+
queue_scan_size
|
28
|
+
)
|
29
|
+
redis_client.with do |rconn|
|
30
|
+
# Step 1:
|
31
|
+
# calculate zombie score (the time marker that shows acquirers that
|
32
|
+
# have not announced live probes for a long time)
|
33
|
+
zombie_score = RedisQueuedLocks::Resource.calc_zombie_score(zombie_ttl / 1_000)
|
34
|
+
|
35
|
+
# Step 2: extract zombie acquirers from the swarm list
|
36
|
+
zombie_hosts = rconn.call('HGETALL', RedisQueuedLocks::Resource::SWARM_KEY)
|
37
|
+
zombie_hosts = zombie_hosts.each_with_object(Set.new) do |(hst_id, ts), zombies|
|
38
|
+
(zombies << hst_id) if (zombie_score > ts.to_f)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Step X: exit if we have no any zombie acquirer
|
42
|
+
next RedisQueuedLocks::Data[
|
43
|
+
ok: true,
|
44
|
+
deleted_zombie_hosts: Set.new,
|
45
|
+
deleted_zombie_acquirers: Set.new,
|
46
|
+
deleted_zombie_locks: Set.new,
|
47
|
+
] if zombie_hosts.empty?
|
48
|
+
|
49
|
+
# Step 3: find zombie locks held by zombies and delete them
|
50
|
+
# TODO: indexing (in order to prevent full database scan);
|
51
|
+
# NOTE: original redis does not support indexing so we need to use
|
52
|
+
# internal data structers to simulate data indexing (such as sorted sets or lists);
|
53
|
+
zombie_locks = Set.new
|
54
|
+
zombie_acquiers = Set.new
|
55
|
+
|
56
|
+
rconn.scan(
|
57
|
+
'MATCH', RedisQueuedLocks::Resource::LOCK_PATTERN, count: lock_scan_size
|
58
|
+
) do |lock_key|
|
59
|
+
acquier_id, host_id = rconn.call('HMGET', lock_key, 'acq_id', 'hst_id')
|
60
|
+
if zombie_hosts.include?(host_id)
|
61
|
+
zombie_locks << lock_key
|
62
|
+
zombie_acquiers << acquier_id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rconn.call('DEL', *zombie_locks) if zombie_locks.any?
|
66
|
+
|
67
|
+
# Step 4: find zombie requests => and drop them
|
68
|
+
# TODO: indexing (in order to prevent full database scan);
|
69
|
+
# NOTE: original redis does not support indexing so we need to use
|
70
|
+
# internal data structers to simulate data indexing (such as sorted sets or lists);
|
71
|
+
rconn.scan(
|
72
|
+
'MATCH', RedisQueuedLocks::Resource::LOCK_QUEUE_PATTERN, count: queue_scan_size
|
73
|
+
) do |lock_queue|
|
74
|
+
zombie_acquiers.each do |zombie_acquier|
|
75
|
+
rconn.call('ZREM', lock_queue, zombie_acquier)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Step 5: drop zombies from the swarm
|
80
|
+
rconn.call('HDEL', RedisQueuedLocks::Resource::SWARM_KEY, *zombie_hosts)
|
81
|
+
|
82
|
+
# Step 6: inform about deleted zombies
|
83
|
+
RedisQueuedLocks::Data[
|
84
|
+
ok: true,
|
85
|
+
deleted_zombie_hosts: zombie_hosts,
|
86
|
+
deleted_zombie_acquiers: zombie_acquiers,
|
87
|
+
deleted_zombie_locks: zombie_locks
|
88
|
+
]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/MethodLength
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [Boolean]
|
95
|
+
#
|
96
|
+
# @api private
|
97
|
+
# @since 1.9.0
|
98
|
+
def enabled?
|
99
|
+
rql_client.config[:swarm][:flush_zombies][:enabled_for_swarm]
|
100
|
+
end
|
101
|
+
|
102
|
+
# @return [void]
|
103
|
+
#
|
104
|
+
# @api private
|
105
|
+
# @since 1.9.0
|
106
|
+
def swarm!
|
107
|
+
@swarm_element = Ractor.new(
|
108
|
+
rql_client.config.slice_value('swarm.flush_zombies.redis_config'),
|
109
|
+
rql_client.config[:swarm][:flush_zombies][:zombie_ttl],
|
110
|
+
rql_client.config[:swarm][:flush_zombies][:zombie_lock_scan_size],
|
111
|
+
rql_client.config[:swarm][:flush_zombies][:zombie_queue_scan_size],
|
112
|
+
rql_client.config[:swarm][:flush_zombies][:zombie_flush_period]
|
113
|
+
) do |rc, z_ttl, z_lss, z_qss, z_fl_prd|
|
114
|
+
RedisQueuedLocks::Swarm::FlushZombies.swarm_loop do
|
115
|
+
Thread.new do
|
116
|
+
redis_client = RedisQueuedLocks::Swarm::RedisClientBuilder.build(
|
117
|
+
pooled: rc['pooled'],
|
118
|
+
sentinel: rc['sentinel'],
|
119
|
+
config: rc['config'],
|
120
|
+
pool_config: rc['pool_config']
|
121
|
+
)
|
122
|
+
|
123
|
+
loop do
|
124
|
+
RedisQueuedLocks::Swarm::FlushZombies.flush_zombies(
|
125
|
+
redis_client, z_ttl, z_lss, z_qss
|
126
|
+
)
|
127
|
+
sleep(z_fl_prd)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 1.9.0
|
5
|
+
class RedisQueuedLocks::Swarm::ProbeHosts < RedisQueuedLocks::Swarm::SwarmElement::Threaded
|
6
|
+
class << self
|
7
|
+
# @param redis_client [RedisClient]
|
8
|
+
# @param uniq_identity [String]
|
9
|
+
# @return [
|
10
|
+
# RedisQueuedLocks::Data[
|
11
|
+
# ok: <Boolean>,
|
12
|
+
# result: {
|
13
|
+
# host_id1 <String> => score1 <String>,
|
14
|
+
# host_id2 <String> => score2 <String>,
|
15
|
+
# etc...
|
16
|
+
# }
|
17
|
+
# ]
|
18
|
+
# ]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
# @since 1.9.0
|
22
|
+
def probe_hosts(redis_client, uniq_identity)
|
23
|
+
possible_hosts = RedisQueuedLocks::Resource.possible_host_identifiers(uniq_identity)
|
24
|
+
probed_hosts = {}
|
25
|
+
|
26
|
+
redis_client.with do |rconn|
|
27
|
+
possible_hosts.each do |host_id|
|
28
|
+
rconn.call(
|
29
|
+
'HSET',
|
30
|
+
RedisQueuedLocks::Resource::SWARM_KEY,
|
31
|
+
host_id,
|
32
|
+
probe_score = Time.now.to_f
|
33
|
+
)
|
34
|
+
probed_hosts[host_id] = probe_score
|
35
|
+
end
|
36
|
+
|
37
|
+
RedisQueuedLocks::Data[ok: true, result: probed_hosts]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Boolean]
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
# @since 1.9.0
|
46
|
+
def enabled?
|
47
|
+
rql_client.config[:swarm][:probe_hosts][:enabled_for_swarm]
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Thread]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
# @since 1.9.0
|
54
|
+
def spawn_main_loop!
|
55
|
+
Thread.new do
|
56
|
+
redis_client = RedisQueuedLocks::Swarm::RedisClientBuilder.build(
|
57
|
+
pooled: rql_client.config[:swarm][:probe_hosts][:redis_config][:pooled],
|
58
|
+
sentinel: rql_client.config[:swarm][:probe_hosts][:redis_config][:sentinel],
|
59
|
+
config: rql_client.config[:swarm][:probe_hosts][:redis_config][:config],
|
60
|
+
pool_config: rql_client.config[:swarm][:probe_hosts][:redis_config][:pool_config]
|
61
|
+
)
|
62
|
+
|
63
|
+
loop do
|
64
|
+
RedisQueuedLocks::Swarm::ProbeHosts.probe_hosts(redis_client, rql_client.uniq_identity)
|
65
|
+
sleep(rql_client.config[:swarm][:probe_hosts][:probe_period])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 1.9.0
|
5
|
+
module RedisQueuedLocks::Swarm::RedisClientBuilder
|
6
|
+
class << self
|
7
|
+
# @option pooled [Boolean]
|
8
|
+
# @option sentinel [Boolean]
|
9
|
+
# @option config [Hash]
|
10
|
+
# @return [RedisClient]
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
# @since 1.9.0
|
14
|
+
# rubocop:disable Style/RedundantAssignment
|
15
|
+
def build(pooled: false, sentinel: false, config: {}, pool_config: {})
|
16
|
+
config.transform_keys!(&:to_sym)
|
17
|
+
pool_config.transform_keys!(&:to_sym)
|
18
|
+
|
19
|
+
redis_config =
|
20
|
+
sentinel ? sentinel_config(config) : non_sentinel_config(config)
|
21
|
+
redis_client =
|
22
|
+
pooled ? pooled_client(redis_config, pool_config) : non_pooled_client(redis_config)
|
23
|
+
|
24
|
+
redis_client
|
25
|
+
end
|
26
|
+
# rubocop:enable Style/RedundantAssignment
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# @param config [Hash]
|
31
|
+
# @return [RedisClient::Config]
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
# @since 1.9.0
|
35
|
+
def sentinel_config(config)
|
36
|
+
RedisClient.sentinel(**config)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param config [Hash]
|
40
|
+
# @return [RedisClient::Config]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
# @since 1.9.0
|
44
|
+
def non_sentinel_config(config)
|
45
|
+
RedisClient.config(**config)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param redis_config [ReidsClient::Config]
|
49
|
+
# @param pool_config [Hash]
|
50
|
+
# @return [RedisClient]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
# @since 1.9.0
|
54
|
+
def pooled_client(redis_config, pool_config)
|
55
|
+
redis_config.new_pool(**pool_config)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param redis_config [ReidsClient::Config]
|
59
|
+
# @return [RedisClient]
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
# @since 1.9.0
|
63
|
+
def non_pooled_client(redis_config)
|
64
|
+
redis_config.new_client
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 1.9.0
|
5
|
+
class RedisQueuedLocks::Swarm::Supervisor
|
6
|
+
# @since 1.9.0
|
7
|
+
include RedisQueuedLocks::Utilities
|
8
|
+
|
9
|
+
# @return [RedisQueuedLocks::Client]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
# @since 1.9.0
|
13
|
+
attr_reader :rql_client
|
14
|
+
|
15
|
+
# @return [Thread,NilClass]
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
# @since 1.9.0
|
19
|
+
attr_reader :visor
|
20
|
+
|
21
|
+
# @return [Proc,NilClass]
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
# @since 1.9.0
|
25
|
+
attr_reader :observable
|
26
|
+
|
27
|
+
# @return [void]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
# @since 1.9.0
|
31
|
+
def initialize(rql_client)
|
32
|
+
@rql_client = rql_client
|
33
|
+
@visor = nil
|
34
|
+
@observable = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param observable [Block]
|
38
|
+
# @return [void]
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
# @since 1.9.0
|
42
|
+
def observe!(&observable)
|
43
|
+
@observable = observable
|
44
|
+
@visor = Thread.new do
|
45
|
+
loop do
|
46
|
+
yield rescue nil # TODO/CHECK: may be we need to process exceptions here
|
47
|
+
sleep(rql_client.config[:swarm][:supervisor][:liveness_probing_period])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
# NOTE: need to give a timespot to initialize visor thread;
|
51
|
+
sleep(0.1)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Boolean]
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
# @since 1.9.0
|
58
|
+
def running?
|
59
|
+
visor != nil && visor.alive?
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [void]
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
# @since 1.9.0
|
66
|
+
def stop!
|
67
|
+
visor.kill if running?
|
68
|
+
@visor = nil
|
69
|
+
@observable = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Hash<Symbol|Hash<Symbol,String|Boolean>>]
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
# @since 1.9.0
|
76
|
+
def status
|
77
|
+
{
|
78
|
+
running: running?,
|
79
|
+
state: (visor == nil) ? 'non_initialized' : thread_state(visor),
|
80
|
+
observable: (observable == nil) ? 'non_initialized' : 'initialized'
|
81
|
+
}
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 1.9.0
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
6
|
+
class RedisQueuedLocks::Swarm::SwarmElement::Isolated
|
7
|
+
# @since 1.9.0
|
8
|
+
include RedisQueuedLocks::Utilities
|
9
|
+
|
10
|
+
# @return [RedisQueuedLocks::Client]
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
# @since 1.9.0
|
14
|
+
attr_reader :rql_client
|
15
|
+
|
16
|
+
# @return [Ractor,NilClass]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
# @since 1.9.0
|
20
|
+
attr_reader :swarm_element
|
21
|
+
|
22
|
+
# @return [RedisQueuedLocks::Utilities::Lock]
|
23
|
+
#
|
24
|
+
# @api private
|
25
|
+
# @since 1.9.0
|
26
|
+
attr_reader :sync
|
27
|
+
|
28
|
+
# @return [void]
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
# @since 1.9.0
|
32
|
+
def initialize(rql_client)
|
33
|
+
@rql_client = rql_client
|
34
|
+
@swarm_element = nil
|
35
|
+
@sync = RedisQueuedLocks::Utilities::Lock.new
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [void]
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
# @since 1.9.0
|
42
|
+
def try_swarm!
|
43
|
+
return unless enabled?
|
44
|
+
|
45
|
+
sync.synchronize do
|
46
|
+
swarm_loop__kill
|
47
|
+
swarm!
|
48
|
+
swarm_loop__start
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [void]
|
53
|
+
#
|
54
|
+
# @api private
|
55
|
+
# @since 1.9.0
|
56
|
+
def reswarm_if_dead!
|
57
|
+
return unless enabled?
|
58
|
+
|
59
|
+
sync.synchronize do
|
60
|
+
if swarmed__stopped?
|
61
|
+
swarm_loop__start
|
62
|
+
elsif swarmed__dead? || idle?
|
63
|
+
swarm!
|
64
|
+
swarm_loop__start
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# @return [void]
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
# @since 1.9.0
|
73
|
+
def try_kill!
|
74
|
+
sync.synchronize do
|
75
|
+
swarm_loop__kill
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# @return [Boolean]
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
# @since 1.9.0
|
83
|
+
def enabled?
|
84
|
+
# NOTE: provide an <is enabled> logic here by analyzing the redis queued locks config.
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Hash<Symbol,Boolean|Hash<Symbol,String|Boolean>>]
|
88
|
+
# Format: {
|
89
|
+
# enabled: <Boolean>,
|
90
|
+
# ractor: {
|
91
|
+
# running: <Boolean>,
|
92
|
+
# state: <String>,
|
93
|
+
# },
|
94
|
+
# main_loop: {
|
95
|
+
# running: <Boolean>,
|
96
|
+
# state: <String>
|
97
|
+
# }
|
98
|
+
# }
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
# @since 1.9.0
|
102
|
+
def status
|
103
|
+
sync.synchronize do
|
104
|
+
ractor_running = swarmed__alive?
|
105
|
+
ractor_state = swarmed? ? ractor_status(swarm_element) : 'non_initialized'
|
106
|
+
|
107
|
+
main_loop_running = nil
|
108
|
+
main_loop_state = nil
|
109
|
+
begin
|
110
|
+
main_loop_running = swarmed__running?
|
111
|
+
main_loop_state =
|
112
|
+
main_loop_running ? swarm_loop__status[:main_loop][:state] : 'non_initialized'
|
113
|
+
rescue Ractor::ClosedError
|
114
|
+
# NOTE: it can happend when you run RedisQueuedLocks::Swarm#deswarm!;
|
115
|
+
main_loop_running = false
|
116
|
+
main_loop_state = 'non_initialized'
|
117
|
+
end
|
118
|
+
|
119
|
+
{
|
120
|
+
enabled: enabled?,
|
121
|
+
ractor: {
|
122
|
+
running: ractor_running,
|
123
|
+
state: ractor_state
|
124
|
+
},
|
125
|
+
main_loop: {
|
126
|
+
running: main_loop_running,
|
127
|
+
state: main_loop_state
|
128
|
+
}
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
# Swarm element lifecycle have the following scheme:
|
136
|
+
# => 1) init (swarm!): create a ractor, main loop is not started;
|
137
|
+
# => 2) start (swarm_loop__start!): run main lopp inside the ractor;
|
138
|
+
# => 3) stop (swarm_loop__stop!): stop the main loop inside a ractor;
|
139
|
+
# => 4) kill (swarm_loop__kill!): kill the main loop inside teh ractor and kill a ractor;
|
140
|
+
#
|
141
|
+
# @return [void]
|
142
|
+
#
|
143
|
+
# @api private
|
144
|
+
# @since 1.9.0
|
145
|
+
def swarm!
|
146
|
+
# IMPORTANT №1: initialize @swarm_element here with Ractor;
|
147
|
+
# IMPORTANT №2: your Ractor should invoke .swarm_loop inside (see below);
|
148
|
+
# IMPORTANT №3: you should pass the main loop logic as a block to .swarm_loop;
|
149
|
+
end
|
150
|
+
|
151
|
+
# @param main_loop_spawner [Block]
|
152
|
+
# @return [void]
|
153
|
+
#
|
154
|
+
# @api private
|
155
|
+
# @since 1.9.0
|
156
|
+
# rubocop:disable Layout/ClassStructure, Lint/IneffectiveAccessModifier
|
157
|
+
def self.swarm_loop(&main_loop_spawner)
|
158
|
+
# NOTE:
|
159
|
+
# This self.-related part of code is placed in the middle of class in order
|
160
|
+
# to provide better code readability (it is placed next to the method inside
|
161
|
+
# wich it should be called (see #swarm!)). That's why some rubocop cops are disabled.
|
162
|
+
|
163
|
+
main_loop = nil
|
164
|
+
|
165
|
+
loop do
|
166
|
+
command = Ractor.receive
|
167
|
+
|
168
|
+
case command
|
169
|
+
when :status
|
170
|
+
main_loop_alive = main_loop != nil && main_loop.alive?
|
171
|
+
main_loop_state =
|
172
|
+
if main_loop == nil
|
173
|
+
'non_initialized'
|
174
|
+
else
|
175
|
+
# NOTE: (full name resolution): ractor has no syntax-based constatnt context;
|
176
|
+
RedisQueuedLocks::Utilities.thread_state(main_loop)
|
177
|
+
end
|
178
|
+
Ractor.yield({ main_loop: { alive: main_loop_alive, state: main_loop_state } })
|
179
|
+
when :is_active
|
180
|
+
Ractor.yield(main_loop != nil && main_loop.alive?)
|
181
|
+
when :start
|
182
|
+
main_loop.kill if main_loop != nil
|
183
|
+
main_loop = yield # REFERENCE: `main_loop_spawner.call`
|
184
|
+
when :stop
|
185
|
+
main_loop.kill if main_loop != nil
|
186
|
+
when :kill
|
187
|
+
main_loop.kill if main_loop != nil
|
188
|
+
exit
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
# rubocop:enable Layout/ClassStructure, Lint/IneffectiveAccessModifier
|
193
|
+
|
194
|
+
# @return [Boolean]
|
195
|
+
#
|
196
|
+
# @api private
|
197
|
+
# @since 1.9.0
|
198
|
+
def idle?
|
199
|
+
swarm_element == nil
|
200
|
+
end
|
201
|
+
|
202
|
+
# @return [Boolean]
|
203
|
+
#
|
204
|
+
# @api private
|
205
|
+
# @since 1.9.0
|
206
|
+
def swarmed?
|
207
|
+
swarm_element != nil
|
208
|
+
end
|
209
|
+
|
210
|
+
# @return [Boolean]
|
211
|
+
#
|
212
|
+
# @api private
|
213
|
+
# @since 1.9.0
|
214
|
+
def swarmed__alive?
|
215
|
+
swarm_element != nil && ractor_alive?(swarm_element)
|
216
|
+
end
|
217
|
+
|
218
|
+
# @return [Boolean]
|
219
|
+
#
|
220
|
+
# @api private
|
221
|
+
# @since 1.9.0
|
222
|
+
def swarmed__dead?
|
223
|
+
swarm_element != nil && !ractor_alive?(swarm_element)
|
224
|
+
end
|
225
|
+
|
226
|
+
# @return [Boolean]
|
227
|
+
#
|
228
|
+
# @api private
|
229
|
+
# @since 1.9.0
|
230
|
+
def swarmed__running?
|
231
|
+
swarm_element != nil && ractor_alive?(swarm_element) && swarm_loop__is_active
|
232
|
+
end
|
233
|
+
|
234
|
+
# @return [Boolean]
|
235
|
+
#
|
236
|
+
# @api private
|
237
|
+
# @since 1.9.0
|
238
|
+
def swarmed__stopped?
|
239
|
+
swarm_element != nil && ractor_alive?(swarm_element) && !swarm_loop__is_active
|
240
|
+
end
|
241
|
+
|
242
|
+
# @return [Boolean]
|
243
|
+
#
|
244
|
+
# @api private
|
245
|
+
# @since 1.9.0
|
246
|
+
def swarm_loop__is_active
|
247
|
+
return if idle? || swarmed__dead?
|
248
|
+
sync.synchronize { swarm_element.send(:is_active).take }
|
249
|
+
end
|
250
|
+
|
251
|
+
# @return [Hash]
|
252
|
+
#
|
253
|
+
# @api private
|
254
|
+
# @since 1.9.0
|
255
|
+
def swarm_loop__status
|
256
|
+
return if idle? || swarmed__dead?
|
257
|
+
sync.synchronize { swarm_element.send(:status).take }
|
258
|
+
end
|
259
|
+
|
260
|
+
# @return [void]
|
261
|
+
#
|
262
|
+
# @api private
|
263
|
+
# @since 1.9.0
|
264
|
+
def swarm_loop__start
|
265
|
+
return if idle? || swarmed__dead?
|
266
|
+
sync.synchronize { swarm_element.send(:start) }
|
267
|
+
end
|
268
|
+
|
269
|
+
# @return [void]
|
270
|
+
#
|
271
|
+
# @api private
|
272
|
+
# @since 1.9.0
|
273
|
+
def swarm_loop__pause
|
274
|
+
return if idle? || swarmed__dead?
|
275
|
+
sync.synchronize { swarm_element.send(:stop) }
|
276
|
+
end
|
277
|
+
|
278
|
+
# @return [void]
|
279
|
+
#
|
280
|
+
# @api private
|
281
|
+
# @since 1.9.0
|
282
|
+
def swarm_loop__kill
|
283
|
+
return if idle? || swarmed__dead?
|
284
|
+
sync.synchronize { swarm_element.send(:kill) }
|
285
|
+
end
|
286
|
+
end
|
287
|
+
# rubocop:enable Metrics/ClassLength
|