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:
@@ -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" });
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user