legion-cache 1.3.16 → 1.3.18
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/.github/CODEOWNERS +7 -0
- data/.github/dependabot.yml +18 -0
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +17 -1
- data/README.md +1 -1
- data/lib/legion/cache/helper.rb +92 -8
- data/lib/legion/cache/redis_hash.rb +118 -0
- data/lib/legion/cache/settings.rb +2 -0
- data/lib/legion/cache/version.rb +1 -1
- data/lib/legion/cache.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aded92a74b60ff3cae8cab7d33d38f147bf17ac308dfd78695b2e6e2ed70298e
|
|
4
|
+
data.tar.gz: 244c88d6428745bd91d949dc7738cf65392e644d2143b3c092ab9b84fc610cd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6b790d262f8d9735c7d3a31a5222b449408deb00126f5184b8404fafbfa95a30a2eb3826a1cff5c0a44420fe0510399ff47c002ec816b65f596079977d00e4b
|
|
7
|
+
data.tar.gz: 774eb4fd9532934894e4d38ab52cba094e5fbab03c33eeb12f9aae0a244efebfddf3eea454a97445598143de680e2eac01ddd6b4f256b4ce116408e398c07e50
|
data/.github/CODEOWNERS
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: bundler
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
day: monday
|
|
8
|
+
open-pull-requests-limit: 5
|
|
9
|
+
labels:
|
|
10
|
+
- "type:dependencies"
|
|
11
|
+
- package-ecosystem: github-actions
|
|
12
|
+
directory: /
|
|
13
|
+
schedule:
|
|
14
|
+
interval: weekly
|
|
15
|
+
day: monday
|
|
16
|
+
open-pull-requests-limit: 5
|
|
17
|
+
labels:
|
|
18
|
+
- "type:dependencies"
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.3.18] - 2026-03-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Layered TTL resolution in Helper (per-call → LEX override → Settings → FALLBACK_TTL)
|
|
9
|
+
- `cache_default_ttl` / `local_cache_default_ttl` — LEX-overridable default TTL methods
|
|
10
|
+
- `cache_exist?` / `local_cache_exist?` — key existence checks
|
|
11
|
+
- `cache_connected?` / `local_cache_connected?` — connection status helpers
|
|
12
|
+
- `cache_pool_size` / `cache_pool_available` — pool info (shared tier)
|
|
13
|
+
- `local_cache_pool_size` / `local_cache_pool_available` — pool info (local tier)
|
|
14
|
+
- `phi:` keyword argument on `cache_set` / `local_cache_set` for PHI TTL enforcement
|
|
15
|
+
- `default_ttl` key in Settings.default and Settings.local (defaults to 60)
|
|
16
|
+
|
|
17
|
+
## [1.3.17] - 2026-03-25
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `Legion::Cache::RedisHash` module: Redis hash and sorted-set operations (`hset`, `hgetall`, `hdel`, `zadd`, `zrangebyscore`, `zrem`, `expire`) with `redis_available?` guard and safe defaults when Redis is not connected
|
|
21
|
+
- Auto-required from `legion/cache.rb` alongside the existing Redis adapter
|
|
22
|
+
|
|
23
|
+
## [1.3.16] - 2026-03-25
|
|
24
|
+
|
|
5
25
|
### Fixed
|
|
6
26
|
- Accept ttl as positional or keyword argument in Cache.set for caller flexibility
|
|
7
27
|
- Align Redis.set signature to positional ttl arg matching parent module convention
|
data/CLAUDE.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Caching wrapper for the LegionIO framework. Provides a consistent interface for Memcached (via `dalli`) and Redis (via `redis` gem) with connection pooling. Driver selection is config-driven.
|
|
9
9
|
|
|
10
10
|
**GitHub**: https://github.com/LegionIO/legion-cache
|
|
11
|
-
**Version**: 1.3.
|
|
11
|
+
**Version**: 1.3.17
|
|
12
12
|
**License**: Apache-2.0
|
|
13
13
|
|
|
14
14
|
## Architecture
|
|
@@ -39,6 +39,7 @@ Legion::Cache (singleton module)
|
|
|
39
39
|
├── Memory # Lite mode adapter: pure in-memory cache, TTL expiry, Mutex thread-safety
|
|
40
40
|
│ └── Activated by LEGION_MODE=lite env var; no Redis/Memcached required
|
|
41
41
|
├── Helper # Injectable cache mixin for LEX extensions (namespaced cache_*/local_cache_*)
|
|
42
|
+
├── RedisHash # Redis-specific sorted set + hash operations (hset/hgetall/hdel/zadd/zrangebyscore/zrem/expire); Redis-only, module_function pattern
|
|
42
43
|
├── Local # Local cache tier (localhost Redis/Memcached, fallback target)
|
|
43
44
|
│ ├── .setup # Connect to local cache server (auto-detect driver)
|
|
44
45
|
│ ├── .shutdown # Close local connection
|
|
@@ -134,10 +135,25 @@ Dalli enforces a 1MB client-side limit by default (`value_max_bytes: 1_048_576`)
|
|
|
134
135
|
| `lib/legion/cache/memory.rb` | Lite mode Memory adapter: in-memory store with TTL + Mutex thread-safety |
|
|
135
136
|
| `lib/legion/cache/helper.rb` | Injectable cache mixin for LEX extensions |
|
|
136
137
|
| `lib/legion/cache/local.rb` | Local cache tier (localhost, fallback target) |
|
|
138
|
+
| `lib/legion/cache/redis_hash.rb` | Redis sorted set + hash operations (hset/hgetall/hdel/zadd/zrangebyscore/zrem/expire) |
|
|
137
139
|
| `lib/legion/cache/pool.rb` | Connection pool management |
|
|
138
140
|
| `lib/legion/cache/settings.rb` | Default configuration + local defaults |
|
|
139
141
|
| `lib/legion/cache/version.rb` | VERSION constant |
|
|
140
142
|
|
|
143
|
+
## PHI TTL Cap
|
|
144
|
+
|
|
145
|
+
When `phi: true` is passed to `set`, the TTL is capped at `cache.compliance.phi_max_ttl` (default 3600s). This enforces the HIPAA PHI TTL policy in legion-logging. The `enforce_phi_ttl(ttl, phi: false)` method applies the cap; without `phi: true` the TTL is passed through unchanged.
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"cache": {
|
|
150
|
+
"compliance": {
|
|
151
|
+
"phi_max_ttl": 3600
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
141
157
|
## Role in LegionIO
|
|
142
158
|
|
|
143
159
|
Optional caching layer initialized during `Legion::Service` startup. Used by `legion-data` for model caching (Sequel caching plugin) and by extensions for general-purpose caching.
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Caching wrapper for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides a consistent interface for Memcached (via `dalli`) and Redis (via `redis` gem) with connection pooling. Driver selection is config-driven.
|
|
4
4
|
|
|
5
|
-
**Version**: 1.3.
|
|
5
|
+
**Version**: 1.3.17
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
data/lib/legion/cache/helper.rb
CHANGED
|
@@ -3,12 +3,38 @@
|
|
|
3
3
|
module Legion
|
|
4
4
|
module Cache
|
|
5
5
|
module Helper
|
|
6
|
+
FALLBACK_TTL = 60
|
|
7
|
+
|
|
8
|
+
# --- TTL Resolution ---
|
|
9
|
+
# Override in your LEX to set a custom default TTL for the extension.
|
|
10
|
+
# Resolution chain: per-call ttl: kwarg -> LEX override -> Settings -> FALLBACK_TTL
|
|
11
|
+
def cache_default_ttl
|
|
12
|
+
return FALLBACK_TTL unless defined?(Legion::Settings)
|
|
13
|
+
|
|
14
|
+
Legion::Settings.dig(:cache, :default_ttl) || FALLBACK_TTL
|
|
15
|
+
rescue StandardError
|
|
16
|
+
FALLBACK_TTL
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def local_cache_default_ttl
|
|
20
|
+
return cache_default_ttl unless defined?(Legion::Settings)
|
|
21
|
+
|
|
22
|
+
Legion::Settings.dig(:cache_local, :default_ttl) || cache_default_ttl
|
|
23
|
+
rescue StandardError
|
|
24
|
+
cache_default_ttl
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# --- Namespace ---
|
|
28
|
+
|
|
6
29
|
def cache_namespace
|
|
7
30
|
@cache_namespace ||= derive_cache_namespace
|
|
8
31
|
end
|
|
9
32
|
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
# --- Core Operations (shared tier) ---
|
|
34
|
+
|
|
35
|
+
def cache_set(key, value, ttl: nil, phi: false)
|
|
36
|
+
effective_ttl = ttl || cache_default_ttl
|
|
37
|
+
Legion::Cache.set(cache_namespace + key, value, effective_ttl, phi: phi)
|
|
12
38
|
end
|
|
13
39
|
|
|
14
40
|
def cache_get(key)
|
|
@@ -19,12 +45,21 @@ module Legion
|
|
|
19
45
|
Legion::Cache.delete(cache_namespace + key)
|
|
20
46
|
end
|
|
21
47
|
|
|
22
|
-
def cache_fetch(key, ttl:
|
|
23
|
-
|
|
48
|
+
def cache_fetch(key, ttl: nil, &)
|
|
49
|
+
effective_ttl = ttl || cache_default_ttl
|
|
50
|
+
Legion::Cache.fetch(cache_namespace + key, effective_ttl, &)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def cache_exist?(key)
|
|
54
|
+
!Legion::Cache.get(cache_namespace + key).nil?
|
|
24
55
|
end
|
|
25
56
|
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
# --- Core Operations (local tier) ---
|
|
58
|
+
|
|
59
|
+
def local_cache_set(key, value, ttl: nil, phi: false)
|
|
60
|
+
effective_ttl = ttl || local_cache_default_ttl
|
|
61
|
+
effective_ttl = Legion::Cache.enforce_phi_ttl(effective_ttl, phi: phi)
|
|
62
|
+
Legion::Cache::Local.set(cache_namespace + key, value, effective_ttl)
|
|
28
63
|
end
|
|
29
64
|
|
|
30
65
|
def local_cache_get(key)
|
|
@@ -35,8 +70,57 @@ module Legion
|
|
|
35
70
|
Legion::Cache::Local.delete(cache_namespace + key)
|
|
36
71
|
end
|
|
37
72
|
|
|
38
|
-
def local_cache_fetch(key, ttl:
|
|
39
|
-
|
|
73
|
+
def local_cache_fetch(key, ttl: nil, &)
|
|
74
|
+
effective_ttl = ttl || local_cache_default_ttl
|
|
75
|
+
Legion::Cache::Local.fetch(cache_namespace + key, effective_ttl, &)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def local_cache_exist?(key)
|
|
79
|
+
!Legion::Cache::Local.get(cache_namespace + key).nil?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- Status ---
|
|
83
|
+
|
|
84
|
+
def cache_connected?
|
|
85
|
+
Legion::Cache.connected?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def local_cache_connected?
|
|
89
|
+
Legion::Cache::Local.connected?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# --- Pool Info ---
|
|
93
|
+
|
|
94
|
+
def cache_pool_size
|
|
95
|
+
return 0 unless cache_connected?
|
|
96
|
+
|
|
97
|
+
Legion::Cache.pool_size
|
|
98
|
+
rescue StandardError
|
|
99
|
+
0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cache_pool_available
|
|
103
|
+
return 0 unless cache_connected?
|
|
104
|
+
|
|
105
|
+
Legion::Cache.available
|
|
106
|
+
rescue StandardError
|
|
107
|
+
0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def local_cache_pool_size
|
|
111
|
+
return 0 unless local_cache_connected?
|
|
112
|
+
|
|
113
|
+
Legion::Cache::Local.pool_size
|
|
114
|
+
rescue StandardError
|
|
115
|
+
0
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def local_cache_pool_available
|
|
119
|
+
return 0 unless local_cache_connected?
|
|
120
|
+
|
|
121
|
+
Legion::Cache::Local.available
|
|
122
|
+
rescue StandardError
|
|
123
|
+
0
|
|
40
124
|
end
|
|
41
125
|
|
|
42
126
|
private
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Cache
|
|
5
|
+
module RedisHash
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Returns true when the Redis driver is loaded and the connection pool is live.
|
|
9
|
+
def redis_available?
|
|
10
|
+
pool = Legion::Cache.instance_variable_get(:@client)
|
|
11
|
+
return false if pool.nil?
|
|
12
|
+
|
|
13
|
+
Legion::Cache.connected?
|
|
14
|
+
rescue StandardError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Set hash fields from a Ruby Hash.
|
|
19
|
+
# Uses Redis HSET key field value [field value ...]
|
|
20
|
+
def hset(key, hash)
|
|
21
|
+
return false unless redis_available?
|
|
22
|
+
|
|
23
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
24
|
+
flat = hash.flat_map { |k, v| [k.to_s, v.to_s] }
|
|
25
|
+
conn.hset(key, *flat)
|
|
26
|
+
end
|
|
27
|
+
true
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
log_redis_error('hset', e)
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns a Ruby Hash (string keys) of all field-value pairs for the key.
|
|
34
|
+
def hgetall(key)
|
|
35
|
+
return nil unless redis_available?
|
|
36
|
+
|
|
37
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
38
|
+
conn.hgetall(key)
|
|
39
|
+
end
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
log_redis_error('hgetall', e)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Delete one or more hash fields.
|
|
46
|
+
def hdel(key, *fields)
|
|
47
|
+
return 0 unless redis_available?
|
|
48
|
+
|
|
49
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
50
|
+
conn.hdel(key, *fields)
|
|
51
|
+
end
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
log_redis_error('hdel', e)
|
|
54
|
+
0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Add a member to a sorted set with the given score.
|
|
58
|
+
def zadd(key, score, member)
|
|
59
|
+
return false unless redis_available?
|
|
60
|
+
|
|
61
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
62
|
+
conn.zadd(key, score.to_f, member.to_s)
|
|
63
|
+
end
|
|
64
|
+
true
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
log_redis_error('zadd', e)
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Range query on a sorted set by score. Returns an array of members.
|
|
71
|
+
# limit: accepts [offset, count] array matching Redis LIMIT semantics.
|
|
72
|
+
def zrangebyscore(key, min, max, limit: nil)
|
|
73
|
+
return [] unless redis_available?
|
|
74
|
+
|
|
75
|
+
opts = {}
|
|
76
|
+
opts[:limit] = limit if limit
|
|
77
|
+
|
|
78
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
79
|
+
conn.zrangebyscore(key, min, max, **opts)
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
log_redis_error('zrangebyscore', e)
|
|
83
|
+
[]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Remove a member from a sorted set.
|
|
87
|
+
def zrem(key, member)
|
|
88
|
+
return false unless redis_available?
|
|
89
|
+
|
|
90
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
91
|
+
conn.zrem(key, member.to_s)
|
|
92
|
+
end
|
|
93
|
+
true
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
log_redis_error('zrem', e)
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Set a TTL (in seconds) on a key.
|
|
100
|
+
def expire(key, seconds)
|
|
101
|
+
return false unless redis_available?
|
|
102
|
+
|
|
103
|
+
Legion::Cache.instance_variable_get(:@client).with do |conn|
|
|
104
|
+
conn.expire(key, seconds.to_i) == 1
|
|
105
|
+
end
|
|
106
|
+
rescue StandardError => e
|
|
107
|
+
log_redis_error('expire', e)
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def log_redis_error(method, error)
|
|
112
|
+
return unless defined?(Legion::Logging)
|
|
113
|
+
|
|
114
|
+
Legion::Logging.warn "[cache:redis_hash] #{method} failed: #{error.class} — #{error.message}"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -25,6 +25,7 @@ module Legion
|
|
|
25
25
|
cache_nils: false,
|
|
26
26
|
pool_size: 10,
|
|
27
27
|
timeout: 5,
|
|
28
|
+
default_ttl: 60,
|
|
28
29
|
serializer: Legion::JSON,
|
|
29
30
|
cluster: nil,
|
|
30
31
|
replica: false,
|
|
@@ -50,6 +51,7 @@ module Legion
|
|
|
50
51
|
cache_nils: false,
|
|
51
52
|
pool_size: 5,
|
|
52
53
|
timeout: 3,
|
|
54
|
+
default_ttl: 60,
|
|
53
55
|
serializer: Legion::JSON,
|
|
54
56
|
username: nil,
|
|
55
57
|
password: nil,
|
data/lib/legion/cache/version.rb
CHANGED
data/lib/legion/cache.rb
CHANGED
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
|
+
version: 1.3.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -90,6 +90,8 @@ extra_rdoc_files:
|
|
|
90
90
|
- LICENSE
|
|
91
91
|
- README.md
|
|
92
92
|
files:
|
|
93
|
+
- ".github/CODEOWNERS"
|
|
94
|
+
- ".github/dependabot.yml"
|
|
93
95
|
- ".github/workflows/ci.yml"
|
|
94
96
|
- ".gitignore"
|
|
95
97
|
- ".rubocop.yml"
|
|
@@ -107,6 +109,7 @@ files:
|
|
|
107
109
|
- lib/legion/cache/memory.rb
|
|
108
110
|
- lib/legion/cache/pool.rb
|
|
109
111
|
- lib/legion/cache/redis.rb
|
|
112
|
+
- lib/legion/cache/redis_hash.rb
|
|
110
113
|
- lib/legion/cache/settings.rb
|
|
111
114
|
- lib/legion/cache/version.rb
|
|
112
115
|
- sonar-project.properties
|