Add project files.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>
|
||||
Provides Hangfire integration and an implementation of Elsa's IJobQueue implementation.
|
||||
</Description>
|
||||
<PackageTags>elsa module hangfire jobs scheduling</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Hangfire"/>
|
||||
<PackageReference Include="Hangfire.MemoryStorage"/>
|
||||
<PackageReference Include="Hangfire.Storage.SQLite"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\common\Elsa.Features\Elsa.Features.csproj"/>
|
||||
<ProjectReference Include="..\Elsa.Scheduling\Elsa.Scheduling.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,55 @@
|
||||
using Elsa.Hangfire.Jobs;
|
||||
using Hangfire;
|
||||
using Hangfire.Storage.Monitoring;
|
||||
|
||||
namespace Elsa.Hangfire.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// A set of extension methods for <see cref="JobStorage"/>.
|
||||
/// </summary>
|
||||
public static class JobStorageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumerates all scheduled jobs of a given type.
|
||||
/// </summary>
|
||||
public static IEnumerable<KeyValuePair<string, ScheduledJobDto>> EnumerateScheduledJobs(this JobStorage storage, string name)
|
||||
{
|
||||
var api = storage.GetMonitoringApi();
|
||||
var skip = 0;
|
||||
const int take = 100;
|
||||
JobList<ScheduledJobDto> scheduledJobs;
|
||||
|
||||
do
|
||||
{
|
||||
scheduledJobs = api.ScheduledJobs(skip, take);
|
||||
|
||||
var jobs = scheduledJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob));
|
||||
foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == name))
|
||||
yield return job;
|
||||
|
||||
skip += take;
|
||||
} while (scheduledJobs.Count == take);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all enqueued jobs of a given type.
|
||||
/// </summary>
|
||||
public static IEnumerable<KeyValuePair<string, EnqueuedJobDto>> EnumerateQueuedJobs(this JobStorage storage, string queueName, string taskName)
|
||||
{
|
||||
var api = storage.GetMonitoringApi();
|
||||
var skip = 0;
|
||||
const int take = 100;
|
||||
JobList<EnqueuedJobDto> enqueuedJobs;
|
||||
|
||||
do
|
||||
{
|
||||
enqueuedJobs = api.EnqueuedJobs(queueName, skip, take);
|
||||
|
||||
var jobs = enqueuedJobs.FindAll(x => x.Value.Job?.Type == typeof(RunWorkflowJob) || x.Value.Job?.Type == typeof(ResumeWorkflowJob));
|
||||
foreach (var job in jobs.Where(x => (string)x.Value.Job.Args[0] == taskName))
|
||||
yield return job;
|
||||
|
||||
skip += take;
|
||||
} while (enqueuedJobs.Count == take);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Hangfire.Features;
|
||||
using Elsa.Scheduling;
|
||||
using Elsa.Scheduling.Features;
|
||||
using Elsa.Workflows.Runtime.Features;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Elsa.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for the <see cref="HangfireFeature"/>.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Installs and configures Hangfire. Only use this feature if you are not configuring Hangfire yourself.
|
||||
/// </summary>
|
||||
public static IModule UseHangfire(this IModule module, Action<HangfireFeature>? configure = default)
|
||||
{
|
||||
return module.Use(configure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures Hangfire to use SQL Server storage. Only use this feature if you are not configuring Hangfire yourself.
|
||||
/// </summary>
|
||||
[Obsolete("Configure storage directly on the HangfireFeature.")]
|
||||
public static HangfireFeature UseSqlServerStorage(this HangfireFeature feature, Action<HangfireSqlServerStorageFeature> configure)
|
||||
{
|
||||
feature.Module.Use(configure);
|
||||
return feature;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures Hangfire to use SQLite storage. Only use this feature if you are not configuring Hangfire yourself.
|
||||
/// </summary>
|
||||
[Obsolete("Configure storage directly on the HangfireFeature.")]
|
||||
public static HangfireFeature UseSqliteStorage(this HangfireFeature feature, Action<HangfireSqliteStorageFeature> configure)
|
||||
{
|
||||
feature.Module.Use(configure);
|
||||
return feature;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a Hangfire implementation for <see cref="IWorkflowScheduler"/>.
|
||||
/// </summary>
|
||||
public static SchedulingFeature UseHangfireScheduler(this SchedulingFeature feature, Action<HangfireSchedulerFeature>? configure = default)
|
||||
{
|
||||
feature.Module.Use(configure);
|
||||
return feature;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a Hangfire implementation for <see cref="IWorkflowScheduler"/>.
|
||||
/// </summary>
|
||||
public static WorkflowRuntimeFeature UseHangfireBackgroundActivityScheduler(this WorkflowRuntimeFeature feature, Action<HangfireBackgroundActivitySchedulerFeature>? configure = default)
|
||||
{
|
||||
feature.Module.Use(configure);
|
||||
return feature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Elsa.Hangfire.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Adds extension methods to <see cref="TimeSpan"/> that converts it to a cron expression.
|
||||
/// </summary>
|
||||
public static class TimeSpanExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the specified time span to a cron expression.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The time span.</param>
|
||||
/// <returns>The cron expression.</returns>
|
||||
public static string ToCronExpression(this TimeSpan timeSpan)
|
||||
{
|
||||
static string CreateCronComponent(int number) => (number > 0 ? $"*/{number}" : "*");
|
||||
|
||||
var cron = CreateCronComponent(timeSpan.Seconds);
|
||||
cron += ' ' + CreateCronComponent(timeSpan.Minutes);
|
||||
cron += ' ' + CreateCronComponent(timeSpan.Hours);
|
||||
cron += ' ' + CreateCronComponent(timeSpan.Days);
|
||||
return cron + " * *";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Hangfire.Services;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Hangfire.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Installs a Hangfire implementation for <see cref="IBackgroundActivityScheduler"/>.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(WorkflowRuntimeFeature))]
|
||||
public class HangfireBackgroundActivitySchedulerFeature : FeatureBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public HangfireBackgroundActivitySchedulerFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Module.Configure<WorkflowRuntimeFeature>(workflowRuntimeFeature =>
|
||||
{
|
||||
workflowRuntimeFeature.BackgroundActivityScheduler = sp => sp.GetRequiredService<HangfireBackgroundActivityScheduler>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddSingleton<HangfireBackgroundActivityScheduler>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
|
||||
namespace Elsa.Hangfire.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up Hangfire. If you're setting up Hangfire yourself, then you should not enable this feature.
|
||||
/// </summary>
|
||||
[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)
|
||||
{
|
||||
/// <summary>
|
||||
/// A delegate that configures Hangfire.
|
||||
/// </summary>
|
||||
private Action<IServiceProvider, IGlobalConfiguration> _configureHangfire = (_, _) => { };
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that configures Hangfire's background job server options.
|
||||
/// </summary>
|
||||
private Action<IServiceProvider, BackgroundJobServerOptions> _configureBackgroundServerOptions = (_, _) => { };
|
||||
|
||||
/// <summary>
|
||||
/// A delegate that creates a job storage instance.
|
||||
/// </summary>
|
||||
private Func<JobStorage> _createJobStorage = () => new MemoryStorage();
|
||||
|
||||
/// <summary>
|
||||
/// Configures Hangfire.
|
||||
/// </summary>
|
||||
public HangfireFeature ConfigureHangfire(Action<IServiceProvider, IGlobalConfiguration> configure)
|
||||
{
|
||||
_configureHangfire += configure;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures Hangfire's background job server options.
|
||||
/// </summary>
|
||||
public HangfireFeature ConfigureBackgroundServerOptions(Action<IServiceProvider, BackgroundJobServerOptions> configure)
|
||||
{
|
||||
_configureBackgroundServerOptions += configure;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HangfireFeature UseMemoryStorage()
|
||||
{
|
||||
return UseJobStorage(new MemoryStorage());
|
||||
}
|
||||
|
||||
public HangfireFeature UseJobStorage(JobStorage storage)
|
||||
{
|
||||
_createJobStorage = () => storage;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
var jobStorage = _createJobStorage();
|
||||
|
||||
Action<IServiceProvider, IGlobalConfiguration> configAction = (sp, cfg) =>
|
||||
{
|
||||
cfg.UseSimpleAssemblyNameTypeSerializer();
|
||||
cfg.UseRecommendedSerializerSettings(json => json.TypeNameHandling = TypeNameHandling.Objects);
|
||||
cfg.UseStorage(jobStorage);
|
||||
};
|
||||
|
||||
Action<IServiceProvider, BackgroundJobServerOptions> serverOptionsAction = (sp, options) =>
|
||||
{
|
||||
options.WorkerCount = 1;
|
||||
options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
|
||||
};
|
||||
|
||||
configAction += _configureHangfire;
|
||||
serverOptionsAction += _configureBackgroundServerOptions;
|
||||
|
||||
Services.AddHangfire(configAction);
|
||||
Services.AddHangfireServer(serverOptionsAction, jobStorage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Hangfire.Handlers;
|
||||
using Elsa.Hangfire.Services;
|
||||
using Elsa.Scheduling;
|
||||
using Elsa.Scheduling.Features;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Hangfire.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Installs a Hangfire implementation for <see cref="IWorkflowScheduler"/>.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(SchedulingFeature))]
|
||||
public class HangfireSchedulerFeature : FeatureBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public HangfireSchedulerFeature(IModule module) : base(module)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Module.Configure<SchedulingFeature>(schedulingFeature =>
|
||||
{
|
||||
schedulingFeature.WorkflowScheduler = sp => sp.GetRequiredService<HangfireWorkflowScheduler>();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Apply()
|
||||
{
|
||||
Services.AddSingleton<HangfireWorkflowScheduler>();
|
||||
Services.AddSingleton<IActivityDescriptorModifier, CronActivityDescriptorModifier>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Hangfire;
|
||||
using Hangfire.SqlServer;
|
||||
|
||||
namespace Elsa.Hangfire.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the Hangfire feature to use SQL Server storage. If you're setting up Hangfire yourself, then you should not enable this feature.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(HangfireFeature))]
|
||||
[Obsolete("Configure storage directly on the HangfireFeature.")]
|
||||
public class HangfireSqlServerStorageFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection string to use when connecting to SQL Server, or the name of the connection string.
|
||||
/// </summary>
|
||||
public string NameOrConnectionString { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the SQL Server storage options.
|
||||
/// </summary>
|
||||
public Action<SqlServerStorageOptions> ConfigureSqlServerStorageOptions { get; set; } = _ => { };
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Module.Use<HangfireFeature>(hangfireFeature =>
|
||||
{
|
||||
var storageOptions = new SqlServerStorageOptions
|
||||
{
|
||||
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
|
||||
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
|
||||
QueuePollInterval = TimeSpan.FromSeconds(15),
|
||||
UseRecommendedIsolationLevel = true
|
||||
};
|
||||
ConfigureSqlServerStorageOptions(storageOptions);
|
||||
|
||||
hangfireFeature.ConfigureHangfire((_, cfg) =>
|
||||
{
|
||||
cfg.UseSqlServerStorage(NameOrConnectionString, storageOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Hangfire.Storage.SQLite;
|
||||
|
||||
namespace Elsa.Hangfire.Features;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the Hangfire feature to use SQLite storage. If you're setting up Hangfire yourself, then you should not enable this feature.
|
||||
/// </summary>
|
||||
[DependsOn(typeof(HangfireFeature))]
|
||||
[Obsolete("Configure storage directly on the HangfireFeature.")]
|
||||
public class HangfireSqliteStorageFeature(IModule module) : FeatureBase(module)
|
||||
{
|
||||
/// <summary>
|
||||
/// The connection string to use when connecting to SQL Server, or the name of the connection string.
|
||||
/// </summary>
|
||||
public string NameOrConnectionString { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Configures the SQL Server storage options.
|
||||
/// </summary>
|
||||
public Action<SQLiteStorageOptions> ConfigureSqlServerStorageOptions { get; set; } = _ => { };
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Configure()
|
||||
{
|
||||
Module.Use<HangfireFeature>(hangfireFeature =>
|
||||
{
|
||||
var storageOptions = new SQLiteStorageOptions
|
||||
{
|
||||
QueuePollInterval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
ConfigureSqlServerStorageOptions(storageOptions);
|
||||
|
||||
hangfireFeature.ConfigureHangfire((_, cfg) =>
|
||||
{
|
||||
cfg.UseSQLiteStorage(NameOrConnectionString, storageOptions);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Elsa.Scheduling.Activities;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Models;
|
||||
|
||||
namespace Elsa.Hangfire.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Modifies the <see cref="ActivityDescriptor"/> of the <see cref="Cron"/> activity.
|
||||
/// </summary>
|
||||
public class CronActivityDescriptorModifier : IActivityDescriptorModifier
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Modify(ActivityDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.TypeName != ActivityTypeNameHelper.GenerateTypeName<Cron>())
|
||||
return;
|
||||
|
||||
descriptor.Description = "Schedules the execution of the activity using a cron expression using Hangfire.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Hangfire.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A job that executes a background activity.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ExecuteBackgroundActivityJob(IBackgroundActivityInvoker backgroundActivityInvoker, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor)
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, string? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null;
|
||||
using var scope = tenantAccessor.PushContext(tenant);
|
||||
await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
[Obsolete("Use the other overload.")]
|
||||
[UsedImplicitly]
|
||||
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await backgroundActivityInvoker.ExecuteAsync(scheduledBackgroundActivity, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Scheduling;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Messages;
|
||||
using Hangfire;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Hangfire.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A job that resumes a workflow.
|
||||
/// </summary>
|
||||
public class ResumeWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor)
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
/// <param name="taskName">A unique name for this job.</param>
|
||||
/// <param name="request">The workflow request.</param>
|
||||
/// <param name="tenantId">The ID of the current tenant scheduling this job.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
// ReSharper disable once UnusedParameter.Global
|
||||
[AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||
public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null;
|
||||
using var scope = tenantAccessor.PushContext(tenant);
|
||||
var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken);
|
||||
var runRequest = new RunWorkflowInstanceRequest
|
||||
{
|
||||
BookmarkId = request.BookmarkId,
|
||||
ActivityHandle = request.ActivityHandle,
|
||||
Input = request.Input,
|
||||
Properties = request.Properties
|
||||
};
|
||||
await client.RunInstanceAsync(runRequest, cancellationToken);
|
||||
}
|
||||
|
||||
[Obsolete("Use the other overload.")]
|
||||
[UsedImplicitly]
|
||||
public async Task ExecuteAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = await workflowRuntime.CreateClientAsync(request.WorkflowInstanceId, cancellationToken);
|
||||
var runRequest = new RunWorkflowInstanceRequest
|
||||
{
|
||||
BookmarkId = request.BookmarkId,
|
||||
ActivityHandle = request.ActivityHandle,
|
||||
Input = request.Input,
|
||||
Properties = request.Properties
|
||||
};
|
||||
await client.RunInstanceAsync(runRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Scheduling;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Elsa.Workflows.Runtime.Messages;
|
||||
using Hangfire;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Hangfire.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A job that resumes a workflow.
|
||||
/// </summary>
|
||||
public class RunWorkflowJob(IWorkflowRuntime workflowRuntime, ITenantFinder tenantFinder, ITenantAccessor tenantAccessor)
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes the job.
|
||||
/// </summary>
|
||||
/// <param name="taskName">A unique name for this job.</param>
|
||||
/// <param name="request">The workflow request.</param>
|
||||
/// <param name="tenantId">The ID of the current tenant scheduling this job.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
// ReSharper disable once UnusedParameter.Global
|
||||
[AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Fail)]
|
||||
public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string? tenantId, CancellationToken cancellationToken)
|
||||
{
|
||||
var tenant = tenantId != null ? await tenantFinder.FindByIdAsync(tenantId, cancellationToken) : null;
|
||||
using var scope = tenantAccessor.PushContext(tenant);
|
||||
var client = await workflowRuntime.CreateClientAsync(cancellationToken);
|
||||
var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest
|
||||
{
|
||||
WorkflowDefinitionHandle = request.WorkflowDefinitionHandle,
|
||||
TriggerActivityId = request.TriggerActivityId,
|
||||
CorrelationId = request.CorrelationId,
|
||||
Input = request.Input,
|
||||
Properties = request.Properties,
|
||||
ParentId = request.ParentId
|
||||
};
|
||||
await client.CreateAndRunInstanceAsync(createAndRunRequest, cancellationToken);
|
||||
}
|
||||
|
||||
[Obsolete("Use the other overload.")]
|
||||
[UsedImplicitly]
|
||||
public async Task ExecuteAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var client = await workflowRuntime.CreateClientAsync(cancellationToken);
|
||||
var createAndRunRequest = new CreateAndRunWorkflowInstanceRequest
|
||||
{
|
||||
WorkflowDefinitionHandle = request.WorkflowDefinitionHandle,
|
||||
TriggerActivityId = request.TriggerActivityId,
|
||||
CorrelationId = request.CorrelationId,
|
||||
Input = request.Input,
|
||||
Properties = request.Properties,
|
||||
ParentId = request.ParentId
|
||||
};
|
||||
await client.CreateAndRunInstanceAsync(createAndRunRequest, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Hangfire.Jobs;
|
||||
using Elsa.Hangfire.States;
|
||||
using Elsa.Workflows.Runtime;
|
||||
using Hangfire;
|
||||
using Hangfire.States;
|
||||
|
||||
namespace Elsa.Hangfire.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Invokes activities from a background worker within the context of its workflow instance using Hangfire.
|
||||
/// </summary>
|
||||
public class HangfireBackgroundActivityScheduler(IBackgroundJobClient backgroundJobClient, ITenantAccessor tenantAccessor) : IBackgroundActivityScheduler
|
||||
{
|
||||
public Task<string> CreateAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantAccessor.Tenant?.Id;
|
||||
var jobId = backgroundJobClient.Create<ExecuteBackgroundActivityJob>(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None), new PendingState());
|
||||
return Task.FromResult(jobId);
|
||||
}
|
||||
|
||||
public Task ScheduleAsync(string jobId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = JobStorage.Current.GetConnection();
|
||||
var jobData = connection.GetJobData(jobId);
|
||||
if (jobData != null) backgroundJobClient.ChangeState(jobId, new EnqueuedState());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<string> ScheduleAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantAccessor.Tenant?.Id;
|
||||
var jobId = backgroundJobClient.Enqueue<ExecuteBackgroundActivityJob>(x => x.ExecuteAsync(scheduledBackgroundActivity, tenantId, CancellationToken.None));
|
||||
return Task.FromResult(jobId);
|
||||
}
|
||||
|
||||
public Task UnscheduledAsync(string jobId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
backgroundJobClient.Delete(jobId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CancelAsync(string jobId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
backgroundJobClient.Delete(jobId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Hangfire.Extensions;
|
||||
using Elsa.Hangfire.Jobs;
|
||||
using Elsa.Scheduling;
|
||||
using Hangfire;
|
||||
using Hangfire.Storage;
|
||||
|
||||
namespace Elsa.Hangfire.Services;
|
||||
|
||||
/// <summary>
|
||||
/// An implementation of <see cref="IWorkflowScheduler"/> that uses Hangfire.
|
||||
/// </summary>
|
||||
public class HangfireWorkflowScheduler(
|
||||
IBackgroundJobClient backgroundJobClient,
|
||||
IRecurringJobManager recurringJobManager,
|
||||
ITenantAccessor tenantAccessor,
|
||||
JobStorage jobStorage) : IWorkflowScheduler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleAtAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantAccessor.Tenant?.Id;
|
||||
backgroundJobClient.Schedule<RunWorkflowJob>(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleAtAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset at, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantAccessor.Tenant?.Id;
|
||||
backgroundJobClient.Schedule<ResumeWorkflowJob>(job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), at);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ScheduleCronAsync(taskName, request, interval.ToCronExpression(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask ScheduleRecurringAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, DateTimeOffset startAt, TimeSpan interval, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await ScheduleCronAsync(taskName, request, interval.ToCronExpression(), cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleCronAsync(string taskName, ScheduleNewWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantAccessor.Tenant?.Id;
|
||||
recurringJobManager.AddOrUpdate<RunWorkflowJob>(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask ScheduleCronAsync(string taskName, ScheduleExistingWorkflowInstanceRequest request, string cronExpression, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantAccessor.Tenant?.Id;
|
||||
recurringJobManager.AddOrUpdate<ResumeWorkflowJob>(taskName, job => job.ExecuteAsync(taskName, request, tenantId, CancellationToken.None), cronExpression);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ValueTask UnscheduleAsync(string taskName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
DeleteJobByTaskName(taskName);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private void DeleteJobByTaskName(string taskName)
|
||||
{
|
||||
var scheduledJobIds = GetScheduledJobIds(taskName);
|
||||
foreach (var jobId in scheduledJobIds) backgroundJobClient.Delete(jobId);
|
||||
|
||||
var queuedJobsIds = GetQueuedJobIds(taskName);
|
||||
foreach (var jobId in queuedJobsIds) backgroundJobClient.Delete(jobId);
|
||||
|
||||
var recurringJobIds = GetRecurringJobIds<RunWorkflowJob>(taskName);
|
||||
foreach (var jobId in recurringJobIds) recurringJobManager.RemoveIfExists(jobId);
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetScheduledJobIds(string taskName)
|
||||
{
|
||||
return jobStorage.EnumerateScheduledJobs(taskName)
|
||||
.Select(x => x.Key)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetQueuedJobIds(string taskName)
|
||||
{
|
||||
return jobStorage.EnumerateQueuedJobs("default", taskName)
|
||||
.Select(x => x.Key)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetRecurringJobIds<TJob>(string taskName)
|
||||
{
|
||||
using var connection = jobStorage.GetConnection();
|
||||
var jobs = connection.GetRecurringJobs().Where(x => x.Job.Type == typeof(TJob) && (string)x.Job.Args[0] == taskName);
|
||||
return jobs.Select(x => x.Id).Distinct().ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Hangfire.States;
|
||||
|
||||
namespace Elsa.Hangfire.States;
|
||||
|
||||
public class PendingState : IState
|
||||
{
|
||||
public static readonly string StateName = "Pending";
|
||||
|
||||
public Dictionary<string, string> SerializeData()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public string Name => StateName;
|
||||
public string Reason => "Pending";
|
||||
public bool IsFinal => false;
|
||||
public bool IgnoreJobLoadException => true;
|
||||
}
|
||||
Reference in New Issue
Block a user