legion-cache 1.3.19 → 1.3.21

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.
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'connection_pool'
4
+ require 'legion/logging/helper'
4
5
 
5
6
  module Legion
6
7
  module Cache
7
8
  module Pool
8
- extend self # rubocop:disable Style/ModuleFunction
9
+ extend self
10
+ extend Legion::Logging::Helper
9
11
 
10
12
  def connected?
11
13
  @connected ||= false
@@ -28,10 +30,12 @@ module Legion
28
30
  end
29
31
 
30
32
  def close
31
- client.shutdown(&:close)
33
+ return unless @client
34
+
35
+ @client.shutdown(&:close)
32
36
  @client = nil
33
37
  @connected = false
34
- Legion::Logging.info "#{name} pool closed" if defined?(Legion::Logging)
38
+ log.info "#{pool_log_name} pool closed"
35
39
  end
36
40
 
37
41
  def restart(**opts)
@@ -42,7 +46,26 @@ module Legion
42
46
  client_hash[:timeout] = opts[:timeout] if opts.key? :timeout
43
47
  client(**client_hash)
44
48
  @connected = true
45
- Legion::Logging.info "#{name} pool restarted" if defined?(Legion::Logging)
49
+ log.info "#{pool_log_name} pool restarted"
50
+ end
51
+
52
+ private
53
+
54
+ def pool_log_name
55
+ if respond_to?(:name)
56
+ label = name.to_s
57
+ return label unless label.empty? || label.start_with?('#<')
58
+ end
59
+
60
+ segments = if instance_variable_defined?(:@component_logger) && @component_logger.respond_to?(:segments)
61
+ Array(@component_logger.segments)
62
+ elsif log.respond_to?(:segments)
63
+ Array(log.segments)
64
+ else
65
+ []
66
+ end
67
+
68
+ segments.empty? ? 'cache.pool' : segments.join('.')
46
69
  end
47
70
  end
48
71
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'openssl'
4
4
  require 'redis'
5
+ require 'legion/logging/helper'
5
6
  require 'legion/cache/pool'
6
7
  require 'legion/cache/settings'
7
8
 
@@ -10,14 +11,17 @@ module Legion
10
11
  module Redis
11
12
  include Legion::Cache::Pool
12
13
  extend self
14
+ extend Legion::Logging::Helper
13
15
 
14
16
  def client(pool_size: 20, timeout: 5, server: nil, servers: [], cluster: nil, replica: false, # rubocop:disable Metrics/ParameterLists
15
- fixed_hostname: nil, username: nil, password: nil, db: nil, reconnect_attempts: 1, **)
17
+ logger: nil,
18
+ fixed_hostname: nil, username: nil, password: nil, db: nil, reconnect_attempts: [0, 0.5, 1], **)
16
19
  return @client unless @client.nil?
17
20
 
18
21
  @pool_size = pool_size
19
22
  @timeout = timeout
20
23
  @cluster_mode = Array(cluster).compact.any?
24
+ @component_logger = logger || log
21
25
 
22
26
  @client = ConnectionPool.new(size: pool_size, timeout: timeout) do
23
27
  build_redis_client(server: server, servers: servers, cluster: cluster,
@@ -26,15 +30,20 @@ module Legion
26
30
  reconnect_attempts: reconnect_attempts)
27
31
  end
28
32
  @connected = true
29
- Legion::Logging.info "Redis connected to #{resolved_redis_address(server: server, servers: servers, cluster: cluster)}" if defined?(Legion::Logging)
33
+ cache_logger.info "Redis connected to #{resolved_redis_address(server: server, servers: servers, cluster: cluster)}"
30
34
  @client
35
+ rescue StandardError => e
36
+ handle_exception(e, level: :error, handled: false, operation: :redis_client,
37
+ server: server, servers: Array(servers), cluster_nodes: Array(cluster))
38
+ @connected = false
39
+ raise
31
40
  end
32
41
 
33
42
  def build_redis_client(server: nil, servers: [], cluster: nil, replica: false, fixed_hostname: nil, # rubocop:disable Metrics/ParameterLists
34
- username: nil, password: nil, db: nil, reconnect_attempts: 1)
43
+ username: nil, password: nil, db: nil, reconnect_attempts: [0, 0.5, 1])
35
44
  nodes = Array(cluster).compact
36
45
  if nodes.any?
