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,16 +1,18 @@
1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodePath from "node:path";
3import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
4import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
5import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as FileSystem from "effect/FileSystem";4import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";6import * as PlatformError from "effect/PlatformError";
7import * as Schema from "effect/Schema";
108
11import * as ServerConfig from "../config.ts";9import * as ServerConfig from "../config.ts";
12import * as ServerEnvironment from "./ServerEnvironment.ts";10import * as ServerEnvironment from "./ServerEnvironment.ts";
1311
12const isServerEnvironmentIdPersistenceError = Schema.is(
13 ServerEnvironment.ServerEnvironmentIdPersistenceError,
14);
15
14const makeServerEnvironmentLayer = (baseDir: string) =>16const makeServerEnvironmentLayer = (baseDir: string) =>
15 ServerEnvironment.layer.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));17 ServerEnvironment.layer.pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), baseDir)));
1618
...@@ -68,62 +70,63 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {...@@ -68,62 +70,63 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
68 }),70 }),
69 );71 );
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", () =>
72 Effect.gen(function* () {74 Effect.gen(function* () {
73 const fileSystem = yield* FileSystem.FileSystem;75 const fileSystem = yield* FileSystem.FileSystem;
74 const baseDir = yield* fileSystem.makeTempDirectoryScoped({76 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
75 prefix: "t3-server-environment-read-error-test-",77 prefix: "t3-server-environment-error-test-",
76 });78 });
77 const serverConfig = yield* makeServerConfig(baseDir);79 const serverConfig = yield* makeServerConfig(baseDir);
78 const environmentIdPath = serverConfig.environmentIdPath;80 const environmentIdPath = serverConfig.environmentIdPath;
79 yield* fileSystem.makeDirectory(NodePath.dirname(environmentIdPath), { recursive: true });81 const methodByOperation = {
80 yield* fileSystem.writeFileString(environmentIdPath, "persisted-environment-id\n");82 check: "exists",
81 const writeAttempts: string[] = [];83 read: "readFileString",
82 const failingFileSystemLayer = FileSystem.layerNoop({84 write: "writeFileString",
83 exists: (path) => Effect.succeed(path === environmentIdPath),85 } as const;
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 });
10986
110 const exit = yield* Effect.gen(function* () {87 for (const operation of ["check", "read", "write"] as const) {
111 const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;88 const writeAttempts: string[] = [];
112 return yield* serverEnvironment.getDescriptor;89 const cause = PlatformError.systemError({
113 }).pipe(90 _tag: "PermissionDenied",
114 Effect.provide(91 module: "FileSystem",
115 ServerEnvironment.layer.pipe(92 method: methodByOperation[operation],
116 Layer.provide(Layer.merge(ServerConfig.layer(serverConfig), failingFileSystemLayer)),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 ),
117 ),114 ),
118 ),115 Effect.flip,
119 Effect.exit,116 );
120 );
121117
122 expect(Exit.isFailure(exit)).toBe(true);118 expect(isServerEnvironmentIdPersistenceError(error)).toBe(true);
123 expect(writeAttempts).toEqual([]);119 if (!isServerEnvironmentIdPersistenceError(error)) {
124 expect(yield* fileSystem.readFileString(environmentIdPath)).toBe(120 throw error;
125 "persisted-environment-id\n",121 }
126 );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 }
127 }),130 }),
128 );131 );
129});132});
apps/server/src/environment/ServerEnvironment.ts+45-7
...@@ -6,12 +6,26 @@ import * as Effect from "effect/Effect";...@@ -6,12 +6,26 @@ import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
8import * as Path from "effect/Path";8import * as Path from "effect/Path";
9import * as Schema from "effect/Schema";
910
10import packageJson from "../../package.json" with { type: "json" };11import packageJson from "../../package.json" with { type: "json" };
11import * as ServerConfig from "../config.ts";12import * as ServerConfig from "../config.ts";
12import * as ProcessRunner from "../processRunner.ts";13import * as ProcessRunner from "../processRunner.ts";
13import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";14import { 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
15export class ServerEnvironment extends Context.Service<29export class ServerEnvironment extends Context.Service<
16 ServerEnvironment,30 ServerEnvironment,
17 {31 {
...@@ -55,22 +69,46 @@ export const make = Effect.gen(function* () {...@@ -55,22 +69,46 @@ export const make = Effect.gen(function* () {
55 const hostArchitecture = yield* HostProcessArchitecture;69 const hostArchitecture = yield* HostProcessArchitecture;
5670
57 const readPersistedEnvironmentId = Effect.gen(function* () {71 const readPersistedEnvironmentId = Effect.gen(function* () {
58 const exists = yield* fileSystem72 const exists = yield* fileSystem.exists(serverConfig.environmentIdPath).pipe(
59 .exists(serverConfig.environmentIdPath)73 Effect.mapError(
60 .pipe(Effect.orElseSucceed(() => false));74 (cause) =>
75 new ServerEnvironmentIdPersistenceError({
76 operation: "check",
77 environmentIdPath: serverConfig.environmentIdPath,
78 cause,
79 }),
80 ),
81 );
61 if (!exists) {82 if (!exists) {
62 return null;83 return null;
63 }84 }
6485
65 const raw = yield* fileSystem86 const raw = yield* fileSystem.readFileString(serverConfig.environmentIdPath).pipe(
66 .readFileString(serverConfig.environmentIdPath)87 Effect.map((value) => value.trim()),
67 .pipe(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
69 return raw.length > 0 ? raw : null;98 return raw.length > 0 ? raw : null;
70 });99 });
71100
72 const persistEnvironmentId = (value: string) =>101 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
75 const environmentIdRaw = yield* Effect.gen(function* () {113 const environmentIdRaw = yield* Effect.gen(function* () {
76 const persisted = yield* readPersistedEnvironmentId;114 const persisted = yield* readPersistedEnvironmentId;