2424
2525namespace SixLabors . ImageSharp . Web . Middleware
2626{
27- public struct ImageResponse
28- {
29- internal ImageContext Context { get ; set ; }
30- public ImageCacheMetadata Metadata { get ; set ; }
31- public Stream Stream { get ; set ; }
32-
33- }
34-
35- public struct CacheImageResponse
36- {
37- public bool NewOrUppdated { get ; set ; }
38- public ImageMetadata Metadata { get ; set ; }
39- public ImageCacheMetadata CacheMetadata { get ; set ; }
40-
41- public IImageCacheResolver Resolver { get ; set ; }
42-
43- }
44-
45-
4627 /// <summary>
4728 /// Middleware for handling the processing of images via image requests.
4829 /// </summary>
@@ -51,14 +32,14 @@ public class ImageSharpMiddleware
5132 /// <summary>
5233 /// The write worker used for limiting identical requests.
5334 /// </summary>
54- private static readonly ConcurrentDictionary < string , Lazy < Task < ImageResponse > > > WriteWorkers
55- = new ConcurrentDictionary < string , Lazy < Task < ImageResponse > > > ( StringComparer . OrdinalIgnoreCase ) ;
35+ private static readonly ConcurrentDictionary < string , Task < ImageWorkerResult > > WriteWorkers
36+ = new ConcurrentDictionary < string , Task < ImageWorkerResult > > ( StringComparer . OrdinalIgnoreCase ) ;
5637
5738 /// <summary>
5839 /// The read worker used for limiting identical requests.
5940 /// </summary>
60- private static readonly ConcurrentDictionary < string , Lazy < Task < CacheImageResponse > > > ReadWorkers
61- = new ConcurrentDictionary < string , Lazy < Task < CacheImageResponse > > > ( StringComparer . OrdinalIgnoreCase ) ;
41+ private static readonly ConcurrentDictionary < string , Task < ImageWorkerResult > > ReadWorkers
42+ = new ConcurrentDictionary < string , Task < ImageWorkerResult > > ( StringComparer . OrdinalIgnoreCase ) ;
6243
6344 /// <summary>
6445 /// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
@@ -270,33 +251,40 @@ private async Task ProcessRequestAsync(
270251
271252 // Check the cache, if present, not out of date and not requiring and update
272253 // we'll simply serve the file from there.
273- var cacheImageResponse =
274- await this . IsNewOrUpdatedAsync ( sourceImageResolver , imageContext , key ) ;
254+ ImageWorkerResult readResult = default ;
255+ try
256+ {
257+ readResult = await this . IsNewOrUpdatedAsync ( sourceImageResolver , imageContext , key ) ;
258+ }
259+ finally
260+ {
261+ ReadWorkers . TryRemove ( key , out Task < ImageWorkerResult > _ ) ;
262+ }
275263
276- if ( ! cacheImageResponse . NewOrUppdated )
264+ if ( ! readResult . IsNewOrUpdated )
277265 {
278- await this . SendResponseAsync ( imageContext , key , cacheImageResponse . CacheMetadata , null , cacheImageResponse . Resolver ) ;
266+ await this . SendResponseAsync ( imageContext , key , readResult . CacheImageMetadata , readResult . Resolver ) ;
279267 return ;
280268 }
281269
282- var sourceImageMetadata = cacheImageResponse . Metadata ;
283-
284270 // Not cached, or is updated? Let's get it from the image resolver.
285- RecyclableMemoryStream outStream = null ;
271+ var sourceImageMetadata = readResult . SourceImageMetadata ;
286272
287- // Enter a write lock which locks writing and any reads for the same request.
288- // This reduces the overheads of unnecessary processing plus avoids file locks.
289- var writeResult = await WriteWorkers . GetOrAdd (
290- key ,
291- _ => new Lazy < Task < ImageResponse > > (
292- async ( ) =>
273+ // Enter an asynchronous write worker which prevents multiple writes and delays any reads for the same request.
274+ // This reduces the overheads of unnecessary processing.
275+ try
276+ {
277+ ImageWorkerResult writeResult = await WriteWorkers . GetOrAddAsync (
278+ key ,
279+ async ( key ) =>
293280 {
281+ RecyclableMemoryStream outStream = null ;
294282 try
295283 {
296284 // Prevent a second request from starting a read during write execution.
297- if ( ReadWorkers . TryGetValue ( key , out Lazy < Task < CacheImageResponse > > readWork ) )
285+ if ( ReadWorkers . TryGetValue ( key , out Task < ImageWorkerResult > readWork ) )
298286 {
299- await readWork . Value ;
287+ await readWork ;
300288 }
301289
302290 ImageCacheMetadata cachedImageMetadata = default ;
@@ -356,31 +344,27 @@ private async Task ProcessRequestAsync(
356344 // Save the image to the cache and send the response to the caller.
357345 await this . cache . SetAsync ( key , outStream , cachedImageMetadata ) ;
358346
359- // Remove the resolver from the cache so we always resolve next request
347+ // Remove any resolver from the cache so we always resolve next request
360348 // for the same key.
361349 CacheResolverLru . TryRemove ( key ) ;
362350
363- // This worker queue should be all about writing.
364- // Not sending. sending would only happen once
365- // when it is part of this queue.
366-
367- var taskCompletionSource = new TaskCompletionSource < ImageResponse > ( ) ;
368-
369-
370- // Return the response to the pipeline so it can return the response to multiple callers.
371- var result = new ImageResponse
372- {
373- Context = imageContext ,
374- Metadata = cachedImageMetadata ,
375- Stream = outStream
376- } ;
377-
378- return result ;
379-
380- // taskCompletionSource.SetResult(result);
351+ // Place the resolver in the lru cache.
352+ ( IImageCacheResolver ImageCacheResolver , ImageCacheMetadata ImageCacheMetadata ) cachedImage = await
353+ CacheResolverLru . GetOrAddAsync (
354+ key ,
355+ async k =>
356+ {
357+ IImageCacheResolver resolver = await this . cache . GetAsync ( k ) ;
358+ ImageCacheMetadata metadata = default ;
359+ if ( resolver != null )
360+ {
361+ metadata = await resolver . GetMetaDataAsync ( ) ;
362+ }
381363
382- // return taskCompletionSource.Task;
364+ return ( resolver , metadata ) ;
365+ } ) ;
383366
367+ return new ImageWorkerResult ( cachedImage . ImageCacheMetadata , cachedImage . ImageCacheResolver ) ;
384368 }
385369 catch ( Exception ex )
386370 {
@@ -391,13 +375,18 @@ private async Task ProcessRequestAsync(
391375 }
392376 finally
393377 {
394- // await this.StreamDisposeAsync(outStream);
395- WriteWorkers . TryRemove ( key , out Lazy < Task < ImageResponse > > _ ) ;
378+ await this . StreamDisposeAsync ( outStream ) ;
396379 }
397- } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
398-
380+ } ) ;
399381
400- await this . SendResponseAsync ( imageContext , key , writeResult . Metadata , writeResult . Stream , null ) ;
382+ await this . SendResponseAsync ( imageContext , key , writeResult . CacheImageMetadata , writeResult . Resolver ) ;
383+ }
384+ finally
385+ {
386+ // As soon as we have sent a response from a writer the result is available from a reader so we remove this task.
387+ // Any existing awaiters will continue to await.
388+ WriteWorkers . TryRemove ( key , out Task < ImageWorkerResult > _ ) ;
389+ }
401390 }
402391
403392 private ValueTask StreamDisposeAsync ( Stream stream )
@@ -422,95 +411,77 @@ private ValueTask StreamDisposeAsync(Stream stream)
422411#endif
423412 }
424413
425- private async Task < CacheImageResponse > IsNewOrUpdatedAsync (
414+ private async Task < ImageWorkerResult > IsNewOrUpdatedAsync (
426415 IImageResolver sourceImageResolver ,
427416 ImageContext imageContext ,
428417 string key )
429418 {
430- if ( WriteWorkers . TryGetValue ( key , out Lazy < Task < ImageResponse > > writeWork ) )
419+ // Pause until the write has been completed.
420+ if ( WriteWorkers . TryGetValue ( key , out Task < ImageWorkerResult > writeWorkResult ) )
431421 {
432- await writeWork . Value ;
422+ await writeWorkResult ;
433423 }
434424
435- if ( ReadWorkers . TryGetValue ( key , out Lazy < Task < CacheImageResponse > > readWork ) )
425+ if ( ReadWorkers . TryGetValue ( key , out Task < ImageWorkerResult > readWorkResult ) )
436426 {
437- return await readWork . Value ;
427+ return await readWorkResult ;
438428 }
439429
440430 return await ReadWorkers . GetOrAdd (
441431 key ,
442- _ => new Lazy < Task < CacheImageResponse > > (
443- async ( ) =>
432+ async ( key ) =>
444433 {
445- try
446- {
447- // Get the source metadata for processing, storing the result for future checks.
448- ImageMetadata sourceImageMetadata = await
449- SourceMetadataLru . GetOrAddAsync (
450- key ,
451- _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
452-
453- // Check to see if the cache contains this image.
454- // If not, we return early. No further checks necessary.
455- ( IImageCacheResolver ImageCacheResolver , ImageCacheMetadata ImageCacheMetadata ) cachedImage = await
456- CacheResolverLru . GetOrAddAsync (
457- key ,
458- async k =>
434+ // Get the source metadata for processing, storing the result for future checks.
435+ ImageMetadata sourceImageMetadata = await
436+ SourceMetadataLru . GetOrAddAsync (
437+ key ,
438+ _ => sourceImageResolver . GetMetaDataAsync ( ) ) ;
439+
440+ // Check to see if the cache contains this image.
441+ // If not, we return early. No further checks necessary.
442+ ( IImageCacheResolver ImageCacheResolver , ImageCacheMetadata ImageCacheMetadata ) cachedImage = await
443+ CacheResolverLru . GetOrAddAsync (
444+ key ,
445+ async k =>
446+ {
447+ IImageCacheResolver resolver = await this . cache . GetAsync ( k ) ;
448+ ImageCacheMetadata metadata = default ;
449+ if ( resolver != null )
459450 {
460- IImageCacheResolver resolver = await this . cache . GetAsync ( k ) ;
461- ImageCacheMetadata metadata = default ;
462- if ( resolver != null )
463- {
464- metadata = await resolver . GetMetaDataAsync ( ) ;
465- }
466-
467- return ( resolver , metadata ) ;
468- } ) ;
469-
470- if ( cachedImage . ImageCacheResolver is null )
471- {
472- // Remove the null resolver from the store.
473- CacheResolverLru . TryRemove ( key ) ;
474-
475- return new CacheImageResponse { NewOrUppdated = true , Metadata = sourceImageMetadata } ;
476- }
451+ metadata = await resolver . GetMetaDataAsync ( ) ;
452+ }
477453
478- // Has the cached image expired?
479- // Or has the source image changed since the image was last cached?
480- if ( cachedImage . ImageCacheMetadata . ContentLength == 0 // Fix for old cache without length property
481- || cachedImage . ImageCacheMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
482- || cachedImage . ImageCacheMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
483- {
484- // We want to remove the resolver from the store so that the next check gets the updated file.
485- CacheResolverLru . TryRemove ( key ) ;
486- return new CacheImageResponse { NewOrUppdated = true , Metadata = sourceImageMetadata } ;
487- }
488-
489- // Likewise this queue should be about getting the image metadata
490- // not
454+ return ( resolver , metadata ) ;
455+ } ) ;
491456
492- // We're pulling the image from the cache.
493- // await this.SendResponseAsync(imageContext, key, cachedImage.ImageCacheMetadata, null, cachedImage.ImageCacheResolver);
457+ if ( cachedImage . ImageCacheResolver is null )
458+ {
459+ // Remove the null resolver from the store.
460+ CacheResolverLru . TryRemove ( key ) ;
494461
495- // The image is cached. Return the cached image so multiple callers can write a response.
496- return new CacheImageResponse {
497- NewOrUppdated = false ,
498- Metadata = sourceImageMetadata ,
499- CacheMetadata = cachedImage . ImageCacheMetadata ,
500- Resolver = cachedImage . ImageCacheResolver } ;
462+ return new ImageWorkerResult ( sourceImageMetadata ) ;
501463 }
502- finally
464+
465+ // Has the cached image expired?
466+ // Or has the source image changed since the image was last cached?
467+ if ( cachedImage . ImageCacheMetadata . ContentLength == 0 // Fix for old cache without length property
468+ || cachedImage . ImageCacheMetadata . CacheLastWriteTimeUtc <= ( DateTimeOffset . UtcNow - this . options . CacheMaxAge )
469+ || cachedImage . ImageCacheMetadata . SourceLastWriteTimeUtc != sourceImageMetadata . LastWriteTimeUtc )
503470 {
504- ReadWorkers . TryRemove ( key , out Lazy < Task < CacheImageResponse > > _ ) ;
471+ // We want to remove the resolver from the store so that the next check gets the updated file.
472+ CacheResolverLru . TryRemove ( key ) ;
473+ return new ImageWorkerResult ( sourceImageMetadata ) ;
505474 }
506- } , LazyThreadSafetyMode . ExecutionAndPublication ) ) . Value ;
475+
476+ // The image is cached. Return the cached image so multiple callers can write a response.
477+ return new ImageWorkerResult ( sourceImageMetadata , cachedImage . ImageCacheMetadata , cachedImage . ImageCacheResolver ) ;
478+ } ) ;
507479 }
508480
509481 private async Task SendResponseAsync (
510482 ImageContext imageContext ,
511483 string key ,
512484 ImageCacheMetadata metadata ,
513- Stream stream ,
514485 IImageCacheResolver cacheResolver )
515486 {
516487 imageContext . ComprehendRequestHeaders ( metadata . CacheLastWriteTimeUtc , metadata . ContentLength ) ;
@@ -528,7 +499,7 @@ private async Task SendResponseAsync(
528499 this . logger . LogImageServed ( imageContext . GetDisplayUrl ( ) , key ) ;
529500
530501 // When stream is null we're sending from the cache.
531- await imageContext . SendAsync ( stream ?? await cacheResolver . OpenReadAsync ( ) , metadata ) ;
502+ await imageContext . SendAsync ( await cacheResolver . OpenReadAsync ( ) , metadata ) ;
532503 return ;
533504
534505 case ImageContext . PreconditionState . NotModified :
0 commit comments