Skip to content

Commit fea0207

Browse files
Merge pull request #194 from kroymann/enable_null_cache
Restore ability to use null cache
2 parents 5cb589c + 0a9a0fb commit fea0207

File tree

14 files changed

+1418
-107
lines changed

14 files changed

+1418
-107
lines changed

src/ImageSharp.Web/DependencyInjection/ServiceCollectionExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using SixLabors.ImageSharp.Web.Middleware;
1212
using SixLabors.ImageSharp.Web.Processors;
1313
using SixLabors.ImageSharp.Web.Providers;
14+
using SixLabors.ImageSharp.Web.Synchronization;
1415

1516
namespace SixLabors.ImageSharp.Web.DependencyInjection
1617
{
@@ -57,6 +58,8 @@ private static void AddDefaultServices(
5758

5859
builder.Services.AddSingleton<FormatUtilities>();
5960

61+
builder.Services.AddSingleton<AsyncKeyReaderWriterLock<string>>();
62+
6063
builder.SetRequestParser<QueryCollectionRequestParser>();
6164

6265
builder.SetCache<PhysicalFileSystemCache>();

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 83 additions & 107 deletions
Large diffs are not rendered by default.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Threading.Tasks;
7+
8+
#nullable enable
9+
10+
namespace SixLabors.ImageSharp.Web.Synchronization
11+
{
12+
/// <summary>
13+
/// Extension of the <see cref="AsyncLock"/> that enables fine-grained locking on a given key.
14+
/// Concurrent lock requests using different keys can execute simultaneously, while requests to lock
15+
/// using the same key will be forced to wait. This object is thread-safe and internally uses a pooling
16+
/// mechanism to minimize allocation of new locks.
17+
/// </summary>
18+
/// <typeparam name="TKey">The type of the key.</typeparam>
19+
public class AsyncKeyLock<TKey> : IDisposable
20+
where TKey : notnull
21+
{
22+
private readonly RefCountedConcurrentDictionary<TKey, AsyncLock> activeLocks;
23+
private readonly ConcurrentBag<AsyncLock> pool;
24+
private readonly int maxPoolSize;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="AsyncKeyLock{TKey}"/> class.
28+
/// </summary>
29+
/// <param name="maxPoolSize">The maximum number of locks that should be pooled for reuse.</param>
30+
public AsyncKeyLock(int maxPoolSize = 64)
31+
{
32+
this.pool = new ConcurrentBag<AsyncLock>();
33+
this.activeLocks = new RefCountedConcurrentDictionary<TKey, AsyncLock>(this.CreateLeasedLock, this.ReturnLeasedLock);
34+
this.maxPoolSize = maxPoolSize;
35+
}
36+
37+
/// <summary>
38+
/// Locks the current thread asynchronously.
39+
/// </summary>
40+
/// <param name="key">The key identifying the specific object to lock against.</param>
41+
/// <returns>
42+
/// The <see cref="IDisposable"/> that will release the lock.
43+
/// </returns>
44+
public Task<IDisposable> LockAsync(TKey key)
45+
=> this.activeLocks.Get(key).LockAsync();
46+
47+
/// <summary>
48+
/// Releases all resources used by the current instance of the <see cref="AsyncKeyLock{TKey}"/> class.
49+
/// </summary>
50+
public void Dispose()
51+
{
52+
// Dispose of all the locks in the pool
53+
while (this.pool.TryTake(out AsyncLock? asyncLock))
54+
{
55+
asyncLock.Dispose();
56+
}
57+
58+
// activeLocks SHOULD be empty at this point so we don't need to try and clear it out.
59+
// If it's not empty, then this object is being disposed of prematurely!
60+
}
61+
62+
private AsyncLock CreateLeasedLock(TKey key)
63+
{
64+
if (!this.pool.TryTake(out AsyncLock? asyncLock))
65+
{
66+
asyncLock = new AsyncLock();
67+
}
68+
69+
asyncLock.OnRelease = () => this.activeLocks.Release(key);
70+
return asyncLock;
71+
}
72+
73+
private void ReturnLeasedLock(AsyncLock asyncLock)
74+
{
75+
if (this.pool.Count < this.maxPoolSize)
76+
{
77+
this.pool.Add(asyncLock);
78+
}
79+
else
80+
{
81+
asyncLock.Dispose();
82+
}
83+
}
84+
}
85+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Threading.Tasks;
7+
8+
#nullable enable
9+
10+
namespace SixLabors.ImageSharp.Web.Synchronization
11+
{
12+
/// <summary>
13+
/// Extension of the <see cref="AsyncReaderWriterLock"/> that enables fine-grained locking on a given key.
14+
/// Concurrent write lock requests using different keys can execute simultaneously, while requests to lock
15+
/// using the same key will be forced to wait. This object is thread-safe and internally uses a pooling
16+
/// mechanism to minimize allocation of new locks.
17+
/// </summary>
18+
/// <typeparam name="TKey">The type of the key.</typeparam>
19+
public class AsyncKeyReaderWriterLock<TKey>
20+
where TKey : notnull
21+
{
22+
private readonly RefCountedConcurrentDictionary<TKey, AsyncReaderWriterLock> activeLocks;
23+
private readonly ConcurrentBag<AsyncReaderWriterLock> pool;
24+
private readonly int maxPoolSize;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="AsyncKeyReaderWriterLock{TKey}"/> class.
28+
/// </summary>
29+
/// <param name="maxPoolSize">The maximum number of locks that should be pooled for reuse.</param>
30+
public AsyncKeyReaderWriterLock(int maxPoolSize = 64)
31+
{
32+
this.pool = new ConcurrentBag<AsyncReaderWriterLock>();
33+
this.activeLocks = new RefCountedConcurrentDictionary<TKey, AsyncReaderWriterLock>(this.CreateLeasedLock, this.ReturnLeasedLock);
34+
this.maxPoolSize = maxPoolSize;
35+
}
36+
37+
/// <summary>
38+
/// Locks the current thread in read mode asynchronously.
39+
/// </summary>
40+
/// <param name="key">The key identifying the specific object to lock against.</param>
41+
/// <returns>
42+
/// The <see cref="IDisposable"/> that will release the lock.
43+
/// </returns>
44+
public Task<IDisposable> ReaderLockAsync(TKey key)
45+
=> this.activeLocks.Get(key).ReaderLockAsync();
46+
47+
/// <summary>
48+
/// Locks the current thread in write mode asynchronously.
49+
/// </summary>
50+
/// <param name="key">The key identifying the specific object to lock against.</param>
51+
/// <returns>
52+
/// The <see cref="IDisposable"/> that will release the lock.
53+
/// </returns>
54+
public Task<IDisposable> WriterLockAsync(TKey key)
55+
=> this.activeLocks.Get(key).WriterLockAsync();
56+
57+
private AsyncReaderWriterLock CreateLeasedLock(TKey key)
58+
{
59+
if (!this.pool.TryTake(out AsyncReaderWriterLock? asyncLock))
60+
{
61+
asyncLock = new AsyncReaderWriterLock();
62+
}
63+
64+
asyncLock.OnRelease = () => this.activeLocks.Release(key);
65+
return asyncLock;
66+
}
67+
68+
private void ReturnLeasedLock(AsyncReaderWriterLock asyncLock)
69+
{
70+
if (this.pool.Count < this.maxPoolSize)
71+
{
72+
this.pool.Add(asyncLock);
73+
}
74+
}
75+
}
76+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
#nullable enable
9+
10+
namespace SixLabors.ImageSharp.Web.Synchronization
11+
{
12+
/// <summary>
13+
/// An asynchronous locker that uses an IDisposable pattern for releasing the lock.
14+
/// </summary>
15+
public class AsyncLock : IDisposable
16+
{
17+
private readonly SemaphoreSlim semaphore;
18+
#pragma warning disable IDE0069 // Disposable fields should be disposed
19+
private readonly IDisposable releaser;
20+
#pragma warning restore IDE0069 // Disposable fields should be disposed
21+
private readonly Task<IDisposable> releaserTask;
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="AsyncLock"/> class.
25+
/// </summary>
26+
public AsyncLock()
27+
{
28+
this.semaphore = new SemaphoreSlim(1, 1);
29+
this.releaser = new Releaser(this);
30+
this.releaserTask = Task.FromResult(this.releaser);
31+
}
32+
33+
/// <summary>
34+
/// Gets or sets the callback that should be invoked whenever this lock is released.
35+
/// </summary>
36+
public Action? OnRelease { get; set; }
37+
38+
/// <summary>
39+
/// Asynchronously obtains the lock. Dispose the returned <see cref="IDisposable"/> to release the lock.
40+
/// </summary>
41+
/// <returns>
42+
/// The <see cref="IDisposable"/> that will release the lock.
43+
/// </returns>
44+
public Task<IDisposable> LockAsync()
45+
{
46+
Task wait = this.semaphore.WaitAsync();
47+
48+
// No-allocation fast path when the semaphore wait completed synchronously
49+
return (wait.Status == TaskStatus.RanToCompletion)
50+
? this.releaserTask
51+
: AwaitThenReturn(wait, this.releaser);
52+
53+
static async Task<IDisposable> AwaitThenReturn(Task t, IDisposable r)
54+
{
55+
await t;
56+
return r;
57+
}
58+
}
59+
60+
private void Release()
61+
{
62+
try
63+
{
64+
this.semaphore.Release();
65+
}
66+
finally
67+
{
68+
this.OnRelease?.Invoke();
69+
}
70+
}
71+
72+
/// <summary>
73+
/// Releases all resources used by the current instance of the <see cref="AsyncLock"/> class.
74+
/// </summary>
75+
public void Dispose() => this.semaphore.Dispose();
76+
77+
/// <summary>
78+
/// Utility class that releases an <see cref="AsyncLock"/> on disposal.
79+
/// </summary>
80+
private sealed class Releaser : IDisposable
81+
{
82+
private readonly AsyncLock toRelease;
83+
84+
internal Releaser(AsyncLock toRelease) => this.toRelease = toRelease;
85+
86+
/// <summary>
87+
/// Releases the <see cref="AsyncLock"/> associated with this <see cref="Releaser"/>.
88+
/// </summary>
89+
public void Dispose() => this.toRelease?.Release();
90+
}
91+
}
92+
}

0 commit comments

Comments
 (0)