feat(FN-3710): retry overlap-class cluster create failures in distributed t

Adds retry logic for distributed task ID overlap conflicts across the cluster, hardening the ID allocator with exponential-backoff recovery, wiring the retry into task workflow registration, and documenting the behavior in architecture docs. Minor CSS tokenization on the Mailbox and Plugin Manager m

Fusion-Task-Id: FN-3710
This commit is contained in:
Fusion
2026-05-07 11:44:42 -07:00
committed by gsxdsm
parent 011ae14aa7
commit 9ad340bba1
8 changed files with 264 additions and 138 deletions

View File

@@ -84,6 +84,28 @@ describe("distributed-task-id allocator", () => {
expect(state.nextSequence).toBe(3702);
});
it("skips stale overlapping nextSequence values and reserves the next free id", async () => {
const db = new Database("/tmp/fusion-test", { inMemory: true });
db.init();
const now = new Date().toISOString();
db.prepare(
"INSERT INTO tasks (id, description, \"column\", createdAt, updatedAt) VALUES (?, '', 'todo', ?, ?)",
).run("FN-002", now, now);
db.prepare(
"INSERT INTO distributed_task_id_state (prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt) VALUES (?, ?, ?, ?, ?)",
).run("FN", 2, 1, "FN-001", now);
const allocator = createDistributedTaskIdAllocator(db);
const reservation = await allocator.reserveDistributedTaskId({ prefix: "FN", nodeId: "node-a" });
expect(reservation.taskId).toBe("FN-003");
expect(reservation.sequence).toBe(3);
const state = await allocator.getDistributedTaskIdState({ prefix: "FN" });
expect(state.nextSequence).toBe(4);
expect(state.committedClusterTaskCount).toBe(1);
});
it("state reports committed count independently from nextSequence", async () => {
const { allocator } = createAllocator();
const first = await allocator.reserveDistributedTaskId({ prefix: "FN", nodeId: "node-a" });

View File

@@ -81,6 +81,22 @@ export function createDistributedTaskIdAllocator(db: Database): DistributedTaskI
return result.changes ?? 0;
};
const taskIdExists = (prefix: string, sequence: number): boolean => {
const taskId = formatDistributedTaskId(prefix, sequence);
const existsInTable = (table: string): boolean => {
try {
const row = db
.prepare(`SELECT 1 as found FROM ${table} WHERE id = ? LIMIT 1`)
.get(taskId) as { found?: number } | undefined;
return row?.found === 1;
} catch {
return false;
}
};
return existsInTable("tasks") || existsInTable("archivedTasks");
};
const ensureStateRow = (prefix: string): void => {
// Seed nextSequence past any pre-existing task ID for this prefix. Without
// this, projects whose tasks were originally allocated through
@@ -161,7 +177,11 @@ export function createDistributedTaskIdAllocator(db: Database): DistributedTaskI
)
.get(prefix) as { nextSequence: number; committedClusterTaskCount: number };
const sequence = state.nextSequence;
let sequence = state.nextSequence;
while (taskIdExists(prefix, sequence)) {
sequence += 1;
}
const taskId = formatDistributedTaskId(prefix, sequence);
const reservationId = randomUUID();