redis_queued_locks 1.8.0 → 1.9.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/.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
|