legion-cache 1.3.18 → 1.3.19
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/CHANGELOG.md +6 -0
- data/Gemfile +1 -0
- data/lib/legion/cache/helper.rb +206 -0
- 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: d491473adc0cc25e2737e471f1dbc644544e2bbc25c67d9ab8e1402836c9f9a7
|
|
4
|
+
data.tar.gz: dd72aaf34ad2f97c37117a9b882a9cf76a34a4bd6d1701316a42eff5dafb1317
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 756116bb625d9aafc055c12c835777e91d1defbe9cc1308a1021789ac27a64382fcc1fb5378d78afb365cc96f0f6a403330d2c4694d151be2bd2c13631303f4c
|
|
7
|
+
data.tar.gz: 117f46e60ca535ee8ce0c4080e3a6f8c71e2cade0ea023ff6edd1fff6ad96cce2ee43448184400f90d5ce58980f58a54f00dc710c5f1fd53127e9a59745a624c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.3.19] - 2026-03-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `cache_mget` / `cache_mset` (and `local_cache_mget` / `local_cache_mset`) on `Helper` mixin — delegates to Redis batch ops, falls back to sequential get/set on Memcached (closes #3)
|
|
9
|
+
- `cache_hset`, `cache_hgetall`, `cache_hdel`, `cache_zadd`, `cache_zrangebyscore`, `cache_zrem`, `cache_expire` on `Helper` mixin — delegates to `RedisHash` with namespace prefixing; hash ops fall back to JSON-serialized Memcached values, sorted-set ops raise `NotImplementedError`, expire is a no-op on Memcached (closes #4)
|
|
10
|
+
|
|
5
11
|
## [1.3.18] - 2026-03-29
|
|
6
12
|
|
|
7
13
|
### Added
|
data/Gemfile
CHANGED
data/lib/legion/cache/helper.rb
CHANGED
|
@@ -54,6 +54,162 @@ module Legion
|
|
|
54
54
|
!Legion::Cache.get(cache_namespace + key).nil?
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# --- Batch Operations (shared tier) ---
|
|
58
|
+
# Issue #3: mget/mset with Memcached safety
|
|
59
|
+
|
|
60
|
+
# Returns a Hash of { key => value } pairs. Prefixes all keys with cache_namespace.
|
|
61
|
+
# Delegates to Legion::Cache.mget on Redis; falls back to sequential gets on Memcached.
|
|
62
|
+
def cache_mget(*keys)
|
|
63
|
+
keys = keys.flatten
|
|
64
|
+
return {} if keys.empty?
|
|
65
|
+
|
|
66
|
+
namespaced = keys.map { |k| cache_namespace + k }
|
|
67
|
+
|
|
68
|
+
if cache_redis?
|
|
69
|
+
raw = Legion::Cache.mget(*namespaced)
|
|
70
|
+
keys.to_h { |k| [k, raw[cache_namespace + k]] }
|
|
71
|
+
else
|
|
72
|
+
keys.to_h { |k| [k, Legion::Cache.get(cache_namespace + k)] }
|
|
73
|
+
end
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
log_cache_error('cache_mget', e)
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Stores multiple key-value pairs. Accepts a Hash of { key => value }.
|
|
80
|
+
# TTL follows the same resolution chain as cache_set.
|
|
81
|
+
# Delegates to Legion::Cache.mset on Redis; falls back to sequential sets on Memcached.
|
|
82
|
+
def cache_mset(hash, ttl: nil)
|
|
83
|
+
return true if hash.empty?
|
|
84
|
+
|
|
85
|
+
effective_ttl = ttl || cache_default_ttl
|
|
86
|
+
|
|
87
|
+
if cache_redis?
|
|
88
|
+
namespaced = hash.transform_keys { |k| cache_namespace + k }
|
|
89
|
+
Legion::Cache.mset(namespaced)
|
|
90
|
+
else
|
|
91
|
+
hash.each { |k, v| Legion::Cache.set(cache_namespace + k, v, effective_ttl) }
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log_cache_error('cache_mset', e)
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# --- Batch Operations (local tier) ---
|
|
100
|
+
|
|
101
|
+
def local_cache_mget(*keys)
|
|
102
|
+
keys = keys.flatten
|
|
103
|
+
return {} if keys.empty?
|
|
104
|
+
|
|
105
|
+
if local_cache_redis?
|
|
106
|
+
namespaced = keys.map { |k| cache_namespace + k }
|
|
107
|
+
raw = Legion::Cache::Local.mget(*namespaced)
|
|
108
|
+
keys.to_h { |k| [k, raw[cache_namespace + k]] }
|
|
109
|
+
else
|
|
110
|
+
keys.to_h { |k| [k, Legion::Cache::Local.get(cache_namespace + k)] }
|
|
111
|
+
end
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
log_cache_error('local_cache_mget', e)
|
|
114
|
+
{}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def local_cache_mset(hash, ttl: nil)
|
|
118
|
+
return true if hash.empty?
|
|
119
|
+
|
|
120
|
+
effective_ttl = ttl || local_cache_default_ttl
|
|
121
|
+
|
|
122
|
+
if local_cache_redis?
|
|
123
|
+
namespaced = hash.transform_keys { |k| cache_namespace + k }
|
|
124
|
+
Legion::Cache::Local.mset(namespaced)
|
|
125
|
+
else
|
|
126
|
+
hash.each { |k, v| Legion::Cache::Local.set(cache_namespace + k, v, effective_ttl) }
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
log_cache_error('local_cache_mset', e)
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# --- RedisHash Helpers (shared tier) ---
|
|
135
|
+
# Issue #4: namespaced wrappers for RedisHash operations with Memcached fallback
|
|
136
|
+
|
|
137
|
+
def cache_hset(key, hash)
|
|
138
|
+
if cache_redis?
|
|
139
|
+
Legion::Cache::RedisHash.hset(cache_namespace + key, hash)
|
|
140
|
+
else
|
|
141
|
+
memcached_hash_merge(cache_namespace + key, hash)
|
|
142
|
+
end
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
log_cache_error('cache_hset', e)
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def cache_hgetall(key)
|
|
149
|
+
if cache_redis?
|
|
150
|
+
Legion::Cache::RedisHash.hgetall(cache_namespace + key)
|
|
151
|
+
else
|
|
152
|
+
memcached_hash_load(cache_namespace + key)
|
|
153
|
+
end
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
log_cache_error('cache_hgetall', e)
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def cache_hdel(key, *fields)
|
|
160
|
+
if cache_redis?
|
|
161
|
+
Legion::Cache::RedisHash.hdel(cache_namespace + key, *fields)
|
|
162
|
+
else
|
|
163
|
+
memcached_hash_delete_fields(cache_namespace + key, fields)
|
|
164
|
+
end
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
log_cache_error('cache_hdel', e)
|
|
167
|
+
0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def cache_zadd(key, score, member)
|
|
171
|
+
raise_sorted_set_unsupported('cache_zadd') unless cache_redis?
|
|
172
|
+
|
|
173
|
+
Legion::Cache::RedisHash.zadd(cache_namespace + key, score, member)
|
|
174
|
+
rescue NotImplementedError
|
|
175
|
+
raise
|
|
176
|
+
rescue StandardError => e
|
|
177
|
+
log_cache_error('cache_zadd', e)
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def cache_zrangebyscore(key, min, max, limit: nil)
|
|
182
|
+
raise_sorted_set_unsupported('cache_zrangebyscore') unless cache_redis?
|
|
183
|
+
|
|
184
|
+
Legion::Cache::RedisHash.zrangebyscore(cache_namespace + key, min, max, limit: limit)
|
|
185
|
+
rescue NotImplementedError
|
|
186
|
+
raise
|
|
187
|
+
rescue StandardError => e
|
|
188
|
+
log_cache_error('cache_zrangebyscore', e)
|
|
189
|
+
[]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def cache_zrem(key, member)
|
|
193
|
+
raise_sorted_set_unsupported('cache_zrem') unless cache_redis?
|
|
194
|
+
|
|
195
|
+
Legion::Cache::RedisHash.zrem(cache_namespace + key, member)
|
|
196
|
+
rescue NotImplementedError
|
|
197
|
+
raise
|
|
198
|
+
rescue StandardError => e
|
|
199
|
+
log_cache_error('cache_zrem', e)
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Sets TTL on a key. No-op on Memcached (TTL is set at write time).
|
|
204
|
+
def cache_expire(key, seconds)
|
|
205
|
+
return false unless cache_redis?
|
|
206
|
+
|
|
207
|
+
Legion::Cache::RedisHash.expire(cache_namespace + key, seconds)
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
log_cache_error('cache_expire', e)
|
|
210
|
+
false
|
|
211
|
+
end
|
|
212
|
+
|
|
57
213
|
# --- Core Operations (local tier) ---
|
|
58
214
|
|
|
59
215
|
def local_cache_set(key, value, ttl: nil, phi: false)
|
|
@@ -147,6 +303,56 @@ module Legion
|
|
|
147
303
|
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
148
304
|
.downcase
|
|
149
305
|
end
|
|
306
|
+
|
|
307
|
+
def cache_redis?
|
|
308
|
+
Legion::Cache::RedisHash.redis_available?
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def local_cache_redis?
|
|
312
|
+
defined?(Legion::Cache::Local) &&
|
|
313
|
+
Legion::Cache::Local.respond_to?(:mget) &&
|
|
314
|
+
Legion::Cache::Local.connected?
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def memcached_hash_merge(full_key, new_fields)
|
|
318
|
+
current = memcached_hash_load(full_key) || {}
|
|
319
|
+
merged = current.merge(new_fields.transform_keys(&:to_s))
|
|
320
|
+
Legion::Cache.set(full_key, Legion::JSON.dump(merged), cache_default_ttl)
|
|
321
|
+
true
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def memcached_hash_load(full_key)
|
|
325
|
+
raw = Legion::Cache.get(full_key)
|
|
326
|
+
return nil if raw.nil?
|
|
327
|
+
|
|
328
|
+
parsed = Legion::JSON.load(raw)
|
|
329
|
+
# Legion::JSON.load returns symbol keys; convert to string keys to mirror Redis hgetall
|
|
330
|
+
parsed.transform_keys(&:to_s)
|
|
331
|
+
rescue StandardError
|
|
332
|
+
nil
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def memcached_hash_delete_fields(full_key, fields)
|
|
336
|
+
current = memcached_hash_load(full_key)
|
|
337
|
+
return 0 if current.nil?
|
|
338
|
+
|
|
339
|
+
str_fields = fields.map(&:to_s)
|
|
340
|
+
removed = str_fields.count { |f| current.key?(f) }
|
|
341
|
+
str_fields.each { |f| current.delete(f) }
|
|
342
|
+
Legion::Cache.set(full_key, Legion::JSON.dump(current), cache_default_ttl)
|
|
343
|
+
removed
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def raise_sorted_set_unsupported(method)
|
|
347
|
+
raise NotImplementedError,
|
|
348
|
+
"#{method} requires a Redis backend — sorted sets are not supported on Memcached"
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def log_cache_error(method, error)
|
|
352
|
+
return unless defined?(Legion::Logging)
|
|
353
|
+
|
|
354
|
+
Legion::Logging.warn "[cache:helper] #{method} failed: #{error.class} — #{error.message}"
|
|
355
|
+
end
|
|
150
356
|
end
|
|
151
357
|
end
|
|
152
358
|
end
|
data/lib/legion/cache/version.rb
CHANGED