legion-cache 1.3.4 → 1.3.6

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: 83c05e01834ac3a2715494122657a540ab6fb0fc23c4b4645f5b3ecf0198279c
4
+ data.tar.gz: e6f944dfa7efa6952810b50766297ce75d39cf6d07bd04af732c8f200e8db051
5
5
  SHA512:
6
- metadata.gz: ee4dfdf2ac8827b26ebfe5048450b951573fdc8e548b7016f33ed1f3caf2aa6ca56310aa5107942d7b5a388644b3a881c0e8ecee5a9440ef51a79308ab9b79f7
7
- data.tar.gz: 185e0503c8bfbfcd839031ffe1d8300e4f3e4d475e627841d1ca9ae5cd9965566fcf5006da522fd29d65bf7e079fd372562aefae528e7db0f892e4f1738aa48c
6
+ metadata.gz: 79f8620e6101271ab6eb1ec63aecba4a79f2416a2c1b970a1fa02333104a06e6818bc1c7af353a3c5b630d2769834bc9bc8c6a17a4707903cfa4519ffb3884d0
7
+ data.tar.gz: 41c24ca832173bce18bd4b58772426f2cb2c798443315aff4fccbd5fc460f9682c60ef15f3e5e83264f89a3bd2d52203eac2f74cd3264e06e1416338077fa5d2
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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.6] - 2026-03-21
4
+
5
+ ### Added
6
+ - Redis Cluster mode: `cluster:`, `replica:`, `fixed_hostname:` options in `build_redis_client`
7
+ - `cluster_mode?` predicate for runtime cluster detection
8
+ - `mget(*keys)` and `mset(hash)` with automatic slot-aware grouping for cross-slot operations
9
+ - Cluster-aware `flush` that iterates all primary nodes via `CLUSTER NODES`
10
+ - Failover logging: `Redis::BaseError` rescues log via `Legion::Logging.warn` before re-raising
11
+ - Settings defaults: `cluster: nil`, `replica: false`, `fixed_hostname: nil`
12
+
13
+ ### Fixed
14
+ - `get`, `set`, `delete`, `flush` visibility changed from private to public (were inaccessible on the module directly)
15
+
16
+ ## [1.3.5] - 2026-03-21
17
+
18
+ ### Added
19
+ - TLS support for Redis driver: `ssl: true` + `ssl_params` when TLS enabled via `Legion::Crypt::TLS.resolve`
20
+ - TLS support for Memcached driver: `ssl_context` option when TLS enabled via `Legion::Crypt::TLS.resolve`
21
+ - Port-based auto-detection: Redis TLS port 6380, Memcached TLS port 11207
22
+
3
23
  ## [1.3.3] - 2026-03-20
4
24
 
5
25
  ### Fixed
@@ -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
@@ -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,36 +9,50 @@ 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
23
26
  @client
24
27
  end
25
28
 
26
- def build_redis_client(server: nil, servers: [], cluster: nil)
29
+ def build_redis_client(server: nil, servers: [], cluster: nil, replica: false, fixed_hostname: nil)
27
30
  nodes = Array(cluster).compact
28
31
  if nodes.any?
29
- ::Redis.new(cluster: nodes)
32
+ opts = { cluster: nodes }
33
+ opts[:replica] = true if replica
34
+ opts[:fixed_hostname] = fixed_hostname unless fixed_hostname.nil?
35
+ ::Redis.new(**opts)
30
36
  else
31
37
  resolved = Legion::Cache::Settings.resolve_servers(
32
38
  driver: 'redis', server: server, servers: servers
33
39
  )
34
40
  host, port = resolved.first.split(':')
35
- ::Redis.new(host: host, port: port.to_i)
41
+ redis_opts = { host: host, port: port.to_i }
42
+ redis_opts.merge!(redis_tls_options(port: port.to_i))
43
+ ::Redis.new(**redis_opts)
36
44
  end
37
45
  end
38
46
 
47
+ def cluster_mode?
48
+ @cluster_mode == true
49
+ end
50
+
39
51
  def get(key)
40
52
  client.with { |conn| conn.get(key) }
53
+ rescue ::Redis::BaseError => e
54
+ log_cluster_error(e)
55
+ raise
41
56
  end
42
57
  alias fetch get
43
58
 
@@ -45,14 +60,132 @@ module Legion
45
60
  args = {}
46
61
  args[:ex] = ttl unless ttl.nil?
