legion-cache 1.3.21 → 1.4.1
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/CHANGELOG.md +60 -0
- data/legion-cache.gemspec +1 -0
- data/lib/legion/cache/async_writer.rb +126 -0
- data/lib/legion/cache/cacheable.rb +4 -4
- data/lib/legion/cache/helper.rb +11 -11
- data/lib/legion/cache/local.rb +91 -29
- data/lib/legion/cache/memcached.rb +53 -40
- data/lib/legion/cache/memory.rb +107 -19
- data/lib/legion/cache/pool.rb +4 -1
- data/lib/legion/cache/reconnector.rb +108 -0
- data/lib/legion/cache/redis.rb +108 -47
- data/lib/legion/cache/redis_hash.rb +8 -8
- data/lib/legion/cache/settings.rb +52 -39
- data/lib/legion/cache/version.rb +1 -1
- data/lib/legion/cache.rb +310 -70
- metadata +17 -1
|
@@ -12,34 +12,38 @@ module Legion
|
|
|
12
12
|
extend self
|
|
13
13
|
extend Legion::Logging::Helper
|
|
14
14
|
|
|
15
|
-
def client(server: nil, servers: nil,
|
|
15
|
+
def client(server: nil, servers: nil, pool_size: nil, timeout: nil, # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/ParameterLists
|
|
16
|
+
username: nil, password: nil, logger: nil, **opts)
|
|
16
17
|
return @client unless @client.nil?
|
|
17
18
|
|
|
18
19
|
settings = defined?(Legion::Settings) ? Legion::Settings[:cache] : {}
|
|
19
20
|
servers ||= settings[:servers] || []
|
|
20
21
|
@component_logger = logger || log
|
|
21
22
|
|
|
22
|
-
@pool_size =
|
|
23
|
-
@timeout =
|
|
23
|
+
@pool_size = pool_size || settings[:pool_size] || 10
|
|
24
|
+
@timeout = timeout || settings[:timeout] || 5
|
|
24
25
|
|
|
25
26
|
resolved = Legion::Cache::Settings.resolve_servers(
|
|
26
27
|
driver: 'memcached', server: server, servers: Array(servers)
|
|
27
28
|
)
|
|
28
29
|
|
|
29
|
-
Dalli.logger =
|
|
30
|
+
Dalli.logger = log
|
|
30
31
|
cache_opts = settings.merge(opts)
|
|
31
32
|
cache_opts[:value_max_bytes] ||= 8 * 1024 * 1024
|
|
32
33
|
cache_opts[:serializer] ||= Legion::JSON
|
|
34
|
+
cache_opts[:username] = username unless username.nil?
|
|
35
|
+
cache_opts[:password] = password unless password.nil?
|
|
33
36
|
|
|
34
37
|
tls_ctx = memcached_tls_context(port: resolved.first.split(':').last.to_i)
|
|
35
38
|
cache_opts[:ssl_context] = tls_ctx if tls_ctx
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
checkout_timeout = opts[:pool_checkout_timeout] || settings[:pool_checkout_timeout] || @timeout
|
|
41
|
+
@client = ConnectionPool.new(size: @pool_size, timeout: checkout_timeout) do
|
|
38
42
|
Dalli::Client.new(resolved, cache_opts)
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
@connected = true
|
|
42
|
-
|
|
46
|
+
log.info "Memcached connected to #{resolved.join(', ')}"
|
|
43
47
|
@client
|
|
44
48
|
rescue StandardError => e
|
|
45
49
|
handle_exception(e, level: :error, handled: false, operation: :memcached_client,
|
|
@@ -50,14 +54,14 @@ module Legion
|
|
|
50
54
|
|
|
51
55
|
def get(key)
|
|
52
56
|
result = client.with { |conn| conn.get(key) }
|
|
53
|
-
|
|
57
|
+
log.debug { "[cache] GET #{key} hit=#{!result.nil?}" }
|
|
54
58
|
result
|
|
55
59
|
rescue StandardError => e
|
|
56
|
-
handle_exception(e, level: :warn, handled:
|
|
57
|
-
|
|
60
|
+
handle_exception(e, level: :warn, handled: true, operation: :memcached_get, key: key)
|
|
61
|
+
nil
|
|
58
62
|
end
|
|
59
63
|
|
|
60
|
-
def fetch(key, ttl
|
|
64
|
+
def fetch(key, ttl: nil, &)
|
|
61
65
|
result = client.with do |conn|
|
|
62
66
|
if block_given?
|
|
63
67
|
conn.fetch(key, ttl, &)
|
|
@@ -65,38 +69,47 @@ module Legion
|
|
|
65
69
|
conn.fetch(key, ttl)
|
|
66
70
|
end
|
|
67
71
|
end
|
|
68
|
-
|
|
72
|
+
log.debug { "[cache] FETCH #{key} hit=#{!result.nil?}" }
|
|
69
73
|
result
|
|
70
74
|
rescue StandardError => e
|
|
71
|
-
handle_exception(e, level: :warn, handled:
|
|
72
|
-
|
|
75
|
+
handle_exception(e, level: :warn, handled: true, operation: :memcached_fetch, key: key, ttl: ttl)
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def set(key, value, ttl: nil, **)
|
|
80
|
+
set_sync(key, value, ttl: ttl, **)
|
|
73
81
|
end
|
|
74
82
|
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
def set_sync(key, value, ttl: nil, **)
|
|
84
|
+
effective_ttl = ttl || default_ttl
|
|
85
|
+
result = client.with { |conn| conn.set(key, value, effective_ttl).positive? }
|
|
86
|
+
log.debug { "[cache] SET #{key} ttl=#{effective_ttl} success=#{result} value_class=#{value.class}" }
|
|
78
87
|
result
|
|
79
88
|
rescue StandardError => e
|
|
80
|
-
handle_exception(e, level: :
|
|
89
|
+
handle_exception(e, level: :error, handled: false, operation: :memcached_set_sync, key: key, ttl: effective_ttl)
|
|
81
90
|
raise
|
|
82
91
|
end
|
|
83
92
|
|
|
84
|
-
def delete(key)
|
|
93
|
+
def delete(key, **)
|
|
94
|
+
delete_sync(key)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def delete_sync(key)
|
|
85
98
|
result = client.with { |conn| conn.delete(key) == true }
|
|
86
|
-
|
|
99
|
+
log.debug { "[cache] DELETE #{key} success=#{result}" }
|
|
87
100
|
result
|
|
88
101
|
rescue StandardError => e
|
|
89
|
-
handle_exception(e, level: :
|
|
102
|
+
handle_exception(e, level: :error, handled: false, operation: :memcached_delete_sync, key: key)
|
|
90
103
|
raise
|
|
91
104
|
end
|
|
92
105
|
|
|
93
|
-
def flush
|
|
94
|
-
result = client.with { |conn| conn.flush
|
|
95
|
-
|
|
106
|
+
def flush
|
|
107
|
+
result = client.with { |conn| conn.flush.first }
|
|
108
|
+
log.debug { '[cache] FLUSH completed' }
|
|
96
109
|
result
|
|
97
110
|
rescue StandardError => e
|
|
98
|
-
handle_exception(e, level: :warn, handled:
|
|
99
|
-
|
|
111
|
+
handle_exception(e, level: :warn, handled: true, operation: :memcached_flush)
|
|
112
|
+
nil
|
|
100
113
|
end
|
|
101
114
|
|
|
102
115
|
def mget(*keys)
|
|
@@ -104,36 +117,36 @@ module Legion
|
|
|
104
117
|
return {} if keys.empty?
|
|
105
118
|
|
|
106
119
|
result = client.with { |conn| conn.get_multi(*keys) }
|
|
107
|
-
|
|
120
|
+
log.debug { "[cache] MGET keys=#{keys.size} hits=#{result.size}" }
|
|
108
121
|
result
|
|
109
122
|
rescue StandardError => e
|
|
110
|
-
handle_exception(e, level: :warn, handled:
|
|
111
|
-
|
|
123
|
+
handle_exception(e, level: :warn, handled: true, operation: :memcached_mget, key_count: keys.size)
|
|
124
|
+
{}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def mset(hash, ttl: nil, **)
|
|
128
|
+
mset_sync(hash, ttl: ttl)
|
|
112
129
|
end
|
|
113
130
|
|
|
114
|
-
def
|
|
131
|
+
def mset_sync(hash, ttl: nil, **) # rubocop:disable Lint/UnusedMethodArgument
|
|
115
132
|
return true if hash.empty?
|
|
116
133
|
|
|
117
134
|
client.with { |conn| conn.set_multi(hash) }
|
|
118
|
-
|
|
135
|
+
log.debug { "[cache] MSET keys=#{hash.size}" }
|
|
119
136
|
true
|
|
120
137
|
rescue StandardError => e
|
|
121
|
-
handle_exception(e, level: :
|
|
138
|
+
handle_exception(e, level: :error, handled: false, operation: :memcached_mset_sync, key_count: hash.size)
|
|
122
139
|
raise
|
|
123
140
|
end
|
|
124
141
|
|
|
125
142
|
private
|
|
126
143
|
|
|
127
|
-
def
|
|
128
|
-
|
|
129
|
-
end
|
|
144
|
+
def default_ttl
|
|
145
|
+
return 3600 unless defined?(Legion::Settings)
|
|
130
146
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
else
|
|
135
|
-
cache_logger
|
|
136
|
-
end
|
|
147
|
+
Legion::Settings.dig(:cache, :default_ttl) || 3600
|
|
148
|
+
rescue StandardError
|
|
149
|
+
3600
|
|
137
150
|
end
|
|
138
151
|
|
|
139
152
|
def memcached_tls_context(port:)
|
data/lib/legion/cache/memory.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'concurrent'
|
|
3
4
|
require 'legion/logging/helper'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
@@ -11,18 +12,31 @@ module Legion
|
|
|
11
12
|
@store = {}
|
|
12
13
|
@expiry = {}
|
|
13
14
|
@mutex = Mutex.new
|
|
14
|
-
@connected = false
|
|
15
|
+
@connected = Concurrent::AtomicBoolean.new(false)
|
|
15
16
|
|
|
16
17
|
def setup(**)
|
|
17
|
-
@connected
|
|
18
|
+
@connected.make_true
|
|
18
19
|
log.info 'Legion::Cache::Memory connected'
|
|
19
|
-
|
|
20
|
+
true
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
@connected.make_false
|
|
23
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_setup)
|
|
24
|
+
false
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def client(**) = self
|
|
23
28
|
|
|
24
29
|
def connected?
|
|
25
|
-
@connected
|
|
30
|
+
@connected.true?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def restart(**)
|
|
34
|
+
shutdown
|
|
35
|
+
setup
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
@connected.make_false
|
|
38
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_restart)
|
|
39
|
+
false
|
|
26
40
|
end
|
|
27
41
|
|
|
28
42
|
def get(key)
|
|
@@ -32,9 +46,17 @@ module Legion
|
|
|
32
46
|
log.debug { "[cache:memory] GET #{key} hit=#{!result.nil?}" }
|
|
33
47
|
result
|
|
34
48
|
end
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_get)
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set(key, value, ttl: nil, async: true, phi: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
55
|
+
set_sync(key, value, ttl: ttl, phi: phi)
|
|
35
56
|
end
|
|
36
57
|
|
|
37
|
-
def
|
|
58
|
+
def set_sync(key, value, ttl: nil, phi: false)
|
|
59
|
+
ttl = enforce_phi_ttl(ttl, phi: phi) if phi
|
|
38
60
|
@mutex.synchronize do
|
|
39
61
|
@store[key] = value
|
|
40
62
|
if ttl&.positive?
|
|
@@ -43,55 +65,109 @@ module Legion
|
|
|
43
65
|
@expiry.delete(key)
|
|
44
66
|
end
|
|
45
67
|
log.debug { "[cache:memory] SET #{key} ttl=#{ttl.inspect}" }
|
|
46
|
-
|
|
68
|
+
true
|
|
47
69
|
end
|
|
48
70
|
end
|
|
49
71
|
|
|
50
|
-
def fetch(key, ttl
|
|
72
|
+
def fetch(key, ttl: nil, &block)
|
|
51
73
|
val = get(key)
|
|
52
74
|
return val unless val.nil?
|
|
53
75
|
|
|
54
76
|
log.debug { "[cache:memory] FETCH #{key} miss=true" }
|
|
55
|
-
val =
|
|
56
|
-
set(key, val, ttl)
|
|
77
|
+
val = block&.call
|
|
78
|
+
set(key, val, ttl: ttl)
|
|
57
79
|
val
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_fetch)
|
|
82
|
+
nil
|
|
58
83
|
end
|
|
59
84
|
|
|
60
|
-
def delete(key)
|
|
85
|
+
def delete(key, async: true) # rubocop:disable Lint/UnusedMethodArgument
|
|
86
|
+
delete_sync(key)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def delete_sync(key)
|
|
61
90
|
@mutex.synchronize do
|
|
62
91
|
removed = @store.delete(key)
|
|
63
92
|
@expiry.delete(key)
|
|
64
93
|
log.debug { "[cache:memory] DELETE #{key} success=#{!removed.nil?}" }
|
|
65
|
-
removed
|
|
94
|
+
!removed.nil?
|
|
66
95
|
end
|
|
67
96
|
end
|
|
68
97
|
|
|
69
|
-
def
|
|
70
|
-
|
|
98
|
+
def mget(*keys)
|
|
99
|
+
keys = keys.flatten
|
|
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
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_mget)
|
|
108
|
+
{}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mset(hash, ttl: nil, async: true)
|
|
112
|
+
return true if hash.empty?
|
|
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
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def mset_sync(hash, ttl: nil, phi: false)
|
|
122
|
+
return true if hash.empty?
|
|
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
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def flush
|
|
139
|
+
@mutex.synchronize do
|
|
71
140
|
@store.clear
|
|
72
141
|
@expiry.clear
|
|
73
142
|
end
|
|
74
143
|
log.info 'Legion::Cache::Memory flushed'
|
|
75
|
-
|
|
144
|
+
true
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_flush)
|
|
147
|
+
false
|
|
76
148
|
end
|
|
77
149
|
|
|
78
150
|
def close = nil
|
|
79
151
|
|
|
80
152
|
def shutdown
|
|
81
153
|
flush
|
|
82
|
-
@connected
|
|
154
|
+
@connected.make_false
|
|
83
155
|
log.info 'Legion::Cache::Memory shut down'
|
|
84
|
-
|
|
156
|
+
false
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
@connected.make_false
|
|
159
|
+
handle_exception(e, level: :warn, handled: true, operation: :memory_shutdown)
|
|
160
|
+
false
|
|
85
161
|
end
|
|
86
162
|
|
|
87
163
|
def reset!
|
|
88
|
-
|
|
164
|
+
@mutex.synchronize do
|
|
165
|
+
@connected.make_false
|
|
89
166
|
@store.clear
|
|
90
167
|
@expiry.clear
|
|
91
|
-
@connected = false
|
|
92
168
|
end
|
|
93
169
|
log.info 'Legion::Cache::Memory state reset'
|
|
94
|
-
|
|
170
|
+
false
|
|
95
171
|
end
|
|
96
172
|
|
|
97
173
|
def size = 1
|
|
@@ -106,6 +182,18 @@ module Legion
|
|
|
106
182
|
@expiry.delete(key)
|
|
107
183
|
log.debug { "[cache:memory] EXPIRE #{key}" }
|
|
108
184
|
end
|
|
185
|
+
|
|
186
|
+
def enforce_phi_ttl(ttl, phi: false)
|
|
187
|
+
return ttl unless phi
|
|
188
|
+
|
|
189
|
+
max = if defined?(Legion::Settings)
|
|
190
|
+
Legion::Settings.dig(:cache, :compliance, :phi_max_ttl) || 3600
|
|
191
|
+
else
|
|
192
|
+
3600
|
|
193
|
+
end
|
|
194
|
+
result = ttl.nil? ? max : [ttl, max].min
|
|
195
|
+
[result, 1].max
|
|
196
|
+
end
|
|
109
197
|
end
|
|
110
198
|
end
|
|
111
199
|
end
|
data/lib/legion/cache/pool.rb
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'concurrent'
|
|
4
|
+
require 'legion/logging/helper'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Cache
|
|
8
|
+
class Reconnector
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
11
|
+
DEFAULT_INITIAL_DELAY = 1
|
|
12
|
+
DEFAULT_MAX_DELAY = 60
|
|
13
|
+
|
|
14
|
+
def initialize(tier:, connect_block:, enabled_block:, settings_key: :cache)
|
|
15
|
+
@tier = tier
|
|
16
|
+
@connect_block = connect_block
|
|
17
|
+
@enabled_block = enabled_block
|
|
18
|
+
@settings_key = settings_key
|
|
19
|
+
@attempts = Concurrent::AtomicFixnum.new(0)
|
|
20
|
+
@thread = nil
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@stop_signal = Concurrent::AtomicBoolean.new(false)
|
|
23
|
+
@next_retry_at = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
return if running?
|
|
29
|
+
|
|
30
|
+
@stop_signal.make_false
|
|
31
|
+
@thread = Thread.new { reconnect_loop }
|
|
32
|
+
log.info "Legion::Cache::Reconnector[#{@tier}] started"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stop
|
|
37
|
+
thread_to_join = nil
|
|
38
|
+
@mutex.synchronize do
|
|
39
|
+
@stop_signal.make_true
|
|
40
|
+
thread_to_join = @thread
|
|
41
|
+
@thread = nil
|
|
42
|
+
end
|
|
43
|
+
thread_to_join&.join(5)
|
|
44
|
+
log.info "Legion::Cache::Reconnector[#{@tier}] stopped"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def running?
|
|
48
|
+
@thread&.alive? == true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def attempts
|
|
52
|
+
@attempts.value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
attr_reader :next_retry_at
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def reconnect_loop
|
|
60
|
+
delay = configured_initial_delay
|
|
61
|
+
|
|
62
|
+
until @stop_signal.true?
|
|
63
|
+
unless @enabled_block.call
|
|
64
|
+
sleep 1
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
@next_retry_at = Time.now + delay
|
|
70
|
+
sleep delay
|
|
71
|
+
return if @stop_signal.true?
|
|
72
|
+
|
|
73
|
+
@connect_block.call
|
|
74
|
+
count = @attempts.value
|
|
75
|
+
@attempts = Concurrent::AtomicFixnum.new(0)
|
|
76
|
+
@next_retry_at = nil
|
|
77
|
+
log.info "Legion::Cache::Reconnector[#{@tier}] reconnected after #{count} attempts"
|
|
78
|
+
return
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
@attempts.increment
|
|
81
|
+
handle_exception(e, level: :warn, handled: true,
|
|
82
|
+
operation: :"reconnector_#{@tier}",
|
|
83
|
+
attempt: @attempts.value, next_delay: delay)
|
|
84
|
+
delay = [delay * 2, configured_max_delay].min
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
handle_exception(e, level: :error, handled: true, operation: :"reconnector_#{@tier}_loop")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def configured_initial_delay
|
|
92
|
+
return DEFAULT_INITIAL_DELAY unless defined?(Legion::Settings)
|
|
93
|
+
|
|
94
|
+
Legion::Settings.dig(@settings_key, :reconnect, :initial_delay) || DEFAULT_INITIAL_DELAY
|
|
95
|
+
rescue StandardError
|
|
96
|
+
DEFAULT_INITIAL_DELAY
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def configured_max_delay
|
|
100
|
+
return DEFAULT_MAX_DELAY unless defined?(Legion::Settings)
|
|
101
|
+
|
|
102
|
+
Legion::Settings.dig(@settings_key, :reconnect, :max_delay) || DEFAULT_MAX_DELAY
|
|
103
|
+
rescue StandardError
|
|
104
|
+
DEFAULT_MAX_DELAY
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|