concurrency.mjs
3.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env node
/**
* 并发与幂等的真实链路脚本(Planning 用例 CC-01、CC-02、CC-03)。CC-03 需要 voko-worker 在跑。
* CC-01 用 6 个 Agent(每个主人最多 3 个)同时发 20 个认领请求,断言恰好 1 个成功。
*/
import { randomUUID } from 'node:crypto'
import { runScenario } from './_lib.mjs'
await runScenario('cc', async ({ db, check, call, createOwner, createGuild, createAgent, publish, waitForTask, wallet: walletOf, submit, claim, review }) => {
const alice = await createOwner('alice')
const bob = await createOwner('bob')
const carol = await createOwner('carol')
const guild = await createGuild('main', alice.user_id)
const publisher = await createAgent('publisher', alice, guild)
const takers = []
for (const [owner, tag] of [[bob, 'bob'], [carol, 'carol']]) {
for (let i = 1; i <= 3; i += 1) takers.push(await createAgent(`${tag}-${i}`, owner, guild))
}
// CC-02 同一 Idempotency-Key 并发发布 10 次 → 1 个任务、1 笔扣款。
const key = randomUUID()
const replays = await Promise.all(Array.from({ length: 10 }, () => publish(publisher, { reward: 30 }, key)))
const taskIds = new Set(replays.map((r) => r.json.data && r.json.data.id))
const wallet = (await call('GET', '/v1/wallet', { token: publisher.key })).json.data
const holds = await db('billing_ledger').where({ reason: 'vk_escrow_hold', ref_id: [...taskIds][0] })
check('CC-02 发布重放 10 次 → 1 个任务、1 笔扣款', replays.every((r) => r.status === 200) && taskIds.size === 1 && holds.length === 1 && wallet.available === 70 && wallet.held === 30, { statuses: replays.map((r) => r.status), task_ids: [...taskIds], wallet })
// CC-01 20 个并发认领同一计件活 → 恰好 1 个成功。
const taskId = [...taskIds][0]
const claims = await Promise.all(Array.from({ length: 20 }, (_, i) => call('POST', `/v1/tasks/${taskId}/claim`, { token: takers[i % takers.length].key })))
const won = claims.filter((r) => r.status === 200)
const openHolds = await db('vk_task_holds').where({ task_id: taskId }).whereNull('ended_at')
check('CC-01 20 个并发认领 → 恰好 1 个成功,其余 409', won.length === 1 && claims.filter((r) => r.status === 409).length === 19 && openHolds.length === 1 && openHolds[0].agent_id === won[0].json.data.holder_agent_id, { statuses: claims.map((r) => r.status) })
// CC-03 发布方 accept 与自动确认 job 同时发生 → 只结算一次。验收窗口 3 秒的公会,在到期时刻连发 accept。
const fast = await createGuild('fast', alice.user_id, { piece_review_window_sec: 3 })
const fastPublisher = await createAgent('fast-publisher', alice, fast)
const dave = await createOwner('dave')
const fastTaker = await createAgent('fast-taker', dave, fast)
const racing = (await publish(fastPublisher, { reward: 25 })).json.data
await claim(fastTaker, racing.id)
const submitted = await submit(fastTaker, racing.id)
const dueIn = new Date(submitted.json.data.review_due).getTime() - Date.now()
const accepts = []
for (const offset of [-150, 0, 150, 400, 900]) accepts.push(new Promise((resolve) => setTimeout(resolve, Math.max(0, dueIn + offset))).then(() => review(fastPublisher, racing.id, 'accept', ['c1'])))
const acceptResults = await Promise.all(accepts)
const settled = await waitForTask(fastPublisher, racing.id, (task) => task.status === 'settled')
const racePayouts = await db('vk_escrow_payouts').where({ task_id: racing.id })
const raceLedger = await db('billing_ledger').where({ ref_id: racing.id, reason: 'vk_task_payout' })
check('CC-03 accept 与自动确认并发 → 只结算一次', settled.status === 'settled' && racePayouts.length === 1 && raceLedger.length === 1 && (await walletOf(fastTaker)).available === 125 && acceptResults.every((r) => r.status === 200 || r.status === 409) && acceptResults.filter((r) => r.status === 200).length <= 1, { statuses: acceptResults.map((r) => r.status), racePayouts })
})