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 		m_persistentFilePath = persistent_file;
48 
49 		if (existsFile(persistent_file)) {
50 			m_deferUpdates = true;
51 			auto data = readJsonFile(persistent_file);
52 			foreach (string name, desc; data) {
53 				auto tp = desc.kind.get!string.to!EventKind;
54 				auto next = SysTime.fromISOExtString(desc.next.get!string);
55 				final switch (tp) with (EventKind) {
56 					case singular: scheduleEvent(name, next); break;
57 					case periodic: scheduleEvent(name, next, desc.period.get!long.usecs); break;
58 					case daily: scheduleDailyEvent(name, next); break;
59 					case weekly: scheduleWeeklyEvent(name, next); break;
60 					case monthly: scheduleMonthlyEvent(name, next); break;
61 					case yearly: scheduleYearlyEvent(name, next); break;
62 				}
63 			}
64 			m_deferUpdates = false;
65 		}
66 	}
67 
68 	void scheduleEvent(string name, SysTime time) { scheduleEvent(name, EventKind.singular, time); }
69 	void scheduleEvent(string name, SysTime first_time, Duration repeat_period) { scheduleEvent(name, EventKind.periodic, first_time, repeat_period); }
70 	void scheduleDailyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.daily, first_time); }
71 	void scheduleWeeklyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.weekly, first_time); }
72 	void scheduleMonthlyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.monthly, first_time); }
73 	void scheduleYearlyEvent(string name, SysTime first_time) { scheduleEvent(name, EventKind.yearly, first_time); }
74 	
75 	void scheduleEvent(string name, EventKind kind, SysTime first_time, Duration repeat_period = 0.seconds)
76 	{
77 		auto now = Clock.currTime(UTC());
78 		if (name !in m_events) {
79 			auto timer = createTimer({ onTimerFired(name); });
80 			auto evt = Event(kind, first_time, repeat_period, timer, null);
81 			m_events[name] = evt; // direct assignment yields "Internal error: backend\cgcs.c 351"
82 		}
83 
84 		auto pevt = name in m_events;
85 
86 		pevt.kind = kind;
87 		pevt.next = first_time;
88 		pevt.period = repeat_period;
89 
90 		writePersistentFile();
91 
92 		if (pevt.handler) {
93 			if (pevt.next <= now) fireEvent(name, now);
94 			else pevt.timer.rearm(pevt.next - now);
95 		}
96 	}
97 	
98 	void deleteEvent(string name)
99 	{
100 		if (auto pevt = name in m_events) {
101 			m_events.remove(name);
102 			writePersistentFile();
103 		}
104 	}
105 
106 	bool existsEvent(string name)
107 	const {
108 		return (name in m_events) !is null;
109 	}
110 
111 	void setEventHandler(string name, void delegate() handler)
112 	{
113 		auto pevt = name in m_events;
114 		assert(pevt !is null, "Non-existent event: "~name);
115 		pevt.handler = handler;
116 		auto now = Clock.currTime(UTC());
117 		if (handler !is null) {
118 			if (pevt.next <= now) fireEvent(name, now);
119 			else pevt.timer.rearm(pevt.next - now);
120 		}
121 	}
122 
123 	private void onTimerFired(string name)
124 	{
125 		auto pevt = name in m_events;
126 		if (!pevt || !pevt.handler) return;
127 
128 		auto now = Clock.currTime(UTC());
129 
130 		if (pevt.next <= now) fireEvent(name, now);
131 		else pevt.timer.rearm(pevt.next - now);
132 	}
133 
134 	private void fireEvent(string name, SysTime now)
135 	{
136 		auto pevt = name in m_events;
137 		assert(pevt.next <= now);
138 		assert(pevt.handler !is null);
139 		auto handler = pevt.handler;
140 
141 		final switch (pevt.kind) with (EventKind) {
142 			case singular: break;
143 			case periodic:
144 				do pevt.next += pevt.period;
145 				while (pevt.next <= now);
146 				break;
147 			case daily:
148 				do pevt.next.dayOfGregorianCal = pevt.next.dayOfGregorianCal + 1;
149 				while (pevt.next <= now);
150 				break;
151 			case weekly:
152 				do pevt.next.dayOfGregorianCal = pevt.next.dayOfGregorianCal + 7;
153 				while (pevt.next <= now);
154 				break;
155 			case monthly:
156 				// FIXME: retain the original day of month after an overflow happened!
157 				do pevt.next.add!"months"(1, AllowDayOverflow.no);
158 				while (pevt.next <= now);
159 				break;
160 			case yearly:
161 				// FIXME: retain the original day of month after an overflow happened!
162 				do pevt.next.add!"years"(1, AllowDayOverflow.no);
163 				while (pevt.next <= now);
164 				break;
165 		}
166 
167 		if (pevt.kind == EventKind.singular) m_events.remove(name);
168 		else pevt.timer.rearm(pevt.next - now);
169 
170 		writePersistentFile();
171 
172 		handler();
173 	}
174 
175 	private void writePersistentFile()
176 	{
177 		if (m_deferUpdates) return;
178 
179 		Json jevents = Json.emptyObject;
180 		foreach (name, desc; m_events) {
181 			auto jdesc = Json.emptyObject;
182 			jdesc.kind = desc.kind.to!string;
183 			jdesc.next = desc.next.toISOExtString();
184 			if (desc.kind == EventKind.periodic)
185 				jdesc.period = desc.period.total!"usecs";
186 			jevents[name] = jdesc;
187 		}
188 		m_persistentFilePath.writeJsonFile(jevents);
189 	}
190 }
191 
192 private Json readJsonFile(Path path)
193 {
194 	import vibe.stream.operations;
195 
196 	auto fil = openFile(path);
197 	scope (exit) fil.close();
198 	return parseJsonString(fil.readAllUTF8());
199 }
200 
201 private void writeJsonFile(Path path, Json data)
202 {
203 	import vibe.stream.wrapper;
204 	auto fil = openFile(path, FileMode.createTrunc);
205 	scope (exit) fil.close();
206 	auto rng = StreamOutputRange(fil);
207 	auto prng = &rng;
208 	writePrettyJsonString(prng, data);
209 }