Skip to content

Commit 14bdf08

Browse files
authored
Merge pull request #35 from NorekZ/async_dispose_fix
Thread-affinity issue during async dispose fixed
2 parents ad17024 + 90a126d commit 14bdf08

File tree

2 files changed

+142
-141
lines changed

2 files changed

+142
-141
lines changed

src/Autofac.Multitenant/MultitenantContainer.cs

Lines changed: 64 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Autofac Project. All rights reserved.
22
// Licensed under the MIT License. See LICENSE in the project root for license information.
33

4+
using System.Collections.Concurrent;
45
using System.Diagnostics;
56
using System.Globalization;
67
#if NET5_0_OR_GREATER
@@ -79,13 +80,12 @@ public class MultitenantContainer : Disposable, IContainer
7980
/// is <see cref="object"/>, value is <see cref="ILifetimeScope"/>.
8081
/// </summary>
8182
// Issue #280: Incorrect double-checked-lock pattern usage in MultitenantContainer.GetTenantScope
82-
private readonly Dictionary<object, ILifetimeScope> _tenantLifetimeScopes = new();
83+
private readonly ConcurrentDictionary<object, ILifetimeScope> _tenantLifetimeScopes = new();
8384

8485
/// <summary>
85-
/// Reader-writer lock for locking modifications and initializations
86-
/// of tenant scopes.
86+
/// Flag that disallows creating a new scope while disposing or after that.
8787
/// </summary>
88-
private readonly ReaderWriterLockSlim _readWriteLock = new();
88+
private int _isDisposed;
8989

9090
/// <summary>
9191
/// Initializes a new instance of the <see cref="MultitenantContainer"/> class.
@@ -329,28 +329,37 @@ public void ConfigureTenant(object tenantId, Action<ContainerBuilder> configurat
329329

330330
tenantId ??= _defaultTenantId;
331331

332-
_readWriteLock.EnterUpgradeableReadLock();
333-
try
332+
if (_tenantLifetimeScopes.ContainsKey(tenantId))
334333
{
335-
if (_tenantLifetimeScopes.ContainsKey(tenantId))
336-
{
337-
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.MultitenantContainer_TenantAlreadyConfigured, tenantId));
338-
}
334+
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.MultitenantContainer_TenantAlreadyConfigured, tenantId));
335+
}
339336

340-
_readWriteLock.EnterWriteLock();
341-
try
342-
{
343-
_tenantLifetimeScopes[tenantId] = ApplicationContainer.BeginLifetimeScope(TenantLifetimeScopeTag, configuration);
344-
}
345-
finally
346-
{
347-
_readWriteLock.ExitWriteLock();
348-
}
337+
CreateTenantScope(tenantId, configuration);
338+
}
339+
340+
/// <summary>
341+
/// Creates new tenant scope without any locking. Uses optimistic approach - creates the scope and in case it fails to insert to the dictionary it's immediately disposed.
342+
/// This should happen very rarely, hopefully never.
343+
/// </summary>
344+
private ILifetimeScope CreateTenantScope(object tenantId, Action<ContainerBuilder> configuration = null)
345+
{
346+
if (_isDisposed == 1)
347+
{
348+
throw new ObjectDisposedException(nameof(ApplicationContainer));
349349
}
350-
finally
350+
351+
var lifetimeScope = configuration != null
352+
? ApplicationContainer.BeginLifetimeScope(TenantLifetimeScopeTag, configuration)
353+
: ApplicationContainer.BeginLifetimeScope(TenantLifetimeScopeTag);
354+
355+
var setLifetimeScope = _tenantLifetimeScopes.GetOrAdd(tenantId, lifetimeScope);
356+
357+
if (setLifetimeScope != lifetimeScope)
351358
{
352-
_readWriteLock.ExitUpgradeableReadLock();
359+
lifetimeScope.Dispose();
353360
}
361+
362+
return setLifetimeScope;
354363
}
355364

