logos-delivery/tests/messaging/test_rate_limit_manager.nim
stubbsta 9ba13655c7
feat: enforce per-epoch budget in RateLimitManager.admit
Replaces the pass-through skeleton with a lazily rolled fixed window:
admit() charges one message against the current epoch, resets the
counter once epochPeriodSec has elapsed, and rejects with OverBudget
when messagesPerEpoch is exhausted. Disabled or non-positive
configurations admit everything, so the default-constructed
MessagingClientConf (enabled = false) keeps today's behaviour.

Parking of over-budget messages stays with the SendService scheduler
(NextRoundRetry); the manager only answers whether one more
transmission fits. The queue / dequeueReady stubs that anticipated
manager-side parking are removed accordingly.

Extends tests/messaging/test_rate_limit_manager.nim with budget
boundary, epoch rollover, resetEpoch, and degenerate-config cases,
replacing the enabled-pass-through placeholder test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:07:15 +02:00

57 lines
2.1 KiB
Nim

{.used.}
import chronos, testutils/unittests, stew/byteutils
import logos_delivery/messaging/rate_limit_manager/rate_limit_manager
suite "RateLimitManager - admission":
asyncTest "admit is a pass-through when disabled":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: false, epochPeriodSec: 600, messagesPerEpoch: 1)
)
for _ in 0 ..< 10:
let res = await rl.admit("payload".toBytes())
check res.isOk()
asyncTest "admits up to the budget then rejects with OverBudget":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 600, messagesPerEpoch: 3)
)
for i in 0 ..< 3:
check (await rl.admit(("msg" & $i).toBytes())).isOk()
let res = await rl.admit("over".toBytes())
check:
res.isErr()
res.error == RateLimitError.OverBudget
asyncTest "budget frees when the epoch rolls over":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 1, messagesPerEpoch: 1)
)
check (await rl.admit("first".toBytes())).isOk()
check (await rl.admit("second".toBytes())).isErr()
await sleepAsync(1100.milliseconds)
check (await rl.admit("third".toBytes())).isOk()
check (await rl.admit("fourth".toBytes())).isErr()
asyncTest "resetEpoch forces a fresh budget":
let rl = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 600, messagesPerEpoch: 1)
)
check (await rl.admit("first".toBytes())).isOk()
check (await rl.admit("second".toBytes())).isErr()
rl.resetEpoch()
check (await rl.admit("third".toBytes())).isOk()
asyncTest "non-positive budget or period is treated as disabled":
let zeroBudget = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 600, messagesPerEpoch: 0)
)
check (await zeroBudget.admit("a".toBytes())).isOk()
let zeroPeriod = RateLimitManager.new(
RateLimitConfig(enabled: true, epochPeriodSec: 0, messagesPerEpoch: 1)
)
check (await zeroPeriod.admit("a".toBytes())).isOk()
check (await zeroPeriod.admit("b".toBytes())).isOk()