Files
massive-cloud/server/algorithmJob.ts

131 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 通用算法任务调度器(Route B)
* ------------------------------------------------------------
* 职责:
* 1. 维护一个带上限的并发池(默认 4 并发),避免一次性 fork 太多
* Python 子进程把宿主机打爆;
* 2. 把每个任务 fork 到独立 Node 子进程(algorithmJobRunner.ts),
* 让 HTTP/Vite 主进程不阻塞;
* 3. 子进程负责按 manifest 执行 process / mock runtime,
* 解析 stdout 进度,写 result.json,更新数据库。
*
* 主进程不直接 spawn Python,而是 fork Node runner —— 与现有
* smoothHeatmapJob / deviationHeatmapJob 的架构保持一致。
*/
import { fork, ChildProcess } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const DEFAULT_CONCURRENCY = Number(process.env.ALGORITHM_JOB_CONCURRENCY ?? 4);
interface PendingJob {
resultId: number;
taskId: number;
pluginId: string;
params: Record<string, unknown>;
/** 可选:只处理选中的点云文件 ID;undefined/空数组 = 全部 */
pointCloudFileIds?: number[];
/** 可选:任务级 CAD 文件 ID;undefined/空数组 = 不传 CAD */
cadFileIds?: number[];
resolve: () => void;
reject: (err: unknown) => void;
}
class ConcurrencyPool {
private running = 0;
private queue: PendingJob[] = [];
private readonly limit: number;
private readonly activeChildren = new Set<ChildProcess>();
constructor(limit: number) {
this.limit = Math.max(1, limit);
}
get stats() {
return { running: this.running, queued: this.queue.length, limit: this.limit };
}
enqueue(job: Omit<PendingJob, 'resolve' | 'reject'>): Promise<void> {
return new Promise((resolve, reject) => {
this.queue.push({ ...job, resolve, reject });
this.pump();
});
}
private pump(): void {
while (this.running < this.limit && this.queue.length > 0) {
const next = this.queue.shift()!;
this.running += 1;
this.spawnChild(next).finally(() => {
this.running = Math.max(0, this.running - 1);
this.pump();
});
}
}
private async spawnChild(job: PendingJob): Promise<void> {
const script = resolveAlgorithmRunnerEntry();
const isProd = process.env.NODE_ENV === 'production';
const args = [
String(job.resultId),
String(job.taskId),
job.pluginId,
JSON.stringify(job.params ?? {}),
JSON.stringify(job.pointCloudFileIds ?? []),
JSON.stringify(job.cadFileIds ?? []),
];
const child = fork(script, args, {
env: { ...process.env },
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
execArgv: isProd ? [] : ['--import', 'tsx'],
});
this.activeChildren.add(child);
return new Promise<void>((resolve) => {
child.on('error', (err) => {
console.error('[AlgorithmJob] fork 失败:', err);
});
child.on('exit', (code, signal) => {
this.activeChildren.delete(child);
if (code !== 0 && code != null) {
console.error(
`[AlgorithmJob] 子进程退出 code=${code} signal=${signal ?? ''} pluginId=${job.pluginId} resultId=${job.resultId}`,
);
}
// 即使失败也 resolve:runner 已经把数据库改成 error,主进程无需再处理
job.resolve();
resolve();
});
});
}
}
function resolveAlgorithmRunnerEntry(): string {
const dir = path.dirname(fileURLToPath(import.meta.url));
if (process.env.NODE_ENV === 'production') {
return path.join(dir, 'algorithmJobRunner.js');
}
return path.join(dir, 'algorithmJobRunner.ts');
}
const pool = new ConcurrencyPool(DEFAULT_CONCURRENCY);
/**
* 把一次插件运行加入并发池。返回的 Promise 在子进程退出时 resolve(无论成功/失败)。
* 真实结果状态写在 task_analysis_results.resultData / status 中,由前端轮询消费。
*/
export function enqueueAlgorithmJob(
resultId: number,
taskId: number,
pluginId: string,
params: Record<string, unknown> = {},
pointCloudFileIds?: number[],
cadFileIds?: number[],
): Promise<void> {
return pool.enqueue({ resultId, taskId, pluginId, params, pointCloudFileIds, cadFileIds });
}
export function algorithmJobStats() {
return pool.stats;
}