legion-cache 1.3.22 → 1.4.2

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.
@@ -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, logger: nil, **opts)
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 = opts.key?(:pool_size) ? opts[:pool_size] : settings[:pool_size] || 10
23
- @timeout = opts.key?(:timeout) ? opts[:timeout] : settings[:timeout] || 5
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 = shared_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
- @client = ConnectionPool.new(size: pool_size, timeout: timeout) do
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
- cache_logger.info "Memcached connected to #{resolved.join(', ')}"
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
- cache_logger.debug "[cache] GET #{key} hit=#{!result.nil?}"
57
+ log.debug { "[cache] GET #{key} hit=#{!result.nil?}" }
54
58
  result
55
59
  rescue StandardError => e
56
- handle_exception(e, level: :warn, handled: false, operation: :memcached_get, key: key)
57
- raise
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 = nil, &)
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,57 @@ module Legion
65
69
  conn.fetch(key, ttl)
66
70
  end
67
71
  end
68
- cache_logger.debug "[cache] FETCH #{key} hit=#{!result.nil?}"
72
+ log.debug { "[cache] FETCH #{key} hit=#{!result.nil?}" }
73
+ result
74
+ rescue StandardError => e
75
+ handle_exception(e, level: :warn, handled: true, operation: :memcached_fetch, key: key, ttl: ttl)
76
+ nil
77
+ end
78
+
79
+ def set_nx(key, value, ttl: nil)
80
+ effective_ttl = ttl || default_ttl
81
+ result = client.with { |conn| conn.add(key, value, effective_ttl) == true }
82
+ log.debug { "[cache] SET_NX #{key} ttl=#{effective_ttl.inspect} result=#{result}" }
69
83
  result
70
84
  rescue StandardError => e
71
- handle_exception(e, level: :warn, handled: false, operation: :memcached_fetch, key: key, ttl: ttl)
85
+ handle_exception(e, level: :error, handled: false, operation: :memcached_set_nx, key: key, ttl: effective_ttl)
72
86
  raise
73
87
  end
74
88
 
75
- def set(key, value, ttl = 180)
76
- result = client.with { |conn| conn.set(key, value, ttl).positive? }
77
- cache_logger.debug "[cache] SET #{key} ttl=#{ttl} success=#{result} value_class=#{value.class}"
89
+ def set(key, value, ttl: nil, **)
90
+ set_sync(key, value, ttl: ttl, **)
91
+ end
92
+
93
+ def set_sync(key, value, ttl: nil, **)
94
+ effective_ttl = ttl || default_ttl
95
+ result = client.with { |conn| conn.set(key, value, effective_ttl).positive? }
96
+ log.debug { "[cache] SET #{key} ttl=#{effective_ttl} success=#{result} value_class=#{value.class}" }
78
97
  result
79
98
  rescue StandardError => e
80
- handle_exception(e, level: :warn, handled: false, operation: :memcached_set, key: key, ttl: ttl)
99
+ handle_exception(e, level: :error, handled: false, operation: :memcached_set_sync, key: key, ttl: effective_ttl)
81
100
  raise
82
101
  end
83
102
 
84
- def delete(key)
103
+ def delete(key, **)
104
+ delete_sync(key)
105
+ end
106
+
107
+ def delete_sync(key)
85
108
  result = client.with { |conn| conn.delete(key) == true }
86
- cache_logger.debug "[cache] DELETE #{key} success=#{result}"
109
+ log.debug { "[cache] DELETE #{key} success=#{result}" }
87
110
  result
88
111
  rescue StandardError => e
89
- handle_exception(e, level: :warn, handled: false, operation: :memcached_delete, key: key)
112
+ handle_exception(e, level: :error, handled: false, operation: :memcached_delete_sync, key: key)
90
113
  raise
91
114
  end
92
115
 
