LCOV - code coverage report
Current view: top level - server/config - ScheduleConfig.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 69 92 75.0 %
Date: 2022-11-19 15:00:39 Functions: 17 19 89.5 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750