Skip to content

Commit 9638e17

Browse files
fix(relay): tunnel cleanup keeps up with new idle tunnels (#26135)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c3b40b9 commit 9638e17

3 files changed

Lines changed: 13 additions & 10 deletions

File tree

‎docs/operations/relay-observability.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ error messages. An expired APNs key or FCM service account fails every job.
147147

148148
**Cloudflare API rate limited / near its limit.** The warning fires on sustained 429s; the page fires
149149
when they are frequent enough to stall provisioning. Compare the call volume on the dashboard with the
150-
account limit.
150+
account limit, which Cloudflare raised to 20,000 calls per 5 minutes in October 2026 (peak use then
151+
was about 2,100).
151152

152153
**Traffic dropped.** The Worker is down, a deploy broke it, or spans stopped reaching Axiom. Check
153154
`https://relay.t3.codes/health` first.

‎infra/relay/src/environments/ManagedEndpointReaper.test.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,8 @@ describe("ManagedEndpointReaper", () => {
677677
});
678678

679679
it.effect("rotates the deletion order so persistent failures do not starve later tunnels", () => {
680-
const entries = Array.from({ length: 200 }, (_, index) =>
680+
const limit = ManagedEndpointReaper.MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT;
681+
const entries = Array.from({ length: limit * 2 }, (_, index) =>
681682
tunnel({
682683
id: `tunnel-${index}`,
683684
suffix: index.toString(16).padStart(16, "0"),
@@ -688,7 +689,7 @@ describe("ManagedEndpointReaper", () => {
688689
const state = harness({
689690
tunnels: entries,
690691
allocations: recoverableOwners(entries),
691-
failDeleteWhen: (id) => Number(id.split("-")[1]) < 100,
692+
failDeleteWhen: (id) => Number(id.split("-")[1]) < limit,
692693
});
693694

694695
return Effect.gen(function* () {
@@ -697,14 +698,15 @@ describe("ManagedEndpointReaper", () => {
697698
yield* reaper.sweep;
698699
yield* TestClock.setTime(NOW_MILLIS + 5 * 60 * 1_000);
699700
yield* reaper.sweep;
700-
const later = state.deleted.filter((id) => Number(id.split("-")[1]) >= 100);
701+
const later = state.deleted.filter((id) => Number(id.split("-")[1]) >= limit);
701702
expect(later.length).toBeGreaterThan(0);
702703
}).pipe(Effect.provide(state.layer));
703704
});
704705

705706
it.effect("rotates the attempt budget across both statuses over consecutive sweeps", () => {
707+
const limit = ManagedEndpointReaper.MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT;
706708
const entries = (["down", "inactive"] as const).flatMap((status) =>
707-
Array.from({ length: 100 }, (_, index) =>
709+
Array.from({ length: limit }, (_, index) =>
708710
tunnel({
709711
id: `${status}-${index}`,
710712
suffix: `${status === "down" ? "a" : "b"}${index.toString(16).padStart(15, "0")}`,
@@ -720,13 +722,13 @@ describe("ManagedEndpointReaper", () => {
720722
yield* TestClock.setTime(NOW_MILLIS);
721723
yield* reaper.sweep;
722724
const firstStatus = state.deleted[0]!.split("-")[0];
723-
expect(state.deleted).toHaveLength(100);
725+
expect(state.deleted).toHaveLength(limit);
724726
expect(state.deleted.every((id) => id.startsWith(`${firstStatus}-`))).toBe(true);
725727

726728
state.deleted.length = 0;
727729
yield* TestClock.setTime(NOW_MILLIS + 5 * 60 * 1_000);
728730
yield* reaper.sweep;
729-
expect(state.deleted).toHaveLength(100);
731+
expect(state.deleted).toHaveLength(limit);
730732
expect(state.deleted.every((id) => !id.startsWith(`${firstStatus}-`))).toBe(true);
731733
}).pipe(Effect.provide(state.layer));
732734
});

‎infra/relay/src/environments/ManagedEndpointReaper.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export const MANAGED_ENDPOINT_GRACE_PERIOD_MINUTES = 5;
1717
// Never-connected tunnels may still be pairing; allow an hour.
1818
export const MANAGED_ENDPOINT_INACTIVE_GRACE_PERIOD_MINUTES = 60;
1919
export const MANAGED_ENDPOINT_SWEEP_PAGE_SIZE = 100;
20-
export const MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT = 100;
20+
export const MANAGED_ENDPOINT_SWEEP_ATTEMPT_LIMIT = 250;
2121
export const MANAGED_ENDPOINT_SWEEP_LIST_REQUEST_LIMIT = 10;
2222
const MANAGED_ENDPOINT_LEGACY_AGE_BUCKET_DAYS = [7, 30, 90] as const;
2323
// Hosts without recovery cannot replace deleted tunnels; allow a longer absence.
2424
const MANAGED_ENDPOINT_LEGACY_GRACE_PERIOD_DAYS = 7;
25-
// Parallelize to fit the cron budget, but stay below Hyperdrive's shared 20-connection limit.
26-
export const MANAGED_ENDPOINT_SWEEP_DELETE_CONCURRENCY = 4;
25+
// Each delete holds a Hyperdrive connection for ~2s; stay well below the 40 request handlers share.
26+
export const MANAGED_ENDPOINT_SWEEP_DELETE_CONCURRENCY = 8;
2727
// Leave time under the cron's two-minute limit to finish deletions and record counters.
2828
const MANAGED_ENDPOINT_SWEEP_DELETE_BUDGET_MS = 90_000;
2929

0 commit comments

Comments
 (0)