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(