/// <summary> /// Returns true if either (a) the file was written, or (b) the file already existed /// Returns false if the in-process lock failed. Throws an exception if any kind of file or processing exception occurs. /// </summary> /// <param name="result"></param> /// <param name="physicalPath"></param> /// <param name="relativePath"></param> /// <param name="writeCallback"></param> /// <param name="timeoutMs"></param> /// <param name="recheckFileSystem"></param> /// <returns></returns> private async Task <bool> TryWriteFile(CacheResult result, string physicalPath, string relativePath, AsyncWriteResult writeCallback, int timeoutMs, bool recheckFileSystem) { // ReSharper disable once InvertIf if (recheckFileSystem) { var miss = !Index.ExistsCertain(relativePath, physicalPath); if (!miss && !Locks.MayBeLocked(relativePath.ToUpperInvariant())) { return(true); } } //Lock execution using relativePath as the sync basis. Ignore casing differences. This locking is process-local, but we also have code to handle file locking. return(await Locks.TryExecuteAsync(relativePath.ToUpperInvariant(), timeoutMs, CancellationToken.None, async() => { //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 (!Index.Exists(relativePath, physicalPath)) { var subdirectoryPath = Path.GetDirectoryName(physicalPath); //Create subdirectory if needed. if (subdirectoryPath != null && !Directory.Exists(subdirectoryPath)) { Directory.CreateDirectory(subdirectoryPath); } //Open stream //Catch IOException, and if it is a file lock, // then it's another process writing to the file, and we can serve the file afterwards //TODO: Catch UnauthorizedAccessException and log issue about file permissions. //... If we can wait for a read handle for a specified timeout. IOException lockedException = null; try { var tempFile = physicalPath + ".tmp_" + new Random().Next(int.MaxValue).ToString("x") + ".tmp"; var fs = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None); var finished = false; try { using (fs) { //Run callback to write the cached data await writeCallback(fs); //Can throw any number of exceptions. await fs.FlushAsync(); fs.Flush(true); if (fs.Position == 0) { throw new InvalidOperationException("Disk cache wrote zero bytes to file"); } finished = true; } } finally { //Don't leave half-written files around. if (!finished) { try { if (File.Exists(tempFile)) { File.Delete(tempFile); } } catch { // ignored } } } var moved = false; // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (finished) { try { File.Move(tempFile, physicalPath); moved = true; } catch (IOException) { //Will throw IO exception if already exists. Which we consider a hit, so we delete the tempFile try { if (File.Exists(tempFile)) { File.Delete(tempFile); } } catch { // ignored } } } if (moved) { var createdUtc = DateTime.UtcNow; //Set the created date, so we know the last time we updated the cache.s File.SetCreationTimeUtc(physicalPath, createdUtc); //Update index //TODO: what should sourceModifiedUtc be when there is no modified date? Index.SetCachedFileInfo(relativePath, new CachedFileInfo(createdUtc, createdUtc)); //This was a cache miss if (result != null) { result.Result = CacheQueryResult.Miss; } } } catch (IOException ex) { if (IsFileLocked(ex)) { lockedException = ex; } else { throw; } } if (lockedException != null) { //Somehow in between verifying the file didn't exist and trying to create it, the file was created and locked by someone else. //When hashModifiedDate==true, we don't care what the file contains, we just want it to exist. If the file is available for //reading within timeoutMs, simply do nothing and let the file be returned as a hit. var waitForFile = new Stopwatch(); var opened = false; while (!opened && waitForFile.ElapsedMilliseconds < timeoutMs) { waitForFile.Start(); var waitABitMore = false; try { using (var unused = new FileStream(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) opened = true; } catch (IOException iex) { if (IsFileLocked(iex)) { waitABitMore = true; } else { throw; } } if (waitABitMore) { await Task.Delay((int)Math.Min(30, Math.Round(timeoutMs / 3.0))); } waitForFile.Stop(); } if (!opened) { throw lockedException; //By not throwing an exception, it is considered a hit by the rest of the code. } } } })); }