feat: backend status & health check

This commit is contained in:
Nicolas Meienberger
2025-09-03 22:18:59 +02:00
parent 63b983b1b1
commit 7fe75c64e8
12 changed files with 239 additions and 4 deletions

View File

@@ -1,3 +1,4 @@
import type { BackendStatus } from "@ironmount/schemas";
import type { Volume } from "../../db/schema";
import { makeDirectoryBackend } from "./directory/directory-backend";
import { makeNfsBackend } from "./nfs/nfs-backend";
@@ -5,6 +6,7 @@ import { makeNfsBackend } from "./nfs/nfs-backend";
export type VolumeBackend = {
mount: () => Promise<void>;
unmount: () => Promise<void>;
checkHealth: () => Promise<{ error?: string; status: BackendStatus }>;
};
export const createVolumeBackend = (volume: Volume): VolumeBackend => {

View File

@@ -1,5 +1,6 @@
import * as fs from "node:fs/promises";
import type { BackendConfig } from "@ironmount/schemas";
import * as npath from "node:path";
import { BACKEND_STATUS, type BackendConfig } from "@ironmount/schemas";
import type { VolumeBackend } from "../backend";
const mount = async (_config: BackendConfig, path: string) => {
@@ -11,7 +12,24 @@ const unmount = async () => {
console.log("Cannot unmount directory volume.");
};
const checkHealth = async (path: string) => {
try {
await fs.access(path);
// Try to create a temporary file to ensure write access
const tempFilePath = npath.join(path, `.healthcheck-${Date.now()}`);
await fs.writeFile(tempFilePath, "healthcheck");
await fs.unlink(tempFilePath);
return { status: BACKEND_STATUS.mounted };
} catch (error) {
console.error("Directory health check failed:", error);
return { status: BACKEND_STATUS.error, error: error instanceof Error ? error.message : String(error) };
}
};
export const makeDirectoryBackend = (config: BackendConfig, path: string): VolumeBackend => ({
mount: () => mount(config, path),
unmount,
checkHealth: () => checkHealth(path),
});

View File

@@ -1,7 +1,8 @@
import { exec } from "node:child_process";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import type { BackendConfig } from "@ironmount/schemas";
import * as npath from "node:path";
import { BACKEND_STATUS, type BackendConfig } from "@ironmount/schemas";
import type { VolumeBackend } from "../backend";
const mount = async (config: BackendConfig, path: string) => {
@@ -54,7 +55,24 @@ const unmount = async (path: string) => {
});
};
const checkHealth = async (path: string) => {
try {
await fs.access(path);
// Try to create a temporary file to ensure the mount is writable
const testFilePath = npath.join(path, `.healthcheck-${Date.now()}`);
await fs.writeFile(testFilePath, "healthcheck");
await fs.unlink(testFilePath);
return { status: BACKEND_STATUS.mounted };
} catch (error) {
console.error("NFS volume health check failed:", error);
return { status: BACKEND_STATUS.error, error: error instanceof Error ? error.message : String(error) };
}
};
export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({
mount: () => mount(config, path),
unmount: () => unmount(path),
checkHealth: () => checkHealth(path),
});

View File

@@ -24,6 +24,7 @@ export const volumeController = new Hono()
...volume,
updatedAt: volume.updatedAt.getTime(),
createdAt: volume.createdAt.getTime(),
lastHealthCheck: volume.lastHealthCheck.getTime(),
})),
} satisfies ListVolumesResponseDto;

View File

@@ -7,8 +7,11 @@ const volumeSchema = type({
name: "string",
path: "string",
type: type.enumerated("nfs", "smb", "directory"),
status: type.enumerated("mounted", "unmounted", "error", "unknown"),
lastError: "string|null",
createdAt: "number",
updatedAt: "number",
lastHealthCheck: "number",
config: volumeConfigSchema,
});

View File

@@ -109,7 +109,9 @@ const updateVolume = async (name: string, backendConfig: BackendConfig) => {
}
const oldBackend = createVolumeBackend(existing);
await oldBackend.unmount();
await oldBackend.unmount().catch((err) => {
console.warn("Failed to unmount backend:", err);
});
const updated = await db
.update(volumesTable)
@@ -135,6 +137,39 @@ const updateVolume = async (name: string, backendConfig: BackendConfig) => {
}
};
const updateVolumeStatus = async (name: string, status: "mounted" | "unmounted" | "error", error?: string) => {
await db
.update(volumesTable)
.set({
status,
lastHealthCheck: new Date(),
lastError: error ?? null,
updatedAt: new Date(),
})
.where(eq(volumesTable.name, name));
};
const getVolumeStatus = async (name: string) => {
const volume = await db.query.volumesTable.findFirst({
where: eq(volumesTable.name, name),
});
if (!volume) {
return { error: new NotFoundError("Volume not found") };
}
const backend = createVolumeBackend(volume);
const healthResult = await backend.checkHealth();
await updateVolumeStatus(name, healthResult.status, healthResult.error);
return {
name: volume.name,
status: healthResult.status,
lastHealthCheck: new Date(),
error: healthResult.error,
};
};
const testConnection = async (backendConfig: BackendConfig) => {
let tempDir: string | null = null;
@@ -148,7 +183,10 @@ const testConnection = async (backendConfig: BackendConfig) => {
config: backendConfig,
createdAt: new Date(),
updatedAt: new Date(),
lastHealthCheck: new Date(),
type: backendConfig.backend,
status: "unmounted" as const,
lastError: null,
};
const backend = createVolumeBackend(mockVolume);
@@ -186,4 +224,6 @@ export const volumeService = {
getVolume,
updateVolume,
testConnection,
updateVolumeStatus,
getVolumeStatus,
};