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.
@@ -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,47 @@ 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?}" }
69
73
  result
70
74
  rescue StandardError => e
71
- handle_exception(e, level: :warn, handled: false, operation: :memcached_fetch, key: key, ttl: ttl)
72
- raise
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 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}"
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: :warn, handled: false, operation: :memcached_set, key: key, ttl: ttl)
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
- cache_logger.debug "[cache] DELETE #{key} success=#{result}"
99
+ log.debug { "[cache] DELETE #{key} success=#{result}" }
87
100
  result
88
101
  rescue StandardError => e
89
- handle_exception(e, level: :warn, handled: false, operation: :memcached_delete, key: key)
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(delay = 0)
94
- result = client.with { |conn| conn.flush(delay).first }
95
- cache_logger.debug '[cache] FLUSH completed'
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: false, operation: :memcached_flush, delay: delay)
99
- raise
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
- cache_logger.debug "[cache] MGET keys=#{keys.size} hits=#{result.size}"
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: false, operation: :memcached_mget, key_count: keys.size)
111
- raise
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 mset(hash)
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
- cache_logger.debug "[cache] MSET keys=#{hash.size}"
135
+ log.debug { "[cache] MSET keys=#{hash.size}" }
119
136
  true
120
137
  rescue StandardError => e
121
- handle_exception(e, level: :warn, handled: false, operation: :memcached_mset, key_count: hash.size)
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 cache_logger
128
- @component_logger || log
129
- end
144
+ def default_ttl
145
+ return 3600 unless defined?(Legion::Settings)
130
146
 
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
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:)
@@ -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,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 set(key, value, ttl = nil)
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
- value
68
+ true
47
69
  end
48
70
  end
49
71
 
50
- def fetch(key, ttl = nil)
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 = yield if block_given?
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 flush(_delay = 0)
70
- result = @mutex.synchronize do
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
- result
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 = false
154
+ @connected.make_false
83
155
  log.info 'Legion::Cache::Memory shut down'
84
- @connected
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
- result = @mutex.synchronize do
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
- result
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
@@ -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