Add per-key 24h periods from a hashed reset offset

Claude-Session: https://claude.ai/code/session_01DeXuhSjq5Gta5qPJgSceFe
This commit is contained in:
binwiederhier committed 2026-10-30 20:15:19 +00:00
1 parent 5c2a1ddeef
commit 9280bc1e14
2 files changed
+73 -3

No files matched your search

+34 -3
View File
@@ -7,9 +7,12 @@ import (
)
const (
fnvOffset = 2166136261 // FNV-1a 32-bit offset basis
fnvPrime = 16777619 // FNV-1a 32-bit prime
oneDay = 24 * time.Hour
fnvOffset = 2166136261 // FNV-1a 32-bit offset basis
fnvPrime = 16777619 // FNV-1a 32-bit prime
fnvOffset64 = 14695981039346656037 // FNV-1a 64-bit offset basis
fnvPrime64 = 1099511628211 // FNV-1a 64-bit prime
oneDay = 24 * time.Hour
daySeconds = int64(oneDay / time.Second)
)
// shardIndex returns the shard a key belongs to (FNV-1a, no allocation)
@@ -31,3 +34,31 @@ func untilReset(now time.Time, resetTime time.Time) time.Duration {
}
return next.Sub(now)
}
// keyOffset returns the key's reset time of day (UTC). It is a hash of the key, so the resets of
// all keys are spread over the day; it is unseeded, so every node and every restart agree.
func keyOffset(key string) time.Duration {
h := uint64(fnvOffset64)
for i := 0; i < len(key); i++ {
h ^= uint64(key[i])
h *= fnvPrime64
}
return time.Duration(h%uint64(daySeconds)) * time.Second
}
// periodStart returns the unix second the key's current 24h period started at t: the latest
// day + keyOffset(key) at or before t. t is database time, so all nodes agree.
func periodStart(key string, t time.Time) int64 {
offset := int64(keyOffset(key) / time.Second)
since := t.Unix() - offset
days := since / daySeconds
if since < 0 && since%daySeconds != 0 {
days-- // Round toward the past, not toward zero
}
return days*daySeconds + offset
}
// periodEnd returns when the key's period that t falls in ends, i.e. when its daily counts reset
func periodEnd(key string, t time.Time) time.Time {
return time.Unix(periodStart(key, t)+daySeconds, 0).UTC()
}
+39
View File
@@ -1,6 +1,7 @@
package limit
import (
"fmt"
"testing"
"time"
@@ -29,3 +30,41 @@ func TestUntilReset(t *testing.T) {
now = time.Date(2026, 10, 25, 6, 0, 0, 0, time.UTC)
require.Equal(t, 30*time.Minute, untilReset(now, resetTime))
}
func TestKeyOffset_StableAndSpread(t *testing.T) {
// Pinned: every node and every restart must agree on a key's offset (FNV-1a 64, unseeded)
require.Equal(t, 62084*time.Second, keyOffset("ip:1.2.3.4")) // 17:14:44 UTC
require.Equal(t, 21643*time.Second, keyOffset("user:u_1")) // 06:00:43 UTC
require.Equal(t, 13706*time.Second, keyOffset("k")) // 03:48:26 UTC
// Spread: a thousand keys reset in every hour of the day, on whole seconds
hours := make(map[time.Duration]bool)
for i := 0; i < 1000; i++ {
offset := keyOffset(fmt.Sprintf("ip:10.0.%d.%d", i/256, i%256))
require.GreaterOrEqual(t, offset, time.Duration(0))
require.Less(t, offset, oneDay)
require.Zero(t, offset%time.Second)
hours[offset/time.Hour] = true
}
require.Len(t, hours, 24)
}
func TestPeriodStart(t *testing.T) {
// A key's period starts at its offset into the UTC day and lasts exactly 24 hours
start := time.Date(2026, 10, 25, 3, 48, 26, 0, time.UTC) // keyOffset("k")
require.Equal(t, start.Unix(), periodStart("k", start))
require.Equal(t, start.Unix(), periodStart("k", start.Add(oneDay-time.Second)))
require.Equal(t, start.Add(oneDay).Unix(), periodStart("k", start.Add(oneDay)))
require.Equal(t, start.Add(-oneDay).Unix(), periodStart("k", start.Add(-time.Second)))
require.Equal(t, start.Add(oneDay), periodEnd("k", start))
require.Equal(t, start.Add(oneDay), periodEnd("k", start.Add(oneDay-time.Second)))
// Midnight UTC is not a boundary for a key whose offset is not 0
midnight := time.Date(2026, 10, 26, 0, 0, 0, 0, time.UTC)
require.Equal(t, periodStart("k", midnight.Add(-time.Second)), periodStart("k", midnight))
// Another key resets at another time
noon := time.Date(2026, 10, 25, 12, 0, 0, 0, time.UTC)
require.Equal(t, time.Date(2026, 10, 25, 17, 14, 44, 0, time.UTC), periodEnd("ip:1.2.3.4", noon))
require.Equal(t, time.Date(2026, 10, 26, 3, 48, 26, 0, time.UTC), periodEnd("k", noon))
}