1 /**
2 	Copyright: © 2014 rejectedsoftware e.K.
3 	License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file.
4 	Authors: Sönke Ludwig
5 */
6 module dubregistry.scheduler;
7 
8 import std.datetime;
9 import vibe.core.core;
10 import vibe.core.file;
11 import vibe.core.path;
12 import vibe.data.json;
13 
14 
15 /** Persistent event scheduler for low-frequency events.
16 
17 	Any outstanding events that should have fired during a down-time will be
18 	triggered once the corresponding handler has been registered. Repeated events
19 	will only be fired once, though. So after a down-time of three days, a daily event
20 	will only be triggered a single time instead of three times.
21 */
22 class PersistentScheduler {
23 @safe:
24 
25 	enum EventKind {
26 		singular,
27 		periodic,
28 		daily,
29 		weekly,
30 		monthly,
31 		yearly
32 	}
33 
34 	struct Event {
35 		EventKind kind;
36 		SysTime next;
37 		Duration period;
38 		Timer timer;
39 		void delegate() @safe nothrow handler;
40 	}
41 
42 	private {
43 		NativePath m_persistentFilePath;
44 		Event[string] m_events;
45 		bool m_deferUpdates;
46 	}
47 
48 	this(NativePath persistent_file)
49 	{
50 		import std.conv : to;
51 
52 		m_persistentFilePath = persistent_file;
53 
54 		if (existsFile(persistent_file)) {
55 			m_deferUpdates = true;
56 			auto data = readJsonFile(persistent_file);
57 			foreach (string name, desc; data.byKeyValue) {
58 				auto tp = desc["kind"].get!string.to!EventKind;
59 				auto next = SysTime.fromISOExtString(desc["next"].get!string);
60 				final switch (tp) with (EventKind) {
61 					case singular: scheduleEvent(name, next); break;
62 					case periodic: scheduleEvent(name, next, desc["period"].get!long.usecs); break;
63 					case daily: scheduleDailyEvent(name, next); break;
64 					case weekly: scheduleWeeklyEvent(name, next); break;
65 					case monthly: scheduleMonthlyEvent(name, next); break;
66 					case yearly: scheduleYearlyEvent(name, next); break;
67 				}
68 			}
69 			m_deferUpdates = false;
70 		}
71 	}
72 
73 	void scheduleEvent(string name, SysTime time) { scheduleEvent(name, EventKind.singular, time); }
74 	void scheduleEvent(string name, SysTime first_time, Duration repeat_period) { scheduleEvent(name, EventKind.periodic, first_time, repeat_period); }
75 	void scheduleDailyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.daily, first_time); }
76 	void scheduleWeeklyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.weekly, first_time); }
77 	void scheduleMonthlyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.monthly, first_time); }
78 	void scheduleYearlyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.yearly, first_time); }
79 
80 	void scheduleEvent(string name, EventKind kind, SysTime first_time, Duration repeat_period = 0.seconds)
81 	{
82 		auto now = Clock.currTime(UTC());
83 		if (name !in m_events) {
84 			auto timer = createTimer({ onTimerFired(name); });
85 			auto evt = Event(kind, first_time, repeat_period, timer, null);
86 			m_events[name] = evt; // direct assignment yields "Internal error: backend\cgcs.c 351"
87 		}
88 
89 		auto pevt = name in m_events;
90 
91 		pevt.kind = kind;
92 		pevt.next = first_time;
93 		pevt.period = repeat_period;
94 
95 		writePersistentFile();
96 
97 		if (pevt.handler) {
98 			if (pevt.next <= now) fireEvent(name, now);
99 			else pevt.timer.rearm(pevt.next - now);
100 		}
101 	}
102 
103 	void deleteEvent(string name)
104 	{
105 		if (auto pevt = name in m_events) {
106 			m_events.remove(name);
107 			writePersistentFile();
108 		}
109 	}
110 
111 	bool existsEvent(string name)
112 	const {
113 		return (name in m_events) !is null;
114 	}
115 
116 	void setEventHandler(string name, void delegate() @safe nothrow handler)
117 	{
118 		auto pevt = name in m_events;
119 		assert(pevt !is null, "Non-existent event: "~name);
120 		pevt.handler = handler;
121 		auto now = Clock.currTime(UTC());
122 		if (handler !is null) {
123 			if (pevt.next <= now) fireEvent(name, now);
124 			else pevt.timer.rearm(pevt.next - now);
125 		}
126 	}
127 
128 	private void onTimerFired(string name)
129 	nothrow {
130 		auto pevt = name in m_events;
131 		if (!pevt || !pevt.handler) return;
132 
133 		SysTime now;
134 		try now = Clock.currTime(UTC());
135 		catch (Exception e) assert(false, "Failed to get current time: "~e.msg);
136 
137 		if (pevt.next <= now) fireEvent(name, now);
138 		else {
139 			scope (failure) assert(false);
140 			pevt.timer.rearm(pevt.next - now);
141 		}
142 	}
143 
144 	private void fireEvent(string name, SysTime now)
145 	nothrow {
146 		auto pevt = name in m_events;
147 		assert(pevt.next <= now);
148 		assert(pevt.handler !is null);
149 		auto handler = pevt.handler;
150 
151 		final switch (pevt.kind) with (EventKind) {
152 			case singular: break;
153 			case periodic:
154 				do pevt.next += pevt.period;
155 				while (pevt.next <= now);
156 				break;
157 			case daily:
158 				do pevt.next.dayOfGregorianCal = pevt.next.dayOfGregorianCal + 1;
159 				while (pevt.next <= now);
160 				break;
161 			case weekly:
162 				do pevt.next.dayOfGregorianCal = pevt.next.dayOfGregorianCal + 7;
163 				while (pevt.next <= now);
164 				break;
165 			case monthly:
166 				// FIXME: retain the original day of month after an overflow happened!
167 				do pevt.next.add!"months"(1, AllowDayOverflow.no);
168 				while (pevt.next <= now);
169 				break;
170 			case yearly:
171 				// FIXME: retain the original day of month after an overflow happened!
172 				do pevt.next.add!"years"(1, AllowDayOverflow.no);
173 				while (pevt.next <= now);
174 				break;
175 		}
176 
177 		if (pevt.kind == EventKind.singular) m_events.remove(name);
178 		else {
179 			scope (failure) assert(false);
180 			pevt.timer.rearm(pevt.next - now);
181 		}
182 
183 		try writePersistentFile();
184 		catch (Exception e) {
185 			import vibe.core.log : logError;
186 			logError("Failed to write persistent scheduling file: %s", e.msg);
187 		}
188 
189 		handler();
190 	}
191 
192 	private void writePersistentFile()
193 	{
194 		import std.conv : to;
195 
196 		if (m_deferUpdates) return;
197 
198 		Json jevents = Json.emptyObject;
199 		foreach (name, desc; m_events) {
200 			auto jdesc = Json.emptyObject;
201 			jdesc["kind"] = desc.kind.to!string;
202 			jdesc["next"] = desc.next.toISOExtString();
203 			if (desc.kind == EventKind.periodic)
204 				jdesc["period"] = desc.period.total!"usecs";
205 			jevents[name] = jdesc;
206 		}
207 		m_persistentFilePath.writeJsonFile(jevents);
208 	}
209 }
210 
211 private Json readJsonFile(NativePath path)
212 @safe {
213 	import vibe.stream.operations;
214 
215 	auto fil = openFile(path);
216 	scope (exit) fil.close();
217 	return parseJsonString(fil.readAllUTF8());
218 }
219 
220 private void writeJsonFile(NativePath path, Json data)
221 @safe {
222 	import vibe.stream.wrapper;
223 	auto fil = openFile(path, FileMode.createTrunc);
224 	scope (exit) fil.close();
225 	auto rng = streamOutputRange(fil);
226 	auto prng = () @trusted { return &rng; } ();
227 	writePrettyJsonString(prng, data);
228 }