Commit a70948c7 by luoqi

merge: perf/persona-recompute → test(plan 侧也去掉批次栅栏)

️ 只合代码,**先不部署** —— 测试服正在跑画像回填(优化后代码),部署会重建容器把它杀掉。
等回填 + 链上的 plan 全量重算跑完再部署。
parents 470a057f b9765cd8
Pipeline #3449 failed in 0 seconds
...@@ -31,6 +31,14 @@ function parseArgs(argv: string[]): Args { ...@@ -31,6 +31,14 @@ function parseArgs(argv: string[]): Args {
async function bootstrap() { async function bootstrap() {
const args = parseArgs(process.argv.slice(2)); const args = parseArgs(process.argv.slice(2));
const logger = new Logger('recompute-plans'); const logger = new Logger('recompute-plans');
// 连接池对齐本进程的写并发(PlanEngine 的 upsert 段用 PAC_PLAN_BATCH_CONCURRENCY,默认 8)。
// 必须在 NestFactory 之前设 —— PrismaService 在容器初始化时就定池,晚了不生效。
// 不设的话池是 Prisma 默认「核数×2+1」(生产 4 核 = 9),把 batch 并发调到 9 以上就会被卡住。
// 详见 prisma.service.ts 的 withCohortDerivedPool。
if (!process.env.PAC_DB_CONCURRENCY) {
const n = Math.max(1, Number(process.env.PAC_PLAN_BATCH_CONCURRENCY) || 8);
if (n > 1) process.env.PAC_DB_CONCURRENCY = String(n);
}
const app = await NestFactory.createApplicationContext(AppModule, { const app = await NestFactory.createApplicationContext(AppModule, {
logger: ['log', 'warn', 'error'], logger: ['log', 'warn', 'error'],
}); });
......
...@@ -4,6 +4,7 @@ import { PrismaService } from '../../../prisma/prisma.service'; ...@@ -4,6 +4,7 @@ import { PrismaService } from '../../../prisma/prisma.service';
import { TreatmentInitiationRecallScenario } from './scenarios/treatment-initiation-recall.scenario'; import { TreatmentInitiationRecallScenario } from './scenarios/treatment-initiation-recall.scenario';
import type { PlanScenarioPlugin, ScenarioHit, ScenarioScope } from './scenario.interface'; import type { PlanScenarioPlugin, ScenarioHit, ScenarioScope } from './scenario.interface';
import { reasonNeedsRefresh } from './reason-refresh'; import { reasonNeedsRefresh } from './reason-refresh';
import { runPool } from '../../../common/run-pool';
/** /**
* PlanEngineService — 跑所有 scenario plugin,产 FollowupPlan + PlanReason * PlanEngineService — 跑所有 scenario plugin,产 FollowupPlan + PlanReason
...@@ -197,58 +198,59 @@ export class PlanEngineService { ...@@ -197,58 +198,59 @@ export class PlanEngineService {
await this.prefetchForBatch(scope, patientIds, now); await this.prefetchForBatch(scope, patientIds, now);
const EMPTY_SNOOZE = new Map<string, Date>(); const EMPTY_SNOOZE = new Map<string, Date>();
const logRows: Prisma.PlanGenerationLogCreateManyInput[] = []; const logRows: Prisma.PlanGenerationLogCreateManyInput[] = [];
// ⭐ 2026-07-26:写操作从「每 N 个一批 await Promise.all」的**栅栏**改成连续调度的
// worker pool(见 common/run-pool.ts)。栅栏每批都要等本批最慢那个,快的 worker 干等。
// 同一处改法在 recompute-persona 上实测过(测试服 35.7 万患者,concurrency=8):
// 并发效率 44% → 94%,吞吐 1,454 → 2,694 人/分。
// plan 侧单患者耗时的离散度只会更大(upsertPlan 里 reason 数量差异明显),栅栏的浪费同理。
const concurrency = Math.max(1, Number(process.env.PAC_PLAN_BATCH_CONCURRENCY) || 8); const concurrency = Math.max(1, Number(process.env.PAC_PLAN_BATCH_CONCURRENCY) || 8);
const entries = [...hitsByPatient.entries()]; const entries = [...hitsByPatient.entries()];
for (let i = 0; i < entries.length; i += concurrency) { await runPool(entries, concurrency, async ([patientId, hits]) => {
await Promise.all( try {
entries.slice(i, i + concurrency).map(async ([patientId, hits]) => { const result = await this.upsertPlan({
try { scope,
const result = await this.upsertPlan({ patientId,
scope, hits,
patientId, prefetched: {
hits, latest: latestByPatient.get(patientId) ?? null,
prefetched: { snoozedAnchors: snoozedByPatient.get(patientId) ?? EMPTY_SNOOZE,
latest: latestByPatient.get(patientId) ?? null, personaId: personaByPatient.get(patientId) ?? null,
snoozedAnchors: snoozedByPatient.get(patientId) ?? EMPTY_SNOOZE, lastVisitClinicId: lastVisitClinicByPatient.get(patientId) ?? null,
personaId: personaByPatient.get(patientId) ?? null, },
lastVisitClinicId: lastVisitClinicByPatient.get(patientId) ?? null, });
}, // 计数:JS 单线程,await 恢复后同步自增,chunk 内并发无交错
}); if (result === 'created') plansCreated++;
// 计数:JS 单线程,await 恢复后同步自增,chunk 内并发无交错 else if (result === 'superseded') plansSuperseded++;
if (result === 'created') plansCreated++; else if (result === 'unchanged') plansUnchanged++;
else if (result === 'superseded') plansSuperseded++; else if (result === 'suppressed') plansSuppressed++;
else if (result === 'unchanged') plansUnchanged++; logRows.push({
else if (result === 'suppressed') plansSuppressed++; hostId: scope.hostId,
logRows.push({ tenantId: scope.tenantId,
hostId: scope.hostId, patientId,
tenantId: scope.tenantId, triggeredBy: 'cli:batch',
patientId, status: 'success',
triggeredBy: 'cli:batch', scenarioRunCount: this.scenarios.length,
status: 'success', plansCreated: result === 'created' || result === 'superseded' ? 1 : 0,
scenarioRunCount: this.scenarios.length, startedAt,
plansCreated: result === 'created' || result === 'superseded' ? 1 : 0, endedAt: new Date(),
startedAt, });
endedAt: new Date(), } catch (err) {
}); logRows.push({
} catch (err) { hostId: scope.hostId,
logRows.push({ tenantId: scope.tenantId,
hostId: scope.hostId, patientId,
tenantId: scope.tenantId, triggeredBy: 'cli:batch',
patientId, status: 'failed',
triggeredBy: 'cli:batch', scenarioRunCount: this.scenarios.length,
status: 'failed', errorMessage: err instanceof Error ? err.message : String(err),
scenarioRunCount: this.scenarios.length, startedAt,
errorMessage: err instanceof Error ? err.message : String(err), endedAt: new Date(),
startedAt, });
endedAt: new Date(), this.logger.error(
}); `plan upsert failed patient=${patientId}: ${err instanceof Error ? err.message : err}`,
this.logger.error( );
`plan upsert failed patient=${patientId}: ${err instanceof Error ? err.message : err}`, }
); });
}
}),
);
}
for (let i = 0; i < logRows.length; i += 5000) { for (let i = 0; i < logRows.length; i += 5000) {
await this.prisma.planGenerationLog.createMany({ data: logRows.slice(i, i + 5000) }); await this.prisma.planGenerationLog.createMany({ data: logRows.slice(i, i + 5000) });
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment