Skip to content

Commit 0efc559

Browse files
committed
wip. Swap AsyncKeyLock for ConcurrentDictionary
1 parent 978a676 commit 0efc559

File tree

1 file changed

+74
-42
lines changed

1 file changed

+74
-42
lines changed

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 74 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.Diagnostics;
78
using System.Globalization;
89
using System.IO;
910
using System.Linq;
1011
using System.Text;
12+
using System.Threading;
1113
using System.Threading.Tasks;
1214
using Microsoft.AspNetCore.Http;
1315
using 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

Comments
 (0)