Renaming for clarification / project conformity

This commit is contained in:
Raymond den Haan 2024-02-16 14:00:28 +01:00
parent 8a9e00d92d
commit 3fa7a06310
21 changed files with 119 additions and 99 deletions

View file

@ -38,7 +38,7 @@ services
identity.UseConfigurationBasedRoleProvider(options => identitySection.Bind(options));
})
.UseDefaultAuthentication()
.UseInstanceManagement(x => x.HeartbeatSettings = settings => heartbeatSection.Bind(settings));
.UseInstanceManagement(x => x.HeartbeatOptions = settings => heartbeatSection.Bind(settings))
.UseWorkflowManagement(management => management.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseWorkflowRuntime(runtime =>
{

View file

@ -63,10 +63,10 @@
},
"Heartbeat": {
"InstanceHeartbeatRhythm": "00:00:01:00",
"InstanceDeactivatedPeriod": "00:00:05:00",
"HeartbeatTimeoutPeriod": "00:00:05:00",
},
"MassTransit": {
"ShortTermQueueLifetime": "00:00:05:00"
"TemporaryQueueTtl": "00:00:05:00"
},
"Smtp": {
"Host": "localhost",

View file

@ -0,0 +1,12 @@
namespace Elsa.Hosting.Management.Contracts;
/// <summary>
/// Provides a name of the current application instance.
/// </summary>
public interface IApplicationInstanceNameProvider
{
/// <summary>
/// Returns a name for the instance.
/// </summary>
public string GetName();
}

View file

@ -1,7 +1,9 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Hosting.Management.Contracts;
using Elsa.Hosting.Management.HostedServices;
using Elsa.Hosting.Management.Options;
using Elsa.Hosting.Management.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Hosting.Management.Features;
@ -15,17 +17,29 @@ public class InstanceManagementFeature : FeatureBase
public InstanceManagementFeature(IModule module) : base(module)
{
}
public Action<HeartbeatSettings> HeartbeatSettings { get; set; } = _ => { };
/// <summary>
/// A factory that instantiates an <see cref="IApplicationInstanceNameProvider"/>.
/// </summary>
public Func<IServiceProvider, IApplicationInstanceNameProvider> InstanceNameProvider { get; set; } = sp =>
ActivatorUtilities.CreateInstance<RandomApplicationInstanceNameProvider>(sp);
/// <summary>
/// Represents the options for heartbeat feature.
/// </summary>
public Action<HeartbeatOptions> HeartbeatOptions { get; set; } = _ => { };
/// <inheritdoc />
public override void ConfigureHostedServices()
{
Module.ConfigureHostedService<InstanceHeartbeatService>();
Module.ConfigureHostedService<InstanceHeartbeatMonitorService>();
}
/// <inheritdoc />
public override void Apply()
{
Services.Configure(HeartbeatSettings);
Services.Configure(HeartbeatOptions)
.AddSingleton(InstanceNameProvider);
}
}

View file

@ -6,6 +6,7 @@ using Elsa.Workflows.Runtime.Models;
using Medallion.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
namespace Elsa.Hosting.Management.HostedServices;
@ -16,21 +17,24 @@ namespace Elsa.Hosting.Management.HostedServices;
public class InstanceHeartbeatMonitorService : IHostedService, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly HeartbeatSettings _heartbeatSettings;
private readonly ISystemClock _systemClock;
private readonly HeartbeatOptions _heartbeatOptions;
private Timer? _timer;
/// <summary>
/// Creates a new instance of the <see cref="InstanceHeartbeatService"/>
/// Creates a new instance of the <see cref="InstanceHeartbeatMonitorService"/>
/// </summary>
public InstanceHeartbeatMonitorService(IServiceProvider serviceProvider, IOptions<HeartbeatSettings> heartbeatSettings)
public InstanceHeartbeatMonitorService(IServiceProvider serviceProvider, ISystemClock systemClock,
IOptions<HeartbeatOptions> heartbeatOptions)
{
_serviceProvider = serviceProvider;
_heartbeatSettings = heartbeatSettings.Value;
_systemClock = systemClock;
_heartbeatOptions = heartbeatOptions.Value;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(MonitorHeartbeats, null, TimeSpan.Zero, _heartbeatSettings.InstanceHeartbeatRhythm);
_timer = new Timer(MonitorHeartbeats, null, TimeSpan.Zero, _heartbeatOptions.InstanceHeartbeatRhythm);
return Task.CompletedTask;
}
@ -49,32 +53,36 @@ public class InstanceHeartbeatMonitorService : IHostedService, IDisposable
{
_ = Task.Run(async () => await MonitorHeartbeatsAsync());
}
private async Task MonitorHeartbeatsAsync()
{
using var scope = _serviceProvider.CreateScope();
var lockProvider = scope.ServiceProvider.GetRequiredService<IDistributedLockProvider>();
var store = scope.ServiceProvider.GetRequiredService<IKeyValueStore>();
var notificationSender = scope.ServiceProvider.GetRequiredService<INotificationSender>();
var lockKey = "InstanceHeartbeatMonitorService";
await using var monitorLock = await lockProvider.TryAcquireLockAsync(lockKey, TimeSpan.Zero);
if (monitorLock == null)
return;
var filter = new KeyValueFilter { StartsWith = true, Key = InstanceHeartbeatService.HeartbeatKeyPrefix };
var filter = new KeyValueFilter
{
StartsWith = true,
Key = InstanceHeartbeatService.HeartbeatKeyPrefix
};
var heartbeats = await store.FindManyAsync(filter, default);
foreach (var heartbeat in heartbeats)
{
var lastHeartbeat = DateTimeOffset.Parse(heartbeat.SerializedValue).UtcDateTime;
if (DateTime.UtcNow - lastHeartbeat <= _heartbeatSettings.InstanceDeactivatedPeriod)
if (_systemClock.UtcNow - lastHeartbeat <= _heartbeatOptions.HeartbeatTimeoutPeriod)
continue;
var instanceName = heartbeat.Key.Substring(InstanceHeartbeatService.HeartbeatKeyPrefix.Length);
await notificationSender.SendAsync(new InstanceDeactivated(instanceName));
await notificationSender.SendAsync(new HeartbeatTimedOut(instanceName));
await store.DeleteAsync(heartbeat.Key, default);
}

View file

@ -1,5 +1,5 @@
using Elsa.Hosting.Management.Contracts;
using Elsa.Hosting.Management.Options;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Microsoft.Extensions.DependencyInjection;
@ -14,22 +14,23 @@ namespace Elsa.Hosting.Management.HostedServices;
public class InstanceHeartbeatService : IHostedService, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly HeartbeatSettings _heartbeatSettings;
private readonly HeartbeatOptions _heartbeatOptions;
private Timer? _timer;
internal static string HeartbeatKeyPrefix = "Heartbeat_";
/// <summary>
/// Creates a new instance of the <see cref="InstanceHeartbeatService"/>
/// </summary>
public InstanceHeartbeatService(IServiceProvider serviceProvider, IOptions<HeartbeatSettings> heartbeatSettings)
public InstanceHeartbeatService(IServiceProvider serviceProvider, IOptions<HeartbeatOptions> heartbeatOptions)
{
_serviceProvider = serviceProvider;
_heartbeatSettings = heartbeatSettings.Value;
_heartbeatOptions = heartbeatOptions.Value;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(WriteHeartbeat, null, TimeSpan.Zero, _heartbeatSettings.InstanceHeartbeatRhythm);
_timer = new Timer(WriteHeartbeat, null, TimeSpan.Zero, _heartbeatOptions.InstanceHeartbeatRhythm);
return Task.CompletedTask;
}
@ -48,17 +49,17 @@ public class InstanceHeartbeatService : IHostedService, IDisposable
{
_ = Task.Run(async () => await WriteHeartbeatAsync());
}
private async Task WriteHeartbeatAsync()
{
using var scope = _serviceProvider.CreateScope();
var instanceNameRetriever = scope.ServiceProvider.GetRequiredService<IInstanceNameRetriever>();
var instanceNameProvider = scope.ServiceProvider.GetRequiredService<IApplicationInstanceNameProvider>();
var store = scope.ServiceProvider.GetRequiredService<IKeyValueStore>();
await store.SaveAsync(new SerializedKeyValuePair
{
Key = $"{HeartbeatKeyPrefix}{instanceNameRetriever.GetName()}",
Key = $"{HeartbeatKeyPrefix}{instanceNameProvider.GetName()}",
SerializedValue = DateTime.UtcNow.ToString("o")
},
default);

View file

@ -2,4 +2,4 @@ using Elsa.Mediator.Contracts;
namespace Elsa.Hosting.Management.Notifications;
public record InstanceDeactivated(string InstanceName) : INotification;
public record HeartbeatTimedOut(string InstanceName) : INotification;

View file

@ -1,7 +1,7 @@
namespace Elsa.Hosting.Management.Options;
public class HeartbeatSettings
public class HeartbeatOptions
{
public TimeSpan InstanceHeartbeatRhythm { get; set; } = TimeSpan.FromMinutes(1);
public TimeSpan InstanceDeactivatedPeriod { get; set; } = TimeSpan.FromHours(1);
public TimeSpan HeartbeatTimeoutPeriod { get; set; } = TimeSpan.FromHours(1);
}

View file

@ -0,0 +1,20 @@
using Elsa.Hosting.Management.Contracts;
using Elsa.Workflows.Services;
namespace Elsa.Hosting.Management.Services;
/// <summary>
/// Returns a randomly generated instance name.
/// </summary>
public class RandomApplicationInstanceNameProvider : IApplicationInstanceNameProvider
{
private readonly string _instanceName;
public RandomApplicationInstanceNameProvider(RandomLongIdentityGenerator identityGenerator)
{
_instanceName = identityGenerator.GenerateId();
}
/// <inheritdoc />
public string GetName() => _instanceName;
}

View file

@ -3,6 +3,7 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Hosting.Management.Contracts;
using Elsa.MassTransit.AzureServiceBus.Handlers;
using Elsa.MassTransit.AzureServiceBus.Models;
using Elsa.MassTransit.AzureServiceBus.Options;
@ -10,7 +11,6 @@ using Elsa.MassTransit.AzureServiceBus.Services;
using Elsa.MassTransit.Features;
using Elsa.MassTransit.Models;
using Elsa.MassTransit.Options;
using Elsa.Workflows.Contracts;
using MassTransit;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -54,29 +54,29 @@ public class AzureServiceBusFeature : FeatureBase
massTransitFeature.BusConfigurator = configure =>
{
var consumers = massTransitFeature.GetConsumers().ToList();
var shortLivedConsumers = consumers
.Where(c => c.IsShortLived)
var temporaryConsumers = consumers
.Where(c => c.IsTemporary)
.ToList();
RegisterConsumers(consumers);
configure.AddServiceBusMessageScheduler();
configure.AddConsumers(shortLivedConsumers.Select(c => c.ConsumerType).ToArray());
configure.AddConsumers(temporaryConsumers.Select(c => c.ConsumerType).ToArray());
configure.UsingAzureServiceBus((context, serviceBus) =>
{
var options = context.GetRequiredService<IOptions<MassTransitWorkflowDispatcherOptions>>().Value;
var instanceNameRetriever = context.GetRequiredService<IInstanceNameRetriever>();
var instanceNameProvider = context.GetRequiredService<IApplicationInstanceNameProvider>();
if (ConnectionString != null)
serviceBus.Host(ConnectionString);
serviceBus.UseServiceBusMessageScheduler();
ConfigureServiceBus?.Invoke(serviceBus);
foreach (var consumer in shortLivedConsumers)
foreach (var consumer in temporaryConsumers)
{
serviceBus.ReceiveEndpoint($"Elsa-{instanceNameRetriever.GetName()}-{consumer.Name}", configurator =>
serviceBus.ReceiveEndpoint($"Elsa-{instanceNameProvider.GetName()}-{consumer.Name}", configurator =>
{
configurator.AutoDeleteOnIdle = options.ShortTermQueueLifetime ?? TimeSpan.FromHours(1);
configurator.AutoDeleteOnIdle = options.TemporaryQueueTtl ?? TimeSpan.FromHours(1);
configurator.ConcurrentMessageLimit = options.ConcurrentMessageLimit;
configurator.ConfigureConsumer(context, consumer.ConsumerType);
});
@ -109,7 +109,7 @@ public class AzureServiceBusFeature : FeatureBase
subscriptionTopology.Add(new MessageSubscriptionTopology(topicName,
consumer.Name ?? genericType.Name.ToLower(),
consumer.IsShortLived));
consumer.IsTemporary));
}
}

View file

@ -11,14 +11,14 @@ namespace Elsa.MassTransit.AzureServiceBus.Handlers;
public class OrphanedSubscriptionRemover(
MessageTopologyProvider topologyProvider,
ServiceBusAdministrationClient client)
: INotificationHandler<InstanceDeactivated>
: INotificationHandler<HeartbeatTimedOut>
{
/// <summary>
/// Removes orphaned subscriptions from Azure Service Bus.
/// </summary>
public async Task HandleAsync(InstanceDeactivated notification, CancellationToken cancellationToken)
public async Task HandleAsync(HeartbeatTimedOut notification, CancellationToken cancellationToken)
{
var subscriptionTopology = topologyProvider.GetShortLivedSubscriptions().ToList();
var subscriptionTopology = topologyProvider.GetTemporarySubscriptions().ToList();
foreach (var subscription in subscriptionTopology)
{

View file

@ -3,4 +3,4 @@ namespace Elsa.MassTransit.AzureServiceBus.Models;
/// <summary>
/// Represents the topology of a message subscription in Azure Service Bus.
/// </summary>
public record MessageSubscriptionTopology(string TopicName, string SubscriptionName, bool IsShortLived);
public record MessageSubscriptionTopology(string TopicName, string SubscriptionName, bool IsTemporary);

View file

@ -18,10 +18,10 @@ public class MessageTopologyProvider
}
/// <summary>
/// Retrieves all the short-lived message subscriptions from the subscription topology.
/// Retrieves all the temporary message subscriptions from the subscription topology.
/// </summary>
public IEnumerable<MessageSubscriptionTopology> GetShortLivedSubscriptions()
public IEnumerable<MessageSubscriptionTopology> GetTemporarySubscriptions()
{
return _subscriptionTopology.Where(x => x.IsShortLived);
return _subscriptionTopology.Where(x => x.IsTemporary);
}
}

View file

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Elsa.Hosting.Management\Elsa.Hosting.Management.csproj" />
<ProjectReference Include="..\Elsa.MassTransit\Elsa.MassTransit.csproj" />
</ItemGroup>

View file

@ -2,10 +2,10 @@ using Elsa.Extensions;
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Hosting.Management.Contracts;
using Elsa.MassTransit.Consumers;
using Elsa.MassTransit.Features;
using Elsa.MassTransit.Options;
using Elsa.Workflows.Contracts;
using MassTransit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -28,7 +28,7 @@ public class RabbitMqServiceBusFeature : FeatureBase
/// Configures the RabbitMQ transport options.
public Action<RabbitMqTransportOptions>? TransportOptions { get; set; }
/// <summary>
/// Configures the RabbitMQ bus.
/// </summary>
@ -42,26 +42,26 @@ public class RabbitMqServiceBusFeature : FeatureBase
massTransitFeature.BusConfigurator = configure =>
{
var tempConsumers = massTransitFeature.GetConsumers()
.Where(c => c.IsShortLived)
.Where(c => c.IsTemporary)
.ToList();
configure.AddConsumers(tempConsumers.Select(c => c.ConsumerType).ToArray());
configure.UsingRabbitMq((context, serviceBus) =>
{
var options = context.GetRequiredService<IOptions<MassTransitWorkflowDispatcherOptions>>().Value;
var instanceNameRetriever = context.GetRequiredService<IInstanceNameRetriever>();
var instanceNameProvider = context.GetRequiredService<IApplicationInstanceNameProvider>();
if (!string.IsNullOrEmpty(ConnectionString))
serviceBus.Host(ConnectionString);
ConfigureServiceBus?.Invoke(serviceBus);
foreach (var consumer in tempConsumers)
{
serviceBus.ReceiveEndpoint($"{instanceNameRetriever.GetName()}-{consumer.Name}", configurator =>
serviceBus.ReceiveEndpoint($"{instanceNameProvider.GetName()}-{consumer.Name}", configurator =>
{
configurator.QueueExpiration = options.ShortTermQueueLifetime ?? TimeSpan.FromHours(1);
configurator.QueueExpiration = options.TemporaryQueueTtl ?? TimeSpan.FromHours(1);
configurator.ConcurrentMessageLimit = options.ConcurrentMessageLimit;
configurator.ConfigureConsumer<DispatchCancelWorkflowsRequestConsumer>(context);
});

View file

@ -94,7 +94,7 @@ public class MassTransitFeature : FeatureBase
// Concatenate the manually registered consumers with the workflow message consumers.
var consumerTypeDefinitions = this.GetConsumers()
//Temporary queues require implementation specific variables which will be handled in their respective projects
.Where(c => c.IsShortLived == false)
.Where(c => c.IsTemporary == false)
.Concat(workflowMessageConsumers).ToArray();
Services.AddMassTransit(bus =>

View file

@ -7,4 +7,4 @@ public record ConsumerTypeDefinition(
Type ConsumerType,
Type? ConsumerDefinitionType = default,
string? Name = null,
bool IsShortLived = false);
bool IsTemporary = false);

View file

@ -5,8 +5,8 @@ namespace Elsa.MassTransit.Options;
/// Provides options to the <see cref="DispatchWorkflowRequestConsumerDefinition"/>
public class MassTransitWorkflowDispatcherOptions
{
/// The TTL of queues that are seen as short lived (typically queues that are created per running instance).
public TimeSpan? ShortTermQueueLifetime { get; set; }
/// The TTL of queues that are seen as temporary (typically queues that are created per running instance).
public TimeSpan? TemporaryQueueTtl { get; set; }
/// The number of concurrent messages to process.
public int? ConcurrentMessageLimit { get; set; }
}

View file

@ -1,12 +0,0 @@
namespace Elsa.Workflows.Contracts;
/// <summary>
/// Retrieves a name of the current instance.
/// </summary>
public interface IInstanceNameRetriever
{
/// <summary>
/// Returns a name for the instance.
/// </summary>
public string GetName();
}

View file

@ -1,19 +0,0 @@
using Elsa.Workflows.Contracts;
namespace Elsa.Workflows.Services;
/// <summary>
/// Returns a randomly generated instance name.
/// </summary>
public class RandomInstanceNameRetriever : IInstanceNameRetriever
{
private readonly string _instanceName;
public RandomInstanceNameRetriever(RandomLongIdentityGenerator identityGenerator)
{
_instanceName = identityGenerator.GenerateId();
}
/// <inheritdoc />
public string GetName() => _instanceName;
}

View file

@ -104,10 +104,6 @@ public class WorkflowRuntimeFeature : FeatureBase
/// </summary>
public Func<IServiceProvider, IBackgroundActivityScheduler> BackgroundActivityScheduler { get; set; } = sp => ActivatorUtilities.CreateInstance<LocalBackgroundActivityScheduler>(sp);
/// <summary>
/// A factory that instantiates an <see cref="IInstanceNameRetriever"/>.
/// </summary>
public Func<IServiceProvider, IInstanceNameRetriever> InstanceNameRetriever { get; set; } = sp => ActivatorUtilities.CreateInstance<RandomInstanceNameRetriever>(sp);
/// <summary>
/// A delegate to configure the <see cref="DistributedLockingOptions"/>.
@ -183,7 +179,6 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddScoped(WorkflowExecutionContextStore)
.AddSingleton(RunTaskDispatcher)
.AddSingleton(BackgroundActivityScheduler)
.AddSingleton(InstanceNameRetriever)
.AddSingleton<RandomLongIdentityGenerator>()
.AddScoped<IBookmarkManager, DefaultBookmarkManager>()
.AddScoped<IActivityExecutionManager, DefaultActivityExecutionManager>()