feat(backups): configure backup webhook timeout
This commit is contained in:
parent
38f5a669ae
commit
9ae8017df1
10 changed files with 19 additions and 5 deletions
|
|
@ -45,6 +45,7 @@ describe("parseConfig", () => {
|
||||||
ENABLE_DEV_PANEL: "true",
|
ENABLE_DEV_PANEL: "true",
|
||||||
SERVER_IP: "0.0.0.0",
|
SERVER_IP: "0.0.0.0",
|
||||||
SERVER_IDLE_TIMEOUT: "120",
|
SERVER_IDLE_TIMEOUT: "120",
|
||||||
|
WEBHOOK_TIMEOUT: "90",
|
||||||
PORT: "8080",
|
PORT: "8080",
|
||||||
APP_VERSION: "1.2.3",
|
APP_VERSION: "1.2.3",
|
||||||
MIGRATIONS_PATH: "/tmp/migrations",
|
MIGRATIONS_PATH: "/tmp/migrations",
|
||||||
|
|
@ -57,6 +58,7 @@ describe("parseConfig", () => {
|
||||||
environment: "development",
|
environment: "development",
|
||||||
serverIp: "0.0.0.0",
|
serverIp: "0.0.0.0",
|
||||||
serverIdleTimeout: 120,
|
serverIdleTimeout: 120,
|
||||||
|
webhookTimeout: 90,
|
||||||
resticHostname: "configured-restic-host",
|
resticHostname: "configured-restic-host",
|
||||||
port: 8080,
|
port: 8080,
|
||||||
migrationsPath: "/tmp/migrations",
|
migrationsPath: "/tmp/migrations",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ const envSchema = z
|
||||||
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
|
NODE_ENV: z.enum(["development", "production", "test"]).default("production"),
|
||||||
SERVER_IP: z.string().default("localhost"),
|
SERVER_IP: z.string().default("localhost"),
|
||||||
SERVER_IDLE_TIMEOUT: z.coerce.number().int().default(60),
|
SERVER_IDLE_TIMEOUT: z.coerce.number().int().default(60),
|
||||||
|
WEBHOOK_TIMEOUT: z.coerce.number().int().default(60),
|
||||||
RESTIC_HOSTNAME: z.string().optional(),
|
RESTIC_HOSTNAME: z.string().optional(),
|
||||||
PORT: z.coerce.number().int().default(4096),
|
PORT: z.coerce.number().int().default(4096),
|
||||||
MIGRATIONS_PATH: z.string().optional(),
|
MIGRATIONS_PATH: z.string().optional(),
|
||||||
|
|
@ -122,6 +123,7 @@ const envSchema = z
|
||||||
environment: s.NODE_ENV,
|
environment: s.NODE_ENV,
|
||||||
serverIp: s.SERVER_IP,
|
serverIp: s.SERVER_IP,
|
||||||
serverIdleTimeout: s.SERVER_IDLE_TIMEOUT,
|
serverIdleTimeout: s.SERVER_IDLE_TIMEOUT,
|
||||||
|
webhookTimeout: s.WEBHOOK_TIMEOUT,
|
||||||
resticHostname: s.RESTIC_HOSTNAME || getResticHostname(),
|
resticHostname: s.RESTIC_HOSTNAME || getResticHostname(),
|
||||||
port: s.PORT,
|
port: s.PORT,
|
||||||
migrationsPath: s.MIGRATIONS_PATH,
|
migrationsPath: s.MIGRATIONS_PATH,
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,7 @@ test("close emits a synthetic backup.cancelled for a queued backup", () => {
|
||||||
},
|
},
|
||||||
webhooks: { pre: null, post: null },
|
webhooks: { pre: null, post: null },
|
||||||
webhookAllowedOrigins: [],
|
webhookAllowedOrigins: [],
|
||||||
|
webhookTimeoutMs: 60_000,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ const createBackupRunPayload = async ({
|
||||||
},
|
},
|
||||||
webhooks: schedule.backupWebhooks ?? { pre: null, post: null },
|
webhooks: schedule.backupWebhooks ?? { pre: null, post: null },
|
||||||
webhookAllowedOrigins: config.webhookAllowedOrigins,
|
webhookAllowedOrigins: config.webhookAllowedOrigins,
|
||||||
|
webhookTimeoutMs: config.webhookTimeout * 1000,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,6 +83,7 @@ const executeBackupWithoutAgent = async (
|
||||||
options: payload.options,
|
options: payload.options,
|
||||||
webhooks: payload.webhooks,
|
webhooks: payload.webhooks,
|
||||||
webhookAllowedOrigins: payload.webhookAllowedOrigins,
|
webhookAllowedOrigins: payload.webhookAllowedOrigins,
|
||||||
|
webhookTimeoutMs: payload.webhookTimeoutMs,
|
||||||
signal,
|
signal,
|
||||||
onProgress,
|
onProgress,
|
||||||
formatError: toErrorDetails,
|
formatError: toErrorDetails,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ test("emits backup.failed when a backup command hits a restic error", async () =
|
||||||
},
|
},
|
||||||
webhooks: { pre: null, post: null },
|
webhooks: { pre: null, post: null },
|
||||||
webhookAllowedOrigins: [],
|
webhookAllowedOrigins: [],
|
||||||
|
webhookTimeoutMs: 60_000,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ const createRunPayload = (overrides: Partial<BackupRunPayload> = {}) =>
|
||||||
},
|
},
|
||||||
webhooks: { pre: null, post: null },
|
webhooks: { pre: null, post: null },
|
||||||
webhookAllowedOrigins: ["http://localhost:8080"],
|
webhookAllowedOrigins: ["http://localhost:8080"],
|
||||||
|
webhookTimeoutMs: 60_000,
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -337,6 +338,7 @@ test("waits for running-job registration before returning to the processor loop"
|
||||||
},
|
},
|
||||||
webhooks: { pre: null, post: null },
|
webhooks: { pre: null, post: null },
|
||||||
webhookAllowedOrigins: [],
|
webhookAllowedOrigins: [],
|
||||||
|
webhookTimeoutMs: 60_000,
|
||||||
});
|
});
|
||||||
const cancelPayload = fromPartial<BackupCancelPayload>({
|
const cancelPayload = fromPartial<BackupCancelPayload>({
|
||||||
jobId: "job-1",
|
jobId: "job-1",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export const handleBackupRunCommand = (context: ControllerCommandContext, payloa
|
||||||
options: payload.options,
|
options: payload.options,
|
||||||
webhooks: payload.webhooks,
|
webhooks: payload.webhooks,
|
||||||
webhookAllowedOrigins: payload.webhookAllowedOrigins,
|
webhookAllowedOrigins: payload.webhookAllowedOrigins,
|
||||||
|
webhookTimeoutMs: payload.webhookTimeoutMs,
|
||||||
signal: abortController.signal,
|
signal: abortController.signal,
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
void Runtime.runPromise(
|
void Runtime.runPromise(
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ const backupRunSchema = z.object({
|
||||||
runtime: backupRuntimeSchema,
|
runtime: backupRuntimeSchema,
|
||||||
webhooks: backupWebhooksSchema,
|
webhooks: backupWebhooksSchema,
|
||||||
webhookAllowedOrigins: z.array(z.string()),
|
webhookAllowedOrigins: z.array(z.string()),
|
||||||
|
webhookTimeoutMs: z.number(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ const runWithHooks = <TResult>(
|
||||||
options: {},
|
options: {},
|
||||||
webhooks: { pre: null, post: null },
|
webhooks: { pre: null, post: null },
|
||||||
webhookAllowedOrigins: ["http://localhost:8080"],
|
webhookAllowedOrigins: ["http://localhost:8080"],
|
||||||
|
webhookTimeoutMs: 60_000,
|
||||||
signal: defaultSignal(),
|
signal: defaultSignal(),
|
||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { z } from "zod";
|
||||||
import type { CompressionMode, RepositoryConfig, ResticBackupProgressDto } from "../restic/index.js";
|
import type { CompressionMode, RepositoryConfig, ResticBackupProgressDto } from "../restic/index.js";
|
||||||
import { toErrorDetails, toMessage } from "../utils/index.js";
|
import { toErrorDetails, toMessage } from "../utils/index.js";
|
||||||
|
|
||||||
const DEFAULT_BACKUP_WEBHOOK_TIMEOUT_MS = 10_000;
|
|
||||||
const MAX_BACKUP_WEBHOOK_BODY_BYTES = 64 * 1024;
|
const MAX_BACKUP_WEBHOOK_BODY_BYTES = 64 * 1024;
|
||||||
const MAX_BACKUP_WEBHOOK_HEADERS = 32;
|
const MAX_BACKUP_WEBHOOK_HEADERS = 32;
|
||||||
const MAX_BACKUP_WEBHOOK_HEADER_BYTES = 8 * 1024;
|
const MAX_BACKUP_WEBHOOK_HEADER_BYTES = 8 * 1024;
|
||||||
|
|
@ -82,6 +81,7 @@ type BackupLifecycleOptions<TResult> = {
|
||||||
options: BackupOptions;
|
options: BackupOptions;
|
||||||
webhooks: BackupWebhooks;
|
webhooks: BackupWebhooks;
|
||||||
webhookAllowedOrigins: readonly string[];
|
webhookAllowedOrigins: readonly string[];
|
||||||
|
webhookTimeoutMs: number;
|
||||||
signal: AbortSignal;
|
signal: AbortSignal;
|
||||||
onProgress?: (progress: ResticBackupProgressDto) => void;
|
onProgress?: (progress: ResticBackupProgressDto) => void;
|
||||||
formatError?: (error: unknown) => string;
|
formatError?: (error: unknown) => string;
|
||||||
|
|
@ -190,7 +190,7 @@ const runBackupWebhook = (
|
||||||
formatError: (error: unknown) => string;
|
formatError: (error: unknown) => string;
|
||||||
allowedOrigins: readonly string[];
|
allowedOrigins: readonly string[];
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
timeoutMs?: number;
|
timeoutMs: number;
|
||||||
},
|
},
|
||||||
) =>
|
) =>
|
||||||
Effect.suspend(() => {
|
Effect.suspend(() => {
|
||||||
|
|
@ -198,8 +198,7 @@ const runBackupWebhook = (
|
||||||
return Effect.succeed(null);
|
return Effect.succeed(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeoutMs = options.timeoutMs ?? DEFAULT_BACKUP_WEBHOOK_TIMEOUT_MS;
|
const controller = createAbortController(options.timeoutMs, options.signal);
|
||||||
const controller = createAbortController(timeoutMs, options.signal);
|
|
||||||
|
|
||||||
return Effect.tryPromise({
|
return Effect.tryPromise({
|
||||||
try: async () => {
|
try: async () => {
|
||||||
|
|
@ -256,6 +255,7 @@ export const runBackupLifecycle = <TResult>({
|
||||||
options,
|
options,
|
||||||
webhooks,
|
webhooks,
|
||||||
webhookAllowedOrigins,
|
webhookAllowedOrigins,
|
||||||
|
webhookTimeoutMs,
|
||||||
signal,
|
signal,
|
||||||
onProgress,
|
onProgress,
|
||||||
formatError = toErrorDetails,
|
formatError = toErrorDetails,
|
||||||
|
|
@ -268,6 +268,7 @@ export const runBackupLifecycle = <TResult>({
|
||||||
{
|
{
|
||||||
formatError,
|
formatError,
|
||||||
allowedOrigins: webhookAllowedOrigins,
|
allowedOrigins: webhookAllowedOrigins,
|
||||||
|
timeoutMs: webhookTimeoutMs,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -312,7 +313,7 @@ export const runBackupLifecycle = <TResult>({
|
||||||
status: backupResult.hookStatus,
|
status: backupResult.hookStatus,
|
||||||
error: backupResult.hookError,
|
error: backupResult.hookError,
|
||||||
},
|
},
|
||||||
{ formatError, allowedOrigins: webhookAllowedOrigins },
|
{ formatError, allowedOrigins: webhookAllowedOrigins, timeoutMs: webhookTimeoutMs },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue