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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aded92a74b60ff3cae8cab7d33d38f147bf17ac308dfd78695b2e6e2ed70298e
4
- data.tar.gz: 244c88d6428745bd91d949dc7738cf65392e644d2143b3c092ab9b84fc610cd2
3
+ metadata.gz: d491473adc0cc25e2737e471f1dbc644544e2bbc25c67d9ab8e1402836c9f9a7
4
+ data.tar.gz: dd72aaf34ad2f97c37117a9b882a9cf76a34a4bd6d1701316a42eff5dafb1317
5
5
  SHA512:
6
- metadata.gz: d6b790d262f8d9735c7d3a31a5222b449408deb00126f5184b8404fafbfa95a30a2eb3826a1cff5c0a44420fe0510399ff47c002ec816b65f596079977d00e4b
7
- data.tar.gz: 774eb4fd9532934894e4d38ab52cba094e5fbab03c33eeb12f9aae0a244efebfddf3eea454a97445598143de680e2eac01ddd6b4f256b4ce116408e398c07e50
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
@@ -8,5 +8,6 @@ group :test do
8
8
  gem 'rspec'
9
9
  gem 'rspec_junit_formatter'
10
10
  gem 'rubocop'
11
+ gem 'rubocop-legion'
11
12
  gem 'simplecov'
12
13
  end
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Cache
5
- VERSION = '1.3.18'
5
+ VERSION = '1.3.19'
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.18
4
+ version: 1.3.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity