legion-cache 1.3.4 → 1.3.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c04121f4d10bcc0076e189557b0036d02237fb97fd8bfa9c67779df4298d2e41
4
- data.tar.gz: faaf6c6c82212257a9e99866948f57269147e0b942301893d100e580cd47e743
3
+ metadata.gz: 99c05f6312bd657aa7c85e953e6ec8cbf73ad621953239964a9cb25e7d5ec120
4
+ data.tar.gz: 95292e0a0e278bf124c4e09bc4dc04a41078d3afff7d3484b64d402f825efba6
5
5
  SHA512:
6
- metadata.gz: ee4dfdf2ac8827b26ebfe5048450b951573fdc8e548b7016f33ed1f3caf2aa6ca56310aa5107942d7b5a388644b3a881c0e8ecee5a9440ef51a79308ab9b79f7
7
- data.tar.gz: 185e0503c8bfbfcd839031ffe1d8300e4f3e4d475e627841d1ca9ae5cd9965566fcf5006da522fd29d65bf7e079fd372562aefae528e7db0f892e4f1738aa48c
6
+ metadata.gz: 2376fa09e3540aa1044d5eb82169c09a2425fa0cef0d3e597633ab96ed3bff0722e459d3e716383b4b9652bce6c6d4d3883c8afe9df61b6b5e0ba0a7fa0f4fcb
7
+ data.tar.gz: 689a94946a67c949cf436ba6786f05e67e143a73bfac84796cdc9240f9c36d17da4312739f43022c5c69d2d52c6003607224132e0f142f6177e64a879e280938
data/.rubocop.yml CHANGED
@@ -48,3 +48,6 @@ Style/FrozenStringLiteralComment:
48
48
 
49
49
  Naming/FileName:
50
50
  Enabled: false
51
+
52
+ Naming/PredicateMethod:
53
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.7] - 2026-03-22
4
+
5
+ ### Added
6
+ - Redis driver: `.debug` logging on get (hit/miss), set (ttl/success), delete, flush, mget (key count), mset (key count)
7
+ - Redis driver: `.info` on successful client creation with host/port address
8
+ - Redis driver: private `resolved_redis_address` helper for extracting address at connect time
9
+ - Pool: `.info` on close and restart
10
+ - Cacheable: `.debug` on cache hit/miss in wrapper; `.warn` on swallowed errors in `local_cache_read`/`local_cache_write`
11
+ - Local: `.debug` on get/set/fetch/delete/flush operations
12
+ - Cache: `.info` on successful shared cache setup (driver + server)
13
+ - All new logging calls guarded with `if defined?(Legion::Logging)` for standalone use
14
+
15
+ ## [1.3.6] - 2026-03-21
16
+
17
+ ### Added
18
+ - Redis Cluster mode: `cluster:`, `replica:`, `fixed_hostname:` options in `build_redis_client`
19
+ - `cluster_mode?` predicate for runtime cluster detection
20
+ - `mget(*keys)` and `mset(hash)` with automatic slot-aware grouping for cross-slot operations
21
+ - Cluster-aware `flush` that iterates all primary nodes via `CLUSTER NODES`
22
+ - Failover logging: `Redis::BaseError` rescues log via `Legion::Logging.warn` before re-raising
23
+ - Settings defaults: `cluster: nil`, `replica: false`, `fixed_hostname: nil`
24
+
25
+ ### Fixed
26
+ - `get`, `set`, `delete`, `flush` visibility changed from private to public (were inaccessible on the module directly)
27
+
28
+ ## [1.3.5] - 2026-03-21
29
+
30
+ ### Added
31
+ - TLS support for Redis driver: `ssl: true` + `ssl_params` when TLS enabled via `Legion::Crypt::TLS.resolve`
32
+ - TLS support for Memcached driver: `ssl_context` option when TLS enabled via `Legion::Crypt::TLS.resolve`
33
+ - Port-based auto-detection: Redis TLS port 6380, Memcached TLS port 11207
34
+
3
35
  ## [1.3.3] - 2026-03-20
4
36
 
5
37
  ### Fixed
@@ -28,7 +28,12 @@ module Legion
28
28
 
