mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-08 01:08:43 +08:00
Opensandbox (#6657)
* Opensandbox (#6651) * volumn manager * feat: opensandbox volumn * perf: action (#6654) * perf: action * doc * doc * deploy tml * update template
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { ProcessPool } from '../../src/pool/process-pool';
|
||||
import { PythonProcessPool } from '../../src/pool/python-process-pool';
|
||||
|
||||
let jsPool: ProcessPool;
|
||||
let pyPool: PythonProcessPool;
|
||||
|
||||
beforeAll(async () => {
|
||||
jsPool = new ProcessPool(1);
|
||||
await jsPool.init();
|
||||
pyPool = new PythonProcessPool(1);
|
||||
await pyPool.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await jsPool.shutdown();
|
||||
await pyPool.shutdown();
|
||||
});
|
||||
|
||||
describe('边界测试 - JS', () => {
|
||||
// ===== 空/特殊代码 =====
|
||||
|
||||
it('空代码(无 main 函数)', async () => {
|
||||
const result = await jsPool.execute({ code: '', variables: {} });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('main 不是函数', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `const main = 42;`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('main 返回 undefined', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() { }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('main 返回 null', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() { return null; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// ===== 大数据 =====
|
||||
|
||||
it('大量 console.log 输出', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
console.log('line ' + i);
|
||||
}
|
||||
return { done: true };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.log).toContain('line 0');
|
||||
expect(result.data?.log).toContain('line 999');
|
||||
});
|
||||
|
||||
it('大对象返回', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
const arr = [];
|
||||
for (let i = 0; i < 10000; i++) arr.push(i);
|
||||
return { count: arr.length, first: arr[0], last: arr[9999] };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.count).toBe(10000);
|
||||
});
|
||||
|
||||
// ===== 变量传递 =====
|
||||
|
||||
it('特殊字符变量', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main(vars) {
|
||||
return { name: vars.name };
|
||||
}`,
|
||||
variables: { name: '你好\n"world"<script>alert(1)</script>' }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.name).toBe('你好\n"world"<script>alert(1)</script>');
|
||||
});
|
||||
|
||||
it('嵌套对象变量', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main(vars) {
|
||||
return { deep: vars.a.b.c };
|
||||
}`,
|
||||
variables: { a: { b: { c: 42 } } }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.deep).toBe(42);
|
||||
});
|
||||
|
||||
it('数组变量', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main(vars) {
|
||||
return { len: vars.items.length, first: vars.items[0] };
|
||||
}`,
|
||||
variables: { items: [1, 2, 3] }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.len).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界测试 - Python', () => {
|
||||
// ===== 空/特殊代码 =====
|
||||
|
||||
it('空代码', async () => {
|
||||
const result = await pyPool.execute({ code: '', variables: {} });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('main 不是函数', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `main = 42`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('main 返回 None', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main():
|
||||
pass`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
// ===== 大数据 =====
|
||||
|
||||
it('大量 print 输出', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main():
|
||||
for i in range(1000):
|
||||
print(f'line {i}')
|
||||
return {'done': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.log).toContain('line 0');
|
||||
expect(result.data?.log).toContain('line 999');
|
||||
});
|
||||
|
||||
it('大列表返回', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main():
|
||||
arr = list(range(10000))
|
||||
return {'count': len(arr), 'first': arr[0], 'last': arr[-1]}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.count).toBe(10000);
|
||||
});
|
||||
|
||||
// ===== 变量传递 =====
|
||||
|
||||
it('特殊字符变量', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main(vars):
|
||||
return {'name': vars['name']}`,
|
||||
variables: { name: '你好\n"world"<script>alert(1)</script>' }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.name).toBe('你好\n"world"<script>alert(1)</script>');
|
||||
});
|
||||
|
||||
it('嵌套字典变量', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main(vars):
|
||||
return {'deep': vars['a']['b']['c']}`,
|
||||
variables: { a: { b: { c: 42 } } }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.deep).toBe(42);
|
||||
});
|
||||
|
||||
it('列表变量', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main(vars):
|
||||
return {'len': len(vars['items']), 'first': vars['items'][0]}`,
|
||||
variables: { items: [1, 2, 3] }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.len).toBe(3);
|
||||
});
|
||||
|
||||
// ===== 类型处理 =====
|
||||
|
||||
it('返回非 JSON 可序列化对象(set)', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main():
|
||||
return {'items': list({1, 2, 3})}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.items).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('返回 datetime 对象(default=str 处理)', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `from datetime import datetime
|
||||
def main():
|
||||
return {'now': datetime(2024, 1, 1, 12, 0, 0)}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.now).toContain('2024');
|
||||
});
|
||||
|
||||
// ===== 补充:更多边界场景 =====
|
||||
|
||||
it('超长变量字符串', async () => {
|
||||
const longStr = 'a'.repeat(100000);
|
||||
const result = await pyPool.execute({
|
||||
code: `def main(v):
|
||||
return {'len': len(v['text'])}`,
|
||||
variables: { text: longStr }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.len).toBe(100000);
|
||||
});
|
||||
|
||||
it('变量包含特殊 JSON 字符', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main(v):
|
||||
return {'text': v['text']}`,
|
||||
variables: { text: 'line1\nline2\ttab\\backslash"quote' }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.text).toContain('line1');
|
||||
expect(result.data?.codeReturn.text).toContain('\\');
|
||||
});
|
||||
|
||||
it('返回浮点数精度', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main():
|
||||
return {'val': 0.1 + 0.2}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.val).toBeCloseTo(0.3, 10);
|
||||
});
|
||||
|
||||
it('返回非常大的整数', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main():
|
||||
return {'big': 2 ** 53}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.big).toBe(9007199254740992);
|
||||
});
|
||||
|
||||
it('缺少必需参数的 main 函数', async () => {
|
||||
const result = await pyPool.execute({
|
||||
code: `def main(a, b, c):
|
||||
return {'sum': a + b + c}`,
|
||||
variables: { a: 1, b: 2 } // 缺少 c
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Missing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界测试 - JS 补充', () => {
|
||||
it('超长变量字符串', async () => {
|
||||
const longStr = 'a'.repeat(100000);
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main(v) {
|
||||
return { len: v.text.length };
|
||||
}`,
|
||||
variables: { text: longStr }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.len).toBe(100000);
|
||||
});
|
||||
|
||||
it('变量包含特殊 JSON 字符', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main(v) {
|
||||
return { text: v.text };
|
||||
}`,
|
||||
variables: { text: 'line1\nline2\ttab\\backslash"quote' }
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.text).toContain('line1');
|
||||
});
|
||||
|
||||
it('返回浮点数精度', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
return { val: 0.1 + 0.2 };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.val).toBeCloseTo(0.3, 10);
|
||||
});
|
||||
|
||||
it('Promise.reject 被正确捕获', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
await Promise.reject(new Error('rejected'));
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('rejected');
|
||||
});
|
||||
|
||||
it('setTimeout 在沙盒中可用', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve({ ok: true }), 50);
|
||||
});
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.ok).toBe(true);
|
||||
});
|
||||
|
||||
it('JSON 循环引用返回错误', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
const obj = {};
|
||||
obj.self = obj;
|
||||
return obj;
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
// JSON.stringify 循环引用会抛错
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('缺少 main 函数', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `const x = 42;`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('async 函数中 try/catch 正常工作', async () => {
|
||||
const result = await jsPool.execute({
|
||||
code: `async function main() {
|
||||
try {
|
||||
JSON.parse('invalid json');
|
||||
} catch(e) {
|
||||
return { caught: true, msg: e.message };
|
||||
}
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.caught).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,961 @@
|
||||
/**
|
||||
* ProcessPool / PythonProcessPool 单元测试
|
||||
*
|
||||
* 覆盖进程池核心逻辑:
|
||||
* - 生命周期(init / shutdown / stats)
|
||||
* - Worker 崩溃自动恢复(respawn)
|
||||
* - 池满排队行为
|
||||
* - 并发正确性
|
||||
* - shutdown 后行为
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { ProcessPool } from '../../src/pool/process-pool';
|
||||
import { PythonProcessPool } from '../../src/pool/python-process-pool';
|
||||
|
||||
// ============================================================
|
||||
// JS ProcessPool
|
||||
// ============================================================
|
||||
describe('ProcessPool 生命周期', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('init 后 stats 正确', async () => {
|
||||
pool = new ProcessPool(2);
|
||||
await pool.init();
|
||||
const s = pool.stats;
|
||||
expect(s.total).toBe(2);
|
||||
expect(s.idle).toBe(2);
|
||||
expect(s.busy).toBe(0);
|
||||
expect(s.queued).toBe(0);
|
||||
expect(s.poolSize).toBe(2);
|
||||
});
|
||||
|
||||
it('shutdown 后 stats 归零', async () => {
|
||||
pool = new ProcessPool(2);
|
||||
await pool.init();
|
||||
await pool.shutdown();
|
||||
const s = pool.stats;
|
||||
expect(s.total).toBe(0);
|
||||
expect(s.idle).toBe(0);
|
||||
expect(s.busy).toBe(0);
|
||||
});
|
||||
|
||||
it('execute 后 worker 归还到 idle', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
await pool.execute({
|
||||
code: `async function main() { return { ok: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
const s = pool.stats;
|
||||
expect(s.idle).toBe(1);
|
||||
expect(s.busy).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProcessPool Worker 恢复', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('worker 崩溃后自动 respawn,后续请求正常', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
// 让 worker 崩溃(process.exit)
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { process.exit(1); }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
|
||||
// 等 respawn 完成
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
|
||||
// 新 worker 应该可用
|
||||
const result2 = await pool.execute({
|
||||
code: `async function main() { return { recovered: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result2.success).toBe(true);
|
||||
expect(result2.data?.codeReturn.recovered).toBe(true);
|
||||
});
|
||||
|
||||
it('超时后 worker 被 kill 并 respawn', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { while(true) {} }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('timed out');
|
||||
|
||||
// 等 respawn
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
|
||||
const result2 = await pool.execute({
|
||||
code: `async function main() { return { ok: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result2.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProcessPool 并发与排队', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('pool size=2,3 个并发请求,1 个排队', async () => {
|
||||
pool = new ProcessPool(2);
|
||||
await pool.init();
|
||||
|
||||
// 3 个并发,每个 sleep 200ms
|
||||
const promises = Array.from({ length: 3 }, (_, i) =>
|
||||
pool.execute({
|
||||
code: `async function main(v) { await new Promise(r => setTimeout(r, 200)); return { idx: v.idx }; }`,
|
||||
variables: { idx: i }
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(results[i].success).toBe(true);
|
||||
expect(results[i].data?.codeReturn.idx).toBe(i);
|
||||
}
|
||||
});
|
||||
|
||||
it('pool size=1,10 个并发请求全部正确完成(串行排队)', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const promises = Array.from({ length: 10 }, (_, i) =>
|
||||
pool.execute({
|
||||
code: `async function main(v) { return { n: v.n * 2 }; }`,
|
||||
variables: { n: i }
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(results[i].success).toBe(true);
|
||||
expect(results[i].data?.codeReturn.n).toBe(i * 2);
|
||||
}
|
||||
});
|
||||
|
||||
it('pool size=2,并发中 1 个崩溃不影响其他请求', async () => {
|
||||
pool = new ProcessPool(2);
|
||||
await pool.init();
|
||||
|
||||
const p1 = pool.execute({
|
||||
code: `async function main() { process.exit(1); }`,
|
||||
variables: {}
|
||||
});
|
||||
const p2 = pool.execute({
|
||||
code: `async function main() { return { ok: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
const [r1, r2] = await Promise.all([p1, p2]);
|
||||
expect(r1.success).toBe(false);
|
||||
expect(r2.success).toBe(true);
|
||||
expect(r2.data?.codeReturn.ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// JS ProcessPool - Worker Ping/Pong 健康检查
|
||||
// ============================================================
|
||||
describe('ProcessPool Worker 健康检查 (ping/pong)', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('worker 正常响应 ping 后仍可执行任务', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// 先执行一个任务确认正常
|
||||
const r1 = await pool.execute({
|
||||
code: `async function main() { return { step: 1 }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r1.success).toBe(true);
|
||||
expect(r1.data?.codeReturn.step).toBe(1);
|
||||
|
||||
// 触发健康检查(通过 triggerHealthCheck)
|
||||
(pool as any).pingWorker((pool as any).idleWorkers[0]);
|
||||
|
||||
// 等 ping/pong 完成
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
// 再执行一个任务确认 worker 没被误杀
|
||||
const r2 = await pool.execute({
|
||||
code: `async function main() { return { step: 2 }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r2.success).toBe(true);
|
||||
expect(r2.data?.codeReturn.step).toBe(2);
|
||||
expect(pool.stats.total).toBe(1);
|
||||
});
|
||||
|
||||
it('连续多次 ping 不影响 worker 状态', async () => {
|
||||
pool = new ProcessPool(2);
|
||||
await pool.init();
|
||||
|
||||
// 对所有 idle worker 连续 ping 3 次
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (const w of [...(pool as any).idleWorkers]) {
|
||||
(pool as any).pingWorker(w);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
// 所有 worker 应该还在
|
||||
expect(pool.stats.total).toBe(2);
|
||||
expect(pool.stats.idle).toBe(2);
|
||||
|
||||
// 执行任务确认功能正常
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return { alive: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// JS ProcessPool - shutdown reject waiters
|
||||
// ============================================================
|
||||
describe('ProcessPool shutdown reject waiters', () => {
|
||||
it('shutdown 后 waitQueue 中的请求被 reject', async () => {
|
||||
const pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// 发起一个长时间运行的任务占住唯一 worker
|
||||
const p1 = pool.execute({
|
||||
code: `async function main() { await new Promise(r => setTimeout(r, 3000)); return { done: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 等一下确保 p1 已经拿到 worker
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
// 发起第二个请求,它会进入 waitQueue
|
||||
const p2 = pool.execute({
|
||||
code: `async function main() { return { queued: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 确认有排队请求
|
||||
expect(pool.stats.queued).toBe(1);
|
||||
|
||||
// shutdown 应该 reject waitQueue 中的请求
|
||||
await pool.shutdown();
|
||||
|
||||
// p2 应该被 reject
|
||||
await expect(p2).rejects.toThrow('shutting down');
|
||||
|
||||
// p1 可能成功也可能因 worker 被 kill 而失败,不关心
|
||||
await p1.catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// JS ProcessPool - 返回值序列化与参数校验(原 base-runner.test.ts)
|
||||
// ============================================================
|
||||
describe('ProcessPool 返回值序列化与参数校验', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('JS main 返回 undefined 序列化为 null', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return undefined; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn).toBeNull();
|
||||
});
|
||||
|
||||
it('JS main 无 return 语句序列化为 null', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { const x = 1; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn).toBeNull();
|
||||
});
|
||||
|
||||
it('code 为非字符串类型返回错误', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
const result = await pool.execute({
|
||||
code: 123 as any,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('empty');
|
||||
});
|
||||
|
||||
it('code 为 null 返回错误', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
const result = await pool.execute({
|
||||
code: null as any,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('empty');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// JS + Python 混合并发(原 base-runner.test.ts)
|
||||
// ============================================================
|
||||
describe('JS + Python 混合并发', () => {
|
||||
let jsPool: ProcessPool;
|
||||
let pyPool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await jsPool?.shutdown();
|
||||
await pyPool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('JS 和 Python 混合并发执行', async () => {
|
||||
jsPool = new ProcessPool(1);
|
||||
await jsPool.init();
|
||||
pyPool = new PythonProcessPool(1);
|
||||
await pyPool.init();
|
||||
|
||||
const jsPromise = jsPool.execute({
|
||||
code: `async function main() { return { lang: 'js' }; }`,
|
||||
variables: {}
|
||||
});
|
||||
const pyPromise = pyPool.execute({
|
||||
code: `def main():\n return {'lang': 'python'}`,
|
||||
variables: {}
|
||||
});
|
||||
const [jsResult, pyResult] = await Promise.all([jsPromise, pyPromise]);
|
||||
expect(jsResult.success).toBe(true);
|
||||
expect(jsResult.data?.codeReturn.lang).toBe('js');
|
||||
expect(pyResult.success).toBe(true);
|
||||
expect(pyResult.data?.codeReturn.lang).toBe('python');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// JS ProcessPool - 健康检查失败路径
|
||||
// ============================================================
|
||||
describe('ProcessPool 健康检查失败路径', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('ping timeout: worker 不响应 pong 时被替换', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
// 拦截 stdin.write 使 ping 消息不到达 worker(但不关闭 stdin),从而触发真正的 timeout
|
||||
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
|
||||
let interceptPing = true;
|
||||
worker.proc.stdin!.write = (...args: any[]) => {
|
||||
if (interceptPing) {
|
||||
interceptPing = false;
|
||||
return true; // 假装写成功但实际不发送
|
||||
}
|
||||
return origWrite(...args);
|
||||
};
|
||||
|
||||
// 触发 ping
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
// 等待 HEALTH_CHECK_TIMEOUT (5s) + respawn
|
||||
await new Promise((r) => setTimeout(r, 8000));
|
||||
|
||||
// worker 应该被替换,池仍然有 1 个 worker
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
// 新 worker 应该可用
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return { ok: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 15000);
|
||||
|
||||
it('stdin not writable: worker stdin 关闭时被替换', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
// 销毁 stdin 使其 writable = false
|
||||
worker.proc.stdin!.destroy();
|
||||
|
||||
// 触发 ping
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
// 等 respawn
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return { replaced: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('health check invalid response: worker 返回错误类型时被替换', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
|
||||
let intercepted = false;
|
||||
worker.proc.stdin!.write = (...args: any[]) => {
|
||||
if (!intercepted) {
|
||||
intercepted = true;
|
||||
setTimeout(() => worker.rl.emit('line', JSON.stringify({ type: 'wrong' })), 50);
|
||||
return true;
|
||||
}
|
||||
return origWrite(...args);
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return { invalidResp: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('returnToIdle with waiter: ping 期间有等待请求时直接分配', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
// ping 期间 worker 不在 idle 中,新请求进入 waitQueue
|
||||
// ping 成功后 returnToIdle 检查 waitQueue 并直接分配
|
||||
const p1 = pool.execute({
|
||||
code: `async function main() { return { fromWaiter: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
const result = await p1;
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.fromWaiter).toBe(true);
|
||||
});
|
||||
|
||||
it('health check parse error: worker 返回非 JSON 时被替换', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
|
||||
let intercepted = false;
|
||||
worker.proc.stdin!.write = (...args: any[]) => {
|
||||
if (!intercepted) {
|
||||
intercepted = true;
|
||||
setTimeout(() => worker.rl.emit('line', 'not-json-at-all'), 50);
|
||||
return true;
|
||||
}
|
||||
return origWrite(...args);
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return { parseError: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('health check write error: stdin.write 抛异常时被替换', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
// 让 stdin.write 抛异常,但 writable 仍为 true
|
||||
worker.proc.stdin!.write = () => {
|
||||
throw new Error('mock write error');
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { return { writeError: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('returnToIdle with waiter: ping 成功后分配给等待中的请求', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// 发起一个长任务占住 worker
|
||||
const p1 = pool.execute({
|
||||
code: `async function main() { await new Promise(r => setTimeout(r, 1000)); return { first: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 等 p1 拿到 worker
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
|
||||
// 发起第二个请求,它会进入 waitQueue
|
||||
const p2 = pool.execute({
|
||||
code: `async function main() { return { second: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 确认有排队
|
||||
expect(pool.stats.queued).toBe(1);
|
||||
|
||||
// 等 p1 完成,p2 应该自动被分配
|
||||
const [r1, r2] = await Promise.all([p1, p2]);
|
||||
expect(r1.success).toBe(true);
|
||||
expect(r1.data?.codeReturn.first).toBe(true);
|
||||
expect(r2.success).toBe(true);
|
||||
expect(r2.data?.codeReturn.second).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Python PythonProcessPool - Worker Ping/Pong 健康检查
|
||||
// ============================================================
|
||||
describe('PythonProcessPool Worker 健康检查 (ping/pong)', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('worker 正常响应 ping 后仍可执行任务', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const r1 = await pool.execute({
|
||||
code: `def main():\n return {'step': 1}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r1.success).toBe(true);
|
||||
expect(r1.data?.codeReturn.step).toBe(1);
|
||||
|
||||
// 触发 ping
|
||||
(pool as any).pingWorker((pool as any).idleWorkers[0]);
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
const r2 = await pool.execute({
|
||||
code: `def main():\n return {'step': 2}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r2.success).toBe(true);
|
||||
expect(r2.data?.codeReturn.step).toBe(2);
|
||||
expect(pool.stats.total).toBe(1);
|
||||
});
|
||||
|
||||
it('连续多次 ping 不影响 worker 状态', async () => {
|
||||
pool = new PythonProcessPool(2);
|
||||
await pool.init();
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (const w of [...(pool as any).idleWorkers]) {
|
||||
(pool as any).pingWorker(w);
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
|
||||
expect(pool.stats.total).toBe(2);
|
||||
expect(pool.stats.idle).toBe(2);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n return {'alive': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Python PythonProcessPool - 健康检查失败路径
|
||||
// ============================================================
|
||||
describe('PythonProcessPool 健康检查失败路径', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('ping timeout: worker 不响应 pong 时被替换', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
// 拦截 stdin.write 使 ping 不到达 worker,触发真正的 timeout
|
||||
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
|
||||
let interceptPing = true;
|
||||
worker.proc.stdin!.write = (...args: any[]) => {
|
||||
if (interceptPing) {
|
||||
interceptPing = false;
|
||||
return true;
|
||||
}
|
||||
return origWrite(...args);
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 8000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n return {'ok': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 15000);
|
||||
|
||||
it('stdin not writable: worker stdin 关闭时被替换', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
worker.proc.stdin!.destroy();
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n return {'replaced': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('health check invalid response: worker 返回错误类型时被替换', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
|
||||
let intercepted = false;
|
||||
worker.proc.stdin!.write = (...args: any[]) => {
|
||||
if (!intercepted) {
|
||||
intercepted = true;
|
||||
setTimeout(() => worker.rl.emit('line', JSON.stringify({ type: 'wrong' })), 50);
|
||||
return true;
|
||||
}
|
||||
return origWrite(...args);
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n return {'invalidResp': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('returnToIdle with waiter: ping 期间有等待请求时直接分配', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
const p1 = pool.execute({
|
||||
code: `def main():\n return {'fromWaiter': True}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
const result = await p1;
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.fromWaiter).toBe(true);
|
||||
});
|
||||
|
||||
it('health check parse error: worker 返回非 JSON 时被替换', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
const origWrite = worker.proc.stdin!.write.bind(worker.proc.stdin!);
|
||||
let intercepted = false;
|
||||
worker.proc.stdin!.write = (...args: any[]) => {
|
||||
if (!intercepted) {
|
||||
intercepted = true;
|
||||
setTimeout(() => worker.rl.emit('line', 'not-json'), 50);
|
||||
return true;
|
||||
}
|
||||
return origWrite(...args);
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n return {'parseError': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
it('health check write error: stdin.write 抛异常时被替换', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const worker = (pool as any).idleWorkers[0];
|
||||
worker.proc.stdin!.write = () => {
|
||||
throw new Error('mock write error');
|
||||
};
|
||||
|
||||
(pool as any).pingWorker(worker);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n return {'writeError': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Python PythonProcessPool - shutdown reject waiters
|
||||
// ============================================================
|
||||
describe('PythonProcessPool shutdown reject waiters', () => {
|
||||
it('shutdown 后 waitQueue 中的请求被 reject', async () => {
|
||||
const pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// 发起一个长时间运行的任务占住唯一 worker
|
||||
const p1 = pool.execute({
|
||||
code: `import time\ndef main():\n time.sleep(3)\n return {'done': True}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 等一下确保 p1 已经拿到 worker
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
|
||||
// 发起第二个请求,它会进入 waitQueue
|
||||
const p2 = pool.execute({
|
||||
code: `def main():\n return {'queued': True}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 确认有排队请求
|
||||
expect(pool.stats.queued).toBe(1);
|
||||
|
||||
// shutdown 应该 reject waitQueue 中的请求
|
||||
await pool.shutdown();
|
||||
|
||||
// p2 应该被 reject
|
||||
await expect(p2).rejects.toThrow('shutting down');
|
||||
|
||||
// p1 可能成功也可能因 worker 被 kill 而失败,不关心
|
||||
await p1.catch(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Python PythonProcessPool
|
||||
// ============================================================
|
||||
describe('PythonProcessPool 生命周期', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('init 后 stats 正确', async () => {
|
||||
pool = new PythonProcessPool(2);
|
||||
await pool.init();
|
||||
const s = pool.stats;
|
||||
expect(s.total).toBe(2);
|
||||
expect(s.idle).toBe(2);
|
||||
expect(s.busy).toBe(0);
|
||||
expect(s.queued).toBe(0);
|
||||
expect(s.poolSize).toBe(2);
|
||||
});
|
||||
|
||||
it('shutdown 后 stats 归零', async () => {
|
||||
pool = new PythonProcessPool(2);
|
||||
await pool.init();
|
||||
await pool.shutdown();
|
||||
const s = pool.stats;
|
||||
expect(s.total).toBe(0);
|
||||
expect(s.idle).toBe(0);
|
||||
expect(s.busy).toBe(0);
|
||||
});
|
||||
|
||||
it('execute 后 worker 归还到 idle', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
await pool.execute({
|
||||
code: `def main():\n return {'ok': True}`,
|
||||
variables: {}
|
||||
});
|
||||
const s = pool.stats;
|
||||
expect(s.idle).toBe(1);
|
||||
expect(s.busy).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PythonProcessPool Worker 恢复', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('超时后 worker 被 kill 并 respawn', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n while True:\n pass`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('timed out');
|
||||
|
||||
// 等 respawn
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const result2 = await pool.execute({
|
||||
code: `def main():\n return {'ok': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result2.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PythonProcessPool 并发与排队', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('pool size=2,3 个并发请求,1 个排队', async () => {
|
||||
pool = new PythonProcessPool(2);
|
||||
await pool.init();
|
||||
|
||||
const promises = Array.from({ length: 3 }, (_, i) =>
|
||||
pool.execute({
|
||||
code: `import time\ndef main(variables):\n time.sleep(0.2)\n return {'idx': variables['idx']}`,
|
||||
variables: { idx: i }
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(results[i].success).toBe(true);
|
||||
expect(results[i].data?.codeReturn.idx).toBe(i);
|
||||
}
|
||||
});
|
||||
|
||||
it('pool size=1,10 个并发请求全部正确完成(串行排队)', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const promises = Array.from({ length: 10 }, (_, i) =>
|
||||
pool.execute({
|
||||
code: `def main(variables):\n return {'n': variables['n'] * 2}`,
|
||||
variables: { n: i }
|
||||
})
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
expect(results[i].success).toBe(true);
|
||||
expect(results[i].data?.codeReturn.n).toBe(i * 2);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,662 @@
|
||||
/**
|
||||
* 资源限制测试
|
||||
*
|
||||
* 覆盖:
|
||||
* - 内存限制(RSS 轮询监控)
|
||||
* - CPU 密集型超时(JS / Python)
|
||||
* - 运行时长限制(wall-clock timeout 验证)
|
||||
* - 网络请求限制(次数、请求体大小、响应大小)
|
||||
|
||||
*/
|
||||
import { describe, it, expect, afterEach, beforeAll } from 'vitest';
|
||||
import { ProcessPool } from '../../src/pool/process-pool';
|
||||
import { PythonProcessPool } from '../../src/pool/python-process-pool';
|
||||
import { config } from '../../src/config';
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log(`\n=== Memory Limit Test Status ===`);
|
||||
console.log(`Method: RSS polling (cross-platform)`);
|
||||
console.log(`User configured memory: ${config.maxMemoryMB}MB`);
|
||||
console.log(
|
||||
`Actual process limit: ${config.maxMemoryMB + config.RUNTIME_MEMORY_OVERHEAD_MB}MB (${config.maxMemoryMB}MB user + ${config.RUNTIME_MEMORY_OVERHEAD_MB}MB runtime)`
|
||||
);
|
||||
console.log(`=============================\n`);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 1. 内存限制(RSS 轮询监控,跨平台)
|
||||
// ============================================================
|
||||
describe('内存限制', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('JS 分配超大内存被 RSS 监控终止后自动 respawn', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
// 实际限制 = 用户配置 + 运行时开销(50MB)
|
||||
const actualLimitMB = config.maxMemoryMB + config.RUNTIME_MEMORY_OVERHEAD_MB;
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
const arr = [];
|
||||
// 逐步分配内存(使用随机字符串填充,防止 OS 内存压缩优化)
|
||||
// 每次 10MB,每轮等待 200ms 让 RSS 监控有机会检测
|
||||
for (let i = 0; i < 40; i++) {
|
||||
arr.push(Buffer.alloc(10 * 1024 * 1024, String(Date.now() + i)));
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
return { allocated: arr.length };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/memory|Memory|crash|Worker|timed out/i);
|
||||
|
||||
// 等 respawn
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
// 新 worker 应该可用
|
||||
const result2 = await pool.execute({
|
||||
code: `async function main() { return { recovered: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result2.success).toBe(true);
|
||||
expect(result2.data?.codeReturn.recovered).toBe(true);
|
||||
}, 30000);
|
||||
|
||||
it('JS 分配配置范围内的内存正常工作', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
// 分配少量内存(远小于限制),确保不会被误杀
|
||||
const allocMB = 10;
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
const arr = [];
|
||||
for (let i = 0; i < ${allocMB}; i++) {
|
||||
arr.push(Buffer.alloc(1024 * 1024));
|
||||
}
|
||||
return { allocated: arr.length, totalMB: arr.length };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.allocated).toBe(allocMB);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('Python 内存限制', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('Python 分配超大内存被 RSS 监控终止后自动 respawn', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `import time\nimport random\ndef main():\n chunks = []\n for i in range(40):\n chunk = bytearray(10 * 1024 * 1024)\n chunk[0] = i % 256\n chunks.append(chunk)\n time.sleep(0.2)\n return {'size': len(chunks)}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/memory|Memory|crash|Worker|timed out/i);
|
||||
|
||||
// 等 respawn
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const result2 = await pool.execute({
|
||||
code: `def main():\n return {'recovered': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result2.success).toBe(true);
|
||||
expect(result2.data?.codeReturn.recovered).toBe(true);
|
||||
}, 30000);
|
||||
|
||||
it('Python 分配配置范围内的内存正常工作', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
expect(pool.stats.total).toBe(1);
|
||||
|
||||
const allocMB = 10;
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n data = bytearray(${allocMB} * 1024 * 1024)\n return {'allocated': len(data), 'totalMB': len(data) // (1024 * 1024)}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.totalMB).toBe(allocMB);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 2. CPU 限制
|
||||
// ============================================================
|
||||
describe('JS CPU 密集型超时', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('纯计算死循环被超时终止', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const start = Date.now();
|
||||
const result = await pool.execute({
|
||||
code: `async function main() { while(true) { Math.random(); } }`,
|
||||
variables: {}
|
||||
});
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/timed out|timeout/i);
|
||||
// 应该在合理时间内被终止(超时 + 一些余量)
|
||||
expect(elapsed).toBeLessThan(30000);
|
||||
});
|
||||
|
||||
it('CPU 密集型计算(大量数学运算)被超时终止', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
let x = 0;
|
||||
while(true) {
|
||||
x += Math.sin(x) * Math.cos(x);
|
||||
}
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/timed out|timeout/i);
|
||||
});
|
||||
|
||||
it('CPU 超时后 worker 恢复正常', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
await pool.execute({
|
||||
code: `async function main() { while(true) {} }`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
|
||||
const r2 = await pool.execute({
|
||||
code: `async function main() { return { ok: true }; }`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r2.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python CPU 密集型超时', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('纯计算死循环被超时终止', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const start = Date.now();
|
||||
const result = await pool.execute({
|
||||
code: `import math\ndef main():\n x = 0\n while True:\n x += math.sin(x) * math.cos(x)`,
|
||||
variables: {}
|
||||
});
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/timed out|timeout/i);
|
||||
expect(elapsed).toBeLessThan(30000);
|
||||
});
|
||||
|
||||
it('CPU 超时后 worker 恢复正常', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
await pool.execute({
|
||||
code: `def main():\n while True:\n pass`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
|
||||
const r2 = await pool.execute({
|
||||
code: `def main():\n return {'ok': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r2.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 3. 运行时长限制(wall-clock timeout)
|
||||
// ============================================================
|
||||
describe('JS 运行时长限制', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('sleep 超过 maxTimeoutMs 被终止', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const start = Date.now();
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
await new Promise(r => setTimeout(r, ${config.maxTimeoutMs + 30000}));
|
||||
return { done: true };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/timed out|timeout/i);
|
||||
// 实际耗时应在 maxTimeoutMs 附近(加上 2s 余量),不会等到 sleep 结束
|
||||
expect(elapsed).toBeLessThan(config.maxTimeoutMs + 10000);
|
||||
});
|
||||
|
||||
it('在超时范围内完成的代码正常返回', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
return { elapsed: true };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.elapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('delay() 超过 10s 上限被拒绝', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
await delay(15000);
|
||||
return { done: true };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('10000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python 运行时长限制', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('sleep 超过超时限制被终止', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const start = Date.now();
|
||||
const result = await pool.execute({
|
||||
code: `import time\ndef main():\n time.sleep(${Math.ceil(config.maxTimeoutMs / 1000) + 30})\n return {'done': True}`,
|
||||
variables: {}
|
||||
});
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toMatch(/timed out|timeout/i);
|
||||
expect(elapsed).toBeLessThan(config.maxTimeoutMs + 10000);
|
||||
});
|
||||
|
||||
it('在超时范围内完成的代码正常返回', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `import time\ndef main():\n time.sleep(0.1)\n return {'elapsed': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.elapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('delay() 超过 10s 上限被拒绝', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n delay(15000)\n return {'done': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 4. 网络请求次数限制
|
||||
// ============================================================
|
||||
describe('JS 网络请求次数限制', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it(`第 maxRequests+1 次请求被拒绝(计数器验证)`, async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// 快速消耗计数器:每次 httpRequest 调用会先 ++requestCount 再发起网络请求
|
||||
// 即使网络请求失败(DNS/连接),计数器也已递增
|
||||
// 为避免超时,用循环快速调用并 catch 所有错误,只关注 limit 错误
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
let limitError = null;
|
||||
for (let i = 0; i < ${config.maxRequests + 1}; i++) {
|
||||
try {
|
||||
await httpRequest('http://0.0.0.0:1');
|
||||
} catch(e) {
|
||||
if (e.message.includes('limit') || e.message.includes('Limit')) {
|
||||
limitError = { idx: i, msg: e.message };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { limitError };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
const le = result.data?.codeReturn.limitError;
|
||||
expect(le).not.toBeNull();
|
||||
expect(le.idx).toBe(config.maxRequests);
|
||||
expect(le.msg).toMatch(/limit/i);
|
||||
});
|
||||
|
||||
it('请求计数每次执行重置', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// 第一次执行:消耗一些计数
|
||||
await pool.execute({
|
||||
code: `async function main() {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try { await httpRequest('http://0.0.0.0:1'); } catch(e) {}
|
||||
}
|
||||
return {};
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
// 第二次执行:计数应该重置,第一次请求不会触发 limit
|
||||
const r2 = await pool.execute({
|
||||
code: `async function main() {
|
||||
let limitHit = false;
|
||||
try { await httpRequest('http://0.0.0.0:1'); } catch(e) {
|
||||
if (e.message.includes('limit') || e.message.includes('Limit')) limitHit = true;
|
||||
}
|
||||
return { limitHit };
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r2.success).toBe(true);
|
||||
expect(r2.data?.codeReturn.limitHit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python 网络请求次数限制', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it(`第 maxRequests+1 次请求被拒绝(计数器验证)`, async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n limit_error = None\n for i in range(${config.maxRequests + 1}):\n try:\n http_request('http://0.0.0.0:1')\n except Exception as e:\n if 'limit' in str(e).lower():\n limit_error = {'idx': i, 'msg': str(e)}\n break\n return {'limit_error': limit_error}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
const le = result.data?.codeReturn.limit_error;
|
||||
expect(le).not.toBeNull();
|
||||
expect(le.idx).toBe(config.maxRequests);
|
||||
expect(le.msg.toLowerCase()).toContain('limit');
|
||||
});
|
||||
|
||||
it('请求计数每次执行重置', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
await pool.execute({
|
||||
code: `def main():\n for i in range(3):\n try:\n http_request('http://0.0.0.0:1')\n except:\n pass\n return {}`,
|
||||
variables: {}
|
||||
});
|
||||
|
||||
const r2 = await pool.execute({
|
||||
code: `def main():\n limit_hit = False\n try:\n http_request('http://0.0.0.0:1')\n except Exception as e:\n if 'limit' in str(e).lower():\n limit_hit = True\n return {'limit_hit': limit_hit}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(r2.success).toBe(true);
|
||||
expect(r2.data?.codeReturn.limit_hit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 5. 网络请求大小限制
|
||||
// ============================================================
|
||||
describe('JS 请求体大小限制', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('请求体超过 maxRequestBodySize 被拒绝', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
// maxRequestBodySize 单位是 MB,生成超过限制的 body
|
||||
const sizeMB = config.maxRequestBodySize;
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
const bigBody = 'x'.repeat(${sizeMB} * 1024 * 1024 + 1);
|
||||
try {
|
||||
await httpRequest('https://example.com', { method: 'POST', body: bigBody });
|
||||
return { blocked: false };
|
||||
} catch(e) {
|
||||
return { blocked: true, msg: e.message };
|
||||
}
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.blocked).toBe(true);
|
||||
expect(result.data?.codeReturn.msg).toMatch(/body.*large|too large/i);
|
||||
});
|
||||
|
||||
it('请求体在限制内正常发送(不因大小被拒)', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
const smallBody = JSON.stringify({ data: 'hello' });
|
||||
try {
|
||||
await httpRequest('https://example.com', { method: 'POST', body: smallBody });
|
||||
return { sizeOk: true };
|
||||
} catch(e) {
|
||||
// 网络错误可以接受,但不应该是 body too large
|
||||
return { sizeOk: !e.message.includes('too large'), msg: e.message };
|
||||
}
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.sizeOk).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python 请求体大小限制', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('请求体超过 maxRequestBodySize 被拒绝', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const sizeMB = config.maxRequestBodySize;
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n big_body = 'x' * (${sizeMB} * 1024 * 1024 + 1)\n try:\n http_request('https://example.com', method='POST', body=big_body)\n return {'blocked': False}\n except Exception as e:\n return {'blocked': True, 'msg': str(e)}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.blocked).toBe(true);
|
||||
expect(result.data?.codeReturn.msg).toMatch(/body.*large|too large/i);
|
||||
});
|
||||
|
||||
it('请求体在限制内正常发送(不因大小被拒)', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n try:\n http_request('https://example.com', method='POST', body='hello')\n return {'size_ok': True}\n except Exception as e:\n return {'size_ok': 'too large' not in str(e).lower(), 'msg': str(e)}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.size_ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 6. 网络协议限制
|
||||
// ============================================================
|
||||
describe('JS 网络协议限制', () => {
|
||||
let pool: ProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('ftp:// 协议被拒绝', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
try {
|
||||
await httpRequest('ftp://example.com/file');
|
||||
return { blocked: false };
|
||||
} catch(e) {
|
||||
return { blocked: true, msg: e.message };
|
||||
}
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.blocked).toBe(true);
|
||||
expect(result.data?.codeReturn.msg).toMatch(/protocol/i);
|
||||
});
|
||||
|
||||
it('file:// 协议被拒绝', async () => {
|
||||
pool = new ProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `async function main() {
|
||||
try {
|
||||
await httpRequest('file:///etc/passwd');
|
||||
return { blocked: false };
|
||||
} catch(e) {
|
||||
return { blocked: true, msg: e.message };
|
||||
}
|
||||
}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.blocked).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Python 网络协议限制', () => {
|
||||
let pool: PythonProcessPool;
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await pool?.shutdown();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it('ftp:// 协议被拒绝', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n try:\n http_request('ftp://example.com/file')\n return {'blocked': False}\n except Exception as e:\n return {'blocked': True, 'msg': str(e)}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.blocked).toBe(true);
|
||||
expect(result.data?.codeReturn.msg.toLowerCase()).toContain('protocol');
|
||||
});
|
||||
|
||||
it('file:// 协议被拒绝', async () => {
|
||||
pool = new PythonProcessPool(1);
|
||||
await pool.init();
|
||||
|
||||
const result = await pool.execute({
|
||||
code: `def main():\n try:\n http_request('file:///etc/passwd')\n return {'blocked': False}\n except Exception as e:\n return {'blocked': True}`,
|
||||
variables: {}
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.codeReturn.blocked).toBe(true);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* Semaphore 信号量单元测试
|
||||
*
|
||||
* 测试并发控制核心逻辑:
|
||||
* - 基本 acquire/release 流程
|
||||
* - 超出 max 后排队等待
|
||||
* - release 唤醒队列中下一个
|
||||
* - stats 返回正确的 current/queued/max
|
||||
* - 并发数为 1 时串行执行
|
||||
* - 大量并发请求排队后依次完成
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Semaphore } from '../../src/utils/semaphore';
|
||||
|
||||
describe('Semaphore', () => {
|
||||
// ===== 基本流程 =====
|
||||
|
||||
it('acquire 在未满时立即返回', async () => {
|
||||
const sem = new Semaphore(3);
|
||||
// 三次 acquire 都应该立即 resolve
|
||||
await sem.acquire();
|
||||
await sem.acquire();
|
||||
await sem.acquire();
|
||||
expect(sem.stats).toEqual({ current: 3, queued: 0, max: 3 });
|
||||
});
|
||||
|
||||
it('release 减少 current 计数', async () => {
|
||||
const sem = new Semaphore(2);
|
||||
await sem.acquire();
|
||||
await sem.acquire();
|
||||
expect(sem.stats.current).toBe(2);
|
||||
sem.release();
|
||||
expect(sem.stats.current).toBe(1);
|
||||
sem.release();
|
||||
expect(sem.stats.current).toBe(0);
|
||||
});
|
||||
|
||||
it('stats 返回正确的 current/queued/max', async () => {
|
||||
const sem = new Semaphore(2);
|
||||
expect(sem.stats).toEqual({ current: 0, queued: 0, max: 2 });
|
||||
|
||||
await sem.acquire();
|
||||
expect(sem.stats).toEqual({ current: 1, queued: 0, max: 2 });
|
||||
|
||||
await sem.acquire();
|
||||
expect(sem.stats).toEqual({ current: 2, queued: 0, max: 2 });
|
||||
|
||||
// 第三个会排队(不 await,因为它不会 resolve)
|
||||
const p3 = sem.acquire();
|
||||
expect(sem.stats).toEqual({ current: 2, queued: 1, max: 2 });
|
||||
|
||||
// 第四个也排队
|
||||
const p4 = sem.acquire();
|
||||
expect(sem.stats).toEqual({ current: 2, queued: 2, max: 2 });
|
||||
|
||||
// release 唤醒队列中第一个,queued 减 1,current 不变(因为立即被新的占用)
|
||||
sem.release();
|
||||
await p3;
|
||||
expect(sem.stats).toEqual({ current: 2, queued: 1, max: 2 });
|
||||
|
||||
sem.release();
|
||||
await p4;
|
||||
expect(sem.stats).toEqual({ current: 2, queued: 0, max: 2 });
|
||||
});
|
||||
|
||||
// ===== 排队与唤醒 =====
|
||||
|
||||
it('超出 max 后排队等待,release 唤醒下一个', async () => {
|
||||
const sem = new Semaphore(1);
|
||||
const order: number[] = [];
|
||||
|
||||
await sem.acquire();
|
||||
order.push(1);
|
||||
|
||||
// 第二个 acquire 会排队
|
||||
const p2 = sem.acquire().then(() => {
|
||||
order.push(2);
|
||||
});
|
||||
expect(sem.stats.queued).toBe(1);
|
||||
|
||||
// release 唤醒排队的
|
||||
sem.release();
|
||||
await p2;
|
||||
expect(order).toEqual([1, 2]);
|
||||
expect(sem.stats.queued).toBe(0);
|
||||
|
||||
sem.release();
|
||||
expect(sem.stats.current).toBe(0);
|
||||
});
|
||||
|
||||
it('release 按 FIFO 顺序唤醒', async () => {
|
||||
const sem = new Semaphore(1);
|
||||
const order: number[] = [];
|
||||
|
||||
await sem.acquire();
|
||||
|
||||
const p1 = sem.acquire().then(() => {
|
||||
order.push(1);
|
||||
});
|
||||
const p2 = sem.acquire().then(() => {
|
||||
order.push(2);
|
||||
});
|
||||
const p3 = sem.acquire().then(() => {
|
||||
order.push(3);
|
||||
});
|
||||
|
||||
expect(sem.stats.queued).toBe(3);
|
||||
|
||||
// 依次 release,应按 FIFO 顺序唤醒
|
||||
sem.release();
|
||||
await p1;
|
||||
|
||||
sem.release();
|
||||
await p2;
|
||||
|
||||
sem.release();
|
||||
await p3;
|
||||
|
||||
expect(order).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
// ===== 并发数为 1 时串行执行 =====
|
||||
|
||||
it('max=1 时保证串行执行', async () => {
|
||||
const sem = new Semaphore(1);
|
||||
const log: string[] = [];
|
||||
|
||||
const task = async (name: string, delayMs: number) => {
|
||||
await sem.acquire();
|
||||
log.push(`${name}-start`);
|
||||
await new Promise((r) => setTimeout(r, delayMs));
|
||||
log.push(`${name}-end`);
|
||||
sem.release();
|
||||
};
|
||||
|
||||
// 同时启动三个任务
|
||||
await Promise.all([task('A', 50), task('B', 50), task('C', 50)]);
|
||||
|
||||
// 串行执行:每个任务的 start 必须在前一个 end 之后
|
||||
// A-start, A-end, B-start, B-end, C-start, C-end
|
||||
for (let i = 0; i < log.length - 1; i += 2) {
|
||||
const startIdx = i;
|
||||
const endIdx = i + 1;
|
||||
expect(log[endIdx]).toContain('-end');
|
||||
expect(log[startIdx]).toContain('-start');
|
||||
// end 在 start 之后
|
||||
expect(endIdx).toBeGreaterThan(startIdx);
|
||||
}
|
||||
|
||||
// 更严格:不能有两个 start 连续出现(说明并行了)
|
||||
for (let i = 0; i < log.length - 1; i++) {
|
||||
if (log[i].endsWith('-start')) {
|
||||
expect(log[i + 1]).toContain('-end');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 大量并发 =====
|
||||
|
||||
it('大量并发请求排队后依次完成', async () => {
|
||||
const sem = new Semaphore(3);
|
||||
const total = 20;
|
||||
let completed = 0;
|
||||
let maxConcurrent = 0;
|
||||
let currentRunning = 0;
|
||||
|
||||
const tasks = Array.from({ length: total }, (_, i) =>
|
||||
(async () => {
|
||||
await sem.acquire();
|
||||
currentRunning++;
|
||||
if (currentRunning > maxConcurrent) {
|
||||
maxConcurrent = currentRunning;
|
||||
}
|
||||
// 模拟异步工作
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
currentRunning--;
|
||||
completed++;
|
||||
sem.release();
|
||||
})()
|
||||
);
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
// 全部完成
|
||||
expect(completed).toBe(total);
|
||||
// 最大并发不超过 max
|
||||
expect(maxConcurrent).toBeLessThanOrEqual(3);
|
||||
// 最终状态归零
|
||||
expect(sem.stats.current).toBe(0);
|
||||
expect(sem.stats.queued).toBe(0);
|
||||
});
|
||||
|
||||
it('max=1 大量并发严格串行', async () => {
|
||||
const sem = new Semaphore(1);
|
||||
const total = 10;
|
||||
let maxConcurrent = 0;
|
||||
let currentRunning = 0;
|
||||
|
||||
const tasks = Array.from({ length: total }, () =>
|
||||
(async () => {
|
||||
await sem.acquire();
|
||||
currentRunning++;
|
||||
if (currentRunning > maxConcurrent) maxConcurrent = currentRunning;
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
currentRunning--;
|
||||
sem.release();
|
||||
})()
|
||||
);
|
||||
|
||||
await Promise.all(tasks);
|
||||
expect(maxConcurrent).toBe(1);
|
||||
expect(sem.stats.current).toBe(0);
|
||||
});
|
||||
|
||||
// ===== 边界情况 =====
|
||||
|
||||
it('release 无排队时 current 不会变为负数', () => {
|
||||
const sem = new Semaphore(3);
|
||||
// 没有 acquire 就 release
|
||||
sem.release();
|
||||
// current 变为 -1,这是实现的已知行为(调用者应保证配对使用)
|
||||
expect(sem.stats.current).toBe(-1);
|
||||
});
|
||||
|
||||
it('max 为很大的数时不排队', async () => {
|
||||
const sem = new Semaphore(1000);
|
||||
const promises = Array.from({ length: 100 }, () => sem.acquire());
|
||||
await Promise.all(promises);
|
||||
expect(sem.stats.current).toBe(100);
|
||||
expect(sem.stats.queued).toBe(0);
|
||||
});
|
||||
|
||||
it('acquire 返回的 Promise 是 void', async () => {
|
||||
const sem = new Semaphore(1);
|
||||
const result = await sem.acquire();
|
||||
expect(result).toBeUndefined();
|
||||
sem.release();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// 竞态条件补充(原 semaphore-race.test.ts)
|
||||
// ============================================================
|
||||
describe('Semaphore 竞态条件补充', () => {
|
||||
it('release 过多后 acquire 仍能正常工作', async () => {
|
||||
const sem = new Semaphore(2);
|
||||
sem.release();
|
||||
expect(sem.stats.current).toBe(-1);
|
||||
|
||||
await sem.acquire();
|
||||
expect(sem.stats.current).toBe(0);
|
||||
await sem.acquire();
|
||||
expect(sem.stats.current).toBe(1);
|
||||
await sem.acquire();
|
||||
expect(sem.stats.current).toBe(2);
|
||||
|
||||
const p = sem.acquire();
|
||||
expect(sem.stats.queued).toBe(1);
|
||||
sem.release();
|
||||
await p;
|
||||
});
|
||||
|
||||
it('快速交替 acquire/release 不丢失状态', async () => {
|
||||
const sem = new Semaphore(1);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await sem.acquire();
|
||||
sem.release();
|
||||
}
|
||||
expect(sem.stats.current).toBe(0);
|
||||
expect(sem.stats.queued).toBe(0);
|
||||
});
|
||||
|
||||
it('异步任务异常后 release 仍被调用(模拟 try/finally)', async () => {
|
||||
const sem = new Semaphore(2);
|
||||
const errors: string[] = [];
|
||||
|
||||
const task = async (shouldFail: boolean) => {
|
||||
await sem.acquire();
|
||||
try {
|
||||
if (shouldFail) throw new Error('task failed');
|
||||
return 'ok';
|
||||
} catch (e: any) {
|
||||
errors.push(e.message);
|
||||
return 'error';
|
||||
} finally {
|
||||
sem.release();
|
||||
}
|
||||
};
|
||||
|
||||
const results = await Promise.all([
|
||||
task(false),
|
||||
task(true),
|
||||
task(false),
|
||||
task(true),
|
||||
task(false)
|
||||
]);
|
||||
|
||||
expect(results.filter((r) => r === 'ok')).toHaveLength(3);
|
||||
expect(results.filter((r) => r === 'error')).toHaveLength(2);
|
||||
expect(errors).toHaveLength(2);
|
||||
expect(sem.stats.current).toBe(0);
|
||||
expect(sem.stats.queued).toBe(0);
|
||||
});
|
||||
|
||||
it('max=0 时所有 acquire 都排队', async () => {
|
||||
const sem = new Semaphore(0);
|
||||
const p1 = sem.acquire();
|
||||
const p2 = sem.acquire();
|
||||
expect(sem.stats.queued).toBe(2);
|
||||
expect(sem.stats.current).toBe(0);
|
||||
|
||||
sem.release();
|
||||
await p1;
|
||||
sem.release();
|
||||
await p2;
|
||||
});
|
||||
|
||||
it('并发 acquire 后批量 release', async () => {
|
||||
const sem = new Semaphore(2);
|
||||
await sem.acquire();
|
||||
await sem.acquire();
|
||||
|
||||
const waiters = Array.from({ length: 5 }, () => sem.acquire());
|
||||
expect(sem.stats.queued).toBe(5);
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
sem.release();
|
||||
}
|
||||
await Promise.all(waiters);
|
||||
|
||||
expect(sem.stats.queued).toBe(0);
|
||||
expect(sem.stats.current).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user