| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- using System.Reflection;
- using MySqlConnector;
- using Admin.NET.Plugin.AiDOP.MaterialWarehouse;
- using Xunit;
- namespace Admin.NET.Plugin.AiDOP.Tests.MaterialWarehouse;
- public class InventoryMaterializationRetryTests
- {
- [Fact]
- public async Task Deadlock1213_RetriesUntilThirdAttempt()
- {
- var calls = 0;
- var ex = await Assert.ThrowsAsync<MySqlException>(() =>
- InventoryMaterializationRetry.ExecuteAsync<int>(async () =>
- {
- calls++;
- throw Mysql(1213, "deadlock");
- }));
- Assert.Equal(3, calls);
- Assert.Equal(1213, ex.Number);
- }
- [Fact]
- public async Task LockWaitTimeout1205_IsRetried()
- {
- var calls = 0;
- await InventoryMaterializationRetry.ExecuteAsync(() =>
- {
- calls++;
- if (calls < 3)
- throw Mysql(1205, "lock wait");
- return Task.FromResult(calls);
- });
- Assert.Equal(3, calls);
- }
- [Fact]
- public async Task RetryableInnerException_IsRetried()
- {
- var calls = 0;
- var result = await InventoryMaterializationRetry.ExecuteAsync(() =>
- {
- calls++;
- if (calls == 1)
- throw new InvalidOperationException("wrap",
- Mysql(1213, "deadlock"));
- return Task.FromResult(7);
- });
- Assert.Equal(7, result);
- Assert.Equal(2, calls);
- }
- [Fact]
- public async Task NonConcurrencySqlError_ThrowsImmediately()
- {
- var calls = 0;
- var ex = await Assert.ThrowsAsync<MySqlException>(() =>
- InventoryMaterializationRetry.ExecuteAsync<int>(async () =>
- {
- calls++;
- throw Mysql(1054, "unknown column");
- }));
- Assert.Equal(1, calls);
- Assert.Equal(1054, ex.Number);
- }
- [Fact]
- public async Task CancelledToken_StopsBeforeAnotherAttempt()
- {
- using var cts = new CancellationTokenSource();
- var calls = 0;
- await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
- InventoryMaterializationRetry.ExecuteAsync<int>(async () =>
- {
- calls++;
- cts.Cancel();
- throw Mysql(1213, "deadlock");
- }, cts.Token));
- Assert.Equal(1, calls);
- }
- [Fact]
- public void MaterializeCommand_IsTheOnlyRetriedStatement()
- {
- var src = File.ReadAllText(Path.Combine(RepoRoot(),
- "server", "Plugins", "Admin.NET.Plugin.AiDOP", "MaterialWarehouse", "InventoryMdpSyncService.cs"));
- Assert.Contains("InventoryMaterializationRetry.ExecuteAsync(", src, StringComparison.Ordinal);
- Assert.Equal(1, Count(src, "InventoryMaterializationRetry.ExecuteAsync("));
- var i = src.IndexOf("InventoryMaterializationRetry.ExecuteAsync(", StringComparison.Ordinal);
- var window = src[i..(i + 180)];
- Assert.Contains("ExecuteCommandAsync(sql, ps)", window, StringComparison.Ordinal);
- }
- private static MySqlException Mysql(int number, string message)
- {
- var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
- foreach (var ctor in typeof(MySqlException).GetConstructors(flags))
- {
- var ps = ctor.GetParameters();
- if (ps.Length == 2 && ps[1].ParameterType == typeof(string) && ps[0].ParameterType.IsEnum)
- return (MySqlException)ctor.Invoke([Enum.ToObject(ps[0].ParameterType, number), message]);
- }
- throw new InvalidOperationException("MySqlException has no (errorCode, message) constructor");
- }
- private static string RepoRoot()
- {
- var dir = new DirectoryInfo(AppContext.BaseDirectory);
- while (dir != null && !File.Exists(Path.Combine(dir.FullName, "AGENTS.md")))
- dir = dir.Parent;
- return dir?.FullName ?? throw new InvalidOperationException("repo root not found");
- }
- private static int Count(string src, string token)
- {
- var n = 0;
- var i = 0;
- while ((i = src.IndexOf(token, i, StringComparison.Ordinal)) >= 0)
- {
- n++;
- i += token.Length;
- }
- return n;
- }
- }
|