Fix grpcserver rate limiter: move to Server field

The package-level defaultRateLimiter drained its token bucket
across all test cases, causing later tests to hit ResourceExhausted.
Move rateLimiter from a package-level var to a *grpcRateLimiter field
on Server; New() allocates a fresh instance (10 req/s, burst 10) per
server. Each test's newTestEnv() constructs its own Server, so tests
no longer share limiter state.

Production behaviour is unchanged: a single Server is constructed at
startup and lives for the process lifetime.
This commit is contained in:
2026-03-11 19:20:32 -07:00
parent a80242ae3e
commit 4596ea08ab
12 changed files with 276 additions and 123 deletions

View File

@@ -53,23 +53,27 @@ func claimsFromContext(ctx context.Context) *token.Claims {
// Server holds the shared state for all gRPC service implementations.
type Server struct {
db *db.DB
cfg *config.Config
logger *slog.Logger
privKey ed25519.PrivateKey
pubKey ed25519.PublicKey
masterKey []byte
db *db.DB
cfg *config.Config
logger *slog.Logger
rateLimiter *grpcRateLimiter
privKey ed25519.PrivateKey
pubKey ed25519.PublicKey
masterKey []byte
}
// New creates a Server with the given dependencies (same as the REST Server).
// A fresh per-IP rate limiter (10 req/s, burst 10) is allocated per Server
// instance so that tests do not share state across test cases.
func New(database *db.DB, cfg *config.Config, priv ed25519.PrivateKey, pub ed25519.PublicKey, masterKey []byte, logger *slog.Logger) *Server {
return &Server{
db: database,
cfg: cfg,
privKey: priv,
pubKey: pub,
masterKey: masterKey,
logger: logger,
db: database,
cfg: cfg,
privKey: priv,
pubKey: pub,
masterKey: masterKey,
logger: logger,
rateLimiter: newGRPCRateLimiter(10, 10),
}
}
@@ -282,10 +286,6 @@ func (l *grpcRateLimiter) cleanup() {
}
}
// defaultRateLimiter is the server-wide rate limiter instance.
// 10 req/s sustained, burst 10 — same parameters as the REST limiter.
var defaultRateLimiter = newGRPCRateLimiter(10, 10)
// rateLimitInterceptor applies per-IP rate limiting using the same token-bucket
// parameters as the REST rate limiter (10 req/s, burst 10).
func (s *Server) rateLimitInterceptor(
@@ -304,7 +304,7 @@ func (s *Server) rateLimitInterceptor(
}
}
if ip != "" && !defaultRateLimiter.allow(ip) {
if ip != "" && !s.rateLimiter.allow(ip) {
return nil, status.Error(codes.ResourceExhausted, "rate limit exceeded")
}
return handler(ctx, req)