356365
/// <summary>
@@ -393,26 +402,16 @@ public bool ReconfigureTenant(object tenantId, Action<ContainerBuilder> configur
393402

394403
tenantId ??= _defaultTenantId;
395404

396-
// we're going to change the dictionary either way, dispense with the read-check
397-
_readWriteLock.EnterWriteLock();
398-
try
405+
var removed = false;
406+
if (_tenantLifetimeScopes.TryRemove(tenantId, out var tenantScope))
399407
{
400-
var removed = false;
401-
if (_tenantLifetimeScopes.TryGetValue(tenantId, out var tenantScope) && tenantScope != null)
402-
{
403-
tenantScope.Dispose();
404-
405-
removed = _tenantLifetimeScopes.Remove(tenantId);
406-
}
408+
tenantScope.Dispose();
409+
removed = true;
410+
}
407411

408-
_tenantLifetimeScopes[tenantId] = ApplicationContainer.BeginLifetimeScope(TenantLifetimeScopeTag, configuration);
412+
CreateTenantScope(tenantId, configuration);
409413

410-
return removed;
411-
}
412-
finally
413-
{
414-
_readWriteLock.ExitWriteLock();
415-
}
414+
return removed;
416415
}
417416

418417
/// <summary>
@@ -449,57 +448,23 @@ public ILifetimeScope GetTenantScope(object tenantId)
449448
{
450449
tenantId ??= _defaultTenantId;
451450

452-
var tenantScope = (ILifetimeScope)null;
453-
_readWriteLock.EnterReadLock();
454-
try
451+
if (_tenantLifetimeScopes.TryGetValue(tenantId, out var tenantScope))
455452
{
456-
_tenantLifetimeScopes.TryGetValue(tenantId, out tenantScope);
457-
}
458-
finally
459-
{
460-
_readWriteLock.ExitReadLock();
461-
}
462-
463-
if (tenantScope == null)
464-
{
465-
// just go straight to write-lock, chances of not needing it at this point would be low
466-
_readWriteLock.EnterWriteLock();
467-
468-
try
469-
{
470-
// The check and [potential] scope creation are locked here to
471-
// ensure atomicity. We don't want to check and then have another
472-
// thread create the lifetime scope behind our backs.
473-
if (!_tenantLifetimeScopes.TryGetValue(tenantId, out tenantScope) || tenantScope == null)
474-
{
475-
tenantScope = ApplicationContainer.BeginLifetimeScope(TenantLifetimeScopeTag);
476-
_tenantLifetimeScopes[tenantId] = tenantScope;
477-
}
478-
}
479-
finally
480-
{
481-
_readWriteLock.ExitWriteLock();
482-
}
453+
return tenantScope;
483454
}
484455

485-
return tenantScope;
456+
return CreateTenantScope(tenantId);
486457
}
487458

488459
/// <summary>
489460
/// Returns collection of all registered tenants IDs.
490461
/// </summary>
462+
#pragma warning disable CA1024
491463
public IEnumerable<object> GetTenants()
492464
{
493-
_readWriteLock.EnterReadLock();
494-
try
495-
{
496-
return new List<object>(_tenantLifetimeScopes.Keys);
497-
}
498-
finally
499-
{
500-
_readWriteLock.ExitReadLock();
501-
}
465+
return new List<object>(_tenantLifetimeScopes.Keys);
502466
}
467+
#pragma warning restore CA1024
503468

504469
/// <summary>
505470
/// Returns whether the given tenant ID has been configured.
@@ -508,17 +473,9 @@ public IEnumerable<object> GetTenants()
508473
/// <returns>If configured, <c>true</c>; otherwise <c>false</c>.</returns>
509474
public bool TenantIsConfigured(object tenantId)
510475
{
511-
_readWriteLock.EnterReadLock();
512-
try
513-
{
514-
tenantId ??= _defaultTenantId;
476+
tenantId ??= _defaultTenantId;
515477

516-
return _tenantLifetimeScopes.ContainsKey(tenantId);
517-
}
518-
finally
519-
{
520-
_readWriteLock.ExitReadLock();
521-
}
478+
return _tenantLifetimeScopes.ContainsKey(tenantId);
522479
}
523480

