| // 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); |
| } |
| } |
| } |