/// <summary>
 /// Tries to enqueue the given async write and callback
 /// </summary>
 /// <param name="w"></param>
 /// <param name="writerDelegate"></param>
 /// <returns></returns>
 public AsyncQueueResult Queue(AsyncWrite w, Func <AsyncWrite, Task> writerDelegate)
 {
     lock (sync)
     {
         if (queuedBytes < 0)
         {
             throw new InvalidOperationException();
         }
         if (queuedBytes + w.GetEntrySizeInMemory() > MaxQueueBytes)
         {
             return(AsyncQueueResult.QueueFull);                                                        //Because we would use too much ram.
         }
         if (c.ContainsKey(w.Key))
         {
             return(AsyncQueueResult.AlreadyPresent);                      //We already have a queued write for this data.
         }
         c.Add(w.Key, w);
         queuedBytes  += w.GetEntrySizeInMemory();
         w.RunningTask = Task.Run(
             async() => {
             try
             {
                 await writerDelegate(w);
             }
             finally
             {
                 Remove(w);
             }
         });
         return(AsyncQueueResult.Enqueued);
     }
 }
 /// <summary>
 /// Removes the specified object based on its relativePath and modifiedDateUtc values.
 /// </summary>
 /// <param name="w"></param>
 private void Remove(AsyncWrite w)
 {
     lock (sync)
     {
         c.Remove(w.Key);
         queuedBytes -= w.GetEntrySizeInMemory();
     }
 }
        /// <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;
                    }
                }
            });