legion-cache 1.4.2 → 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 +90 -26
- data/lib/legion/cache/memory.rb +220 -104
- data/lib/legion/cache/pool.rb +32 -15
- data/lib/legion/cache/reconnector.rb +50 -11
- data/lib/legion/cache/redis.rb +146 -120
- 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 +220 -356
- 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,50 +217,40 @@ module Legion
|
|
|
55
217
|
set_sync(key, value, ttl: ttl, phi: phi)
|
|
56
218
|
end
|
|
57
219
|
|
|
58
|
-
def set_nx(key, value, ttl: nil)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@store[key] = value
|
|
64
|
-
if ttl&.positive?
|
|
65
|
-
@expiry[key] = Time.now + ttl
|
|
66
|
-
else
|
|
67
|
-
@expiry.delete(key)
|
|
68
|
-
end
|
|
69
|
-
log.debug { "[cache:memory] SET_NX #{key} ttl=#{ttl.inspect} result=true" }
|
|
70
|
-
true
|
|
71
|
-
end
|
|
72
|
-
rescue StandardError => e
|
|
73
|
-
handle_exception(e, level: :warn, handled: true, operation: :memory_set_nx)
|
|
74
|
-
false
|
|
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
|
|
75
225
|
end
|
|
76
226
|
|
|
77
227
|
def set_sync(key, value, ttl: nil, phi: false)
|
|
78
|
-
ttl = enforce_phi_ttl(ttl
|
|
79
|
-
@
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
89
239
|
end
|
|
90
240
|
|
|
91
|
-
def fetch(key, ttl: nil, &block)
|
|
241
|
+
def fetch(key, ttl: nil, phi: false, &block)
|
|
242
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
92
243
|
val = get(key)
|
|
93
244
|
return val unless val.nil?
|
|
94
245
|
|
|
95
246
|
log.debug { "[cache:memory] FETCH #{key} miss=true" }
|
|
96
247
|
val = block&.call
|
|
97
|
-
|
|
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
|
|
98
253
|
val
|
|
99
|
-
rescue StandardError => e
|
|
100
|
-
handle_exception(e, level: :warn, handled: true, operation: :memory_fetch)
|
|
101
|
-
nil
|
|
102
254
|
end
|
|
103
255
|
|
|
104
256
|
def delete(key, async: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
@@ -106,59 +258,29 @@ module Legion
|
|
|
106
258
|
end
|
|
107
259
|
|
|
108
260
|
def delete_sync(key)
|
|
109
|
-
@
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
log.debug { "[cache:memory] DELETE #{key} success=#{!removed.nil?}" }
|
|
113
|
-
!removed.nil?
|
|
114
|
-
end
|
|
261
|
+
removed = @store.delete(key)
|
|
262
|
+
log.debug { "[cache:memory] DELETE #{key} success=#{removed}" }
|
|
263
|
+
removed
|
|
115
264
|
end
|
|
116
265
|
|
|
117
266
|
def mget(*keys)
|
|
118
|
-
keys
|
|
119
|
-
return {} if keys.empty?
|
|
120
|
-
|
|
121
|
-
@mutex.synchronize do
|
|
122
|
-
keys.each { |k| expire_if_needed(k) }
|
|
123
|
-
keys.to_h { |k| [k, @store[k]] }
|
|
124
|
-
end
|
|
267
|
+
@store.mget(keys.flatten)
|
|
125
268
|
rescue StandardError => e
|
|
126
269
|
handle_exception(e, level: :warn, handled: true, operation: :memory_mget)
|
|
127
270
|
{}
|
|
128
271
|
end
|
|
129
272
|
|
|
130
|
-
def mset(hash, ttl: nil, async: true)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
hash.each { |k, v| set(k, v, ttl: ttl, async: async) }
|
|
134
|
-
true
|
|
135
|
-
rescue StandardError => e
|
|
136
|
-
handle_exception(e, level: :warn, handled: true, operation: :memory_mset)
|
|
137
|
-
true
|
|
273
|
+
def mset(hash, ttl: nil, async: true, phi: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
274
|
+
mset_sync(hash, ttl: ttl, phi: phi)
|
|
138
275
|
end
|
|
139
276
|
|
|
140
277
|
def mset_sync(hash, ttl: nil, phi: false)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@mutex.synchronize do
|
|
144
|
-
hash.each do |key, value|
|
|
145
|
-
effective_ttl = phi ? enforce_phi_ttl(ttl, phi: true) : ttl
|
|
146
|
-
@store[key] = value
|
|
147
|
-
if effective_ttl&.positive?
|
|
148
|
-
@expiry[key] = Time.now + effective_ttl
|
|
149
|
-
else
|
|
150
|
-
@expiry.delete(key)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
true
|
|
154
|
-
end
|
|
278
|
+
ttl = enforce_phi_ttl(ttl) if phi
|
|
279
|
+
@store.mset(hash, ttl: ttl)
|
|
155
280
|
end
|
|
156
281
|
|
|
157
282
|
def flush
|
|
158
|
-
@
|
|
159
|
-
@store.clear
|
|
160
|
-
@expiry.clear
|
|
161
|
-
end
|
|
283
|
+
@store.clear
|
|
162
284
|
log.info 'Legion::Cache::Memory flushed'
|
|
163
285
|
true
|
|
164
286
|
rescue StandardError => e
|
|
@@ -166,10 +288,12 @@ module Legion
|
|
|
166
288
|
false
|
|
167
289
|
end
|
|
168
290
|
|
|
169
|
-
def close
|
|
291
|
+
def close
|
|
292
|
+
shutdown
|
|
293
|
+
end
|
|
170
294
|
|
|
171
295
|
def shutdown
|
|
172
|
-
|
|
296
|
+
@store.close
|
|
173
297
|
@connected.make_false
|
|
174
298
|
log.info 'Legion::Cache::Memory shut down'
|
|
175
299
|
false
|
|
@@ -180,11 +304,9 @@ module Legion
|
|
|
180
304
|
end
|
|
181
305
|
|
|
182
306
|
def reset!
|
|
183
|
-
@
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@expiry.clear
|
|
187
|
-
end
|
|
307
|
+
@store.clear
|
|
308
|
+
@store.max_entries = ExpiringStore::DEFAULT_MAX_ENTRIES
|
|
309
|
+
@connected.make_false
|
|
188
310
|
log.info 'Legion::Cache::Memory state reset'
|
|
189
311
|
false
|
|
190
312
|
end
|
|
@@ -194,24 +316,18 @@ module Legion
|
|
|
194
316
|
|
|
195
317
|
private
|
|
196
318
|
|
|
197
|
-
def
|
|
198
|
-
return unless @expiry.key?(key) && Time.now > @expiry[key]
|
|
199
|
-
|
|
200
|
-
@store.delete(key)
|
|
201
|
-
@expiry.delete(key)
|
|
202
|
-
log.debug { "[cache:memory] EXPIRE #{key}" }
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def enforce_phi_ttl(ttl, phi: false)
|
|
206
|
-
return ttl unless phi
|
|
207
|
-
|
|
319
|
+
def enforce_phi_ttl(ttl)
|
|
208
320
|
max = if defined?(Legion::Settings)
|
|
209
|
-
Legion::Settings.dig(:cache, :compliance, :phi_max_ttl)
|
|
321
|
+
Legion::Settings.dig(:cache, :compliance, :phi_max_ttl)
|
|
210
322
|
else
|
|
211
323
|
3600
|
|
212
324
|
end
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
215
331
|
end
|
|
216
332
|
end
|
|
217
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
|