legion-cache 1.3.20 → 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
17
+ logger: nil,
15
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,8 +30,13 @@ 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
@@ -44,7 +53,7 @@ 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(':')
56
+ host, port = Legion::Cache::Settings.parse_server_address(resolved.first, default_port: 6379)
48
57
  redis_opts = { host: host, port: port.to_i, reconnect_attempts: reconnect_attempts,
49
58
  timeout: @timeout }
50
59
  redis_opts[:username] = username unless username.nil?
@@ -61,31 +70,39 @@ module Legion
61
70
 
62
71
  def get(key)
63
72
  result = client.with { |conn| conn.get(key) }
64
- Legion::Logging.debug "[cache] GET #{key} hit=#{!result.nil?}" if defined?(Legion::Logging)
73
+ cache_logger.debug "[cache] GET #{key} hit=#{!result.nil?}"
65
74
  result
66
75
  rescue ::Redis::BaseError => e
67
- log_cluster_error(e)
76
+ log_cluster_error('redis_get', e, key: key)
68
77
  raise
69
78
  end
70
- 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
71
88
 
72
89
  def set(key, value, ttl = nil)
73
90
  args = {}
74
91
  args[:ex] = ttl unless ttl.nil?
75
92
  result = client.with { |conn| conn.set(key, value, **args) == 'OK' }
76
- 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}"
77
94
  result
78
95
  rescue ::Redis::BaseError => e
79
- log_cluster_error(e)
96
+ log_cluster_error('redis_set', e, key: key, ttl: ttl)
80
97
  raise
81
98
  end
82
99
 
83
100
  def delete(key)
84
101
  result = client.with { |conn| conn.del(key) == 1 }
85
- Legion::Logging.debug "[cache] DELETE #{key} success=#{result}" if defined?(Legion::Logging)
102
+ cache_logger.debug "[cache] DELETE #{key} success=#{result}"
86
103
  result
87
104
  rescue ::Redis::BaseError => e
88
- log_cluster_error(e)
105
+ log_cluster_error('redis_delete', e, key: key)
89
106
  raise
90
107
  end
91
108
 
@@ -97,10 +114,10 @@ module Legion
97
114
  conn.flushdb == 'OK'
98
115
  end
99
116
  end
100
- Legion::Logging.debug '[cache] FLUSH completed' if defined?(Legion::Logging)
117
+ cache_logger.debug '[cache] FLUSH completed'
101
118
  result
102
119
  rescue ::Redis::BaseError => e
103
- log_cluster_error(e)
120
+ log_cluster_error('redis_flush', e)
104
121
  raise
105
122
  end
106
123
 
@@ -116,10 +133,10 @@ module Legion
116
133
  keys.zip(values).to_h
117
134
  end
118
135
  end
119
- Legion::Logging.debug "[cache] MGET keys=#{keys.size}" if defined?(Legion::Logging)
136
+ cache_logger.debug "[cache] MGET keys=#{keys.size}"
120
137
  result
121
138
  rescue ::Redis::BaseError => e
122
- log_cluster_error(e)
139
+ log_cluster_error('redis_mget', e, key_count: keys.size)
123
140
  raise
124
141
  end
125
142
 
@@ -133,15 +150,19 @@ module Legion
133
150
  conn.mset(*hash.flatten) == 'OK'
134
151
  end
135
152
  end
136
- Legion::Logging.debug "[cache] MSET keys=#{hash.size}" if defined?(Legion::Logging)
153
+ cache_logger.debug "[cache] MSET keys=#{hash.size}"
137
154
  result
138
155
  rescue ::Redis::BaseError => e
139
- log_cluster_error(e)
156
+ log_cluster_error('redis_mset', e, key_count: hash.size)
140
157
  raise
141
158
  end
142
159
 
143
160
  private
144
161
 
162
+ def cache_logger
163
+ @component_logger || log
164
+ end
165
+
145
166
  def cluster_mget(conn, keys)
146
167
  groups = group_keys_by_slot(keys)
147
168
  result = {}
@@ -165,14 +186,14 @@ module Legion
165
186
  node_info = conn.cluster('nodes')
166
187
  primaries = node_info.lines.select { |l| l.include?('master') }.map { |l| l.split[1].split('@').first }
167
188
  primaries.each do |addr|
168
- host, port = addr.split(':')
189
+ host, port = Legion::Cache::Settings.parse_server_address(addr, default_port: 6379)
169
190
  node = ::Redis.new(host: host, port: port.to_i)
170
191
  node.flushdb
171
192
  node.close
172
193
  end
173
194
  true
174
195
  rescue StandardError => e
175
- 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)
176
197
  conn.flushdb == 'OK'
177
198
  end
178
199
 
@@ -190,14 +211,12 @@ module Legion
190
211
 
191
212
  Legion::Cache::Settings.resolve_servers(driver: 'redis', server: server, servers: Array(servers)).first
192
213
  rescue StandardError => e
193
- 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)
194
215
  'unknown'
195
216
  end
196
217
 
197
- def log_cluster_error(error)
198
- return unless defined?(Legion::Logging)
199
-
200
- 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, **)
201
220
  end
202
221
 
203
222
  def redis_tls_options(port:)
@@ -219,7 +238,7 @@ module Legion
219
238
 
220
239
  Legion::Settings[:cache][:tls] || {}
221
240
  rescue StandardError => e
222
- 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)
223
242
  {}
224
243
  end
225
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,
@@ -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.20'
5
+ VERSION = '1.3.21'
6
6
  end
7
7
  end