47
62
  client.with { |conn| conn.set(key, value, **args) == 'OK' }
63
+ rescue ::Redis::BaseError => e
64
+ log_cluster_error(e)
65
+ raise
48
66
  end
49
67
 
50
68
  def delete(key)
51
69
  client.with { |conn| conn.del(key) == 1 }
70
+ rescue ::Redis::BaseError => e
71
+ log_cluster_error(e)
72
+ raise
52
73
  end
53
74
 
54
75
  def flush
55
- client.with { |conn| conn.flushdb == 'OK' }
76
+ client.with do |conn|
77
+ if cluster_mode?
78
+ cluster_flush(conn)
79
+ else
80
+ conn.flushdb == 'OK'
81
+ end
82
+ end
83
+ rescue ::Redis::BaseError => e
84
+ log_cluster_error(e)
85
+ raise
86
+ end
87
+
88
+ def mget(*keys)
89
+ keys = keys.flatten
90
+ return {} if keys.empty?
91
+
92
+ client.with do |conn|
93
+ if cluster_mode?
94
+ cluster_mget(conn, keys)
95
+ else
96
+ result = conn.mget(*keys)
97
+ keys.zip(result).to_h
98
+ end
99
+ end
100
+ rescue ::Redis::BaseError => e
101
+ log_cluster_error(e)
102
+ raise
103
+ end
104
+
105
+ def mset(hash)
106
+ return true if hash.empty?
107
+
108
+ client.with do |conn|
109
+ if cluster_mode?
110
+ cluster_mset(conn, hash)
111
+ else
112
+ conn.mset(*hash.flatten) == 'OK'
113
+ end
114
+ end
115
+ rescue ::Redis::BaseError => e
116
+ log_cluster_error(e)
117
+ raise
118
+ end
119
+
120
+ private
121
+
122
+ def cluster_mget(conn, keys)
123
+ groups = group_keys_by_slot(keys)
124
+ result = {}
125
+ groups.each_value do |group_keys|
126
+ values = conn.mget(*group_keys)
127
+ group_keys.zip(values).each { |k, v| result[k] = v }
128
+ end
129
+ result
130
+ end
131
+
132
+ def cluster_mset(conn, hash)
133
+ groups = group_keys_by_slot(hash.keys)
134
+ groups.each_value do |group_keys|
135
+ pairs = group_keys.flat_map { |k| [k, hash[k]] }
136
+ conn.mset(*pairs)
137
+ end
138
+ true
139
+ end
140
+
141
+ def cluster_flush(conn)
142
+ node_info = conn.cluster('nodes')
143
+ primaries = node_info.lines.select { |l| l.include?('master') }.map { |l| l.split[1].split('@').first }
144
+ primaries.each do |addr|
145
+ host, port = addr.split(':')
146
+ node = ::Redis.new(host: host, port: port.to_i)
147
+ node.flushdb
148
+ node.close
149
+ end
150
+ true
151
+ rescue StandardError
152
+ conn.flushdb == 'OK'
153
+ end
154
+
155
+ def group_keys_by_slot(keys)
156
+ if defined?(::Redis::Cluster::KeySlotConverter)
157
+ keys.group_by { |k| ::Redis::Cluster::KeySlotConverter.convert(k) }
158
+ else
159
+ { 0 => keys }
160
+ end
161
+ end
162
+
163
+ def log_cluster_error(error)
164
+ return unless defined?(Legion::Logging)
165
+
166
+ Legion::Logging.warn "Redis cluster error: #{error.class} — #{error.message}"
167
+ end
168
+
169
+ def redis_tls_options(port:)
170
+ return {} unless defined?(Legion::Crypt::TLS)
171
+
172
+ tls = Legion::Crypt::TLS.resolve(cache_tls_settings, port: port)
173
+ return {} unless tls[:enabled]
174
+
175
+ ssl_params = {
176
+ verify_mode: tls[:verify] == :none ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
177
+ }
178
+ ssl_params[:ca_file] = tls[:ca] if tls[:ca]
179
+
180
+ { ssl: true, ssl_params: ssl_params }
181
+ end
182
+
183
+ def cache_tls_settings
184
+ return {} unless defined?(Legion::Settings)
185
+
186
+ Legion::Settings[:cache][:tls] || {}
187
+ rescue StandardError
188
+ {}
56
189
  end
57
190
  end
58
191
  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.6'
6
6
  end
7
7
  end
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.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity