Sfoglia il codice sorgente

fix(d2-02-t3): DebugSessionClient 审查整改——release抢先置闸防在途心跳误弹失效框+Dispose释放自建HttpClient(对齐MjpegStreamClient谁起谁清)+Interlocked闸幂等+心跳测试放宽防flaky+注释乱码

huangjie 2 giorni fa
parent
commit
5f080a166e

+ 2 - 2
ivf_tl_operate_2.0/control/IvfTl.ControlHost.Tests/DebugSessionClientTests.cs

@@ -74,9 +74,9 @@ namespace IvfTl.ControlHost.Tests
             var h = new FakeHandler { Responder = _ => (HttpStatusCode.OK, "{\"ok\":true,\"result\":\"sid123\"}") };
             var c = new DebugSessionClient("http://127.0.0.1:9/", new HttpClient(h)) { HeartbeatIntervalMs = 50 };
             await c.AcquireAsync(6);
-            await Task.Delay(180);
+            await Task.Delay(300);
             await c.ReleaseAsync();
-            Assert.True(h.HeartbeatCount >= 2);   // 50ms 间隔 180ms 内至少 2 次
+            Assert.True(h.HeartbeatCount >= 2);   // 50ms 间隔 300ms 内至少 2 次(放宽窗口防 CI 慢机 flaky)
         }
     }
 }

+ 16 - 6
ivf_tl_operate_2.0/ivf_tl_Operate/Debug/DebugSessionClient.cs

@@ -17,9 +17,10 @@ namespace ivf_tl_Operate.Debug
     {
         private readonly string _baseUrl;
         private readonly HttpClient _http;
+        private readonly bool _ownsHttp;
         private string _sessionId;
         private Timer _heartbeatTimer;
-        private volatile bool _expiredFired;
+        private int _expiredFired;   // 0=未触发 1=已触发(Interlocked 闸,保证 OnSessionExpired 只触发一次)
 
         /// <summary>心跳间隔(ms),默认 2.5s(远小于 control 端 10s TTL,容忍偶发卡顿)。</summary>
         public int HeartbeatIntervalMs { get; set; } = 2500;
@@ -32,6 +33,7 @@ namespace ivf_tl_Operate.Debug
         public DebugSessionClient(string baseUrl, HttpClient http = null)
         {
             _baseUrl = baseUrl.TrimEnd('/');
+            _ownsHttp = http == null;          // 自建的由本类 Dispose;注入的归调用方
             _http = http ?? new HttpClient();
         }
 
@@ -41,23 +43,23 @@ namespace ivf_tl_Operate.Debug
             var resp = await _http.PostAsync($"{_baseUrl}{path}", content);
             string s = await resp.Content.ReadAsStringAsync();
             var r = JsonConvert.DeserializeObject<AcquireResult>(s) ?? new AcquireResult { Ok = false, Error = "空响应" };
-            if (r.Code == "SESSION_EXPIRED" && !_expiredFired)
+            // Interlocked 闸:并发请求同收 SESSION_EXPIRED 时,回调只触发一次。
+            if (r.Code == "SESSION_EXPIRED" && Interlocked.CompareExchange(ref _expiredFired, 1, 0) == 0)
             {
-                _expiredFired = true;
                 StopHeartbeat();
                 try { OnSessionExpired?.Invoke(); } catch { }
             }
             return r;
         }
 
-        /// <summary>借用某舱。成功则记录 sessionId 并起心跳。返回体含 cultivating/embryoCount(供��认框)。</summary>
+        /// <summary>借用某舱。成功则记录 sessionId 并起心跳。返回体含 cultivating/embryoCount(供认框)。</summary>
         public async Task<AcquireResult> AcquireAsync(int houseSn)
         {
             var r = await PostAsync("/debug/acquire", new { houseSn });
             if (r.Ok && !string.IsNullOrEmpty(r.SessionId))
             {
                 _sessionId = r.SessionId;
-                _expiredFired = false;
+                Interlocked.Exchange(ref _expiredFired, 0);
                 StartHeartbeat();
             }
             return r;
@@ -70,6 +72,10 @@ namespace ivf_tl_Operate.Debug
         /// <summary>归还会话(幂等):停心跳、清 sessionId、通知 control。多次调用不抛。</summary>
         public async Task ReleaseAsync()
         {
+            // 主动归还:抢先置闸,挡掉 release 与"在途心跳"的竞态——
+            // 在途心跳若晚于 release 到达 control,会话已删会回 SESSION_EXPIRED,
+            // 不置闸就会在用户正常关调试时误弹"会话已失效"框。
+            Interlocked.Exchange(ref _expiredFired, 1);
             StopHeartbeat();
             var sid = _sessionId;
             _sessionId = null;
@@ -93,6 +99,10 @@ namespace ivf_tl_Operate.Debug
             _heartbeatTimer = null;
         }
 
-        public void Dispose() { StopHeartbeat(); }
+        public void Dispose()
+        {
+            StopHeartbeat();
+            if (_ownsHttp) { try { _http?.Dispose(); } catch { } }   // 只释放自建的;注入的归调用方
+        }
     }
 }