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