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;