-
-
Save svenrog/6febab982281f957e3b612c1febf87b7 to your computer and use it in GitHub Desktop.
Illustrates a problem with thread safety in SixLabors.ImageSharp.Web
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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