29
29
  unless bypass_local_method_cache
30
30
  cached = Legion::Cache::Cacheable.cache_read(key, scope: config[:scope])
31
- return cached unless cached.nil?
31
+ if cached.nil?
32
+ Legion::Logging.debug "[cacheable] miss key=#{key}" if defined?(Legion::Logging)
33
+ else
34
+ Legion::Logging.debug "[cacheable] hit key=#{key}" if defined?(Legion::Logging)
35
+ return cached
36
+ end
32
37
  end
33
38
 
34
39
  result = super(**kwargs)
@@ -86,7 +91,8 @@ module Legion
86
91
  return nil unless local_cache_available?
87
92
 
88
93
  Legion::Cache::Local.get(key)
89
- rescue StandardError
94
+ rescue StandardError => e
95
+ Legion::Logging.warn "[cacheable] local_cache_read failed key=#{key} error=#{e.message}" if defined?(Legion::Logging)
90
96
  nil
91
97
  end
92
98
 
@@ -94,7 +100,8 @@ module Legion
94
100
  return unless local_cache_available?
95
101
 
96
102
  Legion::Cache::Local.set(key, value, ttl)
97
- rescue StandardError
103
+ rescue StandardError => e
104
+ Legion::Logging.warn "[cacheable] local_cache_write failed key=#{key} error=#{e.message}" if defined?(Legion::Logging)
98
105
  nil
99
106
  end
100
107
 
@@ -36,23 +36,33 @@ module Legion
36
36
  end
37
37
 
38
38
  def get(key)
39
- @driver.get(key)
39
+ result = @driver.get(key)
40
+ Legion::Logging.debug "[cache:local] GET #{key} hit=#{!result.nil?}" if defined?(Legion::Logging)
41
+ result
40
42
  end
41
43
 
42
44
  def set(key, value, ttl = 180)
43
- @driver.set(key, value, ttl)
45
+ result = @driver.set(key, value, ttl)
46
+ Legion::Logging.debug "[cache:local] SET #{key} ttl=#{ttl} success=#{result}" if defined?(Legion::Logging)
47
+ result
44
48
  end
45
49
 
46
50
  def fetch(key, ttl = nil)
47
- @driver.fetch(key, ttl)
51
+ result = @driver.fetch(key, ttl)
52
+ Legion::Logging.debug "[cache:local] FETCH #{key} hit=#{!result.nil?}" if defined?(Legion::Logging)
53
+ result
48
54
  end
49
55
 
50
56
  def delete(key)
51
- @driver.delete(key)
57
+ result = @driver.delete(key)
58
+ Legion::Logging.debug "[cache:local] DELETE #{key} success=#{result}" if defined?(Legion::Logging)
59
+ result
52
60
  end
53
61
 
54
62
  def flush(delay = 0)
55
- @driver.flush(delay)
63
+ result = @driver.flush(delay)
64
+ Legion::Logging.debug '[cache:local] FLUSH completed' if defined?(Legion::Logging)
65
+ result
56
66
  end
57
67
 
58
68
  def client
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openssl'
3
4
  require 'dalli'
4
5
  require 'legion/cache/pool'
5
6
 
@@ -7,7 +8,7 @@ module Legion
7
8
  module Cache
8
9
  module Memcached
9
10
  include Legion::Cache::Pool
10
- extend self # rubocop:disable Style/ModuleFunction
11
+ extend self
11
12
 
12
13
  def client(server: nil, servers: nil, **opts)
13
14
  return @client unless @client.nil?
@@ -27,6 +28,9 @@ module Legion
27
28
  cache_opts[:value_max_bytes] ||= 8 * 1024 * 1024
28
29
  cache_opts[:serializer] ||= Legion::JSON
29
30
 
31
+ tls_ctx = memcached_tls_context(port: resolved.first.split(':').last.to_i)
32
+ cache_opts[:ssl_context] = tls_ctx if tls_ctx
33
+
30
34
  @client = ConnectionPool.new(size: pool_size, timeout: timeout) do
31
35
  Dalli::Client.new(resolved, cache_opts)
32
36
  end
@@ -62,6 +66,28 @@ module Legion
62
66
  def flush(delay = 0)
