summaryrefslogtreecommitdiffstats
path: root/config-model-api
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2017-09-25 13:27:36 +0200
committerMartin Polden <mpolden@mpolden.no>2017-09-25 13:27:36 +0200
commit1433f837ba7e758a68695234e1dfd89ce9aaa7df (patch)
treea2d57b368e416473e35a6ca725917ba14bd3d360 /config-model-api
parentb77d42c9a25531982d2d77af32561850cf56ab36 (diff)
Read block-upgrade tag from deployment spec
Diffstat (limited to 'config-model-api')
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java47
-rw-r--r--config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java141
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java28
-rw-r--r--config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java143
4 files changed, 351 insertions, 8 deletions
diff --git a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java
index 098f5620723..e75ba5871f7 100644
--- a/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/DeploymentSpec.java
@@ -13,6 +13,7 @@ import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -39,25 +40,29 @@ import java.util.stream.Collectors;
public class DeploymentSpec {
/** The empty deployment spec, specifying no zones or rotation, and defaults for all settings */
- public static final DeploymentSpec empty = new DeploymentSpec(Optional.empty(),
- UpgradePolicy.defaultPolicy,
- ImmutableList.of(),
+ public static final DeploymentSpec empty = new DeploymentSpec(Optional.empty(),
+ UpgradePolicy.defaultPolicy,
+ Collections.emptyList(),
+ Collections.emptyList(),
"<deployment version='1.0'/>");
private final Optional<String> globalServiceId;
private final UpgradePolicy upgradePolicy;
+ private final List<TimeWindow> blockUpgrades;
private final List<Step> steps;
private final String xmlForm;
- public DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy, List<Step> steps) {
- this(globalServiceId, upgradePolicy, steps, null);
+ public DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy,
+ List<TimeWindow> blockUpgrades, List<Step> steps) {
+ this(globalServiceId, upgradePolicy, blockUpgrades, steps, null);
}
- private DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy,
- List<Step> steps, String xmlForm) {
+ private DeploymentSpec(Optional<String> globalServiceId, UpgradePolicy upgradePolicy,
+ List<TimeWindow> blockUpgrades, List<Step> steps, String xmlForm) {
validateTotalDelay(steps);
this.globalServiceId = globalServiceId;
this.upgradePolicy = upgradePolicy;
+ this.blockUpgrades = blockUpgrades;
this.steps = ImmutableList.copyOf(completeSteps(new ArrayList<>(steps)));
this.xmlForm = xmlForm;
validateZones(this.steps);
@@ -137,6 +142,14 @@ public class DeploymentSpec {
/** Returns the upgrade policy of this, which is defaultPolicy if none is specified */
public UpgradePolicy upgradePolicy() { return upgradePolicy; }
+ /** Returns whether upgrade can occur at the given instant */
+ public boolean canUpgradeAt(Instant instant) {
+ return blockUpgrades.stream().noneMatch(timeWindow -> timeWindow.includes(instant));
+ }
+
+ /** Returns time windows where upgrade is disallowed */
+ public List<TimeWindow> blockUpgrades() { return blockUpgrades; }
+
/** Returns the deployment steps of this in the order they will be performed */
public List<Step> steps() { return steps; }
@@ -210,7 +223,8 @@ public class DeploymentSpec {
else if (readGlobalServiceId(environmentTag).isPresent())
throw new IllegalArgumentException("Attribute 'global-service-id' is only valid on 'prod' tag.");
}
- return new DeploymentSpec(globalServiceId, readUpgradePolicy(root), steps, xmlForm);
+ return new DeploymentSpec(globalServiceId, readUpgradePolicy(root), readBlockUpgradeWindows(root), steps,
+ xmlForm);
}
/** Returns the given attribute as an integer, or 0 if it is not present */
@@ -244,6 +258,23 @@ public class DeploymentSpec {
return Optional.of(globalServiceId);
}
}
+
+ private static List<TimeWindow> readBlockUpgradeWindows(Element root) {
+ List<TimeWindow> timeWindows = new ArrayList<>();
+ for (Element tag : XML.getChildren(root)) {
+ if (!"block-upgrade".equals(tag.getTagName())) {
+ continue;
+ }
+ String daySpec = tag.getAttribute("days");
+ String hourSpec = tag.getAttribute("hours");
+ String zoneSpec = tag.getAttribute("time-zone");
+ if (zoneSpec.isEmpty()) { // Default to UTC time zone
+ zoneSpec = "UTC";
+ }
+ timeWindows.add(TimeWindow.from(daySpec, hourSpec, zoneSpec));
+ }
+ return Collections.unmodifiableList(timeWindows);
+ }
private static UpgradePolicy readUpgradePolicy(Element root) {
Element upgradeElement = XML.getChild(root, "upgrade");
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
new file mode 100644
index 00000000000..40b5a5370e7
--- /dev/null
+++ b/config-model-api/src/main/java/com/yahoo/config/application/api/TimeWindow.java
@@ -0,0 +1,141 @@
+package com.yahoo.config.application.api;
+
+import java.time.DateTimeException;
+import java.time.DayOfWeek;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.temporal.ChronoField;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.TreeSet;
+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.
+ *
+ * @author mpolden
+ */
+public class TimeWindow {
+
+ private final List<DayOfWeek> days;
+ private final List<Integer> hours;
+ private final ZoneId zone;
+
+ private TimeWindow(List<DayOfWeek> days, List<Integer> hours, ZoneId zone) {
+ this.days = Collections.unmodifiableList(new ArrayList<>(new TreeSet<>(days)));
+ this.hours = Collections.unmodifiableList(new ArrayList<>(new TreeSet<>(hours)));
+ this.zone = zone;
+ }
+
+ /** Returns days in this time window */
+ public List<DayOfWeek> days() {
+ return days;
+ }
+
+ /** Returns hours in this time window */
+ public List<Integer> hours() {
+ return hours;
+ }
+
+ /** Returns the time zone of this time window */
+ public ZoneId zone() { return zone; }
+
+ /** 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());
+ }
+
+ @Override
+ public String toString() {
+ return "time window for hour(s) " +
+ hours.toString() +
+ " on " + days.stream().map(DayOfWeek::name)
+ .map(String::toLowerCase)
+ .collect(Collectors.toList()).toString() +
+ " in " + zone;
+ }
+
+ /** 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);
+ }
+
+ /** Parse a specification, e.g. "1,4-5", using the given value parser */
+ private static <T> List<T> parse(String spec, BiFunction<String, String, List<T>> valueParser) {
+ List<T> values = new ArrayList<>();
+ String[] parts = spec.split(",");
+ for (String part : parts) {
+ if (part.contains("-")) {
+ String[] startAndEnd = part.split("-");
+ if (startAndEnd.length != 2) {
+ throw new IllegalArgumentException("Invalid range '" + part + "'");
+ }
+ values.addAll(valueParser.apply(startAndEnd[0], startAndEnd[1]));
+ } else {
+ values.addAll(valueParser.apply(part, part));
+ }
+ }
+ return Collections.unmodifiableList(values);
+ }
+
+ /** Returns a list of all hours occurring between startInclusive and endInclusive */
+ private static List<Integer> parseHours(String startInclusive, String endInclusive) {
+ int start = hourFrom(startInclusive);
+ int end = hourFrom(endInclusive);
+ if (end < start) {
+ throw new IllegalArgumentException(String.format("Invalid hour range '%s-%s'", startInclusive,
+ endInclusive));
+ }
+ return IntStream.rangeClosed(start, end).boxed()
+ .collect(Collectors.toList());
+ }
+
+ /** Returns a list of all days occurring between startInclusive and endInclusive */
+ private static List<DayOfWeek> parseDays(String startInclusive, String endInclusive) {
+ DayOfWeek start = dayFrom(startInclusive);
+ DayOfWeek end = dayFrom(endInclusive);
+ if (end.getValue() < start.getValue()) {
+ throw new IllegalArgumentException(String.format("Invalid day range '%s-%s'", startInclusive,
+ endInclusive));
+ }
+ return IntStream.rangeClosed(start.getValue(), end.getValue()).boxed()
+ .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 + "'"));
+ }
+
+ /** Parse hour from string */
+ private static int hourFrom(String hour) {
+ try {
+ return ChronoField.HOUR_OF_DAY.checkValidIntValue(Integer.parseInt(hour));
+ } catch (DateTimeException|NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid hour '" + hour + "'", e);
+ }
+ }
+
+ /** Parse time zone from string */
+ private static ZoneId zoneFrom(String zone) {
+ try {
+ return ZoneId.of(zone);
+ } catch (DateTimeException e) {
+ throw new IllegalArgumentException("Invalid time zone '" + zone + "'", e);
+ }
+ }
+
+}
diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java
index 95f9963d6f4..3540a5d785f 100644
--- a/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java
+++ b/config-model-api/src/test/java/com/yahoo/config/application/api/DeploymentSpecTest.java
@@ -6,6 +6,8 @@ import com.yahoo.config.provision.RegionName;
import org.junit.Test;
import java.io.StringReader;
+import java.time.Instant;
+import java.time.ZoneId;
import java.util.Optional;
import static org.junit.Assert.assertEquals;
@@ -276,4 +278,30 @@ public class DeploymentSpecTest {
}
}
+ @Test
+ public void deploymentSpecWithBlockUpgrade() {
+ StringReader r = new StringReader(
+ "<deployment>\n" +
+ " <block-upgrade days='mon,tue' hours='15-16'/>\n" +
+ " <block-upgrade days='sat' hours='10' time-zone='CET'/>\n" +
+ " <prod>\n" +
+ " <region active='true'>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>"
+ );
+ DeploymentSpec spec = DeploymentSpec.fromXml(r);
+ assertEquals(2, spec.blockUpgrades().size());
+ assertEquals(ZoneId.of("UTC"), spec.blockUpgrades().get(0).zone());
+ assertEquals(ZoneId.of("CET"), spec.blockUpgrades().get(1).zone());
+
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-18T14:15:30.00Z")));
+ assertFalse(spec.canUpgradeAt(Instant.parse("2017-09-18T15:15:30.00Z")));
+ assertFalse(spec.canUpgradeAt(Instant.parse("2017-09-18T16:15:30.00Z")));
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-18T17:15:30.00Z")));
+
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-23T09:15:30.00Z")));
+ assertFalse(spec.canUpgradeAt(Instant.parse("2017-09-23T08:15:30.00Z"))); // 10 in CET
+ assertTrue(spec.canUpgradeAt(Instant.parse("2017-09-23T10:15:30.00Z")));
+ }
+
}
diff --git a/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java b/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java
new file mode 100644
index 00000000000..86ce0466213
--- /dev/null
+++ b/config-model-api/src/test/java/com/yahoo/config/application/api/TimeWindowTest.java
@@ -0,0 +1,143 @@
+package com.yahoo.config.application.api;
+
+import org.junit.Test;
+
+import java.time.Instant;
+
+import static java.time.DayOfWeek.FRIDAY;
+import static java.time.DayOfWeek.MONDAY;
+import static java.time.DayOfWeek.SATURDAY;
+import static java.time.DayOfWeek.THURSDAY;
+import static java.time.DayOfWeek.TUESDAY;
+import static java.time.DayOfWeek.WEDNESDAY;
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author mpolden
+ */
+public class TimeWindowTest {
+
+ @Test
+ public void includesInstant() {
+ {
+ TimeWindow tw = TimeWindow.from("mon", "10,11", "UTC");
+ Instant i0 = Instant.parse("2017-09-17T11:15:30.00Z"); // Wrong day
+ Instant i1 = Instant.parse("2017-09-18T09:15:30.00Z"); // Wrong hour
+ Instant i2 = Instant.parse("2017-09-18T10:15:30.00Z");
+ Instant i3 = Instant.parse("2017-09-18T11:15:30.00Z");
+ Instant i4 = Instant.parse("2017-09-18T12:15:30.00Z"); // Wrong hour
+ Instant i5 = Instant.parse("2017-09-19T11:15:30.00Z"); // Wrong day
+
+ assertFalse("Instant " + i0 + " is not in window", tw.includes(i0));
+ assertFalse("Instant " + i1 + " is not in window", tw.includes(i1));
+ assertTrue("Instant " + i2 + " is in window", tw.includes(i2));
+ assertTrue("Instant " + i3 + " is in window", tw.includes(i3));
+ assertFalse("Instant " + i4 + " is not in window", tw.includes(i4));
+ assertFalse("Instant " + i5 + " is not in window", tw.includes(i5));
+ }
+ {
+ TimeWindow tw = TimeWindow.from("mon", "12,13", "CET");
+ Instant i0 = Instant.parse("2017-09-17T11:15:30.00Z");
+ Instant i1 = Instant.parse("2017-09-18T09:15:30.00Z");
+ Instant i2 = Instant.parse("2017-09-18T10:15:30.00Z"); // Including offset this matches hour 12
+ Instant i3 = Instant.parse("2017-09-18T11:15:30.00Z"); // Including offset this matches hour 13
+ Instant i4 = Instant.parse("2017-09-18T12:15:30.00Z");
+ Instant i5 = Instant.parse("2017-09-19T11:15:30.00Z");
+ assertFalse("Instant " + i0 + " is not in window", tw.includes(i0));
+ assertFalse("Instant " + i1 + " is not in window", tw.includes(i1));
+ assertTrue("Instant " + i2 + " is in window", tw.includes(i2));
+ assertTrue("Instant " + i3 + " is in window", tw.includes(i3));
+ assertFalse("Instant " + i4 + " is not in window", tw.includes(i4));
+ assertFalse("Instant " + i5 + " is not in window", tw.includes(i5));
+ }
+ }
+
+ @Test
+ public void validWindows() {
+ {
+ TimeWindow fz = TimeWindow.from("fri", "8,17-19", "UTC");
+ assertEquals(asList(FRIDAY), fz.days());
+ assertEquals(asList(8, 17, 18, 19), fz.hours());
+ }
+ {
+ TimeWindow fz = TimeWindow.from("sat,", "8,17-19", "UTC");
+ assertEquals(asList(SATURDAY), fz.days());
+ assertEquals(asList(8, 17, 18, 19), fz.hours());
+ }
+ {
+ TimeWindow fz = TimeWindow.from("tue,sat", "0,3,7,10", "UTC");
+ assertEquals(asList(TUESDAY, SATURDAY), fz.days());
+ assertEquals(asList(0, 3, 7, 10), fz.hours());
+ }
+ {
+ TimeWindow fz = TimeWindow.from("mon,wed-thu", "0,17-19", "UTC");
+ assertEquals(asList(MONDAY, WEDNESDAY, THURSDAY), fz.days());
+ assertEquals(asList(0, 17, 18, 19), fz.hours());
+ }
+ {
+ // Full day names is allowed
+ TimeWindow fz = TimeWindow.from("monday,wednesday-thursday", "0,17-19", "UTC");
+ assertEquals(asList(MONDAY, WEDNESDAY, THURSDAY), fz.days());
+ assertEquals(asList(0, 17, 18, 19), fz.hours());
+ }
+ {
+ // Duplicate day and overlapping range is allowed
+ TimeWindow fz = TimeWindow.from("mon,wed-thu,mon", "3,1-4", "UTC");
+ assertEquals(asList(MONDAY, WEDNESDAY, THURSDAY), fz.days());
+ assertEquals(asList(1, 2, 3, 4), fz.hours());
+ }
+ }
+
+ @Test
+ public void invalidWindows() {
+ // Invalid time zone
+ assertInvalidZone("foo", "Invalid time zone 'foo'");
+
+ // Malformed day input
+ assertInvalidDays("", "Invalid day ''");
+ assertInvalidDays("foo-", "Invalid range 'foo-'");
+ assertInvalidDays("foo", "Invalid day 'foo'");
+ assertInvalidDays("f", "Invalid day 'f'");
+ // Window crossing week boundary is disallowed
+ assertInvalidDays("fri-tue", "Invalid day range 'fri-tue'");
+
+ // Malformed hour input
+ assertInvalidHours("", "Invalid hour ''");
+ assertInvalidHours("24", "Invalid hour '24'");
+ assertInvalidHours("-1-9", "Invalid range '-1-9'");
+ // Window crossing day boundary is disallowed
+ assertInvalidHours("23-1", "Invalid hour range '23-1'");
+ }
+
+ private static void assertInvalidZone(String zoneSpec, String exceptionMessage) {
+ try {
+ TimeWindow.from("mon", "1", zoneSpec);
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals(exceptionMessage, e.getMessage());
+ }
+ }
+
+ private static void assertInvalidDays(String daySpec, String exceptionMessage) {
+ try {
+ TimeWindow.from(daySpec, "1", "UTC");
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals(exceptionMessage, e.getMessage());
+ }
+ }
+
+ private static void assertInvalidHours(String hourSpec, String exceptionMessage) {
+ try {
+ TimeWindow.from("mon", hourSpec, "UTC");
+ fail("Expected exception");
+ } catch (IllegalArgumentException e) {
+ assertEquals(exceptionMessage, e.getMessage());
+ }
+ }
+
+}