Add default implementations for job queue and scheduler

This commit is contained in:
Sipke Schoorstra 2022-08-11 12:45:07 +02:00
parent b566cad1f2
commit e942f799ec
141 changed files with 946 additions and 408 deletions

View file

@ -64,8 +64,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa", "src\bundles\Elsa\El
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Formatting", "src\common\Elsa.Formatting\Elsa.Formatting.csproj", "{60CEE728-3187-481B-B6EA-10D5284D8B55}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Jobs.Abstractions", "src\common\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj", "{2782C593-98CD-4B80-B595-8BAE066DCBC3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.ServiceBus.Abstractions", "src\common\Elsa.ServiceBus.Abstractions\Elsa.ServiceBus.Abstractions.csproj", "{448FCDE6-DE44-4E44-A1CC-B9AA51471121}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Liquid", "src\modules\Elsa.Liquid\Elsa.Liquid.csproj", "{8E5BD3D9-3529-48BC-B741-CFB7A69DB233}"
@ -118,7 +116,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Api.Common", "src\comm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Tokens.Api", "src\modules\Elsa.Tokens.Api\Elsa.Tokens.Api.csproj", "{30B49E42-AE61-4F7C-981F-923F07BB3986}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Activities.Jobs", "src\modules\Elsa.Activities.Jobs\Elsa.Activities.Jobs.csproj", "{81A2AE6B-3D0C-4A4A-A80B-7A661FBC8117}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Jobs.Activities", "src\modules\Elsa.Jobs.Activities\Elsa.Jobs.Activities.csproj", "{81A2AE6B-3D0C-4A4A-A80B-7A661FBC8117}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Jobs", "src\modules\Elsa.Jobs\Elsa.Jobs.csproj", "{CF1938A0-BD10-4AA1-9ABC-6D02303A9D53}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Common", "src\modules\Elsa.Common\Elsa.Common.csproj", "{D229105F-6879-4452-9189-75DE060C0F4C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -206,10 +208,6 @@ Global
{60CEE728-3187-481B-B6EA-10D5284D8B55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60CEE728-3187-481B-B6EA-10D5284D8B55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60CEE728-3187-481B-B6EA-10D5284D8B55}.Release|Any CPU.Build.0 = Release|Any CPU
{2782C593-98CD-4B80-B595-8BAE066DCBC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2782C593-98CD-4B80-B595-8BAE066DCBC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2782C593-98CD-4B80-B595-8BAE066DCBC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2782C593-98CD-4B80-B595-8BAE066DCBC3}.Release|Any CPU.Build.0 = Release|Any CPU
{448FCDE6-DE44-4E44-A1CC-B9AA51471121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{448FCDE6-DE44-4E44-A1CC-B9AA51471121}.Debug|Any CPU.Build.0 = Debug|Any CPU
{448FCDE6-DE44-4E44-A1CC-B9AA51471121}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -318,6 +316,14 @@ Global
{81A2AE6B-3D0C-4A4A-A80B-7A661FBC8117}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81A2AE6B-3D0C-4A4A-A80B-7A661FBC8117}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81A2AE6B-3D0C-4A4A-A80B-7A661FBC8117}.Release|Any CPU.Build.0 = Release|Any CPU
{CF1938A0-BD10-4AA1-9ABC-6D02303A9D53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF1938A0-BD10-4AA1-9ABC-6D02303A9D53}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF1938A0-BD10-4AA1-9ABC-6D02303A9D53}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF1938A0-BD10-4AA1-9ABC-6D02303A9D53}.Release|Any CPU.Build.0 = Release|Any CPU
{D229105F-6879-4452-9189-75DE060C0F4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D229105F-6879-4452-9189-75DE060C0F4C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D229105F-6879-4452-9189-75DE060C0F4C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D229105F-6879-4452-9189-75DE060C0F4C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -335,7 +341,6 @@ Global
{177F4FF3-4618-4CB1-BA93-8B024BC64CD8} = {56C2FFB8-EA54-45B5-A095-4A78142EB4B5}
{F06B9573-DF68-4606-866C-A7546A10A05A} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
{60CEE728-3187-481B-B6EA-10D5284D8B55} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{2782C593-98CD-4B80-B595-8BAE066DCBC3} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{448FCDE6-DE44-4E44-A1CC-B9AA51471121} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{B2049499-D384-46DF-8837-F1180107DD54} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{FC5D2CE9-FFB0-478D-8D54-E4109D1FD202} = {F06B9573-DF68-4606-866C-A7546A10A05A}
@ -374,5 +379,7 @@ Global
{39CD855E-83B1-4A96-93F7-01608211EBE3} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{30B49E42-AE61-4F7C-981F-923F07BB3986} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{81A2AE6B-3D0C-4A4A-A80B-7A661FBC8117} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{CF1938A0-BD10-4AA1-9ABC-6D02303A9D53} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{D229105F-6879-4452-9189-75DE060C0F4C} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
EndGlobalSection
EndGlobal

View file

@ -10,10 +10,10 @@
<ItemGroup>
<ProjectReference Include="..\..\bundles\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\modules\Elsa.AspNetCore\Elsa.AspNetCore.csproj" />
<ProjectReference Include="..\..\common\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
<ProjectReference Include="..\..\modules\Elsa.ActivityDefinitions\Elsa.ActivityDefinitions.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Hangfire\Elsa.Hangfire.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Http\Elsa.Http.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Quartz\Elsa.Quartz.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Workflows.Api\Elsa.Workflows.Api.csproj" />

View file

@ -1,17 +1,14 @@
using System.Text.Json.Serialization;
using Elsa.Workflows.Api.Extensions;
using Elsa.AspNetCore.Extensions;
using Elsa.Extensions;
using Elsa.Jobs.Extensions;
using Elsa.Hangfire.Implementations;
using Elsa.Http;
using Elsa.Http.Extensions;
using Elsa.JavaScript.Activities;
using Elsa.JavaScript.Extensions;
using Elsa.Quartz.Implementations;
using Elsa.Scheduling.Activities;
using Elsa.Scheduling.Extensions;
using Elsa.Liquid.Extensions;
using Elsa.Scheduling.Extensions;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Activities.Flowchart.Activities;
@ -43,6 +40,8 @@ services
.AddActivity<Switch>()
.AddActivity<RunJavaScript>()
)
.UseJobs()
.UseScheduling()
.UseJavaScript()
.UseLiquid()
.UseHttp()
@ -50,10 +49,6 @@ services
.UseMvc()
);
services
.AddJobServices(new QuartzJobSchedulerProvider(), new HangfireJobQueueProvider())
.AddSchedulingServices();
// Register serialization configurator for configuring what types to allow to be serialized.
services.AddSingleton<ISerializationOptionsConfigurator, CustomSerializationOptionConfigurator>();
services.AddSingleton<ISerializationOptionsConfigurator, SerializationOptionsConfigurator>();

