'use strict'; const assert = require('assert'); const { runConcurrentItems } = require('../src/utils/concurrent_runner'); /** * 让并发任务主动让出事件循环,便于测试真实的滑动窗口并发度。 */ function yieldEventLoop() { return new Promise(resolve => setImmediate(resolve)); } (async () => { const accounts = Array.from({ length: 545 }, (_, index) => ({ email: `user${index}@example.com`, })); const processed = new Set(); const progress = []; let active = 0; let maxActive = 0; const batch = await runConcurrentItems(accounts, async (account, index) => { processed.add(account.email); active += 1; maxActive = Math.max(maxActive, active); try { await yieldEventLoop(); if (index === 109) throw new Error('DB write timeout: updateAccountStatusReason'); return { email: account.email, ok: true }; } finally { active -= 1; } }, { concurrency: 50, onError: (error, account) => ({ email: account.email, ok: false, code: 'ACCOUNT_TASK_ERROR', msg: error.message, }), onProgress: ({ completed }) => { progress.push(completed); if (completed === 110) throw new Error('模拟前端进度通道短暂异常'); }, }); assert.strictEqual(processed.size, 545, '第 110 个账号失败后仍应调度完全部 545 个账号'); assert.strictEqual(batch.completed, 545, '完成计数必须包含成功和失败账号'); assert.strictEqual(batch.results.filter(Boolean).length, 545, '每个账号都必须留下明确结果'); assert.strictEqual(batch.results[109].ok, false, '单账号写库超时应保留为该账号失败'); assert.strictEqual(progress.at(-1), 545, '最终进度必须到达批次总数'); assert.strictEqual(maxActive, 50, '业务并发仍应按用户设置跑满 50'); let paused = true; let pauseWaiters = []; const pauseStarted = []; const pausedBatchPromise = runConcurrentItems([1, 2, 3, 4], async item => { pauseStarted.push(item); await yieldEventLoop(); return item; }, { concurrency: 2, waitUntilReady: async () => { while (paused) await new Promise(resolve => pauseWaiters.push(resolve)); }, }); await new Promise(resolve => setTimeout(resolve, 20)); assert.deepStrictEqual(pauseStarted, [], '暂停期间不得领取任何新项目'); paused = false; const releasing = pauseWaiters; pauseWaiters = []; releasing.forEach(resolve => resolve()); const resumedBatch = await pausedBatchPromise; assert.strictEqual(resumedBatch.completed, 4, '继续后必须处理完全部剩余项目'); assert.deepStrictEqual(pauseStarted.sort(), [1, 2, 3, 4], '继续后不得遗漏项目'); const deferredRuns = []; const attempts = new Map(); const deferredBatch = await runConcurrentItems(['sleeping', 'next-a', 'next-b'], async item => { deferredRuns.push(item); const attempt = (attempts.get(item) || 0) + 1; attempts.set(item, attempt); if (item === 'sleeping' && attempt === 1) { return { defer: true, delayMs: 40 }; } await yieldEventLoop(); return { item, attempt }; }, { concurrency: 1, getDeferDelay: result => (result && result.defer ? result.delayMs : 0), }); assert.deepStrictEqual( deferredRuns, ['sleeping', 'next-a', 'next-b', 'sleeping'], '延迟项目等待期间必须让出唯一并发槽,让后续项目先完成', ); assert.strictEqual(deferredBatch.completed, 3, '延迟重试不应重复累计完成进度'); assert.strictEqual(deferredBatch.results[0].attempt, 2, '延迟项目到期后应在原索引写入最终结果'); const heldSlotRuns = []; const heldSlotAttempts = new Map(); const heldSlotBatch = await runConcurrentItems(['sleeping', 'next'], async item => { heldSlotRuns.push(item); const attempt = (heldSlotAttempts.get(item) || 0) + 1; heldSlotAttempts.set(item, attempt); if (item === 'sleeping' && attempt === 1) return { defer: true, delayMs: 20 }; return { item, attempt }; }, { concurrency: 1, deferReleasesSlot: false, getDeferDelay: result => (result && result.defer ? result.delayMs : 0), }); assert.deepStrictEqual( heldSlotRuns, ['sleeping', 'sleeping', 'next'], '临时比特浏览器任务延迟等待时必须占住串行槽,删除后才能轮到下一账号', ); assert.strictEqual(heldSlotBatch.completed, 2, '占住串行槽时仍应完成全部项目'); console.log('concurrent_runner_resilience.test.js: PASS'); })().catch(error => { console.error(error); process.exitCode = 1; });