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 <vladimir@thecybershadow.net> 12 */ 13 14 module ae.sys.persistence.core; 15 16 import core.time; 17 18 import std.traits; 19 20 enum FlushPolicy 21 { 22 none, 23 manual, 24 atScopeExit, 25 atThreadExit, 26 // TODO: immediate flushing. Could work only with values without mutable indirections. 27 // TODO: this can actually be a bitmask 28 } 29 30 bool delayed(FlushPolicy policy) { return policy > FlushPolicy.manual; } 31 32 struct None {} 33 34 /// Cache values in-memory, and automatically load/save them as needed via the specified functions. 35 /// Actual loading/saving is done via alias functions. 36 /// KeyGetter may return .init (of its return type) if the resource does not yet exist, 37 /// but once it returns non-.init it may not return .init again. 38 /// A bool key can be used to load a resource from disk only once (lazily), 39 /// as is currently done with LoadPolicy.once. 40 /// Delayed flush policies require a bool key, to avoid mid-air collisions. 41 mixin template CacheCore(alias DataGetter, alias KeyGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none) 42 { 43 import std.traits; 44 import ae.sys.memory; 45 46 alias _CacheCore_Data = ReturnType!DataGetter; 47 alias _CacheCore_Key = ReturnType!KeyGetter; 48 49 enum _CacheCore_readOnly = flushPolicy == FlushPolicy.none; 50 51 _CacheCore_Data cachedData; 52 _CacheCore_Key cachedDataKey; 53 54 void _CacheCore_update() 55 { 56 auto newKey = KeyGetter(); 57 58 // No going back to Key.init after returning non-.init 59 assert(cachedDataKey == _CacheCore_Key.init || newKey != _CacheCore_Key.init); 60 61 if (newKey != cachedDataKey) 62 { 63 static if (flushPolicy == FlushPolicy.atThreadExit) 64 { 65 if (cachedDataKey == _CacheCore_Key.init) // first load 66 _CacheCore_registerFlush(); 67 } 68 cachedData = DataGetter(); 69 cachedDataKey = newKey; 70 } 71 } 72 73 static if (_CacheCore_readOnly) 74 @property auto _CacheCore_data() { _CacheCore_update(); return cast(immutable)cachedData; } 75 else 76 @property ref auto _CacheCore_data() { _CacheCore_update(); return cachedData; } 77 78 static if (!_CacheCore_readOnly) 79 { 80 void save(bool exiting=false)() 81 { 82 if (cachedDataKey != _CacheCore_Key.init || cachedData != _CacheCore_Data.init) 83 { 84 DataPutter(cachedData); 85 static if (!exiting) 86 cachedDataKey = KeyGetter(); 87 } 88 } 89 90 static if (flushPolicy.delayed()) 91 { 92 // A bool key implies that data will be loaded only once (lazy loading). 93 static assert(is(_CacheCore_Key==bool), "Delayed flush with automatic reload allows mid-air collisions"); 94 } 95 96 static if (flushPolicy == FlushPolicy.atScopeExit) 97 { 98 ~this() 99 { 100 save!true(); 101 } 102 } 103 104 static if (flushPolicy == FlushPolicy.atThreadExit) 105 { 106 void _CacheCore_registerFlush() 107 { 108 // https://d.puremagic.com/issues/show_bug.cgi?id=12038 109 assert(!onStack(cast(void*)&this)); 110 auto pthis = &this; // Silence "copying &this into allocated memory escapes a reference to parameter variable this" 111 _CacheCore_pending ~= pthis; 112 } 113 114 static typeof(this)*[] _CacheCore_pending; 115 116 static ~this() 117 { 118 foreach (p; _CacheCore_pending) 119 p.save!true(); 120 } 121 } 122 } 123 } 124 125 /// FileCache policy for when to (re)load data from disk. 126 enum LoadPolicy 127 { 128 automatic, /// "onModification" for FlushPolicy.none/manual, "once" for delayed 129 once, 130 onModification, 131 } 132 133 struct FileCache(alias DataGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none, LoadPolicy loadPolicy = LoadPolicy.automatic) 134 { 135 string fileName; 136 137 static if (loadPolicy == LoadPolicy.automatic) 138 enum _FileCache_loadPolicy = flushPolicy.delayed() ? LoadPolicy.once : LoadPolicy.onModification; 139 else 140 enum _FileCache_loadPolicy = loadPolicy; 141 142 ReturnType!DataGetter _FileCache_dataGetter() 143 { 144 import std.file : exists; 145 assert(fileName, "Filename not set"); 146 static if (flushPolicy == FlushPolicy.none) 147 return DataGetter(fileName); // no existence checks if we are never saving it ourselves 148 else 149 if (fileName.exists) 150 return DataGetter(fileName); 151 else 152 return ReturnType!DataGetter.init; 153 } 154 155 static if (is(DataPutter == None)) 156 alias _FileCache_dataPutter = None; 157 else 158 void _FileCache_dataPutter(T)(T t) 159 { 160 assert(fileName, "Filename not set"); 161 DataPutter(fileName, t); 162 } 163 164 static if (_FileCache_loadPolicy == LoadPolicy.onModification) 165 { 166 import std.datetime : SysTime; 167 168 SysTime _FileCache_keyGetter() 169 { 170 import std.file : exists, timeLastModified; 171 172 SysTime result; 173 if (fileName.exists) 174 result = fileName.timeLastModified(); 175 return result; 176 } 177 } 178 else 179 { 180 bool _FileCache_keyGetter() { return true; } 181 } 182 183 mixin CacheCore!(_FileCache_dataGetter, _FileCache_keyGetter, _FileCache_dataPutter, flushPolicy); 184 185 alias _CacheCore_data this; 186 } 187 188 // Sleep between writes to make sure timestamps differ 189 version(unittest) import core.thread; 190 191 version (Windows) 192 enum filesystemTimestampGranularity = 10.msecs; 193 else 194 { 195 // https://issues.dlang.org/show_bug.cgi?id=15803 196 enum filesystemTimestampGranularity = 1.seconds; 197 } 198 199 unittest 200 { 201 import std.file; 202 static void[] readProxy(string fn) { return std.file.read(fn); } 203 204 enum FN = "test.txt"; 205 auto cachedData = FileCache!readProxy(FN); 206 207 std.file.write(FN, "One"); 208 scope(exit) remove(FN); 209 assert(cachedData == "One"); 210 211 Thread.sleep(filesystemTimestampGranularity); 212 std.file.write(FN, "Two"); 213 assert(cachedData == "Two"); 214 auto mtime = FN.timeLastModified(); 215 216 Thread.sleep(filesystemTimestampGranularity); 217 std.file.write(FN, "Three"); 218 FN.setTimes(mtime, mtime); 219 assert(cachedData == "Two"); 220 } 221 222 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 223 static import ae.sys.memory;