From ea3a5026ad76028562ee4aa6b70c79c94fb17d4b Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 03:03:59 +0800 Subject: fix(api): 加固行情采集子进程超时、取消与进程组清理并脱敏错误 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MarketDataService: setsid 独立会话启动采集器,硬超时覆盖子进程退出与管道 EOF 两个阶段;超时、取消与异常路径按进程组杀灭(含持有管道的后台子进程),不遗留孤儿进程 - App.ts/Search/RefreshNav 传入 ctx.RequestAborted,客户端断开即取消采集 - 对外错误仅返回统一短消息(503 MARKET_DATA_UNAVAILABLE 等),不泄露路径、stderr 或密钥;stderr 仅记录服务端日志 - 新增 9 项子进程回归(成功/stderr 洪泛/超时杀灭/取消杀灭/孤儿管道恢复/畸形载荷/非零退出/启动失败/缺脚本)与 HTTP 脱敏回归;全文套件 66 通过(Domain 19 / Web 17 / API 30) - 实机验证:挂起采集器 503 2.4s 无进程残留;真实 akshare 1.18.96 搜索 000001 返回 200,净值刷新持久化 6010 条观测 - README 与 qa/README 补充环境变量、回归命令、G1 过滤断言口径与真实建档标签辨析 --- tests/FundLab.Api.Tests/PersistenceTests.fs | 51 +++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) (limited to 'tests/FundLab.Api.Tests/PersistenceTests.fs') diff --git a/tests/FundLab.Api.Tests/PersistenceTests.fs b/tests/FundLab.Api.Tests/PersistenceTests.fs index 5157a9c..64e857c 100644 --- a/tests/FundLab.Api.Tests/PersistenceTests.fs +++ b/tests/FundLab.Api.Tests/PersistenceTests.fs @@ -695,8 +695,8 @@ type PersistenceTests(fixture: PostgresFixture) = let marketData = { new IMarketDataService with - member _.Search _ = Ok searchPayload - member _.RefreshNav _ = Ok [ observation ] + member _.Search(_, _) = Ok searchPayload + member _.RefreshNav(_, _) = Ok [ observation ] member _.GetNav(_, _, _) = Ok [ observation ] } let app = App.createApplicationWithMarketData (repository ()) marketData @@ -725,6 +725,53 @@ type PersistenceTests(fixture: PostgresFixture) = Assert.Contains("\"navDate\":\"2026-09-18\"", navBody) Assert.Contains("\"source\":\"akshare\"", navBody) + [] + member _.``market data API keeps collector failures and process tree out of HTTP responses``() = + let dir = Path.Combine(Path.GetTempPath(), "fund-lab-collector-" + Guid.NewGuid().ToString("N")) + + Directory.CreateDirectory(dir) |> ignore + + try + let scriptPath = Path.Combine(dir, "hang.sh") + + File.WriteAllText( + scriptPath, + "printf 'secret=%s\\n' 'fund-lab-HTTP-SENTINEL' >&2\nsleep 60 & child=$!\necho $child > pidfile\nwait $child\n" + ) + + let collector = + ProcessMarketDataCollector("/bin/sh", scriptPath, None, 1) :> IMarketDataCollector + + let marketData = MarketDataService(repository (), collector) + let app = App.createApplicationWithMarketData (repository ()) marketData + + let status, responseBody = + PersistenceTestHelpers.invoke + app + "GET" + "/api/instruments/search?q=000001" + [ "Authorization", "Bearer test-token" ] + "" + + Assert.Equal(503, status) + Assert.Contains("MARKET_DATA_UNAVAILABLE", responseBody) + Assert.Contains("did not finish within 1 seconds", responseBody) + Assert.DoesNotContain(dir, responseBody) + Assert.DoesNotContain("fund-lab-HTTP-SENTINEL", responseBody) + + let recordedPid = Int32.Parse(File.ReadAllText(Path.Combine(dir, "pidfile")).Trim()) + let mutable dead = not (Directory.Exists(sprintf "/proc/%d" recordedPid)) + let mutable waited = 0 + + while not dead && waited < 10000 do + Thread.Sleep(100) + waited <- waited + 100 + dead <- not (Directory.Exists(sprintf "/proc/%d" recordedPid)) + + Assert.True(dead, "backgrounded subprocess survived the HTTP timeout") + finally + Directory.Delete(dir, true) + [] member _.``fund API rejects non-object JSON bodies``() = let app = App.createApplication (repository ()) -- cgit v1.2.3