Public: anyone can read this; the crew can change it.

The apps runtime stops writing to its meter before the database closes #3511

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3496-the-apps-runtime-stops-writing-to-its-me → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/3496-the-apps-runtime-stops-writing-to-its-me was merged into main as 9d13d84.

Review

Sign in to review this change.
2 files changed +54 −1
src/apps/runtime.ts +17
@@ -407,6 +407,11 @@ export class AppsRuntime {
407407 ) {
408408 this.gate = options.gate ?? new Gate(options.concurrency ?? 2);
409409 runtimes.set(app, this);
410 // Closing the forge closes the database next: end every open awake
411 // interval now, while it can be written, and write nothing after.
412 app.closing.signal.addEventListener("abort", () => this.closeMeter(), {

This listener is never removed. Detach it in stop() so a stopped runtime does not close intervals again when the forge closes.

413 once: true,
414 });
410415 }
411416
412417 /** The gate this runtime's deploys wait at; a hosted child shares it. */
@@ -1900,6 +1905,7 @@ export class AppsRuntime {
19001905 * worth taking a deploy, a sleep or a shutdown down with it.
19011906 */
19021907 private metered<T>(write: () => T): T | undefined {
1908 if (this.app.closing.signal.aborted) return undefined;
19031909 try {
19041910 return write();
19051911 } catch (error) {
@@ -1908,6 +1914,17 @@ export class AppsRuntime {
19081914 }
19091915 }
19101916
1917 /** The forge is closing: stop the sweeper, end open intervals, then the meter goes quiet. */
1918 private closeMeter(): void {
1919 this.stopped = true;
1920 if (this.sweeper !== undefined) clearInterval(this.sweeper);

closeMeter repeats the closeAllAwake-and-log shape that metered() already has. A small private helper that both stop() and closeMeter call would keep one copy.

1921 try {
1922 closeAllAwake(this.app.db, new Date().toISOString());
1923 } catch (error) {
1924 log.error("meter write failed", { error: String(error) });
1925 }
1926 }
1927
19111928 /** The environment began serving: its awake interval opens, if not open. */
19121929 private awake(state: Environment, at: string): void {
19131930 this.metered(() =>
test/meter.test.ts +37 −1
@@ -2,7 +2,8 @@
22 * The meter: awake intervals and storage samples, and the two sums read
33 * from them — awake seconds and GB-hours — checked against a hand count.
44 */
5 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
5 import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
6 import { Database } from "bun:sqlite";
67 import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
78 import { tmpdir } from "node:os";
89 import { join } from "node:path";
@@ -260,3 +261,38 @@ describe("the runtime meters what it runs", () => {
260261 );
261262 }, 30_000);
262263 });
264
265 describe("closing the forge", () => {
266 test("ends the open interval while the database is open and writes nothing after", async () => {
267 const dataDir = await mkdtemp(join(tmpdir(), "forge-meter-close-"));
268 const lines: string[] = [];
269 const spy = spyOn(console, "error").mockImplementation((...args: unknown[]) => {
270 lines.push(args.map(String).join(" "));
271 });
272 try {
273 const app = createApp({ dataDir, port: 0 });
274 const runtime = new AppsRuntime(app, {
275 domain: "apps.test",
276 scheme: "http",
277 forgeUrl: "http://localhost",
278 sweepEveryMs: 20,
279 });
280 await runtime.start();
281 openAwake(app.db, { repoId: "r9", env: "production", placement: "owned", at: T("09:00") });
282 app.close();
283 // The sweeper would have beaten the meter by now, on a closed database.
284 await Bun.sleep(80);
285 await runtime.stop();
286 expect(lines.filter((l) => l.includes("meter write failed"))).toEqual([]);
287 const db = new Database(join(dataDir, "forge.db"), { readonly: true });
288 const row = db
289 .query<MeterRow, []>("SELECT * FROM meter WHERE repo_id = 'r9'")
290 .get();
291 db.close();
292 expect(row!.ended_at).not.toBeNull();
293 } finally {
294 spy.mockRestore();
295 await rm(dataDir, { recursive: true, force: true });
296 }
297 });
298 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs