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;