524481
/// <summary>
@@ -530,43 +487,27 @@ public bool RemoveTenant(object tenantId)
530487
{
531488
tenantId ??= _defaultTenantId;
532489

533-
// this should be a fairly rare operation, so we'll jump right to the write-lock
534-
_readWriteLock.EnterWriteLock();
535-
try
490+
if (_tenantLifetimeScopes.TryRemove(tenantId, out var tenantScope))
536491
{
537-
if (_tenantLifetimeScopes.TryGetValue(tenantId, out var tenantScope) && tenantScope != null)
538-
{
539-
tenantScope.Dispose();
492+
tenantScope.Dispose();
540493

541-
return _tenantLifetimeScopes.Remove(tenantId);
542-
}
543-
544-
return false;
545-
}
546-
finally
547-
{
548-
_readWriteLock.ExitWriteLock();
494+
return true;
549495
}
496+
497+
return false;
550498
}
551499

552500
/// <summary>
553501
/// Clears all tenants configurations and disposes the associated lifetime scopes.
554502
/// </summary>
555503
public void ClearTenants()
556504
{
557-
_readWriteLock.EnterWriteLock();
558-
try
505+
foreach (var tenantId in _tenantLifetimeScopes.Keys)
559506
{
560-
foreach (var tenantScope in _tenantLifetimeScopes.Values)
507+
if (_tenantLifetimeScopes.TryRemove(tenantId, out var tenantScope))
561508
{
562509
tenantScope.Dispose();
563510
}
564-
565-
_tenantLifetimeScopes.Clear();
566-
}
567-
finally
568-
{
569-
_readWriteLock.ExitWriteLock();
570511
}
571512
}
572513

@@ -598,26 +539,16 @@ public object ResolveComponent(ResolveRequest request)
598539
/// </param>
599540
protected override void Dispose(bool disposing)
600541
{
542+
Interlocked.Exchange(ref _isDisposed, 1);
543+
601544
if (disposing)
602545
{
603-
// Lock the lifetime scope table so no threads can add new lifetime
604-
// scopes while we're disposing.
605-
_readWriteLock.EnterWriteLock();
606-
607-
try
608-
{
609-
foreach (var scope in _tenantLifetimeScopes.Values)
610-
{
611-
scope.Dispose();
612-
}
613-
614-
ApplicationContainer.Dispose();
615-
}
616-
finally
546+
foreach (var scope in _tenantLifetimeScopes.Values)
617547
{
618-
_readWriteLock.ExitWriteLock();
619-
_readWriteLock.Dispose();
548+
scope.Dispose();
620549
}
550+
551+
ApplicationContainer.Dispose();
621552
}
622553

623554
base.Dispose(disposing);
@@ -632,24 +563,16 @@ protected override void Dispose(bool disposing)
632563
/// </param>
633564
protected override async ValueTask DisposeAsync(bool disposing)
634565
{
566+
Interlocked.Exchange(ref _isDisposed, 1);
567+
635568
if (disposing)
636569
{
637-
_readWriteLock.EnterWriteLock();
638-
639-
try
570+
foreach (var scope in _tenantLifetimeScopes.Values)
640571
{
641-
foreach (var scope in _tenantLifetimeScopes.Values)
642-
{
643-
await scope.DisposeAsync().ConfigureAwait(false);
644-
}
645-
646-
await ApplicationContainer.DisposeAsync().ConfigureAwait(false);
647-
}
648-
finally
649-
{
650-
_readWriteLock.ExitWriteLock();
651-
_readWriteLock.Dispose();
572+
await scope.DisposeAsync().ConfigureAwait(false);
652573
}
574+
575+
await ApplicationContainer.DisposeAsync().ConfigureAwait(false);
653576
}
654577

655578
// Do not call the base, otherwise the standard Dispose will fire.

0 commit comments

Comments
 (0)