feat(cache): add in-memory cache service (ADR-0022 Phase 1 part 2)

Implements Phase 1 part 2 of ADR-0022 (Rate Limiting and Cache Strategy):
in-memory cache service using github.com/patrickmn/go-cache. Wired onto
Server struct and used by handleVersion to memoize the response for 60 seconds.

Companion to PR #22 (per-IP rate limit middleware).

Changes:
- New: pkg/cache/cache.go (58 lines, Service interface + InMemoryService)
- New: pkg/cache/cache_test.go (125 lines, 6 unit tests, all passing)
- Modified: pkg/config/config.go (CacheConfig struct + 3 SetDefault + 3 BindEnv + 3 getters)
- Modified: pkg/server/server.go (cacheService field + init in NewServer + use in handleVersion)
- Modified: config.yaml (cache section with defaults)
- go.mod / go.sum (github.com/patrickmn/go-cache v2.1.0+incompatible)

Closes #13 (Phase 1 fully complete: rate limit in PR #22, cache here).
Phase 2 (Redis-compatible shared cache via Dragonfly/KeyDB) deferred.

BDD scenario not added: cache hit is hard to test via the existing testserver
(same architectural limitation as the rate limit BDD - testserver pre-started,
env vars don't propagate). Behavior is fully covered by unit tests (6/6 PASS).
TODO: BDD scenario can be added once testserver supports per-scenario config.

Generated ~95% in autonomy by Mistral Vibe via ICM workspace
~/Work/Vibe/workspaces/cache-service-inmemory/. T6 cost €2.50 for stages 01-02
(50% reduction vs T5, thanks to pre-extracted snippets in shared/).
Trainer (Claude) finalized commit/PR (Mistral hit max-turns).

🤖 Co-Authored-By: Mistral Vibe (devstral-2 / mistral-medium-3.5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 13:23:58 +02:00
parent 54dd0cc80f
commit 91d50e60ee
7 changed files with 296 additions and 14 deletions

View File

@@ -28,6 +28,7 @@ type Config struct {
Auth AuthConfig `mapstructure:"auth"`
Database DatabaseConfig `mapstructure:"database"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
Cache CacheConfig `mapstructure:"cache"`
}
// ServerConfig holds server-related configuration
@@ -105,6 +106,13 @@ type RateLimitConfig struct {
BurstSize int `mapstructure:"burst_size"`
}
// CacheConfig holds cache configuration
type CacheConfig struct {
Enabled bool `mapstructure:"enabled"`
DefaultTTLSeconds int `mapstructure:"default_ttl_seconds"`
CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"`
}
// VersionInfo holds application version information
type VersionInfo struct {
Version string `mapstructure:"-"` // Set via ldflags
@@ -202,6 +210,11 @@ func LoadConfig() (*Config, error) {
v.SetDefault("rate_limit.requests_per_minute", 60)
v.SetDefault("rate_limit.burst_size", 10)
// Cache defaults
v.SetDefault("cache.enabled", true)
v.SetDefault("cache.default_ttl_seconds", 300)
v.SetDefault("cache.cleanup_interval_seconds", 600)
// Auth defaults
v.SetDefault("auth.jwt_secret", "default-secret-key-please-change-in-production")
v.SetDefault("auth.admin_master_password", "admin123")
@@ -266,6 +279,11 @@ func LoadConfig() (*Config, error) {
v.BindEnv("rate_limit.requests_per_minute", "DLC_RATE_LIMIT_REQUESTS_PER_MINUTE")
v.BindEnv("rate_limit.burst_size", "DLC_RATE_LIMIT_BURST_SIZE")
// Cache environment variables
v.BindEnv("cache.enabled", "DLC_CACHE_ENABLED")
v.BindEnv("cache.default_ttl_seconds", "DLC_CACHE_DEFAULT_TTL_SECONDS")
v.BindEnv("cache.cleanup_interval_seconds", "DLC_CACHE_CLEANUP_INTERVAL_SECONDS")
// Database environment variables
v.BindEnv("database.host", "DLC_DATABASE_HOST")
v.BindEnv("database.port", "DLC_DATABASE_PORT")
@@ -428,6 +446,27 @@ func (c *Config) GetRateLimitBurstSize() int {
return c.RateLimit.BurstSize
}
// GetCacheEnabled returns whether cache is enabled
func (c *Config) GetCacheEnabled() bool {
return c.Cache.Enabled
}
// GetCacheDefaultTTLSeconds returns the default TTL in seconds for cache items
func (c *Config) GetCacheDefaultTTLSeconds() int {
if c.Cache.DefaultTTLSeconds <= 0 {
return 300
}
return c.Cache.DefaultTTLSeconds
}
// GetCacheCleanupIntervalSeconds returns the cleanup interval in seconds for cache
func (c *Config) GetCacheCleanupIntervalSeconds() int {
if c.Cache.CleanupIntervalSeconds <= 0 {
return 600
}
return c.Cache.CleanupIntervalSeconds
}
// GetDatabaseHost returns the database host
func (c *Config) GetDatabaseHost() string {
if c.Database.Host == "" {