From d870d67cd0dfe9d48205f16cb8fbcfc4bd5e0b05 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 03:12:09 +0800 Subject: fix(api): EOF 阶段到期一律判定超时,恢复的载荷不得转为成功 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 退出阶段与 EOF 阶段统一用 expired 标志:EOF 等待按剩余总期限计时,未完成读取且非请求取消即视为期限已过,即便清理宽限期回收了载荷也只返回超时错误 - EOF 等待挂接 deadline+请求 联合取消令牌,请求取消即刻中断等待;清理宽限期(2s)仅用于回收日志,不再影响结果分类 - 预先取消的请求不启动采集器进程 - 回归:孤儿管道持有至总期限→超时+后代死亡;EOF 等待中取消→延迟<5s+后代死亡;预取消→无进程启动;保留成功/stderr 洪泛等原行为 - 验证:聚焦 ProcessCollector 11/11,全量 Domain 19 / Web 17 / API 32 通过(构建 0 警告);实机挂起采集器 HTTP 503 2.3s 且无进程残留 --- src/FundLab.Api/MarketDataService.fs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/FundLab.Api/MarketDataService.fs b/src/FundLab.Api/MarketDataService.fs index 0c4714d..e9993f3 100644 --- a/src/FundLab.Api/MarketDataService.fs +++ b/src/FundLab.Api/MarketDataService.fs @@ -46,7 +46,9 @@ type ProcessMarketDataCollector(pythonExecutable: string, scriptPath: string, py Console.Error.WriteLine("fund-lab collector stderr: {0}", bounded) let execute (token: CancellationToken) arguments = - if not (File.Exists scriptPath) then + if token.IsCancellationRequested then + Error "collector run was cancelled" + elif not (File.Exists scriptPath) then Error "collector script is not deployed" else let startInfo = ProcessStartInfo() @@ -98,12 +100,12 @@ type ProcessMarketDataCollector(pythonExecutable: string, scriptPath: string, py use timeoutSource = new CancellationTokenSource(deadline) use linkedSource = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutSource.Token) - let mutable timedOut = false + let mutable expired = false try child.WaitForExitAsync(linkedSource.Token).GetAwaiter().GetResult() with _ -> - if not token.IsCancellationRequested then timedOut <- true + if not token.IsCancellationRequested then expired <- true killOwnedGroup () @@ -112,15 +114,18 @@ type ProcessMarketDataCollector(pythonExecutable: string, scriptPath: string, py with _ -> () let mutable eofRemaining = deadline - clock.Elapsed - - if eofRemaining <= TimeSpan.Zero then eofRemaining <- TimeSpan.FromSeconds(2.0) - + let mutable eofExpired = eofRemaining <= TimeSpan.Zero let mutable readsDone = false try - readsDone <- Task.WhenAll(outputTask, errorTask).Wait(int eofRemaining.TotalMilliseconds) + if not eofExpired then + readsDone <- + Task.WhenAll(outputTask, errorTask) + .Wait(int eofRemaining.TotalMilliseconds, linkedSource.Token) with _ -> () + if not readsDone && not token.IsCancellationRequested then eofExpired <- true + if not readsDone then killOwnedGroup () @@ -138,7 +143,7 @@ type ProcessMarketDataCollector(pythonExecutable: string, scriptPath: string, py logCollectorStderr error - if timedOut then + if expired || eofExpired then Error(sprintf "collector did not finish within %d seconds" timeoutSeconds) elif token.IsCancellationRequested then Error "collector run was cancelled" -- cgit v1.2.3