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 _CacheCore_pending ~= &this; 111 } 112 113 static typeof(this)*[] _CacheCore_pending; 114 115 static ~this() 116 { 117 foreach (p; _CacheCore_pending) 118 p.save!true(); 119 } 120 } 121 } 122 } 123 124 /// FileCache policy for when to (re)load data from disk. 125 enum LoadPolicy 126 { 127 automatic, /// "onModification" for FlushPolicy.none/manual, "once" for delayed 128 once, 129 onModification, 130 } 131 132 struct FileCache(alias DataGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none, LoadPolicy loadPolicy = LoadPolicy.automatic) 133 { 134 string fileName; 135 136 static if (loadPolicy == LoadPolicy.automatic) 137 enum _FileCache_loadPolicy = flushPolicy.delayed() ? LoadPolicy.once : LoadPolicy.onModification; 138 else 139 enum _FileCache_loadPolicy = loadPolicy; 140 141 ReturnType!DataGetter _FileCache_dataGetter() 142 { 143 import std.file : exists; 144 assert(fileName, "Filename not set"); 145 static if (flushPolicy == FlushPolicy.none) 146 return DataGetter(fileName); // no existence checks if we are never saving it ourselves 147 else 148 if (fileName.exists) 149 return DataGetter(fileName); 150 else 151 return ReturnType!DataGetter.init; 152 } 153 154 static if (is(DataPutter == None)) 155 alias _FileCache_dataPutter = None; 156 else 157 void _FileCache_dataPutter(T)(T t) 158 { 159 assert(fileName, "Filename not set"); 160 DataPutter(fileName, t); 161 } 162 163 static if (_FileCache_loadPolicy == LoadPolicy.onModification) 164 { 165 import std.datetime : SysTime; 166 167 SysTime _FileCache_keyGetter() 168 { 169 import std.file : exists, timeLastModified; 170 171 SysTime result; 172 if (fileName.exists) 173 result = fileName.timeLastModified(); 174 return result; 175 } 176 } 177 else 178 { 179 bool _FileCache_keyGetter() { return true; } 180 } 181 182 mixin CacheCore!(_FileCache_dataGetter, _FileCache_keyGetter, _FileCache_dataPutter, flushPolicy); 183 184 alias _CacheCore_data this; 185 } 186 187 // Sleep between writes to make sure timestamps differ 188 version(unittest) import core.thread; 189 190 version (Windows) 191 enum filesystemTimestampGranularity = 10.msecs; 192 else 193 { 194 // https://issues.dlang.org/show_bug.cgi?id=15803 195 enum filesystemTimestampGranularity = 1.seconds; 196 } 197 198 unittest 199 { 200 import std.file; 201 static void[] readProxy(string fn) { return std.file.read(fn); } 202 203 enum FN = "test.txt"; 204 auto cachedData = FileCache!readProxy(FN); 205 206 std.file.write(FN, "One"); 207 scope(exit) remove(FN); 208 assert(cachedData == "One"); 209 210 Thread.sleep(filesystemTimestampGranularity); 211 std.file.write(FN, "Two"); 212 assert(cachedData == "Two"); 213 auto mtime = FN.timeLastModified(); 214 215 Thread.sleep(filesystemTimestampGranularity); 216 std.file.write(FN, "Three"); 217 FN.setTimes(mtime, mtime); 218 assert(cachedData == "Two"); 219 } 220 221 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 222 static import ae.sys.memory;