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.
@@ -13,24 +13,37 @@ module Legion
13
13
  extend self
14
14
  extend Legion::Logging::Helper
15
15
 
16
- def client(pool_size: 20, timeout: 5, server: nil, servers: [], cluster: nil, replica: false, # rubocop:disable Metrics/ParameterLists
17
- logger: nil,
18
- fixed_hostname: nil, username: nil, password: nil, db: nil, reconnect_attempts: [0, 0.5, 1], **)
16
+ def client(server: nil, servers: [], pool_size: nil, timeout: nil, # rubocop:disable Metrics/ParameterLists
17
+ username: nil, password: nil, logger: nil, **opts)
19
18
  return @client unless @client.nil?
20
19
 
20
+ settings = defined?(Legion::Settings) ? Legion::Settings[:cache] : {}
21
+ pool_size ||= settings[:pool_size] || 10
22
+ timeout ||= settings[:timeout] || 5
23
+
24
+ cluster = opts.delete(:cluster)
25
+ replica = opts.delete(:replica) || false
26
+ fixed_hostname = opts.delete(:fixed_hostname)
27
+ db = opts.delete(:db)
28
+ reconnect_attempts = opts.delete(:reconnect_attempts) || [0, 0.5, 1]
29
+
21
30
  @pool_size = pool_size
22
31
  @timeout = timeout
23
32
  @cluster_mode = Array(cluster).compact.any?
24
33
  @component_logger = logger || log
25
34
 
26
- @client = ConnectionPool.new(size: pool_size, timeout: timeout) do
35
+ @connection_opts = { username: username, password: password, timeout: @timeout }.compact
36
+ @connection_opts.merge!(redis_tls_options(port: resolve_primary_port(server: server, servers: servers, cluster: cluster)))
37
+
38
+ checkout_timeout = opts[:pool_checkout_timeout] || settings[:pool_checkout_timeout] || @timeout
39
+ @client = ConnectionPool.new(size: pool_size, timeout: checkout_timeout) do
27
40
  build_redis_client(server: server, servers: servers, cluster: cluster,
28
41
  replica: replica, fixed_hostname: fixed_hostname,
29
42
  username: username, password: password, db: db,
30
43
  reconnect_attempts: reconnect_attempts)
31
44
  end
32
45
  @connected = true
33
- cache_logger.info "Redis connected to #{resolved_redis_address(server: server, servers: servers, cluster: cluster)}"
46
+ log.info "Redis connected to #{resolved_redis_address(server: server, servers: servers, cluster: cluster)}"
34
47
  @client
35
48
  rescue StandardError => e
