summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/config/ScheduleConfig.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/config/ScheduleConfig.java')
-rw-r--r--java/com/google/gerrit/server/config/ScheduleConfig.java343
1 files changed, 343 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/config/ScheduleConfig.java b/java/com/google/gerrit/server/config/ScheduleConfig.java
new file mode 100644
index 0000000000..c5f53b3659
--- /dev/null
+++ b/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -0,0 +1,343 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static java.time.ZoneId.systemDefault;
+import static java.util.Objects.requireNonNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import java.time.DayOfWeek;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * This class reads a schedule for running a periodic background job from a Git config.
+ *
+ * <p>A schedule configuration consists of two parameters:
+ *
+ * <ul>
+ * <li>{@code interval}: Interval for running the periodic background job. The interval must be
+ * larger than zero. The following suffixes are supported to define the time unit for the
+ * interval:
+ * <ul>
+ * <li>{@code s}, {@code sec}, {@code second}, {@code seconds}
+ * <li>{@code m}, {@code min}, {@code minute}, {@code minutes}
+ * <li>{@code h}, {@code hr}, {@code hour}, {@code hours}
+ * <li>{@code d}, {@code day}, {@code days}
+ * <li>{@code w}, {@code week}, {@code weeks} ({@code 1 week} is treated as {@code 7 days})
+ * <li>{@code mon}, {@code month}, {@code months} ({@code 1 month} is treated as {@code 30
+ * days})
+ * <li>{@code y}, {@code year}, {@code years} ({@code 1 year} is treated as {@code 365
+ * days})
+ * </ul>
+ * <li>{@code startTime}: The start time defines the first execution of the periodic background
+ * job. If the configured {@code interval} is shorter than {@code startTime - now} the start
+ * time will be preponed by the maximum integral multiple of {@code interval} so that the
+ * start time is still in the future. {@code startTime} must have one of the following
+ * formats:
+ * <ul>
+ * <li>{@code <day of week> <hours>:<minutes>}
+ * <li>{@code <hours>:<minutes>}
+ * </ul>
+ * The placeholders can have the following values:
+ * <ul>
+ * <li>{@code <day of week>}: {@code Mon}, {@code Tue}, {@code Wed}, {@code Thu}, {@code
+ * Fri}, {@code Sat}, {@code Sun}
+ * <li>{@code <hours>}: {@code 00}-{@code 23}
+ * <li>{@code <minutes>}: {@code 00}-{@code 59}
+ * </ul>
+ * The timezone cannot be specified but is always the system default time-zone.
+ * </ul>
+ *
+ * <p>The section and the subsection from which the {@code interval} and {@code startTime}
+ * parameters are read can be configured.
+ *
+ * <p>Examples for a schedule configuration:
+ *
+ * <ul>
+ * <li>
+ * <pre>
+ * foo.startTime = Fri 10:30
+ * foo.interval = 2 day
+ * </pre>
+ * Assuming that the server is started on {@code Mon 7:00} then {@code startTime - now} is
+ * {@code 4 days 3:30 hours}. This is larger than the interval hence the start time is
+ * preponed by the maximum integral multiple of the interval so that start time is still in
+ * the future, i.e. preponed by 4 days. This yields a start time of {@code Mon 10:30}, next
+ * executions are {@code Wed 10:30}, {@code Fri 10:30}. etc.
+ * <li>
+ * <pre>
+ * foo.startTime = 6:00
+ * foo.interval = 1 day
+ * </pre>
+ * Assuming that the server is started on {@code Mon 7:00} then this yields the first run on
+ * next Tuesday at 6:00 and a repetition interval of 1 day.
+ * </ul>
+ */
+@AutoValue
+public abstract class ScheduleConfig {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ @VisibleForTesting static final String KEY_INTERVAL = "interval";
+ @VisibleForTesting static final String KEY_STARTTIME = "startTime";
+
+ private static final long MISSING_CONFIG = -1L;
+ private static final long INVALID_CONFIG = -2L;
+
+ public static Optional<Schedule> createSchedule(Config config, String section) {
+ return builder(config, section).buildSchedule();
+ }
+
+ public static ScheduleConfig.Builder builder(Config config, String section) {
+ return new AutoValue_ScheduleConfig.Builder()
+ .setNow(computeNow())
+ .setKeyInterval(KEY_INTERVAL)
+ .setKeyStartTime(KEY_STARTTIME)
+ .setConfig(config)
+ .setSection(section);
+ }
+
+ abstract Config config();
+
+ abstract String section();
+
+ @Nullable
+ abstract String subsection();
+
+ abstract String keyInterval();
+
+ abstract String keyStartTime();
+
+ abstract ZonedDateTime now();
+
+ @Memoized
+ public Optional<Schedule> schedule() {
+ long interval = computeInterval(config(), section(), subsection(), keyInterval());
+
+ long initialDelay;
+ if (interval > 0) {
+ initialDelay =
+ computeInitialDelay(config(), section(), subsection(), keyStartTime(), now(), interval);
+ } else {
+ initialDelay = interval;
+ }
+
+ if (isInvalidOrMissing(interval, initialDelay)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(Schedule.create(interval, initialDelay));
+ }
+
+ private boolean isInvalidOrMissing(long interval, long initialDelay) {
+ String key = section() + (subsection() != null ? "." + subsection() : "");
+ if (interval == MISSING_CONFIG && initialDelay == MISSING_CONFIG) {
+ logger.atInfo().log("No schedule configuration for \"%s\".", key);
+ return true;
+ }
+
+ if (interval == MISSING_CONFIG) {
+ logger.atSevere().log(
+ "Incomplete schedule configuration for \"%s\" is ignored. Missing value for \"%s\".",
+ key, key + "." + keyInterval());
+ return true;
+ }
+
+ if (initialDelay == MISSING_CONFIG) {
+ logger.atSevere().log(
+ "Incomplete schedule configuration for \"%s\" is ignored. Missing value for \"%s\".",
+ key, key + "." + keyStartTime());
+ return true;
+ }
+
+ if (interval <= 0 || initialDelay < 0) {
+ logger.atSevere().log("Invalid schedule configuration for \"%s\" is ignored. ", key);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+ b.append(formatValue(keyInterval()));
+ b.append(", ");
+ b.append(formatValue(keyStartTime()));
+ return b.toString();
+ }
+
+ private String formatValue(String key) {
+ StringBuilder b = new StringBuilder();
+ b.append(section());
+ if (subsection() != null) {
+ b.append(".");
+ b.append(subsection());
+ }
+ b.append(".");
+ b.append(key);
+ String value = config().getString(section(), subsection(), key);
+ if (value != null) {
+ b.append(" = ");
+ b.append(value);
+ } else {
+ b.append(": NA");
+ }
+ return b.toString();
+ }
+
+ private static long computeInterval(
+ Config rc, String section, String subsection, String keyInterval) {
+ try {
+ return ConfigUtil.getTimeUnit(
+ rc, section, subsection, keyInterval, MISSING_CONFIG, TimeUnit.MILLISECONDS);
+ } catch (IllegalArgumentException e) {
+ return INVALID_CONFIG;
+ }
+ }
+
+ private static long computeInitialDelay(
+ Config rc,
+ String section,
+ String subsection,
+ String keyStartTime,
+ ZonedDateTime now,
+ long interval) {
+ String start = rc.getString(section, subsection, keyStartTime);
+ if (start == null) {
+ return MISSING_CONFIG;
+ }
+ return computeInitialDelay(interval, start, now);
+ }
+
+ private static long computeInitialDelay(long interval, String start) {
+ return computeInitialDelay(interval, start, computeNow());
+ }
+
+ private static long computeInitialDelay(long interval, String start, ZonedDateTime now) {
+ requireNonNull(start);
+
+ try {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
+ LocalTime firstStartTime = LocalTime.parse(start, formatter);
+ ZonedDateTime startTime = now.with(firstStartTime);
+ try {
+ DayOfWeek dayOfWeek = formatter.parse(start, DayOfWeek::from);
+ startTime = startTime.with(dayOfWeek);
+ } catch (DateTimeParseException ignored) {
+ // Day of week is an optional parameter.
+ }
+ startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
+ long delay = Duration.between(now, startTime).toMillis() % interval;
+ if (delay <= 0) {
+ delay += interval;
+ }
+ return delay;
+ } catch (DateTimeParseException e) {
+ return INVALID_CONFIG;
+ }
+ }
+
+ private static ZonedDateTime computeNow() {
+ return ZonedDateTime.now(systemDefault());
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setConfig(Config config);
+
+ public abstract Builder setSection(String section);
+
+ public abstract Builder setSubsection(@Nullable String subsection);
+
+ public abstract Builder setKeyInterval(String keyInterval);
+
+ public abstract Builder setKeyStartTime(String keyStartTime);
+
+ @VisibleForTesting
+ abstract Builder setNow(ZonedDateTime now);
+
+ abstract ScheduleConfig build();
+
+ public Optional<Schedule> buildSchedule() {
+ return build().schedule();
+ }
+ }
+
+ @AutoValue
+ public abstract static class Schedule {
+ /** Number of milliseconds between events. */
+ public abstract long interval();
+
+ /**
+ * Milliseconds between constructor invocation and first event time.
+ *
+ * <p>If there is any lag between the constructor invocation and queuing the object into an
+ * executor the event will run later, as there is no method to adjust for the scheduling delay.
+ */
+ public abstract long initialDelay();
+
+ /**
+ * Creates a schedule.
+ *
+ * <p>{@link ScheduleConfig} defines details about which values are valid for the {@code
+ * interval} and {@code startTime} parameters.
+ *
+ * @param interval the interval in milliseconds
+ * @param startTime the start time as "{@code <day of week> <hours>:<minutes>}" or "{@code
+ * <hours>:<minutes>}"
+ * @return the schedule
+ * @throws IllegalArgumentException if any of the parameters is invalid
+ */
+ public static Schedule createOrFail(long interval, String startTime) {
+ return create(interval, startTime).orElseThrow(IllegalArgumentException::new);
+ }
+
+ /**
+ * Creates a schedule.
+ *
+ * <p>{@link ScheduleConfig} defines details about which values are valid for the {@code
+ * interval} and {@code startTime} parameters.
+ *
+ * @param interval the interval in milliseconds
+ * @param startTime the start time as "{@code <day of week> <hours>:<minutes>}" or "{@code
+ * <hours>:<minutes>}"
+ * @return the schedule or {@link Optional#empty()} if any of the parameters is invalid
+ */
+ public static Optional<Schedule> create(long interval, String startTime) {
+ long initialDelay = computeInitialDelay(interval, startTime);
+ if (interval <= 0 || initialDelay < 0) {
+ return Optional.empty();
+ }
+ return Optional.of(create(interval, initialDelay));
+ }
+
+ static Schedule create(long interval, long initialDelay) {
+ return new AutoValue_ScheduleConfig_Schedule(interval, initialDelay);
+ }
+ }
+}