| // 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 = 06: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 != INVALID_CONFIG && interval <= 0) { | |
| logger.atSevere().log("Invalid interval value \"%d\" for \"%s\": must be > 0", interval, key); | |
| interval = INVALID_CONFIG; | |
| } | |
| if (initialDelay != INVALID_CONFIG && initialDelay < 0) { | |
| logger.atSevere().log( | |
| "Invalid initial delay value \"%d\" for \"%s\": must be >= 0", initialDelay, key); | |
| initialDelay = INVALID_CONFIG; | |
| } | |
| if (interval == INVALID_CONFIG || initialDelay == INVALID_CONFIG) { | |
| logger.atSevere().log("Invalid schedule configuration for \"%s\" is ignored. ", key); | |
| return true; | |
| } | |
| return false; | |
| } | |
| @Override | |
| public final 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) { | |
| // We only need to log the exception message; it already includes the | |
| // section.subsection.key and bad value. | |
| logger.atSevere().log("%s", e.getMessage()); | |
| 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) { | |
| logger.atSevere().log("Invalid start time: %s", e.getMessage()); | |
| 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); | |
| } | |
| } | |
| } |