135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
import {
|
|
closeSync,
|
|
existsSync,
|
|
fsyncSync,
|
|
mkdirSync,
|
|
openSync,
|
|
renameSync,
|
|
unlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { dirname } from "node:path";
|
|
|
|
export interface MaintenanceDurability {
|
|
writeFile?(descriptor: number, contents: string): void;
|
|
syncFile?(descriptor: number): void;
|
|
syncDirectory(directory: string): void;
|
|
}
|
|
|
|
/** A durable admission barrier. A lease spans the complete create/resume decision. */
|
|
export class MaintenanceBarrier {
|
|
private active: boolean;
|
|
private admissions = 0;
|
|
private waiters: (() => void)[] = [];
|
|
private recoveryRequired = false;
|
|
|
|
constructor(
|
|
private readonly markerFile?: string,
|
|
private readonly durability: MaintenanceDurability = defaultDurability,
|
|
) {
|
|
this.active = markerFile === undefined ? false : existsSync(markerFile);
|
|
}
|
|
|
|
acquire(): (() => void) | undefined {
|
|
this.reconcileActive();
|
|
if (this.active) return undefined;
|
|
this.admissions += 1;
|
|
let released = false;
|
|
return () => {
|
|
if (released) return;
|
|
released = true;
|
|
this.admissions -= 1;
|
|
if (this.admissions === 0) this.waiters.splice(0).forEach((resolve) => resolve());
|
|
};
|
|
}
|
|
|
|
async activate(): Promise<void> {
|
|
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();
|
|
}
|
|
}
|
|
if (!this.active) throw persistError;
|
|
if (this.admissions > 0) {
|
|
await new Promise<void>((resolve) => this.waiters.push(resolve));
|
|
}
|
|
if (persistError !== undefined) throw persistError;
|
|
}
|
|
|
|
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; recoveryRequired?: true } {
|
|
this.reconcileActive();
|
|
const status = { active: this.active, admissions: this.admissions };
|
|
return this.recoveryRequired ? { ...status, recoveryRequired: true } : status;
|
|
}
|
|
|
|
private reconcileActive(): void {
|
|
if (this.markerFile !== undefined) this.active = existsSync(this.markerFile);
|
|
}
|
|
|
|
private persistMarker(): void {
|
|
if (!this.markerFile) return;
|
|
const directory = dirname(this.markerFile);
|
|
mkdirSync(directory, { recursive: true });
|
|
const temporary = `${this.markerFile}.tmp-${process.pid}-${Date.now()}`;
|
|
const fd = openSync(temporary, "wx", 0o600);
|
|
let closed = false;
|
|
try {
|
|
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);
|
|
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) return;
|
|
if (existsSync(this.markerFile)) unlinkSync(this.markerFile);
|
|
else if (!this.recoveryRequired) return;
|
|
this.durability.syncDirectory(dirname(this.markerFile));
|
|
}
|
|
}
|
|
|
|
const defaultDurability: MaintenanceDurability = {
|
|
syncDirectory(directory: string): void {
|
|
if (process.platform === "win32") return;
|
|
const fd = openSync(directory, "r");
|
|
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
},
|
|
};
|