History

perf(server): bootstrap engine from persisted projections

Julius Marminge committed 7 months ago

Commit 39389301 parent f23d1416 files · +117 −8Browse files

Unified · Split

6 files
  1. apps/server/src/orchestration/Layers/CheckpointReactor.test.ts +2 −0
  2. apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +106 −1
  3. apps/server/src/orchestration/Layers/OrchestrationEngine.ts +3 −7
  4. apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +2 −0
  5. apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts +2 −0
  6. apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts +2 −0
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+2−0
@@ -23,6 +23,7 @@ import { GitCoreLive } from "../../git/Layers/GitCore.ts"; · 22 unmodified lines
23import { CheckpointReactorLive } from "./CheckpointReactor.ts";23import { CheckpointReactorLive } from "./CheckpointReactor.ts";
24import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";24import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
25import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";25import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
26import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
26import { RuntimeReceiptBusLive } from "./RuntimeReceiptBus.ts";27import { RuntimeReceiptBusLive } from "./RuntimeReceiptBus.ts";
27import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";28import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
28import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";29import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
@@ -249,6 +250,7 @@ describe("CheckpointReactor", () => { · 220 unmodified lines
249 options?.providerName ?? "codex",250 options?.providerName ?? "codex",
250 );251 );
251 const orchestrationLayer = OrchestrationEngineLive.pipe(252 const orchestrationLayer = OrchestrationEngineLive.pipe(
253 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
252 Layer.provide(OrchestrationProjectionPipelineLive),254 Layer.provide(OrchestrationProjectionPipelineLive),
253 Layer.provide(OrchestrationEventStoreLive),255 Layer.provide(OrchestrationEventStoreLive),
254 Layer.provide(OrchestrationCommandReceiptRepositoryLive),256 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+106−1
@@ -8,7 +8,7 @@ import { · 7 unmodified lines
8 TurnId,8 TurnId,
9 type OrchestrationEvent,9 type OrchestrationEvent,
10} from "@t3tools/contracts";10} from "@t3tools/contracts";
11import { Effect, Layer, ManagedRuntime, Queue, Stream } from "effect";11import { Effect, Layer, ManagedRuntime, Option, Queue, Stream } from "effect";
12import { describe, expect, it } from "vitest";12import { describe, expect, it } from "vitest";
13 13
14import { PersistenceSqlError } from "../../persistence/Errors.ts";14import { PersistenceSqlError } from "../../persistence/Errors.ts";
@@ -21,11 +21,13 @@ import { · 6 unmodified lines
21} from "../../persistence/Services/OrchestrationEventStore.ts";21} from "../../persistence/Services/OrchestrationEventStore.ts";
22import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";22import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
23import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";23import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
24import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
24import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";25import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
25import {26import {
26 OrchestrationProjectionPipeline,27 OrchestrationProjectionPipeline,
27 type OrchestrationProjectionPipelineShape,28 type OrchestrationProjectionPipelineShape,
28} from "../Services/ProjectionPipeline.ts";29} from "../Services/ProjectionPipeline.ts";
30import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
29import { ServerConfig } from "../../config.ts";31import { ServerConfig } from "../../config.ts";
30import * as NodeServices from "@effect/platform-node/NodeServices";32import * as NodeServices from "@effect/platform-node/NodeServices";
31 33
@@ -39,6 +41,7 @@ async function createOrchestrationSystem() { · 7 unmodified lines
39 prefix: "t3-orchestration-engine-test-",41 prefix: "t3-orchestration-engine-test-",
40 });42 });
41 const orchestrationLayer = OrchestrationEngineLive.pipe(43 const orchestrationLayer = OrchestrationEngineLive.pipe(
44 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
42 Layer.provide(OrchestrationProjectionPipelineLive),45 Layer.provide(OrchestrationProjectionPipelineLive),
43 Layer.provide(OrchestrationEventStoreLive),46 Layer.provide(OrchestrationEventStoreLive),
44 Layer.provide(OrchestrationCommandReceiptRepositoryLive),47 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
@@ -60,6 +63,105 @@ function now() { · 15 unmodified lines
60}63}
61 64
62describe("OrchestrationEngine", () => {65describe("OrchestrationEngine", () => {
66 it("bootstraps the in-memory read model from persisted projections", async () => {
67 const failOnHistoricalReplayStore: OrchestrationEventStoreShape = {
68 append: () =>
69 Effect.fail(
70 new PersistenceSqlError({
71 operation: "test.append",
72 detail: "append should not be called during bootstrap",
73 }),
74 ),
75 readFromSequence: () => Stream.empty,
76 readAll: () =>
77 Stream.fail(
78 new PersistenceSqlError({
79 operation: "test.readAll",
80 detail: "historical replay should not be used during bootstrap",
81 }),
82 ),
83 };
84
85 const projectionSnapshot = {
86 snapshotSequence: 7,
87 updatedAt: "2026-03-03T00:00:04.000Z",
88 projects: [
89 {
90 id: asProjectId("project-bootstrap"),
91 title: "Bootstrap Project",
92 workspaceRoot: "/tmp/project-bootstrap",
93 defaultModelSelection: {
94 provider: "codex" as const,
95 model: "gpt-5-codex",
96 },
97 scripts: [],
98 createdAt: "2026-03-03T00:00:00.000Z",
99 updatedAt: "2026-03-03T00:00:01.000Z",
100 deletedAt: null,
101 },
102 ],
103 threads: [
104 {
105 id: ThreadId.makeUnsafe("thread-bootstrap"),
106 projectId: asProjectId("project-bootstrap"),
107 title: "Bootstrap Thread",
108 modelSelection: {
109 provider: "codex" as const,
110 model: "gpt-5-codex",
111 },
112 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
113 runtimeMode: "full-access" as const,
114 branch: null,
115 worktreePath: null,
116 latestTurn: null,
117 createdAt: "2026-03-03T00:00:02.000Z",
118 updatedAt: "2026-03-03T00:00:03.000Z",
119 archivedAt: null,
120 deletedAt: null,
121 messages: [],
122 proposedPlans: [],
123 activities: [],
124 checkpoints: [],
125 session: null,
126 },
127 ],
128 };
129
130 const layer = OrchestrationEngineLive.pipe(
131 Layer.provide(
132 Layer.succeed(ProjectionSnapshotQuery, {
133 getSnapshot: () => Effect.succeed(projectionSnapshot),
134 getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),
135 getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
136 getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
137 getThreadCheckpointContext: () => Effect.succeed(Option.none()),
138 }),
139 ),
140 Layer.provide(
141 Layer.succeed(OrchestrationProjectionPipeline, {
142 bootstrap: Effect.void,
143 projectEvent: () => Effect.void,
144 } satisfies OrchestrationProjectionPipelineShape),
145 ),
146 Layer.provide(Layer.succeed(OrchestrationEventStore, failOnHistoricalReplayStore)),
147 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
148 Layer.provide(SqlitePersistenceMemory),
149 );
150
151 const runtime = ManagedRuntime.make(layer);
152
153 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
154 const readModel = await runtime.runPromise(engine.getReadModel());
155
156 expect(readModel.snapshotSequence).toBe(7);
157 expect(readModel.projects).toHaveLength(1);
158 expect(readModel.projects[0]?.title).toBe("Bootstrap Project");
159 expect(readModel.threads).toHaveLength(1);
160 expect(readModel.threads[0]?.title).toBe("Bootstrap Thread");
161
162 await runtime.dispose();
163 });
164
63 it("returns deterministic read models for repeated reads", async () => {165 it("returns deterministic read models for repeated reads", async () => {
64 const createdAt = now();166 const createdAt = now();
65 const system = await createOrchestrationSystem();167 const system = await createOrchestrationSystem();
@@ -417,6 +519,7 @@ describe("OrchestrationEngine", () => { · 351 unmodified lines
417 519
418 const runtime = ManagedRuntime.make(520 const runtime = ManagedRuntime.make(
419 OrchestrationEngineLive.pipe(521 OrchestrationEngineLive.pipe(
522 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
420 Layer.provide(OrchestrationProjectionPipelineLive),523 Layer.provide(OrchestrationProjectionPipelineLive),
421 Layer.provide(Layer.succeed(OrchestrationEventStore, flakyStore)),524 Layer.provide(Layer.succeed(OrchestrationEventStore, flakyStore)),
422 Layer.provide(OrchestrationCommandReceiptRepositoryLive),525 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
@@ -512,6 +615,7 @@ describe("OrchestrationEngine", () => { · 89 unmodified lines
512 615
513 const runtime = ManagedRuntime.make(616 const runtime = ManagedRuntime.make(
514 OrchestrationEngineLive.pipe(617 OrchestrationEngineLive.pipe(
618 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
515 Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)),619 Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)),
516 Layer.provide(OrchestrationEventStoreLive),620 Layer.provide(OrchestrationEventStoreLive),
517 Layer.provide(OrchestrationCommandReceiptRepositoryLive),621 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
@@ -653,6 +757,7 @@ describe("OrchestrationEngine", () => { · 135 unmodified lines
653 757
654 const runtime = ManagedRuntime.make(758 const runtime = ManagedRuntime.make(
655 OrchestrationEngineLive.pipe(759 OrchestrationEngineLive.pipe(
760 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
656 Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)),761 Layer.provide(Layer.succeed(OrchestrationProjectionPipeline, flakyProjectionPipeline)),
657 Layer.provide(Layer.succeed(OrchestrationEventStore, nonTransactionalStore)),762 Layer.provide(Layer.succeed(OrchestrationEventStore, nonTransactionalStore)),
658 Layer.provide(OrchestrationCommandReceiptRepositoryLive),763 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+3−7
@@ -19,6 +19,7 @@ import { · 18 unmodified lines
19import { decideOrchestrationCommand } from "../decider.ts";19import { decideOrchestrationCommand } from "../decider.ts";
20import { createEmptyReadModel, projectEvent } from "../projector.ts";20import { createEmptyReadModel, projectEvent } from "../projector.ts";
21import { OrchestrationProjectionPipeline } from "../Services/ProjectionPipeline.ts";21import { OrchestrationProjectionPipeline } from "../Services/ProjectionPipeline.ts";
22import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
22import {23import {
23 OrchestrationEngineService,24 OrchestrationEngineService,
24 type OrchestrationEngineShape,25 type OrchestrationEngineShape,
@@ -54,6 +55,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { · 29 unmodified lines
54 const eventStore = yield* OrchestrationEventStore;55 const eventStore = yield* OrchestrationEventStore;
55 const commandReceiptRepository = yield* OrchestrationCommandReceiptRepository;56 const commandReceiptRepository = yield* OrchestrationCommandReceiptRepository;
56 const projectionPipeline = yield* OrchestrationProjectionPipeline;57 const projectionPipeline = yield* OrchestrationProjectionPipeline;
58 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
57 59
58 let readModel = createEmptyReadModel(new Date().toISOString());60 let readModel = createEmptyReadModel(new Date().toISOString());
59 61
@@ -195,13 +197,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { · 135 unmodified lines
195 };197 };
196 198
197 yield* projectionPipeline.bootstrap;199 yield* projectionPipeline.bootstrap;
198 200 readModel = yield* projectionSnapshotQuery.getSnapshot();
199 // bootstrap in-memory read model from event store
200 yield* Stream.runForEach(eventStore.readAll(), (event) =>
201 Effect.gen(function* () {
202 readModel = yield* projectEvent(readModel, event);
203 }),
204 );
205 201
206 const worker = Effect.forever(Queue.take(commandQueue).pipe(Effect.flatMap(processEnvelope)));202 const worker = Effect.forever(Queue.take(commandQueue).pipe(Effect.flatMap(processEnvelope)));
207 yield* Effect.forkScoped(worker);203 yield* Effect.forkScoped(worker);
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+2−0
@@ -25,6 +25,7 @@ import { · 24 unmodified lines
25 ORCHESTRATION_PROJECTOR_NAMES,25 ORCHESTRATION_PROJECTOR_NAMES,
26 OrchestrationProjectionPipelineLive,26 OrchestrationProjectionPipelineLive,
27} from "./ProjectionPipeline.ts";27} from "./ProjectionPipeline.ts";
28import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
28import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";29import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
29import { OrchestrationProjectionPipeline } from "../Services/ProjectionPipeline.ts";30import { OrchestrationProjectionPipeline } from "../Services/ProjectionPipeline.ts";
30import { ServerConfig } from "../../config.ts";31import { ServerConfig } from "../../config.ts";
@@ -1841,6 +1842,7 @@ it.effect("restores pending turn-start metadata across projection pipeline resta · 1810 unmodified lines
1841 1842
1842const engineLayer = it.layer(1843const engineLayer = it.layer(
1843 OrchestrationEngineLive.pipe(1844 OrchestrationEngineLive.pipe(
1845 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
1844 Layer.provide(OrchestrationProjectionPipelineLive),1846 Layer.provide(OrchestrationProjectionPipelineLive),
1845 Layer.provide(OrchestrationEventStoreLive),1847 Layer.provide(OrchestrationEventStoreLive),
1846 Layer.provide(OrchestrationCommandReceiptRepositoryLive),1848 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+2−0
@@ -30,6 +30,7 @@ import { GitCore, type GitCoreShape } from "../../git/Services/GitCore.ts"; · 29 unmodified lines
30import { TextGeneration, type TextGenerationShape } from "../../git/Services/TextGeneration.ts";30import { TextGeneration, type TextGenerationShape } from "../../git/Services/TextGeneration.ts";
31import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";31import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
32import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";32import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
33import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
33import { ProviderCommandReactorLive } from "./ProviderCommandReactor.ts";34import { ProviderCommandReactorLive } from "./ProviderCommandReactor.ts";
34import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";35import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
35import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";36import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
@@ -211,6 +212,7 @@ describe("ProviderCommandReactor", () => { · 175 unmodified lines
211 };212 };
212 213
213 const orchestrationLayer = OrchestrationEngineLive.pipe(214 const orchestrationLayer = OrchestrationEngineLive.pipe(
215 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
214 Layer.provide(OrchestrationProjectionPipelineLive),216 Layer.provide(OrchestrationProjectionPipelineLive),
215 Layer.provide(OrchestrationEventStoreLive),217 Layer.provide(OrchestrationEventStoreLive),
216 Layer.provide(OrchestrationCommandReceiptRepositoryLive),218 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+2−0
@@ -31,6 +31,7 @@ import { · 30 unmodified lines
31} from "../../provider/Services/ProviderService.ts";31} from "../../provider/Services/ProviderService.ts";
32import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";32import { OrchestrationEngineLive } from "./OrchestrationEngine.ts";
33import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";33import { OrchestrationProjectionPipelineLive } from "./ProjectionPipeline.ts";
34import { OrchestrationProjectionSnapshotQueryLive } from "./ProjectionSnapshotQuery.ts";
34import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts";35import { ProviderRuntimeIngestionLive } from "./ProviderRuntimeIngestion.ts";
35import {36import {
36 OrchestrationEngineService,37 OrchestrationEngineService,
@@ -197,6 +198,7 @@ describe("ProviderRuntimeIngestion", () => { · 160 unmodified lines
197 fs.mkdirSync(path.join(workspaceRoot, ".git"));198 fs.mkdirSync(path.join(workspaceRoot, ".git"));
198 const provider = createProviderServiceHarness();199 const provider = createProviderServiceHarness();
199 const orchestrationLayer = OrchestrationEngineLive.pipe(200 const orchestrationLayer = OrchestrationEngineLive.pipe(
201 Layer.provide(OrchestrationProjectionSnapshotQueryLive),
200 Layer.provide(OrchestrationProjectionPipelineLive),202 Layer.provide(OrchestrationProjectionPipelineLive),
201 Layer.provide(OrchestrationEventStoreLive),203 Layer.provide(OrchestrationEventStoreLive),
202 Layer.provide(OrchestrationCommandReceiptRepositoryLive),204 Layer.provide(OrchestrationCommandReceiptRepositoryLive),