Separate quartz (#516)

* Fix sample Elsa.Samples.FileBasedWorkflow

* Separate Quartz from Elsa.Activities.Timers

* Incremental work on Hangfire integration

* Incremental work on Hangfire integration

* Incremental work on Hangfire integration

* Incremental work on Hangfire integration

* Add CancelTimer

* ToCronExpression for Duration

* Removed SweepInterval

* Synchronize access to scheduler

* Refactoring RunHangfireWorkflowJob

* Optimize timer activities to always execute when target time stamp lies in past

* Refactoring TimersOptionsExtensions

* Added common.props and configureawait.props

* Rename CancelTimer to ClearTimer

* Wrong parameter name in comment

* Refactoring TimerConsts and RunQuartzWorkflowJob

Co-authored-by: Rafael Carnucci <rafael.carnucci@icn.de>
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>
This commit is contained in:
Rafael Carnucci 2020-12-23 12:38:09 +01:00 committed by GitHub
parent 3776b651c3
commit 4274996dd9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 886 additions and 92 deletions

View file

@ -141,7 +141,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.RebusWorker",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.RunChildWorkflowWorker", "src\samples\worker\Elsa.Samples.RunChildWorkflowWorker\Elsa.Samples.RunChildWorkflowWorker.csproj", "{A4DF29BD-95CC-4A86-96EC-FB6692270E68}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.Timers", "src\samples\worker\Elsa.Samples.Timers\Elsa.Samples.Timers.csproj", "{FD2D1BD0-1229-4DCF-BE70-6BFD396DD489}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.Timers.Quartz", "src\samples\worker\Elsa.Samples.Timers\Elsa.Samples.Timers.Quartz.csproj", "{FD2D1BD0-1229-4DCF-BE70-6BFD396DD489}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.WhileLoopWorker", "src\samples\worker\Elsa.Samples.WhileLoopWorker\Elsa.Samples.WhileLoopWorker.csproj", "{EA3832C4-8079-4E84-AF8C-12744E4EFD44}"
EndProject
@ -167,6 +167,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Backend", "sr
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.WebAssembly", "src\dashboards\blazor\ElsaDashboard.WebAssembly\ElsaDashboard.WebAssembly.csproj", "{0AA2A003-C79E-4B43-803E-E1150254D2B0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "timers", "timers", "{37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Activities.Timers.Quartz", "src\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj", "{08D9CB18-2BFB-4CF2-9763-5EE913F56D3E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Activities.Timers.Hangfire", "src\activities\Elsa.Activities.Timers.Hangfire\Elsa.Activities.Timers.Hangfire.csproj", "{34FB968B-A147-4811-823D-D206FD0027AD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Timers.Hangfire", "src\samples\worker\Elsa.Samples.Timers.Hangfire\Elsa.Samples.Timers.Hangfire.csproj", "{7BDF193D-CD69-4D47-B7BF-A8A1D93EBCB9}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Activities.Entity", "src\activities\Elsa.Activities.Entity\Elsa.Activities.Entity.csproj", "{A0A8431F-3A2F-433A-BB0D-EB9627DFA739}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.EntityChanged", "src\samples\console\Elsa.Samples.EntityChanged\Elsa.Samples.EntityChanged.csproj", "{A3149FE4-FD39-4DC2-AA9F-208C7216BC03}"
@ -403,6 +410,18 @@ Global
{0AA2A003-C79E-4B43-803E-E1150254D2B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0AA2A003-C79E-4B43-803E-E1150254D2B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0AA2A003-C79E-4B43-803E-E1150254D2B0}.Release|Any CPU.Build.0 = Release|Any CPU
{08D9CB18-2BFB-4CF2-9763-5EE913F56D3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08D9CB18-2BFB-4CF2-9763-5EE913F56D3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08D9CB18-2BFB-4CF2-9763-5EE913F56D3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08D9CB18-2BFB-4CF2-9763-5EE913F56D3E}.Release|Any CPU.Build.0 = Release|Any CPU
{34FB968B-A147-4811-823D-D206FD0027AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{34FB968B-A147-4811-823D-D206FD0027AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{34FB968B-A147-4811-823D-D206FD0027AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{34FB968B-A147-4811-823D-D206FD0027AD}.Release|Any CPU.Build.0 = Release|Any CPU
{7BDF193D-CD69-4D47-B7BF-A8A1D93EBCB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BDF193D-CD69-4D47-B7BF-A8A1D93EBCB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BDF193D-CD69-4D47-B7BF-A8A1D93EBCB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BDF193D-CD69-4D47-B7BF-A8A1D93EBCB9}.Release|Any CPU.Build.0 = Release|Any CPU
{A0A8431F-3A2F-433A-BB0D-EB9627DFA739}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A0A8431F-3A2F-433A-BB0D-EB9627DFA739}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A0A8431F-3A2F-433A-BB0D-EB9627DFA739}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -435,7 +454,7 @@ Global
{1D63E1B4-2386-4BEC-9090-C3F3CBF25703} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{B43B546E-23F3-46E8-ACB7-D04F05CDA180} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925}
{D20FCB88-9DCA-49EA-9CC2-5B94DD935D30} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{E4B71DC4-3E73-49C3-9B4E-CA13909F222D} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{E4B71DC4-3E73-49C3-9B4E-CA13909F222D} = {37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA}
{C4939482-9447-47D1-B6AF-E9F8C6323841} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{5E5E1E84-DDBC-40D6-B891-0D563A15A44A} = {DA71CDAA-8DD3-4D5F-9FBD-8E4B37A2D925}
{823F80B6-E241-43F7-83BD-29FBB58A77F9} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
@ -500,6 +519,10 @@ Global
{CCF8D59E-3672-4238-A900-CE879ABF02D2} = {E02BD53A-5B08-47A0-920E-DD537A6AFAAA}
{B5EAB378-0002-47FD-A0C1-8931DB577006} = {E02BD53A-5B08-47A0-920E-DD537A6AFAAA}
{0AA2A003-C79E-4B43-803E-E1150254D2B0} = {E02BD53A-5B08-47A0-920E-DD537A6AFAAA}
{37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{08D9CB18-2BFB-4CF2-9763-5EE913F56D3E} = {37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA}
{34FB968B-A147-4811-823D-D206FD0027AD} = {37D2E628-5CC1-4C8E-8C02-3B0A29E2ACAA}
{7BDF193D-CD69-4D47-B7BF-A8A1D93EBCB9} = {E42743A0-FBDD-4150-9D53-6000496D9B87}
{A0A8431F-3A2F-433A-BB0D-EB9627DFA739} = {B43B546E-23F3-46E8-ACB7-D04F05CDA180}
{A3149FE4-FD39-4DC2-AA9F-208C7216BC03} = {FC9F520F-BA51-4AD2-BFEE-EF787798E734}
{C3842132-35BC-4F6D-85CC-7098887F4307} = {7CD5C8D5-EC78-4A99-A514-F01CA7197AC8}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>
Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application.
This package provides Hangfire timer provider.
</Description>
<PackageTags>elsa, workflows, timers, background tasks</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.18" />
<PackageReference Include="ncrontab" Version="3.3.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,34 @@
using System;
using System.Linq;
using Elsa.Activities.Timers.Hangfire.Jobs;
using Elsa.Activities.Timers.Hangfire.Models;
using Hangfire;
namespace Elsa.Activities.Timers
{
public static class IBackgroundJobClientExtensions
{
public static void ScheduleWorkflow(this IBackgroundJobClient backgroundJobClient, RunHangfireWorkflowJobModel data, DateTimeOffset dateTimeOffset)
{
backgroundJobClient.UnscheduleJobWhenAlreadyExists(data);
backgroundJobClient.Schedule<RunHangfireWorkflowJob>(job => job.ExecuteAsync(data), dateTimeOffset);
}
public static void UnscheduleJobWhenAlreadyExists(this IBackgroundJobClient backgroundJobClient, RunHangfireWorkflowJobModel data)
{
var identity = data.GetIdentity();
var monitor = JobStorage.Current.GetMonitoringApi();
var workflowJobType = typeof(RunHangfireWorkflowJob);
var jobs = monitor.ScheduledJobs(0, int.MaxValue)
.Where(x => x.Value.Job.Type == workflowJobType && ((RunHangfireWorkflowJobModel)x.Value.Job.Args[0]).GetIdentity() == identity);
foreach (var job in jobs)
{
BackgroundJob.Delete(job.Key);
}
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using Elsa.Activities.Timers.Hangfire.Services;
using Elsa.Activities.Timers.Options;
using Elsa.Activities.Timers.Services;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Activities.Timers.Hangfire.Extensions
{
public static class TimersOptionsExtensions
{
/// <summary>
/// Add Elsa Hangfire Services for background processing
/// </summary>
/// <param name="timersOptions"></param>
public static void UseHangfire(this TimersOptions timersOptions)
{
timersOptions.Services
.AddSingleton<IWorkflowScheduler, HangfireWorkflowScheduler>()
.AddSingleton<ICrontabParser, HangfireCrontabParser>();
}
/// <summary>
/// Add Elsa Hangfire Services for background processing and Hangfire Services
/// </summary>
/// <remarks>
/// Only if Hangfire is not already registered in DI
/// </remarks>
/// <param name="timersOptions"></param>
/// <param name="configure">Hangfire settings</param>
public static void UseHangfire(this TimersOptions timersOptions, Action<IGlobalConfiguration> configure)
{
timersOptions.UseHangfire();
// Add Hangfire services.
timersOptions.Services.AddHangfire(configure);
// Add the processing server as IHostedService
timersOptions.Services.AddHangfireServer();
}
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

View file

@ -0,0 +1,92 @@
using System;
using System.Threading.Tasks;
using Elsa.Activities.Timers.Hangfire.Models;
using Elsa.Activities.Timers.Services;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Services;
using Hangfire;
using Hangfire.States;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Elsa.Activities.Timers.Hangfire.Jobs
{
public class RunHangfireWorkflowJob
{
private readonly IWorkflowRunner _workflowRunner;
private readonly IWorkflowRegistry _workflowRegistry;
private readonly IWorkflowInstanceStore _workflowInstanceManager;
private readonly IServiceProvider _serviceProvider;
public RunHangfireWorkflowJob(IWorkflowRunner workflowRunner, IWorkflowRegistry workflowRegistry, IWorkflowInstanceStore workflowInstanceStore, IServiceProvider serviceProvider)
{
_workflowRunner = workflowRunner;
_workflowRegistry = workflowRegistry;
_workflowInstanceManager = workflowInstanceStore;
_serviceProvider = serviceProvider;
}
public async Task ExecuteAsync(RunHangfireWorkflowJobModel data)
{
var workflowBlueprint = (await _workflowRegistry.GetWorkflowAsync(data.WorkflowDefinitionId, data.TenantId, VersionOptions.Published))!;
if(workflowBlueprint == null)
{
return;
}
WorkflowInstance? workflowInstance = null;
if (data.WorkflowInstanceId == null)
{
if (workflowBlueprint.IsSingleton == false || await _workflowInstanceManager.GetWorkflowIsAlreadyExecutingAsync(data.TenantId, data.WorkflowDefinitionId) == false)
{
await _workflowRunner.RunWorkflowAsync(workflowBlueprint, data.ActivityId);
}
}
else
{
workflowInstance = await GetWorkflowInstanceAsync(data.WorkflowInstanceId);
if (workflowInstance == null)
{
var logger = _serviceProvider.GetRequiredService<ILogger<RunHangfireWorkflowJob>>();
logger.LogError("Could not run Workflow instance with ID {WorkflowInstanceId} because it is not in the database", data.WorkflowInstanceId);
return;
}
await _workflowRunner.RunWorkflowAsync(workflowBlueprint, workflowInstance!, data.ActivityId);
}
// If it is a RecurringJob and the instance is null, the timer activity is a start trigger.
if (data.IsRecurringJob && (workflowInstance == null || workflowInstance.Status is not (WorkflowStatus.Finished or WorkflowStatus.Cancelled)))
{
var backgroundJobClient = _serviceProvider.GetRequiredService<IBackgroundJobClient>();
var crontabParer = _serviceProvider.GetRequiredService<ICrontabParser>();
backgroundJobClient.ScheduleWorkflow(data, crontabParer.GetNextOccurrence(data.CronExpression!).ToDateTimeOffset());
}
}
private async Task<WorkflowInstance?> GetWorkflowInstanceAsync(string workflowInstanceId)
{
WorkflowInstance? workflowInstance = null;
for (var i = 0; i < TimerConsts.MaxRetrayGetWorkflow && workflowInstance == null; i++)
{
workflowInstance = await _workflowInstanceManager.GetByIdAsync(workflowInstanceId);
if (workflowInstance == null)
{
System.Threading.Thread.Sleep(10000);
}
}
return workflowInstance;
}
}
}

View file

@ -0,0 +1,27 @@
using System;
using NodaTime;
namespace Elsa.Activities.Timers.Hangfire.Models
{
public class RunHangfireWorkflowJobModel
{
public RunHangfireWorkflowJobModel(string workflowDefinitionId, string activityId, string? workflowInstanceId, string? tenantId, string? cronExpression)
{
WorkflowDefinitionId = workflowDefinitionId;
WorkflowInstanceId = workflowInstanceId;
ActivityId = activityId;
TenantId = tenantId;
CronExpression = cronExpression;
}
public string WorkflowDefinitionId { get; set; }
public string? WorkflowInstanceId { get; set; }
public string ActivityId { get; set; }
public string? TenantId { get; set; }
public string? CronExpression { get; set; }
public bool IsRecurringJob => string.IsNullOrEmpty(CronExpression) == false;
public string GetIdentity() => $"Elsa-tenant:{TenantId ?? "default"}-workflow-instance:{WorkflowInstanceId ?? WorkflowDefinitionId}-activity:{ActivityId}";
}
}

View file

@ -0,0 +1,20 @@
using System;
using Elsa.Activities.Timers.Services;
using NCrontab;
using NodaTime;
namespace Elsa.Activities.Timers.Hangfire.Services
{
public class HangfireCrontabParser : ICrontabParser
{
public Instant GetNextOccurrence(string cronExpression)
{
var schedule = CrontabSchedule.Parse(cronExpression, new CrontabSchedule.ParseOptions { IncludingSeconds = true });
return Instant.FromDateTimeUtc(schedule.GetNextOccurrence(DateTime.Now).ToUniversalTime());
}
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Timers.Hangfire.Models;
using Elsa.Activities.Timers.Services;
using Elsa.Services.Models;
using Hangfire;
using NodaTime;
namespace Elsa.Activities.Timers.Hangfire.Services
{
public class HangfireWorkflowScheduler : IWorkflowScheduler
{
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly ICrontabParser _crontabParser;
public HangfireWorkflowScheduler(IBackgroundJobClient backgroundJobClient, ICrontabParser crontabParser)
{
_backgroundJobClient = backgroundJobClient;
_crontabParser = crontabParser;
}
public Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string activityId, Instant startAt, Duration interval, CancellationToken cancellationToken = default)
{
var data = CreateData(workflowBlueprint, activityId: activityId, cronExpression: interval.ToCronExpression());
_backgroundJobClient.ScheduleWorkflow(data, startAt.ToDateTimeOffset());
return Task.CompletedTask;
}
public Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string activityId, Instant startAt, CancellationToken cancellationToken = default)
{
var data = CreateData(workflowBlueprint, activityId);
_backgroundJobClient.ScheduleWorkflow(data, startAt.ToDateTimeOffset());
return Task.CompletedTask;
}
public Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string activityId, string cronExpression, CancellationToken cancellationToken = default)
{
var data = CreateData(workflowBlueprint, activityId, cronExpression: cronExpression);
var instant = _crontabParser.GetNextOccurrence(cronExpression);
_backgroundJobClient.ScheduleWorkflow(data, instant.ToDateTimeOffset());
return Task.CompletedTask;
}
public Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string workflowInstanceId, string activityId, Instant startAt, CancellationToken cancellationToken = default)
{
var data = CreateData(workflowBlueprint, activityId, workflowInstanceId);
_backgroundJobClient.ScheduleWorkflow(data, startAt.ToDateTimeOffset());
return Task.CompletedTask;
}
public Task UnscheduleWorkflowAsync(WorkflowExecutionContext workflowExecutionContext, string activityId, CancellationToken cancellationToken = default)
{
_backgroundJobClient.UnscheduleJobWhenAlreadyExists(
CreateData(workflowExecutionContext.WorkflowBlueprint,
activityId: activityId,
workflowInstanceId: workflowExecutionContext.WorkflowInstance.WorkflowInstanceId)
);
return Task.CompletedTask;
}
private RunHangfireWorkflowJobModel CreateData(IWorkflowBlueprint workflowBlueprint, string activityId, string? workflowInstanceId = null, string? cronExpression = null) => CreateData(workflowBlueprint.Id,activityId, workflowInstanceId, workflowBlueprint.TenantId, cronExpression);
private RunHangfireWorkflowJobModel CreateData(string workflowDefinitionId, string activityId, string? workflowInstanceId = null, string? tenantId = null, string? cronExpression = null)
{
return new RunHangfireWorkflowJobModel(
workflowDefinitionId: workflowDefinitionId,
activityId: activityId,
workflowInstanceId: workflowInstanceId,
tenantId: tenantId,
cronExpression: cronExpression);
}
}
}

View file

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<Import Project="..\..\..\configureawait.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>
Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application.
This package provides Quartz timer provider.
</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Quartz" Version="3.2.3" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.2.3" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,55 @@
using System;
using Elsa.Activities.Timers.Options;
using Elsa.Activities.Timers.Quartz.Jobs;
using Elsa.Activities.Timers.Quartz.Services;
using Elsa.Activities.Timers.Services;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
namespace Elsa.Activities.Timers
{
public static class TimersOptionsExtensions
{
/// <summary>
/// Add Quartz for background processing
/// </summary>
/// <param name="timersOptions"></param>
public static void UseQuartzProvider(this TimersOptions timersOptions, Action<QuartzOptions>? configureOptions = default, Action<IServiceCollectionQuartzConfigurator>? configureQuartz = default)
{
if (configureOptions != null)
timersOptions.Services.Configure(configureOptions);
else
timersOptions.Services.AddOptions<QuartzOptions>();
timersOptions.Services.AddQuartz(configure => ConfigureQuartz(configure, configureQuartz))
.AddQuartzHostedService(ConfigureQuartzHostedService)
.AddSingleton<IWorkflowScheduler, QuartzWorkflowScheduler>()
.AddSingleton<ICrontabParser, QuartzCrontabParser>()
.AddTransient<RunQuartzWorkflowJob>();
}
private static void ConfigureQuartzHostedService(QuartzHostedServiceOptions options)
{
options.WaitForJobsToComplete = true;
}
private static void ConfigureQuartz(IServiceCollectionQuartzConfigurator quartz, Action<IServiceCollectionQuartzConfigurator>? configureQuartz)
{
quartz.UseMicrosoftDependencyInjectionScopedJobFactory(options => options.AllowDefaultConstructor = true);
quartz.AddJob<RunQuartzWorkflowJob>(job => job.StoreDurably().WithIdentity(nameof(RunQuartzWorkflowJob)));
if (configureQuartz != null)
{
configureQuartz(quartz);
}
else
{
quartz.UseSimpleTypeLoader();
quartz.UseInMemoryStore();
}
}
}
}

View file

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

View file

@ -1,4 +1,6 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Persistence;
using Elsa.Persistence.Specifications;
@ -6,16 +8,16 @@ using Elsa.Services;
using Microsoft.Extensions.Logging;
using Quartz;
namespace Elsa.Activities.Timers.Jobs
namespace Elsa.Activities.Timers.Quartz.Jobs
{
public class RunWorkflowJob : IJob
public class RunQuartzWorkflowJob : IJob
{
private readonly IWorkflowRunner _workflowRunner;
private readonly IWorkflowRegistry _workflowRegistry;
private readonly IWorkflowInstanceStore _workflowInstanceManager;
private readonly ILogger _logger;
public RunWorkflowJob(IWorkflowRunner workflowRunner, IWorkflowRegistry workflowRegistry, IWorkflowInstanceStore workflowInstanceStore, ILogger<RunWorkflowJob> logger)
public RunQuartzWorkflowJob(IWorkflowRunner workflowRunner, IWorkflowRegistry workflowRegistry, IWorkflowInstanceStore workflowInstanceStore, ILogger<RunQuartzWorkflowJob> logger)
{
_workflowRunner = workflowRunner;
_workflowRegistry = workflowRegistry;
@ -51,16 +53,29 @@ namespace Elsa.Activities.Timers.Jobs
if (workflowInstance == null)
{
_logger.LogWarning("Could not run Workflow instance with ID {WorkflowInstanceId} because it appears not yet to be persisted in the database. Rescheduling.", workflowInstanceId);
var trigger = context.Trigger;
await context.Scheduler.UnscheduleJob(trigger.Key, cancellationToken);
var newTrigger = trigger.GetTriggerBuilder().StartAt(trigger.StartTimeUtc.AddSeconds(10)).Build();
await context.Scheduler.ScheduleJob(newTrigger, cancellationToken);
_logger.LogError("Could not run Workflow instance with ID {WorkflowInstanceId} because it is not in the database", data.WorkflowInstanceId);
return;
}
await _workflowRunner.RunWorkflowAsync(workflowBlueprint, workflowInstance!, activityId, cancellationToken: cancellationToken);
}
}
private async Task<WorkflowInstance?> GetWorkflowInstanceAsync(string workflowInstanceId, CancellationToken cancellationToken)
{
WorkflowInstance? workflowInstance = null;
for (var i = 0; i < TimerConsts.MaxRetrayGetWorkflow && workflowInstance == null; i++)
{
workflowInstance = await _workflowInstanceManager.GetByIdAsync(workflowInstanceId, cancellationToken);
if (workflowInstance == null)
{
System.Threading.Thread.Sleep(10000);
}
}
return workflowInstance;
}
}
}

View file

@ -0,0 +1,25 @@
using Elsa.Activities.Timers.Services;
using NodaTime;
using Quartz;
namespace Elsa.Activities.Timers.Quartz.Services
{
public class QuartzCrontabParser : ICrontabParser
{
private readonly IClock _clock;
public QuartzCrontabParser(IClock clock)
{
_clock = clock;
}
public Instant GetNextOccurrence(string cronExpression)
{
var schedule = new CronExpression(cronExpression);
var now = _clock.GetCurrentInstant();
return Instant.FromDateTimeOffset(schedule.GetTimeAfter(now.ToDateTimeOffset())!.Value);
}
}
}

View file

@ -1,20 +1,24 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Activities.Timers.Jobs;
using Elsa.Activities.Timers.Quartz.Jobs;
using Elsa.Activities.Timers.Services;
using Elsa.Services.Models;
using NodaTime;
using Quartz;
namespace Elsa.Activities.Timers.Services
namespace Elsa.Activities.Timers.Quartz.Services
{
public class WorkflowScheduler : IWorkflowScheduler
public class QuartzWorkflowScheduler: IWorkflowScheduler
{
private static readonly string RunWorkflowJobKey = nameof(RunWorkflowJob);
private static readonly string RunWorkflowJobKey = nameof(RunQuartzWorkflowJob);
private readonly ISchedulerFactory _schedulerFactory;
private readonly SemaphoreSlim _semaphore = new(1);
public WorkflowScheduler(ISchedulerFactory schedulerFactory)
public QuartzWorkflowScheduler(ISchedulerFactory schedulerFactory)
{
_schedulerFactory = schedulerFactory;
}
@ -45,10 +49,24 @@ namespace Elsa.Activities.Timers.Services
{
var trigger = CreateTrigger(workflowBlueprint, activityId, workflowInstanceId)
.StartAt(startAt.ToDateTimeOffset()).Build();
await ScheduleJob(trigger, cancellationToken);
}
public async Task UnscheduleWorkflowAsync(WorkflowExecutionContext workflowExecutionContext, string activityId, CancellationToken cancellationToken = default)
{
var scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
var trigger = CreateTriggerKey(tenantId: workflowExecutionContext.WorkflowBlueprint.TenantId,
workflowDefinitionId: workflowExecutionContext.WorkflowBlueprint.Id,
workflowInstanceId: workflowExecutionContext.WorkflowInstance.WorkflowInstanceId,
activityId: activityId);
var existingTrigger = await scheduler.GetTrigger(trigger, cancellationToken);
if (existingTrigger != null)
await scheduler.UnscheduleJob(existingTrigger.Key, cancellationToken);
}
private async Task ScheduleJob(ITrigger trigger, CancellationToken cancellationToken)
{
await _semaphore.WaitAsync(cancellationToken);
@ -74,15 +92,19 @@ namespace Elsa.Activities.Timers.Services
private TriggerBuilder CreateTrigger(string? tenantId, string workflowDefinitionId, string? workflowInstanceId, string activityId)
{
var groupName = $"tenant:{tenantId ?? "default"}-workflow-instance:{workflowInstanceId ?? workflowDefinitionId}";
return TriggerBuilder.Create()
.ForJob(RunWorkflowJobKey)
.WithIdentity($"activity:{activityId}", groupName)
.WithIdentity(CreateTriggerKey(tenantId, workflowDefinitionId, workflowInstanceId, activityId))
.UsingJobData("TenantId", tenantId!)
.UsingJobData("WorkflowDefinitionId", workflowDefinitionId)
.UsingJobData("WorkflowInstanceId", workflowInstanceId!)
.UsingJobData("ActivityId", activityId);
}
private TriggerKey CreateTriggerKey(string? tenantId, string workflowDefinitionId, string? workflowInstanceId, string activityId)
{
var groupName = $"tenant:{tenantId ?? "default"}-workflow-instance:{workflowInstanceId ?? workflowDefinitionId}";
return new TriggerKey($"activity:{activityId}", groupName);
}
}
}
}

View file

@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Elsa.Activities.Timers.Services;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Services;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Timers
{
[Trigger(Category = "Timers", Description = "Cancel a timer (Cron, StartAt, Timer) so that it is not executed. ")]
public class ClearTimer : Activity
{
private readonly IWorkflowScheduler _workflowScheduler;
public ClearTimer(IWorkflowScheduler workflowScheduler)
{
_workflowScheduler = workflowScheduler;
}
[ActivityProperty(Hint = "The id of the timer (Cron, StartAt, Timer) activity, which is to be cleared")]
public string ActivityId { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
await _workflowScheduler.UnscheduleWorkflowAsync(context.WorkflowExecutionContext, ActivityId);
return Done();
}
}
}

View file

@ -0,0 +1,24 @@
using System;
using Elsa.Builders;
using Elsa.Services.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Timers
{
public static class ClearTimerExtensions
{
public static IActivityBuilder CancelTimer(this IBuilder builder,
Action<ISetupActivity<ClearTimer>>? setup = default) => builder.Then(setup);
public static IActivityBuilder CancelTimer(this IBuilder builder,
Func<ActivityExecutionContext, string> activityId) =>
builder.CancelTimer(setup => setup.Set(x => x.ActivityId, activityId));
public static IActivityBuilder CancelTimer(this IBuilder builder, Func<string> activityId) =>
builder.CancelTimer(setup => setup.Set(x => x.ActivityId, activityId));
public static IActivityBuilder CancelTimer(this IBuilder builder, string activityId) =>
builder.CancelTimer(setup => setup.Set(x => x.ActivityId, activityId));
}
}

View file

@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Elsa.Activities.Timers.Services;
using Elsa.ActivityResults;
@ -5,6 +6,7 @@ using Elsa.Attributes;
using Elsa.Persistence;
using Elsa.Services;
using Elsa.Services.Models;
using NodaTime;
// ReSharper disable once CheckNamespace
@ -20,12 +22,14 @@ namespace Elsa.Activities.Timers
private readonly IClock _clock;
private readonly IWorkflowInstanceStore _workflowInstanceStore;
private readonly IWorkflowScheduler _workflowScheduler;
private readonly ICrontabParser _crontabParser;
public Cron(IWorkflowInstanceStore workflowInstanceStore, IWorkflowScheduler workflowScheduler, IClock clock)
{
_clock = clock;
_workflowInstanceStore = workflowInstanceStore;
_workflowScheduler = workflowScheduler;
_crontabParser = crontabParser;
}
[ActivityProperty(Hint = "Specify a CRON expression. See https://crontab.guru/ for help.")]
@ -45,7 +49,7 @@ namespace Elsa.Activities.Timers
var cancellationToken = context.CancellationToken;
var workflowBlueprint = context.WorkflowExecutionContext.WorkflowBlueprint;
var workflowInstance = context.WorkflowExecutionContext.WorkflowInstance;
var executeAt = GetNextOccurrence(CronExpression);
var executeAt = _crontabParser.GetNextOccurrence(CronExpression);
ExecuteAt = executeAt;
@ -59,12 +63,5 @@ namespace Elsa.Activities.Timers
}
protected override IActivityExecutionResult OnResume() => Done();
private Instant GetNextOccurrence(string cronExpression)
{
var schedule = new Quartz.CronExpression(cronExpression);
var now = _clock.GetCurrentInstant();
return Instant.FromDateTimeOffset(schedule.GetTimeAfter(now.ToDateTimeOffset())!.Value);
}
}
}

View file

@ -7,6 +7,11 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Description>
Elsa is a set of workflow libraries and tools that enable lean and mean workflowing capabilities in any .NET Core application.
</Description>
<Copyright>2020</Copyright>
<PackageProjectUrl>https://github.com/elsa-workflows/elsa-core</PackageProjectUrl>
<RepositoryUrl>https://github.com/elsa-workflows/elsa-core</RepositoryUrl>
<RepositoryType>GitHub</RepositoryType>
This package provides the following Timer activities:
* CronEvent
@ -20,11 +25,4 @@
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Quartz" Version="3.2.3" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.2.3" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.2.3" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,21 @@
using NodaTime;
namespace NodaTime
{
public static class DurationExtensions
{
public static string ToCronExpression(this Duration duration)
{
static string CreateCronComponent(int number)
{
return (number > 0 ? $"*/{number}" : "* ");
}
var cron = CreateCronComponent(duration.Seconds);
cron += ' ' + CreateCronComponent(duration.Minutes);
cron += ' ' + CreateCronComponent(duration.Hours);
cron += ' ' + CreateCronComponent(duration.Days);
return cron + " * *";
}
}
}

View file

@ -1,56 +1,30 @@
using System;
using Elsa.Activities.Timers;
using Elsa.Activities.Timers.HostedServices;
using Elsa.Activities.Timers.Jobs;
using Elsa.Activities.Timers.Services;
using Elsa.Activities.Timers.Options;
using Elsa.Activities.Timers.Triggers;
using Quartz;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddTimerActivities(this IServiceCollection services, Action<QuartzOptions>? configureOptions = default, Action<IServiceCollectionQuartzConfigurator>? configureQuartz = default)
public static IServiceCollection AddTimerActivities(this IServiceCollection services,
Action<TimersOptions>? configure = default)
{
if (configureOptions != null)
services.Configure(configureOptions);
else
services.AddOptions<QuartzOptions>();
var options = new TimersOptions(services);
configure?.Invoke(options);
return services
.AddQuartz(configure => ConfigureQuartz(configure, configureQuartz))
.AddQuartzHostedService(ConfigureQuartzHostedService)
.AddSingleton<IWorkflowScheduler, WorkflowScheduler>()
.AddTransient<RunWorkflowJob>()
return services
.AddHostedService<StartJobs>()
.AddActivity<Cron>()
.AddActivity<Timer>()
.AddActivity<StartAt>()
.AddActivity<ClearTimer>()
.AddTriggerProvider<TimerTriggerProvider>()
.AddTriggerProvider<CronTriggerProvider>()
.AddTriggerProvider<StartAtTriggerProvider>();
}
private static void ConfigureQuartzHostedService(QuartzHostedServiceOptions options)
{
options.WaitForJobsToComplete = true;
}
private static void ConfigureQuartz(IServiceCollectionQuartzConfigurator quartz, Action<IServiceCollectionQuartzConfigurator>? configureQuartz)
{
quartz.UseMicrosoftDependencyInjectionScopedJobFactory(options => options.AllowDefaultConstructor = true);
quartz.AddJob<RunWorkflowJob>(job => job.StoreDurably().WithIdentity(nameof(RunWorkflowJob)));
if (configureQuartz != null)
{
configureQuartz(quartz);
}
else
{
quartz.UseSimpleTypeLoader();
quartz.UseInMemoryStore();
}
}
}
}

