w4c-workflows-api/w4c-workflows-api.Tests/TriggerSchedulerTests.cs
Vitali sharp8n 42ffcb9adc workflows
2026-09-13 19:28:47 +03:00

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");
}
}