93
- def flush(delay = 0)
94
- result = client.with { |conn| conn.flush(delay).first }
95
- cache_logger.debug '[cache] FLUSH completed'
116
+ def flush
117
+ result = client.with { |conn| conn.flush.first }
118
+ log.debug { '[cache] FLUSH completed' }
96
119
  result
97
120
  rescue StandardError => e
98
- handle_exception(e, level: :warn, handled: false, operation: :memcached_flush, delay: delay)
99
- raise
121
+ handle_exception(e, level: :warn, handled: true, operation: :memcached_flush)
122
+ nil
100
123
  end
101
124
 
102
125
  def mget(*keys)
@@ -104,36 +127,36 @@ module Legion
104
127
  return {} if keys.empty?
105
128
 
106
129
  result = client.with { |conn| conn.get_multi(*keys) }
107
- cache_logger.debug "[cache] MGET keys=#{keys.size} hits=#{result.size}"
130
+ log.debug { "[cache] MGET keys=#{keys.size} hits=#{result.size}" }
108
131
  result
109
132
  rescue StandardError => e
110
- handle_exception(e, level: :warn, handled: false, operation: :memcached_mget, key_count: keys.size)
111
- raise
133
+ handle_exception(e, level: :warn, handled: true, operation: :memcached_mget, key_count: keys.size)
134
+ {}
112
135
  end
113
136
 
114
- def mset(hash)
137
+ def mset(hash, ttl: nil, **)
138
+ mset_sync(hash, ttl: ttl)
139
+ end
140
+
141
+ def mset_sync(hash, ttl: nil, **) # rubocop:disable Lint/UnusedMethodArgument
115
142
  return true if hash.empty?
116
143
 
117
144
  client.with { |conn| conn.set_multi(hash) }
118
- cache_logger.debug "[cache] MSET keys=#{hash.size}"
145
+ log.debug { "[cache] MSET keys=#{hash.size}" }
119
146
  true
120
147
  rescue StandardError => e
121
- handle_exception(e, level: :warn, handled: false, operation: :memcached_mset, key_count: hash.size)
148
+ handle_exception(e, level: :error, handled: false, operation: :memcached_mset_sync, key_count: hash.size)
122
149
  raise
123
150
  end
124
151
 
125
152
  private
126
153
 
127
- def cache_logger
128
- @component_logger || log
129
- end
154
+ def default_ttl
155
+ return 3600 unless defined?(Legion::Settings)
130
156
 
131
- def shared_dalli_logger
132
- if defined?(Legion::Cache) && Legion::Cache.respond_to?(:log)
133
- Legion::Cache.log
134
- else
135
- cache_logger
136
- end
157
+ Legion::Settings.dig(:cache, :default_ttl) || 3600
158
+ rescue StandardError
159
+ 3600
137
160
  end
138
161
 
139
162
  def memcached_tls_context(port:)
@@ -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 = true
18
+ @connected.make_true
18
19
  log.info 'Legion::Cache::Memory connected'
19
- @connected
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,36 @@ 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
35
52
  end
36
53
 
37
- def set(key, value, ttl = nil)
54
+ def set(key, value, ttl: nil, async: true, phi: false) # rubocop:disable Lint/UnusedMethodArgument
55
+ set_sync(key, value, ttl: ttl, phi: phi)
56
+ end
57
+
58
+ def set_nx(key, value, ttl: nil)
59
+ @mutex.synchronize do
60
+ expire_if_needed(key)
61
+ return false if @store.key?(key)
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
75
+ end
76
+
77
+ def set_sync(key, value, ttl: nil, phi: false)
78
+ ttl = enforce_phi_ttl(ttl, phi: phi) if phi
38
79
  @mutex.synchronize do
39
80
  @store[key] = value
40
81
  if ttl&.positive?
@@ -43,55 +84,109 @@ module Legion
43
84
  @expiry.delete(key)
44
85
  end
45
86
  log.debug { "[cache:memory] SET #{key} ttl=#{ttl.inspect}" }
46
- value
87
+ true
47
88
  end
48
89
  end
49
90
 
