Skip to content

Commit 2632fc0

Browse files
committed
Introduce ImageWorker Result. Drop Lazy
1 parent ac5a402 commit 2632fc0

File tree

5 files changed

+209
-130
lines changed

5 files changed

+209
-130
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"Logging": {
33
"LogLevel": {
4-
"Default": "Debug",
4+
"Default": "Warning",
55
"System": "Information",
6-
"Microsoft": "Information"
6+
"Microsoft": "Warning"
77
}
88
}
99
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Threading.Tasks;
7+
8+
namespace SixLabors.ImageSharp.Web.Middleware
9+
{
10+
/// <summary>
11+
/// Extensions used to manage asynchronous access to the <see cref="ImageSharpMiddleware"/>
12+
/// https://gist.github.com/davidfowl/3dac8f7b3d141ae87abf770d5781feed
13+
/// </summary>
14+
public static class ConcurrentDictionaryExtensions
15+
{
16+
/// <summary>
17+
/// Provides an alternative to <see cref="ConcurrentDictionary{TKey, TValue}.GetOrAdd(TKey, Func{TKey, TValue})"/> specifically for asynchronous values. The factory method will only run once.
18+
/// </summary>
19+
/// <typeparam name="TKey">The type of the key.</typeparam>
20+
/// <typeparam name="TValue">The value for the dictionary.</typeparam>
21+
/// <param name="dictionary">The <see cref="ConcurrentDictionary{TKey, TValue}"/>.</param>
22+
/// <param name="key">The key of the element to add.</param>
23+
/// <param name="valueFactory">The function used to generate a value for the key</param>
24+
/// <returns>The value for the key. This will be either the existing value for the key if the
25+
/// key is already in the dictionary, or the new value for the key as returned by valueFactory
26+
/// if the key was not in the dictionary.</returns>
27+
public static async Task<TValue> GetOrAddAsync<TKey, TValue>(
28+
this ConcurrentDictionary<TKey, Task<TValue>> dictionary,
29+
TKey key,
30+
Func<TKey, Task<TValue>> valueFactory)
31+
{
32+
while (true)
33+
{
34+
if (dictionary.TryGetValue(key, out var task))
35+
{
36+
return await task;
37+
}
38+
39+
// This is the task that we'll return to all waiters. We'll complete it when the factory is complete
40+
var tcs = new TaskCompletionSource<TValue>(TaskCreationOptions.RunContinuationsAsynchronously);
41+
if (dictionary.TryAdd(key, tcs.Task))
42+
{
43+
try
44+
{
45+
var value = await valueFactory(key);
46+
tcs.TrySetResult(value);
47+
return await tcs.Task;
48+
}
49+
catch (Exception ex)
50+
{
51+
// Make sure all waiters see the exception
52+
tcs.SetException(ex);
53+
54+
// We remove the entry if the factory failed so it's not a permanent failure
55+
// and future gets can retry (this could be a pluggable policy)
56+
dictionary.TryRemove(key, out _);
57+
throw;
58+
}
59+
}
60+
}
61+
}
62+
}
63+
}

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 98 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,6 @@
2424

2525
namespace 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

Comments
 (0)