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