修复 AGC 本地 BgFilter worker 启动链路 (#258)
backend 模式复用 Rust 双进程启动与 watcher AGC 后端复用门禁增加 BgFilter worker readiness 补充启动编排与 ready 门禁回归测试 同步本地开发运维文档 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/258 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #258.
This commit is contained in:
@@ -79,6 +79,7 @@ function resolveBackendTargetsFromState(
|
||||
) {
|
||||
const apiServer = state?.services?.['api-server'];
|
||||
const spacetime = state?.services?.spacetime;
|
||||
const bgfilterWorker = state?.services?.['bgfilter-worker'];
|
||||
const isActive = (service) =>
|
||||
service && ['running', 'reused', 'starting'].includes(service.status ?? '');
|
||||
const database = typeof state?.database === 'string' ? state.database : '';
|
||||
@@ -104,9 +105,14 @@ function resolveBackendTargetsFromState(
|
||||
: requireAgcBackend
|
||||
? ''
|
||||
: 'http://127.0.0.1:3101';
|
||||
const bgfilterWorkerUrl =
|
||||
canReuseState && isActive(bgfilterWorker) && bgfilterWorker.url
|
||||
? bgfilterWorker.url
|
||||
: '';
|
||||
return {
|
||||
apiUrl,
|
||||
spacetimeUrl,
|
||||
bgfilterWorkerUrl,
|
||||
database,
|
||||
spacetimeDataDir,
|
||||
hasMatchingDatabase,
|
||||
@@ -121,16 +127,22 @@ function readBackendTargets({ requireAgcBackend = false } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
async function isBackendReady() {
|
||||
const { apiUrl, spacetimeUrl, hasMatchingBackend } = readBackendTargets({
|
||||
requireAgcBackend: true,
|
||||
});
|
||||
async function isBackendReady({
|
||||
state = readJson(devStackStatePath),
|
||||
isReady = isHttpReady,
|
||||
} = {}) {
|
||||
const { apiUrl, spacetimeUrl, bgfilterWorkerUrl, hasMatchingBackend } =
|
||||
resolveBackendTargetsFromState(state, {
|
||||
requireAgcBackend: true,
|
||||
});
|
||||
return (
|
||||
hasMatchingBackend &&
|
||||
Boolean(apiUrl) &&
|
||||
Boolean(spacetimeUrl) &&
|
||||
(await isHttpReady(`${apiUrl}/healthz`)) &&
|
||||
(await isHttpReady(`${spacetimeUrl}/v1/ping`))
|
||||
Boolean(bgfilterWorkerUrl) &&
|
||||
(await isReady(`${apiUrl}/healthz`)) &&
|
||||
(await isReady(`${spacetimeUrl}/v1/ping`)) &&
|
||||
(await isReady(`${bgfilterWorkerUrl}/readyz`))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -668,6 +680,7 @@ function isDirectModuleExecution() {
|
||||
export {
|
||||
ensureBackend,
|
||||
formatChildFailure,
|
||||
isBackendReady,
|
||||
isDirectModuleExecution,
|
||||
isProcessGroupAlive,
|
||||
preflightExistingVite,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
ensureBackend,
|
||||
isBackendReady,
|
||||
isProcessGroupAlive,
|
||||
preflightExistingVite,
|
||||
readLinuxProcessGroupAlive,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
const expectedDatabase = 'genarrative-game-creator-dev';
|
||||
const expectedDataDir = resolve('server-rs/.spacetimedb/ai-game-creator/data');
|
||||
|
||||
function backendState(spacetimeDataDir?: string) {
|
||||
function backendState(spacetimeDataDir?: string, includeBgfilterWorker = true) {
|
||||
return {
|
||||
schemaVersion: spacetimeDataDir ? 2 : 1,
|
||||
database: expectedDatabase,
|
||||
@@ -35,6 +36,14 @@ function backendState(spacetimeDataDir?: string) {
|
||||
status: 'running',
|
||||
url: 'http://127.0.0.1:3101',
|
||||
},
|
||||
...(includeBgfilterWorker
|
||||
? {
|
||||
'bgfilter-worker': {
|
||||
status: 'running',
|
||||
url: 'http://127.0.0.1:8083',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -87,6 +96,36 @@ describe('AI 游戏创作配套后端复用门禁', () => {
|
||||
expect(matching.hasMatchingBackend).toBe(true);
|
||||
expect(matching.apiUrl).toBe('http://127.0.0.1:8082');
|
||||
expect(matching.spacetimeUrl).toBe('http://127.0.0.1:3101');
|
||||
expect(matching.bgfilterWorkerUrl).toBe('http://127.0.0.1:8083');
|
||||
});
|
||||
|
||||
test('worker 缺失或未 ready 时不允许复用后端', async () => {
|
||||
const isReady = vi.fn(async (_url: string) => true);
|
||||
|
||||
await expect(
|
||||
isBackendReady({
|
||||
state: backendState(expectedDataDir, false),
|
||||
isReady,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
expect(isReady).not.toHaveBeenCalled();
|
||||
|
||||
isReady.mockImplementation(async (url) => url.endsWith('/readyz'));
|
||||
await expect(
|
||||
isBackendReady({
|
||||
state: backendState(expectedDataDir),
|
||||
isReady,
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
expect(isReady).toHaveBeenCalledWith('http://127.0.0.1:8082/healthz');
|
||||
|
||||
isReady.mockImplementation(async () => true);
|
||||
await expect(
|
||||
isBackendReady({
|
||||
state: backendState(expectedDataDir),
|
||||
isReady,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ Linux 本机多用户并发开发时,`npm run dev`、`npm run dev:*` 单模块
|
||||
|
||||
后端日志默认写入 `logs/api-server/`,独立 BgFilter worker 日志默认写入 `logs/bgfilter-worker/`。后端 API smoke 使用 `npm run dev:api-server`,先检查 BgFilter worker `/readyz`,再检查 API `/healthz`;需要确认 API 实例可接生产流量时检查 API `/readyz`。不要使用旧 `api-server:maincloud` 或任何 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 口径。
|
||||
|
||||
AI 游戏创作客户端使用 `npm run agc`。该入口由 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 解析 AGC Vite 实际端口:Linux 默认取当前用户端口段的 `start + 5`,占用时只在本用户段内漂移;Windows / macOS 保留 `3080` 为兼容首选并允许统一漂移。最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI 动态 `build.devUrl` 配置传给 WebView,并通过 Vite CLI `--port` 启动严格监听;Vite 继续使用 `strictPort`,任何一层都不得自行改到另一个端口。启动器在创建原生窗口前预检最终地址;若竞态中该地址被 AGC Vite、无响应监听器或其它服务占用,一律失败关闭,不复用、也不擅自终止无法证明归属的进程。
|
||||
AI 游戏创作客户端使用 `npm run agc`。该入口由 `apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs` 解析 AGC Vite 实际端口:Linux 默认取当前用户端口段的 `start + 5`,占用时只在本用户段内漂移;Windows / macOS 保留 `3080` 为兼容首选并允许统一漂移。最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI 动态 `build.devUrl` 配置传给 WebView,并通过 Vite CLI `--port` 启动严格监听;Vite 继续使用 `strictPort`,任何一层都不得自行改到另一个端口。AGC 配套后端的 `backend` 模式启动 SpacetimeDB、独立 `bgfilter-worker` 和 `api-server`,并在复用现有后端前同时检查三者状态及 `/v1/ping`、`/readyz`、`/healthz`;worker 缺失时不得把不完整的 API/数据库组合误判为 ready。启动器在创建原生窗口前预检最终地址;若竞态中该地址被 AGC Vite、无响应监听器或其它服务占用,一律失败关闭,不复用、也不擅自终止无法证明归属的进程。
|
||||
|
||||
Tauri `beforeDevCommand` 默认与客户端构建并行,不能把上述检查只放在 `beforeDevCommand` 内:选定地址上若已有旧 Vite,Tauri 可能先创建加载旧前端的窗口,随后配套后端才因代理不匹配退出。外层启动器会把 Tauri CLI 放入受控进程树;CLI 正常退出、启动失败或收到终止信号后,POSIX 先向保留的 PGID 发送 `SIGTERM`、有界等待后升级 `SIGKILL`,Windows 使用 `taskkill /PID <pid> /T /F`。Linux 容器中的孤儿后代退出后可能暂时保留为 zombie,`kill(-PGID, 0)` 仍会返回成功;启动器必须结合 `/proc/<pid>/stat` 判断同组是否还存在非 zombie 成员,不能把等待 PID 1 回收误报为清理失败。配套后端和 Vite 仍由 `start-dev-stack.mjs` 各自持有,退出时同样有界收束,避免只剩客户端、Runner、Cargo 或旧订阅进程。排障时同时核对控制台输出的 AGC Vite 实际地址及其 marker、`.app/dev-stack.json` 的实际 API URL 和进程 cwd;不要把“终端已返回”当成客户端及其 Runner 已退出的证据。
|
||||
|
||||
|
||||
+2
-3
@@ -1619,9 +1619,8 @@ class DevRunner {
|
||||
|
||||
if (command === 'backend') {
|
||||
await this.startSpacetimeForFullStack();
|
||||
await this.services.get('api-server').start();
|
||||
await this.waitForApiServer();
|
||||
this.startWatchers(['spacetime', 'api-server']);
|
||||
await this.startRustServicePair();
|
||||
this.startWatchers(['spacetime', 'api-server', 'bgfilter-worker']);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -500,6 +500,30 @@ describe('dev scheduler Rust service orchestration', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('backend 模式复用 Rust 双进程并纳入三服务 watcher', async () => {
|
||||
const { explicitOptions, options } = parseArgs(['backend'], {});
|
||||
const runner = new DevRunner(options, {}, explicitOptions);
|
||||
const startSpacetime = vi
|
||||
.spyOn(runner, 'startSpacetimeForFullStack')
|
||||
.mockResolvedValue(undefined);
|
||||
const startPair = vi
|
||||
.spyOn(runner, 'startRustServicePair')
|
||||
.mockResolvedValue(undefined);
|
||||
const startWatchers = vi
|
||||
.spyOn(runner, 'startWatchers')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
await runner.startCommand('backend');
|
||||
|
||||
expect(startSpacetime).toHaveBeenCalledOnce();
|
||||
expect(startPair).toHaveBeenCalledOnce();
|
||||
expect(startWatchers).toHaveBeenCalledWith([
|
||||
'spacetime',
|
||||
'api-server',
|
||||
'bgfilter-worker',
|
||||
]);
|
||||
});
|
||||
|
||||
test('完整栈只为两个 Rust 角色创建一套组合 watcher', () => {
|
||||
const { explicitOptions, options } = parseArgs(['--watch'], {});
|
||||
const runner = new DevRunner(options, {}, explicitOptions);
|
||||
|
||||
Reference in New Issue
Block a user