Skip to content

Commit ba57f1b

Browse files
authored
fix scoped race (#86)
* fix race * simplify
1 parent 8dedaa7 commit ba57f1b

File tree

2 files changed

+43
-18
lines changed

2 files changed

+43
-18
lines changed

BitFaster.Caching.UnitTests/ScopedTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ public void WhenScopeIsDisposedCreateScopeThrows()
5050
scope.Invoking(s => s.CreateLifetime()).Should().Throw<ObjectDisposedException>();
5151
}
5252

53+
[Fact]
54+
public void WhenScopeIsDisposedTryCreateScopeReturnsFalse()
55+
{
56+
var disposable = new Disposable();
57+
var scope = new Scoped<Disposable>(disposable);
58+
scope.Dispose();
59+
60+
scope.TryCreateLifetime(out var l).Should().BeFalse();
61+
}
62+
5363
[Fact]
5464
public void WhenScopedIsCreatedFromCacheItemHasExpectedLifetime()
5565
{

BitFaster.Caching/Scoped.cs

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,34 @@ public Scoped(T value)
2525
this.refCount = new ReferenceCount<T>(value);
2626
}
2727

28+
/// <summary>
29+
/// Attempts to create a lifetime for the scoped value. The lifetime guarantees the value is alive until
30+
/// the lifetime is disposed.
31+
/// </summary>
32+
/// <param name="lifetime">When this method returns, contains the Lifetime that was created, or the default value of the type if the operation failed.</param>
33+
/// <returns>true if the Lifetime was created; otherwise false.</returns>
34+
public bool TryCreateLifetime(out Lifetime<T> lifetime)
35+
{
36+
while (true)
37+
{
38+
var oldRefCount = this.refCount;
39+
40+
// If old ref count is 0, the scoped object has been disposed and there was a race.
41+
if (this.isDisposed || oldRefCount.Count == 0)
42+
{
43+
lifetime = default;
44+
return false;
45+
}
46+
47+
if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.IncrementCopy(), oldRefCount))
48+
{
49+
// When Lifetime is disposed, it calls DecrementReferenceCount
50+
lifetime = new Lifetime<T>(oldRefCount, this.DecrementReferenceCount);
51+
return true;
52+
}
53+
}
54+
}
55+
2856
/// <summary>
2957
/// Creates a lifetime for the scoped value. The lifetime guarantees the value is alive until
3058
/// the lifetime is disposed.
@@ -33,38 +61,25 @@ public Scoped(T value)
3361
/// <exception cref="ObjectDisposedException">The scope is disposed.</exception>
3462
public Lifetime<T> CreateLifetime()
3563
{
36-
if (this.isDisposed)
64+
if (!TryCreateLifetime(out var lifetime))
3765
{
3866
throw new ObjectDisposedException($"{nameof(T)} is disposed.");
3967
}
4068

41-
while (true)
42-
{
43-
// IncrementCopy will throw ObjectDisposedException if the referenced object has no references.
44-
// This mitigates the race where the value is disposed after the above check is run.
45-
var oldRefCount = this.refCount;
46-
var newRefCount = oldRefCount.IncrementCopy();
47-
48-
if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount))
49-
{
50-
// When Lease is disposed, it calls DecrementReferenceCount
51-
return new Lifetime<T>(oldRefCount, this.DecrementReferenceCount);
52-
}
53-
}
69+
return lifetime;
5470
}
5571

5672
private void DecrementReferenceCount()
5773
{
5874
while (true)
5975
{
6076
var oldRefCount = this.refCount;
61-
var newRefCount = oldRefCount.DecrementCopy();
6277

63-
if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, newRefCount, oldRefCount))
78+
if (oldRefCount == Interlocked.CompareExchange(ref this.refCount, oldRefCount.DecrementCopy(), oldRefCount))
6479
{
65-
if (newRefCount.Count == 0)
80+
if (this.refCount.Count == 0)
6681
{
67-
newRefCount.Value.Dispose();
82+
this.refCount.Value.Dispose();
6883
}
6984

7085
break;

0 commit comments

Comments
 (0)