Skip to content

Commit 434b149

Browse files
Merge pull request #124 from ThisNetWorks/dm/lock-free-cache
Lock free Read/Write Identical Request Implementation
2 parents 978a676 + abdd618 commit 434b149

File tree

1 file changed

+81
-42
lines changed

1 file changed

+81
-42
lines changed

src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs

Lines changed: 81 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,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

Comments
 (0)