Skip to content

Commit

Permalink
[RW-13428][risk=no] Added cron job for checking credit expiration (#8735
Browse files Browse the repository at this point in the history
)
  • Loading branch information
evrii authored Aug 30, 2024
1 parent 7a951e0 commit eea11b1
Show file tree
Hide file tree
Showing 27 changed files with 580 additions and 42 deletions.
3 changes: 2 additions & 1 deletion api/config/config_local.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"offlineBatch": {
"unsafeCloudTasksForwardingHost": "http:\/\/localhost:8081",
"usersPerAuditTask": 20,
"usersPerSynchronizeAccessTask": 50
"usersPerSynchronizeAccessTask": 50,
"usersPerCheckInitialCreditsExpirationTask": 50
},
"egressAlertRemediationPolicy": {
"enableJiraTicketing": false,
Expand Down
3 changes: 2 additions & 1 deletion api/config/config_preprod.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@
},
"offlineBatch": {
"usersPerAuditTask": 20,
"usersPerSynchronizeAccessTask": 50
"usersPerSynchronizeAccessTask": 50,
"usersPerCheckInitialCreditsExpirationTask": 50
},
"termsOfService": {
"latestAouVersion": 1
Expand Down
3 changes: 2 additions & 1 deletion api/config/config_prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@
},
"offlineBatch": {
"usersPerAuditTask": 20,
"usersPerSynchronizeAccessTask": 50
"usersPerSynchronizeAccessTask": 50,
"usersPerCheckInitialCreditsExpirationTask": 50
},
"egressAlertRemediationPolicy": {
"enableJiraTicketing": true,
Expand Down
3 changes: 2 additions & 1 deletion api/config/config_stable.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@
},
"offlineBatch": {
"usersPerAuditTask": 20,
"usersPerSynchronizeAccessTask": 50
"usersPerSynchronizeAccessTask": 50,
"usersPerCheckInitialCreditsExpirationTask": 50
},
"egressAlertRemediationPolicy": {
"enableJiraTicketing": false,
Expand Down
3 changes: 2 additions & 1 deletion api/config/config_staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@
},
"offlineBatch": {
"usersPerAuditTask": 20,
"usersPerSynchronizeAccessTask": 50
"usersPerSynchronizeAccessTask": 50,
"usersPerCheckInitialCreditsExpirationTask": 50
},
"egressAlertRemediationPolicy": {
"enableJiraTicketing": false,
Expand Down
3 changes: 2 additions & 1 deletion api/config/config_test.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@
},
"offlineBatch": {
"usersPerAuditTask": 20,
"usersPerSynchronizeAccessTask": 50
"usersPerSynchronizeAccessTask": 50,
"usersPerCheckInitialCreditsExpirationTask": 50
},
"egressAlertRemediationPolicy": {
"enableJiraTicketing": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="evrii" id="changelog-232-add-user_initial_credits_expiration">
<addColumn tableName="user_initial_credits_expiration">
<column name="notification_status" type="ENUM('NO_NOTIFICATION_SENT','EXPIRATION_NOTIFICATION_SENT')" defaultValue="NO_NOTIFICATION_SENT">
<constraints nullable="false"/>
</column>
</addColumn>

</changeSet>
</databaseChangeLog>
1 change: 1 addition & 0 deletions api/db/changelog/db.changelog-master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
<include file="changelog/db.changelog-230-create-featured-workspace-table.xml"/>
<include file="changelog/db.changelog-231-drop-featured-workspace-description-col.xml"/>
<include file="changelog/db.changelog-232-add-user_initial_credits_expiration.xml"/>
<include file="changelog/db.changelog-233-add-user-initial-credit-expiration-notification-column.xml"/>
<!--
Note: to update the DB locally, do the following:
- Migrate schema changes: `./project.rb run-local-all-migrations`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.pmiops.workbench.db.model.DbUser;
import org.pmiops.workbench.exceptions.WorkbenchException;
import org.pmiops.workbench.google.CloudResourceManagerService;
import org.pmiops.workbench.initialcredits.InitialCreditsExpirationService;
import org.pmiops.workbench.model.AccessModuleStatus;
import org.pmiops.workbench.model.AuditProjectAccessRequest;
import org.pmiops.workbench.model.SynchronizeUserAccessRequest;
Expand Down Expand Up @@ -55,18 +56,21 @@ public class CloudTaskUserController implements CloudTaskUserApiDelegate {
private final UserService userService;
private final AccessModuleService accessModuleService;
private final FreeTierBillingBatchUpdateService freeTierBillingUpdateService;
private final InitialCreditsExpirationService initialCreditsExpirationService;

CloudTaskUserController(
UserDao userDao,
CloudResourceManagerService cloudResourceManagerService,
UserService userService,
AccessModuleService accessModuleService,
FreeTierBillingBatchUpdateService freeTierBillingUpdateService) {
FreeTierBillingBatchUpdateService freeTierBillingUpdateService,
InitialCreditsExpirationService initialCreditsExpirationService) {
this.userDao = userDao;
this.cloudResourceManagerService = cloudResourceManagerService;
this.userService = userService;
this.accessModuleService = accessModuleService;
this.freeTierBillingUpdateService = freeTierBillingUpdateService;
this.initialCreditsExpirationService = initialCreditsExpirationService;
}

@Override
Expand Down Expand Up @@ -173,6 +177,18 @@ public ResponseEntity<Void> synchronizeUserAccess(SynchronizeUserAccessRequest r
return ResponseEntity.noContent().build();
}

/**
* Takes in batch of user Ids check whether users have initial credits that have expired
*
* @param body : Batch of user IDs from cloud task queue: checkCreditsExpirationForUserIDsQueue
* @return
*/
@Override
public ResponseEntity<Void> checkCreditsExpirationForUserIDs(List<Long> userIdsList) {
initialCreditsExpirationService.checkCreditsExpirationForUserIDs(userIdsList);
return ResponseEntity.noContent().build();
}

// v1 cloudresourcemanager project.getParent() returned a ResourceId with type and id fields
// v3 returns a string instead, in the format type/id

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public ResponseEntity<Void> sendAccessExpirationEmails() {
userService.getAllUsers().forEach(userService::maybeSendAccessTierExpirationEmails);
return ResponseEntity.noContent().build();
}

public ResponseEntity<Void> checkInitialCreditsExpiration() {
taskQueueService.groupAndPushCheckInitialCreditExpirationTasks(userService.getAllUserIds());
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.pmiops.workbench.actionaudit.auditors.UserServiceAuditor;
import org.pmiops.workbench.cloudtasks.TaskQueueService;
Expand All @@ -20,6 +25,7 @@
import org.pmiops.workbench.db.model.DbUser;
import org.pmiops.workbench.db.model.DbWorkspace;
import org.pmiops.workbench.db.model.DbWorkspaceFreeTierUsage;
import org.pmiops.workbench.initialcredits.InitialCreditsExpirationService;
import org.pmiops.workbench.model.BillingStatus;
import org.pmiops.workbench.utils.CostComparisonUtils;
import org.pmiops.workbench.workspaces.WorkspaceUtils;
Expand All @@ -39,6 +45,7 @@ public class FreeTierBillingService {
private final WorkspaceDao workspaceDao;
private final WorkspaceFreeTierUsageDao workspaceFreeTierUsageDao;
private final WorkspaceFreeTierUsageService workspaceFreeTierUsageService;
private final InitialCreditsExpirationService initialCreditsExpirationService;

private static final Logger logger = LoggerFactory.getLogger(FreeTierBillingService.class);

Expand All @@ -50,14 +57,16 @@ public FreeTierBillingService(
UserServiceAuditor userServiceAuditor,
WorkspaceDao workspaceDao,
WorkspaceFreeTierUsageDao workspaceFreeTierUsageDao,
WorkspaceFreeTierUsageService workspaceFreeTierUsageService) {
WorkspaceFreeTierUsageService workspaceFreeTierUsageService,
InitialCreditsExpirationService initialCreditsExpirationService) {
this.taskQueueService = taskQueueService;
this.userDao = userDao;
this.workbenchConfigProvider = workbenchConfigProvider;
this.userServiceAuditor = userServiceAuditor;
this.workspaceDao = workspaceDao;
this.workspaceFreeTierUsageDao = workspaceFreeTierUsageDao;
this.workspaceFreeTierUsageService = workspaceFreeTierUsageService;
this.initialCreditsExpirationService = initialCreditsExpirationService;
}

public double getWorkspaceFreeTierBillingUsage(DbWorkspace dbWorkspace) {
Expand Down Expand Up @@ -193,9 +202,11 @@ public double getUserFreeTierDollarLimit(DbUser user) {
public boolean maybeSetDollarLimitOverride(DbUser user, double newDollarLimit) {
final Double previousLimitMaybe = user.getFreeTierCreditsLimitDollarsOverride();

if (previousLimitMaybe != null
|| CostComparisonUtils.costsDiffer(
newDollarLimit, workbenchConfigProvider.get().billing.defaultFreeCreditsDollarLimit)) {
if (!initialCreditsExpirationService.haveCreditsExpired(user)
&& (previousLimitMaybe != null
|| CostComparisonUtils.costsDiffer(
newDollarLimit,
workbenchConfigProvider.get().billing.defaultFreeCreditsDollarLimit))) {

// TODO: prevent setting this limit directly except in this method?
user.setFreeTierCreditsLimitDollarsOverride(newDollarLimit);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public class TaskQueueService {
private static final String DELETE_TEST_WORKSPACES_PATH = BASE_PATH + "/deleteTestUserWorkspaces";
private static final String DELETE_RAWLS_TEST_WORKSPACES_PATH =
BASE_PATH + "/deleteTestUserWorkspacesInRawls";

private static final String CHECK_CREDITS_EXPIRATION_FOR_USER_IDS_PATH =
BASE_PATH + "/checkCreditsExpirationForUserIDs";
private static final String CHECK_AND_ALERT_FREE_TIER_USAGE =
BASE_PATH + "/checkAndAlertFreeTierBillingUsage";

Expand All @@ -58,16 +59,17 @@ public class TaskQueueService {
private static final String DELETE_TEST_WORKSPACES_QUEUE_NAME = "deleteTestUserWorkspacesQueue";
private static final String DELETE_RAWLS_TEST_WORKSPACES_QUEUE_NAME =
"deleteTestUserRawlsWorkspacesQueue";

private static final String FREE_TIER_BILLING_QUEUE = "freeTierBillingQueue";

private static final String EXPIRED_FREE_CREDITS_QUEUE_NAME = "expiredFreeCreditsQueue";
private static final String CHECK_CREDITS_EXPIRATION_FOR_USER_IDS_QUEUE_NAME =
"checkCreditsExpirationForUserIDsQueue";

private static final Logger LOGGER = Logger.getLogger(TaskQueueService.class.getName());

private WorkbenchLocationConfigService locationConfigService;
private Provider<CloudTasksClient> cloudTasksClientProvider;
private Provider<WorkbenchConfig> workbenchConfigProvider;
private Provider<UserAuthentication> userAuthenticationProvider;
private final WorkbenchLocationConfigService locationConfigService;
private final Provider<CloudTasksClient> cloudTasksClientProvider;
private final Provider<WorkbenchConfig> workbenchConfigProvider;
private final Provider<UserAuthentication> userAuthenticationProvider;

public TaskQueueService(
WorkbenchLocationConfigService locationConfigService,
Expand Down Expand Up @@ -219,6 +221,18 @@ public void pushInitialCreditsExpiryTask(
.liveCostByCreator(liveCostByCreator));
}

public void groupAndPushCheckInitialCreditExpirationTasks(List<Long> userIds) {
WorkbenchConfig workbenchConfig = workbenchConfigProvider.get();
CloudTasksUtils.partitionList(
userIds, workbenchConfig.offlineBatch.usersPerCheckInitialCreditsExpirationTask)
.forEach(
batch ->
createAndPushTask(
CHECK_CREDITS_EXPIRATION_FOR_USER_IDS_QUEUE_NAME,
CHECK_CREDITS_EXPIRATION_FOR_USER_IDS_PATH,
batch));
}

private String createAndPushTask(String queueName, String taskUri, Object jsonBody) {
return createAndPushTask(queueName, taskUri, jsonBody, ImmutableMap.of());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ public static class OfflineBatchConfig {
public Integer usersPerAuditTask;
// Number of users to process within a single access synchronization task.
public Integer usersPerSynchronizeAccessTask;
// Number of users to process within a single check initial credits expiration task.
public Integer usersPerCheckInitialCreditsExpirationTask;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class DbUserInitialCreditsExpiration {
private Timestamp expirationTime;
private boolean bypassed;
private int extensionCount;
private NotificationStatus notificationStatus;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down Expand Up @@ -83,4 +84,20 @@ public DbUserInitialCreditsExpiration setExtensionCount(int extensionCount) {
this.extensionCount = extensionCount;
return this;
}

@Column(name = "notification_status")
public NotificationStatus getNotificationStatus() {
return notificationStatus;
}

public DbUserInitialCreditsExpiration setNotificationStatus(
NotificationStatus notificationStatus) {
this.notificationStatus = notificationStatus;
return this;
}

public enum NotificationStatus {
NO_NOTIFICATION_SENT,
EXPIRATION_NOTIFICATION_SENT
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package org.pmiops.workbench.initialcredits;

import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
import org.pmiops.workbench.db.model.DbUser;

public interface InitialCreditsExpirationService {
Optional<Timestamp> getCreditsExpiration(DbUser user);

void checkCreditsExpirationForUserIDs(List<Long> userIdsList);

boolean haveCreditsExpired(DbUser user);
}
Loading

0 comments on commit eea11b1

Please sign in to comment.