diff options
Diffstat (limited to 'java/com/google/gerrit/server/config/ScheduleConfig.java')
-rw-r--r-- | java/com/google/gerrit/server/config/ScheduleConfig.java | 343 |
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); + } + } +} |