66 lines
2.5 KiB
C#
66 lines
2.5 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using w4c_workflows.Services.Triggers;
|
|
using Xunit;
|
|
|
|
namespace w4c_workflows.Tests;
|
|
|
|
/// <summary>
|
|
/// P0-2: the scheduler's <c>try/catch</c> must live <b>inside</b> the poll loop.
|
|
/// If it wrapped the loop, the first throwing tick (a transient DB error) would
|
|
/// end <c>ExecuteAsync</c> — and with <c>BackgroundServiceExceptionBehavior.Ignore</c>
|
|
/// the host would swallow it, silently stopping every cron/interval trigger for
|
|
/// the lifetime of the process.
|
|
/// </summary>
|
|
public class TriggerSchedulerTests
|
|
{
|
|
private sealed class ThrowingScopeFactory : IServiceScopeFactory
|
|
{
|
|
public IServiceScope CreateScope() => throw new InvalidOperationException("transient scope failure");
|
|
}
|
|
|
|
private sealed class NullTriggerState : ITriggerState
|
|
{
|
|
public Task<DateTimeOffset?> GetLastFireAsync(string tenantId, Guid workflowId, CancellationToken ct)
|
|
=> Task.FromResult<DateTimeOffset?>(null);
|
|
|
|
public Task SetLastFireAsync(string tenantId, Guid workflowId, DateTimeOffset at, CancellationToken ct)
|
|
=> Task.CompletedTask;
|
|
}
|
|
|
|
private static TriggerScheduler Scheduler(int pollSeconds, int warmupSeconds)
|
|
{
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Workflows:SchedulerPollSeconds"] = pollSeconds.ToString(),
|
|
["Workflows:SchedulerWarmupSeconds"] = warmupSeconds.ToString(),
|
|
})
|
|
.Build();
|
|
return new TriggerScheduler(
|
|
new ThrowingScopeFactory(),
|
|
new NullTriggerState(),
|
|
TimeProvider.System,
|
|
config,
|
|
NullLogger<TriggerScheduler>.Instance);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_throwing_tick_does_not_end_the_scheduler_loop()
|
|
{
|
|
var scheduler = Scheduler(pollSeconds: 1, warmupSeconds: 0);
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
await scheduler.StartAsync(cts.Token);
|
|
// Give the loop time to run (and survive) at least one throwing tick.
|
|
await Task.Delay(TimeSpan.FromMilliseconds(1500));
|
|
var stillRunning = scheduler.ExecuteTask is { IsCompleted: false };
|
|
|
|
cts.Cancel();
|
|
await scheduler.StopAsync(CancellationToken.None);
|
|
|
|
Assert.True(stillRunning, "the scheduler loop must keep running after a throwing tick");
|
|
}
|
|
}
|