63
67
  client.with { |conn| conn.flush(delay).first }
64
68
  end
69
+
70
+ private
71
+
72
+ def memcached_tls_context(port:)
73
+ return nil unless defined?(Legion::Crypt::TLS)
74
+
75
+ tls = Legion::Crypt::TLS.resolve(memcached_tls_settings, port: port)
76
+ return nil unless tls[:enabled]
77
+
78
+ ctx = OpenSSL::SSL::SSLContext.new
79
+ ctx.verify_mode = tls[:verify] == :none ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
80
+ ctx.ca_file = tls[:ca] if tls[:ca]
81
+ ctx
82
+ end
83
+
84
+ def memcached_tls_settings
85
+ return {} unless defined?(Legion::Settings)
86
+
87
+ Legion::Settings[:cache][:tls] || {}
88
+ rescue StandardError
89
+ {}
90
+ end
65
91
  end
66
92
  end
67
93
  end
@@ -31,6 +31,7 @@ module Legion
31
31
  client.shutdown(&:close)
32
32
  @client = nil
33
33
  @connected = false
34
+ Legion::Logging.info "#{name} pool closed" if defined?(Legion::Logging)
34
35
  end
35
36
 
36
37
  def restart(**opts)
@@ -41,6 +42,7 @@ module Legion
41
42
  client_hash[:timeout] = opts[:timeout] if opts.key? :timeout
42
43
  client(**client_hash)
43
44
  @connected = true
45
+ Legion::Logging.info "#{name} pool restarted" if defined?(Legion::Logging)
44
46
  end
45
47
  end
46
48
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'openssl'
3
4
  require 'redis'
4
5
  require 'legion/cache/pool'
5
6
  require 'legion/cache/settings'
@@ -8,51 +9,205 @@ module Legion
8
9
  module Cache
9
10
  module Redis
10
11
  include Legion::Cache::Pool
11
- extend self # rubocop:disable Style/ModuleFunction
12
+ extend self
12
13
 
13
- def client(pool_size: 20, timeout: 5, server: nil, servers: [], cluster: nil, **) # rubocop:disable Metrics/ParameterLists
14
+ def client(pool_size: 20, timeout: 5, server: nil, servers: [], cluster: nil, replica: false, fixed_hostname: nil, **) # rubocop:disable Metrics/ParameterLists
14
15
  return @client unless @client.nil?
15
16
 
16
17
  @pool_size = pool_size
17
18
  @timeout = timeout
19
+ @cluster_mode = Array(cluster).compact.any?
18
20
 
19
21
  @client = ConnectionPool.new(size: pool_size, timeout: timeout) do
20
- build_redis_client(server: server, servers: servers, cluster: cluster)
22
+ build_redis_client(server: server, servers: servers, cluster: cluster,
23
+ replica: replica, fixed_hostname: fixed_hostname)
21
24
  end
22
25
  @connected = true
26
+ Legion::Logging.info "Redis connected to #{resolved_redis_address(server: server, servers: servers, cluster: cluster)}" if defined?(Legion::Logging)
23
27
  @client
24
28
  end
25
29
 
26
- def build_redis_client(server: nil, servers: [], cluster: nil)
30
+ def build_redis_client(server: nil, servers: [], cluster: nil, replica: false, fixed_hostname: nil)
27
31
  nodes = Array(cluster).compact
28
32
  if nodes.any?
29
- ::Redis.new(cluster: nodes)
33
+ opts = { cluster: nodes }
34
+ opts[:replica] = true if replica
35
+ opts[:fixed_hostname] = fixed_hostname unless fixed_hostname.nil?
36
+ ::Redis.new(**opts)
30
37
  else
31
38
  resolved = Legion::Cache::Settings.resolve_servers(
32
39
  driver: 'redis', server: server, servers: servers
33
40
  )
34
41
  host, port = resolved.first.split(':')
35
- ::Redis.new(host: host, port: port.to_i)
42
+ redis_opts = { host: host, port: port.to_i }
43
+ redis_opts.merge!(redis_tls_options(port: port.to_i))
44
+ ::Redis.new(**redis_opts)
36
45
  end
37
46
  end
38
47
 
