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 并发验证用例
---
tests/Aging.MvpTests/BrokerE2ETests.cs | 187 ++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 187 insertions(+), 0 deletions(-)
diff --git a/tests/Aging.MvpTests/BrokerE2ETests.cs b/tests/Aging.MvpTests/BrokerE2ETests.cs
index dcd0d15..b9315ea 100644
--- a/tests/Aging.MvpTests/BrokerE2ETests.cs
+++ b/tests/Aging.MvpTests/BrokerE2ETests.cs
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Aging.Infrastructure.Broker;
@@ -212,6 +213,192 @@
_clientLease.Release(grant, "test-end");
}
+ // ==================== 骞跺彂涓庣敓鍛藉懆鏈� ====================
+
+ /// <summary>鍗曞鎴风杩炵画 5 娆″揩閫熷彂閫侊細楠岃瘉 SessionGate 涓茶鍖栦笉涓㈠懡浠ゃ�佺绾︿笉琚閲婃斁銆�</summary>
+ [Fact]
+ public async Task SingleClient_RapidSend5Times_AllSucceed_LeaseStaysActive()
+ {
+ var grant = GrabLease();
+ var hash = DeviceConfigHash.Compute(_dataDb);
+ using var driver = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+ await RetryAsync(() => driver.ConnectAsync(CancellationToken.None));
+
+ for (int i = 0; i < 5; i++)
+ {
+ var tx = await driver.SendCommandAsync($"SEND:0x{i:X3}:AA", CancellationToken.None);
+ Assert.Contains("OK:TX", tx);
+ }
+
+ // 5 娆℃搷浣滃悗绉熺害浠� Active锛堟湭琚剰澶栭噴鏀炬垨杩囨湡锛�
+ Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+ _clientLease.Release(grant, "test-end");
+ }
+
+ /// <summary>
+ /// 涓や釜 Broker 浠g悊椹卞姩鍚屾椂鎸佹湁鍚屼竴璁惧鐨勬椿璺冪绾︼紝骞跺彂鍙戦�佸悇 5 娆°��
+ /// 鐢变簬 arb.db 浠� ResourceKey 涓轰富閿彧鍏佽涓�琛� Active锛岀浜屼釜绉熺害閫氳繃鐩存帴 SQL 鎻掑叆妯℃嫙
+ /// 锛圡VP 鏍呮爮浠呮牎楠� Phase=Active + 鏈繃鏈燂紝涓嶆瘮瀵� OwnerId锛夈��
+ /// 楠岃瘉 SessionGate(SemaphoreSlim=1) 灏嗕袱涓苟鍙戣姹備覆琛屾墽琛岋紝涓よ�呭潎鎴愬姛銆�
+ /// </summary>
+ [Fact]
+ public async Task TwoClients_ConcurrentSend_BothSucceed_SessionGateSerializes()
+ {
+ // 瀹㈡埛绔� A锛氱粡姝e父 Acquire 鑾峰彇 Production 绉熺害
+ var grantA = GrabLease();
+ var hash = DeviceConfigHash.Compute(_dataDb);
+ using var driverA = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+ await RetryAsync(() => driverA.ConnectAsync(CancellationToken.None));
+
+ // 瀹㈡埛绔� B锛氱洿鎺ュ線 arb.db 鎻掑叆涓�鏉� Maintenance Active 绉熺害琛岋紝妯℃嫙鍙︿竴涓寔鏈夎��
+ InsertDirectLeaseRow(DevId, "concurrent-client-B", OwnerTypeV3.Maintenance, ttlSeconds: 30);
+ using var driverB = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+ await RetryAsync(() => driverB.ConnectAsync(CancellationToken.None));
+
+ // 骞跺彂鍙戦�佸悇 5 娆�
+ var tasksA = Enumerable.Range(0, 5).Select(i =>
+ driverA.SendCommandAsync($"SEND:0x{i:X3}:AA", CancellationToken.None)).ToArray();
+ var tasksB = Enumerable.Range(0, 5).Select(i =>
+ driverB.SendCommandAsync($"SEND:0x{i + 0x10:X3}:BB", CancellationToken.None)).ToArray();
+
+ var results = await Task.WhenAll(tasksA.Concat(tasksB));
+ Assert.All(results, r => Assert.Contains("OK:TX", r));
+
+ // 涓や釜绉熺害琛屽潎淇濇寔 Active
+ Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+ _clientLease.Release(grantA, "test-end-A");
+ }
+
+ /// <summary>
+ /// 涓や釜 Production 瀹㈡埛绔珵浜夊悓涓�璁惧绉熺害锛氱涓�涓垚鍔熻幏鍙栵紝绗簩涓簲寰楀埌 ConflictHeld銆�
+ /// </summary>
+ [Fact]
+ public void TwoProductionClients_SecondGetsConflictHeld()
+ {
+ var grantA = GrabLease();
+ Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+
+ // 绗簩涓鎴风锛堜笉鍚� OwnerId锛屽悓 OwnerType=Production锛�
+ var secondLease = new DbLeaseStore(_arbDb, "second-client");
+ var outcome = secondLease.Acquire(new AcquireRequest
+ {
+ OwnerId = "competing-client",
+ OwnerType = OwnerTypeV3.Production,
+ ResourceKeys = new[] { DevId },
+ TtlSeconds = 30,
+ WaitTimeoutMs = 0
+ });
+
+ Assert.Equal(AcquireResultCode.ConflictHeld, outcome.Code);
+ Assert.NotNull(outcome.Holder);
+
+ _clientLease.Release(grantA, "test-end");
+ }
+
+ /// <summary>
+ /// 瀹屾暣鐢熷懡鍛ㄦ湡锛欰cquire 鈫� Connect 鈫� Send 鈫� Release 鈫� 鍐� Acquire 鈫� 鍐� Send銆�
+ /// 楠岃瘉绉熺害閲婃斁鍚庤祫婧愬洖鍒� Free锛屽彲琚噸鏂拌幏鍙栧苟缁х画浣跨敤銆�
+ /// </summary>
+ [Fact]
+ public async Task LeaseLifecycle_AcquireSendReleaseAcquireSendAgain_AllSucceed()
+ {
+ var hash = DeviceConfigHash.Compute(_dataDb);
+
+ // 绗竴杞細鑾峰彇绉熺害 鈫� 杩炴帴 鈫� 鍙戦�� 鈫� 閲婃斁
+ LeaseGrant grant1;
+ {
+ var outcome = _clientLease.Acquire(new AcquireRequest
+ {
+ OwnerId = "lifecycle-1",
+ OwnerType = OwnerTypeV3.Production,
+ ResourceKeys = new[] { DevId },
+ TtlSeconds = 30
+ });
+ Assert.Equal(AcquireResultCode.Acquired, outcome.Code);
+ grant1 = outcome.Grants[0];
+ }
+
+ using (var driver1 = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash))
+ {
+ await RetryAsync(() => driver1.ConnectAsync(CancellationToken.None));
+ var tx = await driver1.SendCommandAsync("SEND:0x100:AA", CancellationToken.None);
+ Assert.Contains("OK:TX", tx);
+ }
+
+ Assert.True(_clientLease.Release(grant1, "lifecycle-1-end"));
+ Assert.Equal(LeasePhase.Free, _clientLease.PhaseOf(DevId));
+
+ // 绗簩杞細閲嶆柊鑾峰彇 鈫� 杩炴帴 鈫� 鍙戦��
+ LeaseGrant grant2;
+ {
+ var outcome = _clientLease.Acquire(new AcquireRequest
+ {
+ OwnerId = "lifecycle-2",
+ OwnerType = OwnerTypeV3.Production,
+ ResourceKeys = new[] { DevId },
+ TtlSeconds = 30
+ });
+ Assert.Equal(AcquireResultCode.Acquired, outcome.Code);
+ grant2 = outcome.Grants[0];
+ }
+
+ using (var driver2 = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash))
+ {
+ await RetryAsync(() => driver2.ConnectAsync(CancellationToken.None));
+ var tx = await driver2.SendCommandAsync("SEND:0x200:BB", CancellationToken.None);
+ Assert.Contains("OK:TX", tx);
+ }
+
+ _clientLease.Release(grant2, "lifecycle-2-end");
+ }
+
+ /// <summary>
+ /// 骞跺彂鍘嬪姏锛氬崟瀹㈡埛绔� 20 涓� Task 鍚屾椂鍙戦�侊紝楠岃瘉 SessionGate + 绠¢亾鍐欏叆閿佷笉宕╂簝銆�
+ /// </summary>
+ [Fact]
+ public async Task SingleClient_ConcurrentSend20Tasks_AllSucceed()
+ {
+ var grant = GrabLease();
+ var hash = DeviceConfigHash.Compute(_dataDb);
+ using var driver = CanDeviceDriver.CreateBrokerProxy(DevId, "CAN_E2E:0", _pipeName, hash);
+ await RetryAsync(() => driver.ConnectAsync(CancellationToken.None));
+
+ var tasks = Enumerable.Range(0, 20).Select(i =>
+ driver.SendCommandAsync($"SEND:0x{i:X3}:DD", CancellationToken.None)).ToArray();
+
+ var results = await Task.WhenAll(tasks);
+ Assert.All(results, r => Assert.Contains("OK:TX", r));
+
+ Assert.Equal(LeasePhase.Active, _clientLease.PhaseOf(DevId));
+ _clientLease.Release(grant, "test-end");
+ }
+
+ // ==================== 杈呭姪 ====================
+
+ /// <summary>缁曡繃 Acquire 浜掓枼锛岀洿鎺ュ悜 arb.db 鎻掑叆涓�鏉� Active 绉熺害琛岋紙娴嬭瘯骞跺彂鍦烘櫙鐢級銆�</summary>
+ private void InsertDirectLeaseRow(string deviceId, string ownerId, OwnerTypeV3 ownerType, int ttlSeconds)
+ {
+ long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ using var conn = new System.Data.SQLite.SQLiteConnection(
+ $"Data Source={_arbDb};Version=3;");
+ conn.Open();
+ using var cmd = conn.CreateCommand();
+ cmd.CommandText = @"
+ INSERT OR REPLACE INTO resource_state
+ (ResourceKey, Generation, Phase, LeaseId, OwnerId, OwnerType,
+ ProcessInstanceId, MachineName, HostProcessId, Epoch, Reason,
+ AcquiredUtcMs, ExpiresUtcMs, UpdatedUtcMs)
+ VALUES (@key, 1, 1, @lid, @oid, @otype, '', 'local', 0, 1, 'direct-insert',
+ @now, @exp, @now)";
+ cmd.Parameters.AddWithValue("@key", DbLeaseStore.NormKey(deviceId));
+ cmd.Parameters.AddWithValue("@lid", "lease_" + Guid.NewGuid().ToString("N").Substring(0, 12));
+ cmd.Parameters.AddWithValue("@oid", ownerId);
+ cmd.Parameters.AddWithValue("@otype", (int)ownerType);
+ cmd.Parameters.AddWithValue("@now", now);
+ cmd.Parameters.AddWithValue("@exp", now + ttlSeconds * 1000L);
+ cmd.ExecuteNonQuery();
+ }
+
private sealed class CanFrameish { public string Id; public string Data; public string Src; }
[Fact]
--
Gitblit v1.9.1