BusinessNumberConcurrencyTests.cs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. using System.Collections.Concurrent;
  2. using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Numbering;
  3. using Xunit;
  4. using Xunit.Abstractions;
  5. namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
  6. /// <summary>
  7. /// Phase 4A 取号并发测试(同进程;InMemory provider)。
  8. /// **可现在证明**:同进程高并发下取号结果不重复、批内连续、重试有界。
  9. /// **不能声称**:多实例 + 真实 MySQL 最终唯一 / DB UNIQUE 实拒(→ Phase 4B)。
  10. /// </summary>
  11. public class BusinessNumberConcurrencyTests
  12. {
  13. private readonly ITestOutputHelper _out;
  14. public BusinessNumberConcurrencyTests(ITestOutputHelper output) => _out = output;
  15. private static InMemoryBusinessNumberProvider NewProvider()
  16. => new InMemoryBusinessNumberProvider(
  17. new[]
  18. {
  19. new BusinessNumberRule
  20. {
  21. RuleCode = "IQC_RCT", Prefix1 = "IR", DateType = "YYYYMMDD",
  22. IsDateType = true, MaxValue = 9999999, IniValue = 1,
  23. },
  24. },
  25. clock: () => new DateTime(2026, 8, 8));
  26. [Fact]
  27. public async Task NextBatch_1000Concurrent_AllUnique_NoDuplicate()
  28. {
  29. const int n = 1000;
  30. var provider = NewProvider();
  31. var bag = new ConcurrentBag<string>();
  32. var tasks = Enumerable.Range(0, n).Select(_ => Task.Run(async () =>
  33. {
  34. var b = await provider.NextBatchAsync("IQC_RCT", "8010", 1, "tester");
  35. bag.Add(b[0]);
  36. }));
  37. await Task.WhenAll(tasks);
  38. var all = bag.ToList();
  39. var unique = all.Distinct().Count();
  40. var duplicates = all.Count - unique;
  41. _out.WriteLine($"[取号并发] 总请求={all.Count} 唯一={unique} 重复={duplicates}");
  42. Assert.Equal(n, all.Count);
  43. Assert.Equal(n, unique);
  44. Assert.Equal(0, duplicates);
  45. }
  46. [Fact]
  47. public async Task NextBatch_ReservesContiguousBlock()
  48. {
  49. var provider = NewProvider();
  50. var batch = await provider.NextBatchAsync("IQC_RCT", "8010", 5, "tester");
  51. Assert.Equal(5, batch.Count);
  52. Assert.Equal(5, batch.Distinct().Count());
  53. // 批内连续:末位流水依次 +1
  54. Assert.EndsWith("0000001", batch[0]);
  55. Assert.EndsWith("0000005", batch[4]);
  56. }
  57. [Fact]
  58. public async Task NextBatch_DifferentDomain_IndependentCounters()
  59. {
  60. var provider = NewProvider();
  61. var a = (await provider.NextBatchAsync("IQC_RCT", "8010", 1, "t"))[0];
  62. var b = (await provider.NextBatchAsync("IQC_RCT", "9010", 1, "t"))[0];
  63. // 不同 domain 各自计数,均从 1 开始 → 号相同但作用域不同(说明按 domain 隔离)
  64. Assert.EndsWith("0000001", a);
  65. Assert.EndsWith("0000001", b);
  66. }
  67. [Fact]
  68. public async Task NextWithUniqueRetry_DuplicateThenSuccess_BoundedAttempts()
  69. {
  70. var svc = new BusinessNumberService(NewProvider());
  71. var calls = 0;
  72. // 前 2 次模拟撞 DB UNIQUE(返回 false),第 3 次成功
  73. var r = await svc.NextWithUniqueRetryAsync("IQC_RCT", "8010", "t",
  74. tryConsume: _ => { calls++; return Task.FromResult(calls >= 3); }, maxRetry: 5);
  75. _out.WriteLine($"[取号重试] success={r.Success} attempts={r.Attempts}");
  76. Assert.True(r.Success);
  77. Assert.Equal(3, r.Attempts);
  78. }
  79. [Fact]
  80. public async Task NextWithUniqueRetry_AllDuplicate_FailsBounded_NoInfiniteLoop()
  81. {
  82. var svc = new BusinessNumberService(NewProvider());
  83. var calls = 0;
  84. var r = await svc.NextWithUniqueRetryAsync("IQC_RCT", "8010", "t",
  85. tryConsume: _ => { calls++; return Task.FromResult(false); }, maxRetry: 5);
  86. _out.WriteLine($"[取号重试-全撞] success={r.Success} attempts={r.Attempts} calls={calls}");
  87. Assert.False(r.Success); // 有界失败,不无限循环
  88. Assert.Equal(5, r.Attempts);
  89. Assert.Equal(5, calls);
  90. }
  91. }