37
- opts = { cluster: nodes, reconnect_attempts: reconnect_attempts }
46
+ opts = { cluster: nodes, reconnect_attempts: reconnect_attempts, timeout: @timeout }
38
47
  opts[:replica] = true if replica
39
48
  opts[:fixed_hostname] = fixed_hostname unless fixed_hostname.nil?
40
49
  opts[:username] = username unless username.nil?
@@ -44,8 +53,9 @@ module Legion
44
53
  resolved = Legion::Cache::Settings.resolve_servers(
45
54
  driver: 'redis', server: server, servers: servers
46
55
  )
47
- host, port = resolved.first.split(':')
48
- redis_opts = { host: host, port: port.to_i, reconnect_attempts: reconnect_attempts }
56
+ host, port = Legion::Cache::Settings.parse_server_address(resolved.first, default_port: 6379)
57
+ redis_opts = { host: host, port: port.to_i, reconnect_attempts: reconnect_attempts,
58
+ timeout: @timeout }
49
59
  redis_opts[:username] = username unless username.nil?
50
60
  redis_opts[:password] = password unless password.nil?
51
61
  redis_opts[:db] = db unless db.nil?
@@ -60,31 +70,39 @@ module Legion
60
70
 
61
71
  def get(key)
62
72
  result = client.with { |conn| conn.get(key) }
63
- Legion::Logging.debug "[cache] GET #{key} hit=#{!result.nil?}" if defined?(Legion::Logging)
73
+ cache_logger.debug "[cache] GET #{key} hit=#{!result.nil?}"
64
74
  result
65
75
  rescue ::Redis::BaseError => e
66
- log_cluster_error(e)
76
+ log_cluster_error('redis_get', e, key: key)
67
77
  raise
68
78
  end
69
- alias fetch get
79
+
80
+ def fetch(key, ttl = nil)
81
+ result = get(key)
82
+ return result unless result.nil? && block_given?
83
+
84
+ result = yield
85
+ set(key, result, ttl)
86
+ result
87
+ end
70
88
 
71
89
  def set(key, value, ttl = nil)
72
90
  args = {}
73
91
  args[:ex] = ttl unless ttl.nil?
74
92
  result = client.with { |conn| conn.set(key, value, **args) == 'OK' }
75
- Legion::Logging.debug "[cache] SET #{key} ttl=#{ttl.inspect} success=#{result}" if defined?(Legion::Logging)
93
+ cache_logger.debug "[cache] SET #{key} ttl=#{ttl.inspect} success=#{result}"
76
94
  result
77
95
  rescue ::Redis::BaseError => e
78
- log_cluster_error(e)
96
+ log_cluster_error('redis_set', e, key: key, ttl: ttl)
79
97
  raise
80
98
  end
81
99
 
82
100
  def delete(key)
83
101
  result = client.with { |conn| conn.del(key) == 1 }
84
- Legion::Logging.debug "[cache] DELETE #{key} success=#{result}" if defined?(Legion::Logging)
102
+ cache_logger.debug "[cache] DELETE #{key} success=#{result}"
85
103
  result
86
104
  rescue ::Redis::BaseError => e
87
- log_cluster_error(e)
105
+ log_cluster_error('redis_delete', e, key: key)
88
106
  raise
89
107
  end
90
108
 
@@ -96,10 +114,10 @@ module Legion
96
114
  conn.flushdb == 'OK'
97
115
  end
98
116
  end
99
- Legion::Logging.debug '[cache] FLUSH completed' if defined?(Legion::Logging)
117
+ cache_logger.debug '[cache] FLUSH completed'
100
118
  result
101
119
  rescue ::Redis::BaseError => e
102
- log_cluster_error(e)
120
+ log_cluster_error('redis_flush', e)
103
121
  raise
104
122
  end
105
123
 
@@ -115,10 +133,10 @@ module Legion
115
133
  keys.zip(values).to_h
116
134
  end
117
135
  end
118
- Legion::Logging.debug "[cache] MGET keys=#{keys.size}" if defined?(Legion::Logging)
136
+ cache_logger.debug "[cache] MGET keys=#{keys.size}"
119
137
  result
120
138
  rescue ::Redis::BaseError => e
121
- log_cluster_error(e)
139
+ log_cluster_error('redis_mget', e, key_count: keys.size)
122
140
  raise
