legion-cache 1.3.0 → 1.3.2
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/.gitignore +1 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +1 -0
- data/README.md +46 -1
- data/lib/legion/cache/cacheable.rb +123 -0
- data/lib/legion/cache/local.rb +1 -1
- data/lib/legion/cache/memcached.rb +12 -5
- data/lib/legion/cache/redis.rb +8 -2
- data/lib/legion/cache/settings.rb +23 -2
- data/lib/legion/cache/version.rb +1 -1
- data/lib/legion/cache.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0295f21258e4a7d824099be9c2c12a7c594d0db3e72dd8d5b5f823bb332e6869'
|
|
4
|
+
data.tar.gz: 5e581fc50c7bb13e52e9a38ae99a085af836ae21ec6d126d3a0d1fc6c79ad5aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc08f391040cab62c39795bd1d477f7d8e47ba4b5ffe3e88856957b829876d37ae06e15a48ff5469652e3c3574d5da529bccb7f00e9df162caadcc4d50fd93e1
|
|
7
|
+
data.tar.gz: 67717d9fd58c0669e319f0b0de94dee0d8178f17d78ba3fac2afc142660e26668b6700ccbb370b88274f1295d7ab534627768f981f3e1c4f98f9aa5ac86ae3db
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.2] - 2026-03-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Cache::Cacheable` module for transparent method-level caching
|
|
7
|
+
- `cache_method` DSL: declare cached methods with TTL, scope, and key exclusions
|
|
8
|
+
- `build_cache_key`: deterministic MD5-based cache keys from module path + method + filtered args
|
|
9
|
+
- `bypass_local_method_cache:` kwarg for force-refresh on cached methods
|
|
10
|
+
- In-memory fallback store with TTL expiry when no cache backend is available
|
|
11
|
+
- `memory_clear!` class method for test isolation
|
|
12
|
+
|
|
13
|
+
## [1.3.1] - 2026-03-20
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `Settings.normalize_driver` — maps `:memcached`, `:dalli`, `:redis` to internal gem names
|
|
17
|
+
- `Settings.resolve_servers` — merges `server:` (string) and `servers:` (array), injects default port per driver (memcached: 11211, redis: 6379), deduplicates
|
|
18
|
+
- `Settings::DEFAULT_PORTS` constant for driver default ports
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Redis driver now uses configured `server:`/`servers:` instead of hardcoded localhost
|
|
22
|
+
- Memcached driver accepts `server:` (singular) in addition to `servers:` (plural)
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `Settings.default` and `Settings.local` use `resolve_servers` for driver-aware server defaults
|
|
26
|
+
- Driver selection in `cache.rb` and `local.rb` uses `normalize_driver` for consistent name handling
|
|
27
|
+
|
|
3
28
|
## [1.3.0] - 2026-03-16
|
|
4
29
|
|
|
5
30
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -8,6 +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.0
|
|
11
12
|
**License**: Apache-2.0
|
|
12
13
|
|
|
13
14
|
## Architecture
|
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.2
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -80,6 +80,24 @@ Local uses a separate namespace (`legion_local`) and independent connection pool
|
|
|
80
80
|
|
|
81
81
|
The driver is auto-detected at load time: prefers `dalli` (Memcached) if available, falls back to `redis`. Override with `"driver": "redis"` and update `servers` to point at your Redis instance.
|
|
82
82
|
|
|
83
|
+
### Driver Names
|
|
84
|
+
|
|
85
|
+
Supported driver names: `memcached` (or `dalli`), `redis`. All names are normalized internally — `"memcached"` and `"dalli"` are equivalent.
|
|
86
|
+
|
|
87
|
+
### Server Resolution
|
|
88
|
+
|
|
89
|
+
Both `server` (singular string) and `servers` (array) are accepted and merged. Default ports are injected per driver when omitted: 11211 for memcached, 6379 for redis. Duplicates are removed.
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"cache": {
|
|
94
|
+
"driver": "memcached",
|
|
95
|
+
"server": "10.0.0.5",
|
|
96
|
+
"servers": ["10.0.0.6", "10.0.0.7:22122"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
83
101
|
### Memcached notes
|
|
84
102
|
|
|
85
103
|
- `value_max_bytes` defaults to **8MB**. Dalli enforces a 1MB client-side limit by default, which silently rejects large values. This default overrides that. Your Memcached server should also be started with `-I 8m` to match.
|
|
@@ -99,6 +117,33 @@ The driver is auto-detected at load time: prefers `dalli` (Memcached) if availab
|
|
|
99
117
|
|
|
100
118
|
Override via `Legion::Settings[:cache_local]`.
|
|
101
119
|
|
|
120
|
+
## Method Caching
|
|
121
|
+
|
|
122
|
+
Runner modules can use `cache_method` to transparently cache method results with TTL:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
module Runners::Presence
|
|
126
|
+
extend Legion::Cache::Cacheable
|
|
127
|
+
|
|
128
|
+
cache_method :get_presence, ttl: 300, exclude_from_key: [:token]
|
|
129
|
+
|
|
130
|
+
def get_presence(user_id: 'me', **)
|
|
131
|
+
conn = graph_connection(**)
|
|
132
|
+
response = conn.get("#{user_path(user_id)}/presence")
|
|
133
|
+
{ availability: response.body['availability'], activity: response.body['activity'] }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Every caller of `get_presence` gets cached results for 5 minutes. Use `bypass_local_method_cache: true` to force-refresh:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
runner.get_presence(user_id: 'me') # cached
|
|
142
|
+
runner.get_presence(user_id: 'me', bypass_local_method_cache: true) # fresh
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Options: `ttl:` (seconds), `scope:` (`:local` or `:global`), `exclude_from_key:` (args to ignore in cache key). Falls back to in-memory store when no cache backend is available.
|
|
146
|
+
|
|
102
147
|
## Pool API
|
|
103
148
|
|
|
104
149
|
```ruby
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Cache
|
|
7
|
+
module Cacheable
|
|
8
|
+
def self.extended(base)
|
|
9
|
+
base.instance_variable_set(:@cached_methods, {})
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def cached_methods
|
|
13
|
+
@cached_methods ||= {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def cache_method(method_name, ttl:, scope: :local, exclude_from_key: [])
|
|
17
|
+
exclude_from_key |= %i[token bypass_local_method_cache]
|
|
18
|
+
cached_methods[method_name] = { ttl: ttl, scope: scope, exclude_from_key: exclude_from_key }
|
|
19
|
+
|
|
20
|
+
mod_name = name || 'Anonymous'
|
|
21
|
+
config = cached_methods[method_name]
|
|
22
|
+
|
|
23
|
+
wrapper = Module.new do
|
|
24
|
+
define_method(method_name) do |bypass_local_method_cache: false, **kwargs|
|
|
25
|
+
key = Legion::Cache::Cacheable.build_cache_key(
|
|
26
|
+
mod_name, method_name, exclude: config[:exclude_from_key], **kwargs
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
unless bypass_local_method_cache
|
|
30
|
+
cached = Legion::Cache::Cacheable.cache_read(key, scope: config[:scope])
|
|
31
|
+
return cached unless cached.nil?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
result = super(**kwargs)
|
|
35
|
+
Legion::Cache::Cacheable.cache_write(key, result, ttl: config[:ttl], scope: config[:scope])
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
prepend wrapper
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.build_cache_key(mod_name, method_name, exclude:, **kwargs)
|
|
44
|
+
filtered = kwargs.except(*exclude)
|
|
45
|
+
args_hash = Digest::MD5.hexdigest(filtered.sort.to_s)
|
|
46
|
+
"#{mod_name}.#{method_name}.#{args_hash}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.cache_read(key, scope:)
|
|
50
|
+
case scope
|
|
51
|
+
when :global
|
|
52
|
+
return Legion::Cache.get(key) if global_cache_available?
|
|
53
|
+
|
|
54
|
+
memory_read(key)
|
|
55
|
+
else
|
|
56
|
+
local_cache_read(key) || memory_read(key)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.cache_write(key, value, ttl:, scope:)
|
|
61
|
+
case scope
|
|
62
|
+
when :global
|
|
63
|
+
if global_cache_available?
|
|
64
|
+
Legion::Cache.set(key, value, ttl)
|
|
65
|
+
else
|
|
66
|
+
memory_write(key, value, ttl)
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
if local_cache_available?
|
|
70
|
+
local_cache_write(key, value, ttl)
|
|
71
|
+
else
|
|
72
|
+
memory_write(key, value, ttl)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.global_cache_available?
|
|
78
|
+
defined?(Legion::Cache) && Legion::Cache.respond_to?(:connected?) && Legion::Cache.connected?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.local_cache_available?
|
|
82
|
+
defined?(Legion::Cache::Local) && Legion::Cache::Local.respond_to?(:connected?) && Legion::Cache::Local.connected?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.local_cache_read(key)
|
|
86
|
+
return nil unless local_cache_available?
|
|
87
|
+
|
|
88
|
+
Legion::Cache::Local.get(key)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.local_cache_write(key, value, ttl)
|
|
94
|
+
return unless local_cache_available?
|
|
95
|
+
|
|
96
|
+
Legion::Cache::Local.set(key, value, ttl)
|
|
97
|
+
rescue StandardError
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# In-memory fallback store (class-level, process-wide)
|
|
102
|
+
def self.memory_store
|
|
103
|
+
@memory_store ||= {}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.memory_read(key)
|
|
107
|
+
entry = memory_store[key]
|
|
108
|
+
return nil unless entry
|
|
109
|
+
return nil if Time.now.utc > entry[:expires_at]
|
|
110
|
+
|
|
111
|
+
entry[:value]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.memory_write(key, value, ttl)
|
|
115
|
+
memory_store[key] = { value: value, expires_at: Time.now.utc + ttl }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.memory_clear!
|
|
119
|
+
@memory_store = {}
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/legion/cache/local.rb
CHANGED
|
@@ -9,18 +9,25 @@ module Legion
|
|
|
9
9
|
include Legion::Cache::Pool
|
|
10
10
|
extend self # rubocop:disable Style/ModuleFunction
|
|
11
11
|
|
|
12
|
-
def client(servers:
|
|
12
|
+
def client(server: nil, servers: nil, **opts)
|
|
13
13
|
return @client unless @client.nil?
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
settings = defined?(Legion::Settings) ? Legion::Settings[:cache] : {}
|
|
16
|
+
servers ||= settings[:servers] || []
|
|
17
|
+
|
|
18
|
+
@pool_size = opts.key?(:pool_size) ? opts[:pool_size] : settings[:pool_size] || 10
|
|
19
|
+
@timeout = opts.key?(:timeout) ? opts[:timeout] : settings[:timeout] || 5
|
|
20
|
+
|
|
21
|
+
resolved = Legion::Cache::Settings.resolve_servers(
|
|
22
|
+
driver: 'memcached', server: server, servers: Array(servers)
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
Dalli.logger = Legion::Logging
|
|
19
|
-
cache_opts =
|
|
26
|
+
cache_opts = settings.merge(opts)
|
|
20
27
|
cache_opts[:value_max_bytes] ||= 8 * 1024 * 1024
|
|
21
28
|
|
|
22
29
|
@client = ConnectionPool.new(size: pool_size, timeout: timeout) do
|
|
23
|
-
Dalli::Client.new(
|
|
30
|
+
Dalli::Client.new(resolved, cache_opts)
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
@connected = true
|
data/lib/legion/cache/redis.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'redis'
|
|
4
4
|
require 'legion/cache/pool'
|
|
5
|
+
require 'legion/cache/settings'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
7
8
|
module Cache
|
|
@@ -9,14 +10,19 @@ module Legion
|
|
|
9
10
|
include Legion::Cache::Pool
|
|
10
11
|
extend self # rubocop:disable Style/ModuleFunction
|
|
11
12
|
|
|
12
|
-
def client(pool_size: 20, timeout: 5, **)
|
|
13
|
+
def client(pool_size: 20, timeout: 5, server: nil, servers: [], **)
|
|
13
14
|
return @client unless @client.nil?
|
|
14
15
|
|
|
15
16
|
@pool_size = pool_size
|
|
16
17
|
@timeout = timeout
|
|
17
18
|
|
|
19
|
+
resolved = Legion::Cache::Settings.resolve_servers(
|
|
20
|
+
driver: 'redis', server: server, servers: servers
|
|
21
|
+
)
|
|
22
|
+
host, port = resolved.first.split(':')
|
|
23
|
+
|
|
18
24
|
@client = ConnectionPool.new(size: pool_size, timeout: timeout) do
|
|
19
|
-
::Redis.new
|
|
25
|
+
::Redis.new(host: host, port: port.to_i)
|
|
20
26
|
end
|
|
21
27
|
@connected = true
|
|
22
28
|
@client
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
def self.default
|
|
15
15
|
{
|
|
16
16
|
driver: driver,
|
|
17
|
-
servers:
|
|
17
|
+
servers: resolve_servers(driver: driver),
|
|
18
18
|
connected: false,
|
|
19
19
|
enabled: true,
|
|
20
20
|
namespace: 'legion',
|
|
@@ -32,7 +32,7 @@ module Legion
|
|
|
32
32
|
def self.local
|
|
33
33
|
{
|
|
34
34
|
driver: driver,
|
|
35
|
-
servers:
|
|
35
|
+
servers: resolve_servers(driver: driver),
|
|
36
36
|
connected: false,
|
|
37
37
|
enabled: true,
|
|
38
38
|
namespace: 'legion_local',
|
|
@@ -47,6 +47,27 @@ module Legion
|
|
|
47
47
|
}
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
DEFAULT_PORTS = { 'dalli' => 11_211, 'redis' => 6379 }.freeze
|
|
51
|
+
|
|
52
|
+
def self.resolve_servers(driver:, server: nil, servers: [], port: nil)
|
|
53
|
+
gem_driver = normalize_driver(driver)
|
|
54
|
+
port ||= DEFAULT_PORTS.fetch(gem_driver, 11_211)
|
|
55
|
+
|
|
56
|
+
all = Array(servers) + Array(server)
|
|
57
|
+
all = ["127.0.0.1:#{port}"] if all.empty?
|
|
58
|
+
|
|
59
|
+
all.map! { |s| s.include?(':') ? s : "#{s}:#{port}" }
|
|
60
|
+
all.uniq
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.normalize_driver(name)
|
|
64
|
+
case name.to_s
|
|
65
|
+
when 'redis' then 'redis'
|
|
66
|
+
when 'memcached', 'dalli' then 'dalli'
|
|
67
|
+
else name.to_s
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
50
71
|
def self.driver(prefer = 'dalli')
|
|
51
72
|
secondary = prefer == 'dalli' ? 'redis' : 'dalli'
|
|
52
73
|
if Gem::Specification.find_all_by_name(prefer).any?
|
data/lib/legion/cache/version.rb
CHANGED
data/lib/legion/cache.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'legion/cache/version'
|
|
4
4
|
require 'legion/cache/settings'
|
|
5
|
+
require 'legion/cache/cacheable'
|
|
5
6
|
|
|
6
7
|
require 'legion/cache/memcached'
|
|
7
8
|
require 'legion/cache/redis'
|
|
@@ -9,7 +10,7 @@ require 'legion/cache/local'
|
|
|
9
10
|
|
|
10
11
|
module Legion
|
|
11
12
|
module Cache
|
|
12
|
-
if Legion::Settings[:cache][:driver] == 'redis'
|
|
13
|
+
if Legion::Cache::Settings.normalize_driver(Legion::Settings[:cache][:driver]) == 'redis'
|
|
13
14
|
extend Legion::Cache::Redis
|
|
14
15
|
else
|
|
15
16
|
extend Legion::Cache::Memcached
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -100,6 +100,7 @@ files:
|
|
|
100
100
|
- README.md
|
|
101
101
|
- legion-cache.gemspec
|
|
102
102
|
- lib/legion/cache.rb
|
|
103
|
+
- lib/legion/cache/cacheable.rb
|
|
103
104
|
- lib/legion/cache/local.rb
|
|
104
105
|
- lib/legion/cache/memcached.rb
|
|
105
106
|
- lib/legion/cache/pool.rb
|