History

refactor(cloud): fold execution usage tracking into AutumnService

`makeTrackExecutionUsage` lived in its own `services/autumn-tracker.ts` only to dodge the `api/layers.ts` → `@tanstack/react-start` import cycle that breaks vitest-pool-workers. It baked fire-and-forget semantics + error logging into the function shape — callers got back a `(id) => void` and couldn't compose or test the billing call. Moves the tracker onto the service itself as `AutumnService.trackExecution` (Effect shape, error channel `never`), matching the existing `use` method pattern. Callers now `Effect.runFork(autumn.trackExecution(id))` at the single boundary in `execution-stack.ts`, which is honest about the fire-and-forget choice happening at that layer — not in the service. - `services/autumn.ts`: adds `trackExecution` to `IAutumnService`. Errors are caught and logged; the returned Effect never fails. Wrapped in an `autumn.trackExecution` span so traces pick it up the same way the existing `use` calls do. - `services/execution-stack.ts`: inlines the `Effect.runFork` at the sole caller; drops the `autumn-tracker` import. - `api/autumn.ts`: drops the re-export that pointed at the old file. - Deletes `services/autumn-tracker.ts`.

Rhys Sullivan committed 6 months ago

Commit af8a33a1 parent a6b58b74 files · +27 −49Browse files

Unified · Split

4 files
  1. apps/cloud/src/api/autumn.ts +0 −6
  2. apps/cloud/src/services/autumn-tracker.ts +0 −38
  3. apps/cloud/src/services/autumn.ts +26 −3
  4. apps/cloud/src/services/execution-stack.ts +1 −2
apps/cloud/src/api/autumn.ts+0−6
@@ -8,12 +8,6 @@ import { AutumnService } from "../services/autumn"; · 7 unmodified lines
88 import { HttpResponseError, isServerError, toErrorServerResponse } from "./error-response";
99 import { SharedServices } from "./layers";
1010
11−// Re-export so existing callers don't have to change their import path.
12−// The actual implementation moved to `services/autumn-tracker.ts` to break
13−// a DO → api/* → tanstack-start import cycle that crashed module load
14−// under vitest-pool-workers.
15−export { makeTrackExecutionUsage } from "../services/autumn-tracker";
16−
1711 const handleAutumnRequestEffect = Effect.gen(function* () {
1812 const request = yield* HttpServerRequest.HttpServerRequest;
1913 const webRequest = yield* Effect.mapError(
apps/cloud/src/services/autumn-tracker.ts+0−38
@@ -1,38 +0,0 @@
1−// ---------------------------------------------------------------------------
2−// Fire-and-forget execution usage tracker against Autumn.
3−// ---------------------------------------------------------------------------
4−//
5−// Lives in `services/` rather than `api/` because callers on the Durable
6−// Object path (`mcp-session.ts` → `execution-stack.ts`) transitively pull
7−// it in, and anything under `api/` eventually re-exports from
8−// `api/layers.ts`, which imports `auth/handlers.ts` → `@tanstack/react-start`
9−// — that pulls the `#tanstack-start-plugin-adapters` subpath specifier
10−// into Vite's resolver and breaks vitest-pool-workers module load.
11−//
12−// This file has exactly one responsibility, no HTTP concerns, and imports
13−// nothing from `api/`.
14−// ---------------------------------------------------------------------------
15−
16−import { Effect } from "effect";
17−
18−import type { AutumnService } from "./autumn";
19−
20−export const makeTrackExecutionUsage = (autumn: AutumnService["Type"]) => {
21− return (organizationId: string): void => {
22− autumn
23− .use((client) =>
24− client.track({
25− customerId: organizationId,
26− featureId: "executions",
27− value: 1,
28− }),
29− )
30− .pipe(
31− Effect.catchAll((err) => {
32− console.error("[billing] track failed:", err);
33− return Effect.void;
34− }),
35− Effect.runFork,
36− );
37− };
38−};
apps/cloud/src/services/autumn.ts+26−3
@@ -21,6 +21,13 @@ export class AutumnError extends Data.TaggedError("AutumnError")<{ · 20 unmodified lines
2121
2222 export type IAutumnService = Readonly<{
2323 use: <A>(fn: (client: Autumn) => Promise<A>) => Effect.Effect<A, AutumnError, never>;
24+ /**
25+ * Fire-and-forget-safe execution usage tracker. Errors are caught and
26+ * logged; the returned Effect never fails. Callers typically
27+ * `Effect.runFork` it at the boundary so the billing call can't stall a
28+ * user-facing request.
29+ */
30+ trackExecution: (organizationId: string) => Effect.Effect<void, never, never>;
2431 }>;
2532
2633 // ---------------------------------------------------------------------------
@@ -31,9 +38,13 @@ const make = Effect.sync(() => { · 4 unmodified lines
3138 const secretKey = server.AUTUMN_SECRET_KEY;
3239
3340 if (!secretKey) {
41+ const notConfigured = Effect.die(
42+ new Error("Autumn not configured — AUTUMN_SECRET_KEY is empty"),
43+ );
3444 return {
35− use: () => Effect.die(new Error("Autumn not configured — AUTUMN_SECRET_KEY is empty")),
36− } as IAutumnService;
45+ use: () => notConfigured,
46+ trackExecution: () => Effect.void,
47+ } satisfies IAutumnService;
3748 }
3849
3950 const client = new Autumn({ secretKey });
@@ -44,7 +55,19 @@ const make = Effect.sync(() => { · 4 unmodified lines
4455 catch: (cause) => new AutumnError({ cause }),
4556 }).pipe(Effect.withSpan(`autumn.${fn.name ?? "use"}`));
4657
47− return { use } satisfies IAutumnService;
58+ const trackExecution = (organizationId: string) =>
59+ use((c) =>
60+ c.track({ customerId: organizationId, featureId: "executions", value: 1 }),
61+ ).pipe(
62+ Effect.asVoid,
63+ Effect.tapError((err) =>
64+ Effect.sync(() => console.error("[billing] track failed:", err)),
65+ ),
66+ Effect.orElse(() => Effect.void),
67+ Effect.withSpan("autumn.trackExecution"),
68+ );
69+
70+ return { use, trackExecution } satisfies IAutumnService;
4871 });
4972
5073 export class AutumnService extends Context.Tag("@executor/cloud/AutumnService")<
apps/cloud/src/services/execution-stack.ts+1−2
@@ -12,7 +12,6 @@ import { makeDynamicWorkerExecutor } from "@executor/runtime-dynamic-worker"; · 11 unmodified lines
1212
1313 import { withExecutionUsageTracking } from "../api/execution-usage";
1414 import { AutumnService } from "./autumn";
15−import { makeTrackExecutionUsage } from "./autumn-tracker";
1615 import { createScopedExecutor } from "./executor";
1716
1817 export const makeExecutionStack = (organizationId: string, organizationName: string) =>
@@ -23,7 +22,7 @@ export const makeExecutionStack = (organizationId: string, organizationName: str · 4 unmodified lines
2322 const engine = withExecutionUsageTracking(
2423 organizationId,
2524 createExecutionEngine({ executor, codeExecutor }),
26− makeTrackExecutionUsage(autumn),
25+ (orgId) => Effect.runFork(autumn.trackExecution(orgId)),
2726 );
2827 return { executor, engine };
2928 });