Line data Source code
1 : // Copyright (C) 2014 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.config;
16 :
17 : import static java.time.ZoneId.systemDefault;
18 : import static java.util.Objects.requireNonNull;
19 :
20 : import com.google.auto.value.AutoValue;
21 : import com.google.auto.value.extension.memoized.Memoized;
22 : import com.google.common.annotations.VisibleForTesting;
23 : import com.google.common.flogger.FluentLogger;
24 : import com.google.gerrit.common.Nullable;
25 : import java.time.DayOfWeek;
26 : import java.time.Duration;
27 : import java.time.LocalTime;
28 : import java.time.ZonedDateTime;
29 : import java.time.format.DateTimeFormatter;
30 : import java.time.format.DateTimeParseException;
31 : import java.time.temporal.ChronoUnit;
32 : import java.util.Locale;
33 : import java.util.Optional;
34 : import java.util.concurrent.TimeUnit;
35 : import org.eclipse.jgit.lib.Config;
36 :
37 : /**
38 : * This class reads a schedule for running a periodic background job from a Git config.
39 : *
40 : * <p>A schedule configuration consists of two parameters:
41 : *
42 : * <ul>
43 : * <li>{@code interval}: Interval for running the periodic background job. The interval must be
44 : * larger than zero. The following suffixes are supported to define the time unit for the
45 : * interval:
46 : * <ul>
47 : * <li>{@code s}, {@code sec}, {@code second}, {@code seconds}
48 : * <li>{@code m}, {@code min}, {@code minute}, {@code minutes}
49 : * <li>{@code h}, {@code hr}, {@code hour}, {@code hours}
50 : * <li>{@code d}, {@code day}, {@code days}
51 : * <li>{@code w}, {@code week}, {@code weeks} ({@code 1 week} is treated as {@code 7 days})
52 : * <li>{@code mon}, {@code month}, {@code months} ({@code 1 month} is treated as {@code 30
53 : * days})
54 : * <li>{@code y}, {@code year}, {@code years} ({@code 1 year} is treated as {@code 365
55 : * days})
56 : * </ul>
57 : * <li>{@code startTime}: The start time defines the first execution of the periodic background
58 : * job. If the configured {@code interval} is shorter than {@code startTime - now} the start
59 : * time will be preponed by the maximum integral multiple of {@code interval} so that the
60 : * start time is still in the future. {@code startTime} must have one of the following
61 : * formats:
62 : * <ul>
63 : * <li>{@code <day of week> <hours>:<minutes>}
64 : * <li>{@code <hours>:<minutes>}
65 : * </ul>
66 : * The placeholders can have the following values:
67 : * <ul>
68 : * <li>{@code <day of week>}: {@code Mon}, {@code Tue}, {@code Wed}, {@code Thu}, {@code
69 : * Fri}, {@code Sat}, {@code Sun}
70 : * <li>{@code <hours>}: {@code 00}-{@code 23}
71 : * <li>{@code <minutes>}: {@code 00}-{@code 59}
72 : * </ul>
73 : * The timezone cannot be specified but is always the system default time-zone.
74 : * </ul>
75 : *
76 : * <p>The section and the subsection from which the {@code interval} and {@code startTime}
77 : * parameters are read can be configured.
78 : *
79 : * <p>Examples for a schedule configuration:
80 : *
81 : * <ul>
82 : * <li>
83 : * <pre>
84 : * foo.startTime = Fri 10:30
85 : * foo.interval = 2 day
86 : * </pre>
87 : * Assuming that the server is started on {@code Mon 7:00} then {@code startTime - now} is
88 : * {@code 4 days 3:30 hours}. This is larger than the interval hence the start time is
89 : * preponed by the maximum integral multiple of the interval so that start time is still in
90 : * the future, i.e. preponed by 4 days. This yields a start time of {@code Mon 10:30}, next
91 : * executions are {@code Wed 10:30}, {@code Fri 10:30}. etc.
92 : * <li>
93 : * <pre>
94 : * foo.startTime = 06:00
95 : * foo.interval = 1 day
96 : * </pre>
97 : * Assuming that the server is started on {@code Mon 7:00} then this yields the first run on
98 : * next Tuesday at 6:00 and a repetition interval of 1 day.
99 : * </ul>
100 : */
101 : @AutoValue
102 139 : public abstract class ScheduleConfig {
103 139 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
104 :
105 : @VisibleForTesting static final String KEY_INTERVAL = "interval";
106 : @VisibleForTesting static final String KEY_STARTTIME = "startTime";
107 :
108 : private static final long MISSING_CONFIG = -1L;
109 : private static final long INVALID_CONFIG = -2L;
110 :
111 : public static Optional<Schedule> createSchedule(Config config, String section) {
112 138 : return builder(config, section).buildSchedule();
113 : }
114 :
115 : public static ScheduleConfig.Builder builder(Config config, String section) {
116 139 : return new AutoValue_ScheduleConfig.Builder()
117 139 : .setNow(computeNow())
118 139 : .setKeyInterval(KEY_INTERVAL)
119 139 : .setKeyStartTime(KEY_STARTTIME)
120 139 : .setConfig(config)
121 139 : .setSection(section);
122 : }
123 :
124 : abstract Config config();
125 :
126 : abstract String section();
127 :
128 : @Nullable
129 : abstract String subsection();
130 :
131 : abstract String keyInterval();
132 :
133 : abstract String keyStartTime();
134 :
135 : abstract ZonedDateTime now();
136 :
137 : @Memoized
138 : public Optional<Schedule> schedule() {
139 139 : long interval = computeInterval(config(), section(), subsection(), keyInterval());
140 :
141 : long initialDelay;
142 139 : if (interval > 0) {
143 1 : initialDelay =
144 1 : computeInitialDelay(config(), section(), subsection(), keyStartTime(), now(), interval);
145 : } else {
146 139 : initialDelay = interval;
147 : }
148 :
149 139 : if (isInvalidOrMissing(interval, initialDelay)) {
150 139 : return Optional.empty();
151 : }
152 :
153 1 : return Optional.of(Schedule.create(interval, initialDelay));
154 : }
155 :
156 : private boolean isInvalidOrMissing(long interval, long initialDelay) {
157 139 : String key = section() + (subsection() != null ? "." + subsection() : "");
158 139 : if (interval == MISSING_CONFIG && initialDelay == MISSING_CONFIG) {
159 139 : logger.atInfo().log("No schedule configuration for \"%s\".", key);
160 139 : return true;
161 : }
162 :
163 1 : if (interval == MISSING_CONFIG) {
164 0 : logger.atSevere().log(
165 : "Incomplete schedule configuration for \"%s\" is ignored. Missing value for \"%s\".",
166 0 : key, key + "." + keyInterval());
167 0 : return true;
168 : }
169 :
170 1 : if (initialDelay == MISSING_CONFIG) {
171 1 : logger.atSevere().log(
172 : "Incomplete schedule configuration for \"%s\" is ignored. Missing value for \"%s\".",
173 1 : key, key + "." + keyStartTime());
174 1 : return true;
175 : }
176 :
177 1 : if (interval != INVALID_CONFIG && interval <= 0) {
178 1 : logger.atSevere().log("Invalid interval value \"%d\" for \"%s\": must be > 0", interval, key);
179 1 : interval = INVALID_CONFIG;
180 : }
181 :
182 1 : if (initialDelay != INVALID_CONFIG && initialDelay < 0) {
183 0 : logger.atSevere().log(
184 : "Invalid initial delay value \"%d\" for \"%s\": must be >= 0", initialDelay, key);
185 0 : initialDelay = INVALID_CONFIG;
186 : }
187 :
188 1 : if (interval == INVALID_CONFIG || initialDelay == INVALID_CONFIG) {
189 1 : logger.atSevere().log("Invalid schedule configuration for \"%s\" is ignored. ", key);
190 1 : return true;
191 : }
192 :
193 1 : return false;
194 : }
195 :
196 : @Override
197 : public final String toString() {
198 0 : StringBuilder b = new StringBuilder();
199 0 : b.append(formatValue(keyInterval()));
200 0 : b.append(", ");
201 0 : b.append(formatValue(keyStartTime()));
202 0 : return b.toString();
203 : }
204 :
205 : private String formatValue(String key) {
206 0 : StringBuilder b = new StringBuilder();
207 0 : b.append(section());
208 0 : if (subsection() != null) {
209 0 : b.append(".");
210 0 : b.append(subsection());
211 : }
212 0 : b.append(".");
213 0 : b.append(key);
214 0 : String value = config().getString(section(), subsection(), key);
215 0 : if (value != null) {
216 0 : b.append(" = ");
217 0 : b.append(value);
218 : } else {
219 0 : b.append(": NA");
220 : }
221 0 : return b.toString();
222 : }
223 :
224 : private static long computeInterval(
225 : Config rc, String section, String subsection, String keyInterval) {
226 : try {
227 139 : return ConfigUtil.getTimeUnit(
228 : rc, section, subsection, keyInterval, MISSING_CONFIG, TimeUnit.MILLISECONDS);
229 1 : } catch (IllegalArgumentException e) {
230 : // We only need to log the exception message; it already includes the
231 : // section.subsection.key and bad value.
232 1 : logger.atSevere().log("%s", e.getMessage());
233 1 : return INVALID_CONFIG;
234 : }
235 : }
236 :
237 : private static long computeInitialDelay(
238 : Config rc,
239 : String section,
240 : String subsection,
241 : String keyStartTime,
242 : ZonedDateTime now,
243 : long interval) {
244 1 : String start = rc.getString(section, subsection, keyStartTime);
245 1 : if (start == null) {
246 1 : return MISSING_CONFIG;
247 : }
248 1 : return computeInitialDelay(interval, start, now);
249 : }
250 :
251 : private static long computeInitialDelay(long interval, String start) {
252 5 : return computeInitialDelay(interval, start, computeNow());
253 : }
254 :
255 : private static long computeInitialDelay(long interval, String start, ZonedDateTime now) {
256 5 : requireNonNull(start);
257 :
258 : try {
259 5 : DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
260 5 : LocalTime firstStartTime = LocalTime.parse(start, formatter);
261 5 : ZonedDateTime startTime = now.with(firstStartTime);
262 : try {
263 1 : DayOfWeek dayOfWeek = formatter.parse(start, DayOfWeek::from);
264 1 : startTime = startTime.with(dayOfWeek);
265 5 : } catch (DateTimeParseException ignored) {
266 : // Day of week is an optional parameter.
267 1 : }
268 5 : startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
269 5 : long delay = Duration.between(now, startTime).toMillis() % interval;
270 5 : if (delay <= 0) {
271 5 : delay += interval;
272 : }
273 5 : return delay;
274 1 : } catch (DateTimeParseException e) {
275 1 : logger.atSevere().log("Invalid start time: %s", e.getMessage());
276 1 : return INVALID_CONFIG;
277 : }
278 : }
279 :
280 : private static ZonedDateTime computeNow() {
281 139 : return ZonedDateTime.now(systemDefault());
282 : }
283 :
284 : @AutoValue.Builder
285 139 : public abstract static class Builder {
286 : public abstract Builder setConfig(Config config);
287 :
288 : public abstract Builder setSection(String section);
289 :
290 : public abstract Builder setSubsection(@Nullable String subsection);
291 :
292 : public abstract Builder setKeyInterval(String keyInterval);
293 :
294 : public abstract Builder setKeyStartTime(String keyStartTime);
295 :
296 : @VisibleForTesting
297 : abstract Builder setNow(ZonedDateTime now);
298 :
299 : abstract ScheduleConfig build();
300 :
301 : public Optional<Schedule> buildSchedule() {
302 139 : return build().schedule();
303 : }
304 : }
305 :
306 : @AutoValue
307 5 : public abstract static class Schedule {
308 : /** Number of milliseconds between events. */
309 : public abstract long interval();
310 :
311 : /**
312 : * Milliseconds between constructor invocation and first event time.
313 : *
314 : * <p>If there is any lag between the constructor invocation and queuing the object into an
315 : * executor the event will run later, as there is no method to adjust for the scheduling delay.
316 : */
317 : public abstract long initialDelay();
318 :
319 : /**
320 : * Creates a schedule.
321 : *
322 : * <p>{@link ScheduleConfig} defines details about which values are valid for the {@code
323 : * interval} and {@code startTime} parameters.
324 : *
325 : * @param interval the interval in milliseconds
326 : * @param startTime the start time as "{@code <day of week> <hours>:<minutes>}" or "{@code
327 : * <hours>:<minutes>}"
328 : * @return the schedule
329 : * @throws IllegalArgumentException if any of the parameters is invalid
330 : */
331 : public static Schedule createOrFail(long interval, String startTime) {
332 4 : return create(interval, startTime).orElseThrow(IllegalArgumentException::new);
333 : }
334 :
335 : /**
336 : * Creates a schedule.
337 : *
338 : * <p>{@link ScheduleConfig} defines details about which values are valid for the {@code
339 : * interval} and {@code startTime} parameters.
340 : *
341 : * @param interval the interval in milliseconds
342 : * @param startTime the start time as "{@code <day of week> <hours>:<minutes>}" or "{@code
343 : * <hours>:<minutes>}"
344 : * @return the schedule or {@link Optional#empty()} if any of the parameters is invalid
345 : */
346 : public static Optional<Schedule> create(long interval, String startTime) {
347 5 : long initialDelay = computeInitialDelay(interval, startTime);
348 5 : if (interval <= 0 || initialDelay < 0) {
349 1 : return Optional.empty();
350 : }
351 4 : return Optional.of(create(interval, initialDelay));
352 : }
353 :
354 : static Schedule create(long interval, long initialDelay) {
355 5 : return new AutoValue_ScheduleConfig_Schedule(interval, initialDelay);
356 : }
357 : }
358 : }
|