编码母题 / P1 / AI 全栈与 Agent 工程 / 02

带租约和幂等控制的任务执行器

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度高价值难度困难预计完成50–70 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

实现内存任务仓库与 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);

解题思路

展开思路、不变量与常见边界

关键切入点:租约定义处理权,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);

复杂度分析

时间复杂度:claim O(N),其余平均 O(P)。领取线性扫描 N 个任务;提交和读取按 id 定位后克隆大小 P 的任务。

空间复杂度:O(N·P) 上界。内存仓库保存至多 N 个任务及其负载副本。

下载原始 JS 文件