Skip to content

Commit 3ee0aaf

Browse files
authored
Fix object disposed exception in SingletonCache.Release (#66)
* fix obj disposed ex
1 parent 7237c9b commit 3ee0aaf

File tree

6 files changed

+84
-37
lines changed

6 files changed

+84
-37
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Xunit.Abstractions;
8+
using Xunit.Sdk;
9+
10+
[assembly: Xunit.TestFramework("BitFaster.Caching.UnitTests.AssemblyInitialize", "BitFaster.Caching.UnitTests")]
11+
12+
namespace BitFaster.Caching.UnitTests
13+
{
14+
public class AssemblyInitialize : XunitTestFramework
15+
{
16+
public AssemblyInitialize(IMessageSink messageSink)
17+
: base(messageSink)
18+
{
19+
ThreadPool.SetMinThreads(16, 16);
20+
}
21+
22+
public new void Dispose()
23+
{
24+
// Place tear down code here
25+
base.Dispose();
26+
}
27+
}
28+
}

BitFaster.Caching.UnitTests/ReferenceCountTests.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,21 +52,5 @@ public void WhenObjectsAreDifferentHashcodesAreDifferent()
5252

5353
a.GetHashCode().Should().NotBe(b.GetHashCode());
5454
}
55-
56-
[Fact]
57-
public void WhenObjectDisposed()
58-
{
59-
var a = new ReferenceCount<Disposable>(new Disposable());
60-
var b = a.DecrementCopy();
61-
62-
b.Invoking(rc => rc.IncrementCopy()).Should().Throw<ObjectDisposedException>();
63-
}
64-
65-
private class Disposable : IDisposable
66-
{
67-
public void Dispose()
68-
{
69-
}
70-
}
7155
}
7256
}

BitFaster.Caching.UnitTests/SingletonCacheTests.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using FluentAssertions;
@@ -145,21 +146,48 @@ public void WhenValueIsDisposableItIsDisposedWhenReleased()
145146
{
146147
var cache = new SingletonCache<string, DisposeTest>();
147148

149+
DisposeTest value = null;
150+
148151
using (var lifetime = cache.Acquire("Foo"))
149152
{
150-
DisposeTest.WasDisposed.Should().BeFalse();
153+
value = lifetime.Value;
154+
value.IsDisposed.Should().BeFalse();
151155
}
152156

153-
DisposeTest.WasDisposed.Should().BeTrue();
157+
value.IsDisposed.Should().BeTrue();
158+
}
159+
160+
[Fact]
161+
public async Task DisposeOnManyDifferentThreadsAlwaysReturnsActiveValue()
162+
{
163+
var cache = new SingletonCache<string, DisposeTest>();
164+
165+
var tasks = Enumerable.Range(0, 64).Select(i => Task.Run(() =>
166+
{
167+
using (var handle = cache.Acquire("Foo"))
168+
{
169+
handle.Value.ThrowIfDisposed();
170+
}
171+
}));
172+
173+
await Task.WhenAll(tasks);
154174
}
155175

156176
public class DisposeTest : IDisposable
157177
{
158-
public static bool WasDisposed { get; set; }
178+
public bool IsDisposed { get; private set; }
179+
180+
public void ThrowIfDisposed()
181+
{
182+
if (this.IsDisposed)
183+
{
184+
throw new ObjectDisposedException("Error");
185+
}
186+
}
159187

160188
public void Dispose()
161189
{
162-
WasDisposed = true;
190+
this.IsDisposed = true;
163191
}
164192
}
165193
}

BitFaster.Caching/BitFaster.Caching.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<Description>High performance, thread-safe in-memory caching primitives for .NET.</Description>
99
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1010
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
11-
<Version>1.0.2</Version>
11+
<Version>1.0.3</Version>
1212
<Copyright>Copyright © Alex Peck 2020</Copyright>
1313
<PackageProjectUrl></PackageProjectUrl>
1414
<RepositoryUrl>https://github.com/bitfaster/BitFaster.Caching</RepositoryUrl>
@@ -18,8 +18,8 @@
1818
<RootNamespace>BitFaster.Caching</RootNamespace>
1919
<IncludeSource>True</IncludeSource>
2020
<IncludeSymbols>True</IncludeSymbols>
21-
<AssemblyVersion>1.0.2.0</AssemblyVersion>
22-
<FileVersion>1.0.2.0</FileVersion>
21+
<AssemblyVersion>1.0.3.0</AssemblyVersion>
22+
<FileVersion>1.0.3.0</FileVersion>
2323
</PropertyGroup>
2424

2525
<ItemGroup>

BitFaster.Caching/ReferenceCount.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@ public int Count
5353
/// <returns>A copy of the ReferenceCount with the count incremented by 1.</returns>
5454
public ReferenceCount<TValue> IncrementCopy()
5555
{
56-
if (this.count <= 0 && this.value is IDisposable)
57-
{
58-
throw new ObjectDisposedException($"{typeof(TValue).Name} is disposed.");
59-
}
60-
6156
return new ReferenceCount<TValue>(this.value, this.count + 1);
6257
}
6358

BitFaster.Caching/SingletonCache.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,32 @@ private void Release(TKey key)
5959
{
6060
while (true)
6161
{
62-
var oldRefCount = this.cache[key];
63-
var newRefCount = oldRefCount.DecrementCopy();
64-
if (this.cache.TryUpdate(key, newRefCount, oldRefCount))
62+
if (!this.cache.TryGetValue(key, out var oldRefCount))
6563
{
66-
if (newRefCount.Count == 0)
64+
// already released, exit
65+
break;
66+
}
67+
68+
// if count is 1, the caller's decrement makes refcount 0: it is unreferenced and eligible to remove
69+
if (oldRefCount.Count == 1)
70+
{
71+
var kvp = new KeyValuePair<TKey, ReferenceCount<TValue>>(key, oldRefCount);
72+
73+
// hidden atomic remove
74+
// https://devblogs.microsoft.com/pfxteam/little-known-gems-atomic-conditional-removals-from-concurrentdictionary/
75+
if (((ICollection<KeyValuePair<TKey, ReferenceCount<TValue>>>)this.cache).Remove(kvp))
6776
{
68-
if (((IDictionary<TKey, ReferenceCount<TValue>>)this.cache).Remove(new KeyValuePair<TKey, ReferenceCount<TValue>>(key, newRefCount)))
77+
// no longer in cache, safe to dispose and exit
78+
if (oldRefCount.Value is IDisposable d)
6979
{
70-
if (newRefCount.Value is IDisposable d)
71-
{
72-
d.Dispose();
73-
}
80+
d.Dispose();
7481
}
82+
break;
7583
}
84+
}
85+
else if (this.cache.TryUpdate(key, oldRefCount.DecrementCopy(), oldRefCount))
86+
{
87+
// replaced with decremented copy, exit
7688
break;
7789
}
7890
}

0 commit comments

Comments
 (0)