View file

@ -1,14 +1,17 @@
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
namespace Elsa.Activities.Timers.Options
{
public class TimersOptions
{
public TimersOptions()
public TimersOptions(IServiceCollection services)
{
SweepInterval = Duration.FromMinutes(1);
Services = services;
}
public Duration SweepInterval { get; set; }
public IServiceCollection Services { get; }
}
}

View file

@ -0,0 +1,16 @@
using NodaTime;
namespace Elsa.Activities.Timers.Services
{
/// <summary></summary>
/// <remarks>The providers can support different formats. Quartz, for example, supports years.</remarks>
public interface ICrontabParser
{
/// <summary>
/// Converts a provider dependent cron string to <see cref="Instant"/>
/// </summary>
/// <param name="cronExpression"></param>
/// <returns></returns>
Instant GetNextOccurrence(string cronExpression);
}
}

View file

@ -1,4 +1,4 @@
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Services.Models;
using NodaTime;
@ -11,5 +11,6 @@ namespace Elsa.Activities.Timers.Services
Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string activityId, Instant startAt, CancellationToken cancellationToken = default);
Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string activityId, string cronExpression, CancellationToken cancellationToken = default);
Task ScheduleWorkflowAsync(IWorkflowBlueprint workflowBlueprint, string workflowInstanceId, string activityId, Instant startAt, CancellationToken cancellationToken = default);
Task UnscheduleWorkflowAsync(WorkflowExecutionContext workflowExecutionContext, string activityId, CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Elsa.Activities.Timers
{
public static class TimerConsts
{
public const int MaxRetrayGetWorkflow = 3;
}
}

View file

@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>
</Project>

View file

@ -2,6 +2,7 @@ using Elsa.Samples.ForkJoinTimerAndSignalHttp.BackgroundTasks;
using Elsa.Samples.ForkJoinTimerAndSignalHttp.Workflows;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Elsa.Activities.Timers;
namespace Elsa.Samples.ForkJoinTimerAndSignalHttp
{
@ -15,7 +16,7 @@ namespace Elsa.Samples.ForkJoinTimerAndSignalHttp
services
.AddElsa()
.AddConsoleActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddHostedService<WorkflowStarter<DemoWorkflow>>()
.AddWorkflow<DemoWorkflow>();
}

View file

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>

View file

@ -3,6 +3,7 @@ using Elsa.Samples.Interrupts.Workflows;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Elsa.Activities.Timers;
namespace Elsa.Samples.Interrupts
{
@ -16,7 +17,7 @@ namespace Elsa.Samples.Interrupts
.AddElsa()
.AddConsoleActivities()
.AddHttpActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddActivity<Sleep>()
.StartWorkflow<InterruptableWorkflow>();
}

View file

@ -11,6 +11,8 @@
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.AzureServiceBus\Elsa.Activities.AzureServiceBus.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
</ItemGroup>

View file

@ -1,5 +1,5 @@
using Elsa.Activities.AzureServiceBus.Extensions;
using Elsa.Persistence.InMemory;
using Elsa.Activities.Timers;
using Elsa.Persistence.YesSql.Extensions;
using Elsa.Samples.AzureServiceBusWorker.Workflows;
using Microsoft.Extensions.Configuration;
@ -22,7 +22,7 @@ namespace Elsa.Samples.AzureServiceBusWorker
services
.AddElsa(options => options.UseYesSqlPersistence())
.AddConsoleActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddAzureServiceBusActivities(options => options.ConnectionString = hostContext.Configuration.GetConnectionString("AzureServiceBus"))
.AddWorkflow<ProducerWorkflow>()
.AddWorkflow<ConsumerWorkflow>();

View file

@ -7,11 +7,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj"/>
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj"/>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>
</Project>

View file

@ -2,6 +2,7 @@ using Elsa.Samples.CustomAttributesChildWorker.Messages;
using Elsa.Samples.CustomAttributesChildWorker.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Elsa.Activities.Timers;
namespace Elsa.Samples.CustomAttributesChildWorker
{
@ -19,7 +20,7 @@ namespace Elsa.Samples.CustomAttributesChildWorker
{
services
.AddElsa()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddConsoleActivities()
.AddRebusActivities<OrderReceived>()
.AddWorkflow<GenerateOrdersWorkflow>()

View file

@ -7,10 +7,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\providers\Elsa.DistributedLocking.AzureBlob\Elsa.DistributedLocking.AzureBlob.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj" />
<ProjectReference Include="..\..\..\providers\Elsa.DistributedLocking.Redis\Elsa.DistributedLocking.Redis.csproj" />
<ProjectReference Include="..\..\..\providers\Elsa.DistributedLocking.SqlServer\Elsa.DistributedLocking.SqlServer.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj"/>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers\Elsa.Activities.Timers.csproj"/>
<ProjectReference Include="..\..\..\locking\Elsa.DistributedLocking.AzureBlob\Elsa.DistributedLocking.AzureBlob.csproj"/>

View file

@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading.Tasks;
using Elsa.Activities.Timers;
namespace Elsa.Samples.DistributedLock
{
@ -20,7 +21,7 @@ namespace Elsa.Samples.DistributedLock
services
.AddElsa(options => options.UseRedisLockProvider("localhost:6379,abortConnect=false"))
.AddConsoleActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddWorkflow<RecurringWorkflow>();
});
}

View file

@ -6,11 +6,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj"/>
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj"/>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>
</Project>

View file

@ -2,6 +2,7 @@ using Elsa.Samples.MultiTenantChildWorker.Messages;
using Elsa.Samples.MultiTenantChildWorker.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Elsa.Activities.Timers;
namespace Elsa.Samples.MultiTenantChildWorker
{
@ -19,7 +20,7 @@ namespace Elsa.Samples.MultiTenantChildWorker
{
services
.AddElsa()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddConsoleActivities()
.AddRebusActivities<OrderReceived>()
.AddWorkflow<GenerateOrdersWorkflow>()

View file

@ -7,11 +7,13 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Rebus.AzureServiceBus" Version="8.0.0-a1" />
<PackageReference Include="Rebus.AzureServiceBus" Version="8.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Rebus\Elsa.Activities.Rebus.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\servicebus\Elsa.Rebus.AzureServiceBus\Elsa.Rebus.AzureServiceBus.csproj" />
<ProjectReference Include="..\..\..\servicebus\Elsa.Rebus.RabbitMq\Elsa.Rebus.RabbitMq.csproj" />

View file

@ -2,6 +2,7 @@ using Elsa.Samples.RebusWorker.Messages;
using Elsa.Samples.RebusWorker.Workflows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Elsa.Activities.Timers;
namespace Elsa.Samples.RebusWorker
{
@ -16,7 +17,7 @@ namespace Elsa.Samples.RebusWorker
services
.AddElsa()
.AddConsoleActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddRebusActivities<Greeting>()
.AddWorkflow<ProducerWorkflow>()
.AddWorkflow<ConsumerWorkflow>();

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using Elsa.Activities.Console;
using Elsa.Activities.ControlFlow;
using Elsa.Activities.Timers;
using Elsa.Builders;
using NodaTime;
namespace Elsa.Samples.Timers
{
public class CancelTimerWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder workflow)
{
workflow
.StartAt(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5)))
.WriteLine("CancelTimerWorkflow is executed")
.Then<Fork>(
activity => activity.Set(x => x.Branches, new HashSet<string>(new[] { "Branch 1", "Branch 2" })),
fork =>
{
fork.When("Branch 1").Timer(Duration.FromSeconds(30)).WithId("timer-1").WriteLine("Should not be executed");
fork.When("Branch 2").Timer(Duration.FromSeconds(10))
.CancelTimer("timer-1")
.WriteLine("Timer-1 was canceled")
.Timer(Duration.FromSeconds(40))
.WriteLine("CancelTimerWorkflow finished")
.Finish();
});
}
}
}

