文章揭示面试中 Agent 生成的 HTTP 429 限流代码看似通过测试但实际存在跨租户配额泄漏,并提供了针对多租户隔离的隐藏测试设计方法。
为什么这道题仍然能筛选候选人
针对 HTTP 429 处理的开公 coding 题目,如今已被面试中使用的 coding 模型广泛背熟。候选者或 Agent 可以抛出一个 IP 地址到 reset 时间戳的 Map,却并不理解隔离本身。评审因此需要一道公开展示用例看起来普普通通、而隐藏用例会惩罚全局 key 的题目。Fail-open 存储和挂钟耦合同样属于那一批隐藏用例,因为它们能逃过 fixture 驱动的生成。
本文档面向已允许在 take-home 环节使用 assistant 的 JavaScript 服务端面试。说明不禁止 Agent,也不按候选者使用的编辑器品牌打分——只打分最终模块在 assistant 会话结束后是否仍然恪守租户边界。
面试官应将以下代码块复制进 take-home 题目中,不要额外添加产品叙事。下面的公开 fixture 是候选者唯一能看到的测试文件。隐藏的隔离用例留在面试官私人仓库直到评分当天。
构建一个生产形态的 HTTP 限流器,面向多租户公有 API,不连接真实网络。每个请求携带 tenantId、route 和一个可选的 subject(如来自调用方的用户标识符)。限流器必须在路由 handler 运行之前、在任何下游副作用之前做出允许或拒绝决定。拒绝的调用返回 429 状态码和 Retry-After 头,其值取窗口结束前的整秒数。
限额按租户和按路由计算,绝不跨进程全局计算,也绝不单独从对端 IP 推导。一个噪声租户不得饿死共享同一 Node.js 实例或 worker 的另一个租户。当后端存储抛出异常时,限流器必须 fail closed 并以 429 拒绝请求。时钟必须可注入,以便测试可以推进时间而无需 sleep 或 stub Date.now()。
交付 createLimiter(store, clock, policies) 以及一个纯函数 check(request),后者返回决策对象。包含针对隔离、窗口过期、fail-closed 行为和注入时钟上 Retry-After 算术的测试。
语言为 Node.js,不要求框架,评分模块中不含 HTTP listener。
Store 是一个注入接口,仅暴露 get、set 和 incr。
Clock 为 { nowMs() },且必须是窗口算术中唯一的时间来源。
Policies 以 { route, windowMs, max } 条目传入,以精确路由字符串为主键。
配额状态不得存在于模块级 Map、Date.now() 或 setTimeout 中。
日志不得打印 Authorization 头、Cookie 或原始 bearer token。
候选者可见的公开 fixture
// public-fixture.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createLimiter } from './limiter.js';
export function memoryStore() {
const m = new Map();
return {
async get(k) {
return m.get(k) ?? null;
},
async set(k, v) {
m.set(k, v);
},
async incr(k, n = 1) {
const cur = (m.get(k) ?? 0) + n;
m.set(k, cur);
return cur;
},
};
}
export function fakeClock(start = 1_000_000) {
let t = start;
return {
nowMs: () => t,
advance: (ms) => {
t += ms;
},
};
}
test('allows traffic under the published limit', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 2 },
]);
const req = { tenantId: 't1', route: 'GET /v1/items', subject: 'u1' };
assert.equal((await limiter.check(req)).allowed, true);
assert.equal((await limiter.check(req)).allowed, true);
});
公开 fixture 故意做得很薄,这样一个背熟的 429 代码片段在首次运行时仍然看起来是完整的。在让这个文件变绿之后就停手的 Agent,通常会在隐藏用例的租户 key 或存储错误上折戟。
评审应按提交的模块和隐藏测试打分,而非 README 语气或额外的后端。代码风格小瑕疵、TypeScript 转换和未使用的 Redis 客户端都救不回一个缺失的租户 key。需要在隔离和 fail-closed 拿到满分之后,才讨论其他评分项。
参考实现
以下示例是供面试官教学的草稿,而非库发布。保持隐藏测试不在候选者克隆的仓库中。调用方应将 allowed、status 和 retryAfterSec 视为 check 的唯一契约。
// limiter.js
export function createLimiter(store, clock, policies) {
const policyByRoute = new Map(policies.map((p) => [p.route, p]));
function keyFor(req, windowStart) {
return `rl:${req.tenantId}:${req.route}:${windowStart}`;
}
function windowStart(now, windowMs) {
return now - (now % windowMs);
}
function retryAfterSec(now, start, windowMs) {
return Math.max(1, Math.ceil((start + windowMs - now) / 1000));
}
async function check(req) {
if (!req || typeof req.tenantId !== 'string' || typeof req.route !== 'string') {
return { allowed: false, status: 429, retryAfterSec: 1 };
}
const policy = policyByRoute.get(req.route);
if (!policy) {
return { allowed: true, status: 200, retryAfterSec: 0 };
}
const now = clock.nowMs();
const start = windowStart(now, policy.windowMs);
const key = keyFor(req, start);
let count;
try {
count = await store.incr(key, 1);
if (count === 1) {
await store.set(`${key}:exp`, start + policy.windowMs);
}
} catch {
return {
allowed: false,
status: 429,
retryAfterSec: retryAfterSec(now, start, policy.windowMs),
};
}
if (count <= policy.max) {
return { allowed: true, status: 200, retryAfterSec: 0 };
}
return {
allowed: false,
status: 429,
retryAfterSec: retryAfterSec(now, start, policy.windowMs),
};
}
return { check };
}
将这个模块包装成 Express 或 Fastify 中间件不在评分范围内。如果面试官需要 HTTP 接线,可以在隐藏用例全绿之后再加。
面试官应保密的隐藏测试
这些用例才是真正的过滤器。不要把它们粘贴到候选者 prompt 或候选者能读取的 assistant 对话中。
// hidden.isolation.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createLimiter } from './limiter.js';
import { memoryStore, fakeClock } from './public-fixture.test.js';
test('does not spend tenant B quota on tenant A traffic', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 1 },
]);
const a = { tenantId: 't-a', route: 'GET /v1/items' };
const b = { tenantId: 't-b', route: 'GET /v1/items' };
assert.equal((await limiter.check(a)).allowed, true);
assert.equal((await limiter.check(a)).allowed, false);
assert.equal((await limiter.check(b)).allowed, true);
});
test('does not share budget across different routes', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 1 },
{ route: 'POST /v1/items', windowMs: 60_000, max: 1 },
]);
const getReq = { tenantId: 't1', route: 'GET /v1/items' };
const postReq = { tenantId: 't1', route: 'POST /v1/items' };
assert.equal((await limiter.check(getReq)).allowed, true);
assert.equal((await limiter.check(getReq)).allowed, false);
assert.equal((await limiter.check(postReq)).allowed, true);
});
test('fails closed when incr throws', async () => {
const clock = fakeClock();
const store = {
async get() {
return null;
},
async set() {},
async incr() {
throw new Error('redis down');
},
};
const limiter = createLimiter(store, clock, [
{ route: 'GET /v1/items', windowMs: 60_000, max: 10 },
]);
const res = await limiter.check({ tenantId: 't1', route: 'GET /v1/items' });
assert.equal(res.allowed, false);
assert.equal(res.status, 429);
assert.ok(res.retryAfterSec >= 1);
});
test('opens a new window after injected time passes', async () => {
const clock = fakeClock();
const limiter = createLimiter(memoryStore(), clock, [
{ route: 'GET /v1/items', windowMs: 1_000, max: 1 },
]);
const req = { tenantId: 't1', route: 'GET /v1/items' };
assert.equal((await limiter.check(req)).allowed, true);
assert.equal((await limiter.check(req)).allowed, false);
clock.advance(1_000);
assert.equal((await limiter.check(req)).allowed, true);
});
先运行公开文件,再运行隐藏文件,使用同一工作树和同一个 limiter.js。
node --test public-fixture.test.js
node --test hidden.isolation.test.js
评审应关注 Agent 输出中的以下模式,因为它们会让公开 fixture 变绿、却在租户上崩溃。
以 IP 为 key 的全局 Map。模块编译通过且公开测试通过,然而租户 B 在租户 A 耗尽唯一计数器后也被阻止。
存储错误时 fail open。生成的注释说可用性比配额更重要,这会将一次缓存故障变成一笔无上限的账单。
Date.now() 混入窗口算术。推进假时钟的隐藏测试永远不会使窗口过期,所以过期用例会 false-fail 或挂起。
Key 中遗漏了 route。GET /v1/items 和 POST /v1/items 共享一个配额,这在大多数公有 API 中是一个产品缺陷。
Retry-After 设为整个窗口。客户端在一次本应两秒后在注入时钟上就清空的拒绝后要等待一分钟。
Subject 被用作唯一 key。缺失 subject 会导致限制失效,而盗取的 subject 字符串会错误地节流调用方。
跨 worker 的模块内存状态。两个进程各自允许最大请求数,静默地将公布的配额翻倍。
以上失败无需恶意候选者或损坏的编译器。它们出现在 assistant 针对可见 fixture 优化然后停手之时。
使用 Agent 而不洗白成绩
已经允许候选者使用 assistant 的团队,仍应在 prompt 发布前冻结隐藏测试。Assistant 可能从公开文件 draft 出 limiter.js。评审仍然运行从未粘贴进对话的隔离和 fail-closed 用例。
声明:本文作为 MonkeyCode 产品推广的一部分撰写。需要在 assistant 迭代中持有独立工作空间的面试官,可以使用 MonkeyCode 的免费模型访问和免费服务器选项,同时将隐藏用例套件保持在该工作空间之外。成绩仍来自私有测试,而非生成初稿的工具。
在不使用该工作空间的情况下本文档仍然可用。本地 Node.js 安装和两个测试文件就足够评分。
局限性及不适合使用本文档的人群
本草稿使用固定窗口,而非滑动窗口或带补充速率的令牌桶。固定窗口允许在两个相邻窗口边界处出现短暂突发。在送入计费关键 API 之前不要直接发版,应使用更强的算法和真实共享存储(带原子递增)。
内存存储在跨进程或跨主机场景下不安全。生产环境需要 Redis、DynamDB 或等效方案,能够在不出现读-改-写竞态的情况下递增 key。如果调用方后续针对朴素 async 存储用并发 check 调用包装它,Map 上的参考 incr 也是有竞态的。
对于只需要 HTTP 状态素养的实习筛选跳过本文档。对于不会负责多租户滥用控制的岗位跳过本文档。当面试官无法将隐藏测试文件保持在公开 gist 之外时跳过本文档。不要将公开 fixture 变绿当作 Agent 理解租户的证据。不要在候选者克隆的同一仓库中发布隐藏文件。
如何在面试周运行这个循环
仅发送 prompt 和 public-fixture.test.js,hard constraints 逐字复制。
如果团队已允许工具使用,则允许任何 assistant,包括免费远程工作空间。
收集 limiter.js 以及候选者在公开 fixture 之外编写的任何额外测试。
在候选者看不到的干净 checkout 上运行隐藏隔离、fail-closed 和时钟测试。
用评分表打分,在 debrief 中讨论一种失败模式,而非 README。
有价值的信号在于提交的 key 空间是否编码了 tenant 和 route,以及存储错误时是否拒绝流量。其他一切都是围绕一个要么隔离客户要么不隔离的模块的评注。