✨ 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:
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
httpSwagger "github.com/swaggo/http-swagger"
|
||||
|
||||
"dance-lessons-coach/pkg/cache"
|
||||
"dance-lessons-coach/pkg/config"
|
||||
"dance-lessons-coach/pkg/greet"
|
||||
"dance-lessons-coach/pkg/middleware"
|
||||
@@ -65,6 +66,7 @@ type Server struct {
|
||||
validator *validation.Validator
|
||||
userRepo user.UserRepository
|
||||
userService user.UserService
|
||||
cacheService cache.Service
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
@@ -83,15 +85,28 @@ func NewServer(cfg *config.Config, readyCtx context.Context) *Server {
|
||||
log.Warn().Err(err).Msg("Failed to initialize user services, user functionality will be disabled")
|
||||
}
|
||||
|
||||
// Initialize cache service
|
||||
var cacheService cache.Service
|
||||
if cfg.GetCacheEnabled() {
|
||||
cacheService = cache.NewInMemoryService(
|
||||
time.Duration(cfg.GetCacheDefaultTTLSeconds())*time.Second,
|
||||
time.Duration(cfg.GetCacheCleanupIntervalSeconds())*time.Second,
|
||||
)
|
||||
log.Trace().Msg("Cache service initialized")
|
||||
} else {
|
||||
log.Trace().Msg("Cache service disabled")
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
router: chi.NewRouter(),
|
||||
readyCtx: readyCtx,
|
||||
withOTEL: cfg.GetTelemetryEnabled(),
|
||||
config: cfg,
|
||||
validator: validator,
|
||||
userRepo: userRepo,
|
||||
userService: userService,
|
||||
startedAt: time.Now(),
|
||||
router: chi.NewRouter(),
|
||||
readyCtx: readyCtx,
|
||||
withOTEL: cfg.GetTelemetryEnabled(),
|
||||
config: cfg,
|
||||
validator: validator,
|
||||
userRepo: userRepo,
|
||||
userService: userService,
|
||||
cacheService: cacheService,
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
s.setupRoutes()
|
||||
return s
|
||||
@@ -351,26 +366,49 @@ func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||
format = "plain" // default format
|
||||
}
|
||||
|
||||
// Check cache if enabled
|
||||
cacheKey := "version:" + format
|
||||
if s.cacheService != nil {
|
||||
if cached, ok := s.cacheService.Get(cacheKey); ok {
|
||||
log.Trace().Str("cache_key", cacheKey).Msg("Cache hit for version")
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if format == "json" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
w.Write([]byte(cached.(string)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
var response string
|
||||
switch format {
|
||||
case "plain":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte(version.Short()))
|
||||
response = version.Short()
|
||||
case "full":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte(version.Full()))
|
||||
response = version.Full()
|
||||
case "json":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
jsonResponse := fmt.Sprintf(`{
|
||||
response = fmt.Sprintf(`{
|
||||
"version": "%s",
|
||||
"commit": "%s",
|
||||
"built": "%s",
|
||||
"go": "%s"
|
||||
}`, version.Version, version.Commit, version.Date, version.GoVersion)
|
||||
w.Write([]byte(jsonResponse))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte(version.Short()))
|
||||
response = version.Short()
|
||||
}
|
||||
|
||||
// Cache the response for 60 seconds if cache is enabled
|
||||
if s.cacheService != nil {
|
||||
s.cacheService.Set(cacheKey, response, 60*time.Second)
|
||||
log.Trace().Str("cache_key", cacheKey).Msg("Cached version response")
|
||||
}
|
||||
|
||||
w.Write([]byte(response))
|
||||
}
|
||||
|
||||
// HealthzResponse represents the Kubernetes-style health check response
|
||||
|
||||
Reference in New Issue
Block a user