View file

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Activities.Jobs\Elsa.Activities.Jobs.csproj" />
<ProjectReference Include="..\..\modules\Elsa.Jobs.Activities\Elsa.Jobs.Activities.csproj" />
<ProjectReference Include="..\..\modules\Elsa.ActivityDefinitions.EntityFrameworkCore.Sqlite\Elsa.ActivityDefinitions.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\..\modules\Elsa.ActivityDefinitions.EntityFrameworkCore\Elsa.ActivityDefinitions.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\modules\Elsa.ActivityDefinitions\Elsa.ActivityDefinitions.csproj" />

View file

@ -1,5 +1,5 @@
using Elsa.Activities.Jobs.Features;
using Elsa.Jobs.Abstractions;
using Elsa.Jobs.Activities.Features;
using Elsa.Jobs.Models;
using Elsa.Jobs.Services;
@ -7,7 +7,7 @@ namespace Elsa.WorkflowServer.Web.Jobs;
/// <summary>
/// Jobs can be scheduled manually using <see cref="IJobQueue"/>,
/// but when enabling the <see cref="JobsFeature"/>, these jobs become available as activities too.
/// but when enabling the <see cref="JobActivitiesFeature"/>, these jobs become available as activities too.
/// </summary>
public class IndexBlockchainJob : Job
{

View file

@ -1,8 +1,5 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Elsa.Activities.Jobs.Features;
using Elsa.Activities.Jobs.Implementations;
using Elsa.Activities.Jobs.Services;
using Elsa.ActivityDefinitions.EntityFrameworkCore.Extensions;
using Elsa.ActivityDefinitions.EntityFrameworkCore.Sqlite;
using Elsa.Api.Common;
@ -11,18 +8,17 @@ using Elsa.Api.Common.Options;
using Elsa.AspNetCore.Extensions;
using Elsa.Extensions;
using Elsa.Features.Extensions;
using Elsa.Hangfire.Implementations;
using Elsa.Http;
using Elsa.Http.Extensions;
using Elsa.JavaScript.Activities;
using Elsa.JavaScript.Extensions;
using Elsa.Jobs.Extensions;
using Elsa.Jobs.Activities.Extensions;
using Elsa.Jobs.Activities.Implementations;
using Elsa.Jobs.Activities.Services;
using Elsa.Labels.EntityFrameworkCore.Extensions;
using Elsa.Labels.EntityFrameworkCore.Sqlite;
using Elsa.Labels.Extensions;
using Elsa.Liquid.Extensions;
using Elsa.Quartz.Implementations;
using Elsa.Scheduling.Extensions;
using Elsa.WorkflowContexts.Extensions;
using Elsa.Workflows.Api.Extensions;
using Elsa.Workflows.Core.Activities;
@ -35,7 +31,6 @@ using Elsa.Workflows.Persistence.EntityFrameworkCore.Extensions;
using Elsa.Workflows.Persistence.EntityFrameworkCore.Sqlite;
using Elsa.Workflows.Persistence.Extensions;
using Elsa.Workflows.Runtime.Extensions;
using Elsa.WorkflowServer.Web;
using Elsa.WorkflowServer.Web.Implementations;
using Elsa.WorkflowServer.Web.Jobs;
using FastEndpoints;
@ -80,7 +75,7 @@ services
feature.CredentialsValidator = sp => sp.GetRequiredService<CustomCredentialsValidator>();
feature.AccessTokenIssuer = sp => sp.GetRequiredService<CustomAccessTokenIssuer>();
})
.Use<JobsFeature>()
.UseJobActivities()
.UseWorkflowPersistence(p => p.UseEntityFrameworkCore(ef => ef.UseSqlite()))
.UseWorkflowApiEndpoints()
.UseJavaScript()
@ -91,11 +86,6 @@ services
.UseMvc()
);
services
.AddJobServices(new QuartzJobSchedulerProvider(), new HangfireJobQueueProvider())
.AddSchedulingServices()
;
services.AddFastEndpoints();
services.AddAuthenticationJWTBearer(accessTokenOptions.SigningKey);
services.AddHealthChecks();

View file

@ -11,7 +11,7 @@ public class Module : IModule
{
private record HostedServiceDescriptor(int Order, Type HostedServiceType);
private readonly ISet<IFeature> _configurators = new HashSet<IFeature>();
private readonly ISet<IFeature> _features = new HashSet<IFeature>();
private readonly ICollection<HostedServiceDescriptor> _hostedServiceDescriptors = new List<HostedServiceDescriptor>();
public Module(IServiceCollection services)
@ -25,10 +25,10 @@ public class Module : IModule
public T Configure<T>(Func<IModule, T> factory, Action<T>? configure = default) where T : class, IFeature
{
if (_configurators.FirstOrDefault(x => x is T) is not T configurator)
if (_features.FirstOrDefault(x => x is T) is not T configurator)
{
configurator = factory(this);
_configurators.Add(configurator);
_features.Add(configurator);
}
configure?.Invoke(configurator);
@ -45,18 +45,18 @@ public class Module : IModule
{
ResolveDependencies();
foreach (var configurator in _configurators)
foreach (var feature in _features)
{
configurator.Configure();
configurator.ConfigureHostedServices();
feature.Configure();
feature.ConfigureHostedServices();
}
foreach (var hostedServiceDescriptor in _hostedServiceDescriptors.OrderBy(x => x.Order))
Services.TryAddEnumerable(ServiceDescriptor.Singleton(typeof(IHostedService), hostedServiceDescriptor.HostedServiceType));
foreach (var configurator in _configurators)
foreach (var feature in _features)
{
configurator.Apply();
feature.Apply();
}
}
@ -64,7 +64,7 @@ public class Module : IModule
{
var resolvedDependencyTypes = new HashSet<Type>();
foreach (var configurator in _configurators.ToList())
foreach (var configurator in _features.ToList())
ResolveDependencies(configurator, resolvedDependencyTypes);
}
@ -83,13 +83,13 @@ public class Module : IModule
private IFeature AddConfigurator(Type type)
{
var configurator = _configurators.FirstOrDefault(x => x.GetType() == type);
var configurator = _features.FirstOrDefault(x => x.GetType() == type);
if (configurator != null)
return configurator;
configurator = (IFeature)Activator.CreateInstance(type, this)!;
_configurators.Add(configurator);
_features.Add(configurator);
return configurator;
}
}

View file

@ -1,2 +0,0 @@
using System;
[assembly: CLSCompliant(true)]

View file

@ -1,20 +0,0 @@
using Elsa.Jobs.Implementations;
using Elsa.Jobs.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Jobs.Extensions;
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddJobServices(this IServiceCollection services, IJobSchedulerProvider schedulerProvider, IJobQueueProvider queueProvider)
{
services
.AddSingleton<IJobSerializer, JobSerializer>()
.AddSingleton<IJobFactory, JobFactory>()
.AddSingleton<IJobRunner, JobRunner>();
schedulerProvider.ConfigureServices(services);
queueProvider.ConfigureServices(services);
return services;
}
}

View file

@ -1,8 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Jobs.Services;
public interface IJobQueueProvider
{
void ConfigureServices(IServiceCollection services);
}

View file

@ -1,8 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Jobs.Services;
public interface IJobSchedulerProvider
{
void ConfigureServices(IServiceCollection services);
}

View file

