refactor: wrap backup error in a tagged effect error
This commit is contained in:
parent
39f0ec4085
commit
c40198e7bf
9 changed files with 73 additions and 37 deletions
|
|
@ -56,26 +56,28 @@ export const backupExecutor = {
|
||||||
const volumePath = getVolumePath(volume);
|
const volumePath = getVolumePath(volume);
|
||||||
const backupOptions = createBackupOptions(schedule, volumePath, signal);
|
const backupOptions = createBackupOptions(schedule, volumePath, signal);
|
||||||
|
|
||||||
const result = await Effect.runPromise(
|
const execution = await Effect.runPromise(
|
||||||
restic.backup(repository.config, volumePath, {
|
restic
|
||||||
...backupOptions,
|
.backup(repository.config, volumePath, {
|
||||||
compressionMode: repository.compressionMode ?? "auto",
|
...backupOptions,
|
||||||
organizationId,
|
compressionMode: repository.compressionMode ?? "auto",
|
||||||
onProgress,
|
organizationId,
|
||||||
}),
|
onProgress,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
Effect.map((result) => ({ success: true as const, result })),
|
||||||
|
Effect.catchAll((error) => Effect.succeed({ success: false as const, error })),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
if (!execution.success) {
|
||||||
status: "completed",
|
throw execution.error;
|
||||||
exitCode: result.exitCode,
|
}
|
||||||
result: result.result,
|
|
||||||
warningDetails: result.warningDetails,
|
const { exitCode, result, warningDetails } = execution.result;
|
||||||
} satisfies BackupExecutionResult;
|
return { status: "completed", exitCode, result, warningDetails };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return { status: "failed", error };
|
||||||
status: "failed",
|
|
||||||
error,
|
|
||||||
} satisfies BackupExecutionResult;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel: (scheduleId: number) => {
|
cancel: (scheduleId: number) => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "agent",
|
"name": "agent",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "index.ts",
|
"module": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc --noEmit"
|
"tsc": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|
@ -15,6 +15,6 @@
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5"
|
"typescript": "^6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { type BackupCancelPayload } from "@zerobyte/contracts/agent-protocol";
|
||||||
import { logger } from "@zerobyte/core/node";
|
import { logger } from "@zerobyte/core/node";
|
||||||
import type { ControllerCommandContext } from "../context";
|
import type { ControllerCommandContext } from "../context";
|
||||||
|
|
||||||
export const handleBackupCancelCommand = (context: ControllerCommandContext, payload: BackupCancelPayload) =>
|
export const handleBackupCancelCommand = (context: ControllerCommandContext, payload: BackupCancelPayload) => {
|
||||||
Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const running = yield* context.getRunningJob(payload.jobId);
|
const running = yield* context.getRunningJob(payload.jobId);
|
||||||
if (!running) {
|
if (!running) {
|
||||||
logger.warn(`Backup ${payload.jobId} is not running`);
|
logger.warn(`Backup ${payload.jobId} is not running`);
|
||||||
|
|
@ -18,3 +18,4 @@ export const handleBackupCancelCommand = (context: ControllerCommandContext, pay
|
||||||
|
|
||||||
running.abortController.abort();
|
running.abortController.abort();
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import { createRestic } from "@zerobyte/core/restic/server";
|
||||||
import { toErrorDetails, toMessage } from "@zerobyte/core/utils";
|
import { toErrorDetails, toMessage } from "@zerobyte/core/utils";
|
||||||
import type { ControllerCommandContext } from "../context";
|
import type { ControllerCommandContext } from "../context";
|
||||||
|
|
||||||
export const handleBackupRunCommand = (context: ControllerCommandContext, payload: BackupRunPayload) =>
|
export const handleBackupRunCommand = (context: ControllerCommandContext, payload: BackupRunPayload) => {
|
||||||
Effect.fork(
|
return Effect.fork(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const existing = yield* context.getRunningJob(payload.jobId);
|
const existing = yield* context.getRunningJob(payload.jobId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
|
@ -110,3 +110,4 @@ export const handleBackupRunCommand = (context: ControllerCommandContext, payloa
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
).pipe(Effect.asVoid);
|
).pipe(Effect.asVoid);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ import type { ControllerCommandContext } from "../context";
|
||||||
|
|
||||||
type HeartbeatPingPayload = Extract<ControllerMessage, { type: "heartbeat.ping" }>["payload"];
|
type HeartbeatPingPayload = Extract<ControllerMessage, { type: "heartbeat.ping" }>["payload"];
|
||||||
|
|
||||||
export const handleHeartbeatPingCommand = (context: ControllerCommandContext, payload: HeartbeatPingPayload) =>
|
export const handleHeartbeatPingCommand = (context: ControllerCommandContext, payload: HeartbeatPingPayload) => {
|
||||||
context.offerOutbound(
|
return context.offerOutbound(
|
||||||
createAgentMessage("heartbeat.pong", {
|
createAgentMessage("heartbeat.pong", {
|
||||||
sentAt: payload.sentAt,
|
sentAt: payload.sentAt,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -106,11 +106,6 @@ export const createControllerSession = (ws: WebSocket): ControllerSession => {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onMessage: (data) => {
|
onMessage: (data) => {
|
||||||
if (typeof data !== "string") {
|
|
||||||
logger.warn("Agent received a non-text message");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Effect.runPromise(offerInbound(data as ControllerWireMessage)).catch((error) => {
|
void Effect.runPromise(offerInbound(data as ControllerWireMessage)).catch((error) => {
|
||||||
logger.error(`Failed to queue inbound message: ${toMessage(error)}`);
|
logger.error(`Failed to queue inbound message: ${toMessage(error)}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,29 @@ import { createControllerSession, type ControllerSession } from "./controller-se
|
||||||
|
|
||||||
const controllerUrl = process.env.ZEROBYTE_CONTROLLER_URL;
|
const controllerUrl = process.env.ZEROBYTE_CONTROLLER_URL;
|
||||||
const agentToken = process.env.ZEROBYTE_AGENT_TOKEN;
|
const agentToken = process.env.ZEROBYTE_AGENT_TOKEN;
|
||||||
|
const RECONNECT_DELAY_MS = 1000;
|
||||||
|
|
||||||
class Agent {
|
class Agent {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private controllerSession: ControllerSession | null = null;
|
private controllerSession: ControllerSession | null = null;
|
||||||
|
private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
private scheduleReconnect() {
|
||||||
|
if (this.reconnectTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
|
this.reconnectTimeout = null;
|
||||||
|
this.connect();
|
||||||
|
}, RECONNECT_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
|
if (this.ws) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!controllerUrl) {
|
if (!controllerUrl) {
|
||||||
throw new Error("Env variable ZEROBYTE_CONTROLLER_URL is not set");
|
throw new Error("Env variable ZEROBYTE_CONTROLLER_URL is not set");
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +53,7 @@ class Agent {
|
||||||
this.controllerSession = null;
|
this.controllerSession = null;
|
||||||
this.ws = null;
|
this.ws = null;
|
||||||
logger.info("Agent disconnected from controller");
|
logger.info("Agent disconnected from controller");
|
||||||
|
this.scheduleReconnect();
|
||||||
};
|
};
|
||||||
this.ws.onerror = (error) => {
|
this.ws.onerror = (error) => {
|
||||||
logger.error("Agent encountered an error:", error);
|
logger.error("Agent encountered an error:", error);
|
||||||
|
|
@ -43,5 +61,7 @@ class Agent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const agent = new Agent();
|
if (import.meta.main) {
|
||||||
agent.connect();
|
const agent = new Agent();
|
||||||
|
agent.connect();
|
||||||
|
}
|
||||||
|
|
|
||||||
2
bun.lock
2
bun.lock
|
|
@ -128,7 +128,7 @@
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5",
|
"typescript": "^6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/contracts": {
|
"packages/contracts": {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Effect } from "effect";
|
import { Data, Effect } from "effect";
|
||||||
import { throttle } from "es-toolkit";
|
import { throttle } from "es-toolkit";
|
||||||
import type { CompressionMode, RepositoryConfig } from "../schemas";
|
import type { CompressionMode, RepositoryConfig } from "../schemas";
|
||||||
import { type ResticBackupProgressDto, resticBackupOutputSchema, resticBackupProgressSchema } from "../restic-dto";
|
import { type ResticBackupProgressDto, resticBackupOutputSchema, resticBackupProgressSchema } from "../restic-dto";
|
||||||
|
|
@ -13,6 +13,12 @@ import { validateCustomResticParams } from "../helpers/validate-custom-params";
|
||||||
import { ResticError } from "../error";
|
import { ResticError } from "../error";
|
||||||
import { logger, safeSpawn } from "../../node";
|
import { logger, safeSpawn } from "../../node";
|
||||||
import type { ResticDeps } from "../types";
|
import type { ResticDeps } from "../types";
|
||||||
|
import { toMessage } from "../../utils";
|
||||||
|
|
||||||
|
class ResticBackupCommandError extends Data.TaggedError("ResticBackupCommandError")<{
|
||||||
|
cause: unknown;
|
||||||
|
message: string;
|
||||||
|
}> {}
|
||||||
|
|
||||||
export const backup = (
|
export const backup = (
|
||||||
config: RepositoryConfig,
|
config: RepositoryConfig,
|
||||||
|
|
@ -31,8 +37,8 @@ export const backup = (
|
||||||
customResticParams?: string[];
|
customResticParams?: string[];
|
||||||
},
|
},
|
||||||
deps: ResticDeps,
|
deps: ResticDeps,
|
||||||
) =>
|
) => {
|
||||||
Effect.tryPromise({
|
return Effect.tryPromise({
|
||||||
try: async () => {
|
try: async () => {
|
||||||
const repoUrl = buildRepoUrl(config);
|
const repoUrl = buildRepoUrl(config);
|
||||||
const env = await buildEnv(config, options.organizationId, deps);
|
const env = await buildEnv(config, options.organizationId, deps);
|
||||||
|
|
@ -214,5 +220,15 @@ export const backup = (
|
||||||
warningDetails: stderrLines.length > 0 ? stderrLines.join("\n") : null,
|
warningDetails: stderrLines.length > 0 ? stderrLines.join("\n") : null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
catch: (error) => {
|
||||||
|
if (error instanceof ResticError) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResticBackupCommandError({
|
||||||
|
cause: error,
|
||||||
|
message: toMessage(error),
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue