Skip to content

Commit 440e737

Browse files
committed
fix(e2e): stop credential retries after deadline
1 parent 784fbcf commit 440e737

2 files changed

Lines changed: 119 additions & 4 deletions

File tree

scripts/e2e/npm-telegram-rtt-credentials.mjs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,12 +350,20 @@ async function acquireWithRetry(config) {
350350
let attempt = 0;
351351
while (true) {
352352
attempt += 1;
353+
const attemptElapsedMs = Date.now() - startedAt;
354+
const attemptRemainingMs = config.acquireTimeoutMs - attemptElapsedMs;
355+
if (attemptRemainingMs <= 0) {
356+
throw taggedError(
357+
`credential broker acquire timed out after ${config.acquireTimeoutMs}ms before retry`,
358+
"ETIMEDOUT",
359+
);
360+
}
353361
try {
354362
return await postBroker({
355363
authToken: config.authToken,
356364
bodyMaxBytes: config.httpBodyMaxBytes,
357365
label: "credential broker acquire",
358-
timeoutMs: config.httpTimeoutMs,
366+
timeoutMs: Math.min(config.httpTimeoutMs, attemptRemainingMs),
359367
url: config.acquireUrl,
360368
body: {
361369
kind: "telegram",
@@ -375,9 +383,14 @@ async function acquireWithRetry(config) {
375383
const fallbackDelay = RETRY_BACKOFF_MS[Math.min(attempt - 1, RETRY_BACKOFF_MS.length - 1)];
376384
const retryAfterMs = error instanceof BrokerError ? error.retryAfterMs : undefined;
377385
const delayMs = retryAfterMs ?? fallbackDelay;
378-
await new Promise((resolve) =>
379-
setTimeout(resolve, Math.min(delayMs, Math.max(config.acquireTimeoutMs - elapsedMs, 0))),
380-
);
386+
const remainingMs = config.acquireTimeoutMs - elapsedMs;
387+
if (delayMs >= remainingMs) {
388+
throw taggedError(
389+
`credential broker acquire timed out after ${config.acquireTimeoutMs}ms before retry`,
390+
"ETIMEDOUT",
391+
);
392+
}
393+
await new Promise((resolve) => setTimeout(resolve, delayMs));
381394
}
382395
}
383396
}

test/scripts/rtt-harness.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,108 @@ describe("RTT harness", () => {
257257
}
258258
});
259259

260+
it("does not start another credential acquire after retry delay exhausts the deadline", async () => {
261+
let requests = 0;
262+
const server = createServer((_request, response) => {
263+
requests += 1;
264+
response.writeHead(503, { "content-type": "application/json" });
265+
response.end(
266+
JSON.stringify({
267+
status: "error",
268+
code: "POOL_EXHAUSTED",
269+
message: "credential pool exhausted",
270+
retryAfterMs: 1_000,
271+
}),
272+
);
273+
});
274+
const { port } = await listenOnLoopback(server);
275+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-rtt-credentials-retry-"));
276+
tempDirs.push(tempDir);
277+
const startedAt = Date.now();
278+
279+
try {
280+
await execFileAsync(
281+
process.execPath,
282+
[
283+
CREDENTIAL_SCRIPT_PATH,
284+
"acquire",
285+
"--lease-file",
286+
path.join(tempDir, "lease.json"),
287+
"--credential-env-file",
288+
path.join(tempDir, "credentials.env"),
289+
],
290+
{
291+
env: {
292+
...credentialBrokerEnv(port),
293+
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "75",
294+
OPENCLAW_QA_CREDENTIAL_HTTP_TIMEOUT_MS: "250",
295+
},
296+
maxBuffer: 128 * 1024,
297+
},
298+
);
299+
throw new Error("Expected credential acquire to fail.");
300+
} catch (error) {
301+
const execError = error as Error & { stderr?: string };
302+
expect(execError.stderr).toContain("credential broker acquire timed out after 75ms");
303+
expect(Date.now() - startedAt).toBeLessThan(500);
304+
expect(requests).toBe(1);
305+
} finally {
306+
await closeServer(server);
307+
}
308+
});
309+
310+
it("caps credential acquire HTTP retries to the remaining acquire deadline", async () => {
311+
let requests = 0;
312+
const server = createServer((_request, response) => {
313+
requests += 1;
314+
if (requests === 1) {
315+
response.writeHead(503, { "content-type": "application/json" });
316+
response.end(
317+
JSON.stringify({
318+
status: "error",
319+
code: "POOL_EXHAUSTED",
320+
message: "credential pool exhausted",
321+
retryAfterMs: 1,
322+
}),
323+
);
324+
}
325+
});
326+
const { port } = await listenOnLoopback(server);
327+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-rtt-credentials-cap-"));
328+
tempDirs.push(tempDir);
329+
const startedAt = Date.now();
330+
331+
try {
332+
await execFileAsync(
333+
process.execPath,
334+
[
335+
CREDENTIAL_SCRIPT_PATH,
336+
"acquire",
337+
"--lease-file",
338+
path.join(tempDir, "lease.json"),
339+
"--credential-env-file",
340+
path.join(tempDir, "credentials.env"),
341+
],
342+
{
343+
env: {
344+
...credentialBrokerEnv(port),
345+
OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS: "100",
346+
OPENCLAW_QA_CREDENTIAL_HTTP_TIMEOUT_MS: "900",
347+
},
348+
maxBuffer: 128 * 1024,
349+
},
350+
);
351+
throw new Error("Expected credential acquire to fail.");
352+
} catch (error) {
353+
const execError = error as Error & { stderr?: string };
354+
expect(execError.stderr).toContain("credential broker acquire timed out after");
355+
expect(Date.now() - startedAt).toBeLessThan(500);
356+
expect(requests).toBe(2);
357+
} finally {
358+
await closeServer(server);
359+
}
360+
});
361+
260362
it("preserves empty broker responses for successful lease release", async () => {
261363
const server = createServer((_request, response) => {
262364
response.writeHead(204);

0 commit comments

Comments
 (0)