| 1 | // @effect-diagnostics nodeBuiltinImport:off - v8.writeHeapSnapshot has no Effect equivalent. |
| 2 | import * as NodePath from "node:path"; |
| 3 | import * as NodeV8 from "node:v8"; |
| 4 | |
| 5 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 6 | import * as DateTime from "effect/DateTime"; |
| 7 | import * as Effect from "effect/Effect"; |
| 8 | import * as FileSystem from "effect/FileSystem"; |
| 9 | import * as Layer from "effect/Layer"; |
| 10 | |
| 11 | import * as ServerConfig from "../config.ts"; |
| 12 | |
| 13 | /** |
| 14 | * Writes one V8 heap snapshot into `logsDir` and logs its path. A failed write |
| 15 | * logs a warning and removes any partial file, because that file can hold |
| 16 | * secrets and the failure is often a full disk. |
| 17 | */ |
| 18 | export const writeHeapSnapshot = Effect.fn("server.heapSnapshot", { root: true })( |
| 19 | function* (logsDir: string) { |
| 20 | const fs = yield* FileSystem.FileSystem; |
| 21 | const timestamp = DateTime.formatIso(yield* DateTime.now).replaceAll(":", "-"); |
| 22 | const path = NodePath.join(logsDir, `server-${process.pid}-${timestamp}.heapsnapshot`); |
| 23 | yield* Effect.annotateCurrentSpan({ path }); |
| 24 | yield* Effect.try(() => NodeV8.writeHeapSnapshot(path)).pipe( |
| 25 | Effect.tapError(() => fs.remove(path, { force: true }).pipe(Effect.ignore)), |
| 26 | ); |
| 27 | yield* Effect.logInfo("Wrote heap snapshot.", { path }); |
| 28 | }, |
| 29 | Effect.catch((cause) => Effect.logWarning("Failed to write heap snapshot.", { cause })), |
| 30 | ); |
| 31 | |
| 32 | /** |
| 33 | * Writes a heap snapshot when the process gets SIGUSR2 (`kill -USR2 <pid>`), |
| 34 | * so a maintainer can see what a long-running server holds. See "Heap |
| 35 | * Snapshots" in docs/operations/observability.md. |
| 36 | * |
| 37 | * The write blocks the event loop, so two snapshots never overlap: a signal |
| 38 | * sent during a write waits until it finishes. Windows has no SIGUSR2, so the |
| 39 | * layer does nothing there. |
| 40 | */ |
| 41 | export const layer = Layer.effectDiscard( |
| 42 | Effect.gen(function* () { |
| 43 | if ((yield* HostProcess.Platform) === "win32") return; |
| 44 | const { logsDir } = yield* ServerConfig.ServerConfig; |
| 45 | const runFork = Effect.runForkWith(yield* Effect.context<FileSystem.FileSystem>()); |
| 46 | const onSignal = () => void runFork(writeHeapSnapshot(logsDir)); |
| 47 | yield* Effect.acquireRelease( |
| 48 | Effect.sync(() => process.on("SIGUSR2", onSignal)), |
| 49 | () => Effect.sync(() => process.off("SIGUSR2", onSignal)), |
| 50 | ); |
| 51 | }), |
| 52 | ); |