36
49
  handle_exception(e, level: :error, handled: false, operation: :redis_client,
@@ -69,40 +82,51 @@ module Legion
69
82
  end
70
83
 
71
84
  def get(key)
72
- result = client.with { |conn| conn.get(key) }
73
- cache_logger.debug "[cache] GET #{key} hit=#{!result.nil?}"
85
+ raw = client.with { |conn| conn.get(key) }
86
+ result = deserialize_value(raw)
87
+ log.debug { "[cache] GET #{key} hit=#{!result.nil?}" }
74
88
  result
75
- rescue ::Redis::BaseError => e
76
- log_cluster_error('redis_get', e, key: key)
77
- raise
89
+ rescue StandardError => e
90
+ handle_exception(e, level: :warn, handled: true, operation: :redis_get, key: key)
91
+ nil
78
92
  end
79
93
 
80
- def fetch(key, ttl = nil)
94
+ def fetch(key, ttl: nil)
81
95
  result = get(key)
82
96
  return result unless result.nil? && block_given?
83
97
 
84
98
  result = yield
85
- set(key, result, ttl)
99
+ set(key, result, ttl: ttl)
86
100
  result
87
101
  end
88
102
 
89
- def set(key, value, ttl = nil)
103
+ def set(key, value, ttl: nil, **)
104
+ set_sync(key, value, ttl: ttl, **)
105
+ end
106
+
107
+ def set_sync(key, value, ttl: nil, **)
108
+ effective_ttl = ttl || default_ttl
90
109
  args = {}
91
- args[:ex] = ttl unless ttl.nil?
92
- result = client.with { |conn| conn.set(key, value, **args) == 'OK' }
93
- cache_logger.debug "[cache] SET #{key} ttl=#{ttl.inspect} success=#{result}"
110
+ args[:ex] = effective_ttl unless effective_ttl.nil?
111
+ serialized = serialize_value(value)
112
+ result = client.with { |conn| conn.set(key, serialized, **args) == 'OK' }
113
+ log.debug { "[cache] SET #{key} ttl=#{effective_ttl.inspect} success=#{result}" }
94
114
  result
95
- rescue ::Redis::BaseError => e
96
- log_cluster_error('redis_set', e, key: key, ttl: ttl)
115
+ rescue StandardError => e
116
+ handle_exception(e, level: :error, handled: false, operation: :redis_set_sync, key: key, ttl: effective_ttl)
97
117
  raise
98
118
  end
99
119
 
100
- def delete(key)
120
+ def delete(key, **)
121
+ delete_sync(key)
122
+ end
123
+
124
+ def delete_sync(key)
101
125
  result = client.with { |conn| conn.del(key) == 1 }
102
- cache_logger.debug "[cache] DELETE #{key} success=#{result}"
126
+ log.debug { "[cache] DELETE #{key} success=#{result}" }
103
127
  result
104
- rescue ::Redis::BaseError => e
105
- log_cluster_error('redis_delete', e, key: key)
128
+ rescue StandardError => e
129
+ handle_exception(e, level: :error, handled: false, operation: :redis_delete_sync, key: key)
106
130
  raise
107
131
  end
108
132
 
@@ -114,11 +138,11 @@ module Legion
114
138
  conn.flushdb == 'OK'
115
139
  end
116
140
  end
117
- cache_logger.debug '[cache] FLUSH completed'
141
+ log.debug { '[cache] FLUSH completed' }
118
142
  result
119
- rescue ::Redis::BaseError => e
120
- log_cluster_error('redis_flush', e)
121
- raise
143
+ rescue StandardError => e
144
+ handle_exception(e, level: :warn, handled: true, operation: :redis_flush)
145
+ nil
122
146
  end
123
147
 
124
148
  def mget(*keys)
@@ -133,34 +157,64 @@ module Legion
133
157
  keys.zip(values).to_h
134
158
  end
135
159
  end
136
- cache_logger.debug "[cache] MGET keys=#{keys.size}"
160
+ result = result.transform_values { |v| deserialize_value(v) }
161
+ log.debug { "[cache] MGET keys=#{keys.size}" }
137
162
  result
138
- rescue ::Redis::BaseError => e
139
- log_cluster_error('redis_mget', e, key_count: keys.size)
140
- raise
163
+ rescue StandardError => e
164
+ handle_exception(e, level: :warn, handled: true, operation: :redis_mget, key_count: keys.size)
165
+ {}
166
+ end
167
+
168
+ def mset(hash, ttl: nil, **)
169
+ mset_sync(hash, ttl: ttl)
141
170
  end
142
171
 
143
- def mset(hash)
172
+ def mset_sync(hash, ttl: nil, **)
144
173
  return true if hash.empty?
145
174
 
146
- result = client.with do |conn|
147
- if cluster_mode?
148
- cluster_mset(conn, hash)
149
- else
150
- conn.mset(*hash.flatten) == 'OK'
151
- end
152
- end
153
- cache_logger.debug "[cache] MSET keys=#{hash.size}"
154
- result
155
- rescue ::Redis::BaseError => e
156
- log_cluster_error('redis_mset', e, key_count: hash.size)
175
+ hash.each { |key, value| set_sync(key, value, ttl: ttl) }
176
+ true
177
+ rescue StandardError => e
178
+ handle_exception(e, level: :error, handled: false, operation: :redis_mset_sync, key_count: hash.size)
157
179
  raise
158
180
  end
159
181
 
182
+ SERIALIZE_STRING = "S\x00".b.freeze
183
+ SERIALIZE_JSON = "J\x00".b.freeze
184
+
160
185
  private
161
186
 
162
- def cache_logger
163
- @component_logger || log
187
+ def serialize_value(value)
188
+ case value
189
+ when String
190
+ "#{SERIALIZE_STRING}#{value}"
191
+ else
192
+ "#{SERIALIZE_JSON}#{Legion::JSON.dump(value)}"
193
+ end
194
+ end
195
+
196
+ def deserialize_value(raw)
197
+ return nil if raw.nil?
198
+
199
+ raw = raw.b if raw.respond_to?(:b)
200
+ if raw.start_with?(SERIALIZE_JSON)
201
+ Legion::JSON.load(raw.byteslice(2..))
202
+ elsif raw.start_with?(SERIALIZE_STRING)
203
+ raw.byteslice(2..)
204
+ else
205
+ raw # legacy data, no prefix
206
+ end
207
+ rescue StandardError => e
208
+ handle_exception(e, level: :warn, handled: true, operation: :redis_deserialize)
209
+ raw
210
+ end
211
+
212
+ def default_ttl
213
+ return 3600 unless defined?(Legion::Settings)
214
+
215
+ Legion::Settings.dig(:cache, :default_ttl) || 3600
216
+ rescue StandardError
217
+ 3600
164
218
  end
165
219
 
166
220
  def cluster_mget(conn, keys)
@@ -187,7 +241,7 @@ module Legion
187
241
  primaries = node_info.lines.select { |l| l.include?('master') }.map { |l| l.split[1].split('@').first }
188
242
  primaries.each do |addr|
189
243
  host, port = Legion::Cache::Settings.parse_server_address(addr, default_port: 6379)
190
- node = ::Redis.new(host: host, port: port.to_i)
244
+ node = ::Redis.new(host: host, port: port.to_i, **(@connection_opts || {}))
191
245
  node.flushdb
192
246
  node.close
193
247
  end
@@ -215,8 +269,15 @@ module Legion
215
269
  'unknown'
216
270
  end
217
271
 
218
- def log_cluster_error(operation, error, **)
219
- handle_exception(error, level: :warn, handled: false, operation: operation, **)
272
+ def resolve_primary_port(server: nil, servers: [], cluster: nil)
273
+ nodes = Array(cluster).compact
274
+ return 6379 if nodes.any?
275
+
276
+ resolved = Legion::Cache::Settings.resolve_servers(driver: 'redis', server: server, servers: Array(servers))
277
+ _, port = Legion::Cache::Settings.parse_server_address(resolved.first, default_port: 6379)
278
+ port.to_i
279
+ rescue StandardError
280
+ 6379
220
281
  end
221
282
 
222
283
  def redis_tls_options(port:)
@@ -11,7 +11,7 @@ module Legion
11
11
 
12
12
  # Returns true when the Redis driver is loaded and the connection pool is live.
13
13
  def redis_available?
14
- pool = Legion::Cache.instance_variable_get(:@client)
14
+ pool = Legion::Cache.pool
15
15
  return false if pool.nil?
16
16
  return false unless Legion::Cache.respond_to?(:driver_name) && Legion::Cache.driver_name == 'redis'
17
17
 
@@ -26,7 +26,7 @@ module Legion
26
26
  def hset(key, hash)
27
27
  return false unless redis_available?
28
28
 
29
- Legion::Cache.instance_variable_get(:@client).with do |conn|
29
+ Legion::Cache.pool.with do |conn|
30
30
  flat = hash.flat_map { |k, v| [k.to_s, v.to_s] }
31
31
  conn.hset(key, *flat)
32
32
  end
@@ -41,7 +41,7 @@ module Legion
41
41
  def hgetall(key)
42
42
  return nil unless redis_available?
43
43
 
44
- result = Legion::Cache.instance_variable_get(:@client).with do |conn|
44
+ result = Legion::Cache.pool.with do |conn|
45
45
  conn.hgetall(key)
46
46
  end
47
47
  log.debug "[cache:redis_hash] HGETALL #{key} fields=#{result.size}"
@@ -55,7 +55,7 @@ module Legion
55
55
  def hdel(key, *fields)
56
56
  return 0 unless redis_available?
57
57
 
58
- result = Legion::Cache.instance_variable_get(:@client).with do |conn|
58
+ result = Legion::Cache.pool.with do |conn|
59
59
  conn.hdel(key, *fields)
60
60
  end
61
61
  log.debug "[cache:redis_hash] HDEL #{key} fields=#{fields.size} removed=#{result}"
@@ -69,7 +69,7 @@ module Legion
69
69
  def zadd(key, score, member)
70
70
  return false unless redis_available?
71
71
 
72
- Legion::Cache.instance_variable_get(:@client).with do |conn|
72
+ Legion::Cache.pool.with do |conn|
73
73
  conn.zadd(key, score.to_f, member.to_s)
74
74
  end
75
75
  log.debug "[cache:redis_hash] ZADD #{key} member=#{member}"
@@ -87,7 +87,7 @@ module Legion
87
87
  opts = {}
88
88
  opts[:limit] = limit if limit
89
89
 
90
- result = Legion::Cache.instance_variable_get(:@client).with do |conn|
90
+ result = Legion::Cache.pool.with do |conn|
91
91
  conn.zrangebyscore(key, min, max, **opts)
92
92
  end
93
93
  log.debug "[cache:redis_hash] ZRANGEBYSCORE #{key} results=#{result.size}"
@@ -101,7 +101,7 @@ module Legion
101
101
  def zrem(key, member)
102
102
  return false unless redis_available?
103
103
 
104
- Legion::Cache.instance_variable_get(:@client).with do |conn|
104
+ Legion::Cache.pool.with do |conn|
105
105
  conn.zrem(key, member.to_s)
106
106
  end
107
107
  log.debug "[cache:redis_hash] ZREM #{key} member=#{member}"
@@ -115,7 +115,7 @@ module Legion
115
115
  def expire(key, seconds)
116
116
  return false unless redis_available?
117
117
 
118
- result = Legion::Cache.instance_variable_get(:@client).with do |conn|
118
+ result = Legion::Cache.pool.with do |conn|
119
119
  conn.expire(key, seconds.to_i) == 1
120
120
  end
121
121
  log.debug "[cache:redis_hash] EXPIRE #{key} seconds=#{seconds} success=#{result}"
@@ -19,50 +19,63 @@ module Legion
19
19
 
20
20
  def self.default
21
21
  {
22
- driver: driver,
23
- servers: resolve_servers(driver: driver),
24
- connected: false,
25
- enabled: true,
26
- namespace: 'legion',
27
- compress: false,
28
- failover: true,
29
- threadsafe: true,
30
- expires_in: 0,
31
- cache_nils: false,
32
- pool_size: 10,
33
- timeout: 5,
34
- default_ttl: 60,
35
- serializer: Legion::JSON,
36
- cluster: nil,
37
- replica: false,
38
- fixed_hostname: nil,
39
- username: nil,
40
- password: nil,
41
- db: nil,
42
- reconnect_attempts: [0, 0.5, 1].freeze
22
+ driver: driver,
23
+ servers: [],
24
+ connected: false,
25
+ enabled: true,
26
+ namespace: 'legion',
27
+ compress: false,
28
+ failover: true,
29
+ threadsafe: true,
30
+ expires_in: 0,
31
+ cache_nils: false,
32
+ pool_size: 10,
33
+ timeout: 5,
34
+ pool_checkout_timeout: 5,
35
+ default_ttl: 3600,
36
+ failback_to_local: true,
37
+ serializer: Legion::JSON,
38
+ cluster: nil,
39
+ replica: false,
40
+ fixed_hostname: nil,
41
+ username: nil,
42
+ password: nil,
43
+ db: nil,
44
+ reconnect_attempts: [0, 0.5, 1].freeze,
45
+ async: {
46
+ pool_size: 4,
47
+ queue_size: 1000,
48
+ shutdown_timeout: 5
49
+ }.freeze,
50
+ reconnect: {
51
+ initial_delay: 1,
52
+ max_delay: 60,
53
+ enabled: true
54
+ }.freeze
43
55
  }
44
56
  end
45
57
 
46
58
  def self.local
47
59
  {
48
- driver: driver,
49
- servers: resolve_servers(driver: driver),
50
- connected: false,
51
- enabled: true,
52
- namespace: 'legion_local',
53
- compress: false,
54
- failover: true,
55
- threadsafe: true,
56
- expires_in: 0,
57
- cache_nils: false,
58
- pool_size: 5,
59
- timeout: 3,
60
- default_ttl: 60,
61
- serializer: Legion::JSON,
62
- username: nil,
63
- password: nil,
64
- db: nil,
65
- reconnect_attempts: [0, 0.25, 0.5].freeze
60
+ driver: driver,
61
+ servers: [],
62
+ connected: false,
63
+ enabled: true,
64
+ namespace: 'legion_local',
65
+ compress: false,
66
+ failover: true,
67
+ threadsafe: true,
68
+ expires_in: 0,
69
+ cache_nils: false,
70
+ pool_size: 5,
71
+ timeout: 3,
72
+ pool_checkout_timeout: 5,
73
+ default_ttl: 21_600,
74
+ serializer: Legion::JSON,
75
+ username: nil,
76
+ password: nil,
77
+ db: nil,
78
+ reconnect_attempts: [0, 0.25, 0.5].freeze
66
79
  }
67
80
  end
68
81
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Cache
5
- VERSION = '1.3.21'
5
+ VERSION = '1.4.1'
6
6
  end
7
7
  end