InventoryMaterializationRetryTests.cs 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using System.Reflection;
  2. using MySqlConnector;
  3. using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  4. using Xunit;
  5. namespace Admin.NET.Plugin.AiDOP.Tests.MaterialWarehouse;
  6. public class InventoryMaterializationRetryTests
  7. {
  8. [Fact]
  9. public async Task Deadlock1213_RetriesUntilThirdAttempt()
  10. {
  11. var calls = 0;
  12. var ex = await Assert.ThrowsAsync<MySqlException>(() =>
  13. InventoryMaterializationRetry.ExecuteAsync<int>(async () =>
  14. {
  15. calls++;
  16. throw Mysql(1213, "deadlock");
  17. }));
  18. Assert.Equal(3, calls);
  19. Assert.Equal(1213, ex.Number);
  20. }
  21. [Fact]
  22. public async Task LockWaitTimeout1205_IsRetried()
  23. {
  24. var calls = 0;
  25. await InventoryMaterializationRetry.ExecuteAsync(() =>
  26. {
  27. calls++;
  28. if (calls < 3)
  29. throw Mysql(1205, "lock wait");
  30. return Task.FromResult(calls);
  31. });
  32. Assert.Equal(3, calls);
  33. }
  34. [Fact]
  35. public async Task RetryableInnerException_IsRetried()
  36. {
  37. var calls = 0;
  38. var result = await InventoryMaterializationRetry.ExecuteAsync(() =>
  39. {
  40. calls++;
  41. if (calls == 1)
  42. throw new InvalidOperationException("wrap",
  43. Mysql(1213, "deadlock"));
  44. return Task.FromResult(7);
  45. });
  46. Assert.Equal(7, result);
  47. Assert.Equal(2, calls);
  48. }
  49. [Fact]
  50. public async Task NonConcurrencySqlError_ThrowsImmediately()
  51. {
  52. var calls = 0;
  53. var ex = await Assert.ThrowsAsync<MySqlException>(() =>
  54. InventoryMaterializationRetry.ExecuteAsync<int>(async () =>
  55. {
  56. calls++;
  57. throw Mysql(1054, "unknown column");
  58. }));
  59. Assert.Equal(1, calls);
  60. Assert.Equal(1054, ex.Number);
  61. }
  62. [Fact]
  63. public async Task CancelledToken_StopsBeforeAnotherAttempt()
  64. {
  65. using var cts = new CancellationTokenSource();
  66. var calls = 0;
  67. await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
  68. InventoryMaterializationRetry.ExecuteAsync<int>(async () =>
  69. {
  70. calls++;
  71. cts.Cancel();
  72. throw Mysql(1213, "deadlock");
  73. }, cts.Token));
  74. Assert.Equal(1, calls);
  75. }
  76. [Fact]
  77. public void MaterializeCommand_IsTheOnlyRetriedStatement()
  78. {
  79. var src = File.ReadAllText(Path.Combine(RepoRoot(),
  80. "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "InventoryMdpSyncService.cs"));
  81. Assert.Contains("InventoryMaterializationRetry.ExecuteAsync(", src, StringComparison.Ordinal);
  82. Assert.Equal(1, Count(src, "InventoryMaterializationRetry.ExecuteAsync("));
  83. var i = src.IndexOf("InventoryMaterializationRetry.ExecuteAsync(", StringComparison.Ordinal);
  84. var window = src[i..(i + 180)];
  85. Assert.Contains("ExecuteCommandAsync(sql, ps)", window, StringComparison.Ordinal);
  86. }
  87. private static MySqlException Mysql(int number, string message)
  88. {
  89. var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
  90. foreach (var ctor in typeof(MySqlException).GetConstructors(flags))
  91. {
  92. var ps = ctor.GetParameters();
  93. if (ps.Length == 2 && ps[1].ParameterType == typeof(string) && ps[0].ParameterType.IsEnum)
  94. return (MySqlException)ctor.Invoke([Enum.ToObject(ps[0].ParameterType, number), message]);
  95. }
  96. throw new InvalidOperationException("MySqlException has no (errorCode, message) constructor");
  97. }
  98. private static string RepoRoot()
  99. {
  100. var dir = new DirectoryInfo(AppContext.BaseDirectory);
  101. while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
  102. dir = dir.Parent;
  103. return dir?.FullName ?? throw new InvalidOperationException("repo root not found");
  104. }
  105. private static int Count(string src, string token)
  106. {
  107. var n = 0;
  108. var i = 0;
  109. while ((i = src.IndexOf(token, i, StringComparison.Ordinal)) >= 0)
  110. {
  111. n++;
  112. i += token.Length;
  113. }
  114. return n;
  115. }
  116. }