编码母题 / P1 / AI 全栈与 Agent 工程 / 02
带租约和幂等控制的任务执行器
先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。
重要程度高价值难度困难预计完成50–70 分钟
题目是什么
实现内存任务仓库与 Worker:任务领取带租约、owner 和 version;租约过期后可重领,旧 Worker 不得提交晚到结果。相同幂等键只创建一次任务。
输入输出示例
Worker A 租约过期后 B 重领,A 随后的 complete 必须被拒绝。
练习起手式与验证
复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。
/*
* 题目:带租约和幂等控制的任务执行器
* 要求:实现内存任务仓库与 Worker:任务领取带租约、owner 和 version;
* 租约过期后可重领,旧 Worker 不得提交晚到结果。相同幂等键只创建一次任务。
* 示例:Worker A 租约过期后 B 重领,A 随后的 complete 必须被拒绝。
*/
class TaskRepository {
constructor(now = Date.now) {}
submit(payload, idempotencyKey) {}
claim(owner, leaseMs) {}
complete(id, owner, version, result) {}
fail(id, owner, version, error, maxAttempts) {}
assertLease(task, owner, version) {}
get(id) {}
}
async function processOne(repository, workerId, handler, { leaseMs = 30000, maxAttempts = 3 } = {}) {}
(async () => {
let now = 0;
const repo = new TaskRepository(() => now);
const first = repo.submit({ x: 1 }, "key");
console.log(repo.submit({ x: 2 }, "key").id); // 预期:first.id
const expired = repo.claim("expired-worker", 5);
now = 5;
console.log((() => { try { (() => repo.complete(first.id, "expired-worker", expired.version, 0))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
const lease1 = repo.claim("w1", 10);
now = 16;
const lease2 = repo.claim("w2", 10);
console.log(lease2.version > lease1.version); // 预期:true
})().catch(console.error);
/*
* 题目:带租约和幂等控制的任务执行器
* 要求:实现内存任务仓库与 Worker:任务领取带租约、owner 和 version;
* 租约过期后可重领,旧 Worker 不得提交晚到结果。相同幂等键只创建一次任务。
* 示例:Worker A 租约过期后 B 重领,A 随后的 complete 必须被拒绝。
*/
class TaskRepository {
constructor(now = Date.now) {}
submit(payload, idempotencyKey) {}
claim(owner, leaseMs) {}
complete(id, owner, version, result) {}
fail(id, owner, version, error, maxAttempts) {}
assertLease(task, owner, version) {}
get(id) {}
}
async function processOne(repository, workerId, handler, { leaseMs = 30000, maxAttempts = 3 } = {}) {}
(async () => {
let now = 0;
const repo = new TaskRepository(() => now);
const first = repo.submit({ x: 1 }, "key");
console.log(repo.submit({ x: 2 }, "key").id); // 预期:first.id
const expired = repo.claim("expired-worker", 5);
now = 5;
console.log((() => { try { (() => repo.complete(first.id, "expired-worker", expired.version, 0))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
const lease1 = repo.claim("w1", 10);
now = 16;
const lease2 = repo.claim("w2", 10);
console.log(lease2.version > lease1.version); // 预期:true
})().catch(console.error);解题思路
展开思路、不变量与常见边界
关键切入点:租约定义处理权,version 防止旧持有者提交;生产实现需事务性条件更新。
核心模型:租约不是锁死任务,而是有期限的处理权;version 防止旧 Worker 在任务 被重新领取后提交过期结果。不变量:一个有效版本只有一个 owner;旧版本不能完成; completed 为终态。内存实现用于说明协议,生产环境需要数据库条件更新事务。
参考代码与验证
“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。
// 关键机制:租约定义处理权,version 防止旧持有者提交;生产实现需事务性条件更新。
class TaskRepository {
constructor(now = Date.now) {
this.now = now;
this.tasks = new Map();
this.keys = new Map();
this.sequence = 0;
}
submit(payload, idempotencyKey) {
if (this.keys.has(idempotencyKey))
return this.get(this.keys.get(idempotencyKey));
const task = {
id: `job${++this.sequence}`,
idempotencyKey,
payload,
status: "pending",
attempt: 0,
version: 0,
owner: null,
leaseUntil: 0,
};
this.tasks.set(task.id, task);
this.keys.set(idempotencyKey, task.id);
return this.get(task.id);
}
claim(owner, leaseMs) {
const now = this.now();
const task = [...this.tasks.values()].find(
(item) =>
item.status === "pending" ||
(item.status === "running" && item.leaseUntil <= now),
);
if (!task) return null;
Object.assign(task, {
status: "running",
owner,
leaseUntil: now + leaseMs,
attempt: task.attempt + 1,
version: task.version + 1,
});
return this.get(task.id);
}
complete(id, owner, version, result) {
const task = this.tasks.get(id);
this.assertLease(task, owner, version);
Object.assign(task, {
status: "completed",
result,
owner: null,
leaseUntil: 0,
});
return this.get(id);
}
fail(id, owner, version, error, maxAttempts) {
const task = this.tasks.get(id);
this.assertLease(task, owner, version);
Object.assign(task, {
status: task.attempt >= maxAttempts ? "failed" : "pending",
error: String(error?.message || error),
owner: null,
leaseUntil: 0,
});
return this.get(id);
}
assertLease(task, owner, version) {
if (
!task ||
task.status !== "running" ||
task.owner !== owner ||
task.version !== version ||
task.leaseUntil <= this.now()
)
throw new Error("stale or invalid lease");
}
get(id) {
const value = this.tasks.get(id);
return value ? structuredClone(value) : null;
}
}
async function processOne(
repository,
workerId,
handler,
{ leaseMs = 30000, maxAttempts = 3 } = {},
) {
const task = repository.claim(workerId, leaseMs);
if (!task) return null;
try {
const result = await handler(task.payload);
return repository.complete(task.id, workerId, task.version, result);
} catch (error) {
return repository.fail(task.id, workerId, task.version, error, maxAttempts);
}
}
// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
let now = 0;
const repo = new TaskRepository(() => now);
const first = repo.submit({ x: 1 }, "key");
console.log(repo.submit({ x: 2 }, "key").id); // 预期:first.id
const expired = repo.claim("expired-worker", 5);
now = 5;
console.log((() => { try { (() => repo.complete(first.id, "expired-worker", expired.version, 0))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
const lease1 = repo.claim("w1", 10);
now = 16;
const lease2 = repo.claim("w2", 10);
console.log(lease2.version > lease1.version); // 预期:true
console.log((() => { try { (() => repo.complete(first.id, "w1", lease1.version, 1))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
repo.complete(first.id, "w2", lease2.version, 2);
console.log(repo.get(first.id).result); // 预期:2
const second = repo.submit({ n: 3 }, "key2");
const done = await processOne(repo, "w", async ({ n }) => n * 2);
console.log(done.id); // 预期:second.id
console.log(done.result); // 预期:6
})().catch(console.error);
// 关键机制:租约定义处理权,version 防止旧持有者提交;生产实现需事务性条件更新。
class TaskRepository {
constructor(now = Date.now) {
this.now = now;
this.tasks = new Map();
this.keys = new Map();
this.sequence = 0;
}
submit(payload, idempotencyKey) {
if (this.keys.has(idempotencyKey))
return this.get(this.keys.get(idempotencyKey));
const task = {
id: `job${++this.sequence}`,
idempotencyKey,
payload,
status: "pending",
attempt: 0,
version: 0,
owner: null,
leaseUntil: 0,
};
this.tasks.set(task.id, task);
this.keys.set(idempotencyKey, task.id);
return this.get(task.id);
}
claim(owner, leaseMs) {
const now = this.now();
const task = [...this.tasks.values()].find(
(item) =>
item.status === "pending" ||
(item.status === "running" && item.leaseUntil <= now),
);
if (!task) return null;
Object.assign(task, {
status: "running",
owner,
leaseUntil: now + leaseMs,
attempt: task.attempt + 1,
version: task.version + 1,
});
return this.get(task.id);
}
complete(id, owner, version, result) {
const task = this.tasks.get(id);
this.assertLease(task, owner, version);
Object.assign(task, {
status: "completed",
result,
owner: null,
leaseUntil: 0,
});
return this.get(id);
}
fail(id, owner, version, error, maxAttempts) {
const task = this.tasks.get(id);
this.assertLease(task, owner, version);
Object.assign(task, {
status: task.attempt >= maxAttempts ? "failed" : "pending",
error: String(error?.message || error),
owner: null,
leaseUntil: 0,
});
return this.get(id);
}
assertLease(task, owner, version) {
if (
!task ||
task.status !== "running" ||
task.owner !== owner ||
task.version !== version ||
task.leaseUntil <= this.now()
)
throw new Error("stale or invalid lease");
}
get(id) {
const value = this.tasks.get(id);
return value ? structuredClone(value) : null;
}
}
async function processOne(
repository,
workerId,
handler,
{ leaseMs = 30000, maxAttempts = 3 } = {},
) {
const task = repository.claim(workerId, leaseMs);
if (!task) return null;
try {
const result = await handler(task.payload);
return repository.complete(task.id, workerId, task.version, result);
} catch (error) {
return repository.fail(task.id, workerId, task.version, error, maxAttempts);
}
}
// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
let now = 0;
const repo = new TaskRepository(() => now);
const first = repo.submit({ x: 1 }, "key");
console.log(repo.submit({ x: 2 }, "key").id); // 预期:first.id
const expired = repo.claim("expired-worker", 5);
now = 5;
console.log((() => { try { (() => repo.complete(first.id, "expired-worker", expired.version, 0))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
const lease1 = repo.claim("w1", 10);
now = 16;
const lease2 = repo.claim("w2", 10);
console.log(lease2.version > lease1.version); // 预期:true
console.log((() => { try { (() => repo.complete(first.id, "w1", lease1.version, 1))(); return false; } catch { return true; } })()); // 预期:true(抛出异常)
repo.complete(first.id, "w2", lease2.version, 2);
console.log(repo.get(first.id).result); // 预期:2
const second = repo.submit({ n: 3 }, "key2");
const done = await processOne(repo, "w", async ({ n }) => n * 2);
console.log(done.id); // 预期:second.id
console.log(done.result); // 预期:6
})().catch(console.error);
复杂度分析
时间复杂度:claim O(N),其余平均 O(P)。领取线性扫描 N 个任务;提交和读取按 id 定位后克隆大小 P 的任务。
空间复杂度:O(N·P) 上界。内存仓库保存至多 N 个任务及其负载副本。