aboutsummaryrefslogtreecommitdiffstats
path: root/config-model-api/src/main/java/com/yahoo
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2022-01-07 10:56:41 +0100
committerMartin Polden <mpolden@mpolden.no>2022-01-07 14:43:37 +0100
commitae843dd9bd28be5c8445adf10dc7c0a6a69f2d97 (patch)
tree7d7c5b2fc602c54e3d634d0a0c07ab13d0ab1981 /config-model-api/src/main/java/com/yahoo
parente11472ccb26601fe74004a2400b44ea000c88d6f (diff)
Support date range in block window
Diffstat (limited to 'config-model-api/src/main/java/com/yahoo')
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java40
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java116
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java32
3 files changed, 162 insertions, 26 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
index 23a5dce3c25..67ddb9ef83c 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentInstanceSpec.java
@@ -6,13 +6,16 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.RegionName;
+import java.time.Duration;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* The deployment spec for an application instance
@@ -21,6 +24,9 @@ import java.util.stream.Collectors;
*/
public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
+ /** The maximum number of consecutive days Vespa upgrades are allowed to be blocked */
+ private static final int maxUpgradeBlockingDays = 21;
+
/** The name of the instance this step deploys */
private final InstanceName name;
@@ -40,7 +46,8 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
Optional<String> globalServiceId,
Optional<AthenzService> athenzService,
Notifications notifications,
- List<Endpoint> endpoints) {
+ List<Endpoint> endpoints,
+ Instant now) {
super(steps);
this.name = name;
this.upgradePolicy = upgradePolicy;
@@ -52,6 +59,7 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
this.endpoints = List.copyOf(endpoints);
validateZones(new HashSet<>(), new HashSet<>(), this);
validateEndpoints(steps(), globalServiceId, this.endpoints);
+ validateChangeBlockers(changeBlockers, now);
}
public InstanceName name() { return name; }
@@ -109,6 +117,36 @@ public class DeploymentInstanceSpec extends DeploymentSpec.Steps {
}
}
+ private void validateChangeBlockers(List<DeploymentSpec.ChangeBlocker> changeBlockers, Instant now) {
+ // Find all possible dates an upgrade block window can start
+ Stream<Instant> blockingFrom = changeBlockers.stream()
+ .filter(blocker -> blocker.blocksVersions())
+ .map(blocker -> blocker.window())
+ .map(window -> window.dateRange().start()
+ .map(date -> date.atStartOfDay(window.zone())
+ .toInstant())
+ .orElse(now))
+ .distinct();
+ if (!blockingFrom.allMatch(this::canUpgradeWithinDeadline)) {
+ throw new IllegalArgumentException("Cannot block Vespa upgrades for longer than " +
+ maxUpgradeBlockingDays + " consecutive days");
+ }
+ }
+
+ /** Returns whether this allows upgrade within deadline, relative to given instant */
+ private boolean canUpgradeWithinDeadline(Instant instant) {
+ instant = instant.truncatedTo(ChronoUnit.HOURS);
+ Duration step = Duration.ofHours(1);
+ Duration max = Duration.ofDays(maxUpgradeBlockingDays);
+ for (Instant current = instant; !canUpgradeAt(current); current = current.plus(step)) {
+ Duration blocked = Duration.between(instant, current);
+ if (blocked.compareTo(max) > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/** Returns the upgrade policy of this, which is defaultPolicy if none is specified */
public DeploymentSpec.UpgradePolicy upgradePolicy() { return upgradePolicy; }
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java b/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
index 5a2b3a10fe1..14deb5d5720 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
@@ -4,20 +4,23 @@ package com.yahoo.config.application.api;
import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.Instant;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
- * This class represents a window of time for selected hours on selected days.
+ * This class represents a window of time for selected hours, days and dates.
*
* @author mpolden
*/
@@ -26,11 +29,20 @@ public class TimeWindow {
private final List<DayOfWeek> days;
private final List<Integer> hours;
private final ZoneId zone;
+ private final LocalDateRange dateRange;
- private TimeWindow(List<DayOfWeek> days, List<Integer> hours, ZoneId zone) {
+ private TimeWindow(List<DayOfWeek> days, List<Integer> hours, ZoneId zone, LocalDateRange dateRange) {
this.days = Objects.requireNonNull(days).stream().distinct().sorted().collect(Collectors.toUnmodifiableList());
this.hours = Objects.requireNonNull(hours).stream().distinct().sorted().collect(Collectors.toUnmodifiableList());
this.zone = Objects.requireNonNull(zone);
+ this.dateRange = Objects.requireNonNull(dateRange);
+ if (days.isEmpty()) throw new IllegalArgumentException("At least one day must be specified");
+ if (hours.isEmpty()) throw new IllegalArgumentException("At least one hour must be specified");
+ for (var day : days) {
+ if (!dateRange.includes(day)) {
+ throw new IllegalArgumentException("Invalid day: " + dateRange + " does not contain " + day);
+ }
+ }
}
/** Returns days in this time window */
@@ -46,10 +58,17 @@ public class TimeWindow {
/** Returns the time zone of this time window */
public ZoneId zone() { return zone; }
+ /** Returns the date range of this time window applies to */
+ public LocalDateRange dateRange() {
+ return dateRange;
+ }
+
/** Returns whether the given instant is in this time window */
public boolean includes(Instant instant) {
LocalDateTime dt = LocalDateTime.ofInstant(instant, zone);
- return days.contains(dt.getDayOfWeek()) && hours.contains(dt.getHour());
+ return days.contains(dt.getDayOfWeek()) &&
+ hours.contains(dt.getHour()) &&
+ dateRange.includes(dt.toLocalDate());
}
@Override
@@ -59,15 +78,19 @@ public class TimeWindow {
" on " + days.stream().map(DayOfWeek::name)
.map(String::toLowerCase)
.collect(Collectors.toList()) +
- " in " + zone;
+ " in time zone " + zone + " and " + dateRange.toString();
}
/** Parse a time window from the given day, hour and time zone specification */
- public static TimeWindow from(String daySpec, String hourSpec, String zoneSpec) {
- List<DayOfWeek> days = parse(daySpec, TimeWindow::parseDays);
- List<Integer> hours = parse(hourSpec, TimeWindow::parseHours);
- ZoneId zone = zoneFrom(zoneSpec);
- return new TimeWindow(days, hours, zone);
+ public static TimeWindow from(String daySpec, String hourSpec, String zoneSpec, String dateStart, String dateEnd) {
+ List<DayOfWeek> days = daySpec.isEmpty()
+ ? List.of(DayOfWeek.values()) // All days by default
+ : parse(daySpec, TimeWindow::parseDays);
+ List<Integer> hours = hourSpec.isEmpty()
+ ? IntStream.rangeClosed(0, 23).boxed().collect(Collectors.toList()) // All hours by default
+ : parse(hourSpec, TimeWindow::parseHours);
+ ZoneId zone = zoneFrom(zoneSpec.isEmpty() ? "UTC" : zoneSpec);
+ return new TimeWindow(days, hours, zone, LocalDateRange.from(dateStart, dateEnd));
}
/** Parse a specification, e.g. "1,4-5", using the given value parser */
@@ -97,7 +120,7 @@ public class TimeWindow {
endInclusive));
}
return IntStream.rangeClosed(start, end).boxed()
- .collect(Collectors.toList());
+ .collect(Collectors.toList());
}
/** Returns a list of all days occurring between startInclusive and endInclusive */
@@ -109,16 +132,16 @@ public class TimeWindow {
endInclusive));
}
return IntStream.rangeClosed(start.getValue(), end.getValue()).boxed()
- .map(DayOfWeek::of)
- .collect(Collectors.toList());
+ .map(DayOfWeek::of)
+ .collect(Collectors.toList());
}
/** Parse day of week from string */
private static DayOfWeek dayFrom(String day) {
return Arrays.stream(DayOfWeek.values())
- .filter(dayOfWeek -> day.length() >= 3 && dayOfWeek.name().toLowerCase().startsWith(day))
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Invalid day '" + day + "'"));
+ .filter(dayOfWeek -> day.length() >= 3 && dayOfWeek.name().toLowerCase().startsWith(day))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException("Invalid day '" + day + "'"));
}
/** Parse hour from string */
@@ -139,4 +162,67 @@ public class TimeWindow {
}
}
+ /** A range of local dates, which may be unbounded */
+ public static class LocalDateRange {
+
+ private final Optional<LocalDate> start;
+ private final Optional<LocalDate> end;
+
+ private LocalDateRange(Optional<LocalDate> start, Optional<LocalDate> end) {
+ this.start = Objects.requireNonNull(start);
+ this.end = Objects.requireNonNull(end);
+ if (start.isPresent() && end.isPresent() && start.get().isAfter(end.get())) {
+ throw new IllegalArgumentException("Invalid date range: start date " + start.get() +
+ " is after end date " + end.get());
+ }
+ }
+
+ /** Returns the starting date of this (inclusive), if any */
+ public Optional<LocalDate> start() {
+ return start;
+ }
+
+ /** Returns the ending date of this (inclusive), if any */
+ public Optional<LocalDate> end() {
+ return end;
+ }
+
+ /** Returns whether this contains the given day */
+ private boolean includes(DayOfWeek day) {
+ if (start.isEmpty() || end.isEmpty()) return true;
+ for (LocalDate date = start.get(); !date.isAfter(end.get()); date = date.plusDays(1)) {
+ if (date.getDayOfWeek() == day) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Returns whether includes the given date */
+ private boolean includes(LocalDate date) {
+ if (start.isPresent() && date.isBefore(start.get())) return false;
+ if (end.isPresent() && date.isAfter(end.get())) return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "date range [" + start.map(LocalDate::toString).orElse("any date") +
+ ", " + end.map(LocalDate::toString).orElse("any date") + "]";
+ }
+
+ private static LocalDateRange from(String start, String end) {
+ try {
+ return new LocalDateRange(optionalDate(start), optionalDate(end));
+ } catch (DateTimeParseException e) {
+ throw new IllegalArgumentException("Could not parse date range '" + start + "' and '" + end + "'", e);
+ }
+ }
+
+ private static Optional<LocalDate> optionalDate(String date) {
+ return Optional.of(date).filter(s -> !s.isEmpty()).map(LocalDate::parse);
+ }
+
+ }
+
}
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
index 493a72b2018..8f866654d56 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/xml/DeploymentSpecXmlReader.java
@@ -27,7 +27,9 @@ import org.w3c.dom.Node;
import java.io.IOException;
import java.io.Reader;
+import java.time.Clock;
import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -67,22 +69,28 @@ public class DeploymentSpecXmlReader {
private static final String testerFlavorAttribute = "tester-flavor";
private final boolean validate;
+ private final Clock clock;
private final List<DeprecatedElement> deprecatedElements = new ArrayList<>();
- /** Creates a validating reader */
+ /**
+ * Create a deployment spec reader
+ * @param validate true to validate the input, false to accept any input which can be unambiguously parsed
+ * @param clock clock to use when validating time constraints
+ */
+ public DeploymentSpecXmlReader(boolean validate, Clock clock) {
+ this.validate = validate;
+ this.clock = clock;
+ }
+
public DeploymentSpecXmlReader() {
this(true);
}
- /**
- * Creates a deployment spec reader
- *
- * @param validate true to validate the input, false to accept any input which can be unambiguously parsed
- */
public DeploymentSpecXmlReader(boolean validate) {
- this.validate = validate;
+ this(validate, Clock.systemUTC());
}
+ /** Reads a deployment spec from given reader */
public DeploymentSpec read(Reader reader) {
try {
return read(IOUtils.readAll(reader));
@@ -169,6 +177,7 @@ public class DeploymentSpecXmlReader {
List<Endpoint> endpoints = readEndpoints(instanceTag, Optional.of(instanceNameString), steps);
// Build and return instances with these values
+ Instant now = clock.instant();
return Arrays.stream(instanceNameString.split(","))
.map(name -> name.trim())
.map(name -> new DeploymentInstanceSpec(InstanceName.from(name),
@@ -179,7 +188,8 @@ public class DeploymentSpecXmlReader {
globalServiceId.asOptional(),
athenzService,
notifications,
- endpoints))
+ endpoints,
+ now))
.collect(Collectors.toList());
}
@@ -430,9 +440,11 @@ public class DeploymentSpecXmlReader {
String daySpec = tag.getAttribute("days");
String hourSpec = tag.getAttribute("hours");
String zoneSpec = tag.getAttribute("time-zone");
- if (zoneSpec.isEmpty()) zoneSpec = "UTC"; // default
+ String dateStart = tag.getAttribute("from-date");
+ String dateEnd = tag.getAttribute("to-date");
+
return new DeploymentSpec.ChangeBlocker(blockRevisions, blockVersions,
- TimeWindow.from(daySpec, hourSpec, zoneSpec));
+ TimeWindow.from(daySpec, hourSpec, zoneSpec, dateStart, dateEnd));
}
/** Returns true if the given value is "true", or if it is missing */