123
141
  end
124
142
 
@@ -132,15 +150,19 @@ module Legion
132
150
  conn.mset(*hash.flatten) == 'OK'
133
151
  end
134
152
  end
135
- Legion::Logging.debug "[cache] MSET keys=#{hash.size}" if defined?(Legion::Logging)
153
+ cache_logger.debug "[cache] MSET keys=#{hash.size}"
136
154
  result
137
155
  rescue ::Redis::BaseError => e
138
- log_cluster_error(e)
156
+ log_cluster_error('redis_mset', e, key_count: hash.size)
139
157
  raise
140
158
  end
141
159
 
142
160
  private
143
161
 
162
+ def cache_logger
163
+ @component_logger || log
164
+ end
165
+
144
166
  def cluster_mget(conn, keys)
145
167
  groups = group_keys_by_slot(keys)
146
168
  result = {}
@@ -164,14 +186,14 @@ module Legion
164
186
  node_info = conn.cluster('nodes')
165
187
  primaries = node_info.lines.select { |l| l.include?('master') }.map { |l| l.split[1].split('@').first }
166
188
  primaries.each do |addr|
167
- host, port = addr.split(':')
189
+ host, port = Legion::Cache::Settings.parse_server_address(addr, default_port: 6379)
168
190
  node = ::Redis.new(host: host, port: port.to_i)
169
191
  node.flushdb
170
192
  node.close
171
193
  end
172
194
  true
173
195
  rescue StandardError => e
174
- Legion::Logging.warn("Redis#cluster_flush cluster node flush failed, falling back to single flushdb: #{e.message}") if defined?(Legion::Logging)
196
+ handle_exception(e, level: :warn, handled: true, operation: :cluster_flush, fallback: :single_flushdb)
175
197
  conn.flushdb == 'OK'
176
198
  end
177
199
 
@@ -189,14 +211,12 @@ module Legion
189
211
 
190
212
  Legion::Cache::Settings.resolve_servers(driver: 'redis', server: server, servers: Array(servers)).first
191
213
  rescue StandardError => e
192
- Legion::Logging.debug("Redis#resolved_redis_address failed: #{e.message}") if defined?(Legion::Logging)
214
+ handle_exception(e, level: :warn, handled: true, operation: :resolved_redis_address)
193
215
  'unknown'
194
216
  end
195
217
 
196
- def log_cluster_error(error)
197
- return unless defined?(Legion::Logging)
198
-
199
- Legion::Logging.warn "Redis cluster error: #{error.class} — #{error.message}"
218
+ def log_cluster_error(operation, error, **)
219
+ handle_exception(error, level: :warn, handled: false, operation: operation, **)
200
220
  end
201
221
 
202
222
  def redis_tls_options(port:)
@@ -218,7 +238,7 @@ module Legion
218
238
 
219
239
  Legion::Settings[:cache][:tls] || {}
220
240
  rescue StandardError => e
221
- Legion::Logging.debug("Redis#cache_tls_settings failed: #{e.message}") if defined?(Legion::Logging)
241
+ handle_exception(e, level: :warn, handled: true, operation: :cache_tls_settings)
222
242
  {}
223
243
  end
224
244
  end
@@ -1,17 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'legion/logging/helper'
4
+
3
5
  module Legion
4
6
  module Cache
5
7
  module RedisHash
8
+ extend Legion::Logging::Helper
9
+
6
10
  module_function
7
11
 
8
12
  # Returns true when the Redis driver is loaded and the connection pool is live.
9
13
  def redis_available?
10
14
  pool = Legion::Cache.instance_variable_get(:@client)
11
15
  return false if pool.nil?
16
+ return false unless Legion::Cache.respond_to?(:driver_name) && Legion::Cache.driver_name == 'redis'
12
17
 
13
18
  Legion::Cache.connected?
14
- rescue StandardError
19
+ rescue StandardError => e
20
+ handle_exception(e, level: :warn, handled: true, operation: :redis_hash_available)
15
21
  false
16
22
  end
17
23
 
@@ -24,6 +30,7 @@ module Legion
24
30
  flat = hash.flat_map { |k, v| [k.to_s, v.to_s] }
25
31
  conn.hset(key, *flat)
26
32
  end
33
+ log.debug "[cache:redis_hash] HSET #{key} fields=#{hash.size}"
27
34
  true
