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