feat(saa): U3 gamedef 量测路 + 有界并发池 harness(默认 K=1 字节零变)
003-U1 U3「量对路 + 有界并发(量测用)」:
- AigcExecutorProperties: saaConcurrency(默认 1)/saaPlayPortBase(4320)/saaCdpPortBase(9222) — 默认全为生产字节零变值
- SaaGraphDispatcher: newSingleThreadExecutor→newFixedThreadPool(K) + Semaphore(1)→Semaphore(K) + K 个错开端口槽池(第 i 槽 playPort=base+2i/cdpPort=base+i);runGraphAndCallback 起手 take 槽 + acquire 许可、整 job 持槽、finally 必归还防泄漏;端口覆盖放 runGraphAndCallback(保 buildInputs 签名不变,反射测兼容);构造期 null-properties 兜底(K=1+默认端口,修 TraceTest NPE);allocPortSlots/normalizeConcurrency 抽为可单测纯函数 + 共享面审计结论入注释
- SaaFullGraphE2eTest: 加 -Dsaa.e2e.sourceMode(默认 factory,量 gamedef 须显式设=修发现②量错路) + -Dsaa.e2e.concurrency(默认 1)
- 新增 saa-e2e-briefs-gamedef.txt(34 条多品类≥30) + saa-e2e-briefs-holdout.txt(12 条留出集,与调优集及内置 ALL_BRIEFS 零重叠)
量对路(发现②修复):旧 e2e 只 setDispatcher("saa") 未 setSaaSourceMode → props 默认 factory(iife),量的是旧路;现 setSaaSourceMode(sourceMode),量 gamedef 真基线须 -Dsaa.e2e.sourceMode=gamedef。
K=1 字节零变:newFixedThreadPool(1)≡单线程串行 + Semaphore(1) + 仅槽 0=[4320,9222],与改造前逐字一致。共享面审计(只读 serve-and-play.sh,脚本未改):K>1 下端口为唯一真冲突点,净场 lsof|kill 只杀本槽端口、static-serve 只读共享根、evidence 按 gameId 隔离 → 跨 job 安全。
并发口径(plan 2026-06-19 创始人指令对齐):K 是量测用「真玩并发」上限,受 Mac 实时九门帧预算硬限(C_frame/E_live,~3-4),非 M3 API 限;生成端(网络限)的宽并发(gen≫play 时)留 U5 量 gen:play 占比后按需解耦,不在本单元。
验证:编译绿;GateLogic+TraceTest+NodesRegression+PickAnswer 指定集独立复跑 exit 0、零失败标记;agent -am 全 SAA 包 101/0/0/3skip。真跑 K 路校准(verdict 对等+结构隔离)延 U5 执行期。
默认 K=1/sourceMode=factory 现行字节零变;不碰 gd-runtime/build-from-source(Plan B 域)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
08aa0c13d0
commit
12abfbac4a
@ -28,8 +28,10 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@ -51,9 +53,13 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
* (生成是数十秒级多步活,不在 tick 线程内长挂);任务留 RUNNING 等回调命中、超 deadline 由 watchdog 收尸。
|
||||
* 仅「投递前置失败」(图未就绪/线程池拒绝/game-runtime 不存在)返回 false,调用方按 failed(llm_error) 落终态。
|
||||
*
|
||||
* <p><b>串行真玩门(Semaphore(1))</b>:play 节点用固定端口(4320/9222)+ headless Chrome,多 job 并行真玩必撞端口;
|
||||
* 故图执行经一把全局 {@link Semaphore}(1) 串行化(acquire→跑图→release)。与 Python worker {@code service.py:80}
|
||||
* 串行锁同语义(serve 4320/CDP 9222 不可并发)。后台线程池亦设单线程,双保险。
|
||||
* <p><b>有界并发真玩门(003-U1:信号量(K) + 端口槽池)</b>:play 节点用真玩端口 + headless Chrome,多 job
|
||||
* <b>共用同一对</b>端口必撞;故每 job 整生命期独占一对错开端口(第 i 槽 serve={@code base+2i}/CDP={@code base+i}),
|
||||
* 并发上限 K={@link AigcExecutorProperties#getSaaConcurrency()}(线程池 size=K + {@link Semaphore}(K) + K 个端口槽三者同界)。
|
||||
* <b>默认 K=1 = 生产字节零变</b>:单线程池 + Semaphore(1) + 仅槽 0=[4320,9222],与改造前 {@code newSingleThreadExecutor +
|
||||
* Semaphore(1) + buildInputs 常量 4320/9222} 逐字节等价(与 Python worker {@code service.py:80} 串行锁同语义)。
|
||||
* K>1 时各 job 端口对两两不撞(serve-and-play.sh 净场只杀本槽两端口、static-serve 只读共享根、evidence 按 gameId
|
||||
* 隔离 → 跨 job 安全;共享面审计见 {@link #allocPortSlots} 注释)。
|
||||
*
|
||||
* <p><b>事务边界</b>:图执行<b>不包大 {@code @Transactional}</b>(数十秒级跑图占长事务=连接池杀手);落库由
|
||||
* {@code handleCallback → DifyCallbackTxService} 内部独立短事务完成(既有三表同事务写链不变)。
|
||||
@ -95,22 +101,33 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
private final SourceProjectApi sourceProjectApi;
|
||||
|
||||
/**
|
||||
* 后台单线程池(跑图阻塞活,禁占 tick/Tomcat 线程)。daemon 线程,命名便于排障。
|
||||
* 单线程 = 串行跑图(与 Semaphore 双保险——单线程已天然串行,Semaphore 兜「未来若改多线程」语义不破)。
|
||||
* 后台跑图线程池(跑图阻塞活,禁占 tick/Tomcat 线程)。daemon 线程,命名便于排障。
|
||||
* <b>有界 size=K</b>({@link #concurrency},003-U1):K=1 时是单线程池,与改造前 {@code newSingleThreadExecutor}
|
||||
* 逐字节等价(天然串行);K>1 时 K 路并行跑图,并发上限由本池 + {@link #playLock}(K) + {@link #portSlots} 三者共同界定。
|
||||
* 构造期据 {@code properties.saaConcurrency} 定 size(见构造函数),故非 inline 初始化。
|
||||
*/
|
||||
private final ExecutorService graphExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
||||
private final AtomicInteger seq = new AtomicInteger();
|
||||
private final ExecutorService graphExecutor;
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r, "saa-graph-dispatcher-" + seq.incrementAndGet());
|
||||
t.setDaemon(true); // 守护线程:进程退出不被它阻塞(在飞 job 由 watchdog 兜超时)
|
||||
return t;
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 真玩门信号量(play 用真玩端口,多 job 并行真玩须各占独立端口对,故并发受限)。
|
||||
* <b>容量=K</b>({@link #concurrency},003-U1):K=1 时等价改造前 {@code Semaphore(1)}(严格串行);
|
||||
* K>1 时放行 K 路并发,与端口槽 {@link #portSlots}(恰 K 个错开端口对)一一配对——每路 job 整生命期持一槽,
|
||||
* 杜绝撞端口。线程池已隐含同样上限,信号量为显式语义双保险(与改造前「单线程 + Semaphore 双保险」同范式)。
|
||||
*/
|
||||
private final Semaphore playLock;
|
||||
|
||||
/** 真玩门串行锁(play 用固定端口 4320/9222,多 job 并行真玩必撞,故全局串行)。 */
|
||||
private final Semaphore playLock = new Semaphore(1);
|
||||
/**
|
||||
* 端口槽池(003-U1,有界并发的核心):{@code BlockingQueue<int[2]>},恰 K 个错开端口对 {@code [playPort, cdpPort]}
|
||||
* (第 i 槽 playPort={@code saaPlayPortBase+2i} / cdpPort={@code saaCdpPortBase+i},{@link #allocPortSlots} 算)。
|
||||
* <p><b>整 job 生命期持槽语义</b>:{@code runGraphAndCallback} 起手 {@code take()} 拿一空闲槽(无则阻塞,与信号量同节流),
|
||||
* 用该槽端口覆盖 {@code inputs} 的 port/cdpPort 后跑图,<b>finally 随 release 把同一槽 {@code put} 回池</b>——
|
||||
* 即便跑图超时/异常也归还(防端口泄漏致后续 job 无槽永挂)。<b>K=1 时只有槽 0 = {@code [4320,9222]}</b>,
|
||||
* 覆盖值与改造前 buildInputs 常量逐字一致 → 字节零变。
|
||||
*/
|
||||
private final BlockingQueue<int[]> portSlots;
|
||||
|
||||
/** 后台并发度 K({@code max(1, saaConcurrency)};线程池 size / 信号量容量 / 端口槽数三者同此值)。 */
|
||||
private final int concurrency;
|
||||
|
||||
/**
|
||||
* trace 抽取专用 mapper(W-G1 组B · 闭合 SAA split-brain):dispatcher 原无 ObjectMapper({@code SaaStudioNodes.MAPPER}
|
||||
@ -156,6 +173,32 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
this.dataSource = dataSource;
|
||||
this.observationRegistry = observationRegistry;
|
||||
this.sourceProjectApi = sourceProjectApi;
|
||||
|
||||
// ── 003-U1 有界并发池:据 saaConcurrency 定 K(默认 1=生产字节零变),三件套(线程池/信号量/端口槽)同此值 ──
|
||||
// null-properties 容错:纯 trace 抽取单测(SaaGraphDispatcherTraceTest)以 4-null 依赖构造本类(抽取方法不碰 properties)。
|
||||
// 改造前字段为 inline 初始化、不触 properties;故此处亦须对 properties==null 兜底(K=1 + 默认端口基址),保该单测路不破。
|
||||
Integer rawK = (properties == null) ? null : properties.getSaaConcurrency();
|
||||
Integer playBase = (properties == null) ? null : properties.getSaaPlayPortBase();
|
||||
Integer cdpBase = (properties == null) ? null : properties.getSaaCdpPortBase();
|
||||
this.concurrency = normalizeConcurrency(rawK);
|
||||
// 后台线程池:K=1 时 newFixedThreadPool(1) 与改造前 newSingleThreadExecutor 行为等价(单 worker 串行、无界队列);K>1 时 K 路并行。
|
||||
this.graphExecutor = Executors.newFixedThreadPool(concurrency, new ThreadFactory() {
|
||||
private final AtomicInteger seq = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r, "saa-graph-dispatcher-" + seq.incrementAndGet());
|
||||
t.setDaemon(true); // 守护线程:进程退出不被它阻塞(在飞 job 由 watchdog 兜超时)
|
||||
return t;
|
||||
}
|
||||
});
|
||||
// 真玩门信号量容量=K(K=1 等价改造前 Semaphore(1))。
|
||||
this.playLock = new Semaphore(concurrency);
|
||||
// 端口槽池:K 个错开端口对入队(槽 0 恒 = [saaPlayPortBase, saaCdpPortBase] = 默认 [4320,9222],K=1 时字节零变;allocPortSlots 对 null 基址兜底 4320/9222)。
|
||||
this.portSlots = new LinkedBlockingQueue<>();
|
||||
for (int[] slot : allocPortSlots(playBase, cdpBase, concurrency)) {
|
||||
this.portSlots.add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -241,11 +284,12 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ③ 提交后台单线程跑图(投递握手即返回;图执行+回调全在后台线程,tick 线程不长挂)。
|
||||
// ③ 提交后台跑图(投递握手即返回;图执行+回调全在后台线程,tick 线程不长挂)。
|
||||
// 后台池 size=K(K=1 等价改造前单线程);并发上限由池 + Semaphore(K) + 端口槽共同界定。
|
||||
try {
|
||||
graphExecutor.submit(() -> runGraphAndCallback(graph, inputs, job, traceId));
|
||||
} catch (Exception e) {
|
||||
// 线程池拒绝(理论不可达:单线程无界队列):投递失败留痕。
|
||||
// 线程池拒绝(理论不可达:固定池无界队列):投递失败留痕。
|
||||
log.error("[saa-dispatch] 图执行提交后台线程失败,投递失败 traceId={}", traceId, e);
|
||||
return false;
|
||||
}
|
||||
@ -257,17 +301,30 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
// ============================== 私有:图执行 + 进程内回调 ==============================
|
||||
|
||||
/**
|
||||
* 后台线程体:串行跑图(Semaphore 守真玩端口)→ result state 组 ReqVO → 进程内调 handleCallback。
|
||||
* 任何异常都兜成 failed(llm_error) 回调(绝不静默吞——否则任务卡 RUNNING 直到 watchdog 超时收尸,体验差)。
|
||||
* 后台线程体:取信号量许可 + 端口槽(K=1 严格串行/K>1 有界并发,各 job 独占端口对)→ 跑图 → result state 组 ReqVO
|
||||
* → 进程内调 handleCallback。任何异常都兜成 failed(llm_error) 回调(绝不静默吞——否则任务卡 RUNNING 直到 watchdog 超时收尸,体验差);
|
||||
* finally 必归还端口槽 + 释放许可(即便超时/异常,防端口泄漏致后续 job 无槽永挂)。
|
||||
*/
|
||||
private void runGraphAndCallback(CompiledGraph graph, Map<String, Object> inputs,
|
||||
Map<String, Object> job, String traceId) {
|
||||
boolean acquired = false;
|
||||
int[] slot = null; // 本 job 占用的端口槽 [playPort, cdpPort];finally 必归还(防泄漏)。
|
||||
try {
|
||||
// 串行真玩门:阻塞获取(单线程池下恒能拿到;双保险防未来多线程改动)。
|
||||
// 真玩门信号量:阻塞获取(K=1 下恒能拿到=严格串行;K>1 放行 K 路并发,与端口槽数一一对应)。
|
||||
playLock.acquire();
|
||||
acquired = true;
|
||||
|
||||
// ── 003-U1:取一空闲端口槽(无则阻塞,与信号量同节流),用该槽端口覆盖 inputs 的 port/cdpPort ──
|
||||
// per-job 端口隔离:每 job 整生命期独占一对端口(serve 4320+2i / CDP 9222+i),跑完 finally 归还。
|
||||
// K=1 时只有槽 0=[saaPlayPortBase, saaCdpPortBase]=默认[4320,9222],覆盖值与 buildInputs 常量逐字一致 → 字节零变。
|
||||
slot = portSlots.take();
|
||||
inputs.put("port", slot[0]);
|
||||
inputs.put("cdpPort", slot[1]);
|
||||
if (concurrency > 1) {
|
||||
// 仅 K>1 并发态打点(K=1 不打、保日志字节零变):便于排障「哪 job 用哪端口」。
|
||||
log.info("[saa-dispatch] 取端口槽 playPort={}, cdpPort={}(K={} 并发)traceId={}", slot[0], slot[1], concurrency, traceId);
|
||||
}
|
||||
|
||||
// 同 threadId(traceId) 跑图:checkpoint 开启时(图已挂 MysqlSaver+releaseThread(false)),崩溃后同
|
||||
// traceId 二次 invoke 即从最后已落 checkpoint 的节点续跑(step5 已接;watchdog 兜超时、checkpoint 兜续跑)。
|
||||
RunnableConfig runConfig = RunnableConfig.builder().threadId(traceId).build();
|
||||
@ -292,6 +349,11 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
log.error("[saa-dispatch] 图执行异常,兜 failed(llm_error) 回调 traceId={}", traceId, e);
|
||||
safeFailedCallback(job, traceId, "图执行异常:" + trunc(String.valueOf(e.getMessage()), 200));
|
||||
} finally {
|
||||
// 先归还端口槽(即便超时/异常也归还,防端口泄漏致后续 job 无槽永挂),再释放信号量——保「槽与permit成对」不变量。
|
||||
// put 回的是 take 出的同一 int[](同一槽),K=1 时即把槽 0 放回,下个 job 再取仍是 [4320,9222](字节零变)。
|
||||
if (slot != null) {
|
||||
portSlots.add(slot); // 有界队列容量足够(恰 K 个槽),add 不会阻塞/拒绝
|
||||
}
|
||||
if (acquired) {
|
||||
playLock.release();
|
||||
}
|
||||
@ -912,4 +974,60 @@ public class SaaGraphDispatcher implements GenerationDispatcher {
|
||||
}
|
||||
return s.length() > n ? s.substring(0, n) : s;
|
||||
}
|
||||
|
||||
// ============================== 003-U1 有界并发:端口分配纯逻辑(模型无关,可单测) ==============================
|
||||
|
||||
/**
|
||||
* 规整并发度 K(纯函数):{@code null} 或 {@code <1} → 1(退回单飞,误配不崩、不破生产字节零变);否则原值。
|
||||
*
|
||||
* @param raw {@link AigcExecutorProperties#getSaaConcurrency()}(可能 null/非法)
|
||||
* @return K(恒 {@code >=1})
|
||||
*/
|
||||
static int normalizeConcurrency(Integer raw) {
|
||||
if (raw == null || raw < 1) {
|
||||
return 1;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 端口槽分配(纯函数,003-U1 有界并发核心):为 K 路并发各算一对<b>错开</b>端口 {@code [playPort, cdpPort]}。
|
||||
* <p>第 i 槽({@code 0..k-1}):{@code playPort = httpBase + 2*i}(步长 2 留头寸)、{@code cdpPort = cdpBase + i}(步长 1)。
|
||||
* <b>槽 0 = {@code [httpBase, cdpBase]}</b>(默认 {@code [4320, 9222]})→ K=1 时与改造前 buildInputs 常量逐字一致,字节零变。
|
||||
*
|
||||
* <p><b>不撞端口的依据</b>({@code SaaFullGraphE2eGateLogicTest} 单测断言):playPort 族 {@code {httpBase, httpBase+2, ...}}
|
||||
* 与 cdpPort 族 {@code {cdpBase, cdpBase+1, ...}} 默认区间天然不交叠(4320 区 vs 9222 区,相距数千);族内步长保互异 →
|
||||
* K 槽 2K 个端口两两不同(默认值下成立;若误配 base 致两族交叠属部署配置错,超本单元,由 U5 真跑校准兜)。
|
||||
*
|
||||
* <p><b>K>1 共享面安全审计(只读核 {@code _shared/serve-and-play.sh} + {@code static-serve.cjs} + {@code play.cdp.cjs},
|
||||
* 脚本禁改铁律下仅审计)</b>——结论:除「同一对端口」外,K 路并发的其余共享资源均隔离或读共享安全:
|
||||
* <ol>
|
||||
* <li><b>端口(唯一真冲突点)</b>:每 job 持独立端口对、整生命期独占({@link #portSlots}),serve-and-play.sh 净场
|
||||
* {@code lsof -ti tcp:$PORT|kill} <b>只杀传入的本槽两端口</b>(非全局 kill),故跨 job 不误杀 → 端口隔离即足。</li>
|
||||
* <li><b>服务根 cwd(共享、只读)</b>:N 个 {@code static-serve.cjs} 各 {@code listen} 自己端口,serve 同一 game-runtime
|
||||
* 根但<b>纯只读</b>(仅 {@code fs.readFile}、拒 {@code ..} 穿越,无写)→ 并发只读同一根安全。</li>
|
||||
* <li><b>{@code _shared/} 脚本(共享、只读)</b>:serve-and-play.sh / play.cdp.cjs / static-serve.cjs 是共读脚本文件,
|
||||
* 运行期不自改 → 安全。</li>
|
||||
* <li><b>{@code /tmp/wg1-*-$PORT.log}(按端口键隔离)</b>:serve/chrome 日志名含 {@code $PORT},端口对既隔离则日志路径天然不撞 → 安全。</li>
|
||||
* <li><b>Chrome user-data-dir(按调用隔离)</b>:serve-and-play.sh 用 {@code mktemp -d -t wg1-chrome-XXXX} 每次唯一临时目录 → 安全。</li>
|
||||
* <li><b>evidence 产物(按 gameId 隔离)</b>:play.cdp.cjs 只写 {@code games/_wg1-gen/<gameId>/evidence/},gameId={@code saa-<traceId>} 每 job 唯一 → 写面不交叠。</li>
|
||||
* </ol>
|
||||
* <b>遗留(U5 真跑校准项)</b>:上述为静态审计结论;K 路<b>真并发</b>下「verdict 对等(K=1 vs K=N 同 brief 判定一致)」+「同款×N 结构隔离」
|
||||
* 须九门环境实跑验证,延 003-U5 执行期。
|
||||
*
|
||||
* @param httpBase playPort 基址({@link AigcExecutorProperties#getSaaPlayPortBase()},默认 4320;null 兜底 4320)
|
||||
* @param cdpBase cdpPort 基址({@link AigcExecutorProperties#getSaaCdpPortBase()},默认 9222;null 兜底 9222)
|
||||
* @param k 并发度(须 {@code >=1},由 {@link #normalizeConcurrency} 保证)
|
||||
* @return K 个端口对 {@code [playPort, cdpPort]}(按槽序,槽 0 在首)
|
||||
*/
|
||||
static java.util.List<int[]> allocPortSlots(Integer httpBase, Integer cdpBase, int k) {
|
||||
int hb = (httpBase == null) ? 4320 : httpBase;
|
||||
int cb = (cdpBase == null) ? 9222 : cdpBase;
|
||||
int n = Math.max(1, k);
|
||||
java.util.List<int[]> slots = new java.util.ArrayList<>(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
slots.add(new int[]{hb + 2 * i, cb + i});
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +197,31 @@ public class AigcExecutorProperties {
|
||||
*/
|
||||
private String saaSourceMode = "factory";
|
||||
|
||||
/**
|
||||
* SAA 图后台执行并发度 K(plan 2026-06-19 003-U1 有界并发池,P0-3/P1-5):进程内后台跑图的<b>并行 job 上限</b>。
|
||||
* <b>默认 1 = 生产字节零变</b>(单线程池 + Semaphore(1) + 端口槽 0 → 端口恒为 saaPlayPortBase/saaCdpPortBase,
|
||||
* 与改造前 {@code newSingleThreadExecutor + Semaphore(1) + 常量 4320/9222} 行为逐字节等价,回归红线)。
|
||||
* <p>K>1 时:固定线程池 size=K + Semaphore(K) + K 个错开端口槽(每槽 {@code playPort=base+2i / cdpPort=base+i}),
|
||||
* 多 job 并行真玩各占独立端口对(serve-and-play.sh 净场只杀本槽两端口、static-serve 只读共享 game-runtime 根、
|
||||
* evidence 按 gameId 隔离 → 跨 job 安全,详见 {@code SaaGraphDispatcher} 共享面审计注释)。
|
||||
* <p><b>仅 dispatcher=saa 生效</b>;<1 的误配按 1 兜底(不崩、退回单飞)。真跑 K 路并发校准(verdict 对等 + 结构隔离)
|
||||
* 需九门环境,延 003-U5 执行期;本字段先就位 + 纯端口分配逻辑可单测。
|
||||
*/
|
||||
private Integer saaConcurrency = 1;
|
||||
|
||||
/**
|
||||
* SAA 真玩 HTTP 端口基址(端口池槽 0 的 playPort;plan 003-U1):第 i 槽 playPort = {@code base + 2*i}(步长 2 留头寸)。
|
||||
* 默认 4320(与改造前 buildInputs 常量、serve-and-play.sh 默认逐字一致 → K=1 字节零变)。内网地址按项目规则可入库。
|
||||
*/
|
||||
private Integer saaPlayPortBase = 4320;
|
||||
|
||||
/**
|
||||
* SAA 真玩 CDP 端口基址(端口池槽 0 的 cdpPort;plan 003-U1):第 i 槽 cdpPort = {@code base + i}(步长 1)。
|
||||
* 默认 9222(与改造前 buildInputs 常量、serve-and-play.sh 默认逐字一致 → K=1 字节零变)。
|
||||
* <p>两族端口区间天然不交叠(4320 区 vs 9222 区),族内步长保互异 → K 槽端口对两两不撞({@code SaaGraphDispatcher.allocPortSlots} 单测守)。
|
||||
*/
|
||||
private Integer saaCdpPortBase = 9222;
|
||||
|
||||
// ===== SAA 模型协议(Plan A · U1「用对 M3」;flag 旁挂,默认 openai 字节零变)=====
|
||||
|
||||
/**
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
package com.wanxiang.huijing.game.module.aigc.saa;
|
||||
|
||||
import com.wanxiang.huijing.game.module.aigc.service.executor.AigcExecutorProperties;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
@ -168,4 +171,79 @@ class SaaFullGraphE2eGateLogicTest {
|
||||
assertEquals("stage1", SaaFullGraphE2eTest.extractModelTier(new HashMap<>()), "缺 modelTier → stage1");
|
||||
assertEquals("stage1", SaaFullGraphE2eTest.extractModelTier(null), "trace=null → stage1");
|
||||
}
|
||||
|
||||
// ═══════════════════════ 003-U1 有界并发池:端口分配 + 并发规整(纯逻辑,模型无关) ═══════════════════════
|
||||
|
||||
// ─────────────────────────── saaConcurrency 默认值(回归红线:默认 1 = 生产字节零变前提) ───────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("saaConcurrency 默认 = 1(K=1 字节零变回归红线:默认即单飞,不改生产行为)")
|
||||
void saaConcurrency_defaultsToOne() {
|
||||
AigcExecutorProperties props = new AigcExecutorProperties();
|
||||
assertEquals(1, props.getSaaConcurrency(), "saaConcurrency 默认必须是 1(默认并发=1 才保 K=1 字节零变)");
|
||||
// 端口基址默认亦须与改造前 buildInputs 常量一致(4320/9222),否则槽 0 端口漂移破字节零变。
|
||||
assertEquals(4320, props.getSaaPlayPortBase(), "saaPlayPortBase 默认必须 4320(= 改造前 play 端口常量)");
|
||||
assertEquals(9222, props.getSaaCdpPortBase(), "saaCdpPortBase 默认必须 9222(= 改造前 cdp 端口常量)");
|
||||
}
|
||||
|
||||
// ─────────────────────────── normalizeConcurrency:K 规整(null/<1 → 1) ───────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("normalizeConcurrency:null/0/负 → 1(误配退回单飞,不崩、不破字节零变);正值原样")
|
||||
void normalizeConcurrency_cases() {
|
||||
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(null), "null → 1");
|
||||
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(0), "0 → 1");
|
||||
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(-3), "负 → 1");
|
||||
assertEquals(1, SaaGraphDispatcher.normalizeConcurrency(1), "1 → 1");
|
||||
assertEquals(4, SaaGraphDispatcher.normalizeConcurrency(4), "4 → 4(正值原样)");
|
||||
}
|
||||
|
||||
// ─────────────────────────── allocPortSlots:端口槽分配(槽 0 字节零变 + 步长 + 不撞 + null 兜底) ───────────────────────────
|
||||
|
||||
@Test
|
||||
@DisplayName("allocPortSlots:K=1 槽 0 = [4320,9222](字节零变铁证:与改造前 buildInputs 常量逐字一致)")
|
||||
void allocPortSlots_k1_byteIdentical() {
|
||||
List<int[]> slots = SaaGraphDispatcher.allocPortSlots(4320, 9222, 1);
|
||||
assertEquals(1, slots.size(), "K=1 只有一个槽");
|
||||
assertEquals(4320, slots.get(0)[0], "K=1 playPort 必须恒为 4320(字节零变)");
|
||||
assertEquals(9222, slots.get(0)[1], "K=1 cdpPort 必须恒为 9222(字节零变)");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("allocPortSlots:第 i 槽 playPort=base+2i / cdpPort=base+i(步长 2 / 1)")
|
||||
void allocPortSlots_strideFormula() {
|
||||
List<int[]> slots = SaaGraphDispatcher.allocPortSlots(4320, 9222, 4);
|
||||
assertEquals(4, slots.size());
|
||||
for (int i = 0; i < 4; i++) {
|
||||
assertEquals(4320 + 2 * i, slots.get(i)[0], "槽 " + i + " playPort = 4320+2*" + i);
|
||||
assertEquals(9222 + i, slots.get(i)[1], "槽 " + i + " cdpPort = 9222+" + i);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("allocPortSlots:K 槽 2K 个端口两两不撞(play 族 vs cdp 族不交叠 + 族内互异)")
|
||||
void allocPortSlots_noCollision() {
|
||||
for (int k : new int[]{1, 2, 4, 8, 16}) {
|
||||
List<int[]> slots = SaaGraphDispatcher.allocPortSlots(4320, 9222, k);
|
||||
Set<Integer> seen = new HashSet<>();
|
||||
for (int[] slot : slots) {
|
||||
assertTrue(seen.add(slot[0]), "K=" + k + " playPort " + slot[0] + " 与已分配端口相撞");
|
||||
assertTrue(seen.add(slot[1]), "K=" + k + " cdpPort " + slot[1] + " 与已分配端口相撞");
|
||||
}
|
||||
assertEquals(2 * k, seen.size(), "K=" + k + " 应有 2K=" + (2 * k) + " 个互异端口");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("allocPortSlots:null 基址兜底 4320/9222;k<1 兜底 1 槽(防御误配,不抛)")
|
||||
void allocPortSlots_nullBaseAndClamp() {
|
||||
List<int[]> nullBase = SaaGraphDispatcher.allocPortSlots(null, null, 2);
|
||||
assertEquals(4320, nullBase.get(0)[0], "httpBase=null → 兜底 4320");
|
||||
assertEquals(9222, nullBase.get(0)[1], "cdpBase=null → 兜底 9222");
|
||||
assertEquals(4322, nullBase.get(1)[0], "槽 1 仍按步长");
|
||||
|
||||
List<int[]> clamp = SaaGraphDispatcher.allocPortSlots(4320, 9222, 0);
|
||||
assertEquals(1, clamp.size(), "k<1 → 至少 1 槽(防御,不返空)");
|
||||
assertEquals(4320, clamp.get(0)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,17 +57,30 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
* <b>注</b>:SAA 路只有 token 计量,折¥是 follow-up F3({@code SaaGraphDispatcher} cost 整段省略,§6.3)→ 此处<b>不造假¥</b>。</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>003-U1 量测 flags</b>:
|
||||
* <ul>
|
||||
* <li>{@code -Dsaa.e2e.sourceMode}(默认 {@code factory}):量哪条生成路——{@code factory}=iife 工厂旧路(现行字节零变基线);
|
||||
* {@code gamedef}=真结构化 gameDefinition 路(cutover ≥60% 量测对象)。<b>修发现②</b>:旧版未 setSaaSourceMode→量的是 iife 旧路非 gamedef,
|
||||
* 故量 gamedef 真基线<b>必须</b> {@code -Dsaa.e2e.sourceMode=gamedef};</li>
|
||||
* <li>{@code -Dsaa.e2e.concurrency}(默认 1):后台并发度 K——1=串行(生产字节零变基线);>1 多 job 并行真玩(各占错开端口对,
|
||||
* K=1 vs K=N verdict 对等校准延 003-U5 真跑期);</li>
|
||||
* <li>{@code -Dsaa.e2e.briefFile}(可选):外部 brief 文件(行式,去空白/跳空行与 {@code #} 注释);
|
||||
* A/B 量测用 {@code src/test/resources/saa-e2e-briefs-gamedef.txt}(≥30 条多品类),出集留 {@code saa-e2e-briefs-holdout.txt}(供 final R1)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>运行(mini-desktop,game-runtime 有 node+esbuild+Chrome、端口 4320/9222 空)</b>:
|
||||
* <pre>
|
||||
* // 基线量测(仅打印成功率 + 存在性断言;003-U1 第一步「实测真基线」):
|
||||
* // gamedef 路基线量测(003-U1 第一步「实测真基线」;务必带 sourceMode=gamedef 才量对路):
|
||||
* NEWAPI_KEY=sk-... timeout 6000 mvn -pl ...aigc-server test \
|
||||
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 -Dsaa.e2e.n=10 \
|
||||
* -Dtest=SaaFullGraphE2eTest -Dsaa.e2e=1 -Dsaa.e2e.sourceMode=gamedef -Dsaa.e2e.n=10 \
|
||||
* -Dsaa.e2e.briefFile=src/test/resources/saa-e2e-briefs-gamedef.txt \
|
||||
* -DGAME_RUNTIME_DIR=/root/game-staging/repo/game-runtime \
|
||||
* -Dsurefire.failIfNoSpecifiedTests=false
|
||||
* // flip 前置硬门(成功率须≥80%,否则测试 fail):追加 -Dsaa.e2e.minSuccessRate=0.8
|
||||
* // flip 前置硬门(gamedef 成功率须≥60% iife 基线,否则测试 fail):追加 -Dsaa.e2e.minSuccessRate=0.6
|
||||
* // 有界并发量测(K=4 并行真玩,端口对自动错开):追加 -Dsaa.e2e.concurrency=4
|
||||
* </pre>
|
||||
*
|
||||
* @author 造梦AI(U7 集成顶石;003-U1 验收门工具化)
|
||||
* @author 造梦AI(U7 集成顶石;003-U1 验收门工具化 + sourceMode/concurrency 量测 flags)
|
||||
*/
|
||||
class SaaFullGraphE2eTest {
|
||||
|
||||
@ -198,6 +211,13 @@ class SaaFullGraphE2eTest {
|
||||
int n = briefs.size();
|
||||
System.out.println("[briefs] 本次投递 n=" + n + " 条(源 " + sourceBriefs.size() + " 条)");
|
||||
|
||||
// 量测路别(003-U1 发现②修复):-Dsaa.e2e.sourceMode 决定量的是哪条生成路——
|
||||
// factory(默认)= iife 工厂旧路(现行字节零变基线);gamedef = 真结构化 gameDefinition 路(cutover ≥60% 量测对象)。
|
||||
// 不显式设 setSaaSourceMode 时 props 默认 "factory"(与现行一致),故缺省=量 iife 路;量 gamedef 基线须 -Dsaa.e2e.sourceMode=gamedef。
|
||||
String sourceMode = System.getProperty("saa.e2e.sourceMode", "factory").trim();
|
||||
// 后台并发度(003-U1):-Dsaa.e2e.concurrency 默认 1(串行=生产字节零变基线);>1 时多 job 并行真玩(各占错开端口对)。
|
||||
int concurrency = Integer.getInteger("saa.e2e.concurrency", 1);
|
||||
|
||||
// ── 1) 构造真 AigcExecutorProperties(@Data 即 setter 全有)──
|
||||
// checkpoint 关 + DataSource 传 null → 零 DB 依赖;dispatcher=saa 走进程内全图派发。
|
||||
AigcExecutorProperties props = new AigcExecutorProperties();
|
||||
@ -209,6 +229,9 @@ class SaaFullGraphE2eTest {
|
||||
props.setSaaMaxPlayerRounds(1); // player 顾问轮上限
|
||||
props.setSaaStage2ExtraRepairs(3); // 救场阶梯 stage2 额外修复轮(5+3 阶梯)
|
||||
props.setSaaCheckpointEnabled(false); // 关 checkpoint → 不依赖 DataSource,零 DB
|
||||
props.setSaaSourceMode(sourceMode); // 003-U1 发现②:量对路(gamedef 基线须显式 -Dsaa.e2e.sourceMode=gamedef)
|
||||
props.setSaaConcurrency(concurrency); // 003-U1:后台并发度(默认 1=串行基线;>1 多 job 并行真玩各占错开端口)
|
||||
System.out.println("[config] sourceMode=" + sourceMode + "(factory=iife旧路/gamedef=真结构化), concurrency=" + concurrency);
|
||||
|
||||
// ── 2) 结果容器 + stub 回调(共享 latch(N) + byTrace 精确归位)+ 派发器(5 参,sourceProjectApi=null=create 路)──
|
||||
long runStart = System.currentTimeMillis();
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
# saa-e2e-briefs-gamedef.txt —— 003-U1 gamedef 路 A/B 量测 brief 集(≥30 条多品类)
|
||||
# 用法:-Dsaa.e2e.sourceMode=gamedef -Dsaa.e2e.briefFile=src/test/resources/saa-e2e-briefs-gamedef.txt
|
||||
# 行式:去首尾空白、跳空行与本行式 # 注释(SaaFullGraphE2eTest.parseBriefLines)。
|
||||
# 选品偏向运行时/九门 harness 支持的品类(realtime/physics/tap-targets/clickable),避免单品类偶过/偶不过。
|
||||
# 留出集(不进本 A/B、供 final R1 量):saa-e2e-briefs-holdout.txt——两文件 brief 不重叠。
|
||||
#
|
||||
# ── 挡板/接球类(realtime + 碰撞)──
|
||||
打砖块:挡板接球把上方砖块全部消除即胜,掉球则负。
|
||||
弹球台:用底部挡板把弹球接住反弹,击碎顶部所有彩色方块过关,球落底则失败。
|
||||
双挡板对打:上下各一块挡板来回击球,让球越过对方底线即得分,先得五分者胜。
|
||||
# ── 蛇/贪吃类(网格 + tick 移动)──
|
||||
贪吃蛇:操控小蛇吃食物变长,撞到自己身体或墙壁则游戏结束。
|
||||
吃豆人:在迷宫里移动吃光所有豆子,碰到巡逻的幽灵则失败。
|
||||
# ── 接物/收集类(realtime + 左右移动)──
|
||||
接金币:左右移动篮子接住从天上掉落的金币,漏接太多就失败。
|
||||
接水果:晃动盘子接住掉落的水果,接到炸弹则扣命,命数耗尽结束。
|
||||
雨中接伞:左右移动小人撑伞接住落下的雨滴,三滴落空则游戏结束。
|
||||
# ── 跳跃/躲避类(physics + 点击跳)──
|
||||
躲障碍:角色不断前进,点击跳跃躲开迎面而来的障碍物,撞到就结束。
|
||||
小恐龙跑酷:按键让恐龙跳过仙人掌和飞鸟,撞到障碍即结束,跑得越远分越高。
|
||||
火柴人跳坑:连续点击控制跳跃跨过地面深坑,掉进坑里则失败。
|
||||
直升机穿洞:长按上升松开下降,穿过上下起伏的山洞间隙,撞壁即结束。
|
||||
# ── 消除/三连类(clickable + 网格)──
|
||||
点击消除:点击三个或以上相连的同色方块把它们消掉,消满目标分数过关。
|
||||
方块连连看:点击两个图案相同且可直连的方块成对消除,全部清空即胜。
|
||||
下落消除:交换相邻宝石凑成三连消除得分,步数用尽时达标过关。
|
||||
# ── 射击/弹幕类(realtime + 发射)──
|
||||
太空射击:飞船左右移动并发射子弹击落持续下降的敌机,被敌机撞到则失败。
|
||||
打飞碟:移动炮台瞄准并射击横向飞过的飞碟,限时内击落数量达标过关。
|
||||
保卫基地:旋转底部炮塔射击四面来袭的入侵者,基地被攻破则失败。
|
||||
# ── 点击反应/打地鼠类(tap-targets + 限时)──
|
||||
打地鼠:地鼠随机从洞口冒头,点击敲中得分,限时结束时分数越高越好。
|
||||
戳泡泡:屏幕不断冒出彩色泡泡,快速点击戳破得分,漏掉太多则结束。
|
||||
快速点靶:随机位置闪现靶心,限时内尽量多地点中,结束时统计命中数。
|
||||
点亮方块:方块随机亮起,趁熄灭前点中得分,连续三次没点中则结束。
|
||||
# ── 节奏/判定类(realtime + 时机)──
|
||||
节奏敲击:方块下落到底部判定线的瞬间点击得分,连续错过三次则结束。
|
||||
钢琴块:只点黑色方块别碰白块,节奏越来越快,点错或漏点即结束。
|
||||
# ── 平台/抵达类(physics + 移动跳跃)──
|
||||
平台跳跃:左右移动并跳跃,踩着悬空平台抵达终点旗帜,掉出屏幕底部则失败。
|
||||
跳台阶登顶:不断向上跳到下一块平台,踩空掉落则结束,登得越高分越高。
|
||||
# ── 飞行/穿缝类(physics + 点击上升)──
|
||||
像素鸟穿管:点击让小鸟扇翅上升,穿过一根根上下水管的缝隙,撞管或落地即结束。
|
||||
气球升空:轻点给气球补气上升,避开横向飘来的尖刺,被扎破则游戏结束。
|
||||
# ── 躲陨石/生存类(realtime + 八向移动)──
|
||||
太空躲陨石:上下左右移动飞船躲避飞来的陨石,坚持到计时归零即通关。
|
||||
雪崩求生:左右移动滑雪者躲开滚落的石块与树木,被砸中即结束,撑得越久越好。
|
||||
# ── 弹力/打靶类(physics + 点击施力)──
|
||||
弹力打靶:点击屏幕给小球施加方向,把场上所有靶子撞掉即过关。
|
||||
愤怒投石:拖拽瞄准弹射石块,砸倒所有目标木箱即通关,弹药用尽未清场则失败。
|
||||
# ── 泡泡射手类(physics + 瞄准发射)──
|
||||
泡泡射手:瞄准并发射泡泡,三个同色相连则消除,清空全部泡泡即胜。
|
||||
祖玛吐珠:旋转中心炮台向滚动的珠链射入同色珠子凑三消,珠链到达终点则失败。
|
||||
@ -0,0 +1,30 @@
|
||||
# saa-e2e-briefs-holdout.txt —— 003-U1 留出集(hold-out,不进 A/B 调参,供 final R1 量)
|
||||
# 用法:-Dsaa.e2e.sourceMode=gamedef -Dsaa.e2e.briefFile=src/test/resources/saa-e2e-briefs-holdout.txt
|
||||
# 纪律:本集 brief 与 saa-e2e-briefs-gamedef.txt 及 SaaFullGraphE2eTest.ALL_BRIEFS 均不重叠——
|
||||
# A/B 阶段只用 gamedef.txt 调参/收敛,避免对留出集过拟合;final R1 验收才用本集量真泛化成功率。
|
||||
# 选品同样偏向运行时/九门 harness 支持品类(realtime/physics/tap-targets/clickable)。
|
||||
#
|
||||
# ── 挡板/接球类 ──
|
||||
保龄反弹:底部挡板把弹珠弹向顶部砖墙,敲碎全部砖块即胜,球落底则败。
|
||||
# ── 蛇/网格类 ──
|
||||
吃星变长:操控小蛇在网格内吃星星变长,撞墙或咬到自己则结束。
|
||||
# ── 接物类 ──
|
||||
接落叶:左右移动竹篮接住飘落的树叶,连漏五片则游戏结束。
|
||||
# ── 跳跃/躲避类 ──
|
||||
忍者翻墙:连续点击让忍者起跳越过迎面飞来的飞镖,被击中即结束。
|
||||
# ── 消除类 ──
|
||||
同色爆破:点击成片相连的同色气球一次性引爆,达到目标分数过关。
|
||||
# ── 射击类 ──
|
||||
炮台轰机:左右移动炮台射击编队俯冲的战机,被撞或漏过太多则失败。
|
||||
# ── 打地鼠/点击反应类 ──
|
||||
拍蚊子:屏幕四处乱飞的蚊子,快速点中拍死得分,限时结束统计战果。
|
||||
# ── 节奏类 ──
|
||||
鼓点连击:圆圈缩到判定环瞬间点击得分,连续错失三次则结束。
|
||||
# ── 平台类 ──
|
||||
攀岩登顶:交替左右蹬跳攀上一块块岩点抵达山顶,踩空坠落则失败。
|
||||
# ── 飞行/穿缝类 ──
|
||||
潜艇穿礁:点击上浮松手下沉,驾潜艇穿过上下错落的暗礁缝隙,触礁即结束。
|
||||
# ── 躲避/生存类 ──
|
||||
雷区穿行:上下左右移动小车躲开不断落下的地雷,坚持到倒计时归零即通关。
|
||||
# ── 弹力/打靶类 ──
|
||||
台球清台:拖动球杆瞄准击打母球,把台面所有彩球撞进袋即清台过关。
|
||||
Loading…
x
Reference in New Issue
Block a user