## Summary - Treat **shared PostgreSQL** (`DATABASE_URL`) as the multi-node durable data plane; mesh HTTP is membership + optional auth, not task/settings replication. - **Peer exchange**: under Postgres backend mode, write queue is **topology/auth-only**; non-topology pending rows fail rather than replaying multi-leader task/settings payloads. - **Mesh routes**: task-ID reserve/commit/abort always hit local shared allocator rows (ignore remote `coordinatorNodeId`); mesh sync ignores settings and only exchanges `authMaterial`. - **Docs**: rewrite multi-project runbook, shared cluster protocol, and architecture mesh sections for shared-Postgres + claims/leases. ## Context Follows the SQLite→Postgres cutover. Multiple Fusion nodes can share one external Postgres while keeping **per-node execution** (worktrees, processes, claims via `central.task_claims`). Explicit non-goals remain: scheduler failover and live process migration. Plan: `docs/plans/2026-07-15-001-refactor-mesh-shared-postgres-multinode-plan.md` ## Test plan - [x] `pnpm --filter @fusion/engine exec vitest run src/__tests__/peer-exchange-service.test.ts` - [x] `pnpm --filter @fusion/dashboard exec vitest run src/__tests__/mesh-routes.test.ts` - [x] `pnpm --filter @fusion/core exec vitest run src/__tests__/shared-mesh-state.test.ts` - [ ] CI gate (lint/typecheck/build/gate) - [ ] Manual (optional): two processes, same `DATABASE_URL`, create task on A visible on B; settings change without mesh settings sync; claim exclusivity ## Operator note Multi-node shared board requires **external** `DATABASE_URL` on every node. Default embedded Postgres is still single-host. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved multi-node deployments using shared PostgreSQL as the durable source of execution state. * Task ID reservation/commit/abort now run locally (no remote coordinator forwarding). * Mesh syncing now prioritizes topology visibility and authentication material; settings replication is disabled in shared-Postgres mode. * **Bug Fixes** * Prevented task/settings replication over mesh HTTP in shared-Postgres deployments. * Refined lease ownership, recovery, and reconciliation to converge via shared-database primitives. * **Documentation** * Updated architecture and shared-mesh protocol guidance, including multi-node setup and lease/task-ID allocation behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
export { SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
|
|
import { SHARED_STATE_SNAPSHOT_VERSION } from "./types.js";
|
|
import type {
|
|
GlobalSettings,
|
|
ProjectSettings,
|
|
ProviderAuthEntry,
|
|
} from "./types.js";
|
|
|
|
/*
|
|
FNXC:PostgresCutover 2026-07-12:
|
|
FNXC:SharedPostgresMultiNode 2026-07-14-23:45:
|
|
Task/state mesh replication is REMOVED — shared PostgreSQL is the SoT.
|
|
createProjectSettingsSnapshot remains for legacy helpers/tests only; live mesh
|
|
routes no longer apply or emit projectSettings. authMaterial stays on the wire
|
|
because auth.json is per-machine file state, plus the shared envelope/checksum
|
|
plumbing.
|
|
*/
|
|
|
|
export const SHARED_STATE_DEFAULT_LIMIT = 10_000;
|
|
|
|
export interface SharedSnapshotEnvelope<TPayload> {
|
|
version: number;
|
|
exportedAt: string;
|
|
checksum: string;
|
|
payload: TPayload;
|
|
}
|
|
|
|
export type ProjectSettingsSnapshot = SharedSnapshotEnvelope<{
|
|
global: GlobalSettings;
|
|
projects?: Record<string, ProjectSettings>;
|
|
}>;
|
|
|
|
export type AuthMaterialSnapshot = SharedSnapshotEnvelope<{
|
|
providerAuth?: Record<string, ProviderAuthEntry>;
|
|
}>;
|
|
|
|
function withChecksum<TPayload>(payload: TPayload, exportedAt?: string): SharedSnapshotEnvelope<TPayload> {
|
|
const envelope = {
|
|
version: SHARED_STATE_SNAPSHOT_VERSION,
|
|
exportedAt: exportedAt ?? new Date().toISOString(),
|
|
payload,
|
|
};
|
|
return { ...envelope, checksum: computeSnapshotChecksum(envelope) };
|
|
}
|
|
|
|
export function computeSnapshotChecksum(snapshotWithoutChecksum: Omit<SharedSnapshotEnvelope<unknown>, "checksum">): string {
|
|
return createHash("sha256").update(JSON.stringify(snapshotWithoutChecksum)).digest("hex");
|
|
}
|
|
|
|
export function validateSnapshotEnvelope(snapshot: SharedSnapshotEnvelope<unknown>, expectedVersion = SHARED_STATE_SNAPSHOT_VERSION): void {
|
|
if (snapshot.version !== expectedVersion) {
|
|
throw new Error(`Unsupported shared-state snapshot version ${snapshot.version} (expected ${expectedVersion})`);
|
|
}
|
|
const expectedChecksum = computeSnapshotChecksum({
|
|
version: snapshot.version,
|
|
exportedAt: snapshot.exportedAt,
|
|
payload: snapshot.payload,
|
|
});
|
|
if (snapshot.checksum !== expectedChecksum) {
|
|
throw new Error("Shared-state snapshot checksum mismatch");
|
|
}
|
|
}
|
|
|
|
export function createProjectSettingsSnapshot(payload: ProjectSettingsSnapshot["payload"], exportedAt?: string): ProjectSettingsSnapshot {
|
|
return withChecksum(payload, exportedAt);
|
|
}
|
|
|
|
export function createAuthMaterialSnapshot(providerAuth: Record<string, ProviderAuthEntry> | undefined, exportedAt?: string): AuthMaterialSnapshot {
|
|
return withChecksum({ providerAuth }, exportedAt);
|
|
}
|