authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:31:57-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:31:57-07:00
log708bc7091dcdf5073a374464ffcbe45ba520b8fb
tree87fa77bddefa338d5ec687fb7d30dc92e8a78d3e
parent7eb7b4f4a36aea3b858494e5983ee9bc9a288933
signature Signed by PGP key B5690EEEBB952194

Structure server environment ID failures (#3286)

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

2 files changed, 98 insertions(+), 57 deletions(-)

apps/server/src/environment/ServerEnvironment.test.ts+53-50
......@@ -1,16 +1,18 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodePath from "node:path";
31import * as NodeServices from "@effect/platform-node/NodeServices";
42import { expect, it } from "@effect/vitest";
53import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
74import * as FileSystem from "effect/FileSystem";
85import * as Layer from "effect/Layer";
96import * as PlatformError from "effect/PlatformError";
7import * as Schema from "effect/Schema";
108
119import * as ServerConfig from "../config.ts";
1210import * as ServerEnvironment from "./ServerEnvironment.ts";
1311
12const isServerEnvironmentIdPersistenceError = Schema.is(
13 ServerEnvironment.ServerEnvironmentIdPersistenceError,
14);
15
1416const makeServerEnvironmentLayer = (baseDir: string) =>
1517 ServerEnvironment.layer.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
1618
......@@ -68,62 +70,63 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
6870 }),
6971 );
7072
71 it.effect("fails instead of overwriting a persisted id when reading the file errors", () =>
73 it.effect("structures persisted environment id filesystem failures", () =>
7274 Effect.gen(function* () {
7375 const fileSystem = yield* FileSystem.FileSystem;
7476 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
75 prefix: "t3-server-environment-read-error-test-",
77 prefix: "t3-server-environment-error-test-",
7678 });
7779 const serverConfig = yield* makeServerConfig(baseDir);
7880 const environmentIdPath = serverConfig.environmentIdPath;
79 yield* fileSystem.makeDirectory(NodePath.dirname(environmentIdPath), { recursive: true });
80 yield* fileSystem.writeFileString(environmentIdPath, "persisted-environment-id\n");
81 const writeAttempts: string[] = [];
82 const failingFileSystemLayer = FileSystem.layerNoop({
83 exists: (path) => Effect.succeed(path === environmentIdPath),
84 readFileString: (path) =>
85 path === environmentIdPath
86 ? Effect.fail(
87 PlatformError.systemError({
88 _tag: "PermissionDenied",
89 module: "FileSystem",
90 method: "readFileString",
91 description: "permission denied",
92 pathOrDescriptor: path,
93 }),
94 )
95 : Effect.fail(
96 PlatformError.systemError({
97 _tag: "NotFound",
98 module: "FileSystem",
99 method: "readFileString",
100 description: "not found",
101 pathOrDescriptor: path,
102 }),
103 ),
104 writeFileString: (path) => {
105 writeAttempts.push(path);
106 return Effect.void;
107 },
108 });
81 const methodByOperation = {
82 check: "exists",
83 read: "readFileString",
84 write: "writeFileString",
85 } as const;
10986
110 const exit = yield* Effect.gen(function* () {
111 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
112 return yield* serverEnvironment.getDescriptor;
113 }).pipe(
114 Effect.provide(
115 ServerEnvironment.layer.pipe(
116 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
87 for (const operation of ["check", "read", "write"] as const) {
88 const writeAttempts: string[] = [];
89 const cause = PlatformError.systemError({
90 _tag: "PermissionDenied",
91 module: "FileSystem",
92 method: methodByOperation[operation],
93 description: "permission denied",
94 pathOrDescriptor: environmentIdPath,
95 });
96 const failingFileSystemLayer = FileSystem.layerNoop({
97 exists: () =>
98 operation === "check" ? Effect.fail(cause) : Effect.succeed(operation === "read"),
99 readFileString: () => Effect.fail(cause),
100 writeFileString: (path) => {
101 writeAttempts.push(path);
102 return Effect.fail(cause);
103 },
104 });
105
106 const error = yield* Effect.gen(function* () {
107 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
108 return yield* serverEnvironment.getDescriptor;
109 }).pipe(
110 Effect.provide(
111 ServerEnvironment.layer.pipe(
112 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),
113 ),
117114 ),
118 ),
119 Effect.exit,
120 );
115 Effect.flip,
116 );
121117
122 expect(Exit.isFailure(exit)).toBe(true);
123 expect(writeAttempts).toEqual([]);
124 expect(yield* fileSystem.readFileString(environmentIdPath)).toBe(
125 "persisted-environment-id\n",
126 );
118 expect(isServerEnvironmentIdPersistenceError(error)).toBe(true);
119 if (!isServerEnvironmentIdPersistenceError(error)) {
120 throw error;
121 }
122 expect(error.operation).toBe(operation);
123 expect(error.environmentIdPath).toBe(environmentIdPath);
124 expect(error.cause).toBe(cause);
125 expect(error.message).toBe(
126 `Server environment ID ${operation} failed at '${environmentIdPath}'.`,
127 );
128 expect(writeAttempts).toEqual(operation === "write" ? [environmentIdPath] : []);
129 }
127130 }),
128131 );
129132});
apps/server/src/environment/ServerEnvironment.ts+45-7
......@@ -6,12 +6,26 @@ import * as Effect from "effect/Effect";
66import * as FileSystem from "effect/FileSystem";
77import * as Layer from "effect/Layer";
88import * as Path from "effect/Path";
9import * as Schema from "effect/Schema";
910
1011import packageJson from "../../package.json" with { type: "json" };
1112import * as ServerConfig from "../config.ts";
1213import * as ProcessRunner from "../processRunner.ts";
1314import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
1415
16export class ServerEnvironmentIdPersistenceError extends Schema.TaggedErrorClass<ServerEnvironmentIdPersistenceError>()(
17 "ServerEnvironmentIdPersistenceError",
18 {
19 operation: Schema.Literals(["check", "read", "write"]),
20 environmentIdPath: Schema.String,
21 cause: Schema.Defect(),
22 },
23) {
24 override get message(): string {
25 return `Server environment ID ${this.operation} failed at '${this.environmentIdPath}'.`;
26 }
27}
28
1529export class ServerEnvironment extends Context.Service<
1630 ServerEnvironment,
1731 {
......@@ -55,22 +69,46 @@ export const make = Effect.gen(function* () {
5569 const hostArchitecture = yield* HostProcessArchitecture;
5670
5771 const readPersistedEnvironmentId = Effect.gen(function* () {
58 const exists = yield* fileSystem
59 .exists(serverConfig.environmentIdPath)
60 .pipe(Effect.orElseSucceed(() => false));
72 const exists = yield* fileSystem.exists(serverConfig.environmentIdPath).pipe(
73 Effect.mapError(
74 (cause) =>
75 new ServerEnvironmentIdPersistenceError({
76 operation: "check",
77 environmentIdPath: serverConfig.environmentIdPath,
78 cause,
79 }),
80 ),
81 );
6182 if (!exists) {
6283 return null;
6384 }
6485
65 const raw = yield* fileSystem
66 .readFileString(serverConfig.environmentIdPath)
67 .pipe(Effect.map((value) => value.trim()));
86 const raw = yield* fileSystem.readFileString(serverConfig.environmentIdPath).pipe(
87 Effect.map((value) => value.trim()),
88 Effect.mapError(
89 (cause) =>
90 new ServerEnvironmentIdPersistenceError({
91 operation: "read",
92 environmentIdPath: serverConfig.environmentIdPath,
93 cause,
94 }),
95 ),
96 );
6897
6998 return raw.length > 0 ? raw : null;
7099 });
71100
72101 const persistEnvironmentId = (value: string) =>
73 fileSystem.writeFileString(serverConfig.environmentIdPath, `${value}\n`);
102 fileSystem.writeFileString(serverConfig.environmentIdPath, `${value}\n`).pipe(
103 Effect.mapError(
104 (cause) =>
105 new ServerEnvironmentIdPersistenceError({
106 operation: "write",
107 environmentIdPath: serverConfig.environmentIdPath,
108 cause,
109 }),
110 ),
111 );
74112
75113 const environmentIdRaw = yield* Effect.gen(function* () {
76114 const persisted = yield* readPersistedEnvironmentId;