diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index 820ac8cda..a2ba52cce 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -4,6 +4,7 @@ on: push: branches: - 'main' + - 'bug/*' release: types: [ prereleased ] env: diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 38df82113..e9acd0c49 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -337,6 +337,7 @@ services { options.AllowClrAccess = true; options.DisableWrappers = disableVariableWrappers; + options.RegisterType(); options.ConfigureEngine(engine => { engine.Execute("function greet(name) { return `Hello ${name}!`; }"); @@ -560,9 +561,9 @@ services.AddActivityStateFilter(); // Optionally configure recurring tasks using alternative schedules. services.Configure(options => { - options.Schedule.ConfigureTask(TimeSpan.FromSeconds(30)); + options.Schedule.ConfigureTask(TimeSpan.FromSeconds(300)); + options.Schedule.ConfigureTask(TimeSpan.FromSeconds(300)); options.Schedule.ConfigureTask(TimeSpan.FromHours(4)); - options.Schedule.ConfigureTask(TimeSpan.FromSeconds(11)); }); services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10)); diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedBookmarkQueueWorker.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedBookmarkQueueWorker.cs index 12b0bd12c..3021aa4ed 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedBookmarkQueueWorker.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedBookmarkQueueWorker.cs @@ -8,7 +8,7 @@ public class DistributedBookmarkQueueWorker( IDistributedLockProvider distributedLockProvider, IBookmarkQueueSignaler signaler, IServiceScopeFactory scopeFactory, - ILogger logger) : BookmarkQueueWorker(signaler, scopeFactory) + ILogger logger) : BookmarkQueueWorker(signaler, scopeFactory, logger) { protected override async Task ProcessAsync(CancellationToken cancellationToken) { diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkQueueSignaler.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkQueueSignaler.cs index 49cb5ad1b..b592e19a3 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkQueueSignaler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IBookmarkQueueSignaler.cs @@ -2,6 +2,6 @@ namespace Elsa.Workflows.Runtime; public interface IBookmarkQueueSignaler { - Task AwaitAsync(); - void Trigger(); + Task AwaitAsync(CancellationToken cancellationToken = default); + Task TriggerAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj index a5ed65787..64647ed1b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj @@ -12,6 +12,7 @@ + diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/RateLimitedFuncExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/RateLimitedFuncExtensions.cs new file mode 100644 index 000000000..ed904e127 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/RateLimitedFuncExtensions.cs @@ -0,0 +1,31 @@ +using ThrottleDebounce; + +namespace Elsa.Workflows.Runtime; + +/// +/// Adds extension methods for and . +/// +public static class RateLimitedFuncExtensions +{ + /// + /// Invokes the specified rate limited function. + /// + public static async Task InvokeAsync(this RateLimitedFunc rateLimitedFunc) + { + var task = rateLimitedFunc.Invoke(); + + if (task != null) + await task; + } + + /// + /// Invokes the specified rate limited function. + /// + public static async Task InvokeAsync(this RateLimitedFunc rateLimitedFunc, T arg1) + { + var task = rateLimitedFunc.Invoke(arg1); + + if (task != null) + await task; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 5bf4f976d..91f0592b0 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -230,7 +230,7 @@ public class WorkflowRuntimeFeature : FeatureBase .AddScoped(WorkflowExecutionLogSink) .AddSingleton(BackgroundActivityScheduler) .AddSingleton() - .AddScoped() + .AddSingleton() .AddScoped() .AddScoped() .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/SignalBookmarkQueueWorker.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/SignalBookmarkQueueWorker.cs index de18c1c15..c89ed86ec 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/SignalBookmarkQueueWorker.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/SignalBookmarkQueueWorker.cs @@ -20,9 +20,8 @@ public class SignalBookmarkQueueWorker(IBookmarkQueueSignaler signaler) : INotif return Trigger(); } - private Task Trigger() + private async Task Trigger() { - signaler.Trigger(); - return Task.CompletedTask; + await signaler.TriggerAsync(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueProcessor.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueProcessor.cs index 1926cef76..425b4c63f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueProcessor.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueProcessor.cs @@ -3,10 +3,11 @@ using Elsa.Common.Models; using Elsa.Extensions; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.OrderDefinitions; +using Microsoft.Extensions.Logging; namespace Elsa.Workflows.Runtime; -public class BookmarkQueueProcessor(IBookmarkQueueStore store, IBookmarkResumer bookmarkResumer) : IBookmarkQueueProcessor +public class BookmarkQueueProcessor(IBookmarkQueueStore store, IBookmarkResumer bookmarkResumer, ILogger logger) : IBookmarkQueueProcessor { public async Task ProcessAsync(CancellationToken cancellationToken = default) { @@ -37,11 +38,19 @@ public class BookmarkQueueProcessor(IBookmarkQueueStore store, IBookmarkResumer { var filter = item.CreateBookmarkFilter(); var options = item.Options; + + logger.LogDebug("Processing bookmark queue item {BookmarkQueueItemId} for workflow instance {WorkflowInstanceId} for activity type {ActivityType}", item.Id, item.WorkflowInstanceId, item.ActivityTypeName); + var result = await bookmarkResumer.ResumeAsync(filter, options, cancellationToken); if (result.Matched) { + logger.LogDebug("Successfully resumed workflow instance {WorkflowInstance} using bookmark {BookmarkId} for activity type {ActivityType}", item.WorkflowInstanceId, item.BookmarkId, item.ActivityTypeName); await store.DeleteAsync(item.Id, cancellationToken); } + else + { + logger.LogDebug("No matching bookmark found for bookmark queue item {BookmarkQueueItemId} for workflow instance {WorkflowInstanceId} for activity type {ActivityType}", item.Id, item.WorkflowInstanceId, item.ActivityTypeName); + } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs index d5c0effa1..d97b76cd1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueSignaler.cs @@ -2,16 +2,42 @@ namespace Elsa.Workflows.Runtime; public class BookmarkQueueSignaler : IBookmarkQueueSignaler { - private TaskCompletionSource? _tsc; + private readonly object _lock = new(); + private TaskCompletionSource _tcs = new(); - public Task AwaitAsync() + public async Task AwaitAsync(CancellationToken cancellationToken) { - _tsc ??= new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - return _tsc.Task.ContinueWith(_ => _tsc = null); + Task waitTask; + lock (_lock) + { + // Capture the current TCS and await it + waitTask = _tcs.Task; + } + + await WaitAndResetAsync(waitTask); } - public void Trigger() + public Task TriggerAsync(CancellationToken cancellationToken) { - _tsc?.TrySetResult(); + lock (_lock) + { + // If TCS is already in a completed state, no need to set it again. + if (!_tcs.Task.IsCompleted) + { + _tcs.SetResult(null); + } + } + + return Task.CompletedTask; + } + + private async Task WaitAndResetAsync(Task waitTask) + { + await waitTask; + lock (_lock) + { + // Reset the TCS for the next wait + _tcs = new TaskCompletionSource(); + } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueWorker.cs b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueWorker.cs index 5521f333a..1d6fa8736 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueWorker.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BookmarkQueueWorker.cs @@ -1,11 +1,25 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ThrottleDebounce; namespace Elsa.Workflows.Runtime; -public class BookmarkQueueWorker(IBookmarkQueueSignaler signaler, IServiceScopeFactory scopeFactory) : IBookmarkQueueWorker +public class BookmarkQueueWorker : IBookmarkQueueWorker { + private readonly RateLimitedFunc _rateLimitedProcessAsync; private CancellationTokenSource _cts = default!; private bool _running; + private readonly IBookmarkQueueSignaler _signaler; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + public BookmarkQueueWorker(IBookmarkQueueSignaler signaler, IServiceScopeFactory scopeFactory, ILogger logger) + { + _signaler = signaler; + _scopeFactory = scopeFactory; + _logger = logger; + _rateLimitedProcessAsync = Debouncer.Debounce(ProcessAsync, TimeSpan.FromMilliseconds(500)); + } public void Start() { @@ -33,15 +47,17 @@ public class BookmarkQueueWorker(IBookmarkQueueSignaler signaler, IServiceScopeF { while (!_cts.IsCancellationRequested) { - await signaler.AwaitAsync(); - await ProcessAsync(_cts.Token); + await _signaler.AwaitAsync(_cts.Token); + await _rateLimitedProcessAsync.InvokeAsync(_cts.Token); } } protected virtual async Task ProcessAsync(CancellationToken cancellationToken) { - using var scope = scopeFactory.CreateScope(); + _logger.LogDebug("Processing bookmark queue..."); + using var scope = _scopeFactory.CreateScope(); var processor = scope.ServiceProvider.GetRequiredService(); await processor.ProcessAsync(cancellationToken); + _logger.LogDebug("Processed bookmark queue."); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/StoreBookmarkQueue.cs b/src/modules/Elsa.Workflows.Runtime/Services/StoreBookmarkQueue.cs index 4b952f36b..77b7b5d5b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/StoreBookmarkQueue.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/StoreBookmarkQueue.cs @@ -27,12 +27,12 @@ public class StoreBookmarkQueue( if (result.Matched) { - logger.LogDebug("Successfully resumed workflow instance {WorkflowInstance} using bookmark {BookmarkId}", item.WorkflowInstanceId, item.BookmarkId); + logger.LogDebug("Successfully resumed workflow instance {WorkflowInstance} using bookmark {BookmarkId} for activity type {ActivityType}", item.WorkflowInstanceId, item.BookmarkId, item.ActivityTypeName); return; } // There was no matching bookmark yet. Store the queue item for the system to pick up whenever the bookmark becomes present. - logger.LogDebug("No bookmark with ID {BookmarkId} found for workflow {WorkflowInstance}. Adding the request to the bookmark queue", item.BookmarkId, item.WorkflowInstanceId); + logger.LogDebug("No bookmark with ID {BookmarkId} found for workflow {WorkflowInstance} for activity type {ActivityType}. Adding the request to the bookmark queue", item.BookmarkId, item.WorkflowInstanceId, item.ActivityTypeName); var entity = new BookmarkQueueItem { @@ -49,6 +49,6 @@ public class StoreBookmarkQueue( await store.AddAsync(entity, cancellationToken); // Trigger the bookmark queue processor. - bookmarkQueueSignaler.Trigger(); + await bookmarkQueueSignaler.TriggerAsync(cancellationToken); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Tasks/TriggerBookmarkQueueRecurringTask.cs b/src/modules/Elsa.Workflows.Runtime/Tasks/TriggerBookmarkQueueRecurringTask.cs index 7a1cb170c..d858d6c47 100644 --- a/src/modules/Elsa.Workflows.Runtime/Tasks/TriggerBookmarkQueueRecurringTask.cs +++ b/src/modules/Elsa.Workflows.Runtime/Tasks/TriggerBookmarkQueueRecurringTask.cs @@ -23,7 +23,6 @@ public class TriggerBookmarkQueueRecurringTask(IBookmarkQueueWorker bookmarkQueu public Task ExecuteAsync(CancellationToken stoppingToken) { - signaler.Trigger(); - return Task.CompletedTask; + return signaler.TriggerAsync(stoppingToken); } } \ No newline at end of file