View file

@ -0,0 +1,18 @@
using System;
using Elsa.Activities.Console;
using Elsa.Activities.ControlFlow;
using Elsa.Activities.Timers;
using Elsa.Builders;
namespace Elsa.Samples.Timers
{
public class CronTaskWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder workflow)
{
workflow
.Cron("0/30 * * * * *")
.WriteLine(() => $"CRON event at {DateTime.Now}");
}
}
}

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>Elsa.Samples.Timers</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Hangfire.SqlServer" Version="1.7.18" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Hangfire\Elsa.Activities.Timers.Hangfire.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,29 @@
using Elsa.Activities.Console;
using Elsa.Activities.Timers;
using Elsa.Builders;
using NodaTime;
namespace Elsa.Samples.Timers
{
/// <summary>
/// A workflow that executes only once in the near future.
/// </summary>
public class OneOffWorkflow : IWorkflow
{
private readonly Instant _executeAt;
public OneOffWorkflow(Instant executeAt)
{
_executeAt = executeAt;
}
public void Build(IWorkflowBuilder workflow)
{
workflow
.StartAt(_executeAt)
.WriteLine(context => $"Started at {context.GetService<IClock>().GetCurrentInstant()}. Next event happens 30 seconds from now.")
.StartIn(Duration.FromSeconds(30))
.WriteLine(context => $"Follow-up occurred at {context.GetService<IClock>().GetCurrentInstant()}.");
}
}
}

