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 }