28
35
  rescue StandardError => e
29
36
  log_redis_error('hset', e)
@@ -34,9 +41,11 @@ module Legion
34
41
  def hgetall(key)
35
42
  return nil unless redis_available?
36
43
 
37
- Legion::Cache.instance_variable_get(:@client).with do |conn|
44
+ result = Legion::Cache.instance_variable_get(:@client).with do |conn|
38
45
  conn.hgetall(key)
39
46
  end
47
+ log.debug "[cache:redis_hash] HGETALL #{key} fields=#{result.size}"
48
+ result
40
49
  rescue StandardError => e
41
50
  log_redis_error('hgetall', e)
42
51
  nil
@@ -46,9 +55,11 @@ module Legion
46
55
  def hdel(key, *fields)
47
56
  return 0 unless redis_available?
48
57
 
49
- Legion::Cache.instance_variable_get(:@client).with do |conn|
58
+ result = Legion::Cache.instance_variable_get(:@client).with do |conn|
50
59
  conn.hdel(key, *fields)
51
60
  end
61
+ log.debug "[cache:redis_hash] HDEL #{key} fields=#{fields.size} removed=#{result}"
62
+ result
52
63
  rescue StandardError => e
53
64
  log_redis_error('hdel', e)
54
65
  0
@@ -61,6 +72,7 @@ module Legion
61
72
  Legion::Cache.instance_variable_get(:@client).with do |conn|
62
73
  conn.zadd(key, score.to_f, member.to_s)
63
74
  end
75
+ log.debug "[cache:redis_hash] ZADD #{key} member=#{member}"
64
76
  true
65
77
  rescue StandardError => e
66
78
  log_redis_error('zadd', e)
@@ -75,9 +87,11 @@ module Legion
75
87
  opts = {}
76
88
  opts[:limit] = limit if limit
77
89
 
78
- Legion::Cache.instance_variable_get(:@client).with do |conn|
90
+ result = Legion::Cache.instance_variable_get(:@client).with do |conn|
79
91
  conn.zrangebyscore(key, min, max, **opts)
80
92
  end
93
+ log.debug "[cache:redis_hash] ZRANGEBYSCORE #{key} results=#{result.size}"
94
+ result
81
95
  rescue StandardError => e
82
96
  log_redis_error('zrangebyscore', e)
83
97
  []
@@ -90,6 +104,7 @@ module Legion
90
104
  Legion::Cache.instance_variable_get(:@client).with do |conn|
91
105
  conn.zrem(key, member.to_s)
92
106
  end
107
+ log.debug "[cache:redis_hash] ZREM #{key} member=#{member}"
93
108
  true
94
109
  rescue StandardError => e
95
110
  log_redis_error('zrem', e)
@@ -100,18 +115,18 @@ module Legion
100
115
  def expire(key, seconds)
101
116
  return false unless redis_available?
102
117
 
103
- Legion::Cache.instance_variable_get(:@client).with do |conn|
118
+ result = Legion::Cache.instance_variable_get(:@client).with do |conn|
104
119
  conn.expire(key, seconds.to_i) == 1
105
120
  end
121
+ log.debug "[cache:redis_hash] EXPIRE #{key} seconds=#{seconds} success=#{result}"
122
+ result
106
123
  rescue StandardError => e
107
124
  log_redis_error('expire', e)
108
125
  false
109
126
  end
110
127
 
111
128
  def log_redis_error(method, error)
112
- return unless defined?(Legion::Logging)
113
-
114
- Legion::Logging.warn "[cache:redis_hash] #{method} failed: #{error.class} — #{error.message}"
129
+ handle_exception(error, level: :warn, handled: true, operation: method)
115
130
  end
116
131
  end
117
132
  end
@@ -1,16 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require 'legion/settings'
5
- rescue StandardError => e
6
- warn "legion-cache: failed to require legion/settings: #{e.message}"
7
- end
3
+ require 'ipaddr'
4
+ require 'legion/logging/helper'
8
5
 
9
6
  module Legion
10
7
  module Cache
11
8
  module Settings
