/// <summary> /// Returns false when (a) the specified AsyncWrite value already exists or (b) the queue is full /// </summary> /// <param name="w"></param> /// <param name="writerDelegate"></param> /// <returns></returns> public bool QueueAsync(AsyncWrite w, AsyncWriterDelegate writerDelegate) { lock (_sync) { if (GetQueuedBufferBytes() + w.GetBufferLength() > MaxQueueBytes) { return(false); //Because we would use too much ram. } if (c.ContainsKey(w.Key)) { return(false); //We already have a queued write for this data. } c.Add(w.Key, w); Task.Run( async() => { await writerDelegate(w); Remove(w); }).ConfigureAwait(false); return(true); } }
/// <summary> /// Returns false when (a) the specified AsyncWrite value already exists, (b) the queue is full, or (c) the thread pool queue is full /// </summary> /// <param name="w"></param> /// <returns></returns> public bool Queue(AsyncWrite w, WriterDelegate writerDelegate) { lock (_sync) { if (GetQueuedBufferBytes() + w.GetBufferLength() > MaxQueueBytes) { return(false); //Because we would use too much ram. } if (c.ContainsKey(HashTogether(w.RelativePath, w.ModifiedDateUtc))) { return(false); //We already have a queued write for this data. } if (!ThreadPool.QueueUserWorkItem(delegate(object state){ AsyncWrite job = state as AsyncWrite; writerDelegate(job); }, w)) { return(false); //thread pool refused } return(true); } }
/// <summary> /// Returns false when (a) the specified AsyncWrite value already exists, (b) the queue is full, or (c) the thread pool queue is full /// </summary> /// <param name="w"></param> /// <param name="writerDelegate"></param> /// <returns></returns> public bool Queue(AsyncWrite w, WriterDelegate writerDelegate) { lock (_sync) { if (GetQueuedBufferBytes() + w.GetBufferLength() > MaxQueueBytes) { return(false); //Because we would use too much ram. } if (c.ContainsKey(w.Key)) { return(false); //We already have a queued write for this data. } c.Add(w.Key, w); if (!ThreadPool.QueueUserWorkItem(delegate(object state) { AsyncWrite job = state as AsyncWrite; writerDelegate(job); Remove(job); }, w)) { return(false); //thread pool refused } return(true); } }
/// <summary> /// Removes the specified object based on its relativepath and modifieddateutc values. /// </summary> /// <param name="w"></param> public void Remove(AsyncWrite w) { lock (_sync) { c.Remove(w.Key); } }
/// <summary> /// May return either a physical file name or a MemoryStream with the data. /// Faster than GetCachedFile, as writes are (usually) asynchronous. If the write queue is full, the write is forced to be synchronous again. /// Identical to GetCachedFile() when asynchronous=false /// </summary> /// <param name="keyBasis"></param> /// <param name="extension"></param> /// <param name="writeCallback"></param> /// <param name="sourceModifiedUtc"></param> /// <param name="timeoutMs"></param> /// <returns></returns> public CacheResult GetCachedFile(string keyBasis, string extension, ResizeImageDelegate writeCallback, DateTime sourceModifiedUtc, int timeoutMs, bool asynchronous) { Stopwatch sw = null; if (lp.Logger != null) { sw = new Stopwatch(); sw.Start(); } bool hasModifiedDate = !sourceModifiedUtc.Equals(DateTime.MinValue); //Hash the modified date if needed. if (hashModifiedDate && hasModifiedDate) keyBasis += "|" + sourceModifiedUtc.Ticks.ToString(NumberFormatInfo.InvariantInfo); //Relative to the cache directory. Not relative to the app or domain root string relativePath = new UrlHasher().hash(keyBasis, subfolders, "/") + '.' + extension; //Physical path string physicalPath = PhysicalCachePath.TrimEnd('\\', '/') + System.IO.Path.DirectorySeparatorChar + relativePath.Replace('/', System.IO.Path.DirectorySeparatorChar); CacheResult result = new CacheResult(CacheQueryResult.Hit, physicalPath, relativePath); bool compareModifiedDates = hasModifiedDate && !hashModifiedDate; bool asyncFailed = false; //On the first check, verify the file exists using System.IO directly (the last 'true' parameter). if (!asynchronous) { //On the first check, verify the file exists using System.IO directly (the last 'true' parameter) //May throw an IOException if the file cannot be opened, and is locked by an external processes for longer than timeoutMs. //This method may take longer than timeoutMs under absolute worst conditions. if (!TryWriteFile(result, physicalPath, relativePath, writeCallback, sourceModifiedUtc, timeoutMs, true)) { //On failure result.Result = CacheQueryResult.Failed; } } else if (!compareModifiedDates ? !Index.existsCertain(relativePath, physicalPath) : !Index.modifiedDateMatchesCertainExists(sourceModifiedUtc, relativePath, physicalPath)) { //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 - far less contention, as it doesn't include the //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. if (!QueueLocks.TryExecute(relativePath.ToUpperInvariant(), timeoutMs, delegate() { //Now, if the item we seek is in the queue, we have a memcached hit. If not, we should check the index. 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. AsyncWrite t = CurrentWrites.Get(relativePath, sourceModifiedUtc); if (t != null) result.Data = t.GetReadonlyStream(); //On the second check, use cached data for speed. The cached data should be updated if another thread updated a file (but not if another process did). if (t == null && (!compareModifiedDates ? !Index.exists(relativePath, physicalPath) : !Index.modifiedDateMatches(sourceModifiedUtc, relativePath, physicalPath))) { result.Result = CacheQueryResult.Miss; //Still a miss, we even rechecked the filesystem. Write to memory. MemoryStream ms = new MemoryStream(4096); //4K initial capacity is minimal, but this array will get copied around alot, better to underestimate. //Read, resize, process, and encode the image. Lots of exceptions thrown here. writeCallback(ms); ms.Position = 0; AsyncWrite w = new AsyncWrite(CurrentWrites,ms, physicalPath, relativePath, sourceModifiedUtc); if (CurrentWrites.Queue(w, delegate(AsyncWrite job) { try { Stopwatch swio = new Stopwatch(); swio.Start(); //TODO: perhaps a different timeout? if (!TryWriteFile(null, job.PhysicalPath, job.RelativePath, delegate(Stream s) { ((MemoryStream)job.GetReadonlyStream()).WriteTo(s); }, job.ModifiedDateUtc, timeoutMs, true)) { swio.Stop(); //We failed to lock the file. if (lp.Logger != null) lp.Logger.Warn("Failed to flush async write, timeout exceeded after {1}ms - {0}", result.RelativePath, swio.ElapsedMilliseconds); } else { swio.Stop(); if (lp.Logger != null) lp.Logger.Trace("{0}ms: Async write started {1}ms after enqueue for {2}", swio.ElapsedMilliseconds.ToString().PadLeft(4), DateTime.UtcNow.Subtract(w.JobCreatedAt).Subtract(swio.Elapsed).TotalMilliseconds, result.RelativePath); } } catch (Exception ex) { if (lp.Logger != null) { lp.Logger.Error("Failed to flush async write, {0} {1}\n{2}",ex.ToString(), result.RelativePath,ex.StackTrace); } } finally { CurrentWrites.Remove(job); //Remove from the queue, it's done or failed. } })) { //We queued it! Send back a read-only memory stream result.Data = w.GetReadonlyStream(); } else { asyncFailed = false; //We failed to queue it - either the ThreadPool was exhausted or we exceeded the MB limit for the write queue. //Write the MemoryStream to disk using the normal method. //This is nested inside a queuelock because if we failed here, the next one will also. Better to force it to wait until the file is written to disk. if (!TryWriteFile(result, physicalPath, relativePath, delegate(Stream s) { ms.WriteTo(s); }, sourceModifiedUtc, timeoutMs, false)) { if (lp.Logger != null) lp.Logger.Warn("Failed to queue async write, also failed to lock for sync writing: {0}", result.RelativePath); } } } })) { //On failure result.Result = CacheQueryResult.Failed; } } if (lp.Logger != null) { sw.Stop(); lp.Logger.Trace("{0}ms: {3}{1} for {2}, Key: {4}", sw.ElapsedMilliseconds.ToString(NumberFormatInfo.InvariantInfo).PadLeft(4), result.Result.ToString(), result.RelativePath, asynchronous ? (asyncFailed ? "Fallback to sync " : "Async ") : "", keyBasis); } //Fire event if (CacheResultReturned != null) CacheResultReturned(this, result); return result; }
/// <summary> /// Removes the specified object based on its relativepath and modifieddateutc values. /// </summary> /// <param name="w"></param> public void Remove(AsyncWrite w) { lock (_sync) { c.Remove(HashTogether(w.RelativePath, w.ModifiedDateUtc)); } }
/// <summary> /// Returns false when (a) the specified AsyncWrite value already exists, (b) the queue is full, or (c) the thread pool queue is full /// </summary> /// <param name="w"></param> /// <returns></returns> public bool Queue(AsyncWrite w,WriterDelegate writerDelegate ) { lock (_sync) { if (GetQueuedBufferBytes() + w.GetBufferLength() > MaxQueueBytes) return false; //Because we would use too much ram. if (c.ContainsKey(HashTogether(w.RelativePath, w.ModifiedDateUtc))) return false; //We already have a queued write for this data. if (!ThreadPool.QueueUserWorkItem(delegate(object state){ AsyncWrite job = state as AsyncWrite; writerDelegate(job); }, w)) return false; //thread pool refused return true; } }