Skip to content

Commit

Permalink
Merge pull request #6205 from elsa-workflows/enh/thread-safe-tenant-s…
Browse files Browse the repository at this point in the history
…ervice

Add concurrency locks to DefaultTenantService operations
  • Loading branch information
sfmskywalker authored Dec 12, 2024
2 parents ad4eb74 + 2656e83 commit 9eeed44
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ namespace Elsa.Common.Multitenancy;
public class DefaultTenantService(IServiceScopeFactory scopeFactory, ITenantScopeFactory tenantScopeFactory, TenantEventsManager tenantEvents, ITenantAccessor tenantAccessor) : ITenantService, IAsyncDisposable
{
private readonly AsyncServiceScope _serviceScope = scopeFactory.CreateAsyncScope();
private readonly SemaphoreSlim _initializationLock = new(1, 1);
private readonly SemaphoreSlim _refreshLock = new(1, 1);
private IDictionary<string, Tenant>? _tenantsDictionary;
private IDictionary<Tenant, TenantScope>? _tenantScopesDictionary;

public async ValueTask DisposeAsync()
{
await _serviceScope.DisposeAsync();
_initializationLock.Dispose();
}

public async Task<Tenant?> FindAsync(string id, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -56,47 +59,68 @@ public async Task ActivateTenantsAsync(CancellationToken cancellationToken = def

public async Task DeactivateTenantsAsync(CancellationToken cancellationToken = default)
{
var tenants = _tenantsDictionary!.Values.ToArray();
var dictionary = await GetTenantsDictionaryAsync(cancellationToken);
var tenants = dictionary.Values.ToArray();

foreach (var tenant in tenants)
await UnregisterTenantAsync(tenant, cancellationToken);
}

public async Task RefreshAsync(CancellationToken cancellationToken = default)
{
await using var scope = scopeFactory.CreateAsyncScope();
var tenantsProvider = scope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var currentTenants = await GetTenantsDictionaryAsync(cancellationToken);
var currentTenantIds = currentTenants.Keys;
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id);
var newTenantIds = newTenants.Keys;
var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray();
var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray();

foreach (var removedTenantId in removedTenantIds)
await _refreshLock.WaitAsync(cancellationToken);

try
{
var removedTenant = currentTenants[removedTenantId];
await UnregisterTenantAsync(removedTenant, cancellationToken);
await using var scope = scopeFactory.CreateAsyncScope();
var tenantsProvider = scope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var currentTenants = await GetTenantsDictionaryAsync(cancellationToken);
var currentTenantIds = currentTenants.Keys;
var newTenants = (await tenantsProvider.ListAsync(cancellationToken)).ToDictionary(x => x.Id);
var newTenantIds = newTenants.Keys;
var removedTenantIds = currentTenantIds.Except(newTenantIds).ToArray();
var addedTenantIds = newTenantIds.Except(currentTenantIds).ToArray();

foreach (var removedTenantId in removedTenantIds)
{
var removedTenant = currentTenants[removedTenantId];
await UnregisterTenantAsync(removedTenant, cancellationToken);
}

foreach (var addedTenantId in addedTenantIds)
{
var addedTenant = newTenants[addedTenantId];
await RegisterTenantAsync(addedTenant, cancellationToken);
}
}

foreach (var addedTenantId in addedTenantIds)
finally
{
var addedTenant = newTenants[addedTenantId];
await RegisterTenantAsync(addedTenant, cancellationToken);
_refreshLock.Release();
}
}

private async Task<IDictionary<string, Tenant>> GetTenantsDictionaryAsync(CancellationToken cancellationToken)
{
if (_tenantsDictionary == null)
{
_tenantsDictionary = new Dictionary<string, Tenant>();
_tenantScopesDictionary = new Dictionary<Tenant, TenantScope>();
var tenantsProvider = _serviceScope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var tenants = await tenantsProvider.ListAsync(cancellationToken);

foreach (var tenant in tenants)
await RegisterTenantAsync(tenant, cancellationToken);
await _initializationLock.WaitAsync(cancellationToken); // Lock to ensure single-threaded initialization
try
{
if (_tenantsDictionary == null) // Double-check locking
{
_tenantsDictionary = new Dictionary<string, Tenant>();
_tenantScopesDictionary = new Dictionary<Tenant, TenantScope>();
var tenantsProvider = _serviceScope.ServiceProvider.GetRequiredService<ITenantsProvider>();
var tenants = await tenantsProvider.ListAsync(cancellationToken);

foreach (var tenant in tenants)
await RegisterTenantAsync(tenant, cancellationToken);
}
}
finally
{
_initializationLock.Release();
}
}

return _tenantsDictionary;
Expand All @@ -114,11 +138,12 @@ private async Task RegisterTenantAsync(Tenant tenant, CancellationToken cancella

private async Task UnregisterTenantAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
var scope = _tenantScopesDictionary![tenant];
_tenantsDictionary!.Remove(tenant.Id);
_tenantScopesDictionary!.Remove(tenant);
if (_tenantScopesDictionary!.Remove(tenant, out var scope))
{
_tenantsDictionary!.Remove(tenant.Id, out _);

using (tenantAccessor.PushContext(tenant))
await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken));
using (tenantAccessor.PushContext(tenant))
await tenantEvents.TenantDeactivatedAsync(new TenantDeactivatedEventArgs(tenant, scope, cancellationToken));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class ModuleExtensions
/// <summary>
/// Installs and configures the <see cref="TenantsFeature"/> feature.
/// </summary>
public static IModule UseTenantHttpRouting(this IModule module, Action<MultitenantHttpRoutingFeature>? configure = default)
public static IModule UseTenantHttpRouting(this IModule module, Action<MultitenantHttpRoutingFeature>? configure = null)
{
module.Configure(configure);
return module;
Expand Down

0 comments on commit 9eeed44

Please sign in to comment.