< Summary

Information
Class: Elsa.UserTasks.HostedServices.UserTaskDueWorker
Assembly: Elsa.UserTasks
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/HostedServices/UserTaskWorkers.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 6
Coverable lines: 6
Total lines: 124
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 2
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
get_Interval()100%210%
ExecutePassAsync()0%620%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.UserTasks/HostedServices/UserTaskWorkers.cs

#LineLine coverage
 1using Elsa.Common;
 2using Elsa.UserTasks.Contracts;
 3using Elsa.UserTasks.Models;
 4using Elsa.UserTasks.Options;
 5using Microsoft.Extensions.DependencyInjection;
 6using Microsoft.Extensions.Hosting;
 7using Microsoft.Extensions.Logging;
 8using Microsoft.Extensions.Options;
 9
 10namespace Elsa.UserTasks.HostedServices;
 11
 12/// <summary>
 13/// Shared plumbing for the User Tasks background workers: a periodic loop that never lets one failed pass
 14/// tear down the host, and a bounded scope per pass so scoped providers can be resolved safely.
 15/// </summary>
 16public abstract class UserTaskPeriodicWorker(IServiceScopeFactory scopeFactory, IOptions<UserTasksOptions> options, ILog
 17{
 18    protected UserTasksOptions Options { get; } = options.Value;
 19
 20    protected abstract TimeSpan Interval { get; }
 21
 22    protected abstract Task ExecutePassAsync(IServiceProvider services, CancellationToken cancellationToken);
 23
 24    /// <summary>The tenants a pass sweeps. Defaults to the configured default tenant when no catalog is set.</summary>
 25    protected IReadOnlyCollection<string> TenantIds => Options.WorkerTenantIds.Count > 0
 26        ? Options.WorkerTenantIds.ToArray()
 27        : [Options.DefaultTenantId];
 28
 29    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
 30    {
 31        using var timer = new PeriodicTimer(Interval);
 32        while (!stoppingToken.IsCancellationRequested)
 33        {
 34            try
 35            {
 36                using var scope = scopeFactory.CreateScope();
 37                await ExecutePassAsync(scope.ServiceProvider, stoppingToken);
 38            }
 39            catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
 40            {
 41                return;
 42            }
 43            catch (Exception exception) when (exception is not OperationCanceledException)
 44            {
 45                logger.LogWarning(exception, "A User Tasks background pass failed and will be retried");
 46            }
 47
 48            try
 49            {
 50                if (!await timer.WaitForNextTickAsync(stoppingToken))
 51                    return;
 52            }
 53            catch (OperationCanceledException)
 54            {
 55                return;
 56            }
 57        }
 58    }
 59}
 60
 61/// <summary>Marks tasks overdue and applies the reserved timeout outcome once a due date elapses.</summary>
 62public sealed class UserTaskDueWorker(IServiceScopeFactory scopeFactory, IOptions<UserTasksOptions> options, ILogger<Use
 063    : UserTaskPeriodicWorker(scopeFactory, options, logger)
 64{
 065    protected override TimeSpan Interval => Options.DueSweepInterval;
 66
 67    protected override async Task ExecutePassAsync(IServiceProvider services, CancellationToken cancellationToken)
 68    {
 069        var dueService = services.GetRequiredService<IUserTaskDueService>();
 070        foreach (var tenantId in TenantIds)
 071            await dueService.MarkOverdueAsync(tenantId, cancellationToken: cancellationToken);
 072    }
 73}
 74
 75/// <summary>Repairs projections that diverged from committed bookmarks after an interrupted write.</summary>
 76public sealed class UserTaskReconciliationWorker(IServiceScopeFactory scopeFactory, IOptions<UserTasksOptions> options, 
 77    : UserTaskPeriodicWorker(scopeFactory, options, logger)
 78{
 79    protected override TimeSpan Interval => Options.ReconciliationInterval;
 80
 81    protected override async Task ExecutePassAsync(IServiceProvider services, CancellationToken cancellationToken)
 82    {
 83        var reconciler = services.GetRequiredService<IUserTaskReconciler>();
 84        foreach (var tenantId in TenantIds)
 85            await reconciler.ReconcileAsync(new UserTaskReconciliationRequest { TenantId = tenantId }, cancellationToken
 86    }
 87}
 88
 89/// <summary>
 90/// Drains the encrypted invitation outbox. A failed dispatch is rescheduled with the configured back-off and
 91/// is abandoned once the schedule is exhausted, so an undeliverable secret always expires rather than
 92/// lingering indefinitely.
 93/// </summary>
 94public sealed class UserTaskInvitationDeliveryWorker(IServiceScopeFactory scopeFactory, IOptions<UserTasksOptions> optio
 95    : UserTaskPeriodicWorker(scopeFactory, options, logger)
 96{
 97    private readonly ILogger<UserTaskInvitationDeliveryWorker> _logger = logger;
 98
 99    protected override TimeSpan Interval => TimeSpan.FromSeconds(5);
 100
 101    protected override async Task ExecutePassAsync(IServiceProvider services, CancellationToken cancellationToken)
 102    {
 103        var outbox = services.GetRequiredService<IUserTaskInvitationOutbox>();
 104        var dispatcher = services.GetRequiredService<IUserTaskInvitationDispatcher>();
 105        var clock = services.GetRequiredService<ISystemClock>();
 106
 107        foreach (var delivery in await outbox.DequeueDueAsync(50, cancellationToken))
 108        {
 109            try
 110            {
 111                await dispatcher.DispatchAsync(delivery, cancellationToken);
 112                await outbox.CompleteAsync(delivery.Id, cancellationToken);
 113            }
 114            catch (Exception exception) when (exception is not OperationCanceledException)
 115            {
 116                var delays = Options.InvitationDeliveryRetryDelays;
 117                var delay = delivery.Attempt < delays.Count ? delays[delivery.Attempt] : delays.Count > 0 ? delays[^1] :
 118                // The exception may carry recipient details, so only the invitation ID is logged.
 119                _logger.LogWarning(exception, "Invitation delivery {InvitationId} failed on attempt {Attempt}", delivery
 120                await outbox.RescheduleAsync(delivery.Id, clock.UtcNow.Add(delay), cancellationToken);
 121            }
 122        }
 123    }
 124}