fix: harden embedded Pi lifecycle recovery

This commit is contained in:
2026-08-04 23:14:47 +02:00
parent a368889838
commit 935bb1db0e
15 changed files with 572 additions and 101 deletions
+2
View File
@@ -117,6 +117,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
} catch {
return reply.code(500).send({
...maintenanceBarrier.status(),
code: "maintenance_durability_failed",
error: "maintenance activation durability was not acknowledged",
});
}
@@ -128,6 +129,7 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
} catch {
return reply.code(500).send({
...maintenanceBarrier.status(),
code: "maintenance_durability_failed",
error: "maintenance deactivation durability was not acknowledged",
});
}
+26 -9
View File
@@ -11,6 +11,8 @@ import {
import { dirname } from "node:path";
export interface MaintenanceDurability {
writeFile?(descriptor: number, contents: string): void;
syncFile?(descriptor: number): void;
syncDirectory(directory: string): void;
}
@@ -19,6 +21,7 @@ export class MaintenanceBarrier {
private active: boolean;
private admissions = 0;
private waiters: (() => void)[] = [];
private recoveryRequired = false;
constructor(
private readonly markerFile?: string,
@@ -44,11 +47,14 @@ export class MaintenanceBarrier {
let persistError: unknown;
if (this.markerFile === undefined) {
this.active = true;
this.recoveryRequired = false;
} else {
try {
this.persistMarker();
this.recoveryRequired = false;
} catch (error) {
persistError = error;
this.recoveryRequired = true;
} finally {
this.reconcileActive();
}
@@ -63,17 +69,24 @@ export class MaintenanceBarrier {
deactivate(): void {
if (this.markerFile === undefined) {
this.active = false;
this.recoveryRequired = false;
return;
}
try {
this.removeMarker();
this.recoveryRequired = false;
} catch (error) {
try { this.persistMarker(); } catch { /* marker existence is reconciled below */ }
this.recoveryRequired = true;
throw error;
} finally {
this.reconcileActive();
}
}
status(): { active: boolean; admissions: number } {
status(): { active: boolean; admissions: number; recoveryRequired?: true } {
this.reconcileActive();
return { active: this.active, admissions: this.admissions };
const status = { active: this.active, admissions: this.admissions };
return this.recoveryRequired ? { ...status, recoveryRequired: true } : status;
}
private reconcileActive(): void {
@@ -86,24 +99,28 @@ export class MaintenanceBarrier {
mkdirSync(directory, { recursive: true });
const temporary = `${this.markerFile}.tmp-${process.pid}-${Date.now()}`;
const fd = openSync(temporary, "wx", 0o600);
let closed = false;
try {
writeFileSync(fd, '{"version":1,"active":true}\n', "utf8");
fsyncSync(fd);
} finally {
const contents = '{"version":1,"active":true}\n';
if (this.durability.writeFile) this.durability.writeFile(fd, contents);
else writeFileSync(fd, contents, "utf8");
if (this.durability.syncFile) this.durability.syncFile(fd);
else fsyncSync(fd);
closeSync(fd);
}
try {
closed = true;
renameSync(temporary, this.markerFile);
this.durability.syncDirectory(directory);
} catch (error) {
if (!closed) try { closeSync(fd); } catch { /* preserve the original failure */ }
try { unlinkSync(temporary); } catch { /* already renamed or best-effort cleanup */ }
throw error;
}
}
private removeMarker(): void {
if (!this.markerFile || !existsSync(this.markerFile)) return;
unlinkSync(this.markerFile);
if (!this.markerFile) return;
if (existsSync(this.markerFile)) unlinkSync(this.markerFile);
else if (!this.recoveryRequired) return;
this.durability.syncDirectory(dirname(this.markerFile));
}
}