Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
7ea30d59
Commit
7ea30d59
authored
Sep 05, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
merge: main → test(反向合)—— plan 端到端分批
parents
215f3196
76b0060f
Pipeline
#3673
failed in 0 seconds
Changes
2
Pipelines
1
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
107 additions
and
1 deletions
+107
-1
apps/pac-service/src/modules/plan/engine/plan-engine.service.ts
+0
-0
apps/pac-service/tests/plan-engine-batch.spec.ts
+107
-1
No files found.
apps/pac-service/src/modules/plan/engine/plan-engine.service.ts
View file @
7ea30d59
This diff is collapsed.
Click to expand it.
apps/pac-service/tests/plan-engine-batch.spec.ts
View file @
7ea30d59
import
{
PlanEngineService
}
from
'../src/modules/plan/engine/plan-engine.service'
;
import
{
PlanEngineService
,
resolvePlanBatchSize
}
from
'../src/modules/plan/engine/plan-engine.service'
;
import
type
{
ScenarioHit
}
from
'../src/modules/plan/engine/scenario.interface'
;
import
type
{
ScenarioHit
}
from
'../src/modules/plan/engine/scenario.interface'
;
/**
/**
...
@@ -1156,3 +1156,109 @@ describe('归因继承的边界 — 判据是「客服碰过没」', () => {
...
@@ -1156,3 +1156,109 @@ describe('归因继承的边界 — 判据是「客服碰过没」', () => {
for
(
const
[
args
]
of
called
)
expect
(
args
.
where
.
planId
.
in
).
not
.
toContain
(
'p-g'
);
for
(
const
[
args
]
of
called
)
expect
(
args
.
where
.
planId
.
in
).
not
.
toContain
(
'p-g'
);
});
});
});
});
/**
* 🔴 2026-09-04 端到端分批改造的**核心判据**:结果与批大小无关。
*
* 事故背景:生产 pac-service 涨到 6.03GB 常驻被内核 OOM killer 打掉,整机 swap 抖死 2.5 小时。
* 堆账里 latestByPatient(1.93GB)+ hitsByPatient(1.06GB)是大头,而它们原先**全程不释放**
* —— 取数早就是 2000 一块,但产物累积在跨全量存活的 Map 里,runPool 的闭包又把它们
* context-allocate 到整个 runAllForHost frame 结束。改造把取数分块升格成端到端分批。
*
* 为什么这组用例是必须的:上面那 20+ 条既有用例**一条都跑不到分批路径的差异** ——
* 它们每个只有 1~4 个患者,默认批大小 2000,永远只有一批。分批写错了它们全绿。
* 这里用 describe.each 把同一份 fixture 在不同批大小下各跑一遍,直接编码
* 「结果与批大小无关」这条不变式。
*
* ⚠️ batchSize=1 是最凶的一档:每批一个患者,同时压测三件事 ——
* ① 跨批不误关(stale-close 拿的是跨批累加的 hitPatientIds,不是被逐批清空的 hitsByPatient)
* ② touchedPlanIds 按批算与全量算等价
* ③ snooze 提在循环外之后仍能被每一批查到
*/
describe
(
'端到端分批 — 结果必须与 PAC_PLAN_BATCH_SIZE 无关'
,
()
=>
{
const
prev
=
process
.
env
.
PAC_PLAN_BATCH_SIZE
;
afterEach
(()
=>
{
if
(
prev
===
undefined
)
delete
process
.
env
.
PAC_PLAN_BATCH_SIZE
;
else
process
.
env
.
PAC_PLAN_BATCH_SIZE
=
prev
;
});
/// 与上面「混合一批」同一份 fixture:5 种结局各一,外加一个 0 命中的患者压 stale-close
const
runMixed
=
async
()
=>
{
const
{
prisma
,
plans
,
logs
}
=
makeStore
({
plans
:
[
{
id
:
'u-old'
,
patientId
:
'p2'
,
status
:
'active'
,
priorityScore
:
50
,
reasons
:
[{
scenario
:
SCEN
,
subKey
:
'k@1'
}]
},
{
id
:
's-old'
,
patientId
:
'p3'
,
status
:
'active'
,
reasons
:
[{
scenario
:
SCEN
,
subKey
:
'k@old'
}]
},
{
id
:
't-term'
,
patientId
:
'p4'
,
status
:
'abandoned'
,
snoozedUntil
:
new
Date
(
'2026-12-01T00:00:00Z'
),
reasons
:
[{
scenario
:
SCEN
,
subKey
:
'k@4'
}],
},
// ⭐ p9 本轮 0 命中且有 active plan → 必须被 stale-close 关掉。
// 分批写错(第 3 步进了循环 / 用了被清空的 hitsByPatient)时,
// 这里会连 p1~p4 一起关掉,plansClosed 从 1 变 5 —— 这条就是照妖镜。
{
id
:
'stale-old'
,
patientId
:
'p9'
,
status
:
'active'
,
reasons
:
[{
scenario
:
SCEN
,
subKey
:
'k@gone'
}]
},
],
});
const
res
=
await
engine
(
prisma
,
makeScenario
([
hit
(
'p1'
,
'k@1'
),
hit
(
'p2'
,
'k@1'
,
50
),
hit
(
'p3'
,
'k@new'
),
hit
(
'p4'
,
'k@4'
),
]),
).
runAllForHost
({
hostId
:
HOST
,
tenantId
:
TENANT
,
now
:
NOW
});
return
{
res
,
plans
,
logs
};
};
/// 把最终状态压成一个可比对的快照(引擎返回值 + 落库结果),批大小不该改变它任何一位
const
snapshot
=
(
r
:
Awaited
<
ReturnType
<
typeof
runMixed
>>
)
=>
({
result
:
{
scenariosRun
:
r
.
res
.
scenariosRun
,
patientsHit
:
r
.
res
.
patientsHit
,
plansCreated
:
r
.
res
.
plansCreated
,
plansSuperseded
:
r
.
res
.
plansSuperseded
,
plansUnchanged
:
r
.
res
.
plansUnchanged
,
plansSuppressed
:
r
.
res
.
plansSuppressed
,
plansClosed
:
r
.
res
.
plansClosed
,
plansSkippedAssigned
:
r
.
res
.
plansSkippedAssigned
,
},
// plan 终态:按 (patientId, version) 排序,避免并发写入顺序带来的假差异
plans
:
r
.
plans
.
map
((
p
)
=>
`
${
p
.
patientId
}
|v
${
p
.
version
}
|
${
p
.
status
}
|
${
p
.
priorityScore
}
`
)
.
sort
(),
logs
:
r
.
logs
.
map
((
l
)
=>
`
${
l
.
patientId
}
|
${
l
.
status
}
`
).
sort
(),
});
const
SIZES
=
[
0
,
1
,
2
,
3
,
2000
];
let
baseline
:
ReturnType
<
typeof
snapshot
>
|
null
=
null
;
test
.
each
(
SIZES
)(
'批大小 %s → 结果与基线逐字段一致'
,
async
(
size
)
=>
{
process
.
env
.
PAC_PLAN_BATCH_SIZE
=
String
(
size
);
const
snap
=
snapshot
(
await
runMixed
());
// 先自证这份 fixture 真的走到了 5 种结局,否则"全相等"可能只是都没跑
expect
(
snap
.
result
).
toMatchObject
({
patientsHit
:
4
,
plansCreated
:
1
,
plansUnchanged
:
1
,
plansSuperseded
:
1
,
plansSuppressed
:
1
,
plansClosed
:
1
,
// ⭐ 只关 p9;若变成 5 说明分批把其他批的患者误判成"信号消失"
});
if
(
baseline
===
null
)
baseline
=
snap
;
else
expect
(
snap
).
toEqual
(
baseline
);
});
test
(
'⛔ 0 = single-shot 回退开关(等价于改造前的不分批行为)'
,
()
=>
{
expect
(
resolvePlanBatchSize
({
PAC_PLAN_BATCH_SIZE
:
'0'
}
as
NodeJS
.
ProcessEnv
)).
toBe
(
0
);
});
test
(
'⛔ 批大小夹在 [1, 20000] —— 上限守 PG 32767 bind(latest/persona 走 patientId in ids)'
,
()
=>
{
expect
(
resolvePlanBatchSize
({
PAC_PLAN_BATCH_SIZE
:
'999999'
}
as
NodeJS
.
ProcessEnv
)).
toBe
(
20
_000
);
expect
(
resolvePlanBatchSize
({
PAC_PLAN_BATCH_SIZE
:
'-5'
}
as
NodeJS
.
ProcessEnv
)).
toBe
(
1
);
expect
(
resolvePlanBatchSize
({}
as
NodeJS
.
ProcessEnv
)).
toBe
(
2000
);
expect
(
resolvePlanBatchSize
({
PAC_PLAN_BATCH_SIZE
:
'abc'
}
as
NodeJS
.
ProcessEnv
)).
toBe
(
2000
);
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment