/// <summary> /// Tries to fetch the result from disk cache, the memory queue, or create it. If the memory queue has space, /// the writeCallback() will be executed and the resulting bytes put in a queue for writing to disk. /// If the memory queue is full, writing to disk will be attempted synchronously. /// In either case, writing to disk can also fail if the disk cache is full and eviction fails. /// If the memory queue is full, eviction will be done synchronously and can cause other threads to time out /// while waiting for QueueLock /// </summary> /// <param name="key"></param> /// <param name="dataProviderCallback"></param> /// <param name="cancellationToken"></param> /// <param name="retrieveContentType"></param> /// <returns></returns> /// <exception cref="OperationCanceledException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> public async Task <AsyncCacheResult> GetOrCreateBytes( byte[] key, AsyncBytesResult dataProviderCallback, CancellationToken cancellationToken, bool retrieveContentType) { if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException(cancellationToken); } var swGetOrCreateBytes = Stopwatch.StartNew(); var entry = new CacheEntry(key, PathBuilder); // Tell cleanup what we're using CleanupManager.NotifyUsed(entry); // Fast path on disk hit var swFileExists = Stopwatch.StartNew(); var fileBasedResult = await TryGetFileBasedResult(entry, false, retrieveContentType, cancellationToken); if (fileBasedResult != null) { return(fileBasedResult); } // Just continue on creating the file. It must have been deleted between the calls swFileExists.Stop(); var cacheResult = new AsyncCacheResult(); //Looks like a miss. Let's enter a lock for the creation of the file. This is a different locking system // than for writing to the file //This prevents two identical requests from duplicating efforts. Different requests don't lock. //Lock execution using relativePath as the sync basis. Ignore casing differences. This prevents duplicate entries in the write queue and wasted CPU/RAM usage. var queueLockComplete = await QueueLocks.TryExecuteAsync(entry.StringKey, Options.WaitForIdenticalRequestsTimeoutMs, cancellationToken, async() => { var swInsideQueueLock = Stopwatch.StartNew(); // Now, if the item we seek is in the queue, we have a memcached hit. // If not, we should check the filesystem. It's possible the item has been written to disk already. // If both are a miss, we should see if there is enough room in the write queue. // If not, switch to in-thread writing. var existingQueuedWrite = CurrentWrites.Get(entry.StringKey); if (existingQueuedWrite != null) { cacheResult.Data = existingQueuedWrite.GetReadonlyStream(); cacheResult.ContentType = existingQueuedWrite.ContentType; cacheResult.Detail = AsyncCacheDetailResult.MemoryHit; return; } if (cancellationToken.IsCancellationRequested) { throw new OperationCanceledException(cancellationToken); } swFileExists.Start(); // Fast path on disk hit, now that we're in a synchronized state var fileBasedResult2 = await TryGetFileBasedResult(entry, true, retrieveContentType, cancellationToken); if (fileBasedResult2 != null) { cacheResult = fileBasedResult2; return; } // Just continue on creating the file. It must have been deleted between the calls swFileExists.Stop(); var swDataCreation = Stopwatch.StartNew(); //Read, resize, process, and encode the image. Lots of exceptions thrown here. var result = await dataProviderCallback(cancellationToken); swDataCreation.Stop(); //Create AsyncWrite object to enqueue var w = new AsyncWrite(entry.StringKey, result.Item2, result.Item1); cacheResult.Detail = AsyncCacheDetailResult.Miss; cacheResult.ContentType = w.ContentType; cacheResult.Data = w.GetReadonlyStream(); // Create a lambda which we can call either in a spawned Task (if enqueued successfully), or // in this task, if our buffer is full. async Task <AsyncCacheDetailResult> EvictWriteAndLogUnsynchronized(bool queueFull, TimeSpan dataCreationTime, CancellationToken ct) { var delegateStartedAt = DateTime.UtcNow; var swReserveSpace = Stopwatch.StartNew(); //We only permit eviction proceedings from within the queue or if the queue is disabled var allowEviction = !queueFull || CurrentWrites.MaxQueueBytes <= 0; var reserveSpaceResult = await CleanupManager.TryReserveSpace(entry, w.ContentType, w.GetUsedBytes(), allowEviction, EvictAndWriteLocks, ct); swReserveSpace.Stop(); var syncString = queueFull ? "synchronous" : "async"; if (!reserveSpaceResult.Success) { Logger?.LogError( queueFull ? "HybridCache synchronous eviction failed; {Message}. Time taken: {1}ms - {2}" : "HybridCache async eviction failed; {Message}. Time taken: {1}ms - {2}", syncString, reserveSpaceResult.Message, swReserveSpace.ElapsedMilliseconds, entry.RelativePath); return(AsyncCacheDetailResult.CacheEvictionFailed); } var swIo = Stopwatch.StartNew(); // We only force an immediate File.Exists check when running from the Queue // Otherwise it happens inside the lock var fileWriteResult = await FileWriter.TryWriteFile(entry, delegate(Stream s, CancellationToken ct2) { if (ct2.IsCancellationRequested) { throw new OperationCanceledException(ct2); } var fromStream = w.GetReadonlyStream(); return(fromStream.CopyToAsync(s, 81920, ct2)); }, !queueFull, Options.WaitForIdenticalDiskWritesMs, ct); swIo.Stop(); var swMarkCreated = Stopwatch.StartNew(); // Mark the file as created so it can be deleted await CleanupManager.MarkFileCreated(entry, w.ContentType, w.GetUsedBytes(), DateTime.UtcNow); swMarkCreated.Stop(); switch (fileWriteResult) { case CacheFileWriter.FileWriteStatus.LockTimeout: //We failed to lock the file. Logger?.LogWarning("HybridCache {Sync} write failed; disk lock timeout exceeded after {IoTime}ms - {Path}", syncString, swIo.ElapsedMilliseconds, entry.RelativePath); return(AsyncCacheDetailResult.WriteTimedOut); case CacheFileWriter.FileWriteStatus.FileAlreadyExists: Logger?.LogTrace("HybridCache {Sync} write found file already exists in {IoTime}ms, after a {DelayTime}ms delay and {CreationTime}- {Path}", syncString, swIo.ElapsedMilliseconds, delegateStartedAt.Subtract(w.JobCreatedAt).TotalMilliseconds, dataCreationTime, entry.RelativePath); return(AsyncCacheDetailResult.FileAlreadyExists); case CacheFileWriter.FileWriteStatus.FileCreated: if (queueFull) { Logger?.LogTrace(@"HybridCache synchronous write complete. Create: {CreateTime}ms. Write {WriteTime}ms. Mark Created: {MarkCreatedTime}ms. Eviction: {EvictionTime}ms - {Path}", Math.Round(dataCreationTime.TotalMilliseconds).ToString(CultureInfo.InvariantCulture).PadLeft(4), swIo.ElapsedMilliseconds.ToString().PadLeft(4), swMarkCreated.ElapsedMilliseconds.ToString().PadLeft(4), swReserveSpace.ElapsedMilliseconds.ToString().PadLeft(4), entry.RelativePath); } else { Logger?.LogTrace(@"HybridCache async write complete. Create: {CreateTime}ms. Write {WriteTime}ms. Mark Created: {MarkCreatedTime}ms Eviction {EvictionTime}ms. Delay {DelayTime}ms. - {Path}", Math.Round(dataCreationTime.TotalMilliseconds).ToString(CultureInfo.InvariantCulture).PadLeft(4), swIo.ElapsedMilliseconds.ToString().PadLeft(4), swMarkCreated.ElapsedMilliseconds.ToString().PadLeft(4), swReserveSpace.ElapsedMilliseconds.ToString().PadLeft(4), Math.Round(delegateStartedAt.Subtract(w.JobCreatedAt).TotalMilliseconds).ToString(CultureInfo.InvariantCulture).PadLeft(4), entry.RelativePath); } return(AsyncCacheDetailResult.WriteSucceeded); default: throw new ArgumentOutOfRangeException(); } } async Task <AsyncCacheDetailResult> EvictWriteAndLogSynchronized(bool queueFull, TimeSpan dataCreationTime, CancellationToken ct) { var cacheDetailResult = AsyncCacheDetailResult.Unknown; var writeLockComplete = await EvictAndWriteLocks.TryExecuteAsync(entry.StringKey, Options.WaitForIdenticalRequestsTimeoutMs, cancellationToken, async() => { cacheDetailResult = await EvictWriteAndLogUnsynchronized(queueFull, dataCreationTime, ct); }); if (!writeLockComplete) { cacheDetailResult = AsyncCacheDetailResult.EvictAndWriteLockTimedOut; } return(cacheDetailResult); } var swEnqueue = Stopwatch.StartNew(); var queueResult = CurrentWrites.Queue(w, async delegate { try { var unused = await EvictWriteAndLogSynchronized(false, swDataCreation.Elapsed, CancellationToken.None); } catch (Exception ex) { Logger?.LogError(ex, "HybridCache failed to flush async write, {Exception} {Path}\n{StackTrace}", ex.ToString(), entry.RelativePath, ex.StackTrace); } }); swEnqueue.Stop(); swInsideQueueLock.Stop(); swGetOrCreateBytes.Stop(); if (queueResult == AsyncWriteCollection.AsyncQueueResult.QueueFull) { if (Options.WriteSynchronouslyWhenQueueFull) { var writerDelegateResult = await EvictWriteAndLogSynchronized(true, swDataCreation.Elapsed, cancellationToken); cacheResult.Detail = writerDelegateResult; } } });