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/Core/LayeredResourceManager.cs |  231 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 231 insertions(+), 0 deletions(-)

diff --git a/Aging.Infrastructure/Core/LayeredResourceManager.cs b/Aging.Infrastructure/Core/LayeredResourceManager.cs
new file mode 100644
index 0000000..744d9d0
--- /dev/null
+++ b/Aging.Infrastructure/Core/LayeredResourceManager.cs
@@ -0,0 +1,231 @@
+using System.Collections.Concurrent;
+using Aging.Infrastructure.Contracts;
+using Aging.ResourceContracts;
+using log4net;
+
+namespace Aging.Infrastructure.Core
+{
+    /// <summary>
+    /// 鍩轰簬 DbLeaseStore 鐨� IResourceManager 鍒嗗眰瀹炵幇銆�
+    /// 瀵瑰绛惧悕涓� ResourceManager 瀹屽叏涓�鑷达紙闆舵敼鍔級锛屽唴閮ㄨ浆鍙戝埌 ILeaseCoordinator 浠茶搴撱��
+    /// </summary>
+    public sealed class LayeredResourceManager : IResourceManager
+    {
+        private static readonly ILog Log = LogManager.GetLogger(typeof(LayeredResourceManager));
+
+        private readonly ILeaseCoordinator _lease;
+
+        /// <summary>
+        /// 鎸� TokenId 缂撳瓨宸茶幏鍙栫殑 LeaseGrant 鏁扮粍锛屼緵 ValidateOwner / Release / AcquireOperation 浣跨敤銆�
+        /// </summary>
+        private readonly ConcurrentDictionary<string, LeaseGrant[]> _grantsByToken = new();
+
+        /// <summary>
+        /// 榛樿绉熺害 TTL锛堢锛�
+        /// </summary>
+        private const int DefaultTtlSeconds = 300; // 5 鍒嗛挓锛岃冻澶熼暱鐢ㄤ簬 MVP
+
+        public LayeredResourceManager(ILeaseCoordinator lease)
+        {
+            _lease = lease ?? throw new ArgumentNullException(nameof(lease));
+        }
+
+        /// <summary>
+        /// 鑾峰彇搴曞眰鐨� ILeaseCoordinator锛堜緵 AutoDeviceLease 蹇冭烦缁湡浣跨敤锛�
+        /// </summary>
+        public ILeaseCoordinator Coordinator => _lease;
+
+        // ======================== ReserveAsync ========================
+
+        public async Task<ResourceReservation> ReserveAsync(
+            ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+        {
+            if (owner == null) throw new ArgumentNullException(nameof(owner));
+            if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+            var req = new AcquireRequest
+            {
+                OwnerId = owner.OwnerId,
+                OwnerType = (OwnerTypeV3)(int)owner.OwnerType,
+                ResourceKeys = new[] { resource.ToString() },
+                TtlSeconds = DefaultTtlSeconds,
+                WaitTimeoutMs = 0,
+                Reason = owner.OwnerId
+            };
+
+            AcquireOutcome outcome;
+            try
+            {
+                outcome = await Task.Run(() => _lease.Acquire(req), ct).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException)
+            {
+                throw;
+            }
+            catch (Exception ex)
+            {
+                Log.Error($"[LayeredRM] Acquire 寮傚父: {ex.Message}", ex);
+                throw new ResourceConflictException($"璧勬簮 {resource} 鑾峰彇寮傚父: {ex.Message}", ex);
+            }
+
+            switch (outcome.Code)
+            {
+                case AcquireResultCode.Acquired:
+                case AcquireResultCode.Reused:
+                    _grantsByToken[owner.TokenId] = outcome.Grants;
+                    Log.Info($"[LayeredRM] 璧勬簮 {resource} 棰勭害鎴愬姛锛屾墍鏈夎��: {owner.OwnerId}锛宭ease: {outcome.Grants[0].LeaseId}");
+                    return new ResourceReservation(resource, owner)
+                    {
+                        State = ReservationState.Active,
+                        Description = $"LeaseId={outcome.Grants[0].LeaseId}"
+                    };
+
+                case AcquireResultCode.ConflictHeld:
+                case AcquireResultCode.Timeout:
+                    var msg = BuildConflictMessage(resource, outcome.Holder);
+                    Log.Warn($"[LayeredRM] 璧勬簮鍐茬獊: {msg}");
+                    throw new ResourceConflictException(msg);
+
+                case AcquireResultCode.RecoveryBlocked:
+                    throw new ResourceConflictException(
+                        $"璧勬簮 {resource} 澶勪簬鎭㈠涓紙RecoveryRequired锛夛紝鏆備笉鍙敤");
+
+                default:
+                    throw new ResourceConflictException($"璧勬簮 {resource} 鑾峰彇澶辫触: {outcome.Code}");
+            }
+        }
+
+        // ======================== ReleaseReservationAsync ========================
+
+        public async Task ReleaseReservationAsync(
+            ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+        {
+            if (owner == null) throw new ArgumentNullException(nameof(owner));
+            if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+            var grant = FindGrant(owner.TokenId, resource);
+            if (grant == null)
+            {
+                Log.Warn($"[LayeredRM] 灏濊瘯閲婃斁鏃� grant 鐨勮祫婧�: {resource}, owner: {owner.OwnerId}");
+                return;
+            }
+
+            await Task.Run(() => _lease.Release(grant, owner.OwnerId), ct).ConfigureAwait(false);
+
+            // 娓呴櫎缂撳瓨
+            _grantsByToken.TryRemove(owner.TokenId, out _);
+            Log.Info($"[LayeredRM] 璧勬簮 {resource} 宸查噴鏀撅紝鎵�鏈夎��: {owner.OwnerId}");
+        }
+
+        // ======================== ValidateOwner ========================
+
+        public bool ValidateOwner(ResourceOwnerToken owner, ResourceKey resource)
+        {
+            if (owner == null) throw new ArgumentNullException(nameof(owner));
+            if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+            var grant = FindGrant(owner.TokenId, resource);
+            if (grant == null) return false;
+
+            // 鏍呮爮鏍¢獙锛歊enew 浠呭湪鈥滅绾︿粛 Active 涓� generation/epoch 鍖归厤涓旀湭杩囨湡鈥濇椂杩斿洖 true銆�
+            // 鐢ㄥ畬鏁� TTL 缁湡鑰岄潪 1s锛氭棦鏍¢獙褰掑睘锛屽張浣滀负姣忔鎿嶄綔鐨勪繚娲伙紝
+            // 閬垮厤鎶婄绾﹀埌鏈熸椂闂寸爫鍒� 1s 瀵艰嚧鍚庣画蹇冭烦 Renew 鍥犫�滃凡杩囨湡鈥濊�屽け璐モ啋璇姤绉熺害澶辨晥銆�
+            return _lease.Renew(grant, DefaultTtlSeconds);
+        }
+
+        // ======================== IsReserved ========================
+
+        public bool IsReserved(ResourceKey resource, out ResourceReservation? reservation)
+        {
+            if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+            var conflict = _lease.ProbeConflict(resource.ToString());
+            if (conflict != null)
+            {
+                // 鏋勯�犱竴涓� "鍗犱綅" reservation 杩斿洖缁欒皟鐢ㄦ柟
+                var syntheticOwner = new ResourceOwnerToken(
+                    (OwnerType)(int)conflict.OwnerType, conflict.OwnerId);
+                reservation = new ResourceReservation(resource, syntheticOwner,
+                    $"鍗犵敤涓�: {conflict.OwnerId} ({conflict.Reason})")
+                {
+                    State = ReservationState.Active
+                };
+                return true;
+            }
+
+            reservation = null;
+            return false;
+        }
+
+        // ======================== AcquireOperationAsync ========================
+
+        public async Task<IOperationLease> AcquireOperationAsync(
+            ResourceOwnerToken owner, ResourceKey resource, CancellationToken ct)
+        {
+            if (owner == null) throw new ArgumentNullException(nameof(owner));
+            if (resource == null) throw new ArgumentNullException(nameof(resource));
+
+            // 鏍呮爮鏍¢獙锛氱‘淇� owner 浠嶆寔鏈夎璧勬簮
+            if (!ValidateOwner(owner, resource))
+            {
+                throw new InvalidOperationException(
+                    $"璧勬簮 {resource} 鐨勬墍鏈夎�呮爡鏍忔牎楠屽け璐ワ紝owner: {owner.OwnerId}");
+            }
+
+            await Task.CompletedTask;
+
+            return new OperationLease(
+                resource,
+                owner,
+                onRelease: () => Log.Debug($"[LayeredRM] 鎿嶄綔绉熺害閲婃斁: {resource}"),
+                leaseDurationSeconds: 30);
+        }
+
+        // ======================== 鍐呴儴杈呭姪 ========================
+
+        /// <summary>
+        /// 鏍规嵁 TokenId 鑾峰彇缂撳瓨鐨� LeaseGrant 鏁扮粍锛堜緵 AutoDeviceLease 蹇冭烦缁湡浣跨敤锛�
+        /// </summary>
+        public bool TryGetGrants(string tokenId, out LeaseGrant[] grants)
+        {
+            return _grantsByToken.TryGetValue(tokenId, out grants);
+        }
+
+        /// <summary>
+        /// 鏍规嵁 TokenId 鍜岃祫婧愰敭鏌ユ壘瀵瑰簲鐨� LeaseGrant
+        /// </summary>
+        private LeaseGrant? FindGrant(string tokenId, ResourceKey resource)
+        {
+            if (!_grantsByToken.TryGetValue(tokenId, out var grants))
+                return null;
+
+            var resourceStr = resource.ToString();
+            foreach (var g in grants)
+            {
+                // DbLeaseStore 瀵� key 鍋氫簡 NormKey锛坱rim+lower+绌烘牸鏇挎崲涓�-锛夛紝
+                // 杩欓噷鐢ㄧ浉鍚岀殑瑙勮寖鍖栨潵姣旇緝
+                if (string.Equals(DbLeaseStore.NormKey(g.ResourceKey),
+                                  DbLeaseStore.NormKey(resourceStr),
+                                  StringComparison.OrdinalIgnoreCase))
+                    return g;
+            }
+            return null;
+        }
+
+        /// <summary>
+        /// 鏋勯�犲啿绐佸紓甯告秷鎭�
+        /// </summary>
+        private static string BuildConflictMessage(ResourceKey resource, ConflictInfo? holder)
+        {
+            if (holder == null)
+                return $"璧勬簮 {resource} 姝h鍗犵敤";
+
+            long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+            long remaining = Math.Max(0, (holder.ExpiresUtcMs - nowMs) / 1000);
+
+            return $"璧勬簮 {resource} 姝h鍗犵敤锛歿holder.OwnerId}" +
+                   $"锛坽holder.OwnerType}锛屾満鍣� {holder.MachineName} PID {holder.HostProcessId}锛�" +
+                   $"鍓╀綑绉熺害 {remaining}s锛�";
+        }
+    }
+}

--
Gitblit v1.9.1