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 }