50
- def fetch(key, ttl = nil)
91
+ def fetch(key, ttl: nil, &block)
51
92
  val = get(key)
52
93
  return val unless val.nil?
53
94
 
54
95
  log.debug { "[cache:memory] FETCH #{key} miss=true" }
55
- val = yield if block_given?
56
- set(key, val, ttl)
96
+ val = block&.call
97
+ set(key, val, ttl: ttl)
57
98
  val
99
+ rescue StandardError => e
100
+ handle_exception(e, level: :warn, handled: true, operation: :memory_fetch)
101
+ nil
102
+ end
103
+
104
+ def delete(key, async: true) # rubocop:disable Lint/UnusedMethodArgument
105
+ delete_sync(key)
58
106
  end
59
107
 
60
- def delete(key)
108
+ def delete_sync(key)
61
109
  @mutex.synchronize do
62
110
  removed = @store.delete(key)
63
111
  @expiry.delete(key)
64
112
  log.debug { "[cache:memory] DELETE #{key} success=#{!removed.nil?}" }
65
- removed
113
+ !removed.nil?
66
114
  end
67
115
  end
68
116
 
69
- def flush(_delay = 0)
70
- result = @mutex.synchronize do
117
+ def mget(*keys)
118
+ keys = keys.flatten
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
125
+ rescue StandardError => e
126
+ handle_exception(e, level: :warn, handled: true, operation: :memory_mget)
127
+ {}
128
+ end
129
+
130
+ def mset(hash, ttl: nil, async: true)
131
+ return true if hash.empty?
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
138
+ end
139
+
140
+ def mset_sync(hash, ttl: nil, phi: false)
141
+ return true if hash.empty?
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
155
+ end
156
+
157
+ def flush
158
+ @mutex.synchronize do
71
159
  @store.clear
72
160
  @expiry.clear
73
161
  end
74
162
  log.info 'Legion::Cache::Memory flushed'
75
- result
163
+ true
164
+ rescue StandardError => e
165
+ handle_exception(e, level: :warn, handled: true, operation: :memory_flush)
166
+ false
76
167
  end
77
168
 
78
169
  def close = nil
79
170
 
80
171
  def shutdown
81
172
  flush
82
- @connected = false
173
+ @connected.make_false
83
174
  log.info 'Legion::Cache::Memory shut down'
84
- @connected
175
+ false
176
+ rescue StandardError => e
177
+ @connected.make_false
178
+ handle_exception(e, level: :warn, handled: true, operation: :memory_shutdown)
179
+ false
85
180
  end
86
181
 
87
182
  def reset!
88
- result = @mutex.synchronize do
183
+ @mutex.synchronize do
184
+ @connected.make_false
89
185
  @store.clear
90
186
  @expiry.clear
91
- @connected = false
92
187
  end
93
188
  log.info 'Legion::Cache::Memory state reset'
94
- result
189
+ false
95
190
  end
96
191
 
97
192
  def size = 1
@@ -106,6 +201,18 @@ module Legion
106
201
  @expiry.delete(key)
107
202
  log.debug { "[cache:memory] EXPIRE #{key}" }
108
203
  end
204
+
205
+ def enforce_phi_ttl(ttl, phi: false)
206
+ return ttl unless phi
207
+
208
+ max = if defined?(Legion::Settings)
209
+ Legion::Settings.dig(:cache, :compliance, :phi_max_ttl) || 3600
210
+ else
211
+ 3600
212
+ end
213
+ result = ttl.nil? ? max : [ttl, max].min
214
+ [result, 1].max
215
+ end
109
216
  end
110
217
  end
111
218
  end
@@ -10,7 +10,10 @@ module Legion
10
10
  extend Legion::Logging::Helper
11
11
 
12
12
  def connected?
13
- @connected ||= false
13
+ return false unless defined?(@connected)
14
+ return @connected.true? if @connected.respond_to?(:true?)
15
+
16
+ @connected == true
14
17
  end
15
18
 
16
19
  def size
@@ -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