Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ require (
github.com/stretchr/testify v1.11.1
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
github.com/testcontainers/testcontainers-go v0.41.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0
github.com/ulule/limiter/v3 v3.11.2
Expand Down Expand Up @@ -93,6 +92,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.11.1 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
Expand Down Expand Up @@ -124,6 +124,7 @@ require (
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.2 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
Expand Down
28 changes: 24 additions & 4 deletions internal/bootstrap/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@ func initializeMetrics(cfg *config.Config) core.Recorder {
// cacheOpts holds the parameters needed to initialise any typed cache.
type cacheOpts struct {
cacheType string
cacheName string // Prometheus label for metrics (e.g. "token", "client")
keyPrefix string
clientTTL time.Duration
sizePerConn int
label string // human-readable name for log messages (e.g. "Metrics")
}

// initializeCache is a generic helper that creates a typed cache according to
// the supplied cacheOpts. All three cache-init call-sites delegate to this.
// the supplied cacheOpts. All cache-init call-sites delegate to this.
func initializeCache[T any](
ctx context.Context,
cfg *config.Config,
Expand All @@ -43,6 +44,9 @@ func initializeCache[T any](
ctx, cancel := context.WithTimeout(ctx, cfg.CacheInitTimeout)
defer cancel()

var underlyingCache core.Cache[T]
var closeFunc func() error

switch opts.cacheType {
case config.CacheTypeRedisAside:
c, err := cache.NewRueidisAsideCache[T](
Expand All @@ -61,7 +65,8 @@ func initializeCache[T any](
"%s cache: redis-aside (addr=%s, db=%d, client_ttl=%s, cache_size_per_conn=%dMB)",
opts.label, cfg.RedisAddr, cfg.RedisDB, opts.clientTTL, opts.sizePerConn,
)
return c, c.Close, nil
underlyingCache = c
closeFunc = c.Close

case config.CacheTypeRedis:
c, err := cache.NewRueidisCache[T](
Expand All @@ -73,13 +78,23 @@ func initializeCache[T any](
return nil, nil, fmt.Errorf("failed to initialize redis %s cache: %w", opts.label, err)
}
log.Printf("%s cache: redis (addr=%s, db=%d)", opts.label, cfg.RedisAddr, cfg.RedisDB)
return c, c.Close, nil
underlyingCache = c
closeFunc = c.Close

default: // memory
c := cache.NewMemoryCache[T]()
log.Printf("%s cache: memory (single instance only)", opts.label)
return c, c.Close, nil
underlyingCache = c
closeFunc = c.Close
}

// Wrap with instrumentation if metrics are enabled
if cfg.MetricsEnabled {
instrumentedCache := cache.NewInstrumentedCache(underlyingCache, opts.cacheName)
return instrumentedCache, instrumentedCache.Close, nil
}

return underlyingCache, closeFunc, nil
}

// initializeMetricsCache initializes the metrics cache based on configuration
Expand All @@ -92,6 +107,7 @@ func initializeMetricsCache(
}
return initializeCache[int64](ctx, cfg, cacheOpts{
cacheType: cfg.MetricsCacheType,
cacheName: "metrics",
keyPrefix: "authgate:metrics:",
clientTTL: cfg.MetricsCacheClientTTL,
sizePerConn: cfg.MetricsCacheSizePerConn,
Expand All @@ -106,6 +122,7 @@ func initializeClientCountCache(
) (core.Cache[int64], func() error, error) {
return initializeCache[int64](ctx, cfg, cacheOpts{
cacheType: cfg.ClientCountCacheType,
cacheName: "client_count",
keyPrefix: "authgate:client-count:",
clientTTL: cfg.ClientCountCacheClientTTL,
sizePerConn: cfg.ClientCountCacheSizePerConn,
Expand All @@ -124,6 +141,7 @@ func initializeTokenCache(
}
return initializeCache[models.AccessToken](ctx, cfg, cacheOpts{
cacheType: cfg.TokenCacheType,
cacheName: "token",
keyPrefix: "authgate:tokens:",
clientTTL: cfg.TokenCacheClientTTL,
sizePerConn: cfg.TokenCacheSizePerConn,
Expand All @@ -138,6 +156,7 @@ func initializeClientCache(
) (core.Cache[models.OAuthApplication], func() error, error) {
return initializeCache[models.OAuthApplication](ctx, cfg, cacheOpts{
cacheType: cfg.ClientCacheType,
cacheName: "client",
keyPrefix: "authgate:clients:",
clientTTL: cfg.ClientCacheClientTTL,
sizePerConn: cfg.ClientCacheSizePerConn,
Expand All @@ -152,6 +171,7 @@ func initializeUserCache(
) (core.Cache[models.User], func() error, error) {
return initializeCache[models.User](ctx, cfg, cacheOpts{
cacheType: cfg.UserCacheType,
cacheName: "user",
keyPrefix: "authgate:users:",
clientTTL: cfg.UserCacheClientTTL,
sizePerConn: cfg.UserCacheSizePerConn,
Expand Down
225 changes: 225 additions & 0 deletions internal/bootstrap/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package bootstrap

import (
"context"
"testing"
"time"

"github.com/go-authgate/authgate/internal/cache"
"github.com/go-authgate/authgate/internal/config"
)

func TestInitializeCache_WithInstrumentation(t *testing.T) {
cfg := &config.Config{
MetricsEnabled: true,
CacheInitTimeout: 5 * time.Second,
}

ctx := context.Background()
c, closeFunc, err := initializeCache[int64](ctx, cfg, cacheOpts{
cacheType: config.CacheTypeMemory,
cacheName: "test_with",
keyPrefix: "authgate:test:",
label: "Test",
})
if err != nil {
t.Fatalf("Failed to initialize cache: %v", err)
}
defer closeFunc()

// Verify the returned cache is an InstrumentedCache (wrapping happened)
if _, ok := c.(*cache.InstrumentedCache[int64]); !ok {
t.Errorf("Expected *InstrumentedCache, got %T", c)
}

// Verify it still works
_ = c.Set(ctx, "key1", int64(42), time.Minute)
value, err := c.Get(ctx, "key1")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if value != 42 {
t.Errorf("Expected 42, got %d", value)
}
}

func TestInitializeCache_NoInstrumentation(t *testing.T) {
cfg := &config.Config{
MetricsEnabled: false,
CacheInitTimeout: 5 * time.Second,
}

ctx := context.Background()
c, closeFunc, err := initializeCache[int64](ctx, cfg, cacheOpts{
cacheType: config.CacheTypeMemory,
cacheName: "test_without",
keyPrefix: "authgate:test:",
label: "Test",
})
if err != nil {
t.Fatalf("Failed to initialize cache: %v", err)
}
defer closeFunc()

// Verify the returned cache is NOT instrumented
if _, ok := c.(*cache.InstrumentedCache[int64]); ok {
t.Error("Expected raw MemoryCache, got InstrumentedCache")
}

_ = c.Set(ctx, "key1", int64(42), time.Minute)
value, err := c.Get(ctx, "key1")
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if value != 42 {
t.Errorf("Expected 42, got %d", value)
}
}

func TestInitializeTokenCache_Disabled(t *testing.T) {
cfg := &config.Config{
MetricsEnabled: true,
TokenCacheEnabled: false,
CacheInitTimeout: 5 * time.Second,
}

ctx := context.Background()
c, closeFunc, err := initializeTokenCache(ctx, cfg)
if err != nil {
t.Fatalf("Failed to initialize token cache: %v", err)
}
defer closeFunc()

// Disabled token cache returns a NoopCache — not wrapped since we removed
// the pointless instrumentation of noop (it would only show 100% miss rate)
if c == nil {
t.Fatal("Expected non-nil cache")
}

// NoopCache always returns cache miss
_, err = c.Get(ctx, "key1")
if err == nil {
t.Error("Expected cache miss from NoopCache, got nil error")
}
}

func TestInitializeAllCaches(t *testing.T) {
cfg := &config.Config{
MetricsEnabled: true,
MetricsGaugeUpdateEnabled: true,
TokenCacheEnabled: true,
CacheInitTimeout: 5 * time.Second,

TokenCacheType: config.CacheTypeMemory,
TokenCacheClientTTL: time.Hour,
TokenCacheSizePerConn: 32,
ClientCacheType: config.CacheTypeMemory,
ClientCacheClientTTL: 30 * time.Second,
ClientCacheSizePerConn: 32,
UserCacheType: config.CacheTypeMemory,
UserCacheClientTTL: 30 * time.Second,
UserCacheSizePerConn: 32,
MetricsCacheType: config.CacheTypeMemory,
MetricsCacheClientTTL: 30 * time.Second,
MetricsCacheSizePerConn: 32,
ClientCountCacheType: config.CacheTypeMemory,
ClientCountCacheClientTTL: 10 * time.Minute,
ClientCountCacheSizePerConn: 32,
}

ctx := context.Background()

closers := make([]func() error, 0)
addCloser := func(f func() error) {
if f != nil {
closers = append(closers, f)
}
}
defer func() {
for _, f := range closers {
_ = f()
}
}()

tc, f, err := initializeTokenCache(ctx, cfg)
if err != nil {
t.Fatalf("token cache: %v", err)
}
addCloser(f)
_ = tc // different type, just verify no error

cc, f, err := initializeClientCache(ctx, cfg)
if err != nil {
t.Fatalf("client cache: %v", err)
}
addCloser(f)
_ = cc

uc, f, err := initializeUserCache(ctx, cfg)
if err != nil {
t.Fatalf("user cache: %v", err)
}
addCloser(f)
_ = uc

mc, f, err := initializeMetricsCache(ctx, cfg)
if err != nil {
t.Fatalf("metrics cache: %v", err)
}
addCloser(f)
_ = mc

ccc, f, err := initializeClientCountCache(ctx, cfg)
if err != nil {
t.Fatalf("client count cache: %v", err)
}
addCloser(f)

// Sanity check: client count cache should work
_ = ccc.Set(ctx, "count1", int64(5), time.Minute)
value, err := ccc.Get(ctx, "count1")
if err != nil {
t.Errorf("Expected value from client count cache, got error: %v", err)
}
if value != 5 {
t.Errorf("Expected 5, got %d", value)
}
}

func TestInitializeCache_MemoryType(t *testing.T) {
for _, tc := range []struct {
name string
metricsEnabled bool
}{
{"with metrics", true},
{"without metrics", false},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := &config.Config{
MetricsEnabled: tc.metricsEnabled,
CacheInitTimeout: 5 * time.Second,
}

ctx := context.Background()
c, closeFunc, err := initializeCache[int64](ctx, cfg, cacheOpts{
cacheType: config.CacheTypeMemory,
cacheName: "test_mem",
keyPrefix: "authgate:test:",
label: "Test",
})
if err != nil {
t.Fatalf("Failed to initialize cache: %v", err)
}
defer closeFunc()

_ = c.Set(ctx, "key", int64(123), time.Minute)
value, err := c.Get(ctx, "key")
if err != nil {
t.Fatalf("Expected value, got error: %v", err)
}
if value != 123 {
t.Errorf("Expected 123, got %d", value)
}
})
}
}
Loading
Loading