feat(cache-valkey): initial implementation — Provider, adapters (v1.0.0)
Introduces code.nochebuena.dev/einherjar/cache-valkey — the Valkey cache starter for the Einherjar framework. Absorbs the valkey package from micro-lib and adds three duck-typed adapters that wire directly into auth, web, and auth-jwt. Core: - Provider interface — Get, Set, Del, Exists, Expire, IncrWithTTL, Native() - Component interface — lifecycle.Component + observability.Checkable + Provider - Config struct (EINHERJAR_VALKEY_* env vars) - New(logger, cfg) Component — creates valkey-go client in OnInit; PING in OnStart; logs "valkey: connected" - IncrWithTTL implemented with Lua script (atomic INCR + conditional EXPIRE); race-free fixed-window semantics with no MULTI/EXEC overhead - Priority: LevelDegraded — Valkey outage degrades, does not halt the service Adapters (duck-typed — no import of auth, web, or auth-jwt): - PermissionCache — Get/Set int64 bitmasks as string; satisfies auth/rbac.Cache - RateLimiterStore — fixed-window via IncrWithTTL; satisfies web/mw.RateLimiterStore - Blacklist — Exists/Set "1" with TTL; satisfies auth-jwt.Blacklist Compliance test verifies CT-6, duck-type shape assignments, and full adapter behavioural coverage with a mockProvider (no live server required). - Component interface embeds observability.Identifiable; identifiable.go implements ModulePath and ModuleVersion via runtime/debug.ReadBuildInfo() — prints in launcher banner
This commit is contained in:
98
docs/adr/index.md
Normal file
98
docs/adr/index.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Architecture Decision Records — cache-valkey
|
||||
|
||||
## ADR-001: Flat package (no sub-packages)
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
All three adapters (PermissionCache, RateLimiterStore, Blacklist) wrap the same
|
||||
`Provider` object and cannot function without the connection. Splitting into
|
||||
sub-packages (e.g., `cachevalkey/permcache`) would add import verbosity without
|
||||
providing any isolation benefit — every caller still needs the `Component` from
|
||||
`New()` before constructing any adapter.
|
||||
|
||||
Consistent with `auth-jwt` (single concern, flat package).
|
||||
|
||||
---
|
||||
|
||||
## ADR-002: Provider interface wraps common ops; Native() is the escape hatch
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
`Provider` exposes six operations using only standard Go types (`string`, `bool`,
|
||||
`int64`, `time.Duration`). Callers that need operations beyond this set call
|
||||
`Native() vk.Client` to access the full valkey-go client. This means:
|
||||
|
||||
- Code that only uses Provider methods does not need to import `valkey-go`.
|
||||
- Code that calls `Native()` opts in to the valkey-go dependency explicitly.
|
||||
- The `Component` interface extends `Provider` — pass the result of `New()` wherever
|
||||
a `Provider` is expected.
|
||||
|
||||
Pattern reference: micro-lib `minio` uses the same `Client` + `Native()` architecture.
|
||||
|
||||
---
|
||||
|
||||
## ADR-003: Adapters take Provider, not vk.Client
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
Adapter constructors (`NewPermissionCache`, `NewRateLimiterStore`, `NewBlacklist`)
|
||||
accept `Provider` instead of `vk.Client`. Benefits:
|
||||
|
||||
1. **Testability**: adapter behavior is testable with a `mockProvider` in-process —
|
||||
no live Valkey server, no valkey-go mock library required.
|
||||
2. **Abstraction**: adapters depend only on the six Provider methods, not on the full
|
||||
client API. If the underlying driver changes, adapters are unaffected.
|
||||
3. **Duck-typed wiring**: in application code, `Component` satisfies `Provider`, so
|
||||
the result of `New()` is passed directly to adapter constructors:
|
||||
|
||||
```go
|
||||
vk := cachevalkey.New(logger, cfg)
|
||||
permCache := cachevalkey.NewPermissionCache(vk) // → auth/rbac.Cache
|
||||
rateLimiter := cachevalkey.NewRateLimiterStore(vk, time.Second, 100) // → web/mw.RateLimiterStore
|
||||
blacklist := cachevalkey.NewBlacklist(vk) // → auth-jwt.Blacklist
|
||||
```
|
||||
|
||||
No cast, no glue interface. Go's structural typing handles the assignment.
|
||||
|
||||
---
|
||||
|
||||
## ADR-004: IncrWithTTL uses Lua for atomic fixed-window rate limiting
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
The `IncrWithTTL` method implements the standard fixed-window counter via a Lua script:
|
||||
|
||||
```lua
|
||||
local count = redis.call('INCR', KEYS[1])
|
||||
if count == 1 then
|
||||
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
||||
end
|
||||
return count
|
||||
```
|
||||
|
||||
Lua scripts execute atomically in Valkey. The INCR + conditional EXPIRE pattern is
|
||||
race-free: the EXPIRE is set only on the first increment, preventing the window from
|
||||
being extended by racing requests.
|
||||
|
||||
Alternatives considered:
|
||||
- **MULTI/EXEC**: achieves atomicity but is heavier and not pipeline-friendly.
|
||||
- **True sliding window with sorted sets**: more accurate, but significantly higher
|
||||
memory and complexity for minimal real-world benefit in most rate-limiting scenarios.
|
||||
|
||||
---
|
||||
|
||||
## ADR-005: Health priority LevelDegraded
|
||||
|
||||
**Status:** Accepted
|
||||
|
||||
`Priority()` returns `observability.LevelDegraded`, not `LevelCritical`. A Valkey
|
||||
outage degrades — it does not halt — the service:
|
||||
|
||||
- Rate limiting (`RateLimiterStore`) fails open (web/mw contract).
|
||||
- Permission cache (`PermissionCache`) misses fall through to the base provider
|
||||
(rbac.CachedPermissionProvider degrades to uncached reads).
|
||||
- Blacklist (`Blacklist`) errors propagate to `authjwt.RefreshTokenPair`, which
|
||||
returns the error to the caller (token refresh fails — safe outcome).
|
||||
|
||||
No case requires Valkey to be available for the service to process requests.
|
||||
Consistent with micro-lib `valkey` (`health.LevelDegraded`).
|
||||
Reference in New Issue
Block a user