From b544d5cccd76735e034c8055918a50927b437474 Mon Sep 17 00:00:00 2001
From: wenxiangchao <wenxiangchao@ihengxin.com.cn>
Date: 星期二, 10 十一月 2026 08:29:04 +0800
Subject: [PATCH] fix: 维护模式重复进入失败与按钮灰死 + Broker 并发验证用例

---
 Aging.Infrastructure/Services/AutoDeviceFacade.cs |  287 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 287 insertions(+), 0 deletions(-)

diff --git a/Aging.Infrastructure/Services/AutoDeviceFacade.cs b/Aging.Infrastructure/Services/AutoDeviceFacade.cs
new file mode 100644
index 0000000..45e0939
--- /dev/null
+++ b/Aging.Infrastructure/Services/AutoDeviceFacade.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Aging.Infrastructure.Contracts;
+using Aging.Infrastructure.Core;
+using Aging.ResourceContracts;
+using log4net;
+
+namespace Aging.Infrastructure.Services
+{
+    /// <summary>
+    /// 鑷姩鍖栬澶囬棬闈㈠疄鐜般��
+    /// 
+    /// 鐢熷懡鍛ㄦ湡锛�
+    ///   1) ReserveAsync 鐢宠璧勬簮鍗犵敤 鈫� 寰楀埌 ResourceOwnerToken
+    ///   2) DeviceService 鍒嗘淳鍛戒护 鈫� 鍐呴儴鍐嶆嬁鎿嶄綔绉熺害
+    ///   3) ReleaseReservationAsync 閲婃斁璧勬簮
+    /// 
+    /// 涓ょ璋冪敤妯″紡锛�
+    ///   - ExecuteAsync/QueryAsync锛氫竴娆℃�ф墽琛岋紙鍐呴儴瀹屾垚 1鈫�2鈫�3锛�
+    ///   - AcquireAsync锛氫綔鐢ㄥ煙绉熺害锛�1 鎵嬪姩瑙﹀彂锛�3 鍦� lease.Dispose 鏃惰Е鍙戯紱2 鍙娆★級
+    /// </summary>
+    public sealed class AutoDeviceFacade : IAutoDeviceFacade
+    {
+        private static readonly ILog Log = LogManager.GetLogger(typeof(AutoDeviceFacade));
+
+        private readonly IResourceManager _resourceManager;
+        private readonly IDeviceService _deviceService;
+
+        /// <summary>
+        /// 榛樿绉熺害鐢宠瓒呮椂锛堢锛�
+        /// </summary>
+        public int DefaultLeaseTimeoutSeconds { get; set; } = 30;
+
+        /// <summary>
+        /// 鏋勯�犲嚱鏁�
+        /// </summary>
+        public AutoDeviceFacade(IResourceManager resourceManager, IDeviceService deviceService)
+        {
+            _resourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
+            _deviceService = deviceService ?? throw new ArgumentNullException(nameof(deviceService));
+        }
+
+        // ==================== 涓�娆℃�ф墽琛屾ā寮� ====================
+
+        /// <inheritdoc/>
+        public async Task<DeviceCommandResult> ExecuteAsync(
+            string deviceId, string command, CancellationToken ct = default)
+        {
+            if (string.IsNullOrWhiteSpace(deviceId))
+                throw new ArgumentException("璁惧 ID 涓嶈兘涓虹┖", nameof(deviceId));
+            if (string.IsNullOrWhiteSpace(command))
+                throw new ArgumentException("鍛戒护涓嶈兘涓虹┖", nameof(command));
+
+            // 1. 鐢宠璧勬簮鍗犵敤
+            var owner = await ReserveCoreAsync(deviceId, "OneShot", ct).ConfigureAwait(false);
+            try
+            {
+                // 2. 閫氳繃 DeviceService 鍒嗘淳鍛戒护
+                return await _deviceService.ExecuteCommandAsync(deviceId, command, owner, ct)
+                    .ConfigureAwait(false);
+            }
+            finally
+            {
+                // 3. 鏃犺鎴愬姛澶辫触閮介噴鏀捐祫婧�
+                await ReleaseCoreAsync(deviceId, owner).ConfigureAwait(false);
+            }
+        }
+
+        /// <inheritdoc/>
+        public async Task<DeviceCommandResult> QueryAsync(
+            string deviceId, string query, CancellationToken ct = default)
+        {
+            if (string.IsNullOrWhiteSpace(deviceId))
+                throw new ArgumentException("璁惧 ID 涓嶈兘涓虹┖", nameof(deviceId));
+
+            var owner = await ReserveCoreAsync(deviceId, "OneShotQuery", ct).ConfigureAwait(false);
+            try
+            {
+                return await _deviceService.ExecuteQueryAsync(deviceId, query, owner, ct)
+                    .ConfigureAwait(false);
+            }
+            finally
+            {
+                await ReleaseCoreAsync(deviceId, owner).ConfigureAwait(false);
+            }
+        }
+
+        // ==================== 浣滅敤鍩熺绾︽ā寮� ====================
+
+        /// <inheritdoc/>
+        public async Task<IAutoDeviceLease> AcquireAsync(
+            string deviceId, string stepName, CancellationToken ct = default)
+        {
+            if (string.IsNullOrWhiteSpace(deviceId))
+                throw new ArgumentException("璁惧 ID 涓嶈兘涓虹┖", nameof(deviceId));
+
+            var owner = await ReserveCoreAsync(deviceId, stepName ?? "Step", ct).ConfigureAwait(false);
+            var sessionId = Guid.NewGuid().ToString();
+
+            Log.Info($"[AutoFacade] 璁惧 {deviceId} 宸茶幏寰椾綔鐢ㄥ煙绉熺害: Session={sessionId.Substring(0, 8)}, Step={stepName}");
+
+            var lease = new AutoDeviceLease(this, _deviceService, deviceId, sessionId, stepName ?? "Step", owner);
+
+            // 濡傛灉搴曞眰鏄� LayeredResourceManager锛屽惎鍔ㄥ績璺崇画鏈�
+            if (_resourceManager is LayeredResourceManager lrm)
+            {
+                if (lrm.TryGetGrants(owner.TokenId, out var grants))
+                {
+                    lease.StartHeartbeat(lrm.Coordinator, grants);
+                }
+            }
+
+            return lease;
+        }
+
+        // ==================== 鍐呴儴杈呭姪 ====================
+
+        /// <summary>
+        /// 鍐呴儴锛氱敵璇疯祫婧愬崰鐢ㄣ��
+        /// 浣跨敤 OwnerType.Production锛堣嚜鍔ㄥ寲娴嬭瘯瑙嗕负鐢熶骇娴佺▼锛夛紝
+        /// 浼氫笌鍏朵粬娴嬭瘯/缁存姢浼氳瘽浜掓枼銆�
+        /// </summary>
+        private async Task<ResourceOwnerToken> ReserveCoreAsync(
+            string deviceId, string purpose, CancellationToken ct)
+        {
+            var owner = new ResourceOwnerToken(OwnerType.Production, $"auto_{Guid.NewGuid():N}");
+            var resourceKey = new ResourceKey(deviceId, "Generic");
+
+            // 浣跨敤瓒呮椂鍖呰
+            using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
+            {
+                cts.CancelAfter(TimeSpan.FromSeconds(DefaultLeaseTimeoutSeconds));
+                try
+                {
+                    await _resourceManager.ReserveAsync(owner, resourceKey, cts.Token).ConfigureAwait(false);
+                    Log.Debug($"[AutoFacade] 璁惧 {deviceId} 宸查绾� (purpose={purpose}, owner={owner.OwnerId})");
+                }
+                catch (OperationCanceledException)
+                {
+                    throw new TimeoutException($"璁惧 {deviceId} 绉熺害鐢宠瓒呮椂锛坽DefaultLeaseTimeoutSeconds}s锛夛紝鍙兘琚敓浜ф垨缁存姢鍗犵敤");
+                }
+                catch (ResourceConflictException ex)
+                {
+                    throw new InvalidOperationException(
+                        $"璁惧 {deviceId} 宸茶鍏朵粬鎵�鏈夎�呭崰鐢�: {ex.Message}", ex);
+                }
+            }
+
+            return owner;
+        }
+
+        /// <summary>
+        /// 鍐呴儴锛氶噴鏀捐祫婧愬崰鐢ㄣ��
+        /// </summary>
+        internal async Task ReleaseCoreAsync(string deviceId, ResourceOwnerToken owner)
+        {
+            try
+            {
+                var resourceKey = new ResourceKey(deviceId, "Generic");
+                await _resourceManager.ReleaseReservationAsync(owner, resourceKey, CancellationToken.None)
+                    .ConfigureAwait(false);
+                Log.Debug($"[AutoFacade] 璁惧 {deviceId} 宸查噴鏀鹃绾� (owner={owner.OwnerId})");
+            }
+            catch (Exception ex)
+            {
+                // 閲婃斁澶辫触涓嶉樆濉炰富娴佺▼锛屼粎璁板綍
+                Log.Warn($"[AutoFacade] 閲婃斁璁惧 {deviceId} 棰勭害寮傚父: {ex.Message}");
+            }
+        }
+    }
+
+    /// <summary>
+    /// 鑷姩鍖栬澶囦綔鐢ㄥ煙绉熺害瀹炵幇銆�
+    /// Dispose 鏃惰嚜鍔ㄩ噴鏀惧簳灞傝祫婧愬崰鐢ㄣ��
+    /// 骞傜瓑锛氬娆� Dispose 鍙噴鏀句竴娆°��
+    /// </summary>
+    internal sealed class AutoDeviceLease : IAutoDeviceLease
+    {
+        private readonly AutoDeviceFacade _facade;
+        private readonly IDeviceService _deviceService;
+        private readonly ResourceOwnerToken _owner;
+        private bool _disposed;
+
+        // V3.0 蹇冭烦缁湡瀛楁
+        private LeaseGrant[] _grants = new LeaseGrant[0];
+        private Timer? _beacon;
+        private volatile int _renewFailures;
+        private ILeaseCoordinator? _coordinator;
+        public event EventHandler? LeaseLost;
+        public bool HandoverRequested => false;
+
+        public string DeviceId { get; }
+        public string SessionId { get; }
+        public string StepName { get; }
+        public DateTime AcquiredAt { get; }
+        public bool IsReleased => _disposed;
+
+        internal AutoDeviceLease(
+            AutoDeviceFacade facade,
+            IDeviceService deviceService,
+            string deviceId,
+            string sessionId,
+            string stepName,
+            ResourceOwnerToken owner)
+        {
+            _facade = facade;
+            _deviceService = deviceService;
+            DeviceId = deviceId;
+            SessionId = sessionId;
+            StepName = stepName;
+            AcquiredAt = DateTime.Now;
+            _owner = owner;
+        }
+
+        internal void StartHeartbeat(ILeaseCoordinator coordinator, LeaseGrant[] grants)
+        {
+            _coordinator = coordinator;
+            _grants = grants;
+            _beacon = new Timer(_ =>
+            {
+                if (_disposed) return;
+                try
+                {
+                    bool allOk = true;
+                    foreach (var g in _grants)
+                    {
+                        if (!_coordinator.Renew(g, 30)) { allOk = false; break; }
+                    }
+                    if (allOk)
+                        Interlocked.Exchange(ref _renewFailures, 0);
+                    else
+                        Interlocked.Increment(ref _renewFailures);
+                    if (Volatile.Read(ref _renewFailures) >= 2) LeaseLost?.Invoke(this, EventArgs.Empty);
+                }
+                catch
+                {
+                    if (Interlocked.Increment(ref _renewFailures) >= 2) LeaseLost?.Invoke(this, EventArgs.Empty);
+                }
+            }, null, 8000, 8000);
+        }
+
+        public Task<DeviceCommandResult> ExecuteAsync(string command, CancellationToken ct = default)
+        {
+            ThrowIfReleased();
+            ThrowIfStale();
+            return _deviceService.ExecuteCommandAsync(DeviceId, command, _owner, ct);
+        }
+
+        public Task<DeviceCommandResult> QueryAsync(string query, CancellationToken ct = default)
+        {
+            ThrowIfReleased();
+            ThrowIfStale();
+            return _deviceService.ExecuteQueryAsync(DeviceId, query, _owner, ct);
+        }
+
+        public Task ReleaseAsync(CancellationToken ct = default)
+        {
+            // Dispose() 鍐呴儴宸插鐞� _beacon?.Dispose() 鍜� _disposed 鏍囪
+            Dispose();
+            return Task.CompletedTask;
+        }
+
+        public void Dispose()
+        {
+            if (_disposed) return;
+            _beacon?.Dispose();
+            _disposed = true;
+            // 鍚屾閲婃斁锛�.NET 4.7.2 鏃� IAsyncDisposable锛夛紝闃诲绛夊緟
+            _facade.ReleaseCoreAsync(DeviceId, _owner).GetAwaiter().GetResult();
+            GC.SuppressFinalize(this);
+        }
+
+        private void ThrowIfReleased()
+        {
+            if (_disposed)
+                throw new ObjectDisposedException(nameof(AutoDeviceLease),
+                    $"璁惧 {DeviceId} 鐨勭绾﹀凡閲婃斁 (Session={SessionId.Substring(0, 8)})");
+        }
+
+        private void ThrowIfStale()
+        {
+            if (Volatile.Read(ref _renewFailures) >= 2)
+                throw new InvalidOperationException("LEASE_STALE: 绉熺害宸插け鏁�");
+        }
+    }
+}

--
Gitblit v1.9.1