Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lock free Read/Write Identical Request Implementation #124

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 81 additions & 42 deletions src/ImageSharp.Web/Middleware/ImageSharpMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
Expand All @@ -28,9 +30,16 @@ namespace SixLabors.ImageSharp.Web.Middleware
public class ImageSharpMiddleware
{
/// <summary>
/// The key-lock used for limiting identical requests.
/// The write worker used for limiting identical requests.
/// </summary>
private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
private static readonly ConcurrentDictionary<string, Lazy<Task>> WriteWorkers
= new ConcurrentDictionary<string, Lazy<Task>>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// The read worker used for limiting identical requests.
/// </summary>
private static readonly ConcurrentDictionary<string, Lazy<Task<ValueTuple<bool, ImageMetadata>>>> ReadWorkers
= new ConcurrentDictionary<string, Lazy<Task<ValueTuple<bool, ImageMetadata>>>>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Used to temporarily store source metadata reads to reduce the overhead of cache lookups.
Expand Down Expand Up @@ -261,10 +270,19 @@ private async Task ProcessRequestAsync(

// Enter a write lock which locks writing and any reads for the same request.
// This reduces the overheads of unnecessary processing plus avoids file locks.
using (await AsyncLock.WriterLockAsync(key))
await WriteWorkers.GetOrAdd(
key,
x => new Lazy<Task>(
async () =>
{
try
{
// Prevent a second request from starting a read during write execution.
if (ReadWorkers.TryGetValue(key, out var readWork))
{
await readWork.Value;
}

ImageCacheMetadata cachedImageMetadata = default;
outStream = new RecyclableMemoryStream(this.options.MemoryStreamManager);
IImageFormat format;
Expand Down Expand Up @@ -338,8 +356,9 @@ private async Task ProcessRequestAsync(
finally
{
await this.StreamDisposeAsync(outStream);
WriteWorkers.TryRemove(key, out var _);
}
}
}, LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}

private ValueTask StreamDisposeAsync(Stream stream)
Expand Down Expand Up @@ -369,49 +388,69 @@ private async Task<ValueTuple<bool, ImageMetadata>> IsNewOrUpdatedAsync(
ImageContext imageContext,
string key)
{
using (await AsyncLock.ReaderLockAsync(key))
if (WriteWorkers.TryGetValue(key, out var writeWork))
{
await writeWork.Value;
}

if (ReadWorkers.TryGetValue(key, out var readWork))
{
return await readWork.Value;
}

return await ReadWorkers.GetOrAdd(
key,
x => new Lazy<Task<ValueTuple<bool, ImageMetadata>>>(
async () =>
{
// Get the source metadata for processing, storing the result for future checks.
ImageMetadata sourceImageMetadata = await
SourceMetadataLru.GetOrAddAsync(
key,
_ => sourceImageResolver.GetMetaDataAsync());

// Check to see if the cache contains this image.
// If not, we return early. No further checks necessary.
IImageCacheResolver cachedImageResolver = await
CacheResolverLru.GetOrAddAsync(
key,
k => this.cache.GetAsync(k));

if (cachedImageResolver is null)
try
{
// Remove the null resolver from the store.
CacheResolverLru.TryRemove(key);
return (true, sourceImageMetadata);
}
// Get the source metadata for processing, storing the result for future checks.
ImageMetadata sourceImageMetadata = await
SourceMetadataLru.GetOrAddAsync(
key,
_ => sourceImageResolver.GetMetaDataAsync());

// Check to see if the cache contains this image.
// If not, we return early. No further checks necessary.
IImageCacheResolver cachedImageResolver = await
CacheResolverLru.GetOrAddAsync(
key,
k => this.cache.GetAsync(k));

if (cachedImageResolver is null)
{
// Remove the null resolver from the store.
CacheResolverLru.TryRemove(key);
return (true, sourceImageMetadata);
}

// Now resolve the cached image metadata storing the result.
ImageCacheMetadata cachedImageMetadata = await
CacheMetadataLru.GetOrAddAsync(
key,
_ => cachedImageResolver.GetMetaDataAsync());

// Has the cached image expired?
// Or has the source image changed since the image was last cached?
if (cachedImageMetadata.ContentLength == 0 // Fix for old cache without length property
|| cachedImageMetadata.CacheLastWriteTimeUtc <= (DateTimeOffset.UtcNow - this.options.CacheMaxAge)
|| cachedImageMetadata.SourceLastWriteTimeUtc != sourceImageMetadata.LastWriteTimeUtc)
{
// We want to remove the metadata from the store so that the next check gets the updated file.
CacheMetadataLru.TryRemove(key);
return (true, sourceImageMetadata);
}

// Now resolve the cached image metadata storing the result.
ImageCacheMetadata cachedImageMetadata = await
CacheMetadataLru.GetOrAddAsync(
key,
_ => cachedImageResolver.GetMetaDataAsync());

// Has the cached image expired?
// Or has the source image changed since the image was last cached?
if (cachedImageMetadata.ContentLength == 0 // Fix for old cache without length property
|| cachedImageMetadata.CacheLastWriteTimeUtc <= (DateTimeOffset.UtcNow - this.options.CacheMaxAge)
|| cachedImageMetadata.SourceLastWriteTimeUtc != sourceImageMetadata.LastWriteTimeUtc)
// We're pulling the image from the cache.
await this.SendResponseAsync(imageContext, key, cachedImageMetadata, null, cachedImageResolver);
return (false, sourceImageMetadata);
}
finally
{
// We want to remove the metadata from the store so that the next check gets the updated file.
CacheMetadataLru.TryRemove(key);
return (true, sourceImageMetadata);
ReadWorkers.TryRemove(key, out var _);
}

// We're pulling the image from the cache.
await this.SendResponseAsync(imageContext, key, cachedImageMetadata, null, cachedImageResolver);
return (false, sourceImageMetadata);
}
}, LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}

private async Task SendResponseAsync(
Expand Down