1 /** 2 * Wrappers for automatically loading/saving data. 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; 15 16 import std.traits; 17 18 enum FlushPolicy 19 { 20 none, 21 manual, 22 atScopeExit, 23 atThreadExit, 24 // TODO: immediate flushing. Could work only with values without mutable indirections. 25 // TODO: this can actually be a bitmask 26 } 27 28 bool delayed(FlushPolicy policy) { return policy > FlushPolicy.manual; } 29 30 struct None {} 31 32 /// Cache values in-memory, and automatically load/save them as needed via the specified functions. 33 /// Actual loading/saving is done via alias functions. 34 /// KeyGetter may return .init (of its return type) if the resource does not yet exist, 35 /// but once it returns non-.init it may not return .init again. 36 /// A bool key can be used to load a resource from disk only once (lazily), 37 /// as is currently done with LoadPolicy.once. 38 /// Delayed flush policies require a bool key, to avoid mid-air collisions. 39 mixin template CacheCore(alias DataGetter, alias KeyGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none) 40 { 41 import std.traits; 42 import ae.sys.memory; 43 44 alias _CacheCore_Data = ReturnType!DataGetter; 45 alias _CacheCore_Key = ReturnType!KeyGetter; 46 47 enum _CacheCore_readOnly = flushPolicy == FlushPolicy.none; 48 49 _CacheCore_Data cachedData; 50 _CacheCore_Key cachedDataKey; 51 52 void _CacheCore_update() 53 { 54 auto newKey = KeyGetter(); 55 56 // No going back to Key.init after returning non-.init 57 assert(cachedDataKey == _CacheCore_Key.init || newKey != _CacheCore_Key.init); 58 59 if (newKey != cachedDataKey) 60 { 61 static if (flushPolicy == FlushPolicy.atThreadExit) 62 { 63 if (cachedDataKey == _CacheCore_Key.init) // first load 64 _CacheCore_registerFlush(); 65 } 66 cachedData = DataGetter(); 67 cachedDataKey = newKey; 68 } 69 } 70 71 static if (_CacheCore_readOnly) 72 @property auto _CacheCore_data() { _CacheCore_update(); return cast(immutable)cachedData; } 73 else 74 @property ref auto _CacheCore_data() { _CacheCore_update(); return cachedData; } 75 76 static if (!_CacheCore_readOnly) 77 { 78 void save(bool exiting=false)() 79 { 80 if (cachedDataKey != _CacheCore_Key.init || cachedData != _CacheCore_Data.init) 81 { 82 DataPutter(cachedData); 83 static if (!exiting) 84 cachedDataKey = KeyGetter(); 85 } 86 } 87 88 static if (flushPolicy.delayed()) 89 { 90 // A bool key implies that data will be loaded only once (lazy loading). 91 static assert(is(_CacheCore_Key==bool), "Delayed flush with automatic reload allows mid-air collisions"); 92 } 93 94 static if (flushPolicy == FlushPolicy.atScopeExit) 95 { 96 ~this() 97 { 98 save!true(); 99 } 100 } 101 102 static if (flushPolicy == FlushPolicy.atThreadExit) 103 { 104 void _CacheCore_registerFlush() 105 { 106 // https://d.puremagic.com/issues/show_bug.cgi?id=12038 107 assert(!onStack(cast(void*)&this)); 108 _CacheCore_pending ~= &this; 109 } 110 111 static typeof(this)*[] _CacheCore_pending; 112 113 static ~this() 114 { 115 foreach (p; _CacheCore_pending) 116 p.save!true(); 117 } 118 } 119 } 120 } 121 122 /// FileCache policy for when to (re)load data from disk. 123 enum LoadPolicy 124 { 125 automatic, /// "onModification" for FlushPolicy.none/manual, "once" for delayed 126 once, 127 onModification, 128 } 129 130 struct FileCache(alias DataGetter, alias DataPutter = None, FlushPolicy flushPolicy = FlushPolicy.none, LoadPolicy loadPolicy = LoadPolicy.automatic) 131 { 132 string fileName; 133 134 static if (loadPolicy == LoadPolicy.automatic) 135 enum _FileCache_loadPolicy = flushPolicy.delayed() ? LoadPolicy.once : LoadPolicy.onModification; 136 else 137 enum _FileCache_loadPolicy = loadPolicy; 138 139 ReturnType!DataGetter _FileCache_dataGetter() 140 { 141 import std.file : exists; 142 assert(fileName, "Filename not set"); 143 static if (flushPolicy == FlushPolicy.none) 144 return DataGetter(fileName); // no existence checks if we are never saving it ourselves 145 else 146 if (fileName.exists) 147 return DataGetter(fileName); 148 else 149 return ReturnType!DataGetter.init; 150 } 151 152 static if (is(DataPutter == None)) 153 alias _FileCache_dataPutter = None; 154 else 155 void _FileCache_dataPutter(T)(T t) 156 { 157 assert(fileName, "Filename not set"); 158 DataPutter(fileName, t); 159 } 160 161 static if (_FileCache_loadPolicy == LoadPolicy.onModification) 162 { 163 import std.datetime : SysTime; 164 165 SysTime _FileCache_keyGetter() 166 { 167 import std.file : exists, timeLastModified; 168 169 SysTime result; 170 if (fileName.exists) 171 result = fileName.timeLastModified(); 172 return result; 173 } 174 } 175 else 176 { 177 bool _FileCache_keyGetter() { return true; } 178 } 179 180 mixin CacheCore!(_FileCache_dataGetter, _FileCache_keyGetter, _FileCache_dataPutter, flushPolicy); 181 182 alias _CacheCore_data this; 183 } 184 185 // Sleep between writes to make sure timestamps differ 186 version(unittest) import core.thread; 187 188 unittest 189 { 190 import std.file; 191 192 enum FN = "test.txt"; 193 auto cachedData = FileCache!read(FN); 194 195 std.file.write(FN, "One"); 196 scope(exit) remove(FN); 197 assert(cachedData == "One"); 198 199 Thread.sleep(10.msecs); 200 std.file.write(FN, "Two"); 201 assert(cachedData == "Two"); 202 auto mtime = FN.timeLastModified(); 203 204 Thread.sleep(10.msecs); 205 std.file.write(FN, "Three"); 206 FN.setTimes(mtime, mtime); 207 assert(cachedData == "Two"); 208 } 209 210 // **************************************************************************** 211 212 template JsonFileCache(T, FlushPolicy flushPolicy = FlushPolicy.none) 213 { 214 import std.file; 215 import ae.utils.json; 216 217 static T getJson(T)(string fileName) 218 { 219 return fileName.readText.jsonParse!T; 220 } 221 222 static void putJson(T)(string fileName, in T t) 223 { 224 std.file.write(fileName, t.toJson()); 225 } 226 227 alias JsonFileCache = FileCache!(getJson!T, putJson!T, flushPolicy); 228 } 229 230 unittest 231 { 232 import std.file; 233 234 enum FN = "test1.json"; 235 std.file.write(FN, "{}"); 236 scope(exit) remove(FN); 237 238 auto cache = JsonFileCache!(string[string])(FN); 239 assert(cache.length == 0); 240 } 241 242 unittest 243 { 244 import std.file; 245 246 enum FN = "test2.json"; 247 scope(exit) if (FN.exists) remove(FN); 248 249 auto cache = JsonFileCache!(string[string], FlushPolicy.manual)(FN); 250 assert(cache.length == 0); 251 cache["foo"] = "bar"; 252 cache.save(); 253 254 auto cache2 = JsonFileCache!(string[string])(FN); 255 assert(cache2["foo"] == "bar"); 256 } 257 258 unittest 259 { 260 import std.file; 261 262 enum FN = "test3.json"; 263 scope(exit) if (FN.exists) remove(FN); 264 265 { 266 auto cache = JsonFileCache!(string[string], FlushPolicy.atScopeExit)(FN); 267 cache["foo"] = "bar"; 268 } 269 270 auto cache2 = JsonFileCache!(string[string])(FN); 271 assert(cache2["foo"] == "bar"); 272 } 273 274 // **************************************************************************** 275 276 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 277 static import ae.utils.json; 278 279 /// std.functional.memoize variant with automatic persistence 280 struct PersistentMemoized(alias fun, FlushPolicy flushPolicy = FlushPolicy.atThreadExit) 281 { 282 import std.traits; 283 import std.typecons; 284 import ae.utils.json; 285 286 alias ReturnType!fun[string] AA; 287 private JsonFileCache!(AA, flushPolicy) memo; 288 289 this(string fileName) { memo.fileName = fileName; } 290 291 ReturnType!fun opCall(ParameterTypeTuple!fun args) 292 { 293 string key; 294 static if (args.length==1 && is(typeof(args[0]) : string)) 295 key = args[0]; 296 else 297 key = toJson(tuple(args)); 298 auto p = key in memo; 299 if (p) return *p; 300 auto r = fun(args); 301 return memo[key] = r; 302 } 303 } 304 305 unittest 306 { 307 import std.file; 308 309 static int value = 42; 310 int getValue(int x) { return value; } 311 312 enum FN = "test4.json"; 313 scope(exit) if (FN.exists) remove(FN); 314 315 { 316 auto getValueMemoized = PersistentMemoized!(getValue, FlushPolicy.atScopeExit)(FN); 317 318 assert(getValueMemoized(1) == 42); 319 value = 24; 320 assert(getValueMemoized(1) == 42); 321 assert(getValueMemoized(2) == 24); 322 } 323 324 value = 0; 325 326 { 327 auto getValueMemoized = PersistentMemoized!(getValue, FlushPolicy.atScopeExit)(FN); 328 assert(getValueMemoized(1) == 42); 329 assert(getValueMemoized(2) == 24); 330 } 331 } 332 333 // **************************************************************************** 334 335 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 336 static import ae.sys.file; 337 338 /// A string hashset, stored one line per entry. 339 struct PersistentStringSet 340 { 341 import ae.utils.aa : HashSet; 342 343 static HashSet!string load(string fileName) 344 { 345 import std.file : readText; 346 import std.string : splitLines; 347 348 return HashSet!string(fileName.readText().splitLines()); 349 } 350 351 static void save(string fileName, HashSet!string data) 352 { 353 import std.array : join; 354 import ae.sys.file : atomicWrite; 355 356 atomicWrite(fileName, data.keys.join("\n")); 357 } 358 359 alias Cache = FileCache!(load, save, FlushPolicy.manual); 360 Cache cache; 361 362 this(string fileName) { cache = Cache(fileName); } 363 364 auto opIn_r(string key) 365 { 366 return key in cache; 367 } 368 369 void add(string key) 370 { 371 assert(key !in cache); 372 cache.add(key); 373 cache.save(); 374 } 375 376 void remove(string key) 377 { 378 assert(key in cache); 379 cache.remove(key); 380 cache.save(); 381 } 382 383 @property string[] lines() { return cache.keys; } 384 @property size_t length() { return cache.length; } 385 } 386 387 unittest 388 { 389 import std.file; 390 391 enum FN = "test.txt"; 392 scope(exit) if (FN.exists) remove(FN); 393 394 { 395 auto s = PersistentStringSet(FN); 396 assert("foo" !in s); 397 assert(s.length == 0); 398 s.add("foo"); 399 } 400 { 401 auto s = PersistentStringSet(FN); 402 assert("foo" in s); 403 assert(s.length == 1); 404 s.remove("foo"); 405 } 406 { 407 auto s = PersistentStringSet(FN); 408 assert("foo" !in s); 409 std.file.write(FN, "foo\n"); 410 assert("foo" in s); 411 std.file.write(FN, "bar\n"); 412 assert(s.lines == ["bar"]); 413 } 414 }