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 +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +20 -0
- data/lib/legion/cache/memcached.rb +27 -1
- data/lib/legion/cache/redis.rb +140 -7
- data/lib/legion/cache/settings.rb +16 -13
- data/lib/legion/cache/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83c05e01834ac3a2715494122657a540ab6fb0fc23c4b4645f5b3ecf0198279c
|
|
4
|
+
data.tar.gz: e6f944dfa7efa6952810b50766297ce75d39cf6d07bd04af732c8f200e8db051
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79f8620e6101271ab6eb1ec63aecba4a79f2416a2c1b970a1fa02333104a06e6818bc1c7af353a3c5b630d2769834bc9bc8c6a17a4707903cfa4519ffb3884d0
|
|
7
|
+
data.tar.gz: 41c24ca832173bce18bd4b58772426f2cb2c798443315aff4fccbd5fc460f9682c60ef15f3e5e83264f89a3bd2d52203eac2f74cd3264e06e1416338077fa5d2
|
data/.rubocop.yml
CHANGED
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
|
|
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
|
data/lib/legion/cache/redis.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
17
|
-
servers:
|
|
18
|
-
connected:
|
|
19
|
-
enabled:
|
|
20
|
-
namespace:
|
|
21
|
-
compress:
|
|
22
|
-
failover:
|
|
23
|
-
threadsafe:
|
|
24
|
-
expires_in:
|
|
25
|
-
cache_nils:
|
|
26
|
-
pool_size:
|
|
27
|
-
timeout:
|
|
28
|
-
serializer:
|
|
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
|
|
data/lib/legion/cache/version.rb
CHANGED