summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@vespa.ai>2023-10-13 15:44:47 +0200
committerBjørn Christian Seime <bjorncs@vespa.ai>2023-10-16 10:23:06 +0200
commitce9556b4cdb280e621f3710b93755469c5e3627a (patch)
tree3606d206b30e722a402c7893872fd7e438b099ab
parent4e98a3c331bb2b036c7a64f40fbf4251063294c1 (diff)
Add prototype email notification for trial account events
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java96
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java1
-rw-r--r--controller-server/src/main/resources/mail/cloud-trial-notification.vm3
3 files changed, 98 insertions, 2 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 6c50afe7fb2..ed8cd3f54f8 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
@@ -7,20 +7,33 @@ import com.yahoo.vespa.flags.ListFlag;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
+import com.yahoo.vespa.hosted.controller.notification.Notification;
+import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
+import com.yahoo.vespa.hosted.controller.persistence.TrialNotifications;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
+import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
+import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRED;
+import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRES_IMMEDIATELY;
+import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRES_SOON;
+import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.MID_CHECK_IN;
+import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.SIGNED_UP;
+import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.UNKNOWN;
+
/**
* Expires unused tenants from Vespa Cloud.
* <p>
- * TODO: Should support sending notifications some time before the various expiry events happen.
*
* @author ogronnesby
*/
@@ -40,7 +53,8 @@ public class CloudTrialExpirer extends ControllerMaintainer {
protected double maintain() {
var a = tombstoneNonePlanTenants();
var b = moveInactiveTenantsToNonePlan();
- return (a ? 0.0 : -0.5) + (b ? 0.0 : -0.5);
+ var c = notifyTenants();
+ return (a ? 0.0 : -(1D/3)) + (b ? 0.0 : -(1D/3) + (c ? 0.0 : -(1D/3)));
}
private boolean moveInactiveTenantsToNonePlan() {
@@ -76,6 +90,84 @@ public class CloudTrialExpirer extends ControllerMaintainer {
return tombstoneTenants(idleOldPlanTenants);
}
+ private boolean notifyTenants() {
+ try {
+ // TODO Introduce tenant specific feature flag
+
+ var currentStatus = controller().curator().readTrialNotifications()
+ .map(TrialNotifications::tenants).orElse(List.of());
+ log.fine(() -> "Current: %s".formatted(currentStatus));
+ var currentStatusByTenant = new HashMap<TenantName, TrialNotifications.Status>();
+ currentStatus.forEach(status -> currentStatusByTenant.put(status.tenant(), status));
+ var updatedStatus = new ArrayList<TrialNotifications.Status>();
+ var now = controller().clock().instant();
+
+ for (var tenant : controller().tenants().asList()) {
+ var status = currentStatusByTenant.get(tenant.name());
+ var state = status == null ? UNKNOWN : status.state();
+ var plan = controller().serviceRegistry().billingController().getPlan(tenant.name()).value();
+ var ageInDays = Duration.between(tenant.createdAt(), now).toDays();
+
+ // TODO Replace stubs with proper email content stored in templates.
+ if (!List.of("none", "trial").contains(plan)) {
+ // Ignore tenants that are on a paid plan and skip from inclusion in updated data structure
+ } else if (status == null && "trial".equals(plan) && ageInDays <= 1) {
+ updatedStatus.add(updatedStatus(tenant, now, SIGNED_UP));
+ queueNotification(tenant, "Welcome to Vespa Cloud", "Welcome to Vespa Cloud",
+ "Welcome to Vespa Cloud! We hope you will enjoy your trial. " +
+ "Please reach out to us if you have any questions or feedback.");
+ } else if ("none".equals(plan) && !List.of(EXPIRED).contains(state)) {
+ updatedStatus.add(updatedStatus(tenant, now, EXPIRED));
+ queueNotification(tenant, "Your Vespa Cloud trial has expired", "Your Vespa Cloud trial has expired",
+ "Your Vespa Cloud trial has expired. " +
+ "Please reach out to us if you have any questions or feedback.");
+ } else if ("trial".equals(plan) && ageInDays >= 13
+ && !List.of(EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) {
+ updatedStatus.add(updatedStatus(tenant, now, EXPIRES_IMMEDIATELY));
+ queueNotification(tenant, "Your Vespa Cloud trial expires tomorrow", "Your Vespa Cloud trial expires tomorrow",
+ "Your Vespa Cloud trial expires tomorrow. " +
+ "Please reach out to us if you have any questions or feedback.");
+ } else if ("trial".equals(plan) && ageInDays >= 12
+ && !List.of(EXPIRES_SOON, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) {
+ updatedStatus.add(updatedStatus(tenant, now, EXPIRES_SOON));
+ queueNotification(tenant, "Your Vespa Cloud trial expires in 2 days", "Your Vespa Cloud trial expires in 2 days",
+ "Your Vespa Cloud trial expires in 2 days. " +
+ "Please reach out to us if you have any questions or feedback.");
+ } else if ("trial".equals(plan) && ageInDays >= 7
+ && !List.of(MID_CHECK_IN, EXPIRES_SOON, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) {
+ updatedStatus.add(updatedStatus(tenant, now, MID_CHECK_IN));
+ queueNotification(tenant, "How is your Vespa Cloud trial going?", "How is your Vespa Cloud trial going?",
+ "How is your Vespa Cloud trial going? " +
+ "Please reach out to us if you have any questions or feedback.");
+ } else {
+ updatedStatus.add(status);
+ }
+ }
+ log.fine(() -> "Updated: %s".formatted(updatedStatus));
+ controller().curator().writeTrialNotifications(new TrialNotifications(updatedStatus));
+ return true;
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Failed to process trial notifications", e);
+ return false;
+ }
+ }
+
+ private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, String emailMsg) {
+ var mail = Optional.of(Notification.MailContent.fromTemplate("default-mail-content")
+ .subject(emailSubject)
+ .with("mailMessageTemplate", "cloud-trial-notification")
+ .with("cloudTrialMessage", emailMsg)
+ .build());
+ var source = NotificationSource.from(tenant.name());
+ // Remove previous notification to ensure new notification is sent by email
+ controller().notificationsDb().removeNotification(source, Notification.Type.account);
+ controller().notificationsDb().setNotification(
+ source, Notification.Type.account, Notification.Level.info, List.of(consoleMsg), mail);
+ }
+
+ private static TrialNotifications.Status updatedStatus(Tenant t, Instant i, TrialNotifications.State s) {
+ return new TrialNotifications.Status(t.name(), s, i);
+ }
private boolean tenantIsCloudTenant(Tenant tenant) {
return tenant.type() == Tenant.Type.cloud;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
index c1e1f075552..6468a4c397b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
@@ -72,6 +72,7 @@ public class Notifier {
registerTemplate(repo, "mail");
registerTemplate(repo, "default-mail-content");
registerTemplate(repo, "notification-message");
+ registerTemplate(repo, "cloud-trial-notification");
return v;
}
diff --git a/controller-server/src/main/resources/mail/cloud-trial-notification.vm b/controller-server/src/main/resources/mail/cloud-trial-notification.vm
new file mode 100644
index 00000000000..27bc9b1ad1b
--- /dev/null
+++ b/controller-server/src/main/resources/mail/cloud-trial-notification.vm
@@ -0,0 +1,3 @@
+<p>
+ $esc.html($cloudTrialMessage):
+</p> \ No newline at end of file