48
+ def cluster_mode?
49
+ @cluster_mode == true
50
+ end
51
+
39
52
  def get(key)
40
- client.with { |conn| conn.get(key) }
53
+ result = client.with { |conn| conn.get(key) }
54
+ Legion::Logging.debug "[cache] GET #{key} hit=#{!result.nil?}" if defined?(Legion::Logging)
55
+ result
56
+ rescue ::Redis::BaseError => e
57
+ log_cluster_error(e)
58
+ raise
41
59
  end
42
60
  alias fetch get
43
61
 
44
62
  def set(key, value, ttl: nil)
45
63
  args = {}
46
64
  args[:ex] = ttl unless ttl.nil?
47
- client.with { |conn| conn.set(key, value, **args) == 'OK' }
65
+ result = client.with { |conn| conn.set(key, value, **args) == 'OK' }
66
+ Legion::Logging.debug "[cache] SET #{key} ttl=#{ttl.inspect} success=#{result}" if defined?(Legion::Logging)
67
+ result
68
+ rescue ::Redis::BaseError => e
69
+ log_cluster_error(e)
70
+ raise
48
71
  end
49
72
 
50
73
  def delete(key)
51
- client.with { |conn| conn.del(key) == 1 }
74
+ result = client.with { |conn| conn.del(key) == 1 }
75
+ Legion::Logging.debug "[cache] DELETE #{key} success=#{result}" if defined?(Legion::Logging)
76
+ result
77
+ rescue ::Redis::BaseError => e
78
+ log_cluster_error(e)
79
+ raise
52
80
  end
53
81
 
54
82
  def flush
55
- client.with { |conn| conn.flushdb == 'OK' }
83
+ result = client.with do |conn|
84
+ if cluster_mode?
85
+ cluster_flush(conn)
86
+ else
87
+ conn.flushdb == 'OK'
88
+ end
89
+ end
90
+ Legion::Logging.debug '[cache] FLUSH completed' if defined?(Legion::Logging)
91
+ result
92
+ rescue ::Redis::BaseError => e
93
+ log_cluster_error(e)
94
+ raise
95
+ end
96
+
97
+ def mget(*keys)
98
+ keys = keys.flatten
99
+ return {} if keys.empty?
100
+
101
+ result = client.with do |conn|
102
+ if cluster_mode?
103
+ cluster_mget(conn, keys)
104
+ else
105
+ values = conn.mget(*keys)
106
+ keys.zip(values).to_h
107
+ end
108
+ end
109
+ Legion::Logging.debug "[cache] MGET keys=#{keys.size}" if defined?(Legion::Logging)
110
+ result
111
+ rescue ::Redis::BaseError => e
112
+ log_cluster_error(e)
113
+ raise
114
+ end
115
+
116
+ def mset(hash)
117
+ return true if hash.empty?
118
+
119
+ result = client.with do |conn|
120
+ if cluster_mode?
121
+ cluster_mset(conn, hash)
122
+ else
123
+ conn.mset(*hash.flatten) == 'OK'
124
+ end
125
+ end
126
+ Legion::Logging.debug "[cache] MSET keys=#{hash.size}" if defined?(Legion::Logging)
127
+ result
128
+ rescue ::Redis::BaseError => e
129
+ log_cluster_error(e)
130
+ raise
131
+ end
132
+
133
+ private
134
+
135
+ def cluster_mget(conn, keys)
136
+ groups = group_keys_by_slot(keys)
137
+ result = {}
138
+ groups.each_value do |group_keys|
139
+ values = conn.mget(*group_keys)
140
+ group_keys.zip(values).each { |k, v| result[k] = v }
141
+ end
142
+ result
143
+ end
144
+
145
+ def cluster_mset(conn, hash)
146
+ groups = group_keys_by_slot(hash.keys)
147
+ groups.each_value do |group_keys|
148
+ pairs = group_keys.flat_map { |k| [k, hash[k]] }
149
+ conn.mset(*pairs)
150
+ end
151
+ true
152
+ end
153
+
154
+ def cluster_flush(conn)
155
+ node_info = conn.cluster('nodes')
156
+ primaries = node_info.lines.select { |l| l.include?('master') }.map { |l| l.split[1].split('@').first }
157
+ primaries.each do |addr|
158
+ host, port = addr.split(':')
159
+ node = ::Redis.new(host: host, port: port.to_i)
160
+ node.flushdb
161
+ node.close
162
+ end
163
+ true
164
+ rescue StandardError
165
+ conn.flushdb == 'OK'
166
+ end
167
+
168
+ def group_keys_by_slot(keys)
169
+ if defined?(::Redis::Cluster::KeySlotConverter)
170
+ keys.group_by { |k| ::Redis::Cluster::KeySlotConverter.convert(k) }
171
+ else
172
+ { 0 => keys }
173
+ end
174
+ end
175
+
176
+ def resolved_redis_address(server:, servers:, cluster:)
177
+ nodes = Array(cluster).compact
178
+ return nodes.join(', ') if nodes.any?
179
+
180
+ Legion::Cache::Settings.resolve_servers(driver: 'redis', server: server, servers: Array(servers)).first
181
+ rescue StandardError
182
+ 'unknown'
183
+ end
184
+
185
+ def log_cluster_error(error)
186
+ return unless defined?(Legion::Logging)
187
+
188
+ Legion::Logging.warn "Redis cluster error: #{error.class} — #{error.message}"
189
+ end
190
+
191
+ def redis_tls_options(port:)
192
+ return {} unless defined?(Legion::Crypt::TLS)
193
+
194
+ tls = Legion::Crypt::TLS.resolve(cache_tls_settings, port: port)
195
+ return {} unless tls[:enabled]
196
+
197
+ ssl_params = {
198
+ verify_mode: tls[:verify] == :none ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
199
+ }
200
+ ssl_params[:ca_file] = tls[:ca] if tls[:ca]
201
+
202
+ { ssl: true, ssl_params: ssl_params }
203
+ end
204
+
205
+ def cache_tls_settings
206
+ return {} unless defined?(Legion::Settings)
207
+
208
+ Legion::Settings[:cache][:tls] || {}
209
+ rescue StandardError
210
+ {}
56
211
  end
