Skip to content

Instantly share code, notes, and snippets.

@svenrog
Last active February 5, 2022 09:54
Show Gist options
  • Save svenrog/6febab982281f957e3b612c1febf87b7 to your computer and use it in GitHub Desktop.
Save svenrog/6febab982281f957e3b612c1febf87b7 to your computer and use it in GitHub Desktop.
Illustrates a problem with thread safety in SixLabors.ImageSharp.Web
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using NUnit.Framework;
using NUnit.Framework.Internal;
using SixLabors.ImageSharp.Web;
using SixLabors.ImageSharp.Web.Caching;
using SixLabors.ImageSharp.Web.DependencyInjection;
using SixLabors.ImageSharp.Web.Providers;
using SixLabors.ImageSharp.Web.Resolvers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace ImageSharp.Tests
{
[TestFixture]
public class ResizingTests
{
[Test]
public async Task Can_Resize_Concurrently()
{
var resolver = new PhysicalFileResolver("images/product.jpg");
var provider = new PredeterminedImageProvider(resolver, minDelay: 1, maxDelay: 2, seed: 1024);
var resultWriter = TestExecutionContext.CurrentContext.CurrentResult.OutWriter;
var hostBuilder = CreateHostBuilder(provider);
using var host = await hostBuilder.StartAsync();
var client = host.GetTestClient();
var tasks = new List<Task<HttpResponseMessage>>();
for (var i = 10; i < 400; i += 5)
{
//This single thread logic works
//var task = client.GetAsync($"https://somewebsite.net/globalassets/all-products/product-category/product.jpg?width={i}");
//This multi-thread logic seems to reuse images
var task = Task.Run(() => client.GetAsync($"https://somewebsite.net/globalassets/all-products/product-category/product.jpg?width={i}"));
tasks.Add(task);
}
var imageSizes = new HashSet<long>();
var results = await Task.WhenAll(tasks);
for (var i = 0; i < results.Length; i++)
{
var result = results[i];
var imageSize = result.Content.Headers.ContentLength ?? throw new InvalidOperationException("Content-Length header not set by server");
resultWriter.WriteLine($"Image {i + 1}: size {imageSize} bytes");
Assert.False(imageSizes.Contains(imageSize));
imageSizes.Add(imageSize);
}
}
private static IHostBuilder CreateHostBuilder<TProvider>(TProvider provider)
where TProvider : class, IImageProvider
{
return CreateHostBuilder(provider, new NullCache());
}
private static IHostBuilder CreateHostBuilder<TProvider, TCache>(TProvider provider, TCache cache)
where TProvider : class, IImageProvider
where TCache : class, IImageCache
{
var builder = new HostBuilder();
builder.ConfigureWebHost(webBuilder =>
{
webBuilder.UseTestServer();
webBuilder.ConfigureServices(services =>
{
services.AddImageSharp()
.ClearProviders()
.AddProvider((locator) => provider)
.SetCache((locator) => cache);
});
webBuilder.Configure(app =>
{
app.UseImageSharp();
});
});
return builder;
}
}
public class PhysicalFileResolver : IImageResolver
{
private readonly FileInfo _fileInfo;
public PhysicalFileResolver(string path)
{
_fileInfo = new FileInfo(path);
}
public Task<ImageMetadata> GetMetaDataAsync()
{
var metaData = new ImageMetadata(_fileInfo.LastWriteTimeUtc, _fileInfo.Length);
return Task.FromResult(metaData);
}
public Task<Stream> OpenReadAsync()
{
var stream = new FileStream(_fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read);
return Task.FromResult<Stream>(stream);
}
}
public class PredeterminedImageProvider : IImageProvider
{
private readonly IImageResolver _result;
private readonly int? _minDelay;
private readonly int? _maxDelay;
private readonly Random _random;
public PredeterminedImageProvider(IImageResolver result, int? minDelay = null, int? maxDelay = null, int? seed = null)
{
_result = result;
_minDelay = minDelay;
_maxDelay = maxDelay ?? minDelay;
if (_minDelay.HasValue && _maxDelay.HasValue && _maxDelay < _minDelay)
throw new ArgumentException($"cannot be less than {nameof(minDelay)}", nameof(maxDelay));
_random = seed.HasValue ? new Random(seed.Value) : new Random();
}
public ProcessingBehavior ProcessingBehavior => ProcessingBehavior.All;
public Func<HttpContext, bool> Match
{
get => (context) => IsValidRequest(context);
set => throw new NotSupportedException();
}
public async Task<IImageResolver> GetAsync(HttpContext context)
{
if (_minDelay.HasValue && _minDelay.Value > 0 && _maxDelay > 0)
await Task.Delay(_random.Next(_minDelay.Value, _maxDelay.Value));
return _result;
}
public bool IsValidRequest(HttpContext context)
{
return true;
}
}
public class NullCache : IImageCache
{
public Task<IImageCacheResolver> GetAsync(string key)
{
return Task.FromResult<IImageCacheResolver>(null);
}
public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
{
return Task.CompletedTask;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment