Merge pull request #7904 from elsa-workflows/codex/fix-7306-tenant-agnostic-registry-population
Avoid repeated tenant-agnostic registry population
This commit is contained in:
commit
ed50e1cc96
|
|
@ -95,10 +95,21 @@ public interface IActivityRegistry : IActivityProvider
|
|||
/// <summary>
|
||||
/// Refreshes the activity descriptors in the registry by querying the specified activity provider.
|
||||
/// </summary>
|
||||
Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all activity descriptors from the registry.
|
||||
Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that descriptors from a tenant-agnostic activity provider have been initialized.
|
||||
/// </summary>
|
||||
/// <param name="activityProvider">The activity provider used to retrieve the descriptors.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <remarks>
|
||||
/// Tenant-sensitive providers are refreshed on every call. The default implementation refreshes every provider to preserve compatibility with custom registry implementations.
|
||||
/// </remarks>
|
||||
Task EnsureDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default) =>
|
||||
RefreshDescriptorsAsync(activityProvider, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all activity descriptors from the registry.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
|
|
@ -107,4 +118,4 @@ public interface IActivityRegistry : IActivityProvider
|
|||
/// </summary>
|
||||
/// <param name="providerType">The type of the activity provider.</param>
|
||||
void ClearProvider(Type providerType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Elsa.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Marks an activity provider whose descriptors do not depend on the current tenant.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Descriptors from a tenant-agnostic provider can be initialized once for each
|
||||
/// <see cref="IActivityRegistry"/> instance.
|
||||
/// </remarks>
|
||||
public interface ITenantAgnosticActivityProvider : IActivityProvider;
|
||||
|
|
@ -16,9 +16,13 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
|
|||
// Per-tenant activity descriptors (workflow-as-activities, tenant-specific providers, etc.)
|
||||
private readonly ConcurrentDictionary<string, TenantRegistryData> _tenantRegistries = new();
|
||||
|
||||
// Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
|
||||
private readonly TenantRegistryData _agnosticRegistry = new();
|
||||
|
||||
// Tenant-agnostic activity descriptors (built-in activities, manually registered, etc.)
|
||||
private readonly TenantRegistryData _agnosticRegistry = new();
|
||||
|
||||
// Tracks tenant-agnostic providers initialized for this registry instance.
|
||||
private readonly ConcurrentDictionary<Type, byte> _initializedProviders = new();
|
||||
private readonly ConcurrentDictionary<Type, SemaphoreSlim> _providerInitializationLocks = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(Type providerType, ActivityDescriptor descriptor)
|
||||
{
|
||||
|
|
@ -182,7 +186,7 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
|
|||
await RefreshDescriptorsAsync(activityProvider, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default)
|
||||
public async Task RefreshDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var providerType = activityProvider.GetType();
|
||||
|
||||
|
|
@ -216,9 +220,39 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
|
|||
|
||||
// Update the provider's descriptor list in this registry
|
||||
registry.ProvidedActivityDescriptors[providerType] = providerDescriptors;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task EnsureDescriptorsAsync(IActivityProvider activityProvider, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (activityProvider is not ITenantAgnosticActivityProvider)
|
||||
{
|
||||
await RefreshDescriptorsAsync(activityProvider, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var providerType = activityProvider.GetType();
|
||||
if (_initializedProviders.ContainsKey(providerType))
|
||||
return;
|
||||
|
||||
var initializationLock = _providerInitializationLocks.GetOrAdd(providerType, _ => new(1, 1));
|
||||
await initializationLock.WaitAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (_initializedProviders.ContainsKey(providerType))
|
||||
return;
|
||||
|
||||
await RefreshDescriptorsAsync(activityProvider, cancellationToken);
|
||||
_initializedProviders.TryAdd(providerType, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
initializationLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void Add(ActivityDescriptor? descriptor, TenantRegistryData registry, ICollection<ActivityDescriptor> providerDescriptors)
|
||||
{
|
||||
if (descriptor is null)
|
||||
|
|
@ -253,15 +287,17 @@ public class ActivityRegistry(IActivityDescriber activityDescriber, IEnumerable<
|
|||
{
|
||||
_manualActivityDescriptors.Clear();
|
||||
_tenantRegistries.Clear();
|
||||
_agnosticRegistry.ActivityDescriptors.Clear();
|
||||
_agnosticRegistry.LatestActivityDescriptors.Clear();
|
||||
_agnosticRegistry.ProvidedActivityDescriptors.Clear();
|
||||
}
|
||||
_agnosticRegistry.ActivityDescriptors.Clear();
|
||||
_agnosticRegistry.LatestActivityDescriptors.Clear();
|
||||
_agnosticRegistry.ProvidedActivityDescriptors.Clear();
|
||||
_initializedProviders.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearProvider(Type providerType)
|
||||
{
|
||||
var currentTenantId = tenantAccessor.TenantId;
|
||||
public void ClearProvider(Type providerType)
|
||||
{
|
||||
_initializedProviders.TryRemove(providerType, out _);
|
||||
var currentTenantId = tenantAccessor.TenantId;
|
||||
|
||||
// Clear from current tenant's registry
|
||||
if (_tenantRegistries.TryGetValue(currentTenantId, out var tenantRegistry)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Elsa.Workflows.Management.Activities.HostMethod;
|
|||
/// Inputs come from public properties and method parameters.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class HostMethodActivityProvider(IOptions<HostMethodActivitiesOptions> options, IHostMethodActivityDescriber hostMethodActivityDescriber) : IActivityProvider
|
||||
public class HostMethodActivityProvider(IOptions<HostMethodActivitiesOptions> options, IHostMethodActivityDescriber hostMethodActivityDescriber) : ITenantAgnosticActivityProvider
|
||||
{
|
||||
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,4 +10,14 @@ public interface IActivityRegistryPopulator
|
|||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
Task PopulateRegistryAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that activity descriptors have been populated, initializing tenant-agnostic providers once.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <remarks>
|
||||
/// The default implementation preserves the existing behavior for custom populators.
|
||||
/// </remarks>
|
||||
Task EnsureRegistryPopulatedAsync(CancellationToken cancellationToken = default) =>
|
||||
PopulateRegistryAsync(cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace Elsa.Workflows.Management.Providers;
|
|||
/// Provides activity descriptors based on a list of activity types registered in the <see cref="ManagementOptions"/>.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class TypedActivityProvider : IActivityProvider
|
||||
public class TypedActivityProvider : ITenantAgnosticActivityProvider
|
||||
{
|
||||
private readonly IActivityDescriber _activityDescriber;
|
||||
private readonly ManagementOptions _options;
|
||||
|
|
@ -40,4 +40,4 @@ public class TypedActivityProvider : IActivityProvider
|
|||
yield return descriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,11 @@ public class ActivityRegistryPopulator(IEnumerable<IActivityProvider> providers,
|
|||
{
|
||||
await registry.RefreshDescriptorsAsync(providers, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task EnsureRegistryPopulatedAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var provider in providers)
|
||||
await registry.EnsureDescriptorsAsync(provider, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@ public class DefaultRegistriesPopulator(
|
|||
/// <inheritdoc />
|
||||
public async Task PopulateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Stage 1: Populate the activity registry.
|
||||
// Because workflow definitions can be used as activities, we need to make sure that the activity registry is populated before we populate the workflow definition store.
|
||||
await activityRegistryPopulator.PopulateRegistryAsync(cancellationToken);
|
||||
// Stage 1: Ensure the activity registry is populated.
|
||||
// Tenant-agnostic providers are initialized once, while tenant-sensitive providers are refreshed before the workflow definition store is populated.
|
||||
await activityRegistryPopulator.EnsureRegistryPopulatedAsync(cancellationToken);
|
||||
|
||||
// Stage 2: Populate the workflow definition store.
|
||||
await workflowDefinitionStorePopulator.PopulateStoreAsync(false, cancellationToken);
|
||||
|
||||
// Stage 3: Re-populate the activity registry.
|
||||
// After the workflow definition store has been populated, we need to re-populate the activity registry to make sure that the activity descriptors are up-to-date.
|
||||
await activityRegistryPopulator.PopulateRegistryAsync(cancellationToken);
|
||||
// Stage 3: Refresh tenant-sensitive activity providers.
|
||||
// After the workflow definition store has been populated, refresh tenant-specific descriptors such as workflow-as-activity definitions.
|
||||
await activityRegistryPopulator.EnsureRegistryPopulatedAsync(cancellationToken);
|
||||
|
||||
// Stage 4. Re-update the workflow definition store with the current set of activities.
|
||||
// Finally, we need to re-populate the workflow definition store to make sure that the workflow definitions are up-to-date.
|
||||
|
|
@ -34,4 +34,4 @@ public class DefaultRegistriesPopulator(
|
|||
var notification = new WorkflowDefinitionsReloaded(reloadedWorkflowDefinitions);
|
||||
await notificationSender.SendAsync(notification, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -425,6 +425,112 @@ public class ActivityRegistryTests
|
|||
AssertDescriptor(result, CurrentTenant, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_InitializesTenantAgnosticProviderOnlyOnce_WhenCalledRepeatedly()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new CountingTenantAgnosticProvider();
|
||||
|
||||
// Act
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, provider.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_InitializesTenantAgnosticProviderOnlyOnce_WhenCalledConcurrently()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new BlockingTenantAgnosticProvider();
|
||||
var initializationTasks = Enumerable.Range(0, 8)
|
||||
.Select(_ => _registry.EnsureDescriptorsAsync(provider))
|
||||
.ToArray();
|
||||
|
||||
await provider.FirstCallStarted.Task.WaitAsync(TimeSpan.FromSeconds(1));
|
||||
await Task.Yield();
|
||||
provider.Release();
|
||||
|
||||
// Act
|
||||
await Task.WhenAll(initializationTasks);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, provider.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_RetriesTenantAgnosticProvider_AfterFailure()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new CountingTenantAgnosticProvider([new InvalidOperationException("Expected failure")]);
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => _registry.EnsureDescriptorsAsync(provider));
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, provider.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_RetriesTenantAgnosticProvider_AfterCancellation()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new CountingTenantAgnosticProvider([new OperationCanceledException()]);
|
||||
|
||||
// Act
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => _registry.EnsureDescriptorsAsync(provider));
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, provider.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_ReinitializesTenantAgnosticProvider_AfterRegistryIsCleared()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new CountingTenantAgnosticProvider();
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Act
|
||||
_registry.Clear();
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, provider.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_ReinitializesTenantAgnosticProvider_AfterProviderIsCleared()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new CountingTenantAgnosticProvider();
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Act
|
||||
_registry.ClearProvider(provider.GetType());
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, provider.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureDescriptorsAsync_RefreshesTenantSensitiveProvider_OnEveryCall()
|
||||
{
|
||||
// Arrange
|
||||
var provider = new CountingTenantSensitiveProvider();
|
||||
|
||||
// Act
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
await _registry.EnsureDescriptorsAsync(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, provider.CallCount);
|
||||
}
|
||||
|
||||
|
||||
private ActivityDescriptor CreateDescriptor(string typeName, int version, string? tenantId) =>
|
||||
new()
|
||||
|
|
@ -464,4 +570,73 @@ public class ActivityRegistryTests
|
|||
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default) => new(Descriptors);
|
||||
}
|
||||
|
||||
private sealed class CountingTenantAgnosticProvider(IEnumerable<Exception>? failures = null) : ITenantAgnosticActivityProvider
|
||||
{
|
||||
private readonly Queue<Exception> _failures = new(failures ?? []);
|
||||
private int _callCount;
|
||||
|
||||
public int CallCount => Volatile.Read(ref _callCount);
|
||||
|
||||
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref _callCount);
|
||||
|
||||
if (_failures.TryDequeue(out var failure))
|
||||
return ValueTask.FromException<IEnumerable<ActivityDescriptor>>(failure);
|
||||
|
||||
return new([new ActivityDescriptor
|
||||
{
|
||||
TypeName = nameof(CountingTenantAgnosticProvider),
|
||||
Version = 1,
|
||||
TenantId = Tenant.AgnosticTenantId,
|
||||
Kind = ActivityKind.Action
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BlockingTenantAgnosticProvider : ITenantAgnosticActivityProvider
|
||||
{
|
||||
private readonly TaskCompletionSource _release = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private int _callCount;
|
||||
|
||||
public TaskCompletionSource FirstCallStarted { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public int CallCount => Volatile.Read(ref _callCount);
|
||||
|
||||
public async ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref _callCount);
|
||||
FirstCallStarted.TrySetResult();
|
||||
await _release.Task.WaitAsync(cancellationToken);
|
||||
|
||||
return [new ActivityDescriptor
|
||||
{
|
||||
TypeName = nameof(BlockingTenantAgnosticProvider),
|
||||
Version = 1,
|
||||
TenantId = Tenant.AgnosticTenantId,
|
||||
Kind = ActivityKind.Action
|
||||
}];
|
||||
}
|
||||
|
||||
public void Release() => _release.TrySetResult();
|
||||
}
|
||||
|
||||
private sealed class CountingTenantSensitiveProvider : IActivityProvider
|
||||
{
|
||||
private int _callCount;
|
||||
|
||||
public int CallCount => Volatile.Read(ref _callCount);
|
||||
|
||||
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref _callCount);
|
||||
return new([new ActivityDescriptor
|
||||
{
|
||||
TypeName = nameof(CountingTenantSensitiveProvider),
|
||||
Version = 1,
|
||||
TenantId = CurrentTenant,
|
||||
Kind = ActivityKind.Action
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
using Elsa.Common.Multitenancy;
|
||||
using Elsa.Workflows.Management.Activities.HostMethod;
|
||||
using Elsa.Workflows.Management.Activities.WorkflowDefinitionActivity;
|
||||
using Elsa.Workflows.Management.Providers;
|
||||
using Elsa.Workflows.Management.Services;
|
||||
using Elsa.Workflows.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Workflows.Management.UnitTests.Services;
|
||||
|
||||
public class ActivityRegistryPopulatorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task EnsureRegistryPopulatedAsync_InitializesAgnosticProvidersOnceAcrossScopedPopulators_AndRefreshesTenantSensitiveProvidersPerPass()
|
||||
{
|
||||
// Arrange
|
||||
var agnosticProviderCallCount = new CallCounter();
|
||||
var tenantSensitiveProviderCallCount = new CallCounter();
|
||||
var registry = CreateRegistry();
|
||||
IActivityRegistryPopulator firstScopePopulator = new ActivityRegistryPopulator(
|
||||
[new CountingTenantAgnosticProvider(agnosticProviderCallCount), new CountingTenantSensitiveProvider(tenantSensitiveProviderCallCount)], registry);
|
||||
IActivityRegistryPopulator secondScopePopulator = new ActivityRegistryPopulator(
|
||||
[new CountingTenantAgnosticProvider(agnosticProviderCallCount), new CountingTenantSensitiveProvider(tenantSensitiveProviderCallCount)], registry);
|
||||
|
||||
// Act
|
||||
await firstScopePopulator.EnsureRegistryPopulatedAsync();
|
||||
await secondScopePopulator.EnsureRegistryPopulatedAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1, agnosticProviderCallCount.Value);
|
||||
Assert.Equal(2, tenantSensitiveProviderCallCount.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PopulateRegistryAsync_ForceRefreshesAllProvidersOnEveryPass()
|
||||
{
|
||||
// Arrange
|
||||
var agnosticProviderCallCount = new CallCounter();
|
||||
var tenantSensitiveProviderCallCount = new CallCounter();
|
||||
var agnosticProvider = new CountingTenantAgnosticProvider(agnosticProviderCallCount);
|
||||
var tenantSensitiveProvider = new CountingTenantSensitiveProvider(tenantSensitiveProviderCallCount);
|
||||
IActivityRegistryPopulator populator = new ActivityRegistryPopulator([agnosticProvider, tenantSensitiveProvider], CreateRegistry());
|
||||
|
||||
// Act
|
||||
await populator.PopulateRegistryAsync();
|
||||
await populator.PopulateRegistryAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, agnosticProviderCallCount.Value);
|
||||
Assert.Equal(2, tenantSensitiveProviderCallCount.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuiltInTenantAgnosticProviders_OptIntoOncePerRegistryPopulation()
|
||||
{
|
||||
Assert.True(typeof(ITenantAgnosticActivityProvider).IsAssignableFrom(typeof(TypedActivityProvider)));
|
||||
Assert.True(typeof(ITenantAgnosticActivityProvider).IsAssignableFrom(typeof(HostMethodActivityProvider)));
|
||||
Assert.False(typeof(ITenantAgnosticActivityProvider).IsAssignableFrom(typeof(WorkflowDefinitionActivityProvider)));
|
||||
}
|
||||
|
||||
private static ActivityRegistry CreateRegistry()
|
||||
{
|
||||
var tenantAccessor = Substitute.For<ITenantAccessor>();
|
||||
tenantAccessor.TenantId.Returns("tenant-1");
|
||||
return new ActivityRegistry(
|
||||
Substitute.For<IActivityDescriber>(),
|
||||
[],
|
||||
tenantAccessor,
|
||||
Substitute.For<ILogger<ActivityRegistry>>());
|
||||
}
|
||||
|
||||
private sealed class CountingTenantAgnosticProvider(CallCounter callCounter) : ITenantAgnosticActivityProvider
|
||||
{
|
||||
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
callCounter.Increment();
|
||||
return new([new ActivityDescriptor
|
||||
{
|
||||
TypeName = nameof(CountingTenantAgnosticProvider),
|
||||
Version = 1,
|
||||
TenantId = Tenant.AgnosticTenantId,
|
||||
Kind = ActivityKind.Action
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CountingTenantSensitiveProvider(CallCounter callCounter) : IActivityProvider
|
||||
{
|
||||
public ValueTask<IEnumerable<ActivityDescriptor>> GetDescriptorsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
callCounter.Increment();
|
||||
return new([new ActivityDescriptor
|
||||
{
|
||||
TypeName = nameof(CountingTenantSensitiveProvider),
|
||||
Version = 1,
|
||||
TenantId = "tenant-1",
|
||||
Kind = ActivityKind.Action
|
||||
}]);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CallCounter
|
||||
{
|
||||
private int _value;
|
||||
|
||||
public int Value => Volatile.Read(ref _value);
|
||||
|
||||
public void Increment() => Interlocked.Increment(ref _value);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,18 +9,19 @@ namespace Elsa.Workflows.Runtime.UnitTests.Services;
|
|||
public class DefaultRegistriesPopulatorTests
|
||||
{
|
||||
private readonly IWorkflowDefinitionStorePopulator _workflowDefinitionStorePopulatorMock;
|
||||
private readonly IActivityRegistryPopulator _activityRegistryPopulatorMock;
|
||||
private readonly INotificationSender _notificationSenderMock;
|
||||
private readonly DefaultRegistriesPopulator _populator;
|
||||
|
||||
public DefaultRegistriesPopulatorTests()
|
||||
{
|
||||
_workflowDefinitionStorePopulatorMock = Substitute.For<IWorkflowDefinitionStorePopulator>();
|
||||
var activityRegistryPopulatorMock = Substitute.For<IActivityRegistryPopulator>();
|
||||
_activityRegistryPopulatorMock = Substitute.For<IActivityRegistryPopulator>();
|
||||
_notificationSenderMock = Substitute.For<INotificationSender>();
|
||||
|
||||
_populator = new(
|
||||
_workflowDefinitionStorePopulatorMock,
|
||||
activityRegistryPopulatorMock,
|
||||
_activityRegistryPopulatorMock,
|
||||
_notificationSenderMock);
|
||||
}
|
||||
|
||||
|
|
@ -56,4 +57,28 @@ public class DefaultRegistriesPopulatorTests
|
|||
Arg.Is<WorkflowDefinitionsReloaded>(n => n.ReloadedWorkflowDefinitions.Count == 2),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PopulateAsync_EnsuresActivityRegistryBeforeEachWorkflowDefinitionStorePopulation()
|
||||
{
|
||||
// Arrange
|
||||
_workflowDefinitionStorePopulatorMock
|
||||
.PopulateStoreAsync(Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Array.Empty<WorkflowDefinition>());
|
||||
|
||||
// Act
|
||||
await _populator.PopulateAsync();
|
||||
|
||||
// Assert
|
||||
Received.InOrder(() =>
|
||||
{
|
||||
_activityRegistryPopulatorMock.EnsureRegistryPopulatedAsync(Arg.Any<CancellationToken>());
|
||||
_workflowDefinitionStorePopulatorMock.PopulateStoreAsync(false, Arg.Any<CancellationToken>());
|
||||
_activityRegistryPopulatorMock.EnsureRegistryPopulatedAsync(Arg.Any<CancellationToken>());
|
||||
_workflowDefinitionStorePopulatorMock.PopulateStoreAsync(true, Arg.Any<CancellationToken>());
|
||||
_notificationSenderMock.SendAsync(Arg.Any<WorkflowDefinitionsReloaded>(), Arg.Any<CancellationToken>());
|
||||
});
|
||||
|
||||
await _activityRegistryPopulatorMock.Received(2).EnsureRegistryPopulatedAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue