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 }