From 91a80f709b69f0817ffa8e44b0ee9da51b2b9f00 Mon Sep 17 00:00:00 2001 From: Simon Thum Date: Fri, 16 Feb 2018 11:37:27 +0100 Subject: [PATCH 1/2] Implement a filename exposing raw cache mode This intends to support zerocopy/SendFile scenarios. There is no serialization in this mode, the cache is basically managing a bunch of files. The mode intentionally only accepts byte[] or streams, it does not support fetching them to avoid memory and stream lifetime issues. Because of the lifetime issue, a configurable safety margin is added to the expiry logic. --- src/FileCache.UnitTests/FileCacheTest.cs | 34 ++++ src/FileCache.sln | 12 ++ src/FileCache/FileCache.cs | 210 +++++++++++++++++------ 3 files changed, 201 insertions(+), 55 deletions(-) diff --git a/src/FileCache.UnitTests/FileCacheTest.cs b/src/FileCache.UnitTests/FileCacheTest.cs index 8e8d919..b25918d 100644 --- a/src/FileCache.UnitTests/FileCacheTest.cs +++ b/src/FileCache.UnitTests/FileCacheTest.cs @@ -135,6 +135,8 @@ public void CacheSizeTest() cacheSize.Should().NotBe(0); _cache.Remove("foo"); + cacheSize = _cache.GetCacheSize(); + cacheSize.Should().Be(0); cacheSize = _cache.CurrentCacheSize; cacheSize.Should().Be(0); } @@ -385,5 +387,37 @@ public void CleanCacheTest() _cache["foo"].Should().BeNull(); _cache["bar"].Should().NotBeNull(); } + + [TestMethod] + public void RawFileCacheTest() + { + // Test with filename based payload + FileCache perfCache = new FileCache("filePayload"); + perfCache.PayloadReadMode = FileCache.PayloadMode.Filename; + perfCache.PayloadWriteMode = FileCache.PayloadMode.RawBytes; + + perfCache["mybytes"] = new byte[] { 4, 2 }; + perfCache.Get("mybytes").Should().BeOfType(typeof(string)); + File.Exists((string)perfCache.Get("mybytes")).Should().BeTrue(); + } + + + [TestMethod] + public void RawFileWithExpiryTest() + { + // Test with sliding expiry + FileCache perfCacheWithExpiry = new FileCache("filePayloadExpiry"); + var pol = new CacheItemPolicy(); + pol.SlidingExpiration = new TimeSpan(0, 0, 2); + perfCacheWithExpiry.DefaultPolicy = pol; + perfCacheWithExpiry.FilenameAsPayloadSafetyMargin = new TimeSpan(0, 0, 1); + perfCacheWithExpiry.PayloadReadMode = FileCache.PayloadMode.Filename; + perfCacheWithExpiry.PayloadWriteMode = FileCache.PayloadMode.RawBytes; + + perfCacheWithExpiry["mybytes"] = new byte[] { 4, 2 }; + File.Exists((string)perfCacheWithExpiry.Get("mybytes")).Should().BeTrue(); + Thread.Sleep(2000); + perfCacheWithExpiry.Get("mybytes").Should().BeNull(); + } } } diff --git a/src/FileCache.sln b/src/FileCache.sln index 4d98691..6dec382 100644 --- a/src/FileCache.sln +++ b/src/FileCache.sln @@ -49,6 +49,18 @@ Global {F7C96C00-C7CD-4503-B6BE-B79700D24F68}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {F7C96C00-C7CD-4503-B6BE-B79700D24F68}.Release|Mixed Platforms.Build.0 = Release|Any CPU {F7C96C00-C7CD-4503-B6BE-B79700D24F68}.Release|x86.ActiveCfg = Release|Any CPU + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Any CPU.ActiveCfg = Debug|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Any CPU.Build.0 = Debug|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|Mixed Platforms.Build.0 = Debug|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|x86.ActiveCfg = Debug|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Debug|x86.Build.0 = Debug|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Any CPU.ActiveCfg = Release|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Any CPU.Build.0 = Release|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Mixed Platforms.ActiveCfg = Release|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|Mixed Platforms.Build.0 = Release|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|x86.ActiveCfg = Release|x86 + {3E796953-08A9-4B32-991D-9D7E187BA56D}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/FileCache/FileCache.cs b/src/FileCache/FileCache.cs index 4f4c91d..9da6f21 100644 --- a/src/FileCache/FileCache.cs +++ b/src/FileCache/FileCache.cs @@ -30,6 +30,7 @@ public class FileCache : ObjectCache // this is a file used to prevent multiple processes from trying to "clean" at the same time private const string SemaphoreFile = "cache.sem"; private long _currentCacheSize = 0; + private PayloadMode _readMode = PayloadMode.Serializable; public string CacheDir { get; protected set; } @@ -43,6 +44,51 @@ public class FileCache : ObjectCache /// public CacheItemPolicy DefaultPolicy { get; set; } + /// + /// Specified how the cache payload is to be handled. + /// + public enum PayloadMode + { + /// + /// Treat the payload a a serializable object. + /// + Serializable, + /// + /// Treat the payload as a file name. File content will be copied on add, while get returns the file name. + /// + Filename, + /// + /// Treat the paylad as raw bytes. A byte[] and readable streams are supported on add. + /// + RawBytes + } + + /// + /// Specified whether the payload is deserialized or just the file name. + /// + public PayloadMode PayloadReadMode { + get => _readMode; + set { + if (value == PayloadMode.RawBytes) + { + throw new ArgumentException("The read mode cannot be set to RawBytes. Use the file name please."); + } + _readMode = value; + } + } + + /// + /// Specified how the payload is to be handled on add operations. + /// + public PayloadMode PayloadWriteMode { get; set; } = PayloadMode.Serializable; + + /// + /// The amount of time before expiry that a filename will be used as a payoad. I.e. + /// the amount of time the cache's user can safely use the file delivered as a payload. + /// Default 10 minutes. + /// + public TimeSpan FilenameAsPayloadSafetyMargin = TimeSpan.FromMinutes(10); + /// /// Used to determine how long the FileCache will wait for a file to become /// available. Default (00:00:00) is indefinite. Should the timeout be @@ -536,7 +582,7 @@ private void FlushHelper(DirectoryInfo root, DateTime minDate) public CacheItemPolicy GetPolicy(string key, string regionName = null) { CacheItemPolicy policy = new CacheItemPolicy(); - FileCachePayload payload = ReadFile(key, regionName) as FileCachePayload; + FileCachePayload payload = ReadFile(PayloadMode.Filename, key, regionName) as FileCachePayload; if (payload != null) { try @@ -619,10 +665,11 @@ private FileStream GetStream(string path, FileMode mode, FileAccess access, File /// /// This function serves to centralize file reads within this class. /// - /// + /// the payload reading mode + /// /// /// - private FileCachePayload ReadFile(string key, string regionName = null, SerializationBinder objectBinder = null) + private FileCachePayload ReadFile(PayloadMode mode, string key, string regionName = null, SerializationBinder objectBinder = null) { object data = null; SerializableCacheItemPolicy policy = new SerializableCacheItemPolicy(); @@ -632,33 +679,18 @@ private FileCachePayload ReadFile(string key, string regionName = null, Serializ if (File.Exists(cachePath)) { - using (FileStream stream = GetStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + switch (mode) { - BinaryFormatter formatter = new BinaryFormatter(); - - //AC: From http://spazzarama.com//2009/06/25/binary-deserialize-unable-to-find-assembly/ - // Needed to deserialize custom objects - if (objectBinder != null) - { - //take supplied binder over default binder - formatter.Binder = objectBinder; - } - else if (_binder != null) - { - formatter.Binder = _binder; - } - try - { - data = formatter.Deserialize(stream); - } - catch (SerializationException) - { - data = null; - } - finally - { - stream.Close(); - } + default: + case PayloadMode.Filename: + data = cachePath; + break; + case PayloadMode.Serializable: + data = DeserializePayloadData(objectBinder, cachePath); + break; + case PayloadMode.RawBytes: + data = LoadRawPayloadData(cachePath); + break; } } if (File.Exists(policyPath)) @@ -675,10 +707,6 @@ private FileCachePayload ReadFile(string key, string regionName = null, Serializ { policy = new SerializableCacheItemPolicy(); } - finally - { - stream.Close(); - } } } payload.Payload = data; @@ -686,36 +714,102 @@ private FileCachePayload ReadFile(string key, string regionName = null, Serializ return payload; } + private object LoadRawPayloadData(string cachePath) + { + throw new NotSupportedException("Reading raw payload is not currently supported."); + } + + private object DeserializePayloadData(SerializationBinder objectBinder, string cachePath) + { + object data; + using (FileStream stream = GetStream(cachePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + BinaryFormatter formatter = new BinaryFormatter(); + + //AC: From http://spazzarama.com//2009/06/25/binary-deserialize-unable-to-find-assembly/ + // Needed to deserialize custom objects + if (objectBinder != null) + { + //take supplied binder over default binder + formatter.Binder = objectBinder; + } + else if (_binder != null) + { + formatter.Binder = _binder; + } + + try + { + data = formatter.Deserialize(stream); + } + catch (SerializationException) + { + data = null; + } + } + + return data; + } + /// /// This function serves to centralize file writes within this class /// - private void WriteFile(string key, FileCachePayload data, string regionName = null) + private void WriteFile(PayloadMode mode, string key, FileCachePayload data, string regionName = null, bool policyUpdateOnly = false) { string cachedPolicy = GetPolicyPath(key, regionName); string cachedItemPath = GetCachePath(key, regionName); - //remove current item / policy from cache size calculations - if(File.Exists(cachedItemPath)) - { - CurrentCacheSize -= new FileInfo(cachedItemPath).Length; - } - if(File.Exists(cachedPolicy)) + + if (!policyUpdateOnly) { - CurrentCacheSize -= new FileInfo(cachedPolicy).Length; - } + long oldBlobSize = 0; + if (File.Exists(cachedItemPath)) + { + oldBlobSize = new FileInfo(cachedItemPath).Length; + } - //write the object payload (lock the file so we can write to it and force others to wait for us to finish) - using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None)) - { - BinaryFormatter formatter = new BinaryFormatter(); - formatter.Serialize(stream, data.Payload); + switch (mode) { + case PayloadMode.Serializable: + using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + + BinaryFormatter formatter = new BinaryFormatter(); + formatter.Serialize(stream, data.Payload); + } + break; + case PayloadMode.RawBytes: + using (FileStream stream = GetStream(cachedItemPath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + + if (data.Payload is byte[]) + { + byte[] dataPayload = (byte[]) data.Payload; + stream.Write(dataPayload, 0, dataPayload.Length); + } + else if (data.Payload is Stream) + { + Stream dataPayload = (Stream) data.Payload; + dataPayload.CopyTo(stream); + // no close or the like, we are not the owner + } + } + break; + + case PayloadMode.Filename: + File.Copy((string) data.Payload, cachedItemPath, true); + break; + } //adjust cache size (while we have the file to ourselves) - CurrentCacheSize += new FileInfo(cachedItemPath).Length; + CurrentCacheSize += new FileInfo(cachedItemPath).Length - oldBlobSize; + } - stream.Close(); + //remove current policy file from cache size calculations + if (File.Exists(cachedPolicy)) + { + CurrentCacheSize -= new FileInfo(cachedPolicy).Length; } - + //write the cache policy using (FileStream stream = GetStream(cachedPolicy, FileMode.Create, FileAccess.Write, FileShare.None)) { @@ -868,7 +962,7 @@ public override object AddOrGetExisting(string key, object value, CacheItemPolic } SerializableCacheItemPolicy cachePolicy = new SerializableCacheItemPolicy(policy); FileCachePayload newPayload = new FileCachePayload(value, cachePolicy); - WriteFile(key, newPayload, regionName); + WriteFile(PayloadWriteMode, key, newPayload, regionName); //As documented in the spec (http://msdn.microsoft.com/en-us/library/dd780602.aspx), return the old //cached value or null @@ -924,14 +1018,20 @@ public override DefaultCacheCapabilities DefaultCacheCapabilities public override object Get(string key, string regionName = null) { - FileCachePayload payload = ReadFile(key, regionName) as FileCachePayload; + FileCachePayload payload = ReadFile(PayloadReadMode, key, regionName) as FileCachePayload; string cachedItemPath = GetCachePath(key, regionName); + DateTime cutoff = DateTime.Now; + if (PayloadReadMode == PayloadMode.Filename) + { + cutoff += FilenameAsPayloadSafetyMargin; + } + //null payload? if (payload != null) { //did the item expire? - if (payload.Policy.AbsoluteExpiration < DateTime.Now) + if (payload.Policy.AbsoluteExpiration < cutoff) { //set the payload to null payload.Payload = null; @@ -953,7 +1053,7 @@ public override object Get(string key, string regionName = null) if (payload.Policy.SlidingExpiration > new TimeSpan()) { payload.Policy.AbsoluteExpiration = DateTime.Now.Add(payload.Policy.SlidingExpiration); - WriteFile(cachedItemPath, payload, regionName); + WriteFile(PayloadWriteMode, cachedItemPath, payload, regionName, true); } } @@ -1053,7 +1153,7 @@ public override object Remove(string key, string regionName = null) // CT note: calling Get from remove leads to an infinite loop and stack overflow, // so I replaced it with a simple ReadFile call. None of the code here actually // uses this object returned, but just in case someone else's outside code does. - FileCachePayload fcp = ReadFile(key, regionName); + FileCachePayload fcp = ReadFile(PayloadMode.Filename, key, regionName); valueToDelete = fcp.Payload; string path = GetCachePath(key, regionName); CurrentCacheSize -= new FileInfo(path).Length; From 5b669a1f22ebaf1f9c7079513e323104d6611a2b Mon Sep 17 00:00:00 2001 From: Simon Thum Date: Tue, 6 Nov 2018 16:17:10 +0100 Subject: [PATCH 2/2] Favor enumeration over array creation --- src/FileCache/FileCache.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/FileCache/FileCache.cs b/src/FileCache/FileCache.cs index 9da6f21..2cabc27 100644 --- a/src/FileCache/FileCache.cs +++ b/src/FileCache/FileCache.cs @@ -497,13 +497,13 @@ private long CacheSizeHelper(DirectoryInfo root) long size = 0; // Add file sizes. - FileInfo[] fis = root.GetFiles(); + var fis = root.EnumerateFiles(); foreach (FileInfo fi in fis) { size += fi.Length; } // Add subdirectory sizes. - DirectoryInfo[] dis = root.GetDirectories(); + var dis = root.EnumerateDirectories(); foreach (DirectoryInfo di in dis) { size += CacheSizeHelper(di); @@ -555,8 +555,7 @@ public void Flush(DateTime minDate, string regionName = null) private void FlushHelper(DirectoryInfo root, DateTime minDate) { // check files. - FileInfo[] fis = root.GetFiles(); - foreach (FileInfo fi in fis) + foreach (FileInfo fi in root.EnumerateFiles()) { //is the file stale? if(minDate > File.GetLastAccessTime(fi.FullName)) @@ -566,8 +565,7 @@ private void FlushHelper(DirectoryInfo root, DateTime minDate) } // check subdirectories - DirectoryInfo[] dis = root.GetDirectories(); - foreach (DirectoryInfo di in dis) + foreach (DirectoryInfo di in root.EnumerateDirectories()) { FlushHelper(di, minDate); } @@ -602,7 +600,7 @@ public CacheItemPolicy GetPolicy(string key, string regionName = null) /// /// /// - public string[] GetKeys(string regionName = null) + public IEnumerable GetKeys(string regionName = null) { string region = ""; if (string.IsNullOrEmpty(regionName) == false) @@ -610,15 +608,13 @@ public string[] GetKeys(string regionName = null) region = regionName; } string directory = Path.Combine(CacheDir, _cacheSubFolder, region); - List keys = new List(); if (Directory.Exists(directory)) { - foreach (string file in Directory.GetFiles(directory)) + foreach (string file in Directory.EnumerateFiles(directory)) { - keys.Add(Path.GetFileNameWithoutExtension(file)); + yield return Path.GetFileNameWithoutExtension(file); } } - return keys.ToArray(); } #endregion