From 29f5341cadd1d4a188aadf7fb2511bf809ca4303 Mon Sep 17 00:00:00 2001 From: Minseong Choi Date: Wed, 1 Jul 2026 00:17:35 +0900 Subject: [PATCH] docs(api): correct cooldownLimiter doc for its OTP reuse cooldownLimiter began as the wake-only throttle; the OTP-start hardening reused it via the atomic reserve/release. Its type comment still called it a per-server wake limiter and justified the per-replica behaviour as "acceptable because the operator reconcile is idempotent" -- true for wake, false for OTP, whose every admitted send is a non-idempotent email. Rewrite the comment to describe the shared per-key limiter and record the honest KNOWN-LIMITATION: the atomic reserve/release closes the intra-replica concurrent burst, but the in-memory map throttles per replica, so cross-replica bounding still needs a shared store. No behaviour change. --- internal/api/api.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index dfbc00f..85dfe0c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -379,11 +379,19 @@ func principalFromContext(ctx context.Context) *Principal { return nil } -// ---- wake cooldown ---- +// ---- per-key cooldown (wake + OTP) ---- -// cooldownLimiter is an in-memory per-server rate limiter for the wake lever. -// It is process-local; with multiple api replicas the effective cooldown is -// per-replica, which is acceptable because the operator reconcile is idempotent. +// cooldownLimiter is an in-memory per-key cooldown. It backs two throttles with +// separate keyspaces: the wake lever (key = server name, via allowed/record) and +// the email-OTP start (keys = principal and recipient, via the atomic +// reserve/release). It is process-local, so with multiple api replicas the +// effective cooldown is per-replica. For wake that is acceptable — the operator +// reconcile is idempotent, so a burst slipping through is harmless. For OTP it is a +// real KNOWN-LIMITATION: each admitted send is a non-idempotent email, so across N +// replicas a determined caller could draw up to N codes per window. The atomic +// reserve/release pair closes the intra-replica concurrent burst (the bug fixed in +// #35); cross-replica bounding would need a shared store (out of scope for the +// single-replica demo). type cooldownLimiter struct { mu sync.Mutex now func() time.Time