@ -1,8 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.ServiceBus.Abstractions.Services;
public interface IServiceBusProvider
{
void ConfigureServices(IServiceCollection services);
}

View file

@ -1,6 +1,7 @@
using Elsa.ActivityDefinitions.Entities;
using Elsa.ActivityDefinitions.Implementations;
using Elsa.ActivityDefinitions.Services;
using Elsa.Common.Features;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
@ -14,6 +15,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace Elsa.ActivityDefinitions.Features;
[DependsOn(typeof(MediatorFeature))]
[DependsOn(typeof(SystemClockFeature))]
public class ActivityDefinitionsFeature : FeatureBase
{
public ActivityDefinitionsFeature(IModule module) : base(module)

View file

@ -2,6 +2,7 @@ using System.Text.Json;
using Elsa.ActivityDefinitions.Entities;
using Elsa.ActivityDefinitions.Notifications;
using Elsa.ActivityDefinitions.Services;
using Elsa.Common.Services;
using Elsa.Mediator.Services;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Core.Activities.Flowchart.Activities;

View file

@ -3,7 +3,6 @@ using Elsa.AzureServiceBus.Activities;
using Elsa.AzureServiceBus.Models;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Services;
using Microsoft.Extensions.Logging;

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,19 @@
using Elsa.Common.Implementations;
using Elsa.Common.Services;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Common.Features;
public class SystemClockFeature : FeatureBase
{
public SystemClockFeature(IModule module) : base(module)
{
}
public override void Apply()
{
Services.AddSingleton<ISystemClock, SystemClock>();
}
}

View file

@ -1,6 +1,6 @@
using Elsa.Workflows.Core.Services;
using Elsa.Common.Services;
namespace Elsa.Workflows.Core.Implementations;
namespace Elsa.Common.Implementations;
public class SystemClock : ISystemClock
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Workflows.Core.Services;
namespace Elsa.Common.Services;
public interface ISystemClock
{

View file

@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
<ProjectReference Include="..\Elsa.Jobs\Elsa.Jobs.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,44 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.SqlServer;
using Newtonsoft.Json;
namespace Elsa.Hangfire.Features;
public class HangfireFeature : FeatureBase
{
public HangfireFeature(IModule module) : base(module)
{
}
public bool UseSqlServerStorage { get; set; }
public SqlServerStorageOptions? SqlServerStorageOptions { get; set; }
public string? SqlServerConnectionString { get; set; }
public Action<BackgroundJobServerOptions>? ConfigureBackgroundServerOptions { get; set; }
public override void Configure()
{
Services.AddHangfire(configuration =>
{
configuration.UseSimpleAssemblyNameTypeSerializer();
configuration.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
if (UseSqlServerStorage)
{
var storageOptions = SqlServerStorageOptions ?? new SqlServerStorageOptions();
configuration.UseSqlServerStorage(SqlServerConnectionString, storageOptions);
}
else
{
configuration.UseMemoryStorage();
}
});
if (UseSqlServerStorage)
Services.AddHangfireServer((_, options) => ConfigureBackgroundServerOptions?.Invoke(options), new SqlServerStorage(SqlServerConnectionString));
else
Services.AddHangfireServer(options => { ConfigureBackgroundServerOptions?.Invoke(options); });
}
}

View file

@ -0,0 +1,23 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Extensions;
using Elsa.Features.Services;
using Elsa.Hangfire.Implementations;
using Elsa.Jobs.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Hangfire.Features;
[DependsOn(typeof(JobsFeature))]
[DependsOn(typeof(HangfireFeature))]
public class HangfireJobsFeature : FeatureBase
{
public HangfireJobsFeature(IModule module) : base(module)
{
}
public override void Configure()
{
Module.Use<JobsFeature>(f => f.JobQueueFactory = ActivatorUtilities.GetServiceOrCreateInstance<HangfireJobQueue>);
}
}

View file

@ -1,7 +1,6 @@
using Elsa.Hangfire.Jobs;
using Elsa.Jobs.Services;
using Hangfire;
using Hangfire.Server;
using Hangfire.States;
using HangfireJob = Hangfire.Common.Job;
@ -16,11 +15,11 @@ public class HangfireJobQueue : IJobQueue
_backgroundJobClient = backgroundJobClient;
}
public Task<string> SubmitJobAsync(IJob job, string? queueName = default, CancellationToken cancellationToken = default)
public Task SubmitJobAsync(IJob job, string? queueName = default, CancellationToken cancellationToken = default)
{
var hangfireJob = HangfireJob.FromExpression<RunElsaJob>(x => x.RunAsync(job, CancellationToken.None));
var jobId = _backgroundJobClient.Create(hangfireJob, new EnqueuedState(queueName ?? "default"));
_backgroundJobClient.Create(hangfireJob, new EnqueuedState(queueName ?? "default"));
return Task.FromResult(jobId);
return Task.CompletedTask;
}
}

View file

@ -1,61 +0,0 @@
using Elsa.Jobs.Services;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.SqlServer;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
namespace Elsa.Hangfire.Implementations;
public class HangfireJobQueueProvider : IJobQueueProvider
{
public HangfireJobQueueProvider(bool registerHangfire = true)
{
RegisterHangfire = registerHangfire;
}
public HangfireJobQueueProvider(string sqlServerConnectionString)
{
SqlServerConnectionString = sqlServerConnectionString;
UseSqlServerStorage = true;
RegisterHangfire = true;
}
public bool RegisterHangfire { get; set; }
public bool UseSqlServerStorage { get; set; }
public string? SqlServerConnectionString { get; set; }
public SqlServerStorageOptions? SqlServerStorageOptions { get; set; }
public Action<BackgroundJobServerOptions>? ConfigureBackgroundServerOptions { get; set; }
public void ConfigureServices(IServiceCollection services)
{
if (RegisterHangfire)
{
services.AddHangfire(configuration =>
{
configuration.UseSimpleAssemblyNameTypeSerializer();
configuration.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
if (UseSqlServerStorage)
{
var storageOptions = SqlServerStorageOptions ?? new SqlServerStorageOptions();
configuration.UseSqlServerStorage(SqlServerConnectionString, storageOptions);
}
else
{
configuration.UseMemoryStorage();
}
});
if (UseSqlServerStorage)
services.AddHangfireServer((_, options) => ConfigureBackgroundServerOptions?.Invoke(options), new SqlServerStorage(SqlServerConnectionString));
else
services.AddHangfireServer(options =>
{
ConfigureBackgroundServerOptions?.Invoke(options);
});
}
services.AddSingleton<IJobQueue, HangfireJobQueue>();
}
}

View file

@ -1,5 +1,4 @@
using Elsa.Jobs.Services;
using Hangfire.Server;
namespace Elsa.Hangfire.Jobs;

View file

@ -1,11 +1,12 @@
using Elsa.Activities.Jobs.Models;
using Elsa.Jobs.Activities.Models;
using Elsa.Jobs.Services;
using Elsa.Workflows.Core.Models;
// ReSharper disable once CheckNamespace
namespace Elsa.Activities.Jobs.Activities;
/// <summary>
/// Executes a given job, suspending execution of the workflow until the job finishes.
/// Executes a job of a given type, suspending execution of the workflow until the job finishes.
/// </summary>
public class JobActivity : ActivityBase
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Activities.Jobs.Attributes;
namespace Elsa.Jobs.Activities.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public class JobAttribute : Attribute

