legion-cache 1.4.1 → 1.4.3
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/.gitignore +1 -0
- data/CHANGELOG.md +26 -0
- data/README.md +152 -30
- data/legion-cache.gemspec +3 -2
- data/lib/legion/cache/async_writer.rb +88 -36
- data/lib/legion/cache/cacheable.rb +37 -36
- data/lib/legion/cache/helper.rb +14 -12
- data/lib/legion/cache/local.rb +118 -109
- data/lib/legion/cache/memcached.rb +97 -23
- data/lib/legion/cache/memory.rb +222 -87
- data/lib/legion/cache/pool.rb +32 -15
- data/lib/legion/cache/reconnector.rb +50 -11
- data/lib/legion/cache/redis.rb +155 -118
- data/lib/legion/cache/redis_hash.rb +16 -13
- data/lib/legion/cache/settings.rb +8 -0
- data/lib/legion/cache/version.rb +1 -1
- data/lib/legion/cache.rb +222 -345
- metadata +35 -3
data/lib/legion/cache/memory.rb
CHANGED
|
@@ -5,16 +5,181 @@ require 'legion/logging/helper'
|
|
|
5
5
|
|
|
6
6
|
module Legion
|
|
7
7
|
module Cache
|
|
8
|
+
# Shared bounded storage for Memory and Cacheable's process-local fallback.
|
|
9
|
+
# Cleanup exists only while expiring entries exist; clear/close joins it.
|
|
10
|
+
class ExpiringStore
|
|
11
|
+
DEFAULT_MAX_ENTRIES = 10_000
|
|
12
|
+
|
|
13
|
+
def initialize(max_entries: DEFAULT_MAX_ENTRIES)
|
|
14
|
+
@entries = {}
|
|
15
|
+
@expiry = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@condition = ConditionVariable.new
|
|
18
|
+
self.max_entries = max_entries
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def max_entries=(limit)
|
|
22
|
+
raise ArgumentError, 'max_entries must be a positive integer' unless limit.is_a?(Integer) && limit.positive?
|
|
23
|
+
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
@max_entries = limit
|
|
26
|
+
evict_oldest while @entries.size > limit
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(key)
|
|
31
|
+
@mutex.synchronize do
|
|
32
|
+
expire_key(key)
|
|
33
|
+
@entries[key]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set(key, value, ttl: nil, unless_exist: false)
|
|
38
|
+
deadline = expiry_deadline(ttl)
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
expire_key(key)
|
|
41
|
+
return false if unless_exist && @entries.key?(key)
|
|
42
|
+
|
|
43
|
+
write_entry(key, value, deadline)
|
|
44
|
+
end
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def cas(key, ttl: nil)
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
expire_key(key)
|
|
51
|
+
value = yield @entries[key]
|
|
52
|
+
write_entry(key, value, expiry_deadline(ttl))
|
|
53
|
+
value
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def mget(keys)
|
|
58
|
+
@mutex.synchronize do
|
|
59
|
+
keys.to_h do |key|
|
|
60
|
+
expire_key(key)
|
|
61
|
+
[key, @entries[key]]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def mset(values, ttl: nil)
|
|
67
|
+
deadline = expiry_deadline(ttl)
|
|
68
|
+
@mutex.synchronize { values.each { |key, value| write_entry(key, value, deadline) } }
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def delete(key)
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
expire_key(key)
|
|
75
|
+
present = @entries.key?(key)
|
|
76
|
+
@entries.delete(key)
|
|
77
|
+
@expiry.delete(key)
|
|
78
|
+
@condition.signal
|
|
79
|
+
present
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear
|
|
84
|
+
worker = @mutex.synchronize do
|
|
85
|
+
@entries.clear
|
|
86
|
+
@expiry.clear
|
|
87
|
+
@next_expiry = nil
|
|
88
|
+
thread = @cleanup_thread
|
|
89
|
+
@cleanup_thread = nil
|
|
90
|
+
@condition.broadcast
|
|
91
|
+
thread
|
|
92
|
+
end
|
|
93
|
+
worker&.join
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def close
|
|
98
|
+
clear
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Does not opportunistically expire entries: cleanup reclaims inactive keys.
|
|
102
|
+
def size
|
|
103
|
+
@mutex.synchronize { @entries.size }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def expiry_deadline(ttl)
|
|
109
|
+
return if ttl.nil? || ttl.zero?
|
|
110
|
+
raise ArgumentError, 'ttl must be a finite nonnegative number' unless ttl.is_a?(Numeric) && ttl.real? && ttl.finite? && ttl.positive?
|
|
111
|
+
|
|
112
|
+
monotonic_now + ttl
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def monotonic_now
|
|
116
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def expire_key(key)
|
|
120
|
+
return unless @expiry.key?(key) && @expiry[key] <= monotonic_now
|
|
121
|
+
|
|
122
|
+
@entries.delete(key)
|
|
123
|
+
@expiry.delete(key)
|
|
124
|
+
@condition.signal
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write_entry(key, value, deadline)
|
|
128
|
+
if !@entries.key?(key) && @entries.size >= @max_entries
|
|
129
|
+
purge_expired if @next_expiry && @next_expiry <= monotonic_now
|
|
130
|
+
evict_oldest if @entries.size >= @max_entries
|
|
131
|
+
end
|
|
132
|
+
@entries[key] = value
|
|
133
|
+
if deadline
|
|
134
|
+
@expiry[key] = deadline
|
|
135
|
+
@next_expiry = deadline if @next_expiry.nil? || deadline < @next_expiry
|
|
136
|
+
@cleanup_thread ||= Thread.new { cleanup_loop }
|
|
137
|
+
else
|
|
138
|
+
@expiry.delete(key)
|
|
139
|
+
end
|
|
140
|
+
@condition.signal
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def evict_oldest
|
|
144
|
+
key, = @entries.shift
|
|
145
|
+
@expiry.delete(key)
|
|
146
|
+
@condition.signal
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def purge_expired
|
|
150
|
+
now = monotonic_now
|
|
151
|
+
@expiry.delete_if do |key, deadline|
|
|
152
|
+
next false if deadline > now
|
|
153
|
+
|
|
154
|
+
@entries.delete(key)
|
|
155
|
+
true
|
|
156
|
+
end
|
|
157
|
+
@next_expiry = @expiry.each_value.min
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def cleanup_loop
|
|
161
|
+
@mutex.synchronize do
|
|
162
|
+
while @cleanup_thread.equal?(Thread.current)
|
|
163
|
+
purge_expired if @next_expiry && @next_expiry <= monotonic_now
|
|
164
|
+
break if @expiry.empty?
|
|
165
|
+
|
|
166
|
+
delay = @next_expiry - monotonic_now
|
|
167
|
+
@condition.wait(@mutex, delay) if delay.positive?
|
|
168
|
+
end
|
|
169
|
+
@cleanup_thread = nil if @cleanup_thread.equal?(Thread.current)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
8
174
|
module Memory
|
|
9
175
|
extend self
|
|
10
176
|
extend Legion::Logging::Helper
|
|
11
177
|
|
|
12
|
-
@store =
|
|
13
|
-
@expiry = {}
|
|
14
|
-
@mutex = Mutex.new
|
|
178
|
+
@store = ExpiringStore.new
|
|
15
179
|
@connected = Concurrent::AtomicBoolean.new(false)
|
|
16
180
|
|
|
17
|
-
def setup(**)
|
|
181
|
+
def setup(max_entries: nil, **)
|
|
182
|
+
@store.max_entries = max_entries unless max_entries.nil?
|
|
18
183
|
@connected.make_true
|
|
19
184
|
log.info 'Legion::Cache::Memory connected'
|
|
20
185
|
true
|
|
@@ -32,7 +197,7 @@ module Legion
|
|
|
32
197
|
|
|
33
198
|
def restart(**)
|
|
34
199
|
shutdown
|
|
35
|
-
setup
|
|
200
|
+
setup(**)
|
|
36
201
|
rescue StandardError => e
|
|
37
202
|
@connected.make_false
|
|
38
203
|
handle_exception(e, level: :warn, handled: true, operation: :memory_restart)
|
|
@@ -40,12 +205,9 @@ module Legion
|
|
|
40
205
|
end
|
|
41
206
|
|
|
42
207
|
def get(key)
|
|
43
|
-
@
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
log.debug { "[cache:memory] GET #{key} hit=#{!result.nil?}" }
|
|
47
|
-
result
|
|
48
|
-
end
|
|
208
|
+
result = @store.get(key)
|
|
209
|
+
log.debug { "[cache:memory] GET #{key} hit=#{!result.nil?}" }
|
|
210
|
+
result
|
|
49
211
|
rescue StandardError => e
|
|
50
212
|
handle_exception(e, level: :warn, handled: true, operation: :memory_get)
|
|
51
213
|
nil
|
|
@@ -55,31 +217,40 @@ module Legion
|
|
|
55
217
|
set_sync(key, value, ttl: ttl, phi: phi)
|
|
56
218
|
end
|
|
57
219
|
|
|
220
|
+
def set_nx(key, value, ttl: nil, phi: false)
|
|
221
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
222
|
+
result = @store.set(key, value, ttl: ttl, unless_exist: true)
|
|
223
|
+
log.debug { "[cache:memory] SET_NX #{key} ttl=#{ttl.inspect} result=#{result}" }
|
|
224
|
+
result
|
|
225
|
+
end
|
|
226
|
+
|
|
58
227
|
def set_sync(key, value, ttl: nil, phi: false)
|
|
59
|
-
ttl = enforce_phi_ttl(ttl
|
|
60
|
-
@
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
228
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
229
|
+
@store.set(key, value, ttl: ttl)
|
|
230
|
+
log.debug { "[cache:memory] SET #{key} ttl=#{ttl.inspect}" }
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def cas(key, ttl: nil, phi: false, &)
|
|
235
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
236
|
+
value = @store.cas(key, ttl: ttl, &)
|
|
237
|
+
log.debug { "[cache:memory] CAS #{key} ttl=#{ttl.inspect}" }
|
|
238
|
+
value
|
|
70
239
|
end
|
|
71
240
|
|
|
72
|
-
def fetch(key, ttl: nil, &block)
|
|
241
|
+
def fetch(key, ttl: nil, phi: false, &block)
|
|
242
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
73
243
|
val = get(key)
|
|
74
244
|
return val unless val.nil?
|
|
75
245
|
|
|
76
246
|
log.debug { "[cache:memory] FETCH #{key} miss=true" }
|
|
77
247
|
val = block&.call
|
|
78
|
-
|
|
248
|
+
begin
|
|
249
|
+
set(key, val, ttl: ttl)
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_fetch_write)
|
|
252
|
+
end
|
|
79
253
|
val
|
|
80
|
-
rescue StandardError => e
|
|
81
|
-
handle_exception(e, level: :warn, handled: true, operation: :memory_fetch)
|
|
82
|
-
nil
|
|
83
254
|
end
|
|
84
255
|
|
|
85
256
|
def delete(key, async: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
@@ -87,59 +258,29 @@ module Legion
|
|
|
87
258
|
end
|
|
88
259
|
|
|
89
260
|
def delete_sync(key)
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
log.debug { "[cache:memory] DELETE #{key} success=#{!removed.nil?}" }
|
|
94
|
-
!removed.nil?
|
|
95
|
-
end
|
|
261
|
+
removed = @store.delete(key)
|
|
262
|
+
log.debug { "[cache:memory] DELETE #{key} success=#{removed}" }
|
|
263
|
+
removed
|
|
96
264
|
end
|
|
97
265
|
|
|
98
266
|
def mget(*keys)
|
|
99
|
-
keys
|
|
100
|
-
return {} if keys.empty?
|
|
101
|
-
|
|
102
|
-
@mutex.synchronize do
|
|
103
|
-
keys.each { |k| expire_if_needed(k) }
|
|
104
|
-
keys.to_h { |k| [k, @store[k]] }
|
|
105
|
-
end
|
|
267
|
+
@store.mget(keys.flatten)
|
|
106
268
|
rescue StandardError => e
|
|
107
269
|
handle_exception(e, level: :warn, handled: true, operation: :memory_mget)
|
|
108
270
|
{}
|
|
109
271
|
end
|
|
110
272
|
|
|
111
|
-
def mset(hash, ttl: nil, async: true)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
hash.each { |k, v| set(k, v, ttl: ttl, async: async) }
|
|
115
|
-
true
|
|
116
|
-
rescue StandardError => e
|
|
117
|
-
handle_exception(e, level: :warn, handled: true, operation: :memory_mset)
|
|
118
|
-
true
|
|
273
|
+
def mset(hash, ttl: nil, async: true, phi: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
274
|
+
mset_sync(hash, ttl: ttl, phi: phi)
|
|
119
275
|
end
|
|
120
276
|
|
|
121
277
|
def mset_sync(hash, ttl: nil, phi: false)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@mutex.synchronize do
|
|
125
|
-
hash.each do |key, value|
|
|
126
|
-
effective_ttl = phi ? enforce_phi_ttl(ttl, phi: true) : ttl
|
|
127
|
-
@store[key] = value
|
|
128
|
-
if effective_ttl&.positive?
|
|
129
|
-
@expiry[key] = Time.now + effective_ttl
|
|
130
|
-
else
|
|
131
|
-
@expiry.delete(key)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
true
|
|
135
|
-
end
|
|
278
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
279
|
+
@store.mset(hash, ttl: ttl)
|
|
136
280
|
end
|
|
137
281
|
|
|
138
282
|
def flush
|
|
139
|
-
@
|
|
140
|
-
@store.clear
|
|
141
|
-
@expiry.clear
|
|
142
|
-
end
|
|
283
|
+
@store.clear
|
|
143
284
|
log.info 'Legion::Cache::Memory flushed'
|
|
144
285
|
true
|
|
145
286
|
rescue StandardError => e
|
|
@@ -147,10 +288,12 @@ module Legion
|
|
|
147
288
|
false
|
|
148
289
|
end
|
|
149
290
|
|
|
150
|
-
def close
|
|
291
|
+
def close
|
|
292
|
+
shutdown
|
|
293
|
+
end
|
|
151
294
|
|
|
152
295
|
def shutdown
|
|
153
|
-
|
|
296
|
+
@store.close
|
|
154
297
|
@connected.make_false
|
|
155
298
|
log.info 'Legion::Cache::Memory shut down'
|
|
156
299
|
false
|
|
@@ -161,11 +304,9 @@ module Legion
|
|
|
161
304
|
end
|
|
162
305
|
|
|
163
306
|
def reset!
|
|
164
|
-
@
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@expiry.clear
|
|
168
|
-
end
|
|
307
|
+
@store.clear
|
|
308
|
+
@store.max_entries = ExpiringStore::DEFAULT_MAX_ENTRIES
|
|
309
|
+
@connected.make_false
|
|
169
310
|
log.info 'Legion::Cache::Memory state reset'
|
|
170
311
|
false
|
|
171
312
|
end
|
|
@@ -175,24 +316,18 @@ module Legion
|
|
|
175
316
|
|
|
176
317
|
private
|
|
177
318
|
|
|
178
|
-
def
|
|
179
|
-
return unless @expiry.key?(key) && Time.now > @expiry[key]
|
|
180
|
-
|
|
181
|
-
@store.delete(key)
|
|
182
|
-
@expiry.delete(key)
|
|
183
|
-
log.debug { "[cache:memory] EXPIRE #{key}" }
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def enforce_phi_ttl(ttl, phi: false)
|
|
187
|
-
return ttl unless phi
|
|
188
|
-
|
|
319
|
+
def enforce_phi_ttl(ttl)
|
|
189
320
|
max = if defined?(Legion::Settings)
|
|
190
|
-
Legion::Settings.dig(:cache, :compliance, :phi_max_ttl)
|
|
321
|
+
Legion::Settings.dig(:cache, :compliance, :phi_max_ttl)
|
|
191
322
|
else
|
|
192
323
|
3600
|
|
193
324
|
end
|
|
194
|
-
|
|
195
|
-
|
|
325
|
+
max = 3600 if max.nil?
|
|
326
|
+
raise ArgumentError, 'phi_max_ttl must be a finite positive number' unless max.is_a?(Numeric) && max.real? && max.finite? && max.positive?
|
|
327
|
+
return max if ttl.nil?
|
|
328
|
+
raise ArgumentError, 'ttl must be a finite number' unless ttl.is_a?(Numeric) && ttl.real? && ttl.finite?
|
|
329
|
+
|
|
330
|
+
ttl.positive? ? [ttl, max].min : max
|
|
196
331
|
end
|
|
197
332
|
end
|
|
198
333
|
end
|
data/lib/legion/cache/pool.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'connection_pool'
|
|
4
|
+
require 'concurrent'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require 'socket'
|
|
4
7
|
require 'legion/logging/helper'
|
|
5
8
|
|
|
6
9
|
module Legion
|
|
@@ -10,10 +13,7 @@ module Legion
|
|
|
10
13
|
extend Legion::Logging::Helper
|
|
11
14
|
|
|
12
15
|
def connected?
|
|
13
|
-
|
|
14
|
-
return @connected.true? if @connected.respond_to?(:true?)
|
|
15
|
-
|
|
16
|
-
@connected == true
|
|
16
|
+
@connected&.value == true
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def size
|
|
@@ -33,27 +33,44 @@ module Legion
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def close
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@client.shutdown(&:close)
|
|
36
|
+
pool = @client
|
|
39
37
|
@client = nil
|
|
40
|
-
|
|
38
|
+
cache_mark_connected(false)
|
|
39
|
+
pool&.shutdown(&:close)
|
|
41
40
|
log.info "#{pool_log_name} pool closed"
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
def restart(**
|
|
43
|
+
def restart(**)
|
|
45
44
|
close
|
|
46
|
-
|
|
47
|
-
client_hash = opts
|
|
48
|
-
client_hash[:pool_size] = opts[:pool_size] if opts.key? :pool_size
|
|
49
|
-
client_hash[:timeout] = opts[:timeout] if opts.key? :timeout
|
|
50
|
-
client(**client_hash)
|
|
51
|
-
@connected = true
|
|
45
|
+
client(**)
|
|
52
46
|
log.info "#{pool_log_name} pool restarted"
|
|
53
47
|
end
|
|
54
48
|
|
|
55
49
|
private
|
|
56
50
|
|
|
51
|
+
def cache_mark_connected(value)
|
|
52
|
+
@connected ||= Concurrent::AtomicBoolean.new(false)
|
|
53
|
+
@connected.value = value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cache_backend_failed(error)
|
|
57
|
+
return unless cache_connection_error?(error)
|
|
58
|
+
|
|
59
|
+
cache_mark_connected(false)
|
|
60
|
+
@cache_failure_callback&.call(error)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cache_connection_error?(error)
|
|
64
|
+
return true if error.is_a?(IOError) || error.is_a?(SystemCallError) || error.is_a?(Timeout::Error) || error.is_a?(SocketError)
|
|
65
|
+
return true if error.is_a?(ConnectionPool::TimeoutError)
|
|
66
|
+
return error.errors.values.any? { |nested| cache_connection_error?(nested) } if error.instance_of?(::Redis::Cluster::CommandErrorCollection)
|
|
67
|
+
|
|
68
|
+
error.class.ancestors.any? do |type|
|
|
69
|
+
%w[Dalli::NetworkError Dalli::RingError Redis::BaseConnectionError RedisClient::ConnectionError
|
|
70
|
+
Redis::Cluster::InitialSetupError Redis::Cluster::NodeMightBeDown].include?(type.name)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
57
74
|
def pool_log_name
|
|
58
75
|
if respond_to?(:name)
|
|
59
76
|
label = name.to_s
|
|
@@ -10,6 +10,7 @@ module Legion
|
|
|
10
10
|
|
|
11
11
|
DEFAULT_INITIAL_DELAY = 1
|
|
12
12
|
DEFAULT_MAX_DELAY = 60
|
|
13
|
+
STOP_TIMEOUT = 1
|
|
13
14
|
|
|
14
15
|
def initialize(tier:, connect_block:, enabled_block:, settings_key: :cache)
|
|
15
16
|
@tier = tier
|
|
@@ -19,13 +20,17 @@ module Legion
|
|
|
19
20
|
@attempts = Concurrent::AtomicFixnum.new(0)
|
|
20
21
|
@thread = nil
|
|
21
22
|
@mutex = Mutex.new
|
|
23
|
+
@wake = ConditionVariable.new
|
|
24
|
+
@stopping = false
|
|
22
25
|
@stop_signal = Concurrent::AtomicBoolean.new(false)
|
|
23
26
|
@next_retry_at = nil
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def start
|
|
30
|
+
return unless configured_enabled?
|
|
31
|
+
|
|
27
32
|
@mutex.synchronize do
|
|
28
|
-
return if running?
|
|
33
|
+
return if @stopping || running?
|
|
29
34
|
|
|
30
35
|
@stop_signal.make_false
|
|
31
36
|
@thread = Thread.new { reconnect_loop }
|
|
@@ -34,13 +39,25 @@ module Legion
|
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
def stop
|
|
37
|
-
thread_to_join =
|
|
38
|
-
|
|
42
|
+
thread_to_join = @mutex.synchronize do
|
|
43
|
+
@stopping = true unless @thread == Thread.current
|
|
39
44
|
@stop_signal.make_true
|
|
40
|
-
|
|
41
|
-
@thread
|
|
45
|
+
@wake.broadcast
|
|
46
|
+
@thread
|
|
47
|
+
end
|
|
48
|
+
Thread.exit if thread_to_join == Thread.current
|
|
49
|
+
|
|
50
|
+
if thread_to_join && !thread_to_join.join(STOP_TIMEOUT)
|
|
51
|
+
thread_to_join.kill
|
|
52
|
+
raise ThreadError, "Legion::Cache::Reconnector[#{@tier}] could not stop its connection callback" unless thread_to_join.join(STOP_TIMEOUT)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
if @thread == thread_to_join
|
|
57
|
+
@thread = nil
|
|
58
|
+
@stopping = false
|
|
59
|
+
end
|
|
42
60
|
end
|
|
43
|
-
thread_to_join&.join(5)
|
|
44
61
|
log.info "Legion::Cache::Reconnector[#{@tier}] stopped"
|
|
45
62
|
end
|
|
46
63
|
|
|
@@ -60,19 +77,19 @@ module Legion
|
|
|
60
77
|
delay = configured_initial_delay
|
|
61
78
|
|
|
62
79
|
until @stop_signal.true?
|
|
63
|
-
unless @enabled_block.call
|
|
64
|
-
|
|
80
|
+
unless configured_enabled? && @enabled_block.call
|
|
81
|
+
wait_for_retry(1)
|
|
65
82
|
next
|
|
66
83
|
end
|
|
67
84
|
|
|
68
85
|
begin
|
|
69
86
|
@next_retry_at = Time.now + delay
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
return unless wait_for_retry(delay)
|
|
88
|
+
next unless configured_enabled? && @enabled_block.call
|
|
72
89
|
|
|
73
90
|
@connect_block.call
|
|
74
91
|
count = @attempts.value
|
|
75
|
-
@attempts =
|
|
92
|
+
@attempts.value = 0
|
|
76
93
|
@next_retry_at = nil
|
|
77
94
|
log.info "Legion::Cache::Reconnector[#{@tier}] reconnected after #{count} attempts"
|
|
78
95
|
return
|
|
@@ -86,6 +103,28 @@ module Legion
|
|
|
86
103
|
end
|
|
87
104
|
rescue StandardError => e
|
|
88
105
|
handle_exception(e, level: :error, handled: true, operation: :"reconnector_#{@tier}_loop")
|
|
106
|
+
ensure
|
|
107
|
+
@next_retry_at = nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def wait_for_retry(delay)
|
|
111
|
+
deadline = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC) + delay
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
until @stop_signal.true?
|
|
114
|
+
remaining = deadline - ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
115
|
+
return true unless remaining.positive?
|
|
116
|
+
|
|
117
|
+
@wake.wait(@mutex, remaining)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def configured_enabled?
|
|
124
|
+
return true unless defined?(Legion::Settings)
|
|
125
|
+
|
|
126
|
+
Legion::Settings.dig(@settings_key, :enabled) != false &&
|
|
127
|
+
Legion::Settings.dig(@settings_key, :reconnect, :enabled) != false
|
|
89
128
|
end
|
|
90
129
|
|
|
91
130
|
def configured_initial_delay
|