Skip to content

Commit 08a91cf

Browse files
authored
perf(api): cache ProjectAuth in Redis and increase DB connection pool (#468)
Under high concurrency (50+ concurrent sessions), the auth middleware's per-request DB query on projects table caused connection pool saturation, leading to cascading timeouts (88 read timeouts, 8 gateway 504s). - Add Redis cache for project auth lookups (key: project:auth:{hmac}, TTL: 5min) with graceful fallback to DB on Redis errors - Increase DB maxOpen from 50→100 and maxIdle from 25→50
1 parent 340f4ae commit 08a91cf

4 files changed

Lines changed: 52 additions & 7 deletions

File tree

src/server/api/go/cmd/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func main() {
9898
engine := router.NewRouter(router.RouterDeps{
9999
Config: cfg,
100100
DB: db,
101+
Redis: rdb,
101102
Log: log,
102103
SessionHandler: sessionHandler,
103104
DiskHandler: diskHandler,

src/server/api/go/configs/config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ log:
1414

1515
database:
1616
dsn: "host=${DATABASE_HOST} user=${DATABASE_USER} password=${DATABASE_PASSWORD} dbname=${DATABASE_NAME} port=${DATABASE_EXPORT_PORT} sslmode=disable TimeZone=UTC"
17-
maxOpen: 50
18-
maxIdle: 25
17+
maxOpen: 100
18+
maxIdle: 50
1919
maxIdleTimeSec: 300
2020
autoMigrate: true
2121
enableTLS: ${DATABASE_ENABLE_TLS}

src/server/api/go/internal/middleware/auth.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package middleware
22

33
import (
4+
"context"
5+
"encoding/json"
46
"errors"
57
"net/http"
68
"strings"
9+
"time"
710

811
"github.com/gin-gonic/gin"
12+
"github.com/redis/go-redis/v9"
913
"go.opentelemetry.io/otel"
1014
"go.opentelemetry.io/otel/attribute"
1115
"go.opentelemetry.io/otel/trace"
@@ -18,8 +22,14 @@ import (
1822
"github.com/memodb-io/Acontext/internal/pkg/utils/tokens"
1923
)
2024

25+
const (
26+
projectAuthCachePrefix = "project:auth:"
27+
projectAuthCacheTTL = 5 * time.Minute
28+
)
29+
2130
// ProjectAuth returns a middleware that authenticates requests using project bearer tokens.
22-
func ProjectAuth(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
31+
// It caches project lookups in Redis to avoid hitting the database on every request.
32+
func ProjectAuth(cfg *config.Config, db *gorm.DB, rdb *redis.Client) gin.HandlerFunc {
2333
return func(c *gin.Context) {
2434
// Create auth span without propagating context to avoid nested span hierarchy
2535
authCtx, authSpan := otel.Tracer("middleware").Start(
@@ -47,8 +57,8 @@ func ProjectAuth(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
4757

4858
lookup := tokens.HMAC256Hex(cfg.Root.SecretPepper, secret)
4959

50-
var project model.Project
51-
if err := db.WithContext(authCtx).Where(&model.Project{SecretKeyHMAC: lookup}).First(&project).Error; err != nil {
60+
project, err := lookupProject(authCtx, db, rdb, lookup)
61+
if err != nil {
5262
if errors.Is(err, gorm.ErrRecordNotFound) {
5363
authSpan.SetAttributes(attribute.Bool("authenticated", false))
5464
authSpan.End()
@@ -88,8 +98,40 @@ func ProjectAuth(cfg *config.Config, db *gorm.DB) gin.HandlerFunc {
8898
)
8999
authSpan.End()
90100

91-
c.Set("project", &project)
101+
c.Set("project", project)
92102
SetWideEventField(c, "project_id", project.ID.String())
93103
c.Next()
94104
}
95105
}
106+
107+
// lookupProject tries Redis cache first, falls back to DB on miss or Redis error.
108+
func lookupProject(ctx context.Context, db *gorm.DB, rdb *redis.Client, hmac string) (*model.Project, error) {
109+
cacheKey := projectAuthCachePrefix + hmac
110+
111+
// Try Redis first
112+
if rdb != nil {
113+
data, err := rdb.Get(ctx, cacheKey).Bytes()
114+
if err == nil {
115+
var project model.Project
116+
if json.Unmarshal(data, &project) == nil {
117+
return &project, nil
118+
}
119+
}
120+
// On redis.Nil or any other error, fall through to DB
121+
}
122+
123+
// DB lookup
124+
var project model.Project
125+
if err := db.WithContext(ctx).Where(&model.Project{SecretKeyHMAC: hmac}).First(&project).Error; err != nil {
126+
return nil, err
127+
}
128+
129+
// Write-back to Redis (best-effort, don't block on failure)
130+
if rdb != nil {
131+
if data, err := json.Marshal(&project); err == nil {
132+
_ = rdb.Set(ctx, cacheKey, data, projectAuthCacheTTL).Err()
133+
}
134+
}
135+
136+
return &project, nil
137+
}

src/server/api/go/internal/router/router.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55

66
"github.com/gin-gonic/gin"
7+
"github.com/redis/go-redis/v9"
78
"go.uber.org/zap"
89
"gorm.io/gorm"
910

@@ -19,6 +20,7 @@ import (
1920
type RouterDeps struct {
2021
Config *config.Config
2122
DB *gorm.DB
23+
Redis *redis.Client
2224
Log *zap.Logger
2325
SessionHandler *handler.SessionHandler
2426
DiskHandler *handler.DiskHandler
@@ -62,7 +64,7 @@ func NewRouter(d RouterDeps) *gin.Engine {
6264
{
6365
projectAuth := d.ProjectAuthOverride
6466
if projectAuth == nil {
65-
projectAuth = middleware.ProjectAuth(d.Config, d.DB)
67+
projectAuth = middleware.ProjectAuth(d.Config, d.DB, d.Redis)
6668
}
6769
v1.Use(projectAuth)
6870

0 commit comments

Comments
 (0)