mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-19 12:16:28 +00:00
feat(cache): add build-tag-gated Redis cache backend
Add optional Redis cache support via `go build -tags redis`, following the same paired-stub pattern as OTel and Tailscale. The Cache[V] interface is unchanged; Redis and in-memory implementations are injected at startup without altering usage logic. - Add RedisCache[V] implementation with JSON serialization, fail-open on errors - Add gateway_redis.go / gateway_redis_noop.go paired wiring files - Refactor GroupWriterCache and ContextFileInterceptor to accept injected caches - Add GOCLAW_REDIS_DSN env var, docker-compose.redis.yml overlay - Update Dockerfile and GitHub Actions with ENABLE_REDIS build arg - Add Redis variant to CI matrix (5 variants: latest, otel, tsnet, redis, full)
This commit is contained in:
1 parent
7ea27c4c16
commit
0d3230b2bf
17 files changed
+464
-61
No files matched your search
+5
-1
@@ -346,6 +346,10 @@ func runGateway() {
|
||||
initOTelExporter(context.Background(), cfg, traceCollector)
|
||||
}
|
||||
|
||||
// Redis cache: compiled via build tags. Build with 'go build -tags redis' to enable.
|
||||
redisClient := initRedisClient(cfg)
|
||||
defer shutdownRedis(redisClient)
|
||||
|
||||
// Wire cron retry config from config.json
|
||||
cronRetryCfg := cfg.Cron.ToRetryConfig()
|
||||
// Apply retry config via type assertion on the concrete cron store.
|
||||
@@ -575,7 +579,7 @@ func runGateway() {
|
||||
}
|
||||
|
||||
var mcpPool *mcpbridge.Pool
|
||||
contextFileInterceptor, delegateMgr, mcpPool = wireExtras(pgStores, agentRouter, providerRegistry, msgBus, pgStores.Sessions, toolsReg, toolPE, skillsLoader, hasMemory, traceCollector, workspace, cfg.Gateway.InjectionAction, cfg, sandboxMgr, dynamicLoader)
|
||||
contextFileInterceptor, delegateMgr, mcpPool = wireExtras(pgStores, agentRouter, providerRegistry, msgBus, pgStores.Sessions, toolsReg, toolPE, skillsLoader, hasMemory, traceCollector, workspace, cfg.Gateway.InjectionAction, cfg, sandboxMgr, dynamicLoader, redisClient)
|
||||
if mcpPool != nil {
|
||||
defer mcpPool.Stop()
|
||||
}
|
||||
|
||||
@@ -44,18 +44,22 @@ func wireExtras(
|
||||
appCfg *config.Config,
|
||||
sandboxMgr sandbox.Manager,
|
||||
dynamicLoader *tools.DynamicToolLoader,
|
||||
redisClient any, // nil when built without -tags redis or when Redis is unconfigured
|
||||
) (*tools.ContextFileInterceptor, *tools.DelegateManager, *mcpbridge.Pool) {
|
||||
// 1. Context file interceptor (created before resolver so callbacks can reference it)
|
||||
// 1. Build cache instances (in-memory or Redis depending on build tags)
|
||||
agentCtxCache, userCtxCache, gwCache := makeCaches(redisClient)
|
||||
|
||||
// 1a. Context file interceptor (created before resolver so callbacks can reference it)
|
||||
var contextFileInterceptor *tools.ContextFileInterceptor
|
||||
var delegateMgr *tools.DelegateManager
|
||||
if stores.Agents != nil {
|
||||
contextFileInterceptor = tools.NewContextFileInterceptor(stores.Agents, workspace)
|
||||
contextFileInterceptor = tools.NewContextFileInterceptor(stores.Agents, workspace, agentCtxCache, userCtxCache)
|
||||
}
|
||||
|
||||
// 1b. Group writer cache (wraps ListGroupFileWriters with TTL cache)
|
||||
var groupWriterCache *store.GroupWriterCache
|
||||
if stores.Agents != nil {
|
||||
groupWriterCache = store.NewGroupWriterCache(stores.Agents)
|
||||
groupWriterCache = store.NewGroupWriterCache(stores.Agents, gwCache)
|
||||
}
|
||||
|
||||
// 2. User seeding callback: seeds per-user context files on first chat
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//go:build redis
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/cache"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// initRedisClient creates a Redis client when built with -tags redis.
|
||||
// Returns nil (typed as any) if GOCLAW_REDIS_DSN is empty or connection fails.
|
||||
func initRedisClient(cfg *config.Config) any {
|
||||
dsn := cfg.Database.RedisDSN
|
||||
if dsn == "" {
|
||||
slog.Debug("Redis available but not configured (set GOCLAW_REDIS_DSN)")
|
||||
return nil
|
||||
}
|
||||
client, err := cache.NewRedisClient(dsn)
|
||||
if err != nil {
|
||||
slog.Warn("Redis connection failed, falling back to in-memory cache", "error", err)
|
||||
return nil
|
||||
}
|
||||
slog.Info("Redis cache connected")
|
||||
return client
|
||||
}
|
||||
|
||||
// makeCaches creates typed cache instances backed by Redis (or in-memory if client is nil).
|
||||
func makeCaches(raw any) (
|
||||
agentCtxCache cache.Cache[[]store.AgentContextFileData],
|
||||
userCtxCache cache.Cache[[]store.AgentContextFileData],
|
||||
groupWriterCache cache.Cache[[]store.GroupFileWriterData],
|
||||
) {
|
||||
client, _ := raw.(*redis.Client)
|
||||
if client == nil {
|
||||
slog.Info("cache backend: in-memory (Redis not connected)")
|
||||
return cache.NewInMemoryCache[[]store.AgentContextFileData](),
|
||||
cache.NewInMemoryCache[[]store.AgentContextFileData](),
|
||||
cache.NewInMemoryCache[[]store.GroupFileWriterData]()
|
||||
}
|
||||
slog.Info("cache backend: redis")
|
||||
return cache.NewRedisCache[[]store.AgentContextFileData](client, "ctx:agent"),
|
||||
cache.NewRedisCache[[]store.AgentContextFileData](client, "ctx:user"),
|
||||
cache.NewRedisCache[[]store.GroupFileWriterData](client, "grp:writers")
|
||||
}
|
||||
|
||||
// shutdownRedis closes the Redis client connection.
|
||||
func shutdownRedis(raw any) {
|
||||
if client, ok := raw.(*redis.Client); ok && client != nil {
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build !redis
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/nextlevelbuilder/goclaw/internal/cache"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/config"
|
||||
"github.com/nextlevelbuilder/goclaw/internal/store"
|
||||
)
|
||||
|
||||
// initRedisClient is a no-op when built without the "redis" tag.
|
||||
// Build with `go build -tags redis` to enable Redis cache.
|
||||
func initRedisClient(_ *config.Config) any { return nil }
|
||||
|
||||
// makeCaches returns in-memory cache instances when Redis is not compiled in.
|
||||
func makeCaches(_ any) (
|
||||
agentCtxCache cache.Cache[[]store.AgentContextFileData],
|
||||
userCtxCache cache.Cache[[]store.AgentContextFileData],
|
||||
groupWriterCache cache.Cache[[]store.GroupFileWriterData],
|
||||
) {
|
||||
slog.Info("cache backend: in-memory")
|
||||
return cache.NewInMemoryCache[[]store.AgentContextFileData](),
|
||||
cache.NewInMemoryCache[[]store.AgentContextFileData](),
|
||||
cache.NewInMemoryCache[[]store.GroupFileWriterData]()
|
||||
}
|
||||
|
||||
// shutdownRedis is a no-op when built without the "redis" tag.
|
||||
func shutdownRedis(_ any) {}
|
||||
Reference in new issue
Block a user