redis_queued_locks 1.7.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +60 -1
  4. data/README.md +485 -46
  5. data/lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue/log_visitor.rb +4 -0
  6. data/lib/redis_queued_locks/acquier/acquire_lock/dequeue_from_lock_queue.rb +4 -1
  7. data/lib/redis_queued_locks/acquier/acquire_lock/instr_visitor.rb +20 -5
  8. data/lib/redis_queued_locks/acquier/acquire_lock/log_visitor.rb +24 -0
  9. data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock/log_visitor.rb +56 -0
  10. data/lib/redis_queued_locks/acquier/acquire_lock/try_to_lock.rb +37 -30
  11. data/lib/redis_queued_locks/acquier/acquire_lock/with_acq_timeout.rb +41 -7
  12. data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire/log_visitor.rb +8 -0
  13. data/lib/redis_queued_locks/acquier/acquire_lock/yield_expire.rb +21 -9
  14. data/lib/redis_queued_locks/acquier/acquire_lock.rb +61 -22
  15. data/lib/redis_queued_locks/acquier/clear_dead_requests.rb +5 -1
  16. data/lib/redis_queued_locks/acquier/extend_lock_ttl.rb +5 -1
  17. data/lib/redis_queued_locks/acquier/lock_info.rb +4 -3
  18. data/lib/redis_queued_locks/acquier/locks.rb +2 -2
  19. data/lib/redis_queued_locks/acquier/queue_info.rb +2 -2
  20. data/lib/redis_queued_locks/acquier/release_all_locks.rb +12 -2
  21. data/lib/redis_queued_locks/acquier/release_lock.rb +12 -2
  22. data/lib/redis_queued_locks/client.rb +320 -10
  23. data/lib/redis_queued_locks/errors.rb +8 -0
  24. data/lib/redis_queued_locks/instrument.rb +8 -1
  25. data/lib/redis_queued_locks/logging.rb +8 -1
  26. data/lib/redis_queued_locks/resource.rb +59 -1
  27. data/lib/redis_queued_locks/swarm/acquirers.rb +44 -0
  28. data/lib/redis_queued_locks/swarm/flush_zombies.rb +133 -0
  29. data/lib/redis_queued_locks/swarm/probe_hosts.rb +69 -0
  30. data/lib/redis_queued_locks/swarm/redis_client_builder.rb +67 -0
  31. data/lib/redis_queued_locks/swarm/supervisor.rb +83 -0
  32. data/lib/redis_queued_locks/swarm/swarm_element/isolated.rb +287 -0
  33. data/lib/redis_queued_locks/swarm/swarm_element/threaded.rb +351 -0
  34. data/lib/redis_queued_locks/swarm/swarm_element.rb +8 -0
  35. data/lib/redis_queued_locks/swarm/zombie_info.rb +145 -0
  36. data/lib/redis_queued_locks/swarm.rb +241 -0
  37. data/lib/redis_queued_locks/utilities/lock.rb +22 -0
  38. data/lib/redis_queued_locks/utilities.rb +75 -0
  39. data/lib/redis_queued_locks/version.rb +2 -2
  40. data/lib/redis_queued_locks.rb +2 -0
  41. data/redis_queued_locks.gemspec +6 -10
  42. metadata +24 -6
  43. data/lib/redis_queued_locks/watcher.rb +0 -1
@@ -21,6 +21,12 @@ module RedisQueuedLocks::Resource
21
21
  # @since 1.0.0
22
22
  LOCK_QUEUE_PATTERN = 'rql:lock_queue:*'
23
23
 
24
+ # @return [String]
25
+ #
26
+ # @api private
27
+ # @since 1.9.0
28
+ SWARM_KEY = 'rql:swarm:hsts'
29
+
24
30
  # @return [Integer] Redis time error (in milliseconds).
25
31
  #
26
32
  # @api private
@@ -53,6 +59,22 @@ module RedisQueuedLocks::Resource
53
59
  "rql:acq:#{process_id}/#{thread_id}/#{fiber_id}/#{ractor_id}/#{identity}"
54
60
  end
55
61
 
62
+ # @param process_id [Integer,String]
63
+ # @param thread_id [Integer,String]
64
+ # @param ractor_id [Integer,String]
65
+ # @param identity [String]
66
+ # @return [String]
67
+ #
68
+ # @api private
69
+ # @since 1.9.0
70
+ def host_identifier(process_id, thread_id, ractor_id, identity)
71
+ # NOTE:
72
+ # - fiber's object_id is not used cuz we can't analyze fiber objects via ObjectSpace
73
+ # after the any new Ractor object is created in the current process
74
+ # (ObjectSpace no longer sees objects of Thread and Fiber classes after that);
75
+ "rql:hst:#{process_id}/#{thread_id}/#{ractor_id}/#{identity}"
76
+ end
77
+
56
78
  # @param lock_name [String]
57
79
  # @return [String]
58
80
  #
@@ -88,8 +110,17 @@ module RedisQueuedLocks::Resource
88
110
  Time.now.to_f - queue_ttl
89
111
  end
90
112
 
113
+ # @param zombie_ttl [Float] In seconds with milliseconds.
114
+ # @return [Float]
115
+ #
116
+ # @api private
117
+ # @since 1.9.0
118
+ def calc_zombie_score(zombie_ttl)
119
+ Time.now.to_f - zombie_ttl
120
+ end
121
+
91
122
  # @param acquier_position [Float]
92
- # A time (epoch, seconds.microseconds) that represents
123
+ # A time (epoch, seconds.milliseconds) that represents
93
124
  # the acquier position in lock request queue.
94
125
  # @parma queue_ttl [Integer]
95
126
  # In second.
@@ -143,5 +174,32 @@ module RedisQueuedLocks::Resource
143
174
  def get_process_id
144
175
  ::Process.pid
145
176
  end
177
+
178
+ # @return [Array<String>]
179
+ #
180
+ # @api private
181
+ # @since 1.9.0
182
+ def possible_host_identifiers(identity)
183
+ # NOTE №1: we can not use ObjectSpace.each_object for Thread and Fiber cuz after the any
184
+ # ractor creation the ObjectSpace stops seeing ::Thread and ::Fiber objects: each_object
185
+ # for each of them returns `0`;
186
+ # NOTE №2: we have no any approach to count Fiber objects in the current process without
187
+ # object space API (or super memory-expensive) so host identification works without fibers;
188
+ # NOTE №3: we still can extract thread objects via Thread.list API;
189
+ current_process_id = get_process_id
190
+ current_threads = ::Thread.list
191
+ current_ractor_id = get_ractor_id
192
+
193
+ [].tap do |acquiers|
194
+ current_threads.each do |thread|
195
+ acquiers << host_identifier(
196
+ current_process_id,
197
+ thread.object_id,
198
+ current_ractor_id,
199
+ identity
200
+ )
201
+ end
202
+ end
203
+ end
146
204
  end
147
205
  end
@@ -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