aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java49
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java10
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java39
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java2
8 files changed, 150 insertions, 3 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java
new file mode 100644
index 00000000000..0565a916dfa
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java
@@ -0,0 +1,49 @@
+package com.yahoo.vespa.hosted.controller.api.identifiers;
+
+import com.yahoo.config.provision.ClusterSpec;
+
+import java.util.Objects;
+
+/**
+ * DeploymentId x ClusterSpec.Id = ClusterId
+ *
+ * @author ogronnesby
+ */
+public class ClusterId {
+ private final DeploymentId deploymentId;
+ private final ClusterSpec.Id clusterId;
+
+ public ClusterId(DeploymentId deploymentId, ClusterSpec.Id clusterId) {
+ this.deploymentId = deploymentId;
+ this.clusterId = clusterId;
+ }
+
+ public DeploymentId deploymentId() {
+ return deploymentId;
+ }
+
+ public ClusterSpec.Id clusterId() {
+ return clusterId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ClusterId clusterId1 = (ClusterId) o;
+ return Objects.equals(deploymentId, clusterId1.deploymentId) && Objects.equals(clusterId, clusterId1.clusterId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(deploymentId, clusterId);
+ }
+
+ @Override
+ public String toString() {
+ return "ClusterId{" +
+ "deploymentId=" + deploymentId +
+ ", clusterId=" + clusterId +
+ '}';
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java
index 6e60ec76199..b500cd1c133 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java
@@ -7,6 +7,7 @@ import com.yahoo.config.provision.ClusterSpec;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
/**
@@ -131,6 +132,28 @@ public class Cluster {
public Instant at() { return at; }
public Optional<Instant> completion() { return completion; }
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ScalingEvent that = (ScalingEvent) o;
+ return Objects.equals(from, that.from) && Objects.equals(to, that.to) && Objects.equals(at, that.at) && Objects.equals(completion, that.completion);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(from, to, at, completion);
+ }
+
+ @Override
+ public String toString() {
+ return "ScalingEvent{" +
+ "from=" + from +
+ ", to=" + to +
+ ", at=" + at +
+ ", completion=" + completion +
+ '}';
+ }
}
}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
index 8ae12c0e7ac..2e281b1759b 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
@@ -1,14 +1,18 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.api.integration.resource;
-import com.yahoo.config.provision.ApplicationName;
+import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
+import java.time.Instant;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
/**
@@ -22,6 +26,10 @@ public interface ResourceDatabaseClient {
void refreshMaterializedView();
+ void writeScalingEvents(ClusterId clusterId, Collection<Cluster.ScalingEvent> scalingEvents);
+
+ List<Cluster.ScalingEvent> scalingEvents(Instant from, Instant to, Optional<ApplicationId> application);
+
Set<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName);
List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
index e9bfc4fe78c..e2003e24332 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
@@ -1,9 +1,12 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.api.integration.resource;
+import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
import java.math.BigDecimal;
import java.time.Duration;
@@ -30,6 +33,7 @@ public class ResourceDatabaseClientMock implements ResourceDatabaseClient {
PlanRegistry planRegistry;
Map<TenantName, Plan> planMap = new HashMap<>();
List<ResourceSnapshot> resourceSnapshots = new ArrayList<>();
+ Map<ClusterId, List<Cluster.ScalingEvent>> scalingEvents = new HashMap<>();
private boolean hasRefreshedMaterializedView = false;
public ResourceDatabaseClientMock(PlanRegistry planRegistry) {
@@ -121,6 +125,16 @@ public class ResourceDatabaseClientMock implements ResourceDatabaseClient {
hasRefreshedMaterializedView = true;
}
+ @Override
+ public void writeScalingEvents(ClusterId clusterId, Collection<Cluster.ScalingEvent> scalingEvents) {
+ this.scalingEvents.put(clusterId, List.copyOf(scalingEvents));
+ }
+
+ @Override
+ public List<Cluster.ScalingEvent> scalingEvents(Instant from, Instant to, Optional<ApplicationId> application) {
+ return List.of();
+ }
+
public void setPlan(TenantName tenant, Plan plan) {
planMap.put(tenant, plan);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java
index b7d3a882ae2..205fb7e0e79 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java
@@ -10,12 +10,16 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.jdisc.Metric;
+import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.Instance;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.MeteringClient;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
@@ -38,6 +42,7 @@ import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collector;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Creates a {@link ResourceSnapshot} per application, which is then passed on to a MeteringClient
@@ -95,6 +100,7 @@ public class ResourceMeterMaintainer extends ControllerMaintainer {
}
if (systemName.isPublic()) reportResourceSnapshots(resourceSnapshots);
+ if (systemName.isPublic()) reportAllScalingEvents();
updateDeploymentCost(resourceSnapshots);
return 1.0;
}
@@ -148,6 +154,37 @@ public class ResourceMeterMaintainer extends ControllerMaintainer {
.collect(Collectors.toList());
}
+ private Stream<Instance> mapApplicationToInstances(Application application) {
+ return application.instances().values().stream();
+ }
+
+ private Stream<DeploymentId> mapInstanceToDeployments(Instance instance) {
+ return instance.deployments().keySet().stream().map(zoneId -> {
+ return new DeploymentId(instance.id(), zoneId);
+ });
+ }
+
+ private Stream<Map.Entry<ClusterId, List<Cluster.ScalingEvent>>> mapDeploymentToClusterScalingEvent(DeploymentId deploymentId) {
+ return nodeRepository.getApplication(deploymentId.zoneId(), deploymentId.applicationId())
+ .clusters().entrySet().stream()
+ .map(cluster -> Map.entry(new ClusterId(deploymentId, cluster.getKey()), cluster.getValue().scalingEvents()));
+ }
+
+ private void reportAllScalingEvents() {
+ var clusters = controller().applications().asList().stream()
+ .flatMap(this::mapApplicationToInstances)
+ .flatMap(this::mapInstanceToDeployments)
+ .flatMap(this::mapDeploymentToClusterScalingEvent)
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue
+ ));
+
+ for (var cluster : clusters.entrySet()) {
+ resourceClient.writeScalingEvents(cluster.getKey(), cluster.getValue());
+ }
+ }
+
private Collection<ResourceSnapshot> createResourceSnapshotsFromNodes(ZoneId zoneId, List<Node> nodes) {
return nodes.stream()
.filter(this::unlessNodeOwnerIsSystemApplication)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
index df30b6b57ee..187b8f932cf 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
@@ -83,6 +83,13 @@ public class CloudTrialExpirerTest {
assertTrue(tester.controller().tenants().get("with-apps").isEmpty());
}
+ @Test
+ public void keep_tenants_without_applications_that_are_idle() {
+ registerTenant("active", "none", Duration.ofDays(364));
+ expirer.maintain();
+ assertPlan("active", "none");
+ }
+
private void registerTenant(String tenantName, String plan, Duration timeSinceLastLogin) {
var name = TenantName.from(tenantName);
tester.createTenant(tenantName, Tenant.Type.cloud);
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java
index 320938f00e4..8952e93c778 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java
@@ -121,6 +121,15 @@ public class ResourceMeterMaintainerTest {
assertEquals(lastRefreshTime + millisAdvanced, tester.curator().readMeteringRefreshTime());
}
+ @Test
+ public void scaling_events_report() {
+ tester.createTenant("tenant1");
+ tester.createApplication("tenant1", "app1", "default");
+
+ setUpZones();
+ maintainer.maintain();
+ }
+
private void setUpZones() {
ZoneApiMock zone1 = ZoneApiMock.newBuilder().withId("prod.region-2").build();
ZoneApiMock zone2 = ZoneApiMock.newBuilder().withId("test.region-3").build();
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java
index 94ca4268000..5936c135af9 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java
@@ -161,7 +161,7 @@ public class ControllerApiTest extends ControllerContainerTest {
new ResourceSnapshot(applicationId, 12,48,1200, NodeResources.Architecture.arm64, timestamp, zoneId),
new ResourceSnapshot(applicationId, 24, 96,2400, NodeResources.Architecture.x86_64, timestamp, zoneId)
);
- tester.controller().serviceRegistry().meteringService().consume(snapshots);
+ tester.controller().serviceRegistry().resourceDatabase().writeResourceSnapshots(snapshots);
tester.assertResponse(
operatorRequest("http://localhost:8080/controller/v1/metering/tenant/tenantName/month/2020-02", "", Request.Method.GET),
new File("metering.json")