All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.config.ScheduleConfig Maven / Gradle / Ivy

There is a newer version: 3.11.0-rc3
Show newest version
// 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.
 *
 * 

A schedule configuration consists of two parameters: * *

    *
  • {@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: *
      *
    • {@code s}, {@code sec}, {@code second}, {@code seconds} *
    • {@code m}, {@code min}, {@code minute}, {@code minutes} *
    • {@code h}, {@code hr}, {@code hour}, {@code hours} *
    • {@code d}, {@code day}, {@code days} *
    • {@code w}, {@code week}, {@code weeks} ({@code 1 week} is treated as {@code 7 days}) *
    • {@code mon}, {@code month}, {@code months} ({@code 1 month} is treated as {@code 30 * days}) *
    • {@code y}, {@code year}, {@code years} ({@code 1 year} is treated as {@code 365 * days}) *
    *
  • {@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: *
      *
    • {@code :} *
    • {@code :} *
    * The placeholders can have the following values: *
      *
    • {@code }: {@code Mon}, {@code Tue}, {@code Wed}, {@code Thu}, {@code * Fri}, {@code Sat}, {@code Sun} *
    • {@code }: {@code 00}-{@code 23} *
    • {@code }: {@code 00}-{@code 59} *
    * The timezone cannot be specified but is always the system default time-zone. *
* *

The section and the subsection from which the {@code interval} and {@code startTime} * parameters are read can be configured. * *

Examples for a schedule configuration: * *

    *
  • *
     * foo.startTime = Fri 10:30
     * foo.interval  = 2 day
     * 
    * 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. *
  • *
     * foo.startTime = 06:00
     * foo.interval = 1 day
     * 
    * 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. *
*/ @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 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() { 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 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. * *

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. * *

{@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 :}" or "{@code * :}" * @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. * *

{@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 :}" or "{@code * :}" * @return the schedule or {@link Optional#empty()} if any of the parameters is invalid */ public static Optional 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); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy