Public: anyone can read this; the crew can change it.

test/strangers.test.ts's rate windows pass every time on a loaded check machine #4625

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4581-test-strangers-test-ts-s-rate-windows-pa → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/4581-test-strangers-test-ts-s-rate-windows-pa was merged into main as d5d3ab6.

Review

Sign in to review this change.
2 files changed +66 −12
src/http/strangers.ts +37 −5
@@ -90,6 +90,10 @@ interface State {
9090 readonly forge: Map<Door, RateLimiter>;
9191 /** What is held right now, by key: "transfer:<bucket>", "code", "stream:*". */
9292 readonly held: Map<string, number>;
93 /** The time the rate windows are counted by; a test hands in its own. */
94 now: () => number;
95 /** Woken, once, by the next hold or release: what a waiting test sleeps on. */
96 changed: (() => void)[];
9397 }
9498
9599 const states = new WeakMap<App, State>();
@@ -102,23 +106,29 @@ function stateOf(app: App): State {
102106 perAddress: new Map(),
103107 forge: new Map(),
104108 held: new Map(),
109 now: Date.now,
110 changed: [],
105111 };
106112 states.set(app, state);
107113 }
108114 return state;
109115 }
110116
111 const minute = (perMinute: number): RateLimiter =>
112 new RateLimiter({ perMinute, adminPerMinute: perMinute, enabled: true });
117 const minute = (state: State, perMinute: number): RateLimiter =>
118 new RateLimiter(
119 { perMinute, adminPerMinute: perMinute, enabled: true },
120 () => state.now(),
121 );
113122
114123 function limiter(
124 state: State,
115125 map: Map<Door, RateLimiter>,
116126 door: Door,
117127 perMinute: number,
118128 ): RateLimiter {
119129 let found = map.get(door);
120130 if (found === undefined) {
121 found = minute(perMinute);
131 found = minute(state, perMinute);
122132 map.set(door, found);
123133 }
124134 return found;
@@ -141,6 +151,26 @@ export function setStrangerLimits(
141151 state.forge.clear();
142152 }
143153
154 /**
155 * Test seam: count the rate windows by `now` instead of the wall clock, so a
156 * slow request never slides one. Counted minutes start over.
157 */
158 export function setStrangerClock(app: App, now: () => number): void {
159 const state = stateOf(app);
160 state.now = now;
161 state.perAddress.clear();
162 state.forge.clear();
163 }
164
165 /** Resolves at the next hold or release on this forge — for tests to wait on. */
166 export const heldChanged = (app: App): Promise<void> =>
167 new Promise((resolve) => stateOf(app).changed.push(resolve));
168
169 function wake(state: State): void {
170 const waiting = state.changed.splice(0);
171 for (const resolve of waiting) resolve();
172 }
173
144174 /**
145175 * Count one stranger's request through `door`: refused when its address or
146176 * every stranger together has spent the minute. A refusal is not counted
@@ -156,10 +186,10 @@ export const takeDoor = (app: App, door: Door, request: Request): Verdict =>
156186 */
157187 export function takeDoorAt(app: App, door: Door, bucket: string): Verdict {
158188 const state = stateOf(app);
159 const mine = limiter(state.perAddress, door, state.limits.perAddress[door]);
189 const mine = limiter(state, state.perAddress, door, state.limits.perAddress[door]);
160190 const own = mine.take(bucket, "ro");
161191 if (!own.allowed) return own;
162 const all = limiter(state.forge, door, state.limits.forge[door]).take("*", "ro");
192 const all = limiter(state, state.forge, door, state.limits.forge[door]).take("*", "ro");
163193 if (!all.allowed) {
164194 mine.refund(bucket);
165195 return all;
@@ -172,6 +202,7 @@ function hold(state: State, key: string, max: number): (() => void) | undefined
172202 const now = state.held.get(key) ?? 0;
173203 if (now >= max) return undefined;
174204 state.held.set(key, now + 1);
205 wake(state);
175206 let released = false;
176207 return () => {
177208 if (released) return;
@@ -179,6 +210,7 @@ function hold(state: State, key: string, max: number): (() => void) | undefined
179210 const left = (state.held.get(key) ?? 1) - 1;
180211 if (left <= 0) state.held.delete(key);
181212 else state.held.set(key, left);
213 wake(state);
182214 };
183215 }
184216
test/strangers.test.ts +29 −7
@@ -19,7 +19,9 @@ import type { User } from "../src/core/users.ts";
1919 import { transferGate } from "../src/git/env.ts";
2020 import { serve, type Servers } from "../src/http/server.ts";
2121 import {
22 heldChanged,
2223 heldNow,
24 setStrangerClock,
2325 setStrangerLimits,
2426 STRANGER_LIMITS,
2527 strangerCodeSearch,
@@ -82,6 +84,9 @@ describe("strangers at every door", () => {
8284 beforeAll(async () => {
8385 dataDir = await mkdtemp(join(tmpdir(), "forge-strangers-"));
8486 app = createApp({ dataDir, port: 0 });
87 // The windows stand still: a slow request on a loaded machine never
88 // slides one, so a count is the same count however long the test takes.
89 setStrangerClock(app, () => 1_000_000);
8590 servers = await serve(app);
8691 base = servers.url;
8792 port = servers.port;
@@ -115,12 +120,10 @@ describe("strangers at every door", () => {
115120 await res.arrayBuffer();
116121 return res.status;
117122 };
118 const until = async (done: () => boolean, ms = 10_000): Promise<boolean> => {
119 const end = Date.now() + ms;
120 while (!done()) {
121 if (Date.now() > end) return false;
122 await Bun.sleep(25);
123 }
123 // Waits on the event it means: every hold or release wakes it to look again.
124 // No deadline of its own: the test's timeout is the only bound.
125 const until = async (done: () => boolean): Promise<boolean> => {

Nit: until can only return true now, so it could return Promise<void> and the callers could just await until(...). A hang still shows as the test's timeout.

126 while (!done()) await heldChanged(app);
124127 return true;
125128 };
126129
@@ -177,6 +180,25 @@ describe("strangers at every door", () => {
177180 }
178181 });
179182
183 test("a window slides with the clock it is given, never with the time a request took", async () => {
184 let at = 5_000_000;
185 setStrangerClock(app, () => at);
186 setStrangerLimits(app, { perAddress: { ...STRANGER_LIMITS.perAddress, api: 2 } });
187 try {
188 const ip = address();
189 expect(await status("/api/v1/repos", from(ip))).toBe(200);
190 expect(await status("/api/v1/repos", from(ip))).toBe(200);
191 expect(await status("/api/v1/repos", from(ip))).toBe(429);
192 at += 59_999;
193 expect(await status("/api/v1/repos", from(ip))).toBe(429);
194 at += 1;
195 expect(await status("/api/v1/repos", from(ip))).toBe(200);
196 } finally {
197 setStrangerClock(app, () => 1_000_000);
198 setStrangerLimits(app, STRANGER_LIMITS);
199 }
200 });
201
180202 test("anonymous ref advertisements are counted per address", async () => {
181203 setStrangerLimits(app, { perAddress: { ...STRANGER_LIMITS.perAddress, git: 3 } });
182204 try {
@@ -404,7 +426,7 @@ describe("strangers at every door", () => {
404426 expect(res.status).toBe(200);
405427 await res.arrayBuffer();
406428 // The GET's stream behind the HEAD is cancelled, not left holding a slot.
407 expect(await until(() => heldNow(app, "stream:*") === 0, 2_000)).toBe(true);
429 expect(await until(() => heldNow(app, "stream:*") === 0)).toBe(true);
408430 });
409431
410432 test("the audit card is made once in its while, however many ask", async () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs