89 lines
3.9 KiB
C#
89 lines
3.9 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.GalaxyRepository;
|
|
|
|
namespace ZB.MOM.WW.GalaxyRepository.Tests;
|
|
|
|
/// <summary>
|
|
/// Server-005 regression: the initial <c>RefreshAsync</c> call in
|
|
/// <see cref="GalaxyHierarchyRefreshService"/> must not let a transient,
|
|
/// non-cancellation first-load failure (e.g. a <see cref="TimeoutException"/>
|
|
/// or <see cref="System.ComponentModel.Win32Exception"/> from connection
|
|
/// establishment) escape and fault the host <c>BackgroundService</c>.
|
|
/// </summary>
|
|
public sealed class GalaxyHierarchyRefreshServiceTests
|
|
{
|
|
/// <summary>Verifies that the background service does not fault when the first refresh throws a non-cancellation exception.</summary>
|
|
[Fact]
|
|
public async Task ExecuteAsync_WhenFirstRefreshThrowsNonCancellationException_DoesNotFaultBackgroundService()
|
|
{
|
|
ThrowingCache cache = new(new TimeoutException("connection establishment timed out"));
|
|
GalaxyHierarchyRefreshService service = CreateService(cache);
|
|
|
|
using CancellationTokenSource cts = new();
|
|
|
|
await service.StartAsync(cts.Token);
|
|
|
|
// Wait until the first RefreshAsync has actually been attempted (and
|
|
// thrown) before cancelling, so cancellation cannot race ahead of the
|
|
// first-load path under test — this is what made the test flaky under
|
|
// parallel load.
|
|
await cache.FirstRefreshAttempted.WaitAsync(TimeSpan.FromSeconds(10));
|
|
|
|
await cts.CancelAsync();
|
|
|
|
// The background loop must have stopped cleanly: ExecuteTask reaches a
|
|
// terminal state that is not Faulted (RanToCompletion or Canceled)
|
|
// rather than faulting on the first refresh. WhenAny is used so a
|
|
// Canceled task does not rethrow before the IsFaulted assertion.
|
|
Task? executeTask = service.ExecuteTask;
|
|
Assert.NotNull(executeTask);
|
|
Task completed = await Task.WhenAny(executeTask, Task.Delay(TimeSpan.FromSeconds(10)));
|
|
Assert.Same(executeTask, completed);
|
|
Assert.False(executeTask.IsFaulted);
|
|
Assert.Equal(1, cache.RefreshCallCount);
|
|
|
|
await service.StopAsync(CancellationToken.None);
|
|
}
|
|
|
|
private static GalaxyHierarchyRefreshService CreateService(IGalaxyHierarchyCache cache)
|
|
{
|
|
GalaxyRepositoryOptions options = new()
|
|
{
|
|
DashboardRefreshIntervalSeconds = 3600,
|
|
};
|
|
return new GalaxyHierarchyRefreshService(
|
|
cache,
|
|
Options.Create(options),
|
|
NullLogger<GalaxyHierarchyRefreshService>.Instance);
|
|
}
|
|
|
|
private sealed class ThrowingCache(Exception toThrow) : IGalaxyHierarchyCache
|
|
{
|
|
private readonly TaskCompletionSource firstRefreshAttempted =
|
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
/// <summary>Gets the number of refresh calls.</summary>
|
|
public int RefreshCallCount { get; private set; }
|
|
|
|
/// <summary>Gets a task that completes once refresh has been invoked at least once.</summary>
|
|
public Task FirstRefreshAttempted => firstRefreshAttempted.Task;
|
|
|
|
/// <summary>Gets the current cache entry.</summary>
|
|
public GalaxyHierarchyCacheEntry Current => GalaxyHierarchyCacheEntry.Empty;
|
|
|
|
/// <summary>Refreshes the cache asynchronously and throws the configured exception.</summary>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
public Task RefreshAsync(CancellationToken cancellationToken)
|
|
{
|
|
RefreshCallCount++;
|
|
firstRefreshAttempted.TrySetResult();
|
|
throw toThrow;
|
|
}
|
|
|
|
/// <summary>Waits for the first load and completes immediately.</summary>
|
|
/// <param name="cancellationToken">Token to observe for cancellation.</param>
|
|
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
|
}
|
|
}
|