diff --git a/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs b/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
index 282d3b6b..30d1d798 100644
--- a/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
+++ b/src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
@@ -2,12 +2,14 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -28,9 +30,16 @@ namespace SixLabors.ImageSharp.Web.Middleware
public class ImageSharpMiddleware
{
///
- /// The key-lock used for limiting identical requests.
+ /// The write worker used for limiting identical requests.
///
- private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
+ private static readonly ConcurrentDictionary> WriteWorkers
+ = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// The read worker used for limiting identical requests.
+ ///
+ private static readonly ConcurrentDictionary>>> ReadWorkers
+ = new ConcurrentDictionary>>>(StringComparer.OrdinalIgnoreCase);
///
/// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
@@ -261,10 +270,19 @@ private async Task ProcessRequestAsync(
// Enter a write lock which locks writing and any reads for the same request.
// This reduces the overheads of unnecessary processing plus avoids file locks.
- using (await AsyncLock.WriterLockAsync(key))
+ await WriteWorkers.GetOrAdd(
+ key,
+ x => new Lazy(
+ async () =>
{
try
{
+ // Prevent a second request from starting a read during write execution.
+ if (ReadWorkers.TryGetValue(key, out var readWork))
+ {
+ await readWork.Value;
+ }
+
ImageCacheMetadata cachedImageMetadata = default;
outStream = new RecyclableMemoryStream(this.options.MemoryStreamManager);
IImageFormat format;
@@ -338,8 +356,9 @@ private async Task ProcessRequestAsync(
finally
{
await this.StreamDisposeAsync(outStream);
+ WriteWorkers.TryRemove(key, out var _);
}
- }
+ }, LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}
private ValueTask StreamDisposeAsync(Stream stream)
@@ -369,49 +388,69 @@ private async Task> IsNewOrUpdatedAsync(
ImageContext imageContext,
string key)
{
- using (await AsyncLock.ReaderLockAsync(key))
+ if (WriteWorkers.TryGetValue(key, out var writeWork))
+ {
+ await writeWork.Value;
+ }
+
+ if (ReadWorkers.TryGetValue(key, out var readWork))
+ {
+ return await readWork.Value;
+ }
+
+ return await ReadWorkers.GetOrAdd(
+ key,
+ x => new Lazy>>(
+ async () =>
{
- // Get the source metadata for processing, storing the result for future checks.
- ImageMetadata sourceImageMetadata = await
- SourceMetadataLru.GetOrAddAsync(
- key,
- _ => sourceImageResolver.GetMetaDataAsync());
-
- // Check to see if the cache contains this image.
- // If not, we return early. No further checks necessary.
- IImageCacheResolver cachedImageResolver = await
- CacheResolverLru.GetOrAddAsync(
- key,
- k => this.cache.GetAsync(k));
-
- if (cachedImageResolver is null)
+ try
{
- // Remove the null resolver from the store.
- CacheResolverLru.TryRemove(key);
- return (true, sourceImageMetadata);
- }
+ // Get the source metadata for processing, storing the result for future checks.
+ ImageMetadata sourceImageMetadata = await
+ SourceMetadataLru.GetOrAddAsync(
+ key,
+ _ => sourceImageResolver.GetMetaDataAsync());
+
+ // Check to see if the cache contains this image.
+ // If not, we return early. No further checks necessary.
+ IImageCacheResolver cachedImageResolver = await
+ CacheResolverLru.GetOrAddAsync(
+ key,
+ k => this.cache.GetAsync(k));
+
+ if (cachedImageResolver is null)
+ {
+ // Remove the null resolver from the store.
+ CacheResolverLru.TryRemove(key);
+ return (true, sourceImageMetadata);
+ }
+
+ // Now resolve the cached image metadata storing the result.
+ ImageCacheMetadata cachedImageMetadata = await
+ CacheMetadataLru.GetOrAddAsync(
+ key,
+ _ => cachedImageResolver.GetMetaDataAsync());
+
+ // Has the cached image expired?
+ // Or has the source image changed since the image was last cached?
+ if (cachedImageMetadata.ContentLength == 0 // Fix for old cache without length property
+ || cachedImageMetadata.CacheLastWriteTimeUtc <= (DateTimeOffset.UtcNow - this.options.CacheMaxAge)
+ || cachedImageMetadata.SourceLastWriteTimeUtc != sourceImageMetadata.LastWriteTimeUtc)
+ {
+ // We want to remove the metadata from the store so that the next check gets the updated file.
+ CacheMetadataLru.TryRemove(key);
+ return (true, sourceImageMetadata);
+ }
- // Now resolve the cached image metadata storing the result.
- ImageCacheMetadata cachedImageMetadata = await
- CacheMetadataLru.GetOrAddAsync(
- key,
- _ => cachedImageResolver.GetMetaDataAsync());
-
- // Has the cached image expired?
- // Or has the source image changed since the image was last cached?
- if (cachedImageMetadata.ContentLength == 0 // Fix for old cache without length property
- || cachedImageMetadata.CacheLastWriteTimeUtc <= (DateTimeOffset.UtcNow - this.options.CacheMaxAge)
- || cachedImageMetadata.SourceLastWriteTimeUtc != sourceImageMetadata.LastWriteTimeUtc)
+ // We're pulling the image from the cache.
+ await this.SendResponseAsync(imageContext, key, cachedImageMetadata, null, cachedImageResolver);
+ return (false, sourceImageMetadata);
+ }
+ finally
{
- // We want to remove the metadata from the store so that the next check gets the updated file.
- CacheMetadataLru.TryRemove(key);
- return (true, sourceImageMetadata);
+ ReadWorkers.TryRemove(key, out var _);
}
-
- // We're pulling the image from the cache.
- await this.SendResponseAsync(imageContext, key, cachedImageMetadata, null, cachedImageResolver);
- return (false, sourceImageMetadata);
- }
+ }, LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}
private async Task SendResponseAsync(