57
212
  end
58
213
  end
@@ -13,19 +13,22 @@ module Legion
13
13
  Legion::Settings.merge_settings(:cache_local, local) if Legion::Settings.method_defined? :merge_settings
14
14
  def self.default
15
15
  {
16
- driver: driver,
17
- servers: resolve_servers(driver: driver),
18
- connected: false,
19
- enabled: true,
20
- namespace: 'legion',
21
- compress: false,
22
- failover: true,
23
- threadsafe: true,
24
- expires_in: 0,
25
- cache_nils: false,
26
- pool_size: 10,
27
- timeout: 5,
28
- serializer: Legion::JSON
16
+ driver: driver,
17
+ servers: resolve_servers(driver: driver),
18
+ connected: false,
19
+ enabled: true,
20
+ namespace: 'legion',
21
+ compress: false,
22
+ failover: true,
23
+ threadsafe: true,
24
+ expires_in: 0,
25
+ cache_nils: false,
26
+ pool_size: 10,
27
+ timeout: 5,
28
+ serializer: Legion::JSON,
29
+ cluster: nil,
30
+ replica: false,
31
+ fixed_hostname: nil
29
32
  }
30
33
  end
31
34
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Cache
5
- VERSION = '1.3.4'
5
+ VERSION = '1.3.7'
6
6
  end
7
7
  end
data/lib/legion/cache.rb CHANGED
@@ -86,6 +86,9 @@ module Legion
86
86
  @connected = true
87
87
  @using_local = false
88
88
  Legion::Settings[:cache][:connected] = true
89
+ driver = Legion::Settings[:cache][:driver] || 'dalli'
90
+ servers = Legion::Settings[:cache][:servers] || []
91
+ Legion::Logging.info "Legion::Cache connected (driver=#{driver} servers=#{Array(servers).first})" if defined?(Legion::Logging)
89
92
  rescue StandardError => e
90
93
  Legion::Logging.warn "Shared cache unavailable (#{e.message}), falling back to Local" if defined?(Legion::Logging)
91
94
  if Legion::Cache::Local.connected?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 1.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity