From 48c453d153c624f6fdbac0c4a625cc7b3a326d2d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sat, 11 Jan 2025 19:12:38 +0100 Subject: [PATCH] Enable customizable Hangfire job storage and deprecate obsolete APIs. Added support for configuring Hangfire job storage per database provider, including PostgreSql, SQLite, and SQL Server. Introduced new methods for flexible Hangfire setup, while marking older APIs and storage configuration extensions as obsolete. Refactored related configurations for streamlined and centralized job scheduling logic. --- Directory.Packages.props | 77 ++++++++++--------- .../Elsa.Server.Web/Elsa.Server.Web.csproj | 1 + src/apps/Elsa.Server.Web/Program.cs | 41 +++++++++- src/apps/Elsa.Server.Web/appsettings.json | 9 +-- .../Elsa.Hangfire/Elsa.Hangfire.csproj | 10 +-- .../Extensions/ModuleExtensions.cs | 2 + .../Elsa.Hangfire/Features/HangfireFeature.cs | 54 ++++++++++--- .../HangfireSqlServerStorageFeature.cs | 20 ++--- .../Features/HangfireSqliteStorageFeature.cs | 16 ++-- .../Contexts/WorkflowExecutionContext.cs | 20 ++--- 10 files changed, 150 insertions(+), 100 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index dd2e28eac..005c17183 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -49,6 +49,7 @@ + @@ -112,44 +113,44 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj index 8801d54d9..30802d6a5 100644 --- a/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj +++ b/src/apps/Elsa.Server.Web/Elsa.Server.Web.csproj @@ -58,6 +58,7 @@ + diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 7cae86cb3..d065d535b 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -49,6 +49,12 @@ using Elsa.Workflows.Runtime.Distributed.Extensions; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Stores; using Elsa.Workflows.Runtime.Tasks; +using Hangfire; +using Hangfire.MemoryStorage; +using Hangfire.PostgreSql; +using Hangfire.PostgreSql.Factories; +using Hangfire.SqlServer; +using Hangfire.Storage.SQLite; using JetBrains.Annotations; using Medallion.Threading.FileSystem; using Medallion.Threading.Postgres; @@ -78,8 +84,8 @@ const bool useSignalR = false; // Disabled until Elsa Studio sends authenticated const WorkflowRuntime workflowRuntime = WorkflowRuntime.Distributed; const DistributedCachingTransport distributedCachingTransport = DistributedCachingTransport.MassTransit; const MassTransitBroker massTransitBroker = MassTransitBroker.Memory; -const bool useMultitenancy = false; -const bool useTenantsFromConfiguration = false; +const bool useMultitenancy = true; +const bool useTenantsFromConfiguration = true; const bool useAgents = false; const bool useSecrets = false; const bool disableVariableWrappers = false; @@ -133,7 +139,36 @@ services }); if (useHangfire) - elsa.UseHangfire(); + { + JobStorage jobStorage; + if (sqlDatabaseProvider == SqlDatabaseProvider.PostgreSql) + { + jobStorage = new PostgreSqlStorage(new NpgsqlConnectionFactory(postgresConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + })); + } + else if (sqlDatabaseProvider == SqlDatabaseProvider.Sqlite) + { + jobStorage = new SQLiteStorage(sqliteConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + }); + } + else if (sqlDatabaseProvider == SqlDatabaseProvider.SqlServer) + { + jobStorage = new SqlServerStorage(sqlServerConnectionString, new() + { + QueuePollInterval = TimeSpan.FromSeconds(1) + }); + } + else + { + jobStorage = new MemoryStorage(); + } + + elsa.UseHangfire(hangfire => hangfire.UseJobStorage(jobStorage)); + } elsa .AddActivitiesFrom() diff --git a/src/apps/Elsa.Server.Web/appsettings.json b/src/apps/Elsa.Server.Web/appsettings.json index 66598c7b9..221cdcf22 100644 --- a/src/apps/Elsa.Server.Web/appsettings.json +++ b/src/apps/Elsa.Server.Web/appsettings.json @@ -2,14 +2,7 @@ "Logging": { "LogLevel": { "Default": "Warning", - "Elsa": "Warning", - "MassTransit": "Warning", - "Microsoft.Extensions.Http": "Warning", - "Microsoft.Hosting.Lifetime": "Information", - "Microsoft.EntityFrameworkCore": "Warning", - "Microsoft.AspNetCore": "Warning", - "Quartz": "Warning", - "System.Net.Http": "Warning" + "Microsoft.Hosting.Lifetime": "Information" } }, "HostBuilder": { diff --git a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj index 8b940ae22..2474602ce 100644 --- a/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj +++ b/src/modules/Elsa.Hangfire/Elsa.Hangfire.csproj @@ -8,14 +8,14 @@ - - - + + + - - + + diff --git a/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs b/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs index ab493f267..b06060b46 100644 --- a/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs +++ b/src/modules/Elsa.Hangfire/Extensions/ModuleExtensions.cs @@ -25,6 +25,7 @@ public static class ModuleExtensions /// /// Configures Hangfire to use SQL Server storage. Only use this feature if you are not configuring Hangfire yourself. /// + [Obsolete("Configure storage directly on the HangfireFeature.")] public static HangfireFeature UseSqlServerStorage(this HangfireFeature feature, Action configure) { feature.Module.Use(configure); @@ -34,6 +35,7 @@ public static class ModuleExtensions /// /// Configures Hangfire to use SQLite storage. Only use this feature if you are not configuring Hangfire yourself. /// + [Obsolete("Configure storage directly on the HangfireFeature.")] public static HangfireFeature UseSqliteStorage(this HangfireFeature feature, Action configure) { feature.Module.Use(configure); diff --git a/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs index 25c359cfe..0162ccd11 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireFeature.cs @@ -1,5 +1,7 @@ using Elsa.Features.Abstractions; +using Elsa.Features.Attributes; using Elsa.Features.Services; +using Elsa.Workflows.Runtime.Features; using Hangfire; using Hangfire.MemoryStorage; using Newtonsoft.Json; @@ -9,35 +11,63 @@ namespace Elsa.Hangfire.Features; /// /// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature. /// -public class HangfireFeature : FeatureBase +[DependsOn(typeof(WorkflowRuntimeFeature))] // Ensure that the workflow runtime feature's hosted services have executed before Hangfire Server starts. +public class HangfireFeature(IModule module) : FeatureBase(module) { - /// - public HangfireFeature(IModule module) : base(module) - { - } - /// /// A delegate that configures Hangfire. /// - public Action ConfigureHangfire { get; set; } = (_, cfg) => cfg.UseMemoryStorage(); + private Action _configureHangfire = (_, _) => { }; /// /// A delegate that configures Hangfire's background job server options. /// - public Action ConfigureBackgroundServerOptions { get; set; } = (_, _) => { }; + private Action _configureBackgroundServerOptions = (_, _) => { }; /// /// A delegate that creates a job storage instance. /// - public Func CreateJobStorage { get; set; } = () => new MemoryStorage(); + private Func _createJobStorage = () => new MemoryStorage(); + + /// + /// Configures Hangfire. + /// + public HangfireFeature ConfigureHangfire(Action configure) + { + _configureHangfire += configure; + return this; + } + + /// + /// Configures Hangfire's background job server options. + /// + public HangfireFeature ConfigureBackgroundServerOptions(Action configure) + { + _configureBackgroundServerOptions += configure; + return this; + } + + public HangfireFeature UseMemoryStorage() + { + return UseJobStorage(new MemoryStorage()); + } + + public HangfireFeature UseJobStorage(JobStorage storage) + { + _createJobStorage = () => storage; + return this; + } /// public override void Apply() { + var jobStorage = _createJobStorage(); + Action configAction = (sp, cfg) => { cfg.UseSimpleAssemblyNameTypeSerializer(); cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects); + cfg.UseStorage(jobStorage); }; Action serverOptionsAction = (sp, options) => @@ -46,10 +76,10 @@ public class HangfireFeature : FeatureBase options.SchedulePollingInterval = TimeSpan.FromSeconds(1); }; - configAction += ConfigureHangfire; - serverOptionsAction += ConfigureBackgroundServerOptions; + configAction += _configureHangfire; + serverOptionsAction += _configureBackgroundServerOptions; Services.AddHangfire(configAction); - Services.AddHangfireServer(serverOptionsAction, CreateJobStorage()); + Services.AddHangfireServer(serverOptionsAction, jobStorage); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs index 480a5a3bf..fafb92528 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireSqlServerStorageFeature.cs @@ -11,18 +11,14 @@ namespace Elsa.Hangfire.Features; /// Configures the Hangfire feature to use SQL Server storage. If you're setting up Hangfire yourself, then you should not enable this feature. /// [DependsOn(typeof(HangfireFeature))] -public class HangfireSqlServerStorageFeature : FeatureBase +[Obsolete("Configure storage directly on the HangfireFeature.")] +public class HangfireSqlServerStorageFeature(IModule module) : FeatureBase(module) { - /// - public HangfireSqlServerStorageFeature(IModule module) : base(module) - { - } - /// /// The connection string to use when connecting to SQL Server, or the name of the connection string. /// - public string NameOrConnectionString { get; set; } = default!; - + public string NameOrConnectionString { get; set; } = null!; + /// /// Configures the SQL Server storage options. /// @@ -41,13 +37,11 @@ public class HangfireSqlServerStorageFeature : FeatureBase UseRecommendedIsolationLevel = true }; ConfigureSqlServerStorageOptions(storageOptions); - - hangfireFeature.ConfigureHangfire = (_, cfg) => + + hangfireFeature.ConfigureHangfire((_, cfg) => { cfg.UseSqlServerStorage(NameOrConnectionString, storageOptions); - }; - - hangfireFeature.CreateJobStorage = () => new SqlServerStorage(NameOrConnectionString, storageOptions); + }); }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs b/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs index 7982a078b..dcd7353df 100644 --- a/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs +++ b/src/modules/Elsa.Hangfire/Features/HangfireSqliteStorageFeature.cs @@ -10,17 +10,13 @@ namespace Elsa.Hangfire.Features; /// Configures the Hangfire feature to use SQLite storage. If you're setting up Hangfire yourself, then you should not enable this feature. /// [DependsOn(typeof(HangfireFeature))] -public class HangfireSqliteStorageFeature : FeatureBase +[Obsolete("Configure storage directly on the HangfireFeature.")] +public class HangfireSqliteStorageFeature(IModule module) : FeatureBase(module) { - /// - public HangfireSqliteStorageFeature(IModule module) : base(module) - { - } - /// /// The connection string to use when connecting to SQL Server, or the name of the connection string. /// - public string NameOrConnectionString { get; set; } = default!; + public string NameOrConnectionString { get; set; } = null!; /// /// Configures the SQL Server storage options. @@ -38,12 +34,10 @@ public class HangfireSqliteStorageFeature : FeatureBase }; ConfigureSqlServerStorageOptions(storageOptions); - hangfireFeature.ConfigureHangfire = (_, cfg) => + hangfireFeature.ConfigureHangfire((_, cfg) => { cfg.UseSQLiteStorage(NameOrConnectionString, storageOptions); - }; - - hangfireFeature.CreateJobStorage = () => new SQLiteStorage(NameOrConnectionString, storageOptions); + }); }); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index bd8623ce8..545ba3bbc 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -22,7 +22,7 @@ namespace Elsa.Workflows; /// The child being scheduled. /// The delegate to invoke when the scheduled activity completes. /// An optional tag. -public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = default); +public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = null); /// /// Provides context to the currently executing workflow. @@ -239,7 +239,7 @@ public partial class WorkflowExecutionContext : IExecutionContext public WorkflowSubStatus SubStatus { get; internal set; } /// The root associated with the execution context. - public MemoryRegister MemoryRegister { get; private set; } = default!; + public MemoryRegister MemoryRegister { get; private set; } = null!; /// A unique ID of the execution context. public string Id { get; set; } @@ -406,7 +406,7 @@ public partial class WorkflowExecutionContext : IExecutionContext /// /// Registers a completion callback for the specified activity. /// - internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = default, object? tag = default) + internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = null, object? tag = null) { var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback, tag); _completionCallbackEntries.Add(entry); @@ -420,7 +420,7 @@ public partial class WorkflowExecutionContext : IExecutionContext var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child); if (entry == null) - return default; + return null; RemoveCompletionCallback(entry); return entry; @@ -449,25 +449,25 @@ public partial class WorkflowExecutionContext : IExecutionContext ? FindActivityByInstanceId(handle.ActivityInstanceId) : handle.ActivityHash != null ? FindActivityByHash(handle.ActivityHash) - : default; + : null; } /// /// Returns the with the specified activity ID from the workflow graph. /// - public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : default; + public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : null; /// /// Returns the with the specified hash of the activity node ID from the workflow graph. /// /// The hash of the activity node ID. /// The with the specified hash of the activity node ID. - public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : default; + public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : null; /// Returns the containing the specified activity from the workflow graph. public ActivityNode? FindNodeByActivity(IActivity activity) { - return NodeActivityLookup.TryGetValue(activity, out var node) ? node : default; + return NodeActivityLookup.TryGetValue(activity, out var node) ? node : null; } /// Returns the associated with the specified activity ID. @@ -526,7 +526,7 @@ public partial class WorkflowExecutionContext : IExecutionContext } /// Creates a new for the specified activity. - public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = default) + public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = null) { var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type); var tag = options?.Tag; @@ -568,7 +568,7 @@ public partial class WorkflowExecutionContext : IExecutionContext public ActivityOutputRegister GetActivityOutputRegister() => TransientProperties.GetOrAdd(ActivityOutputRegistryKey, () => new ActivityOutputRegister()); /// Returns the last activity result. - public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : default; + public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : null; /// Adds the specified to the workflow execution context. public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context);