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,7 +270,10 @@ 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 {
@@ -339,7 +351,7 @@ private async Task ProcessRequestAsync(
339351 {
340352 await this . StreamDisposeAsync ( outStream ) ;
341353 }
342- }
354+ } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
343355 }
344356
345357 private ValueTask StreamDisposeAsync ( Stream stream )
@@ -369,49 +381,69 @@ private async Task<ValueTuple<bool, ImageMetadata>> IsNewOrUpdatedAsync(
369381 ImageContext imageContext ,
370382 string key )
371383 {
372- using ( await AsyncLock . ReaderLockAsync ( key ) )
384+ if ( WriteWorkers . TryGetValue ( key , out var writeWork ) )
373385 {
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 )
386+ await writeWork . Value ;
387+ }
388+
389+ if ( ReadWorkers . TryGetValue ( key , out var readWork ) )
390+ {
391+ return await readWork . Value ;
392+ }
393+
394+ return await ReadWorkers . GetOrAdd (
395+ key ,
396+ x => new Lazy < Task < ValueTuple < bool , ImageMetadata > > > (
397+ async ( ) =>
398+ {
399+ try
388400 {
389- // Remove the null resolver from the store.
390- CacheResolverLru . TryRemove ( key ) ;
391- return ( true , sourceImageMetadata ) ;
392- }
401+ // Get the source metadata for processing, storing the result for future checks.
402+ ImageMetadata sourceImageMetadata = await
403+ SourceMetadataLru . GetOrAddAsync (
404+ key ,
405+ _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
406+
407+ // Check to see if the cache contains this image.
408+ // If not, we return early. No further checks necessary.
409+ IImageCacheResolver cachedImageResolver = await
410+ CacheResolverLru . GetOrAddAsync (
411+ key ,
412+ k => this . cache . GetAsync ( k ) ) ;
413+
414+ if ( cachedImageResolver is null )
415+ {
416+ // Remove the null resolver from the store.
417+ CacheResolverLru . TryRemove ( key ) ;
418+ return ( true , sourceImageMetadata ) ;
419+ }
393420
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 )
421+ // Now resolve the cached image metadata storing the result.
422+ ImageCacheMetadata cachedImageMetadata = await
423+ CacheMetadataLru . GetOrAddAsync (
424+ key ,
425+ _ => cachedImageResolver . GetMetaDataAsync ( ) ) ;
426+
427+ // Has the cached image expired?
428+ // Or has the source image changed since the image was last cached?
429+ if ( cachedImageMetadata . ContentLength == 0 // Fix for old cache without length property
430+ || cachedImageMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
431+ || cachedImageMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
432+ {
433+ // We want to remove the metadata from the store so that the next check gets the updated file.
434+ CacheMetadataLru . TryRemove ( key ) ;
435+ return ( true , sourceImageMetadata ) ;
436+ }
437+
438+ // We're pulling the image from the cache.
439+ await this . SendResponseAsync ( imageContext , key , cachedImageMetadata , null , cachedImageResolver ) ;
440+ return ( false , sourceImageMetadata ) ;
441+ }
442+ finally
405443 {
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 ) ;
444+ ReadWorkers . TryRemove ( key , out var _ ) ;
409445 }
410-
411- // We're pulling the image from the cache.
412- await this . SendResponseAsync ( imageContext , key , cachedImageMetadata , null , cachedImageResolver ) ;
413- return ( false , sourceImageMetadata ) ;
414- }
446+ } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
415447 }
416448
417449 private async Task SendResponseAsync (
0 commit comments