12
- Legion::Settings.merge_settings(:cache, default) if Legion::Settings.method_defined? :merge_settings
13
- Legion::Settings.merge_settings(:cache_local, local) if Legion::Settings.method_defined? :merge_settings
9
+ extend Legion::Logging::Helper
10
+
11
+ begin
12
+ require 'legion/settings'
13
+ rescue StandardError => e
14
+ handle_exception(e,
15
+ level: :error,
16
+ handled: true,
17
+ operation: :cache_settings_require_legion_settings)
18
+ end
19
+
14
20
  def self.default
15
21
  {
16
22
  driver: driver,
@@ -33,7 +39,7 @@ module Legion
33
39
  username: nil,
34
40
  password: nil,
35
41
  db: nil,
36
- reconnect_attempts: 1
42
+ reconnect_attempts: [0, 0.5, 1].freeze
37
43
  }
38
44
  end
39
45
 
@@ -56,7 +62,7 @@ module Legion
56
62
  username: nil,
57
63
  password: nil,
58
64
  db: nil,
59
- reconnect_attempts: 1
65
+ reconnect_attempts: [0, 0.25, 0.5].freeze
60
66
  }
61
67
  end
62
68
 
@@ -69,8 +75,34 @@ module Legion
69
75
  all = Array(servers) + Array(server)
70
76
  all = ["127.0.0.1:#{port}"] if all.empty?
71
77
 
72
- all.map! { |s| s.include?(':') ? s : "#{s}:#{port}" }
73
- all.uniq
78
+ all.map! { |s| normalize_server(s, port: port) }
79
+ resolved = all.uniq
80
+ log.debug "Legion::Cache::Settings resolved driver=#{gem_driver} servers=#{resolved.join(', ')}"
81
+ resolved
82
+ end
83
+
84
+ def self.parse_server_address(server, default_port:)
85
+ raw = server.to_s.strip
86
+ return ['127.0.0.1', default_port] if raw.empty?
87
+
88
+ bracketed = raw.match(/\A\[(?<host>[^\]]+)\](?::(?<port>\d+))?\z/)
89
+ return [bracketed[:host], (bracketed[:port] || default_port).to_i] if bracketed
90
+
91
+ return [raw, default_port] if ipv6_literal?(raw)
92
+
93
+ host, explicit_port = raw.split(':', 2)
94
+ if explicit_port&.match?(/\A\d+\z/)
95
+ [host, explicit_port.to_i]
96
+ else
97
+ [raw, default_port]
98
+ end
99
+ end
100
+
101
+ def self.register_defaults!
102
+ return unless defined?(Legion::Settings) && Legion::Settings.respond_to?(:merge_settings)
103
+
104
+ Legion::Settings.merge_settings(:cache, default)
105
+ Legion::Settings.merge_settings(:cache_local, local)
74
106
  end
75
107
 
76
108
  def self.normalize_driver(name)
@@ -84,13 +116,36 @@ module Legion
84
116
  def self.driver(prefer = 'dalli')
85
117
  secondary = prefer == 'dalli' ? 'redis' : 'dalli'
86
118
  if Gem::Specification.find_all_by_name(prefer).any?
119
+ log.debug "Legion::Cache::Settings selected driver=#{prefer}"
87
120
  prefer
88
121
  elsif Gem::Specification.find_all_by_name(secondary).any?
122
+ log.info "Legion::Cache::Settings falling back driver=#{secondary} preferred=#{prefer}"
89
123
  secondary
90
124
  else
91
- raise NameError('Legion::Cache.driver is nil')
125
+ error = NameError.new('Legion::Cache.driver is nil')
126
+ handle_exception(error, level: :error, handled: false, operation: :cache_settings_driver, preferred: prefer)
127
+ raise error
92
128
  end
93
129
  end
130
+
131
+ def self.normalize_server(server, port:)
132
+ host, resolved_port = parse_server_address(server, default_port: port)
133
+ format_server(host, resolved_port)
134
+ end
135
+
136
+ def self.format_server(host, port)
137
+ return "[#{host}]:#{port}" if ipv6_literal?(host)
138
+
139
+ "#{host}:#{port}"
140
+ end
141
+
142
+ def self.ipv6_literal?(value)
143
+ IPAddr.new(value).ipv6?
144
+ rescue IPAddr::InvalidAddressError
145
+ false
146
+ end
147
+
148
+ register_defaults!
94
149
  end
95
150
  end
96
151
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Cache
5
- VERSION = '1.3.19'
5
+ VERSION = '1.3.21'
6
6
  end
7
7
  end