Skip to content

Commit d1273a3

Browse files
t3dotggclaude
andcommitted
perf(server): restarts no longer replay PR discovery for every old thread
Each boot marked every settled branch thread without a saved PR for discovery. A "no PR" answer is not saved, so every restart repeated the whole pass: 4-6 git spawns per thread plus a gh call, retried up to 5 times. With gh missing, the scale bench measured about 3,850 git spawns/min, 17k spans/min and 24 MB/min of trace for about 5 min after each boot. The backfill came from #10101 as a one-time migration. Limit it to threads settled in the last 7 days (BACKFILL_WINDOW). The check uses settledAt, not updatedAt: auto-settle stamps updatedAt with the settle time, so a mass auto-settle would put every old thread back in the window. A branch or link change on a settled thread still gets its own single-thread pass. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 8872666 commit d1273a3

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

‎apps/server/src/orchestration/ThreadPullRequestReactor.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
} from "@t3tools/contracts";
1818
import { describe, expect, it } from "@effect/vitest";
1919
import * as Crypto from "effect/Crypto";
20+
import * as DateTime from "effect/DateTime";
2021
import * as Deferred from "effect/Deferred";
22+
import * as Duration from "effect/Duration";
2123
import * as Effect from "effect/Effect";
2224
import * as FileSystem from "effect/FileSystem";
2325
import * as Layer from "effect/Layer";
@@ -764,6 +766,34 @@ describe("ThreadPullRequestReactor", () => {
764766
),
765767
);
766768

769+
it.effect("skips the startup backfill for threads older than the backfill window", () =>
770+
Effect.scoped(
771+
Effect.gen(function* () {
772+
const old = DateTime.formatIso(
773+
DateTime.makeUnsafe(
774+
Date.parse(NOW) - Duration.toMillis(ThreadPullRequestReactor.BACKFILL_WINDOW) - 1,
775+
),
776+
);
777+
const fixture = yield* makeHarness({
778+
threads: [
779+
thread("recent", { branch: "recent", settledAt: NOW }),
780+
thread("old", { branch: "old", settledAt: old, updatedAt: old }),
781+
// Auto-settle keeps the last activity as settledAt but stamps updatedAt now.
782+
thread("auto-settled", { branch: "auto-settled", settledAt: old }),
783+
],
784+
branchPullRequest: () => Effect.succeed(branchPullRequest()),
785+
});
786+
yield* TestClock.setTime(Date.parse(NOW));
787+
yield* Effect.gen(function* () {
788+
yield* fixture.start();
789+
expect((yield* Ref.get(fixture.commands)).map((command) => command.threadId)).toEqual([
790+
"recent",
791+
]);
792+
}).pipe(Effect.provide(fixture.layer));
793+
}),
794+
),
795+
);
796+
767797
it.effect("matches Azure SSH projects to HTTPS PRs with the provider repository selector", () =>
768798
Effect.scoped(
769799
Effect.gen(function* () {

‎apps/server/src/orchestration/ThreadPullRequestReactor.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import {
1212
} from "@t3tools/contracts";
1313
import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
1414
import * as Cause from "effect/Cause";
15+
import * as Clock from "effect/Clock";
1516
import * as Context from "effect/Context";
1617
import * as Crypto from "effect/Crypto";
18+
import * as Duration from "effect/Duration";
1719
import * as Effect from "effect/Effect";
1820
import * as FileSystem from "effect/FileSystem";
1921
import * as Layer from "effect/Layer";
@@ -54,6 +56,14 @@ function samePullRequest(
5456
/** Startup lookups per settled thread before discovery gives up on it. */
5557
export const BACKFILL_ATTEMPTS = 5;
5658

59+
/**
60+
* Startup discovery only covers threads settled this recently. Use settledAt,
61+
* not updatedAt: auto-settle stamps updatedAt with the settle time, so a mass
62+
* auto-settle would put every old thread back in the window. Older threads
63+
* were checked while active or on an earlier boot.
64+
*/
65+
export const BACKFILL_WINDOW = Duration.days(7);
66+
5767
interface RefreshRequest {
5868
readonly threadId: ThreadId | null;
5969
readonly refresh: boolean;
@@ -112,9 +122,9 @@ export const make = Effect.gen(function* () {
112122
const repositoryIdentities = yield* RepositoryIdentityResolver.RepositoryIdentityResolver;
113123
const crypto = yield* Crypto.Crypto;
114124
const fileSystem = yield* FileSystem.FileSystem;
115-
// Settled threads get one link discovery at startup. Failed lookups retry on
116-
// the periodic pass a few times, then stop until the thread changes or the
117-
// server restarts, so a missing or logged-out CLI cannot loop forever.
125+
// Recently settled threads get one link discovery at startup. Failed lookups
126+
// retry on the periodic pass a few times, then stop until the thread changes
127+
// or the server restarts, so a missing or logged-out CLI cannot loop forever.
118128
const pendingBackfill = new Map<ThreadId, number>();
119129
const finishBackfill = (threads: ReadonlyArray<{ readonly id: ThreadId }>) => {
120130
for (const thread of threads) pendingBackfill.delete(thread.id);
@@ -138,10 +148,12 @@ export const make = Effect.gen(function* () {
138148
: yield* readSweepSnapshot(snapshots, request.threadId);
139149
const projects = new Map(snapshot.projects.map((project) => [project.id, project]));
140150
if (request.backfill) {
151+
const cutoff = (yield* Clock.currentTimeMillis) - Duration.toMillis(BACKFILL_WINDOW);
141152
for (const thread of snapshot.threads) {
142153
if (
143154
(thread.settledOverride === "settled" || thread.settledAt !== null) &&
144-
thread.branchPullRequest == null
155+
thread.branchPullRequest == null &&
156+
Date.parse(thread.settledAt ?? thread.updatedAt) >= cutoff
145157
) {
146158
pendingBackfill.set(thread.id, BACKFILL_ATTEMPTS);
147159
}

0 commit comments

Comments
 (0)