View file

@ -7,14 +7,10 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
<ProjectReference Include="..\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Core\Elsa.Workflows.Core.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Management\Elsa.Workflows.Management.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Options" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,13 @@
using Elsa.Features.Services;
using Elsa.Jobs.Activities.Features;
namespace Elsa.Jobs.Activities.Extensions;
public static class ModuleExtensions
{
public static IModule UseJobActivities(this IModule module, Action<JobActivitiesFeature>? configure = default )
{
module.Configure(configure);
return module;
}
}

View file

@ -1,19 +1,21 @@
using Elsa.Activities.Jobs.Implementations;
using Elsa.Activities.Jobs.Services;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Jobs.Activities.Implementations;
using Elsa.Jobs.Activities.Services;
using Elsa.Jobs.Features;
using Elsa.Mediator.Extensions;
using Elsa.Mediator.Features;
using Elsa.Workflows.Management.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Activities.Jobs.Features;
namespace Elsa.Jobs.Activities.Features;
[DependsOn(typeof(MediatorFeature))]
public class JobsFeature : FeatureBase
[DependsOn(typeof(JobsFeature))]
public class JobActivitiesFeature : FeatureBase
{
public JobsFeature(IModule module) : base(module)
public JobActivitiesFeature(IModule module) : base(module)
{
}

View file

@ -1,10 +1,10 @@
using Elsa.Activities.Jobs.Helpers;
using Elsa.Activities.Jobs.Models;
using Elsa.Jobs.Activities.Helpers;
using Elsa.Jobs.Activities.Models;
using Elsa.Jobs.Notifications;
using Elsa.Mediator.Services;
using Elsa.Workflows.Runtime.Services;
namespace Elsa.Activities.Jobs.Handlers;
namespace Elsa.Jobs.Activities.Handlers;
public class JobExecutedHandler : INotificationHandler<JobExecuted>
{

View file

@ -1,10 +1,8 @@
using System.Reflection;
using Elsa.Activities.Jobs.Attributes;
using Elsa.Jobs.Activities.Attributes;
using Elsa.Jobs.Services;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Services;
namespace Elsa.Activities.Jobs.Helpers;
namespace Elsa.Jobs.Activities.Helpers;
public static class JobTypeNameHelper
{

View file

@ -1,9 +1,9 @@
using System.ComponentModel;
using System.Reflection;
using Elsa.Activities.Jobs.Activities;
using Elsa.Activities.Jobs.Attributes;
using Elsa.Activities.Jobs.Helpers;
using Elsa.Activities.Jobs.Services;
using Elsa.Jobs.Activities.Attributes;
using Elsa.Jobs.Activities.Helpers;
using Elsa.Jobs.Activities.Services;
using Elsa.Jobs.Services;
using Elsa.Workflows.Core.Helpers;
using Elsa.Workflows.Core.Models;
@ -11,7 +11,7 @@ using Elsa.Workflows.Management.Extensions;
using Elsa.Workflows.Management.Services;
using Humanizer;
namespace Elsa.Activities.Jobs.Implementations;
namespace Elsa.Jobs.Activities.Implementations;
/// <summary>
/// Provides activity descriptors based on registered <see cref="IJob"/> implementations.

View file

@ -1,6 +1,6 @@
using Elsa.Activities.Jobs.Services;
using Elsa.Jobs.Activities.Services;
namespace Elsa.Activities.Jobs.Implementations;
namespace Elsa.Jobs.Activities.Implementations;
public class JobRegistry : IJobRegistry
{

View file

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Elsa.Activities.Jobs.Models;
namespace Elsa.Jobs.Activities.Models;
public record EnqueuedJobPayload
{

View file

@ -1,4 +1,4 @@
namespace Elsa.Activities.Jobs.Services;
namespace Elsa.Jobs.Activities.Services;
/// <summary>
/// Represents a registry of jobs.

View file

@ -4,6 +4,9 @@ using Elsa.Jobs.Services;
namespace Elsa.Jobs.Abstractions;
/// <summary>
/// A base class for job implementations.
/// </summary>
public abstract class Job : IJob
{
public string Id { get; set; } = default!;

View file

@ -8,11 +8,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cronos" Version="0.7.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Elsa.Mediator\Elsa.Mediator.csproj" />
<ProjectReference Include="..\Elsa.Common\Elsa.Common.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,26 @@
using System;
using System.Threading.Channels;
using Elsa.Features.Services;
using Elsa.Jobs.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Jobs.Extensions;
public static class ModuleExtensions
{
public static IModule UseJobs(this IModule module, Action<JobsFeature>? configure = default )
{
module.Configure(configure);
return module;
}
public static IServiceCollection CreateChannel<T>(this IServiceCollection services) =>
services
.AddSingleton(CreateChannel<T>())
.AddSingleton(CreateChannelReader<T>)
.AddSingleton(CreateChannelWriter<T>);
private static Channel<T> CreateChannel<T>() => Channel.CreateUnbounded<T>(new UnboundedChannelOptions());
private static ChannelReader<T> CreateChannelReader<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Reader;
private static ChannelWriter<T> CreateChannelWriter<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Writer;
}

View file

@ -0,0 +1,44 @@
using System;
using Elsa.Common.Features;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Jobs.Extensions;
using Elsa.Jobs.HostedServices;
using Elsa.Jobs.Implementations;
using Elsa.Jobs.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Jobs.Features;
[DependsOn(typeof(SystemClockFeature))]
public class JobsFeature : FeatureBase
{
public JobsFeature(IModule module) : base(module)
{
}
public Func<IServiceProvider, IJobQueue> JobQueueFactory { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<LocalJobQueue>;
public Func<IServiceProvider, IJobScheduler> JobSchedulerFactory { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<LocalJobScheduler>;
public override void Configure()
{
Services
.AddSingleton<IJobSerializer, JobSerializer>()
.AddSingleton<IJobFactory, JobFactory>()
.AddSingleton<IJobRunner, JobRunner>();
}
public override void ConfigureHostedServices()
{
Services.AddHostedService<JobQueueHostedService>();
}
public override void Apply()
{
Services
.AddSingleton(JobQueueFactory)
.AddSingleton(JobSchedulerFactory)
.CreateChannel<IJob>();
}
}

View file

@ -0,0 +1,41 @@
using System;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Elsa.Jobs.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Elsa.Jobs.HostedServices;
/// <summary>
/// Continuously reads from a channel to which jobs can be sent, executing each received job.
/// </summary>
public class JobQueueHostedService : BackgroundService
{
private readonly ChannelReader<IJob> _channelReader;
private readonly IJobRunner _jobRunner;
private readonly ILogger _logger;
public JobQueueHostedService(ChannelReader<IJob> channelReader, IJobRunner jobRunner, ILogger<JobQueueHostedService> logger)
{
_channelReader = channelReader;
_jobRunner = jobRunner;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var job in _channelReader.ReadAllAsync(cancellationToken))
{
try
{
await _jobRunner.RunJobAsync(job, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while running a job");
}
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using Elsa.Jobs.Services;
namespace Elsa.Jobs.Services;
namespace Elsa.Jobs.Implementations;
public class JobFactory : IJobFactory
{

View file

@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Elsa.Jobs.Services;
namespace Elsa.Jobs.Implementations;
/// <summary>
/// Represents a local, in-memory queue of jobs that will be processed in-process.
/// </summary>
public class LocalJobQueue : IJobQueue
{
private readonly ChannelWriter<IJob> _channelWriter;
public LocalJobQueue(ChannelWriter<IJob> channelWriter)
{
_channelWriter = channelWriter;
}
public async Task SubmitJobAsync(IJob job, string? queueName = default, CancellationToken cancellationToken = default)
{
await _channelWriter.WriteAsync(job, cancellationToken);
}
}

View file

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Common.Services;
using Elsa.Jobs.ScheduledJobs;
using Elsa.Jobs.Schedules;
using Elsa.Jobs.Services;
namespace Elsa.Jobs.Implementations;
public class LocalJobScheduler : IJobScheduler
{
private readonly ISystemClock _systemClock;
private readonly IJobRunner _jobRunner;
private readonly IDictionary<string, IScheduledJob> _scheduledJobs = new Dictionary<string, IScheduledJob>();
public LocalJobScheduler(ISystemClock systemClock, IJobRunner jobRunner)
{
_systemClock = systemClock;
_jobRunner = jobRunner;
}
public Task ScheduleAsync(IJob job, string name, ISchedule schedule, string[]? groupKeys = default, CancellationToken cancellationToken = default)
{
switch (schedule)
{
case CronSchedule cronSchedule:
{
var scheduledJob = new ScheduledCronJob(name, job, _systemClock, cronSchedule.CronExpression, _jobRunner, cancellationToken);
RegisterScheduledJob(scheduledJob);
break;
}
case RecurringSchedule recurringSchedule:
{
var scheduledJob = new ScheduledRecurringJob(name, job, _systemClock, recurringSchedule.StartAt, recurringSchedule.Interval, _jobRunner, cancellationToken);
RegisterScheduledJob(scheduledJob);
break;
}
case SpecificInstantSchedule specificInstantSchedule:
{
var scheduledJob = new ScheduledSpecificInstantJob(name, job, _systemClock, specificInstantSchedule.DateTime, _jobRunner, cancellationToken);
RegisterScheduledJob(scheduledJob);
break;
}
default:
throw new NotSupportedException($"Schedule of type {schedule.GetType()} is not supported.");
}
return Task.CompletedTask;
}
private void RegisterScheduledJob(IScheduledJob scheduledJob)
{
if (_scheduledJobs.TryGetValue(scheduledJob.Name, out var existingScheduledJob)) existingScheduledJob.Cancel();
_scheduledJobs[scheduledJob.Name] = scheduledJob;
}
public Task UnscheduleAsync(string name, CancellationToken cancellationToken = default)
{
if (_scheduledJobs.TryGetValue(name, out var existingScheduledJob)) existingScheduledJob.Cancel();
return Task.CompletedTask;
}
public Task ClearAsync(string[]? groupKeys = default, CancellationToken cancellationToken = default)
{
foreach (var scheduledJob in _scheduledJobs.Values) scheduledJob.Cancel();
_scheduledJobs.Clear();
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,59 @@
using System.Threading;
using Cronos;
using Elsa.Common.Services;
using Elsa.Jobs.Services;
using Timer = System.Timers.Timer;
namespace Elsa.Jobs.ScheduledJobs;
public class ScheduledCronJob : IScheduledJob
{
private readonly IJob _job;
private readonly ISystemClock _systemClock;
private readonly IJobRunner _jobRunner;
private readonly CronExpression _parsedCronExpression;
private readonly CancellationToken _cancellationToken;
private Timer? _timer;
public ScheduledCronJob(string name, IJob job, ISystemClock systemClock, string cronExpression, IJobRunner jobRunner, CancellationToken cancellationToken)
{
Name = name;
_job = job;
_systemClock = systemClock;
_parsedCronExpression = CronExpression.Parse(cronExpression);
_jobRunner = jobRunner;
_cancellationToken = cancellationToken;
Schedule();
}
public string Name { get; set; }
public void Cancel() => _timer?.Dispose();
private void Schedule()
{
var now = _systemClock.UtcNow;
var parsedCronExpression = _parsedCronExpression;
var nextOccurence = parsedCronExpression.GetNextOccurrence(now.UtcDateTime)!;
var delay = nextOccurence.Value - now;
if (delay.Milliseconds <= 0)
{
Schedule();
return;
}
_timer = new Timer(delay.TotalMilliseconds);
_timer.Elapsed += async (_, _) =>
{
_timer.Dispose();
_timer = null;
if (!_cancellationToken.IsCancellationRequested) await _jobRunner.RunJobAsync(_job, _cancellationToken);
if (!_cancellationToken.IsCancellationRequested) Schedule();
};
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Threading;
using Elsa.Common.Services;
using Elsa.Jobs.Services;
using Timer = System.Timers.Timer;
namespace Elsa.Jobs.ScheduledJobs;
public class ScheduledRecurringJob : IScheduledJob
{
private readonly IJob _job;
private readonly ISystemClock _systemClock;
private readonly TimeSpan _interval;
private readonly IJobRunner _jobRunner;
private readonly CancellationToken _cancellationToken;
private DateTimeOffset _startAt;
private Timer? _timer;
public ScheduledRecurringJob(string name, IJob job, ISystemClock systemClock, DateTimeOffset startAt, TimeSpan interval, IJobRunner jobRunner, CancellationToken cancellationToken)
{
Name = name;
_job = job;
_systemClock = systemClock;
_startAt = startAt;
_interval = interval;
_jobRunner = jobRunner;
_cancellationToken = cancellationToken;
Schedule();
}
public string Name { get; set; }
public void Cancel() => _timer?.Dispose();
private void Schedule()
{
var now = _systemClock.UtcNow;
var delay = now - _startAt;
if (delay.Milliseconds <= 0)
{
Schedule();
return;
}
_timer = new Timer(delay.TotalMilliseconds);
_timer.Elapsed += async (_, _) =>
{
_timer.Dispose();
_timer = null;
_startAt = now + _interval;
if (!_cancellationToken.IsCancellationRequested) await _jobRunner.RunJobAsync(_job, _cancellationToken);
if (!_cancellationToken.IsCancellationRequested) Schedule();
};
}
}

View file

@ -0,0 +1,51 @@
using System;
using System.Threading;
using Elsa.Common.Services;
using Elsa.Jobs.Services;
using Timer = System.Timers.Timer;
namespace Elsa.Jobs.ScheduledJobs;
public class ScheduledSpecificInstantJob : IScheduledJob
{
private readonly IJob _job;
private readonly ISystemClock _systemClock;
private readonly DateTimeOffset _startAt;
private readonly IJobRunner _jobRunner;
private readonly CancellationToken _cancellationToken;
private Timer? _timer;
public ScheduledSpecificInstantJob(string name, IJob job, ISystemClock systemClock, DateTimeOffset startAt, IJobRunner jobRunner, CancellationToken cancellationToken)
{
_job = job;
_systemClock = systemClock;
_startAt = startAt;
_jobRunner = jobRunner;
_cancellationToken = cancellationToken;
Name = name;
Schedule();
}
public string Name { get; set; }
public void Cancel() => _timer?.Dispose();
private void Schedule()
{
var now = _systemClock.UtcNow;
var delay = now - _startAt;
if (delay.Milliseconds <= 0)
delay = TimeSpan.Zero;
_timer = new Timer(delay.TotalMilliseconds);
_timer.Elapsed += async (_, _) =>
{
_timer.Dispose();
_timer = null;
if (!_cancellationToken.IsCancellationRequested) await _jobRunner.RunJobAsync(_job, _cancellationToken);
};
}
}

View file

@ -3,6 +3,9 @@ using Elsa.Jobs.Models;
namespace Elsa.Jobs.Services;
/// <summary>
/// Implemented by types that represent a background job.
/// </summary>
public interface IJob
{
string Id { get; set; }

View file

@ -3,9 +3,12 @@ using System;
namespace Elsa.Jobs.Services;
/// <summary>
/// Instantiates new jobs of a given type.
/// Instantiates jobs of a given type.
/// </summary>
public interface IJobFactory
{
/// <summary>
/// Instantiates a job of the specified type.
/// </summary>
IJob Create(Type jobType);
}

View file

@ -8,5 +8,5 @@ namespace Elsa.Jobs.Services;
/// </summary>
public interface IJobQueue
{
Task<string> SubmitJobAsync(IJob job, string? queueName = default, CancellationToken cancellationToken = default);
Task SubmitJobAsync(IJob job, string? queueName = default, CancellationToken cancellationToken = default);
}

View file

@ -3,7 +3,13 @@ using System.Threading.Tasks;
namespace Elsa.Jobs.Services;
/// <summary>
/// Runs specified jobs.
/// </summary>
public interface IJobRunner
{
/// <summary>
/// Runs the specified job.
/// </summary>
Task RunJobAsync(IJob job, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,7 @@
namespace Elsa.Jobs.Services;
internal interface IScheduledJob
{
string Name { get; set; }
void Cancel();
}

View file

@ -1,13 +0,0 @@
using Elsa.MassTransit.Implementations;
using Elsa.Workflows.Runtime.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.MassTransit.Extensions;
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddMassTransitServices(this IServiceCollection services)
{
return services.AddSingleton<IWorkflowDispatcher, MassTransitWorkflowDispatcher>();
}
}

View file

@ -0,0 +1,13 @@
using Elsa.Features.Services;
using Elsa.MassTransit.Features;
namespace Elsa.MassTransit.Extensions;
public static class ModuleExtensions
{
public static IModule AddMassTransitWorkflowDispatchers(this IModule module)
{
module.Configure<MassTransitDispatchersFeature>();
return module;
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.MassTransit.Implementations;
using Elsa.Workflows.Runtime.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.MassTransit.Features;
[DependsOn(typeof(WorkflowRuntimeFeature))]
[DependsOn(typeof(MassTransitServiceBusFeature))]
public class MassTransitDispatchersFeature : FeatureBase
{
public MassTransitDispatchersFeature(IModule module) : base(module)
{
}
public override void Configure()
{
Module.Configure<WorkflowRuntimeFeature>(f => f.WorkflowDispatcherFactory = ActivatorUtilities.GetServiceOrCreateInstance<MassTransitWorkflowDispatcher>);
}
}

View file

@ -0,0 +1,19 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.MassTransit.Implementations;
using Elsa.ServiceBus.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.MassTransit.Features;
public class MassTransitServiceBusFeature : FeatureBase
{
public MassTransitServiceBusFeature(IModule module) : base(module)
{
}
public override void Apply()
{
Services.AddSingleton<IServiceBus, MassTransitServiceBus>();
}
}

View file

@ -1,16 +0,0 @@
using Elsa.ServiceBus.Abstractions.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.MassTransit.Implementations;
/// <summary>
/// A MassTransit implementation for <see cref="IServiceBus"/>.
/// </summary>
public class MassTransitServiceBusProvider : IServiceBusProvider
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton<IServiceBus, MassTransitServiceBus>();
}
}

View file

@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
</ItemGroup>
<ItemGroup>

View file

@ -1,4 +1,5 @@
using System.Reflection;
using System.Threading.Channels;
using Elsa.Mediator.Implementations;
using Elsa.Mediator.Middleware.Command;
using Elsa.Mediator.Middleware.Command.Contracts;
@ -10,6 +11,7 @@ using Elsa.Mediator.Models;
using Elsa.Mediator.Services;
using Elsa.Features.Services;
using Elsa.Mediator.Features;
using Elsa.Mediator.HostedServices;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Mediator.Extensions;
@ -29,9 +31,16 @@ public static class DependencyInjectionExtensions
.AddSingleton<IRequestSender>(sp => sp.GetRequiredService<IMediator>())
.AddSingleton<ICommandSender>(sp => sp.GetRequiredService<IMediator>())
.AddSingleton<IEventPublisher>(sp => sp.GetRequiredService<IMediator>())
.AddSingleton<IBackgroundCommandSender, BackgroundCommandSender>()
.AddSingleton<IBackgroundEventPublisher, BackgroundEventPublisher>()
.AddSingleton<IRequestPipeline, RequestPipeline>()
.AddSingleton<ICommandPipeline, CommandPipeline>()
.AddSingleton<INotificationPipeline, NotificationPipeline>();
.AddSingleton<INotificationPipeline, NotificationPipeline>()
.AddHostedService<BackgroundCommandSenderHostedService>()
.AddHostedService<BackgroundEventPublisherHostedService>()
.CreateChannel<ICommand>()
.CreateChannel<INotification>()
;
}
public static IServiceCollection AddCommandHandler<THandler, TCommand>(this IServiceCollection services)
@ -76,6 +85,12 @@ public static class DependencyInjectionExtensions
public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services, Type markerType) => services.AddHandlersFromInternal<ICommandHandler>(markerType);
public static IServiceCollection AddCommandHandlersFrom(this IServiceCollection services, Assembly assembly) => services.AddHandlersFromInternal<ICommandHandler>(assembly);
public static IServiceCollection CreateChannel<T>(this IServiceCollection services) =>
services
.AddSingleton(CreateChannel<T>())
.AddSingleton(CreateChannelReader<T>)
.AddSingleton(CreateChannelWriter<T>);
private static IServiceCollection AddHandlersFromInternal<TService, TMarker>(this IServiceCollection services) => services.AddHandlersFromInternal<TService>(typeof(TMarker));
private static IServiceCollection AddHandlersFromInternal<TService>(this IServiceCollection services, Type assemblyMarkerType) => services.AddHandlersFromInternal<TService>(assemblyMarkerType.Assembly);
@ -90,4 +105,8 @@ public static class DependencyInjectionExtensions
return services;
}
private static Channel<T> CreateChannel<T>() => Channel.CreateUnbounded<T>(new UnboundedChannelOptions());
private static ChannelReader<T> CreateChannelReader<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Reader;
private static ChannelWriter<T> CreateChannelWriter<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Writer;
}

View file

@ -0,0 +1,38 @@
using System.Threading.Channels;
using Elsa.Mediator.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Elsa.Mediator.HostedServices;
/// <summary>
/// Continuously reads from a channel to which commands can be sent, executing each received command.
/// </summary>
public class BackgroundCommandSenderHostedService : BackgroundService
{
private readonly ChannelReader<ICommand> _channelReader;
private readonly ICommandSender _commandSender;
private readonly ILogger _logger;
public BackgroundCommandSenderHostedService(ChannelReader<ICommand> channelReader, ICommandSender commandSender, ILogger<BackgroundCommandSenderHostedService> logger)
{
_channelReader = channelReader;
_commandSender = commandSender;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var request in _channelReader.ReadAllAsync(cancellationToken))
{
try
{
await _commandSender.ExecuteAsync(request, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while processing the queue");
}
}
}
}

View file

@ -0,0 +1,38 @@
using System.Threading.Channels;
using Elsa.Mediator.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Elsa.Mediator.HostedServices;
/// <summary>
/// Continuously reads from a channel to which notifications can be sent, publishing each received notification.
/// </summary>
public class BackgroundEventPublisherHostedService : BackgroundService
{
private readonly ChannelReader<INotification> _channelReader;
private readonly IEventPublisher _eventPublisher;
private readonly ILogger _logger;
public BackgroundEventPublisherHostedService(ChannelReader<INotification> channelReader, IEventPublisher eventPublisher, ILogger<BackgroundEventPublisherHostedService> logger)
{
_channelReader = channelReader;
_eventPublisher = eventPublisher;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var notification in _channelReader.ReadAllAsync(cancellationToken))
{
try
{
await _eventPublisher.PublishAsync(notification, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while processing the queue");
}
}
}
}

View file

@ -0,0 +1,11 @@
using System.Threading.Channels;
using Elsa.Mediator.Services;
namespace Elsa.Mediator.Implementations;
public class BackgroundCommandSender : IBackgroundCommandSender
{
private readonly ChannelWriter<ICommand> _channelWriter;
public BackgroundCommandSender(ChannelWriter<ICommand> channelWriter) => _channelWriter = channelWriter;
public async Task SendAsync(ICommand command, CancellationToken cancellationToken = default) => await _channelWriter.WriteAsync(command, cancellationToken);
}

View file

@ -0,0 +1,11 @@
using System.Threading.Channels;
using Elsa.Mediator.Services;
namespace Elsa.Mediator.Implementations;
public class BackgroundEventPublisher : IBackgroundEventPublisher
{
private readonly ChannelWriter<INotification> _channelWriter;
public BackgroundEventPublisher(ChannelWriter<INotification> channelWriter) => _channelWriter = channelWriter;
public async Task PublishAsync(INotification notification, CancellationToken cancellationToken = default) => await _channelWriter.WriteAsync(notification, cancellationToken);
}

View file

@ -0,0 +1,12 @@
namespace Elsa.Mediator.Services;
/// <summary>
/// Send requests to be processed asynchronously in the background.
/// </summary>
public interface IBackgroundCommandSender
{
/// <summary>
/// Submits the specified command to a channel writer. The channel is processed asynchronously from a background service.
/// </summary>
Task SendAsync(ICommand command, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,12 @@
namespace Elsa.Mediator.Services;
/// <summary>
/// Publish notifications to be processed asynchronously in the background.
/// </summary>
public interface IBackgroundEventPublisher
{
/// <summary>
/// Publish the specified notification using from a background service.
/// </summary>
Task PublishAsync(INotification notification, CancellationToken cancellationToken = default);
}

View file

@ -12,7 +12,6 @@ using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Persistence.Entities;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Elsa.Workflows.Runtime.Services;
using Proto;

View file

@ -1,6 +1,5 @@
using Elsa.Persistence.Common.Models;
using Elsa.Runtime.Protos;
using Elsa.Workflows.Persistence.Models;
using Proto.Cluster;
namespace Elsa.ProtoActor.Implementations;

View file

@ -11,7 +11,6 @@ using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Core.State;
using Elsa.Workflows.Persistence.Entities;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Services;
using Proto;

View file

@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
<ProjectReference Include="..\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\Elsa.Scheduling\Elsa.Scheduling.csproj" />
</ItemGroup>

View file

@ -0,0 +1,48 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Quartz.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
namespace Elsa.Quartz.Features;
public class QuartzFeature : FeatureBase
{
public QuartzFeature(IModule module) : base(module)
{
}
public Action<QuartzOptions>? ConfigureQuartzOptions { get; set; }
public Action<IServiceCollectionQuartzConfigurator>? ConfigureQuartz { get; set; }
public Action<QuartzHostedServiceOptions>? ConfigureQuartzHostedService { get; set; }
public override void Configure()
{
if (ConfigureQuartzOptions != null)
Services.Configure(ConfigureQuartzOptions);
Services
.AddQuartz(configure =>
{
ConfigureQuartzInternal(configure, ConfigureQuartz);
configure.AddElsaJobs();
});
}
public override void ConfigureHostedServices()
{
Services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
ConfigureQuartzHostedService?.Invoke(options);
});
}
private static void ConfigureQuartzInternal(IServiceCollectionQuartzConfigurator quartz, Action<IServiceCollectionQuartzConfigurator>? configureQuartz)
{
quartz.UseMicrosoftDependencyInjectionJobFactory();
quartz.UseSimpleTypeLoader();
quartz.UseInMemoryStore();
configureQuartz?.Invoke(quartz);
}
}

View file

@ -0,0 +1,22 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Jobs.Features;
using Elsa.Quartz.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Quartz.Features;
[DependsOn(typeof(JobsFeature))]
[DependsOn(typeof(QuartzFeature))]
public class QuartzSchedulerFeature : FeatureBase
{
public QuartzSchedulerFeature(IModule module) : base(module)
{
}
public override void Configure()
{
Module.Configure<JobsFeature>(f => f.JobSchedulerFactory = ActivatorUtilities.GetServiceOrCreateInstance<QuartzJobScheduler>);
}
}

View file

@ -87,7 +87,7 @@ public class QuartzJobScheduler : IJobScheduler
}
default:
throw new NotSupportedException($"Schedule of type {schedule.GetType()} is not supported. But if you create an issue, we'll make this logic extensible & replaceable :)");
throw new NotSupportedException($"Schedule of type {schedule.GetType()} is not supported.");
}
return builder.Build();

View file

@ -1,80 +0,0 @@
using Elsa.Jobs.Services;
using Elsa.Quartz.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Quartz;
namespace Elsa.Quartz.Implementations;
/// <summary>
/// A Quartz.NET implementation for <see cref="IJobSchedulerProvider"/>.
/// </summary>
public class QuartzJobSchedulerProvider : IJobSchedulerProvider
{
public QuartzJobSchedulerProvider(bool registerQuartz = true)
{
RegisterQuartz = registerQuartz;
}
public QuartzJobSchedulerProvider(Action<QuartzOptions> configureQuartzOptions)
{
RegisterQuartz = true;
ConfigureQuartzOptions = configureQuartzOptions;
}
public QuartzJobSchedulerProvider(
Action<QuartzOptions> configureQuartzOptions,
Action<IServiceCollectionQuartzConfigurator> configureQuartz)
{
RegisterQuartz = true;
ConfigureQuartzOptions = configureQuartzOptions;
ConfigureQuartz = configureQuartz;
}
public QuartzJobSchedulerProvider(
Action<QuartzOptions> configureQuartzOptions,
Action<IServiceCollectionQuartzConfigurator> configureQuartz,
Action<QuartzHostedServiceOptions> configureQuartzHostedService)
{
RegisterQuartz = true;
ConfigureQuartzOptions = configureQuartzOptions;
ConfigureQuartz = configureQuartz;
ConfigureQuartzHostedService = configureQuartzHostedService;
}
public bool RegisterQuartz { get; set; }
public Action<QuartzOptions>? ConfigureQuartzOptions { get; set; }
public Action<IServiceCollectionQuartzConfigurator>? ConfigureQuartz { get; set; }
public Action<QuartzHostedServiceOptions>? ConfigureQuartzHostedService { get; set; }
public void ConfigureServices(IServiceCollection services)
{
if (RegisterQuartz)
{
if (ConfigureQuartzOptions != null)
services.Configure(ConfigureQuartzOptions);
services
.AddQuartz(configure =>
{
ConfigureQuartzInternal(configure, ConfigureQuartz);
configure.AddElsaJobs();
})
.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
ConfigureQuartzHostedService?.Invoke(options);
});
}
services.AddSingleton<IJobScheduler, QuartzJobScheduler>();
}
private static void ConfigureQuartzInternal(IServiceCollectionQuartzConfigurator quartz, Action<IServiceCollectionQuartzConfigurator>? configureQuartz)
{
quartz.UseMicrosoftDependencyInjectionJobFactory();
quartz.UseSimpleTypeLoader();
quartz.UseInMemoryStore();
configureQuartz?.Invoke(quartz);
}
}

View file

@ -1,4 +1,5 @@
using System;
using Elsa.Common.Services;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;

View file

@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Elsa.Common.Services;
using Elsa.Expressions.Models;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;

View file

@ -1,5 +1,6 @@
using System;
using System.Text.Json.Serialization;
using Elsa.Common.Services;
using Elsa.Workflows.Core;
using Elsa.Workflows.Core.Attributes;
using Elsa.Workflows.Core.Models;

View file

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\common\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
<ProjectReference Include="..\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\Elsa.Workflows.Runtime\Elsa.Workflows.Runtime.csproj" />
</ItemGroup>

View file

@ -1,20 +0,0 @@
using Elsa.Mediator.Extensions;
using Elsa.Scheduling.Implementations;
using Elsa.Scheduling.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Scheduling.Extensions;
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddSchedulingServices(this IServiceCollection services)
{
services
.AddSingleton<IWorkflowTriggerScheduler, WorkflowTriggerScheduler>()
.AddSingleton<IWorkflowBookmarkScheduler, WorkflowBookmarkScheduler>()
.AddNotificationHandlersFrom<HostedServices.ScheduleWorkflows>()
.AddHostedService<HostedServices.ScheduleWorkflows>();
return services;
}
}

View file

@ -0,0 +1,14 @@
using System;
using Elsa.Features.Services;
using Elsa.Scheduling.Features;
namespace Elsa.Scheduling.Extensions;
public static class ModuleExtensions
{
public static IModule UseScheduling(this IModule module, Action<SchedulingFeature>? configure = default )
{
module.Configure(configure);
return module;
}
}

View file

@ -0,0 +1,32 @@
using Elsa.Common.Features;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Mediator.Extensions;
using Elsa.Scheduling.HostedServices;
using Elsa.Scheduling.Implementations;
using Elsa.Scheduling.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Scheduling.Features;
[DependsOn(typeof(SystemClockFeature))]
public class SchedulingFeature : FeatureBase
{
public SchedulingFeature(IModule module) : base(module)
{
}
public override void ConfigureHostedServices()
{
Services.AddHostedService<ScheduleWorkflowsHostedService>();
}
public override void Apply()
{
Services
.AddSingleton<IWorkflowTriggerScheduler, WorkflowTriggerScheduler>()
.AddSingleton<IWorkflowBookmarkScheduler, WorkflowBookmarkScheduler>()
.AddNotificationHandlersFrom<ScheduleWorkflowsHostedService>();
}
}

View file

@ -13,12 +13,12 @@ namespace Elsa.Scheduling.HostedServices;
/// <summary>
/// Loads all timer-specific workflow bookmarks from the database and create scheduled jobs for them.
/// </summary>
public class ScheduleWorkflows : BackgroundService
public class ScheduleWorkflowsHostedService : BackgroundService
{
private readonly IWorkflowBookmarkStore _bookmarkStore;
private readonly IWorkflowBookmarkScheduler _workflowBookmarkScheduler;
public ScheduleWorkflows(IWorkflowBookmarkStore bookmarkStore, IWorkflowBookmarkScheduler workflowBookmarkScheduler)
public ScheduleWorkflowsHostedService(IWorkflowBookmarkStore bookmarkStore, IWorkflowBookmarkScheduler workflowBookmarkScheduler)
{
_bookmarkStore = bookmarkStore;
_workflowBookmarkScheduler = workflowBookmarkScheduler;

View file

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Elsa.Jobs.Abstractions;
using Elsa.Jobs.Models;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Services;

View file

@ -1,7 +1,6 @@
using System.Net;
using System.Threading.Tasks;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Services;
using Microsoft.AspNetCore.Http;

View file

@ -2,7 +2,6 @@ using System.Threading;
using System.Threading.Tasks;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Services;
using Microsoft.AspNetCore.Http;

View file

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Elsa.AspNetCore.Attributes;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Management.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -5,7 +5,6 @@ using Elsa.AspNetCore.Attributes;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -5,7 +5,6 @@ using Elsa.AspNetCore.Attributes;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Management.Services;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -3,7 +3,6 @@ using System.Threading.Tasks;
using Elsa.AspNetCore.Attributes;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Api.ApiResults;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Elsa.Workflows.Runtime.Models;
using Microsoft.AspNetCore.Http;

View file

@ -4,7 +4,6 @@ using Elsa.AspNetCore.Attributes;
using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Api.ApiResults;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

View file

@ -8,7 +8,6 @@ using Elsa.Persistence.Common.Models;
using Elsa.Workflows.Api.Models;
using Elsa.Workflows.Core.Serialization;
using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Persistence.Models;
using Elsa.Workflows.Persistence.Services;
using Elsa.Workflows.Runtime.Services;
using Humanizer;

Some files were not shown because too many files have changed in this diff Show more