View file

@ -0,0 +1,31 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NodaTime;
using Elsa.Activities.Timers.Hangfire.Extensions;
using Hangfire;
namespace Elsa.Samples.Timers
{
internal class Program
{
private static async Task Main() => await CreateHostBuilder().UseConsoleLifetime().Build().RunAsync();
private static IHostBuilder CreateHostBuilder() =>
Host.CreateDefaultBuilder()
.ConfigureServices(
(context, services) =>
{
services
.AddElsa()
.AddConsoleActivities()
.AddTimerActivities(options =>
options.UseHangfire(configure =>
configure.UseSqlServerStorage("Server=(localdb)\\MSSQLLocalDB;Database=ElsaHangfire;Trusted_Connection=True;MultipleActiveResultSets=true")))
.AddWorkflow<RecurringTaskWorkflow>()
.AddWorkflow<CronTaskWorkflow>()
.AddWorkflow<CancelTimerWorkflow>()
.AddWorkflow(new OneOffWorkflow(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5))));
});
}
}

View file

@ -0,0 +1,27 @@
using Elsa.Activities.Console;
using Elsa.Activities.Timers;
using Elsa.Builders;
using NodaTime;
namespace Elsa.Samples.Timers
{
public class RecurringTaskWorkflow : IWorkflow
{
private readonly IClock _clock;
public RecurringTaskWorkflow(IClock clock)
{
_clock = clock;
}
public void Build(IWorkflowBuilder workflow)
{
workflow
.AsSingleton()
.Timer(Duration.FromSeconds(30))
.WriteLine(context => $"{context.WorkflowExecutionContext.WorkflowInstance.WorkflowInstanceId} triggered by timer at {_clock.GetCurrentInstant()}.")
.Timer(Duration.FromSeconds(30))
.WriteLine(context => $"{context.WorkflowExecutionContext.WorkflowInstance.WorkflowInstanceId} resumed by timer at {_clock.GetCurrentInstant()}.");
}
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using Elsa.Activities.Console;
using Elsa.Activities.ControlFlow;
using Elsa.Activities.Timers;
using Elsa.Builders;
using NodaTime;
namespace Elsa.Samples.Timers
{
public class CancelTimerWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder workflow)
{
workflow
.StartAt(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5)))
.WriteLine("CancelTimerWorkflow is executed")
.Then<Fork>(
activity => activity.Set(x => x.Branches, new HashSet<string>(new[] { "Branch 1", "Branch 2" })),
fork =>
{
fork.When("Branch 1").Timer(Duration.FromSeconds(30)).WithId("timer-1").WriteLine("Should not be executed");
fork.When("Branch 2").Timer(Duration.FromSeconds(10))
.CancelTimer("timer-1")
.WriteLine("Timer-1 was canceled")
.Timer(Duration.FromSeconds(40))
.WriteLine("CancelTimerWorkflow finished")
.Finish();
});
}
}
}

View file

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>Elsa.Samples.Timers</RootNamespace>
</PropertyGroup>
<ItemGroup>
@ -11,6 +12,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
</ItemGroup>

View file

@ -3,6 +3,7 @@ using Elsa.Persistence.YesSql.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NodaTime;
using Elsa.Activities.Timers;
namespace Elsa.Samples.Timers
{
@ -18,8 +19,9 @@ namespace Elsa.Samples.Timers
services
.AddElsa(options => options.UseYesSqlPersistence())
.AddConsoleActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddWorkflow<RecurringTaskWorkflow>()
.AddWorkflow<CancelTimerWorkflow>()
//.AddWorkflow<CronTaskWorkflow>()
//.AddWorkflow(new OneOffWorkflow(SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromSeconds(5))))
;

View file

@ -10,6 +10,8 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
</ItemGroup>

View file

@ -20,7 +20,7 @@ namespace Elsa.Samples.WhileLoopWorker
services
.AddElsa(options => options.UseYesSqlPersistence())
.AddConsoleActivities()
.AddTimerActivities()
.AddTimerActivities(options => options.UseQuartzProvider())
.AddSingleton<PhoneCallService>()
.AddHostedService<PhoneCallWorker>()
.AddActivity<MakePhoneCall>()