History

Structure server environment ID failures (#3286)

Julius Marminge committed 4 months ago

Commit 708bc701 parent 7eb7b4f2 files · +98 −57Browse files

Unified · Split

2 files
  1. apps/server/src/environment/ServerEnvironment.test.ts +53 −50
  2. apps/server/src/environment/ServerEnvironment.ts +45 −7
apps/server/src/environment/ServerEnvironment.test.ts+53−50
@@ -1,16 +1,18 @@
1−// @effect-diagnostics nodeBuiltinImport:off
2−import * as NodePath from "node:path";
31 import * as NodeServices from "@effect/platform-node/NodeServices";
42 import { expect, it } from "@effect/vitest";
53 import * as Effect from "effect/Effect";
6−import * as Exit from "effect/Exit";
74 import * as FileSystem from "effect/FileSystem";
85 import * as Layer from "effect/Layer";
96 import * as PlatformError from "effect/PlatformError";
7+import * as Schema from "effect/Schema";
108
119 import * as ServerConfig from "../config.ts";
1210 import * as ServerEnvironment from "./ServerEnvironment.ts";
1311
12+const isServerEnvironmentIdPersistenceError = Schema.is(
13+ ServerEnvironment.ServerEnvironmentIdPersistenceError,
14+);
15+
1416 const 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) => { · 51 unmodified lines
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"; · 5 unmodified lines
66 import * as FileSystem from "effect/FileSystem";
77 import * as Layer from "effect/Layer";
88 import * as Path from "effect/Path";
9+import * as Schema from "effect/Schema";
910
1011 import packageJson from "../../package.json" with { type: "json" };
1112 import * as ServerConfig from "../config.ts";
1213 import * as ProcessRunner from "../processRunner.ts";
1314 import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts";
1415
16+export 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+
1529 export class ServerEnvironment extends Context.Service<
1630 ServerEnvironment,
1731 {
@@ -55,22 +69,46 @@ export const make = Effect.gen(function* () { · 37 unmodified lines
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;