-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathTimers.java
444 lines (395 loc) · 15.1 KB
/
Timers.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.kernel.timer;
import sirius.kernel.Sirius;
import sirius.kernel.Startable;
import sirius.kernel.Stoppable;
import sirius.kernel.async.Orchestration;
import sirius.kernel.async.Tasks;
import sirius.kernel.commons.Explain;
import sirius.kernel.commons.TimeProvider;
import sirius.kernel.commons.Watch;
import sirius.kernel.di.PartCollection;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Parts;
import sirius.kernel.di.std.Register;
import sirius.kernel.health.Exceptions;
import sirius.kernel.health.Log;
import sirius.kernel.nls.NLS;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Internal service which is responsible for executing timers.
* <p>
* Other than for statistical reasons, this class does not need to be called directly. It automatically
* discovers all parts registered for one of the timer interfaces (<tt>EveryMinute</tt>, <tt>EveryTenMinutes</tt>,
* <tt>EveryHour</tt>, <tt>EveryDay</tt>) and invokes them appropriately.
* <p>
* To access this class, a <tt>Part</tt> annotation can be used on a field of type <tt>TimerService</tt>.
*/
@Register(classes = {Timers.class, Startable.class, Stoppable.class})
public class Timers implements Startable, Stoppable {
@SuppressWarnings("squid:S1192")
@Explain("These constants are semantically different.")
protected static final Log LOG = Log.get("timer");
private static final String TIMER = "timer";
/**
* Contains the config prefix to load settings for daily tasks from.
*/
public static final String TIMER_DAILY_PREFIX = "timer.daily.";
private static final int TEN_SECONDS_IN_MILLIS = 10000;
@Part
private Tasks tasks;
@Part
private TimeProvider timeProvider;
@Part
@Nullable
private Orchestration orchestration;
@Parts(EveryTenSeconds.class)
private PartCollection<EveryTenSeconds> everyTenSeconds;
private long lastTenSecondsExecution = 0;
@Parts(EveryMinute.class)
private PartCollection<EveryMinute> everyMinute;
private long lastOneMinuteExecution = 0;
@Parts(EveryTenMinutes.class)
private PartCollection<EveryTenMinutes> everyTenMinutes;
private long lastTenMinutesExecution = 0;
@Parts(EveryHour.class)
private PartCollection<EveryHour> everyHour;
private long lastHourExecution = 0;
@Parts(EveryDay.class)
private PartCollection<EveryDay> everyDay;
private Timer timer;
private final ReentrantLock timerLock = new ReentrantLock();
/*
* Contains the relative paths of all loaded files
*/
private final List<WatchedResource> loadedFiles = new CopyOnWriteArrayList<>();
/*
* Used to frequently check loaded properties when running in DEVELOP mode.
*/
private Timer reloadTimer;
/*
* Determines the interval which files are checked for update
*/
private static final int RELOAD_INTERVAL = 1000;
/**
* Determines the start and stop order of the timer's lifecycle. Exposed as public so that
* dependent lifecycles can determine their own priority based on this.
*/
public static final int LIFECYCLE_PRIORITY = 1000;
@Override
public int getPriority() {
return LIFECYCLE_PRIORITY;
}
private class InnerTimerTask extends TimerTask {
@Override
public void run() {
try {
runTenSecondTimers();
if (TimeUnit.MINUTES.convert(timeProvider.currentTimeMillis() - lastOneMinuteExecution,
TimeUnit.MILLISECONDS) >= 1) {
runOneMinuteTimers();
}
if (TimeUnit.MINUTES.convert(timeProvider.currentTimeMillis() - lastTenMinutesExecution,
TimeUnit.MILLISECONDS) >= 10) {
runTenMinuteTimers();
}
if (TimeUnit.MINUTES.convert(timeProvider.currentTimeMillis() - lastHourExecution,
TimeUnit.MILLISECONDS) >= 60) {
runOneHourTimers();
runEveryDayTimers(timeProvider.localTimeNow().getHour());
}
} catch (Exception exception) {
Exceptions.handle(LOG, exception);
}
}
}
/*
* Used to monitor a resource for changes
*/
private static class WatchedResource {
private final File file;
private long lastModified;
private final Runnable callback;
private WatchedResource(File file, Runnable callback) {
this.file = file;
this.lastModified = file.lastModified();
this.callback = callback;
}
}
/**
* Returns the timestamp of the last execution of the 10-second timer.
*
* @return a textual representation of the last execution of the ten seconds timer. Returns "-" if the timer didn't
* run yet.
*/
public String getLastTenSecondsExecution() {
if (lastTenSecondsExecution == 0) {
return "-";
}
return NLS.toUserString(Instant.ofEpochMilli(lastTenSecondsExecution));
}
/**
* Returns the timestamp of the last execution of the one-minute timer.
*
* @return a textual representation of the last execution of the one-minute timer. Returns "-" if the timer didn't
* run yet.
*/
public String getLastOneMinuteExecution() {
if (lastOneMinuteExecution == 0) {
return "-";
}
return NLS.toUserString(Instant.ofEpochMilli(lastOneMinuteExecution));
}
/**
* Returns the timestamp of the last execution of the ten minutes timer.
*
* @return a textual representation of the last execution of the ten minutes timer. Returns "-" if the timer didn't
* run yet.
*/
public String getLastTenMinutesExecution() {
if (lastTenMinutesExecution == 0) {
return "-";
}
return NLS.toUserString(Instant.ofEpochMilli(lastTenMinutesExecution));
}
/**
* Returns the timestamp of the last execution of the one-hour timer.
*
* @return a textual representation of the last execution of the one-hour timer. Returns "-" if the timer didn't
* run yet.
*/
public String getLastHourExecution() {
if (lastHourExecution == 0) {
return "-";
}
return NLS.toUserString(Instant.ofEpochMilli(lastHourExecution));
}
@Override
public void started() {
if (Sirius.isFrameworkEnabled("kernel.timer")) {
startTimer();
}
if (Sirius.isDev()) {
startResourceWatcher();
}
}
@Override
public void stopped() {
try {
timerLock.lock();
try {
if (timer != null) {
timer.cancel();
}
} finally {
timerLock.unlock();
}
} catch (Exception exception) {
Exceptions.handle(LOG, exception);
}
}
/**
* Adds the given file to the list of watched resources in DEVELOP mode ({@link Sirius#isDev()}.
* <p>
* This is used to reload files like properties in development environments. In production systems, no
* reloading will be performed.
*
* @param url the file to watch
* @param callback the callback to invoke once the file has changed
*/
@SuppressWarnings("squid:S2250")
@Explain("Resources are only collected once at startup, so there is no performance hotspot")
public void addWatchedResource(@Nonnull URL url, @Nonnull Runnable callback) {
try {
loadedFiles.add(new WatchedResource(new File(url.toURI()), callback));
} catch (IllegalArgumentException | URISyntaxException exception) {
Exceptions.ignore(exception);
Exceptions.handle()
.withSystemErrorMessage("Cannot monitor URL '%s' for changes: %s (%s)", url)
.to(LOG)
.handle();
}
}
/**
* Executes all one minute timers (implementing <tt>EveryTenSeconds</tt>) now (out of schedule).
*/
public void runTenSecondTimers() {
for (final TimedTask task : everyTenSeconds.getParts()) {
executeTask(task);
}
lastTenSecondsExecution = timeProvider.currentTimeMillis();
}
/**
* Executes all one minute timers (implementing <tt>EveryMinute</tt>) now (out of schedule).
*/
public void runOneMinuteTimers() {
for (final TimedTask task : everyMinute.getParts()) {
executeTask(task);
}
lastOneMinuteExecution = timeProvider.currentTimeMillis();
}
/**
* Executes all ten minutes timers (implementing <tt>EveryTenMinutes</tt>) now (out of schedule).
*/
public void runTenMinuteTimers() {
for (final TimedTask task : everyTenMinutes.getParts()) {
executeTask(task);
}
lastTenMinutesExecution = timeProvider.currentTimeMillis();
}
/**
* Executes all one hour timers (implementing <tt>EveryHour</tt>) now (out of schedule).
*/
public void runOneHourTimers() {
for (final TimedTask task : everyHour.getParts()) {
executeTask(task);
}
lastHourExecution = timeProvider.currentTimeMillis();
}
/**
* Executes all daily timers (implementing <tt>EveryDay</tt>) if applicable, or if outOfASchedule is <tt>true</tt>.
*
* @param currentHour determines the current hour. Most probably this will be wall-clock time. However, for
* out-of-schedule execution, this can be set to any value.
*/
public void runEveryDayTimers(int currentHour) {
runEveryDayTimers(currentHour, false);
}
/**
* Executes all daily timers (implementing <tt>EveryDay</tt>) if applicable, or if outOfASchedule is <tt>true</tt>.
*
* @param currentHour determines the current hour. Most probably this will be wall-clock time. However, for
* out-of-schedule eecution, this can be set to any value.
* @param forced if <b>true</b>, the task will be executed even if it is not scheduled according to
* {@link Orchestration#shouldRunDailyTask(String)}
*/
public void runEveryDayTimers(int currentHour, boolean forced) {
for (final EveryDay task : getDailyTasks()) {
runDailyTimer(currentHour, task, forced);
}
}
/**
* Returns all known daily tasks.
*
* @return a collection of all known daily tasks
*/
public Collection<EveryDay> getDailyTasks() {
return Collections.unmodifiableCollection(everyDay.getParts());
}
/**
* Determines the execution hour (0..23) in which the given task is to be executed.
*
* @param task the task to check
* @return the execution hour wrapped as optional or an empty optional if the config is missing
*/
public Optional<Integer> getExecutionHour(EveryDay task) {
String configPath = TIMER_DAILY_PREFIX + task.getConfigKeyName();
if (!Sirius.getSettings().getConfig().hasPath(configPath)) {
return Optional.empty();
}
return Optional.of(Sirius.getSettings().getInt(configPath));
}
private void startResourceWatcher() {
if (reloadTimer == null) {
reloadTimer = new Timer(true);
reloadTimer.schedule(new TimerTask() {
@Override
public void run() {
watchLoadedResources();
}
}, RELOAD_INTERVAL, RELOAD_INTERVAL);
}
}
private void watchLoadedResources() {
Thread.currentThread().setName("Resource-Watch");
loadedFiles.forEach(resource -> {
long lastModified = resource.file.lastModified();
if (lastModified > resource.lastModified) {
resource.lastModified = resource.file.lastModified();
LOG.INFO("Reloading: %s", resource.file.toString());
try {
resource.callback.run();
} catch (Exception exception) {
Exceptions.handle()
.withSystemErrorMessage("Error reloading %s: %s (%s)", resource.file.toString())
.error(exception)
.handle();
}
}
});
}
private void startTimer() {
try {
timerLock.lock();
try {
if (timer != null) {
timer.cancel();
}
timer = new Timer(true);
timer.schedule(new InnerTimerTask(), TEN_SECONDS_IN_MILLIS, TEN_SECONDS_IN_MILLIS);
} finally {
timerLock.unlock();
}
} catch (Exception exception) {
Exceptions.handle(LOG, exception);
}
}
private void executeTask(final TimedTask task) {
tasks.executor(TIMER)
.dropOnOverload(() -> Exceptions.handle()
.to(LOG)
.withSystemErrorMessage(
"Dropping timer task '%s' (%s) due to system overload!",
task,
task.getClass())
.handle())
.start(() -> {
try {
Watch watch = Watch.start();
task.runTimer();
if (watch.elapsed(TimeUnit.SECONDS, false) > 1) {
LOG.WARN("TimedTask '%s' (%s) took over a second to complete! "
+ "Consider executing the work in a separate executor!", task, task.getClass());
}
} catch (Exception exception) {
Exceptions.handle(LOG, exception);
}
});
}
private void runDailyTimer(int currentHour, EveryDay task, boolean forced) {
Optional<Integer> executionHour = getExecutionHour(task);
if (executionHour.isEmpty()) {
LOG.WARN("Skipping daily timer %s as config key '%s' is missing!",
task.getClass().getName(),
TIMER_DAILY_PREFIX + task.getConfigKeyName());
return;
}
if (executionHour.get() != currentHour) {
return;
}
if (!forced && orchestration != null && !orchestration.shouldRunDailyTask(task.getConfigKeyName())) {
return;
}
executeTask(task);
}
}