diff options
Diffstat (limited to 'controller-server')
2 files changed, 78 insertions, 52 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java index b1b7e80e9a0..9baa1ad37a1 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java @@ -13,14 +13,13 @@ import com.yahoo.vespa.hosted.controller.tenant.Tenant; import java.time.Duration; import java.util.List; import java.util.Optional; -import java.util.function.Consumer; import java.util.function.Predicate; import java.util.logging.Logger; import java.util.stream.Collectors; /** * Expires unused tenants from Vespa Cloud. - * + * <p> * TODO: Should support sending notifications some time before the various expiry events happen. * * @author ogronnesby @@ -39,38 +38,43 @@ public class CloudTrialExpirer extends ControllerMaintainer { @Override protected double maintain() { - tombstoneNonePlanTenants(); - moveInactiveTenantsToNonePlan(); - return 1.0; + var a = tombstoneNonePlanTenants(); + var b = moveInactiveTenantsToNonePlan(); + return (a ? 0.5 : 0.0) + (b ? 0.5 : 0.0); } - private void moveInactiveTenantsToNonePlan() { - var predicate = tenantReadersNotLoggedIn(nonePlanAfter) - .and(this::tenantHasTrialPlan); - - forTenant("'none' plan", predicate, this::setPlanNone); - } + private boolean moveInactiveTenantsToNonePlan() { + var idleTrialTenants = controller().tenants().asList().stream() + .filter(this::tenantIsCloudTenant) + .filter(this::tenantIsNotExemptFromExpiry) + .filter(this::tenantHasNoDeployments) + .filter(this::tenantHasTrialPlan) + .filter(tenantReadersNotLoggedIn(nonePlanAfter)) + .toList(); + + if (! idleTrialTenants.isEmpty()) { + var tenants = idleTrialTenants.stream().map(Tenant::name).map(TenantName::value).collect(Collectors.joining(", ")); + log.info("Setting tenants to 'none' plan: " + tenants); + } - private void tombstoneNonePlanTenants() { - var predicate = tenantReadersNotLoggedIn(tombstoneAfter).and(this::tenantHasNonePlan); - forTenant("tombstoned", predicate, this::tombstoneTenants); + return setPlanNone(idleTrialTenants); } - private void forTenant(String name, Predicate<Tenant> p, Consumer<List<Tenant>> c) { - var predicate = p.and(this::tenantIsCloudTenant) - .and(this::tenantIsNotExemptFromExpiry) - .and(this::tenantHasNoDeployments); - - var tenants = controller().tenants().asList().stream() - .filter(predicate) - .collect(Collectors.toList()); - - if (! tenants.isEmpty()) { - var tenantNames = tenants.stream().map(Tenant::name).map(TenantName::value).collect(Collectors.joining(", ")); - log.info("Setting tenants as " + name + ": " + tenantNames); + private boolean tombstoneNonePlanTenants() { + var idleOldPlanTenants = controller().tenants().asList().stream() + .filter(this::tenantIsCloudTenant) + .filter(this::tenantIsNotExemptFromExpiry) + .filter(this::tenantHasNoDeployments) + .filter(this::tenantHasNonePlan) + .filter(tenantReadersNotLoggedIn(tombstoneAfter)) + .toList(); + + if (! idleOldPlanTenants.isEmpty()) { + var tenants = idleOldPlanTenants.stream().map(Tenant::name).map(TenantName::value).collect(Collectors.joining(", ")); + log.info("Setting tenants as tombstoned: " + tenants); } - c.accept(tenants); + return tombstoneTenants(idleOldPlanTenants); } private boolean tenantIsCloudTenant(Tenant tenant) { @@ -98,7 +102,7 @@ public class CloudTrialExpirer extends ControllerMaintainer { } private boolean tenantIsNotExemptFromExpiry(Tenant tenant) { - return ! extendedTrialTenants.value().contains(tenant.name().value()); + return !extendedTrialTenants.value().contains(tenant.name().value()); } private boolean tenantHasNoDeployments(Tenant tenant) { @@ -108,23 +112,46 @@ public class CloudTrialExpirer extends ControllerMaintainer { .sum() == 0; } - private void setPlanNone(List<Tenant> tenants) { - tenants.forEach(tenant -> { - controller().serviceRegistry().billingController().setPlan(tenant.name(), PlanId.from("none"), false, false); - }); + private boolean setPlanNone(List<Tenant> tenants) { + var success = true; + for (var tenant : tenants) { + try { + controller().serviceRegistry().billingController().setPlan(tenant.name(), PlanId.from("none"), false, false); + } catch (RuntimeException e) { + log.info("Could not change plan for " + tenant.name() + ": " + e.getMessage()); + success = false; + } + } + return success; } - private void tombstoneTenants(List<Tenant> tenants) { - tenants.forEach(tenant -> { - deleteApplicationsWithNoDeployments(tenant); - controller().tenants().delete(tenant.name(), Optional.empty(), false); - }); + private boolean tombstoneTenants(List<Tenant> tenants) { + var success = true; + for (var tenant : tenants) { + success &= deleteApplicationsWithNoDeployments(tenant); + log.fine("Tombstoning empty tenant: " + tenant.name()); + try { + controller().tenants().delete(tenant.name(), Optional.empty(), false); + } catch (RuntimeException e) { + log.info("Could not tombstone tenant " + tenant.name() + ": " + e.getMessage()); + success = false; + } + } + return success; } - private void deleteApplicationsWithNoDeployments(Tenant tenant) { - controller().applications().asList(tenant.name()).forEach(application -> { - // this only removes applications with no active deployments - controller().applications().deleteApplication(application.id(), Optional.empty()); - }); + private boolean deleteApplicationsWithNoDeployments(Tenant tenant) { + // this method only removes applications with no active deployments in them + var success = true; + for (var application : controller().applications().asList(tenant.name())) { + try { + log.fine("Removing empty application: " + application.id()); + controller().applications().deleteApplication(application.id(), Optional.empty()); + } catch (RuntimeException e) { + log.info("Could not removing application " + application.id() + ": " + e.getMessage()); + success = false; + } + } + return success; } } 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 5ecf28ce7e6..8022067b29f 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 @@ -9,7 +9,6 @@ import com.yahoo.vespa.hosted.controller.ControllerTester; import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; -import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo; import com.yahoo.vespa.hosted.controller.tenant.Tenant; import org.junit.jupiter.api.Test; @@ -32,28 +31,28 @@ public class CloudTrialExpirerTest { @Test void expire_inactive_tenant() { registerTenant("trial-tenant", "trial", Duration.ofDays(14).plusMillis(1)); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("trial-tenant", "none"); } @Test void tombstone_inactive_none() { registerTenant("none-tenant", "none", Duration.ofDays(365).plusMillis(1)); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertEquals(Tenant.Type.deleted, tester.controller().tenants().get(TenantName.from("none-tenant"), true).get().type()); } @Test void keep_inactive_nontrial_tenants() { registerTenant("not-a-trial-tenant", "pay-as-you-go", Duration.ofDays(30)); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("not-a-trial-tenant", "pay-as-you-go"); } @Test void keep_active_trial_tenants() { registerTenant("active-trial-tenant", "trial", Duration.ofHours(14).minusMillis(1)); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("active-trial-tenant", "trial"); } @@ -61,7 +60,7 @@ public class CloudTrialExpirerTest { void keep_inactive_exempt_tenants() { registerTenant("exempt-trial-tenant", "trial", Duration.ofDays(40)); ((InMemoryFlagSource) tester.controller().flagSource()).withListFlag(PermanentFlags.EXTENDED_TRIAL_TENANTS.id(), List.of("exempt-trial-tenant"), String.class); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("exempt-trial-tenant", "trial"); } @@ -69,7 +68,7 @@ public class CloudTrialExpirerTest { void keep_inactive_trial_tenants_with_deployments() { registerTenant("with-deployments", "trial", Duration.ofDays(30)); registerDeployment("with-deployments", "my-app", "default"); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("with-deployments", "trial"); } @@ -77,16 +76,16 @@ public class CloudTrialExpirerTest { void delete_tenants_with_applications_with_no_deployments() { registerTenant("with-apps", "trial", Duration.ofDays(366)); tester.createApplication("with-apps", "app1", "instance1"); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("with-apps", "none"); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertTrue(tester.controller().tenants().get("with-apps").isEmpty()); } @Test void keep_tenants_without_applications_that_are_idle() { registerTenant("active", "none", Duration.ofDays(364)); - expirer.maintain(); + assertEquals(1.0, expirer.maintain()); assertPlan("active", "none"); } |