diff options
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") |