mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 21:04:45 +00:00
fix(infra): positive jitter 는 Math.ceil 로 non-integer retryAfterMs 하방도 보장 (cross-review)
This commit is contained in:
committed by
Peter Steinberger
parent
a77bd213ce
commit
b7fc2451e7
@@ -109,6 +109,37 @@ describe("retryAsync respects server-supplied Retry-After as a lower bound", ()
|
||||
expect(delays[0]).toBeLessThan(1_000);
|
||||
});
|
||||
|
||||
it("does not round a non-integer retryAfterMs below its lower bound", async () => {
|
||||
// Retry-After is typed as `number` (milliseconds), so non-integer values
|
||||
// are legal. With `Math.round` in positive mode, `retryAfterMs=1.4` and
|
||||
// `fraction=0` produce `Math.round(1.4 * 1) = 1`, which dips below the
|
||||
// caller's lower bound. `Math.ceil` in positive mode closes that gap.
|
||||
randomMocks.generateSecureFraction.mockReturnValue(0);
|
||||
|
||||
vi.useFakeTimers();
|
||||
const delays: number[] = [];
|
||||
const fn = vi
|
||||
.fn<() => Promise<string>>()
|
||||
.mockRejectedValueOnce(new Error("429 non-integer retry-after"))
|
||||
.mockResolvedValueOnce("ok");
|
||||
|
||||
const promise = retryAsync(fn, {
|
||||
attempts: 2,
|
||||
minDelayMs: 0,
|
||||
maxDelayMs: 10,
|
||||
jitter: 0.5,
|
||||
retryAfterMs: () => 1.4,
|
||||
onRetry: (info) => delays.push(info.delayMs),
|
||||
});
|
||||
await vi.runAllTimersAsync();
|
||||
await expect(promise).resolves.toBe("ok");
|
||||
|
||||
expect(delays).toHaveLength(1);
|
||||
// Ceil-to-integer invariant: delay is at least the ceiling of the
|
||||
// caller-supplied lower bound, never below it.
|
||||
expect(delays[0]).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("never schedules a delay below retryAfterMs even at the low end of jitter", async () => {
|
||||
// fraction = 0 is the adversarial case: symmetric jitter yields -jitter,
|
||||
// i.e. delay = base * (1 - jitter), which would violate the Retry-After
|
||||
|
||||
@@ -72,7 +72,14 @@ function applyJitter(delayMs: number, jitter: number, mode: JitterMode = "symmet
|
||||
// below the caller's floor.
|
||||
const fraction = generateSecureFraction();
|
||||
const offset = mode === "positive" ? fraction * jitter : (fraction * 2 - 1) * jitter;
|
||||
return Math.max(0, Math.round(delayMs * (1 + offset)));
|
||||
const raw = delayMs * (1 + offset);
|
||||
// Rounding choice preserves the mode's contract. `positive` guarantees
|
||||
// `delay >= delayMs`, so a non-integer `delayMs` (e.g. retryAfterMs=1.4)
|
||||
// must round *up* — plain `Math.round(1.4)=1` would drop the delay below
|
||||
// the caller's lower bound and violate the Retry-After invariant the
|
||||
// positive branch exists to enforce. Symmetric has no floor contract so
|
||||
// it stays on `Math.round`.
|
||||
return Math.max(0, mode === "positive" ? Math.ceil(raw) : Math.round(raw));
|
||||
}
|
||||
|
||||
export async function retryAsync<T>(
|
||||
|
||||
Reference in New Issue
Block a user