authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 10:54:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 10:54:12-07:00
log8c3755aeccd5fc90fe66f6ca4d776c0e370fe46e
treeb25d05b1919cf6fe97f455a482f04132fe62556f
parentb4fe8faa1d59aa602a04737aa4054ee8acb62193
signature Signed by PGP key B5690EEEBB952194

[codex] Structure bootstrap errors (#3256)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 180 insertions(+), 25 deletions(-)

apps/server/src/bootstrap.test.ts+102-5
......@@ -3,7 +3,7 @@ import * as NodeFS from "node:fs";
33import * as NodePath from "node:path";
44import * as NodeChildProcess from "node:child_process";
55import * as NodeServices from "@effect/platform-node/NodeServices";
6import { it } from "@effect/vitest";
6import { assert, it } from "@effect/vitest";
77import * as FileSystem from "effect/FileSystem";
88import * as Schema from "effect/Schema";
99import * as Duration from "effect/Duration";
......@@ -13,10 +13,19 @@ import * as TestClock from "effect/testing/TestClock";
1313import { vi } from "vite-plus/test";
1414import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1515
16import { readBootstrapEnvelope } from "./bootstrap.ts";
16import {
17 BootstrapEnvelopeDecodeError,
18 BootstrapFdStatError,
19 BootstrapInputStreamOpenError,
20 readBootstrapEnvelope,
21} from "./bootstrap.ts";
1722import { assertNone, assertSome } from "@effect/vitest/utils";
1823
19const openSyncInterceptor = vi.hoisted(() => ({ failPath: null as string | null }));
24const openSyncInterceptor = vi.hoisted(() => ({
25 failPath: null as string | null,
26 errorCode: "ENXIO",
27}));
28const fstatSyncInterceptor = vi.hoisted(() => ({ failFd: null as number | null }));
2029
2130vi.mock("node:fs", async (importOriginal) => {
2231 const actual = await importOriginal<typeof import("node:fs")>();
......@@ -29,12 +38,20 @@ vi.mock("node:fs", async (importOriginal) => {
2938 filePath === openSyncInterceptor.failPath &&
3039 flags === "r"
3140 ) {
32 const error = new Error("no such device or address");
33 Object.assign(error, { code: "ENXIO" });
41 const error = new Error(`open failed with ${openSyncInterceptor.errorCode}`);
42 Object.assign(error, { code: openSyncInterceptor.errorCode });
3443 throw error;
3544 }
3645 return (actual.openSync as (...a: typeof args) => number)(...args);
3746 },
47 fstatSync: (...args: Parameters<typeof actual.fstatSync>) => {
48 if (args[0] === fstatSyncInterceptor.failFd) {
49 const error = new Error("permission denied");
50 Object.assign(error, { code: "EACCES" });
51 throw error;
52 }
53 return (actual.fstatSync as (...a: typeof args) => NodeFS.Stats)(...args);
54 },
3855 };
3956});
4057
......@@ -94,6 +111,39 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
94111 }),
95112 );
96113
114 it.effect("preserves fd path, platform, and cause when opening the input stream fails", () =>
115 Effect.gen(function* () {
116 const fs = yield* FileSystem.FileSystem;
117 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
118 const fd = yield* Effect.acquireRelease(
119 Effect.sync(() => NodeFS.openSync(filePath, "r")),
120 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
121 );
122 const fdPath = `/proc/self/fd/${fd}`;
123
124 openSyncInterceptor.failPath = fdPath;
125 openSyncInterceptor.errorCode = "EIO";
126 try {
127 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
128 timeoutMs: 100,
129 }).pipe(Effect.provideService(HostProcessPlatform, "linux"), Effect.flip);
130
131 assert.instanceOf(error, BootstrapInputStreamOpenError);
132 assert.equal(error.fd, fd);
133 assert.equal(error.platform, "linux");
134 assert.equal(error.fdPath, fdPath);
135 assert.equal((error.cause as NodeJS.ErrnoException).code, "EIO");
136 assert.equal(
137 error.message,
138 `Failed to open bootstrap input stream for file descriptor ${fd} via '${fdPath}' on 'linux'.`,
139 );
140 } finally {
141 openSyncInterceptor.failPath = null;
142 openSyncInterceptor.errorCode = "ENXIO";
143 }
144 }),
145 );
146
97147 it.effect("returns none when the fd is unavailable", () =>
98148 Effect.gen(function* () {
99149 const fd = NodeFS.openSync("/dev/null", "r");
......@@ -104,6 +154,53 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
104154 }),
105155 );
106156
157 it.effect("preserves fd and cause when stat fails for a non-availability reason", () =>
158 Effect.gen(function* () {
159 const fd = yield* Effect.acquireRelease(
160 Effect.sync(() => NodeFS.openSync("/dev/null", "r")),
161 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
162 );
163
164 fstatSyncInterceptor.failFd = fd;
165 try {
166 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
167 timeoutMs: 100,
168 }).pipe(Effect.flip);
169
170 assert.instanceOf(error, BootstrapFdStatError);
171 assert.equal(error.fd, fd);
172 assert.equal((error.cause as NodeJS.ErrnoException).code, "EACCES");
173 assert.equal(error.message, `Failed to stat bootstrap file descriptor ${fd}.`);
174 } finally {
175 fstatSyncInterceptor.failFd = null;
176 }
177 }),
178 );
179
180 it.effect("preserves fd and schema cause when decoding the envelope fails", () =>
181 Effect.gen(function* () {
182 const fs = yield* FileSystem.FileSystem;
183 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
184 yield* fs.writeFileString(filePath, '{"mode":42}\n');
185
186 const fd = yield* Effect.acquireRelease(
187 Effect.sync(() => NodeFS.openSync(filePath, "r")),
188 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
189 );
190 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
191 timeoutMs: 100,
192 }).pipe(Effect.flip);
193
194 assert.instanceOf(error, BootstrapEnvelopeDecodeError);
195 assert.equal(error.fd, fd);
196 assert.isDefined(error.cause);
197 assert.equal(
198 error.message,
199 `Failed to decode bootstrap envelope from file descriptor ${fd}.`,
200 );
201 }),
202 );
203
107204 it.effect("returns none when the bootstrap read times out before any value arrives", () =>
108205 Effect.gen(function* () {
109206 const fs = yield* FileSystem.FileSystem;
apps/server/src/bootstrap.ts+78-20
......@@ -4,7 +4,6 @@ import * as NodeNet from "node:net";
44import * as NodeReadline from "node:readline";
55import type * as NodeStream from "node:stream";
66
7import * as Data from "effect/Data";
87import * as Effect from "effect/Effect";
98import * as Option from "effect/Option";
109import * as Predicate from "effect/Predicate";
......@@ -13,10 +12,64 @@ import * as Schema from "effect/Schema";
1312import { decodeJsonResult } from "@t3tools/shared/schemaJson";
1413import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1514
16class BootstrapError extends Data.TaggedError("BootstrapError")<{
17 readonly message: string;
18 readonly cause?: unknown;
19}> {}
15export class BootstrapFdStatError extends Schema.TaggedErrorClass<BootstrapFdStatError>()(
16 "BootstrapFdStatError",
17 {
18 fd: Schema.Number,
19 cause: Schema.Defect(),
20 },
21) {
22 override get message(): string {
23 return `Failed to stat bootstrap file descriptor ${this.fd}.`;
24 }
25}
26
27export class BootstrapInputStreamOpenError extends Schema.TaggedErrorClass<BootstrapInputStreamOpenError>()(
28 "BootstrapInputStreamOpenError",
29 {
30 fd: Schema.Number,
31 platform: Schema.String,
32 fdPath: Schema.optional(Schema.String),
33 cause: Schema.Defect(),
34 },
35) {
36 override get message(): string {
37 const path = this.fdPath === undefined ? "" : ` via '${this.fdPath}'`;
38 return `Failed to open bootstrap input stream for file descriptor ${this.fd}${path} on '${this.platform}'.`;
39 }
40}
41
42export class BootstrapEnvelopeReadError extends Schema.TaggedErrorClass<BootstrapEnvelopeReadError>()(
43 "BootstrapEnvelopeReadError",
44 {
45 fd: Schema.Number,
46 cause: Schema.Defect(),
47 },
48) {
49 override get message(): string {
50 return `Failed to read bootstrap envelope from file descriptor ${this.fd}.`;
51 }
52}
53
54export class BootstrapEnvelopeDecodeError extends Schema.TaggedErrorClass<BootstrapEnvelopeDecodeError>()(
55 "BootstrapEnvelopeDecodeError",
56 {
57 fd: Schema.Number,
58 cause: Schema.Defect(),
59 },
60) {
61 override get message(): string {
62 return `Failed to decode bootstrap envelope from file descriptor ${this.fd}.`;
63 }
64}
65
66export const BootstrapError = Schema.Union([
67 BootstrapFdStatError,
68 BootstrapInputStreamOpenError,
69 BootstrapEnvelopeReadError,
70 BootstrapEnvelopeDecodeError,
71]);
72export type BootstrapError = typeof BootstrapError.Type;
2073
2174export const readBootstrapEnvelope = Effect.fn("readBootstrapEnvelope")(function* <A, I>(
2275 schema: Schema.Codec<A, I>,
......@@ -32,7 +85,10 @@ export const readBootstrapEnvelope = Effect.fn("readBootstrapEnvelope")(function
3285
3386 const timeoutMs = options?.timeoutMs ?? 1000;
3487
35 return yield* Effect.callback<Option.Option<A>, BootstrapError>((resume) => {
88 return yield* Effect.callback<
89 Option.Option<A>,
90 BootstrapEnvelopeReadError | BootstrapEnvelopeDecodeError
91 >((resume) => {
3692 const input = NodeReadline.createInterface({
3793 input: stream,
3894 crlfDelay: Infinity,
......@@ -53,8 +109,8 @@ export const readBootstrapEnvelope = Effect.fn("readBootstrapEnvelope")(function
53109 }
54110 resume(
55111 Effect.fail(
56 new BootstrapError({
57 message: "Failed to read bootstrap envelope.",
112 new BootstrapEnvelopeReadError({
113 fd,
58114 cause: error,
59115 }),
60116 ),
......@@ -68,8 +124,8 @@ export const readBootstrapEnvelope = Effect.fn("readBootstrapEnvelope")(function
68124 } else {
69125 resume(
70126 Effect.fail(
71 new BootstrapError({
72 message: "Failed to decode bootstrap envelope.",
127 new BootstrapEnvelopeDecodeError({
128 fd,
73129 cause: parsed.failure,
74130 }),
75131 ),
......@@ -98,24 +154,24 @@ const isFdReady = (fd: number) =>
98154 Effect.try({
99155 try: () => NodeFS.fstatSync(fd),
100156 catch: (error) =>
101 new BootstrapError({
102 message: "Failed to stat bootstrap fd.",
157 new BootstrapFdStatError({
158 fd,
103159 cause: error,
104160 }),
105161 }).pipe(
106162 Effect.as(true),
107 Effect.catchIf(
108 (error) => isUnavailableBootstrapFdError(error.cause),
109 () => Effect.succeed(false),
110 ),
163 Effect.catchTags({
164 BootstrapFdStatError: (error) =>
165 isUnavailableBootstrapFdError(error.cause) ? Effect.succeed(false) : Effect.fail(error),
166 }),
111167 );
112168
113169const makeBootstrapInputStream = (fd: number) =>
114170 Effect.gen(function* () {
115171 const platform = yield* HostProcessPlatform;
116 return yield* Effect.try<NodeStream.Readable, BootstrapError>({
172 const fdPath = resolveFdPath(fd, platform);
173 return yield* Effect.try<NodeStream.Readable, BootstrapInputStreamOpenError>({
117174 try: () => {
118 const fdPath = resolveFdPath(fd, platform);
119175 if (fdPath === undefined) {
120176 return makeDirectBootstrapStream(fd);
121177 }
......@@ -139,8 +195,10 @@ const makeBootstrapInputStream = (fd: number) =>
139195 }
140196 },
141197 catch: (error) =>
142 new BootstrapError({
143 message: "Failed to duplicate bootstrap fd.",
198 new BootstrapInputStreamOpenError({
199 fd,
200 platform,
201 ...(fdPath === undefined ? {} : { fdPath }),
144202 cause: error,
145203 }),
146204 });