22// Licensed under the Apache License, Version 2.0.
33
44using System ;
5+ using System . Collections . Concurrent ;
56using System . Collections . Generic ;
67using System . Diagnostics ;
78using System . Globalization ;
89using System . IO ;
910using System . Linq ;
1011using System . Text ;
12+ using System . Threading ;
1113using System . Threading . Tasks ;
1214using Microsoft . AspNetCore . Http ;
1315using Microsoft . Extensions . Logging ;
@@ -28,9 +30,16 @@ namespace SixLabors.ImageSharp.Web.Middleware
2830 public class ImageSharpMiddleware
2931 {
3032 /// <summary>
31- /// The key-lock used for limiting identical requests.
33+ /// The write worker used for limiting identical requests.
3234 /// </summary>
33- private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock ( ) ;
35+ private static readonly ConcurrentDictionary < string , Lazy < Task > > WriteWorkers
36+ = new ConcurrentDictionary < string , Lazy < Task > > ( StringComparer . OrdinalIgnoreCase ) ;
37+
38+ /// <summary>
39+ /// The read worker used for limiting identical requests.
40+ /// </summary>
41+ private static readonly ConcurrentDictionary < string , Lazy < Task < ValueTuple < bool , ImageMetadata > > > > ReadWorkers
42+ = new ConcurrentDictionary < string , Lazy < Task < ValueTuple < bool , ImageMetadata > > > > ( StringComparer . OrdinalIgnoreCase ) ;
3443
3544 /// <summary>
3645 /// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
@@ -261,10 +270,19 @@ private async Task ProcessRequestAsync(
261270
262271 // Enter a write lock which locks writing and any reads for the same request.
263272 // This reduces the overheads of unnecessary processing plus avoids file locks.
264- using ( await AsyncLock . WriterLockAsync ( key ) )
273+ await WriteWorkers . GetOrAdd (
274+ key ,
275+ x => new Lazy < Task > (
276+ async ( ) =>
265277 {
266278 try
267279 {
280+ // Prevent a second request from starting a read during write execution.
281+ if ( ReadWorkers . TryGetValue ( key , out var readWork ) )
282+ {
283+ await readWork . Value ;
284+ }
285+
268286 ImageCacheMetadata cachedImageMetadata = default ;
269287 outStream = new RecyclableMemoryStream ( this . options . MemoryStreamManager ) ;
270288 IImageFormat format ;
@@ -338,8 +356,9 @@ private async Task ProcessRequestAsync(
338356 finally
339357 {
340358 await this . StreamDisposeAsync ( outStream ) ;
359+ WriteWorkers . TryRemove ( key , out var _ ) ;
341360 }
342- }
361+ } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
343362 }
344363
345364 private ValueTask StreamDisposeAsync ( Stream stream )
@@ -369,49 +388,69 @@ private async Task<ValueTuple<bool, ImageMetadata>> IsNewOrUpdatedAsync(
369388 ImageContext imageContext ,
370389 string key )
371390 {
372- using ( await AsyncLock . ReaderLockAsync ( key ) )
391+ if ( WriteWorkers . TryGetValue ( key , out var writeWork ) )
392+ {
393+ await writeWork . Value ;
394+ }
395+
396+ if ( ReadWorkers . TryGetValue ( key , out var readWork ) )
397+ {
398+ return await readWork . Value ;
399+ }
400+
401+ return await ReadWorkers . GetOrAdd (
402+ key ,
403+ x => new Lazy < Task < ValueTuple < bool , ImageMetadata > > > (
404+ async ( ) =>
373405 {
374- // Get the source metadata for processing, storing the result for future checks.
375- ImageMetadata sourceImageMetadata = await
376- SourceMetadataLru . GetOrAddAsync (
377- key ,
378- _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
379-
380- // Check to see if the cache contains this image.
381- // If not, we return early. No further checks necessary.
382- IImageCacheResolver cachedImageResolver = await
383- CacheResolverLru . GetOrAddAsync (
384- key ,
385- k => this . cache . GetAsync ( k ) ) ;
386-
387- if ( cachedImageResolver is null )
406+ try
388407 {
389- // Remove the null resolver from the store.
390- CacheResolverLru . TryRemove ( key ) ;
391- return ( true , sourceImageMetadata ) ;
392- }
408+ // Get the source metadata for processing, storing the result for future checks.
409+ ImageMetadata sourceImageMetadata = await
410+ SourceMetadataLru . GetOrAddAsync (
411+ key ,
412+ _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
413+
414+ // Check to see if the cache contains this image.
415+ // If not, we return early. No further checks necessary.
416+ IImageCacheResolver cachedImageResolver = await
417+ CacheResolverLru . GetOrAddAsync (
418+ key ,
419+ k => this . cache . GetAsync ( k ) ) ;
420+
421+ if ( cachedImageResolver is null )
422+ {
423+ // Remove the null resolver from the store.
424+ CacheResolverLru . TryRemove ( key ) ;
425+ return ( true , sourceImageMetadata ) ;
426+ }
427+
428+ // Now resolve the cached image metadata storing the result.
429+ ImageCacheMetadata cachedImageMetadata = await
430+ CacheMetadataLru . GetOrAddAsync (
431+ key ,
432+ _ => cachedImageResolver . GetMetaDataAsync ( ) ) ;
433+
434+ // Has the cached image expired?
435+ // Or has the source image changed since the image was last cached?
436+ if ( cachedImageMetadata . ContentLength == 0 // Fix for old cache without length property
437+ || cachedImageMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
438+ || cachedImageMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
439+ {
440+ // We want to remove the metadata from the store so that the next check gets the updated file.
441+ CacheMetadataLru . TryRemove ( key ) ;
442+ return ( true , sourceImageMetadata ) ;
443+ }
393444
394- // Now resolve the cached image metadata storing the result.
395- ImageCacheMetadata cachedImageMetadata = await
396- CacheMetadataLru . GetOrAddAsync (
397- key ,
398- _ => cachedImageResolver . GetMetaDataAsync ( ) ) ;
399-
400- // Has the cached image expired?
401- // Or has the source image changed since the image was last cached?
402- if ( cachedImageMetadata . ContentLength == 0 // Fix for old cache without length property
403- || cachedImageMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
404- || cachedImageMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
445+ // We're pulling the image from the cache.
446+ await this . SendResponseAsync ( imageContext , key , cachedImageMetadata , null , cachedImageResolver ) ;
447+ return ( false , sourceImageMetadata ) ;
448+ }
449+ finally
405450 {
406- // We want to remove the metadata from the store so that the next check gets the updated file.
407- CacheMetadataLru . TryRemove ( key ) ;
408- return ( true , sourceImageMetadata ) ;
451+ ReadWorkers . TryRemove ( key , out var _ ) ;
409452 }
410-
411- // We're pulling the image from the cache.
412- await this . SendResponseAsync ( imageContext , key , cachedImageMetadata , null , cachedImageResolver ) ;
413- return ( false , sourceImageMetadata ) ;
414- }
453+ } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
415454 }
416455
417456 private async Task SendResponseAsync (
0 commit comments