public ImageWorkerResult(ImageCacheMetadata cacheImageMetadata, IImageCacheResolver resolver) { this.IsNewOrUpdated = false; this.SourceImageMetadata = default; this.CacheImageMetadata = cacheImageMetadata; this.Resolver = resolver; }
private async Task ProcessRequestAsync(HttpContext context, bool processRequest, IImageResolver sourceImageResolver, ImageContext imageContext, IDictionary <string, string> commands) { // Create a cache key based on all the components of the requested url string uri = GetUri(context, commands); string key = this.cacheHash.Create(uri, this.options.CachedNameLength); ImageMetadata sourceImageMetadata = default; if (processRequest) { // Lock any reads when a write is being done for the same key to prevent potential file locks. using (await AsyncLock.ReaderLockAsync(key).ConfigureAwait(false)) { // Check to see if the cache contains this image sourceImageMetadata = await sourceImageResolver.GetMetaDataAsync().ConfigureAwait(false); IImageCacheResolver cachedImageResolver = await this.cache.GetAsync(key).ConfigureAwait(false); if (cachedImageResolver != null) { ImageCacheMetadata cachedImageMetadata = await cachedImageResolver.GetMetaDataAsync().ConfigureAwait(false); if (cachedImageMetadata != default) { // Has the cached image expired or has the source image been updated? if (cachedImageMetadata.SourceLastWriteTimeUtc == sourceImageMetadata.LastWriteTimeUtc && cachedImageMetadata.CacheLastWriteTimeUtc > DateTimeOffset.Now.AddDays(-this.options.MaxCacheDays)) { // We're pulling the image from the cache. using (Stream cachedBuffer = await cachedImageResolver.OpenReadAsync().ConfigureAwait(false)) { await this.SendResponseAsync(imageContext, key, cachedBuffer, cachedImageMetadata).ConfigureAwait(false); } return; } } } } // Not cached? Let's get it from the image resolver. ChunkedMemoryStream outStream = null; try { if (processRequest) { // 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).ConfigureAwait(false)) { // No allocations here for inStream since we are passing the raw input stream. // outStream allocation depends on the memory allocator used. ImageCacheMetadata cachedImageMetadata = default; outStream = new ChunkedMemoryStream(this.memoryAllocator); using (Stream inStream = await sourceImageResolver.OpenReadAsync().ConfigureAwait(false)) using (var image = FormattedImage.Load(this.options.Configuration, inStream)) { image.Process(this.logger, this.processors, commands); this.options.OnBeforeSave?.Invoke(image); image.Save(outStream); // Check to see if the source metadata has a cachecontrol max-age value and use it to // override the default max age from our options. var maxAge = TimeSpan.FromDays(this.options.MaxBrowserCacheDays); if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue)) { maxAge = sourceImageMetadata.CacheControlMaxAge; } cachedImageMetadata = new ImageCacheMetadata( sourceImageMetadata.LastWriteTimeUtc, DateTime.UtcNow, image.Format.DefaultMimeType, maxAge); } // Allow for any further optimization of the image. Always reset the position just in case. outStream.Position = 0; string contentType = cachedImageMetadata.ContentType; string extension = this.formatUtilities.GetExtensionFromContentType(contentType); this.options.OnProcessed?.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension)); outStream.Position = 0; // Save the image to the cache and send the response to the caller. await this.cache.SetAsync(key, outStream, cachedImageMetadata).ConfigureAwait(false); await this.SendResponseAsync(imageContext, key, outStream, cachedImageMetadata).ConfigureAwait(false); } } } catch (Exception ex) { // Log the error internally then rethrow. // We don't call next here, the pipeline will automatically handle it this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); throw; } finally { outStream?.Dispose(); } } }
private async Task ProcessRequestAsync( HttpContext context, IImageResolver sourceImageResolver, ImageContext imageContext, IDictionary <string, string> commands) { // Create a cache key based on all the components of the requested url string uri = GetUri(context, commands); string key = this.cacheHash.Create(uri, this.options.CachedNameLength); // Check the cache, if present, not out of date and not requiring and update // we'll simply serve the file from there. ImageWorkerResult readResult = default; try { readResult = await this.IsNewOrUpdatedAsync(sourceImageResolver, imageContext, key); } finally { ReadWorkers.TryRemove(key, out Task <ImageWorkerResult> _); } if (!readResult.IsNewOrUpdated) { await this.SendResponseAsync(imageContext, key, readResult.CacheImageMetadata, readResult.Resolver); return; } // Not cached, or is updated? Let's get it from the image resolver. var sourceImageMetadata = readResult.SourceImageMetadata; // Enter an asynchronous write worker which prevents multiple writes and delays any reads for the same request. // This reduces the overheads of unnecessary processing. try { ImageWorkerResult writeResult = await WriteWorkers.GetOrAddAsync( key, async (key) => { RecyclableMemoryStream outStream = null; try { // Prevent a second request from starting a read during write execution. if (ReadWorkers.TryGetValue(key, out Task <ImageWorkerResult> readWork)) { await readWork; } ImageCacheMetadata cachedImageMetadata = default; outStream = new RecyclableMemoryStream(this.options.MemoryStreamManager); IImageFormat format; // 14.9.3 CacheControl Max-Age // Check to see if the source metadata has a CacheControl Max-Age value // and use it to override the default max age from our options. TimeSpan maxAge = this.options.BrowserMaxAge; if (!sourceImageMetadata.CacheControlMaxAge.Equals(TimeSpan.MinValue)) { maxAge = sourceImageMetadata.CacheControlMaxAge; } using (Stream inStream = await sourceImageResolver.OpenReadAsync()) { // No commands? We simply copy the stream across. if (commands.Count == 0) { await inStream.CopyToAsync(outStream); outStream.Position = 0; format = await Image.DetectFormatAsync(this.options.Configuration, outStream); } else { using var image = FormattedImage.Load(this.options.Configuration, inStream); image.Process( this.logger, this.processors, commands, this.commandParser, this.parserCulture); await this.options.OnBeforeSaveAsync.Invoke(image); image.Save(outStream); format = image.Format; } } // Allow for any further optimization of the image. outStream.Position = 0; string contentType = format.DefaultMimeType; string extension = this.formatUtilities.GetExtensionFromContentType(contentType); await this.options.OnProcessedAsync.Invoke(new ImageProcessingContext(context, outStream, commands, contentType, extension)); outStream.Position = 0; cachedImageMetadata = new ImageCacheMetadata( sourceImageMetadata.LastWriteTimeUtc, DateTime.UtcNow, contentType, maxAge, outStream.Length); // Save the image to the cache and send the response to the caller. await this.cache.SetAsync(key, outStream, cachedImageMetadata); // Remove any resolver from the cache so we always resolve next request // for the same key. CacheResolverLru.TryRemove(key); // Place the resolver in the lru cache. (IImageCacheResolver ImageCacheResolver, ImageCacheMetadata ImageCacheMetadata)cachedImage = await CacheResolverLru.GetOrAddAsync( key, async k => { IImageCacheResolver resolver = await this.cache.GetAsync(k); ImageCacheMetadata metadata = default; if (resolver != null) { metadata = await resolver.GetMetaDataAsync(); } return(resolver, metadata); }); return(new ImageWorkerResult(cachedImage.ImageCacheMetadata, cachedImage.ImageCacheResolver)); } catch (Exception ex) { // Log the error internally then rethrow. // We don't call next here, the pipeline will automatically handle it this.logger.LogImageProcessingFailed(imageContext.GetDisplayUrl(), ex); throw; } finally { await this.StreamDisposeAsync(outStream); } }); await this.SendResponseAsync(imageContext, key, writeResult.CacheImageMetadata, writeResult.Resolver); } finally { // As soon as we have sent a response from a writer the result is available from a reader so we remove this task. // Any existing awaiters will continue to await. WriteWorkers.TryRemove(key, out Task <ImageWorkerResult> _); } }