1 /** 2 * ae.sys.persistence.core 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.persistence.core; 15 16 import core.time; 17 18 import std.traits; 19 20 /// When to flush data. 21 enum FlushPolicy 22 { 23 none, /// Never. The cache is not writable. 24 manual, /// Only manually (using `save`). 25 atScopeExit, /// When the cache object is destroyed (object destructor). 26 atThreadExit, /// When the thread exits (static destructor). 27 // TODO: immediate flushing. Could work only with values without mutable indirections. 28 // TODO: this can actually be a bitmask 29 } 30 31 private bool delayed(FlushPolicy policy) { return policy > FlushPolicy.manual; } 32 33 /// Placeholder `DataPutter` argument to indicate no writing. 34 struct None {} 35 36 /// Cache values in-memory, and automatically load/save them as needed via the specified functions. 37 /// Actual loading/saving is done via alias functions. 38 /// KeyGetter may return .init (of its return type) if the resource does not yet exist, 39 /// but once it returns non-.init it may not return .init again. 40 /// A bool key can be used to load a resource from disk only once (lazily), 41 /// as is currently done with LoadPolicy.once. 42 /// Delayed flush policies require a bool key, to avoid mid-air collisions. 43 /// Params: 44 /// DataGetter = Callable which obtains a new copy of the data. 45 /// KeyGetter = Callable which (cheaply) obtains the current version of the data. 46 /// DataPutter = Callable which saves the data (or `None`). 47 /// flushPolicy = When to save the data. 48 mixin template CacheCore(alias DataGetter, alias KeyGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none) 49 { 50 import std.traits; 51 import ae.sys.memory; 52 53 alias _CacheCore_Data = ReturnType!DataGetter; 54 alias _CacheCore_Key = ReturnType!KeyGetter; 55 56 enum _CacheCore_readOnly = flushPolicy == FlushPolicy.none; 57 58 _CacheCore_Data cachedData; /// The currently loaded version of the data. 59 _CacheCore_Key cachedDataKey; /// The key (version) of the last loaded version of the data. 60 61 void _CacheCore_update() 62 { 63 auto newKey = KeyGetter(); 64 65 // No going back to Key.init after returning non-.init 66 assert(cachedDataKey == _CacheCore_Key.init || newKey != _CacheCore_Key.init); 67 68 if (newKey != cachedDataKey) 69 { 70 static if (flushPolicy == FlushPolicy.atThreadExit) 71 { 72 if (cachedDataKey == _CacheCore_Key.init) // first load 73 _CacheCore_registerFlush(); 74 } 75 cachedData = DataGetter(); 76 cachedDataKey = newKey; 77 } 78 } 79 80 static if (_CacheCore_readOnly) 81 @property auto _CacheCore_data() { _CacheCore_update(); return cast(immutable)cachedData; } 82 else 83 @property ref auto _CacheCore_data() { _CacheCore_update(); return cachedData; } 84 85 static if (!_CacheCore_readOnly) 86 { 87 /// Save the data. 88 /// Obtain a fresh copy of the key afterwards 89 /// (by reading it again), unless `exiting` is true. 90 void save(bool exiting=false)() 91 { 92 if (cachedDataKey != _CacheCore_Key.init || cachedData != _CacheCore_Data.init) 93 { 94 DataPutter(cachedData); 95 static if (!exiting) 96 cachedDataKey = KeyGetter(); 97 } 98 } 99 100 static if (flushPolicy.delayed()) 101 { 102 // A bool key implies that data will be loaded only once (lazy loading). 103 static assert(is(_CacheCore_Key==bool), "Delayed flush with automatic reload allows mid-air collisions"); 104 } 105 106 static if (flushPolicy == FlushPolicy.atScopeExit) 107 { 108 ~this() 109 { 110 save!true(); 111 } 112 } 113 114 static if (flushPolicy == FlushPolicy.atThreadExit) 115 { 116 void _CacheCore_registerFlush() 117 { 118 // https://issues.dlang.org/show_bug.cgi?id=12038 119 assert(!onStack(cast(void*)&this)); 120 auto pthis = &this; // Silence "copying &this into allocated memory escapes a reference to parameter variable this" 121 _CacheCore_pending ~= pthis; 122 } 123 124 static typeof(this)*[] _CacheCore_pending; 125 126 static ~this() 127 { 128 foreach (p; _CacheCore_pending) 129 p.save!true(); 130 } 131 } 132 } 133 } 134 135 /// `FileCache` policy for when to (re)load data from disk. 136 enum LoadPolicy 137 { 138 automatic, /// `onModification` for `FlushPolicy.none`/`.manual`, `once` for delayed 139 once, /// On first request. 140 onModification, /// When the file's timestamp is updated. 141 } 142 143 /// Wraps some data stored in a file on disk. 144 /// Params: 145 /// DataGetter = Callable which accepts a file name and returns its contents as some D value. 146 /// DataPutter = Callable which performs the reverse operation, writing the value to the file. 147 /// flushPolicy = When to save changes to the data to disk. 148 /// loadPolicy = When to reload data from disk. 149 struct FileCache(alias DataGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none, LoadPolicy loadPolicy = LoadPolicy.automatic) 150 { 151 string fileName; /// File name containing the data. 152 153 static if (loadPolicy == LoadPolicy.automatic) 154 enum _FileCache_loadPolicy = flushPolicy.delayed() ? LoadPolicy.once : LoadPolicy.onModification; 155 else 156 enum _FileCache_loadPolicy = loadPolicy; 157 158 ReturnType!DataGetter _FileCache_dataGetter() 159 { 160 import std.file : exists; 161 assert(fileName, "Filename not set"); 162 static if (flushPolicy == FlushPolicy.none) 163 return DataGetter(fileName); // no existence checks if we are never saving it ourselves 164 else 165 if (fileName.exists) 166 return DataGetter(fileName); 167 else 168 return ReturnType!DataGetter.init; 169 } 170 171 static if (is(DataPutter == None)) 172 alias _FileCache_dataPutter = None; 173 else 174 void _FileCache_dataPutter(T)(T t) 175 { 176 assert(fileName, "Filename not set"); 177 DataPutter(fileName, t); 178 } 179 180 static if (_FileCache_loadPolicy == LoadPolicy.onModification) 181 { 182 import std.datetime : SysTime; 183 184 SysTime _FileCache_keyGetter() 185 { 186 import std.file : exists, timeLastModified; 187 188 SysTime result; 189 if (fileName.exists) 190 result = fileName.timeLastModified(); 191 return result; 192 } 193 } 194 else 195 { 196 bool _FileCache_keyGetter() { return true; } 197 } 198 199 mixin CacheCore!(_FileCache_dataGetter, _FileCache_keyGetter, _FileCache_dataPutter, flushPolicy); 200 201 alias _CacheCore_data this; 202 } 203 204 // Sleep between writes to make sure timestamps differ 205 version(unittest) import core.thread; 206 207 package 208 version (Windows) 209 enum filesystemTimestampGranularity = 10.msecs; 210 else 211 version (OSX) 212 enum filesystemTimestampGranularity = 1.seconds; 213 else 214 static if (__VERSION__ > 2_072) 215 enum filesystemTimestampGranularity = 10.msecs; 216 else 217 { 218 // https://issues.dlang.org/show_bug.cgi?id=15803 219 enum filesystemTimestampGranularity = 1.seconds; 220 } 221 222 /// 223 unittest 224 { 225 import std.file; 226 static void[] readProxy(string fn) { return std.file.read(fn); } 227 228 enum FN = "test.txt"; 229 auto cachedData = FileCache!readProxy(FN); 230 231 std.file.write(FN, "One"); 232 scope(exit) remove(FN); 233 assert(cachedData == "One"); 234 235 Thread.sleep(filesystemTimestampGranularity); 236 std.file.write(FN, "Two"); 237 assert(cachedData == "Two"); 238 auto mtime = FN.timeLastModified(); 239 240 version (OSX) {} else // setTimes does not work on macOS 10.15 ? 241 { 242 Thread.sleep(filesystemTimestampGranularity); 243 std.file.write(FN, "Three"); 244 FN.setTimes(mtime, mtime); 245 assert(cachedData == "Two"); 246 } 247 } 248 249 // https://issues.dlang.org/show_bug.cgi?id=7016 250 static import ae.sys.memory;