feat(loadtest): sync add real 500-session capacity harness
This commit is contained in:
parent
ac0566f779
commit
141f2f20c4
39 changed files with 4157 additions and 42 deletions
|
|
@ -58,21 +58,32 @@ func (l *RateLimiter) AllowN(ctx context.Context, key string, cost, limit int, w
|
|||
if err != nil {
|
||||
return false, 0, fmt.Errorf("redis increment rate limit: %w", err)
|
||||
}
|
||||
items, ok := value.([]interface{})
|
||||
if !ok || len(items) != 2 {
|
||||
return false, 0, fmt.Errorf("redis increment rate limit: unexpected result %T", value)
|
||||
}
|
||||
count, countOK := items[0].(int64)
|
||||
ttlMillis, ttlOK := items[1].(int64)
|
||||
if !countOK || !ttlOK || ttlMillis <= 0 {
|
||||
return false, 0, fmt.Errorf("redis increment rate limit: invalid result %#v", items)
|
||||
count, ttlMillis, err := decodeRateLimitIncrementResult(value)
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
if count <= int64(limit) {
|
||||
return true, 0, nil
|
||||
}
|
||||
// Redis PTTL returns 0 when less than one millisecond remains. That is a
|
||||
// valid fixed-window boundary, not a corrupt result. Round it up to the
|
||||
// smallest protocol-safe FLOOD_WAIT instead of leaking a transient 500.
|
||||
retry := (ttlMillis + 999) / 1000
|
||||
if retry <= 0 {
|
||||
retry = 1
|
||||
}
|
||||
return false, int(retry), nil
|
||||
}
|
||||
|
||||
func decodeRateLimitIncrementResult(value any) (count int64, ttlMillis int64, err error) {
|
||||
items, ok := value.([]interface{})
|
||||
if !ok || len(items) != 2 {
|
||||
return 0, 0, fmt.Errorf("redis increment rate limit: unexpected result %T", value)
|
||||
}
|
||||
count, countOK := items[0].(int64)
|
||||
ttlMillis, ttlOK := items[1].(int64)
|
||||
if !countOK || !ttlOK || count <= 0 || ttlMillis < 0 {
|
||||
return 0, 0, fmt.Errorf("redis increment rate limit: invalid result %#v", items)
|
||||
}
|
||||
return count, ttlMillis, nil
|
||||
}
|
||||
|
|
|
|||
32
internal/store/redisstore/ratelimit_test.go
Normal file
32
internal/store/redisstore/ratelimit_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package redisstore
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecodeRateLimitIncrementResultAcceptsPTTLBoundary(t *testing.T) {
|
||||
count, ttlMillis, err := decodeRateLimitIncrementResult([]interface{}{int64(7), int64(0)})
|
||||
if err != nil {
|
||||
t.Fatalf("decode zero PTTL: %v", err)
|
||||
}
|
||||
if count != 7 || ttlMillis != 0 {
|
||||
t.Fatalf("decoded count=%d ttl=%d, want 7/0", count, ttlMillis)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeRateLimitIncrementResultRejectsInvalidShape(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
}{
|
||||
{name: "wrong type", value: "7,1"},
|
||||
{name: "wrong length", value: []interface{}{int64(7)}},
|
||||
{name: "zero count", value: []interface{}{int64(0), int64(1)}},
|
||||
{name: "negative ttl", value: []interface{}{int64(7), int64(-1)}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if _, _, err := decodeRateLimitIncrementResult(test.value); err == nil {
|
||||
t.Fatal("expected decode error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue