From 5191c9c018174fdc0de67ef11f92b72ed3cab5fd Mon Sep 17 00:00:00 2001 From: Joel Thibault Date: Tue, 11 Feb 2025 16:33:22 -0500 Subject: [PATCH 1/2] rename expiration to exhaustion where appropriate initialCreditsExhaustionQueue and ExhaustedInitialCreditsEventRequest; --- ...askInitialCreditsExhaustionController.java | 4 +- .../cloudtasks/TaskQueueService.java | 8 +-- api/src/main/resources/workbench-api.yaml | 16 +++-- api/src/main/webapp/WEB-INF/cron_base.yaml | 2 +- api/src/main/webapp/WEB-INF/cron_test.yaml | 2 +- api/src/main/webapp/WEB-INF/queue.yaml | 2 +- ...nitialCreditsExhaustionControllerTest.java | 70 +++++++++---------- .../workbench/api/UserControllerTest.java | 6 +- 8 files changed, 55 insertions(+), 55 deletions(-) diff --git a/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java b/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java index b914fac224e..8409c88bea8 100644 --- a/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java +++ b/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java @@ -24,7 +24,7 @@ import org.pmiops.workbench.exceptions.WorkbenchException; import org.pmiops.workbench.leonardo.LeonardoApiClient; import org.pmiops.workbench.mail.MailService; -import org.pmiops.workbench.model.ExpiredInitialCreditsEventRequest; +import org.pmiops.workbench.model.ExhaustedInitialCreditsEventRequest; import org.pmiops.workbench.utils.CostComparisonUtils; import org.pmiops.workbench.workspaces.WorkspaceService; import org.slf4j.Logger; @@ -64,7 +64,7 @@ public class CloudTaskInitialCreditsExhaustionController @SuppressWarnings("unchecked") @Override public ResponseEntity handleInitialCreditsExhaustionBatch( - ExpiredInitialCreditsEventRequest request) { + ExhaustedInitialCreditsEventRequest request) { if (request.getUsers().isEmpty()) { logger.warn("users are empty"); diff --git a/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java b/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java index 24a74056315..9b6794de219 100644 --- a/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java +++ b/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java @@ -20,7 +20,7 @@ import org.pmiops.workbench.exceptions.BadRequestException; import org.pmiops.workbench.model.CreateWorkspaceTaskRequest; import org.pmiops.workbench.model.DuplicateWorkspaceTaskRequest; -import org.pmiops.workbench.model.ExpiredInitialCreditsEventRequest; +import org.pmiops.workbench.model.ExhaustedInitialCreditsEventRequest; import org.pmiops.workbench.model.ProcessEgressEventRequest; import org.pmiops.workbench.model.TestUserRawlsWorkspace; import org.pmiops.workbench.model.TestUserWorkspace; @@ -62,7 +62,7 @@ public class TaskQueueService { 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 INITIAL_CREDITS_EXHAUSTION_QUEUE = "initialCreditsExhaustionQueue"; private static final String CHECK_CREDITS_EXPIRATION_FOR_USER_IDS_QUEUE_NAME = "checkCreditsExpirationForUserIDsQueue"; private static final String DELETE_WORKSPACE_ENVIRONMENTS_QUEUE_NAME = @@ -235,9 +235,9 @@ public void pushDuplicateWorkspaceTask( public void pushInitialCreditsExhaustionTask( List users, Map dbCostByCreator, Map liveCostByCreator) { createAndPushTask( - EXPIRED_FREE_CREDITS_QUEUE_NAME, + INITIAL_CREDITS_EXHAUSTION_QUEUE, INITIAL_CREDITS_EXHAUSTION_PATH, - new ExpiredInitialCreditsEventRequest() + new ExhaustedInitialCreditsEventRequest() .users(users) .dbCostByCreator(dbCostByCreator) .liveCostByCreator(liveCostByCreator)); diff --git a/api/src/main/resources/workbench-api.yaml b/api/src/main/resources/workbench-api.yaml index ce786259e1c..71328de1a89 100644 --- a/api/src/main/resources/workbench-api.yaml +++ b/api/src/main/resources/workbench-api.yaml @@ -4377,19 +4377,20 @@ paths: - cloudTaskInitialCreditExhaustion - cloudTask description: | - Run the business logic to handle users who have consumed or about to consume their free credits. Including disabling their workspaces and sending them notifications. + Run the business logic to handle users who have consumed or about to consume their + initial credits, including disabling their workspaces and sending them notifications. operationId: handleInitialCreditsExhaustionBatch security: [ ] requestBody: - description: Users that should be alerted about their free credits use. + description: Users that should be alerted about their initial credits use. content: application/json: schema: - $ref: '#/components/schemas/ExpiredInitialCreditsEventRequest' + $ref: '#/components/schemas/ExhaustedInitialCreditsEventRequest' required: true responses: 200: - description: Handle Free Credits Exhaustion Task Ran Successfully + description: Handle Initial Credits Exhaustion Task Ran Successfully content: { } x-codegen-request-body-name: request /v1/cloudTask/checkCreditsExpirationForUserIDs: @@ -4398,7 +4399,8 @@ paths: - cloudTaskUser - cloudTask description: | - Run the business logic to handle users whose free credits have expired. Including disabling compute, deleting cloud environments, and sending them notifications. + Run the business logic to handle users whose free credits have expired, including disabling compute, deleting + cloud environments, and sending them notifications. operationId: checkCreditsExpirationForUserIDsBatch security: [ ] requestBody: @@ -9969,14 +9971,14 @@ components: items: type: string description: Institution-specific access tier membership requirement and configs. - ExpiredInitialCreditsEventRequest: + ExhaustedInitialCreditsEventRequest: required: - users type: object properties: users: type: array - description: List of User IDs to check for free credits + description: List of User IDs to check for initial credits items: type: integer format: int64 diff --git a/api/src/main/webapp/WEB-INF/cron_base.yaml b/api/src/main/webapp/WEB-INF/cron_base.yaml index df636d0f9d1..c3dcb8f4705 100644 --- a/api/src/main/webapp/WEB-INF/cron_base.yaml +++ b/api/src/main/webapp/WEB-INF/cron_base.yaml @@ -80,7 +80,7 @@ cron: schedule: every day 21:00 timezone: America/Chicago target: api -- description: Find and alert users whose initial credits have expired using cloud task +- description: Find and alert users whose initial credits have expired using cloud tasks url: /v1/cron/checkInitialCreditsExpiration schedule: 1st mon of sep 17:00 timezone: UTC diff --git a/api/src/main/webapp/WEB-INF/cron_test.yaml b/api/src/main/webapp/WEB-INF/cron_test.yaml index ca3ab15e221..fcaf7281900 100644 --- a/api/src/main/webapp/WEB-INF/cron_test.yaml +++ b/api/src/main/webapp/WEB-INF/cron_test.yaml @@ -3,7 +3,7 @@ cron: schedule: every 24 hours - url: /v1/cron/checkFreeTierBillingUsage schedule: every 24 hours -- description: Find and alert users whose initial credits have expired using cloud task +- description: Find and alert users whose initial credits have expired using cloud tasks url: /v1/cron/checkInitialCreditsExpiration schedule: every 24 hours timezone: UTC diff --git a/api/src/main/webapp/WEB-INF/queue.yaml b/api/src/main/webapp/WEB-INF/queue.yaml index 4347b47df3d..71cbc59b14e 100644 --- a/api/src/main/webapp/WEB-INF/queue.yaml +++ b/api/src/main/webapp/WEB-INF/queue.yaml @@ -105,7 +105,7 @@ queue: task_retry_limit: 1 task_age_limit: 5m -- name: expiredFreeCreditsQueue +- name: initialCreditsExhaustionQueue target: api # rate parameters diff --git a/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java index a0ee0852d5a..28e11c4182a 100644 --- a/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java @@ -23,7 +23,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,7 +53,7 @@ import org.pmiops.workbench.institution.InstitutionService; import org.pmiops.workbench.leonardo.LeonardoApiClient; import org.pmiops.workbench.mail.MailService; -import org.pmiops.workbench.model.ExpiredInitialCreditsEventRequest; +import org.pmiops.workbench.model.ExhaustedInitialCreditsEventRequest; import org.pmiops.workbench.model.WorkspaceActiveStatus; import org.pmiops.workbench.test.FakeClock; import org.pmiops.workbench.utils.mappers.FeaturedWorkspaceMapper; @@ -180,8 +179,8 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_whenDollarThres allBQCosts.put(String.valueOf(user.getUserId()), costUnderThreshold); // check that we have not alerted before the threshold - ExpiredInitialCreditsEventRequest request = - buildExpiredInitialCreditsEventRequest(List.of(user), allBQCosts, allDbCosts); + ExhaustedInitialCreditsEventRequest request = + buildExhaustedInitialCreditsEventRequest(List.of(user), allBQCosts, allDbCosts); controller.handleInitialCreditsExhaustionBatch(request); verifyNoInteractions(mailService); @@ -257,8 +256,8 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_whenDollarThres Map allBQCosts = Maps.newHashMap(); allBQCosts.put(String.valueOf(user.getUserId()), costUnderThreshold); - ExpiredInitialCreditsEventRequest request = - buildExpiredInitialCreditsEventRequest(List.of(user), allBQCosts, allDbCosts); + ExhaustedInitialCreditsEventRequest request = + buildExhaustedInitialCreditsEventRequest(List.of(user), allBQCosts, allDbCosts); controller.handleInitialCreditsExhaustionBatch(request); verifyNoInteractions(mailService); @@ -326,8 +325,8 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_evenWhenUserIsD final DbWorkspace workspace = createWorkspace(user, SINGLE_WORKSPACE_TEST_PROJECT); - ExpiredInitialCreditsEventRequest request = - buildExpiredInitialCreditsEventRequest( + ExhaustedInitialCreditsEventRequest request = + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d)); controller.handleInitialCreditsExhaustionBatch(request); @@ -349,8 +348,8 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_evenWhenWorkspa Map allBQCosts = Maps.newHashMap(); allBQCosts.put(String.valueOf(user.getUserId()), 100.01); - ExpiredInitialCreditsEventRequest request = - buildExpiredInitialCreditsEventRequest( + ExhaustedInitialCreditsEventRequest request = + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d)); controller.handleInitialCreditsExhaustionBatch(request); @@ -370,8 +369,8 @@ public void handleInitialCreditsExpiry_noAlert_ifCostIsBelowLowestThreshold() { Map allBQCosts = Maps.newHashMap(); allBQCosts.put(String.valueOf(user.getUserId()), 49.99); - ExpiredInitialCreditsEventRequest request = - buildExpiredInitialCreditsEventRequest( + ExhaustedInitialCreditsEventRequest request = + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d)); controller.handleInitialCreditsExhaustionBatch(request); @@ -390,8 +389,8 @@ public void handleInitialCreditsExpiry_doesntThrowNPE_whenWorkspaceIsMissingCrea // set limit so usage is just under the 50% threshold Map allBQCosts = Map.of(String.valueOf(user.getUserId()), 49.99); - ExpiredInitialCreditsEventRequest request = - buildExpiredInitialCreditsEventRequest( + ExhaustedInitialCreditsEventRequest request = + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d)); controller.handleInitialCreditsExhaustionBatch(request); @@ -411,7 +410,7 @@ public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenTo allBQCosts.put(String.valueOf(user.getUserId()), 150.0); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); @@ -422,7 +421,7 @@ public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenTo assertThat(workspace.isInitialCreditsExhausted()).isEqualTo(false); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 150.0))); verifyNoMoreInteractions(mailService); @@ -445,7 +444,7 @@ public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenTo allBQCosts.put(String.valueOf(user.getUserId()), 300.0); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); assertThat(workspace.isInitialCreditsExhausted()).isEqualTo(true); @@ -454,7 +453,7 @@ public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenTo assertThat(workspace.isInitialCreditsExhausted()).isEqualTo(true); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 300.0))); verifyNoMoreInteractions(mailService); assertThat(workspace.isInitialCreditsExhausted()).isEqualTo(true); @@ -479,7 +478,7 @@ public void handleInitialCreditsExpiry_disabledAllWorkspaces_ifTheCombinedCostEx allBQCosts.put(String.valueOf(user.getUserId()), sum); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); @@ -512,7 +511,7 @@ public void handleInitialCreditsExpiry_alertsAllUsers_ifTheyExceedFreeTierLimit( allBQCosts.put(String.valueOf(user2.getUserId()), cost2); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user1, user2), allBQCosts, Map.of(String.valueOf(user1.getUserId()), 0d, String.valueOf(user2.getUserId()), 0d))); @@ -538,7 +537,7 @@ public void handleInitialCreditsExpiry_alertsOnlyOnce_ifCostKeepIncreasingAboveT allBQCosts.put(String.valueOf(user.getUserId()), 100.01); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); assertThat(workspace.isInitialCreditsExhausted()).isEqualTo(true); @@ -550,7 +549,7 @@ public void handleInitialCreditsExpiry_alertsOnlyOnce_ifCostKeepIncreasingAboveT // we do not alert again controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 100.01))); verify(mailService, times(1)).alertUserInitialCreditsExhausted(eq(user)); @@ -571,7 +570,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t allBQCosts.put(String.valueOf(user.getUserId()), 100.01); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); @@ -579,7 +578,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t workspaceDao.save(workspace.setBillingAccountName(fullBillingAccountName("byo-account"))); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 100.01))); verifyNoMoreInteractions(mailService); } @@ -603,7 +602,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t Map allBQCosts = Map.of(String.valueOf(user.getUserId()), 100.01); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); @@ -629,14 +628,14 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t workspaceDao.save(workspace); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verifyNoInteractions(mailService); allBQCosts = Map.of(String.valueOf(user.getUserId()), 100.1); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 50.0))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); } @@ -657,7 +656,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t workspaceDao.save(workspace); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 0d))); verifyNoInteractions(mailService); @@ -667,7 +666,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t allBQCosts.put(String.valueOf(user.getUserId()), 100.1); controller.handleInitialCreditsExhaustionBatch( - buildExpiredInitialCreditsEventRequest( + buildExhaustedInitialCreditsEventRequest( List.of(user), allBQCosts, Map.of(String.valueOf(user.getUserId()), 50.0))); verify(mailService).alertUserInitialCreditsExhausted(eq(user)); @@ -688,7 +687,7 @@ public void handleInitialCreditsExpiry_withMissingUsersInRequest_NoNPE() throws createWorkspace(user2, SINGLE_WORKSPACE_TEST_PROJECT); createWorkspace(user3, SINGLE_WORKSPACE_TEST_PROJECT); - ExpiredInitialCreditsEventRequest request = new ExpiredInitialCreditsEventRequest(); + ExhaustedInitialCreditsEventRequest request = new ExhaustedInitialCreditsEventRequest(); request.setUsers(Arrays.asList(user1.getUserId(), user2.getUserId())); Map liveCostByCreator = new HashMap<>(); @@ -732,12 +731,11 @@ private DbWorkspace createWorkspace(DbUser creator, String project) { } @NotNull - private static ExpiredInitialCreditsEventRequest buildExpiredInitialCreditsEventRequest( + private static ExhaustedInitialCreditsEventRequest buildExhaustedInitialCreditsEventRequest( List users, Map allBQCosts, Map dbCostByCreator) { - ExpiredInitialCreditsEventRequest request = new ExpiredInitialCreditsEventRequest(); - request.setUsers(users.stream().map(DbUser::getUserId).collect(Collectors.toList())); - request.setLiveCostByCreator(allBQCosts); - request.setDbCostByCreator(dbCostByCreator); - return request; + return new ExhaustedInitialCreditsEventRequest() + .users(users.stream().map(DbUser::getUserId).toList()) + .liveCostByCreator(allBQCosts) + .dbCostByCreator(dbCostByCreator); } } diff --git a/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java index 5a7800d0bba..df66cb85eff 100644 --- a/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java @@ -349,7 +349,7 @@ public void testUserSort() { } // Combinatorial tests for listBillingAccounts: - // free tier available vs. expired + // free tier available vs. exhausted // cloud accounts available vs. none static final String INITIAL_CREDITS_ID = "free-tier"; @@ -423,7 +423,7 @@ public void listBillingAccounts_upgradeYES_freeYES_cloudNO() throws IOException assertThat(response.getBillingAccounts()).isEqualTo(expectedWorkbenchBillingAccounts); } - // billing upgrade is true, free tier is expired, cloud accounts exist + // billing upgrade is true, free tier is exhausted, cloud accounts exist @Test public void listBillingAccounts_upgradeYES_freeNO_cloudYES() throws IOException { @@ -441,7 +441,7 @@ public void listBillingAccounts_upgradeYES_freeNO_cloudYES() throws IOException assertThat(response.getBillingAccounts()).isEqualTo(expectedWorkbenchBillingAccounts); } - // billing upgrade is true, free tier is expired, no cloud accounts + // billing upgrade is true, free tier is exhausted, no cloud accounts @Test public void listBillingAccounts_upgradeYES_freeNO_cloudNO() throws IOException { From d5f25929f3a7777af0df4c7cc979dc38e23715e1 Mon Sep 17 00:00:00 2001 From: Joel Thibault Date: Fri, 7 Feb 2025 14:54:42 -0500 Subject: [PATCH 2/2] initial credits renaming --- .../auditors/UserServiceAuditor.java | 2 +- .../auditors/UserServiceAuditorImpl.java | 4 +- .../AccountTargetProperty.java | 2 +- ...askInitialCreditsExhaustionController.java | 88 ++++---- .../api/CloudTaskUserController.java | 11 +- .../api/OfflineBillingController.java | 26 +-- .../api/OfflineEnvironmentsController.java | 2 +- .../workbench/api/OfflineUserController.java | 8 +- .../pmiops/workbench/api/UserController.java | 9 +- .../workbench/api/WorkspacesController.java | 14 +- .../cloudtasks/TaskQueueService.java | 6 +- .../workbench/config/WorkbenchConfig.java | 10 +- .../pmiops/workbench/db/dao/WorkspaceDao.java | 8 +- .../org/pmiops/workbench/db/model/DbUser.java | 14 +- .../InitialCreditsBatchUpdateService.java | 87 ++------ .../initialcredits/InitialCreditsService.java | 188 +++++++++--------- .../WorkspaceInitialCreditUsageService.java | 19 +- .../workbench/profile/ProfileService.java | 4 +- .../workbench/utils/CostComparisonUtils.java | 16 +- .../workspaces/WorkspaceAuthService.java | 2 +- .../workspaces/WorkspaceServiceImpl.java | 2 +- api/src/main/webapp/WEB-INF/queue.yaml | 2 +- .../auditors/UserServiceAuditorTest.java | 12 +- ...nitialCreditsExhaustionControllerTest.java | 46 +++-- .../api/CloudTaskUserControllerTest.java | 8 +- .../api/OfflineBillingControllerTest.java | 21 +- .../OfflineEnvironmentsControllerTest.java | 2 +- .../workbench/api/ProfileControllerTest.java | 8 +- .../workbench/api/UserControllerTest.java | 28 +-- .../api/WorkspacesControllerTest.java | 6 +- .../InitialCreditsBatchUpdateServiceTest.java | 4 +- .../InitialCreditsServiceTest.java | 160 ++++++++------- .../fixtures/ReportingUserFixture.java | 2 +- .../ArgumentMatchers.java} | 8 +- 34 files changed, 376 insertions(+), 453 deletions(-) rename api/src/test/java/org/pmiops/workbench/{initialcredits/InitialCreditsExpiryTaskMatchers.java => utils/ArgumentMatchers.java} (77%) diff --git a/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditor.java b/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditor.java index 3c66a6bbc6d..a8a75afdab4 100644 --- a/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditor.java +++ b/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditor.java @@ -24,6 +24,6 @@ void fireAdministrativeBypassTime( void fireAcknowledgeTermsOfService(DbUser targetUser, Integer termsOfServiceVersion); - void fireSetFreeTierDollarLimitOverride( + void fireSetInitialCreditsOverride( Long targetUserId, @Nullable Double previousDollarQuota, @Nullable Double newDollarQuota); } diff --git a/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorImpl.java b/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorImpl.java index 0e65dd8c867..121afd11bda 100644 --- a/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorImpl.java +++ b/api/src/main/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorImpl.java @@ -117,7 +117,7 @@ public void fireAcknowledgeTermsOfService(DbUser targetUser, Integer termsOfServ } @Override - public void fireSetFreeTierDollarLimitOverride( + public void fireSetInitialCreditsOverride( Long targetUserId, @Nullable Double previousDollarQuota, @Nullable Double newDollarQuota) { DbUser adminUser = dbUserProvider.get(); Builder builder = @@ -129,7 +129,7 @@ public void fireSetFreeTierDollarLimitOverride( .actionId(actionIdProvider.get()) .actionType(ActionType.EDIT) .targetType(TargetType.ACCOUNT) - .targetPropertyMaybe(AccountTargetProperty.FREE_TIER_DOLLAR_QUOTA.getPropertyName()) + .targetPropertyMaybe(AccountTargetProperty.INITIAL_CREDITS_OVERRIDE.getPropertyName()) .targetIdMaybe(targetUserId); if (previousDollarQuota != null) { diff --git a/api/src/main/java/org/pmiops/workbench/actionaudit/targetproperties/AccountTargetProperty.java b/api/src/main/java/org/pmiops/workbench/actionaudit/targetproperties/AccountTargetProperty.java index 1752aadcadb..516ccbe3b38 100644 --- a/api/src/main/java/org/pmiops/workbench/actionaudit/targetproperties/AccountTargetProperty.java +++ b/api/src/main/java/org/pmiops/workbench/actionaudit/targetproperties/AccountTargetProperty.java @@ -5,7 +5,7 @@ public enum AccountTargetProperty implements SimpleTargetProperty { IS_ENABLED("is_enabled"), ACKNOWLEDGED_TOS_VERSION("acknowledged_tos_version"), - FREE_TIER_DOLLAR_QUOTA("free_tier_dollar_quota"), + INITIAL_CREDITS_OVERRIDE("initial_credits_override"), ACCESS_TIERS("access_tiers"); private final String propertyName; diff --git a/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java b/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java index 8409c88bea8..783af28ea84 100644 --- a/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java +++ b/api/src/main/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionController.java @@ -1,7 +1,7 @@ package org.pmiops.workbench.api; import static org.pmiops.workbench.utils.BillingUtils.isInitialCredits; -import static org.pmiops.workbench.utils.CostComparisonUtils.getUserFreeTierDollarLimit; +import static org.pmiops.workbench.utils.CostComparisonUtils.getUserInitialCreditsLimit; import com.google.common.collect.Sets; import jakarta.inject.Provider; @@ -39,26 +39,26 @@ public class CloudTaskInitialCreditsExhaustionController private static final Logger logger = LoggerFactory.getLogger(CloudTaskInitialCreditsExhaustionController.class); - private final WorkspaceDao workspaceDao; - private final WorkspaceService workspaceService; - private final UserDao userDao; - private final Provider workbenchConfig; private final LeonardoApiClient leonardoApiClient; private final MailService mailService; + private final Provider workbenchConfig; + private final UserDao userDao; + private final WorkspaceDao workspaceDao; + private final WorkspaceService workspaceService; CloudTaskInitialCreditsExhaustionController( - WorkspaceDao workspaceDao, - WorkspaceService workspaceService, - UserDao userDao, - Provider workbenchConfig, LeonardoApiClient leonardoApiClient, - MailService mailService) { - this.workspaceDao = workspaceDao; - this.workspaceService = workspaceService; - this.userDao = userDao; - this.workbenchConfig = workbenchConfig; + MailService mailService, + Provider workbenchConfig, + UserDao userDao, + WorkspaceDao workspaceDao, + WorkspaceService workspaceService) { this.leonardoApiClient = leonardoApiClient; this.mailService = mailService; + this.userDao = userDao; + this.workbenchConfig = workbenchConfig; + this.workspaceDao = workspaceDao; + this.workspaceService = workspaceService; } @SuppressWarnings("unchecked") @@ -85,11 +85,12 @@ public ResponseEntity handleInitialCreditsExhaustionBatch( Map stringKeyLiveCostMap = (Map) request.getLiveCostByCreator(); Map liveCostByCreator = convertMapKeysToLong(stringKeyLiveCostMap); - var newlyExpiredUsers = getNewlyExpiredUsers(usersSet, dbCostByCreator, liveCostByCreator); + var newlyExhaustedUsers = getNewlyExhaustedUsers(usersSet, dbCostByCreator, liveCostByCreator); - handleExpiredUsers(newlyExpiredUsers); + handleExhaustedUsers(newlyExhaustedUsers); - alertUsersBasedOnTheThreshold(usersSet, dbCostByCreator, liveCostByCreator, newlyExpiredUsers); + alertUsersBasedOnTheThreshold( + usersSet, dbCostByCreator, liveCostByCreator, newlyExhaustedUsers); logger.info( "handleInitialCreditsExhaustionBatch: Finished processing request for users: {}", @@ -98,19 +99,20 @@ public ResponseEntity handleInitialCreditsExhaustionBatch( return ResponseEntity.noContent().build(); } - private void handleExpiredUsers(Set newlyExpiredUsers) { - newlyExpiredUsers.forEach( + private void handleExhaustedUsers(Set newlyExhaustedUsers) { + newlyExhaustedUsers.forEach( user -> { logger.info( - "Free tier Billing Service: handling user with expired credits {}", + "handleInitialCreditsExhaustionBatch: handling user with exhausted credits {}", user.getUsername()); workspaceService.updateInitialCreditsExhaustion(user, true); // delete apps and runtimes - deleteAppsAndRuntimesInFreeTierWorkspaces(user); + deleteAppsAndRuntimesInInitialCreditsWorkspaces(user); try { mailService.alertUserInitialCreditsExhausted(user); } catch (MessagingException e) { - logger.warn("failed to send free tier expiration email to {}", user.getUsername(), e); + logger.warn( + "failed to send initial credits exhaustion email to {}", user.getUsername(), e); } }); } @@ -119,7 +121,7 @@ private void alertUsersBasedOnTheThreshold( Set users, Map dbCostByCreator, Map liveCostByCreator, - Set newlyExpiredUsers) { + Set newlyExhaustedUsers) { final List costThresholdsInDescOrder = workbenchConfig.get().billing.freeTierCostAlertThresholds; costThresholdsInDescOrder.sort(Comparator.reverseOrder()); @@ -129,10 +131,10 @@ private void alertUsersBasedOnTheThreshold( .filter(u -> liveCostByCreator.containsKey(u.getUserId())) .collect(Collectors.toMap(DbUser::getUserId, Function.identity())); - // Filter out the users who have recently expired because we already alerted them + // Filter out the users who have recently exhausted because we already alerted them Map filteredLiveCostByCreator = liveCostByCreator.entrySet().stream() - .filter(entry -> !newlyExpiredUsers.contains(usersCache.get(entry.getKey()))) + .filter(entry -> !newlyExhaustedUsers.contains(usersCache.get(entry.getKey()))) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); logger.info("Handling cost alerts for users: {}", usersCache.keySet()); @@ -153,29 +155,30 @@ private void alertUsersBasedOnTheThreshold( } /** - * Get the list of newly expired users (who exceeded their free tier limit) and mark all their - * workspaces as inactive + * Get the list of newly exhausted users (those who exceeded their initial credits limit) and mark + * all their workspaces as inactive * - * @param allUsers set of all users to filter them whether they have active free tier workspace + * @param allUsers set of all users to filter whether they have active initial credits workspaces * @param dbCostByCreator Map of userId->dbCost * @param liveCostByCreator Map of userId->liveCost - * @return a {@link Set} of newly expired users + * @return a {@link Set} of newly exhausted users */ - private Set getNewlyExpiredUsers( + private Set getNewlyExhaustedUsers( final Set allUsers, Map dbCostByCreator, Map liveCostByCreator) { final Map dbUsersWithChangedCosts = findDbUsersWithChangedCosts(allUsers, dbCostByCreator, liveCostByCreator); - Set freeTierUsers = getFreeTierActiveWorkspaceCreatorsIn(allUsers); + Set creatorsWithInitialCredits = + filterToWorkspaceCreatorsWithActiveInitialCredits(allUsers); - // Find users who exceeded their free tier limit + // Find users who exceeded their initial credits limit // Here costs in liveCostByCreator could be outdated because we're filtering on active or // recently deleted workspaces in previous steps. // However, dbCostByCreator will contain the up-to-date costs for all the // other workspaces. This is why Math.max is used - final Set expiredUsers = + final Set exhaustedUsers = dbUsersWithChangedCosts.entrySet().stream() .filter( e -> @@ -187,14 +190,15 @@ private Set getNewlyExpiredUsers( .map(Map.Entry::getValue) .collect(Collectors.toSet()); - final Set newlyExpiredFreeTierUsers = Sets.intersection(expiredUsers, freeTierUsers); + final Set newlyExhaustedCreatorsWithInitialCredits = + Sets.intersection(exhaustedUsers, creatorsWithInitialCredits); logger.info( String.format( - "Found %d users exceeding their free tier limit, out of which, %d are new", - expiredUsers.size(), newlyExpiredFreeTierUsers.size())); + "Found %d users exceeding their initial credits limit, out of which, %d are new", + exhaustedUsers.size(), newlyExhaustedCreatorsWithInitialCredits.size())); - return newlyExpiredFreeTierUsers; + return newlyExhaustedCreatorsWithInitialCredits; } /** @@ -229,12 +233,12 @@ private Map findDbUsersWithChangedCosts( return dbUsersWithChangedCosts; } - private Set getFreeTierActiveWorkspaceCreatorsIn(Set users) { + private Set filterToWorkspaceCreatorsWithActiveInitialCredits(Set users) { return workspaceDao.findCreatorsByActiveInitialCredits( List.of(workbenchConfig.get().billing.initialCreditsBillingAccountName()), users); } - private void deleteAppsAndRuntimesInFreeTierWorkspaces(DbUser user) { + private void deleteAppsAndRuntimesInInitialCreditsWorkspaces(DbUser user) { logger.info("Deleting apps and runtimes for user {}", user.getUsername()); workspaceDao.findAllByCreator(user).stream() @@ -258,7 +262,7 @@ private void deleteAppsAndRuntimesInFreeTierWorkspaces(DbUser user) { * Has this user passed a cost threshold between this check and the previous run? * *

Compare this user's total cost with that of the previous run, and trigger an alert if this - * is the run which pushed it over a free credits threshold. + * is the run which pushed it over an initial credits threshold. * * @param user The user to check * @param currentCost The current total cost incurred by this user, according to BigQuery @@ -270,7 +274,7 @@ private void maybeAlertOnCostThresholds( DbUser user, double currentCost, double previousCost, List thresholdsInDescOrder) { final double limit = - getUserFreeTierDollarLimit( + getUserInitialCreditsLimit( user, workbenchConfig.get().billing.defaultFreeCreditsDollarLimit); final double remainingBalance = limit - currentCost; @@ -279,7 +283,7 @@ private void maybeAlertOnCostThresholds( if (CostComparisonUtils.compareCosts(currentCost, previousCost) < 0) { String msg = String.format( - "User %s (%s) has %f in total free tier spending in BigQuery, " + "User %s (%s) has %f in total initial credits spending in BigQuery, " + "which is less than the %f previous spending we have recorded in the DB", user.getUsername(), Optional.ofNullable(user.getContactEmail()).orElse("NULL"), diff --git a/api/src/main/java/org/pmiops/workbench/api/CloudTaskUserController.java b/api/src/main/java/org/pmiops/workbench/api/CloudTaskUserController.java index 0d16512dd88..fccf58a1381 100644 --- a/api/src/main/java/org/pmiops/workbench/api/CloudTaskUserController.java +++ b/api/src/main/java/org/pmiops/workbench/api/CloudTaskUserController.java @@ -57,7 +57,7 @@ public class CloudTaskUserController implements CloudTaskUserApiDelegate { private final AccessModuleService accessModuleService; private final CloudResourceManagerService cloudResourceManagerService; - private final InitialCreditsBatchUpdateService freeTierBillingUpdateService; + private final InitialCreditsBatchUpdateService initialCreditsBatchUpdateService; private final InitialCreditsService initialCreditsService; private final Provider stopwatchProvider; private final UserService userService; @@ -65,13 +65,13 @@ public class CloudTaskUserController implements CloudTaskUserApiDelegate { CloudTaskUserController( AccessModuleService accessModuleService, CloudResourceManagerService cloudResourceManagerService, - InitialCreditsBatchUpdateService freeTierBillingUpdateService, + InitialCreditsBatchUpdateService initialCreditsBatchUpdateService, InitialCreditsService initialCreditsService, Provider stopwatchProvider, UserService userService) { this.accessModuleService = accessModuleService; this.cloudResourceManagerService = cloudResourceManagerService; - this.freeTierBillingUpdateService = freeTierBillingUpdateService; + this.initialCreditsBatchUpdateService = initialCreditsBatchUpdateService; this.initialCreditsService = initialCreditsService; this.stopwatchProvider = stopwatchProvider; this.userService = userService; @@ -131,8 +131,7 @@ private int auditOneUser(DbUser user) { * Takes in batch of user Ids check whether users have incurred sufficient cost in their * workspaces to trigger alerts due to passing thresholds or exceeding limits * - * @param userIds : Batch of user IDs from cloud task queue: freeTierBillingQueue - * @return + * @param userIds : Batch of user IDs from cloud task queue: initialCreditsUsageQueue */ @Override public ResponseEntity checkAndAlertFreeTierBillingUsageBatch(List userIds) { @@ -143,7 +142,7 @@ public ResponseEntity checkAndAlertFreeTierBillingUsageBatch(List us return processUserIdBatch( userIds, "alerting for initial credits usage", - freeTierBillingUpdateService::checkAndAlertFreeTierBillingUsage); + initialCreditsBatchUpdateService::checkAndAlertInitialCreditsUsage); } @Override diff --git a/api/src/main/java/org/pmiops/workbench/api/OfflineBillingController.java b/api/src/main/java/org/pmiops/workbench/api/OfflineBillingController.java index c371ca276dd..19492b993f6 100644 --- a/api/src/main/java/org/pmiops/workbench/api/OfflineBillingController.java +++ b/api/src/main/java/org/pmiops/workbench/api/OfflineBillingController.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.pmiops.workbench.cloudtasks.TaskQueueService; import org.pmiops.workbench.db.dao.GoogleProjectPerCostDao; import org.pmiops.workbench.db.dao.UserService; @@ -15,34 +14,31 @@ @RestController public class OfflineBillingController implements OfflineBillingApiDelegate { - private final InitialCreditsBatchUpdateService freeTierBillingService; private final GoogleProjectPerCostDao googleProjectPerCostDao; + private final InitialCreditsBatchUpdateService initialCreditsBatchUpdateService; private final TaskQueueService taskQueueService; - private final UserService userService; @Autowired OfflineBillingController( - InitialCreditsBatchUpdateService freeTierBillingService, GoogleProjectPerCostDao googleProjectPerCostDao, - UserService userService, - TaskQueueService taskQueueService) { - this.freeTierBillingService = freeTierBillingService; + InitialCreditsBatchUpdateService initialCreditsBatchUpdateService, + TaskQueueService taskQueueService, + UserService userService) { + this.googleProjectPerCostDao = googleProjectPerCostDao; + this.initialCreditsBatchUpdateService = initialCreditsBatchUpdateService; this.taskQueueService = taskQueueService; this.userService = userService; - this.googleProjectPerCostDao = googleProjectPerCostDao; } @Override public ResponseEntity checkFreeTierBillingUsage() { // Get cost for all workspace from BQ - Map freeTierForAllWorkspace = - freeTierBillingService.getFreeTierWorkspaceCostsFromBQ(); + Map workspaceCostsFromBQ = + initialCreditsBatchUpdateService.getWorkspaceCostsFromBQ(); List googleProjectCostList = - freeTierForAllWorkspace.entrySet().stream() - .map(DbGoogleProjectPerCost::new) - .collect(Collectors.toList()); + workspaceCostsFromBQ.entrySet().stream().map(DbGoogleProjectPerCost::new).toList(); // Clear table googleproject_cost and then insert all entries from BQ googleProjectPerCostDao.deleteAll(); @@ -51,8 +47,8 @@ public ResponseEntity checkFreeTierBillingUsage() { List allUserIds = userService.getAllUserIds(); - taskQueueService.groupAndPushFreeTierBilling(allUserIds); - log.info("Pushed all users to Cloud Task for Free Tier Billing"); + taskQueueService.groupAndPushInitialCreditsUsage(allUserIds); + log.info("Pushed all users to the Initial Credits Usage Cloud Task"); return ResponseEntity.noContent().build(); } diff --git a/api/src/main/java/org/pmiops/workbench/api/OfflineEnvironmentsController.java b/api/src/main/java/org/pmiops/workbench/api/OfflineEnvironmentsController.java index a63def979ac..32b25b7fdec 100644 --- a/api/src/main/java/org/pmiops/workbench/api/OfflineEnvironmentsController.java +++ b/api/src/main/java/org/pmiops/workbench/api/OfflineEnvironmentsController.java @@ -341,7 +341,7 @@ private boolean notifyForUnusedDisk(ListPersistentDiskResponse disk, int daysUnu if (BillingUtils.isInitialCredits( workspace.get().getBillingAccountName(), configProvider.get())) { initialCreditsRemaining = - initialCreditsService.getWorkspaceCreatorFreeCreditsRemaining(workspace.get()); + initialCreditsService.getWorkspaceCreatorInitialCreditsRemaining(workspace.get()); } mailService.alertUsersUnusedDiskWarningThreshold( diff --git a/api/src/main/java/org/pmiops/workbench/api/OfflineUserController.java b/api/src/main/java/org/pmiops/workbench/api/OfflineUserController.java index 46754b7564f..1f524db9858 100644 --- a/api/src/main/java/org/pmiops/workbench/api/OfflineUserController.java +++ b/api/src/main/java/org/pmiops/workbench/api/OfflineUserController.java @@ -1,7 +1,5 @@ package org.pmiops.workbench.api; -import java.util.logging.Logger; -import java.util.stream.Collectors; import org.pmiops.workbench.cloudtasks.TaskQueueService; import org.pmiops.workbench.db.dao.UserService; import org.pmiops.workbench.db.model.DbUser; @@ -12,8 +10,6 @@ /** Handles offline / cron-based API requests related to user management. */ @RestController public class OfflineUserController implements OfflineUserApiDelegate { - private static final Logger log = Logger.getLogger(OfflineUserController.class.getName()); - private final UserService userService; private final TaskQueueService taskQueueService; @@ -48,9 +44,7 @@ public ResponseEntity sendAccessExpirationEmails() { public ResponseEntity checkInitialCreditsExpiration() { taskQueueService.groupAndPushCheckInitialCreditExpirationTasks( - userService.getAllUsersWithActiveInitialCredits().stream() - .map(DbUser::getUserId) - .collect(Collectors.toList())); + userService.getAllUsersWithActiveInitialCredits().stream().map(DbUser::getUserId).toList()); return ResponseEntity.noContent().build(); } } diff --git a/api/src/main/java/org/pmiops/workbench/api/UserController.java b/api/src/main/java/org/pmiops/workbench/api/UserController.java index 4ab96de195a..b76a61952fa 100644 --- a/api/src/main/java/org/pmiops/workbench/api/UserController.java +++ b/api/src/main/java/org/pmiops/workbench/api/UserController.java @@ -197,8 +197,7 @@ private ResponseEntity processSearchResults( @Override public ResponseEntity listBillingAccounts() { List billingAccounts = - Stream.concat(maybeFreeTierBillingAccount(), maybeCloudBillingAccounts()) - .collect(Collectors.toList()); + Stream.concat(maybeInitialCreditsAccount(), maybeCloudBillingAccounts()).toList(); return ResponseEntity.ok( new WorkbenchListBillingAccountsResponse().billingAccounts(billingAccounts)); @@ -211,10 +210,10 @@ public ResponseEntity signOut() { } /** - * @return the free tier billing account, if the user has free credits + * @return the initial credits billing account, if the user has any remaining credits */ - private Stream maybeFreeTierBillingAccount() { - if (!initialCreditsService.userHasRemainingFreeTierCredits(userProvider.get())) { + private Stream maybeInitialCreditsAccount() { + if (!initialCreditsService.userHasRemainingInitialCredits(userProvider.get())) { return Stream.empty(); } diff --git a/api/src/main/java/org/pmiops/workbench/api/WorkspacesController.java b/api/src/main/java/org/pmiops/workbench/api/WorkspacesController.java index 7865fc4acf5..e298389bb7f 100644 --- a/api/src/main/java/org/pmiops/workbench/api/WorkspacesController.java +++ b/api/src/main/java/org/pmiops/workbench/api/WorkspacesController.java @@ -186,11 +186,11 @@ public ResponseEntity createWorkspace(Workspace workspace) throws Bad try { dbWorkspace = workspaceDao.save(dbWorkspace); } catch (Exception e) { - // Tell Google to set the billing account back to the free tier if the workspace + // Tell Google to set the billing account back to initial credits if the workspace // creation fails log.log( Level.SEVERE, - "Could not save new workspace to database. Calling Google Cloud billing to update the failed billing project's billing account back to the free tier.", + "Could not save new workspace to database. Calling Google Cloud billing to update the failed billing project's billing account back to initial credits.", e); // I don't think this is a bug but it's confusing that we're calling a function that is @@ -409,8 +409,8 @@ private DbWorkspace createDbWorkspace( // A little unintuitive but setting this here reflects the current state of the workspace // while it was in the billing buffer. Setting this value will inform the update billing - // code to skip an unnecessary GCP API call if the billing account is being kept at the free - // tier + // code to skip an unnecessary GCP API call if the billing account is being kept as + // initial-credits dbWorkspace.setBillingAccountName( workbenchConfigProvider.get().billing.initialCreditsBillingAccountName()); @@ -624,7 +624,7 @@ public ResponseEntity cloneWorkspace( dbWorkspace = workspaceService.saveAndCloneCohortsConceptSetsAndDataSets(fromWorkspace, dbWorkspace); } catch (Exception e) { - // Tell Google to set the billing account back to the free tier if our clone fails + // Tell Google to set the billing account back to initial-credits if our clone fails workspaceService.updateWorkspaceBillingAccount( dbWorkspace, workbenchConfigProvider.get().billing.initialCreditsBillingAccountName()); throw e; @@ -677,7 +677,7 @@ public ResponseEntity getBillingUsage( DbWorkspace workspace = workspaceDao.getRequired(workspaceNamespace, workspaceTerraName); return ResponseEntity.ok( new WorkspaceBillingUsageResponse() - .cost(initialCreditsService.getWorkspaceFreeTierBillingUsage(workspace))); + .cost(initialCreditsService.getWorkspaceInitialCreditsUsage(workspace))); } @Override @@ -887,7 +887,7 @@ public ResponseEntity getWorkspaceResourcesV2( workspaceNamespace, workspaceTerraName, WorkspaceAccessLevel.WRITER); DbWorkspace dbWorkspace = workspaceDao.getRequired(workspaceNamespace, workspaceTerraName); double freeCreditsRemaining = - initialCreditsService.getWorkspaceCreatorFreeCreditsRemaining(dbWorkspace); + initialCreditsService.getWorkspaceCreatorInitialCreditsRemaining(dbWorkspace); return ResponseEntity.ok( new WorkspaceCreatorFreeCreditsRemainingResponse() .freeCreditsRemaining(freeCreditsRemaining)); diff --git a/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java b/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java index 9b6794de219..851af02a595 100644 --- a/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java +++ b/api/src/main/java/org/pmiops/workbench/cloudtasks/TaskQueueService.java @@ -61,7 +61,7 @@ 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 INITIAL_CREDITS_USAGE_QUEUE = "initialCreditsUsageQueue"; private static final String INITIAL_CREDITS_EXHAUSTION_QUEUE = "initialCreditsExhaustionQueue"; private static final String CHECK_CREDITS_EXPIRATION_FOR_USER_IDS_QUEUE_NAME = "checkCreditsExpirationForUserIDsQueue"; @@ -121,14 +121,14 @@ public void groupAndPushAuditProjectsTasks(List userIds) { .forEach(batch -> createAndPushTask(AUDIT_PROJECTS_QUEUE_NAME, AUDIT_PROJECTS_PATH, batch)); } - public void groupAndPushFreeTierBilling(List userIds) { + public void groupAndPushInitialCreditsUsage(List userIds) { Integer freeTierCronUserBatchSize = workbenchConfigProvider.get().billing.freeTierCronUserBatchSize; CloudTasksUtils.partitionList(userIds, freeTierCronUserBatchSize) .forEach( batch -> createAndPushTask( - FREE_TIER_BILLING_QUEUE, CHECK_AND_ALERT_FREE_TIER_USAGE_PATH, batch)); + INITIAL_CREDITS_USAGE_QUEUE, CHECK_AND_ALERT_FREE_TIER_USAGE_PATH, batch)); } public List groupAndPushSynchronizeAccessTasks(List userIds) { diff --git a/api/src/main/java/org/pmiops/workbench/config/WorkbenchConfig.java b/api/src/main/java/org/pmiops/workbench/config/WorkbenchConfig.java index 1ef571c2d0d..df5eb5c95df 100644 --- a/api/src/main/java/org/pmiops/workbench/config/WorkbenchConfig.java +++ b/api/src/main/java/org/pmiops/workbench/config/WorkbenchConfig.java @@ -99,7 +99,7 @@ public String initialCreditsBillingAccountName() { public String exportBigQueryTable; // The default dollar limit to apply to free-credit usage in this environment. public Double defaultFreeCreditsDollarLimit; - // Thresholds for email alerting based on free tier usage, by cost + // Thresholds for email alerting based on initial credits usage, by cost public List freeTierCostAlertThresholds; // The contact email from Carahsoft for billing account setup public String carahsoftEmail; @@ -111,10 +111,10 @@ public String initialCreditsBillingAccountName() { // information public Integer minutesBeforeLastFreeTierJob; - // A value that defines the number of days to consider between the last update of the Free tier - // usage in the database and the last workspace update when calculating the eligibility of a - // workspace free tier usage to be updated. To account for charges that may occur after the - // workspace gets deleted and after the last cron had run + // A value that defines the number of days to consider between the last update of initial + // credits usage in the database and the last workspace update when calculating the eligibility + // of a workspace initial credits usage to be updated. To account for charges that may occur + // after the workspace gets deleted and after the last cron had run public Long numberOfDaysToConsiderForFreeTierUsageUpdate; // The number of days that initial credits are valid for. diff --git a/api/src/main/java/org/pmiops/workbench/db/dao/WorkspaceDao.java b/api/src/main/java/org/pmiops/workbench/db/dao/WorkspaceDao.java index 7da32fb005c..906a83a5daf 100644 --- a/api/src/main/java/org/pmiops/workbench/db/dao/WorkspaceDao.java +++ b/api/src/main/java/org/pmiops/workbench/db/dao/WorkspaceDao.java @@ -123,9 +123,9 @@ interface WorkspaceCostView { Long getCreatorId(); - Double getFreeTierCost(); + Double getInitialCreditsCost(); - Timestamp getFreeTierLastUpdated(); + Timestamp getInitialCreditsLastUpdated(); Timestamp getWorkspaceLastUpdated(); @@ -136,8 +136,8 @@ interface WorkspaceCostView { "SELECT w.workspaceId AS workspaceId, " + "w.googleProject AS googleProject, " + "w.creator.id AS creatorId, " - + "f.cost AS freeTierCost, " - + "f.lastUpdateTime AS freeTierLastUpdated, " + + "f.cost AS initialCreditsCost, " + + "f.lastUpdateTime AS initialCreditsLastUpdated, " + "w.lastModifiedTime AS workspaceLastUpdated, " + "w.activeStatus AS activeStatus " + "FROM DbWorkspace w " diff --git a/api/src/main/java/org/pmiops/workbench/db/model/DbUser.java b/api/src/main/java/org/pmiops/workbench/db/model/DbUser.java index 6d0812db5b0..116a8c0397e 100644 --- a/api/src/main/java/org/pmiops/workbench/db/model/DbUser.java +++ b/api/src/main/java/org/pmiops/workbench/db/model/DbUser.java @@ -58,7 +58,7 @@ public class DbUser { private String username; // The email address that can be used to contact the user. private String contactEmail; - private Double freeTierCreditsLimitDollarsOverride = null; + private Double initialCreditsLimitOverride = null; private Timestamp firstSignInTime; private Set authorities = new HashSet<>(); private boolean disabled; @@ -214,12 +214,12 @@ public DbUser setFamilyName(String familyName) { } @Column(name = "free_tier_credits_limit_dollars_override") - public Double getFreeTierCreditsLimitDollarsOverride() { - return freeTierCreditsLimitDollarsOverride; + public Double getInitialCreditsLimitOverride() { + return initialCreditsLimitOverride; } - public DbUser setFreeTierCreditsLimitDollarsOverride(Double freeTierCreditsLimitDollarsOverride) { - this.freeTierCreditsLimitDollarsOverride = freeTierCreditsLimitDollarsOverride; + public DbUser setInitialCreditsLimitOverride(Double freeTierCreditsLimitDollarsOverride) { + this.initialCreditsLimitOverride = freeTierCreditsLimitDollarsOverride; return this; } @@ -629,7 +629,7 @@ public boolean equals(Object o) { .append(disabled, dbUser.disabled) .append(username, dbUser.username) .append(contactEmail, dbUser.contactEmail) - .append(freeTierCreditsLimitDollarsOverride, dbUser.freeTierCreditsLimitDollarsOverride) + .append(initialCreditsLimitOverride, dbUser.initialCreditsLimitOverride) .append(firstSignInTime, dbUser.firstSignInTime) .append(creationTime, dbUser.creationTime) .append(givenName, dbUser.givenName) @@ -644,7 +644,7 @@ public int hashCode() { .append(userId) .append(username) .append(contactEmail) - .append(freeTierCreditsLimitDollarsOverride) + .append(initialCreditsLimitOverride) .append(firstSignInTime) .append(disabled) .append(creationTime) diff --git a/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateService.java b/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateService.java index eb48385eb19..f9ce6479f7d 100644 --- a/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateService.java +++ b/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateService.java @@ -2,16 +2,11 @@ import com.google.cloud.bigquery.FieldValueList; import com.google.cloud.bigquery.QueryJobConfiguration; -import com.google.common.collect.Iterables; -import com.google.common.collect.Iterators; -import com.google.common.collect.Range; import jakarta.inject.Provider; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.Logger; import java.util.stream.Collectors; import org.pmiops.workbench.api.BigQueryService; import org.pmiops.workbench.config.WorkbenchConfig; @@ -25,43 +20,32 @@ /** * Class to call the {@link InitialCreditsService} with batches of users. This ensures that - * FreeTierBillingService will commit the transaction for a smaller batch of users instead of - * processing all users in one transaction and eventually timing out. See RW-6280 + * InitialCreditsBatchUpdateService will commit the transaction for a smaller batch of users instead + * of processing all users in one transaction and eventually timing out. See RW-6280 */ @Service public class InitialCreditsBatchUpdateService { - private static final Logger logger = - Logger.getLogger(InitialCreditsBatchUpdateService.class.getName()); - - private final UserDao userDao; - private final InitialCreditsService initialCreditsService; - private final Provider workbenchConfigProvider; - private final BigQueryService bigQueryService; - private final GoogleProjectPerCostDao googleProjectPerCostDao; - + private final InitialCreditsService initialCreditsService; + private final Provider workbenchConfigProvider; + private final UserDao userDao; private final WorkspaceDao workspaceDao; - private static final int MIN_USERS_BATCH = 5; - private static final int MAX_USERS_BATCH = 999; - public static final Range batchSizeRange = - Range.closed(MIN_USERS_BATCH, MAX_USERS_BATCH); - @Autowired public InitialCreditsBatchUpdateService( + BigQueryService bigQueryService, GoogleProjectPerCostDao googleProjectPerCostDao, - UserDao userDao, InitialCreditsService initialCreditsService, Provider workbenchConfigProvider, - WorkspaceDao workspaceDao, - BigQueryService bigQueryService) { + UserDao userDao, + WorkspaceDao workspaceDao) { + this.bigQueryService = bigQueryService; this.googleProjectPerCostDao = googleProjectPerCostDao; - this.userDao = userDao; this.initialCreditsService = initialCreditsService; + this.userDao = userDao; this.workbenchConfigProvider = workbenchConfigProvider; - this.bigQueryService = bigQueryService; this.workspaceDao = workspaceDao; } @@ -70,7 +54,7 @@ public InitialCreditsBatchUpdateService( * * @param userIdList */ - public void checkAndAlertFreeTierBillingUsage(List userIdList) { + public void checkAndAlertInitialCreditsUsage(List userIdList) { Set googleProjectsForUserSet = workspaceDao.getGoogleProjectForUserList(userIdList); List googleProjectPerCostList = @@ -87,39 +71,10 @@ public void checkAndAlertFreeTierBillingUsage(List userIdList) { Set dbUserSet = userIdList.stream().map(userDao::findUserByUserId).collect(Collectors.toSet()); - initialCreditsService.checkFreeTierBillingUsageForUsers(dbUserSet, userWorkspaceBQCosts); - } - - /** - * 1- Get users who have active free tier workspaces 2- Iterate over these users in batches of X - * and find the cost of their workspaces before/after - */ - public void checkFreeTierBillingUsage() { - logger.info("Checking Free Tier Billing usage - start"); - - Iterable freeTierActiveWorkspaceCreators = userDao.findAll(); - long numberOfUsers = Iterators.size(freeTierActiveWorkspaceCreators.iterator()); - int count = 0; - - Map allBQCosts = getFreeTierWorkspaceCostsFromBQ(); - - logger.info(String.format("Retrieved all BQ costs, size is: %d", allBQCosts.size())); - - for (List usersPartition : - Iterables.partition( - freeTierActiveWorkspaceCreators, freeTierCronUserBatchSizeFromConfig())) { - logger.info( - String.format( - "Processing users batch of size/total: %d/%d. Current iteration is: %d", - usersPartition.size(), numberOfUsers, count++)); - initialCreditsService.checkFreeTierBillingUsageForUsers( - new HashSet<>(usersPartition), allBQCosts); - } - - logger.info("Checking Free Tier Billing usage - finish"); + initialCreditsService.checkInitialCreditsUsageForUsers(dbUserSet, userWorkspaceBQCosts); } - public Map getFreeTierWorkspaceCostsFromBQ() { + public Map getWorkspaceCostsFromBQ() { final QueryJobConfiguration queryConfig = QueryJobConfiguration.newBuilder( "SELECT id, SUM(cost) cost FROM `" @@ -136,20 +91,4 @@ public Map getFreeTierWorkspaceCostsFromBQ() { return liveCostByWorkspace; } - - private int freeTierCronUserBatchSizeFromConfig() { - Integer freeTierCronUserBatchSize = - workbenchConfigProvider.get().billing.freeTierCronUserBatchSize; - logger.info(String.format("freeTierCronUserBatchSize is %d", freeTierCronUserBatchSize)); - - if (freeTierCronUserBatchSize == null || !batchSizeRange.contains(freeTierCronUserBatchSize)) { - freeTierCronUserBatchSize = - freeTierCronUserBatchSize != null - && freeTierCronUserBatchSize < batchSizeRange.lowerEndpoint() - ? batchSizeRange.lowerEndpoint() - : batchSizeRange.upperEndpoint(); - } - - return freeTierCronUserBatchSize; - } } diff --git a/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsService.java b/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsService.java index 48af534f26c..711f5d7330b 100644 --- a/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsService.java +++ b/api/src/main/java/org/pmiops/workbench/initialcredits/InitialCreditsService.java @@ -47,41 +47,40 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -/** Methods relating to Free Tier credit usage and limits */ +/** Methods relating to initial credits usage and limits */ @Service public class InitialCreditsService { - - private final TaskQueueService taskQueueService; + private final Clock clock; + private final FireCloudService fireCloudService; + private final InstitutionService institutionService; + private final LeonardoApiClient leonardoApiClient; + private final MailService mailService; private final Provider workbenchConfigProvider; + private final TaskQueueService taskQueueService; private final UserDao userDao; - private final Clock clock; private final UserServiceAuditor userServiceAuditor; private final WorkspaceDao workspaceDao; private final WorkspaceFreeTierUsageDao workspaceFreeTierUsageDao; private final WorkspaceInitialCreditUsageService workspaceInitialCreditUsageService; - private final LeonardoApiClient leonardoApiClient; - private final InstitutionService institutionService; - private final MailService mailService; private final WorkspaceMapper workspaceMapper; - private final FireCloudService fireCloudService; private static final Logger logger = LoggerFactory.getLogger(InitialCreditsService.class); @Autowired public InitialCreditsService( - TaskQueueService taskQueueService, + Clock clock, + FireCloudService fireCloudService, + InstitutionService institutionService, + LeonardoApiClient leonardoApiClient, + MailService mailService, Provider workbenchConfigProvider, + TaskQueueService taskQueueService, UserDao userDao, - Clock clock, UserServiceAuditor userServiceAuditor, WorkspaceDao workspaceDao, WorkspaceFreeTierUsageDao workspaceFreeTierUsageDao, WorkspaceInitialCreditUsageService workspaceInitialCreditUsageService, - LeonardoApiClient leonardoApiClient, - InstitutionService institutionService, - MailService mailService, - WorkspaceMapper workspaceMapper, - FireCloudService fireCloudService) { + WorkspaceMapper workspaceMapper) { this.clock = clock; this.fireCloudService = fireCloudService; this.institutionService = institutionService; @@ -97,20 +96,19 @@ public InitialCreditsService( this.workspaceMapper = workspaceMapper; } - public double getWorkspaceFreeTierBillingUsage(DbWorkspace dbWorkspace) { - DbWorkspaceFreeTierUsage dbWorkspaceFreeTierUsage = - workspaceFreeTierUsageDao.findOneByWorkspace(dbWorkspace); - if (dbWorkspaceFreeTierUsage == null) { + public double getWorkspaceInitialCreditsUsage(DbWorkspace dbWorkspace) { + DbWorkspaceFreeTierUsage usage = workspaceFreeTierUsageDao.findOneByWorkspace(dbWorkspace); + if (usage == null) { return 0; } - return dbWorkspaceFreeTierUsage.getCost(); + return usage.getCost(); } /** * Check whether users have incurred sufficient cost in their workspaces to trigger alerts due to * passing thresholds or exceeding limits. */ - public void checkFreeTierBillingUsageForUsers( + public void checkInitialCreditsUsageForUsers( Set users, final Map liveCostsInBQ) { String userIdsAsString = users.stream() @@ -127,7 +125,7 @@ public void checkFreeTierBillingUsageForUsers( logger.info("No workspaces require updates"); return; } - updateFreeTierUsageInDb(allCostsInDbForUsers, liveCostsInBQ, workspaceByProject); + updateInitialCreditsUsageInDb(allCostsInDbForUsers, liveCostsInBQ, workspaceByProject); // Cache cost in DB by creator final Map dbCostByCreator = getDbCostByCreatorCache(allCostsInDbForUsers); @@ -149,9 +147,10 @@ public void checkFreeTierBillingUsageForUsers( /** * Filter the users to get only the users that have costs higher than the lowest threshold. This * means we'll be pushing cloud tasks for these users to notify them about their costs but the - * decision to notify or not is left to the FreeCreditExpiry cloud task. Another way to think - * about this is to have the logic to decide whether to include the user to be notified here. This - * is done to avoid pushing cloud tasks for users that have costs lower than the lowest threshold. + * decision to notify or not is left to the handleInitialCreditsExhaustionBatch cloud task. + * Another way to think about this is to have the logic to decide whether to include the user to + * be notified here. This is done to avoid pushing cloud tasks for users that have costs lower + * than the lowest threshold. * * @param users the users to filter * @param liveCostByCreator the live cost by creator @@ -166,7 +165,7 @@ private Set filterUsersHigherThanTheLowestThreshold( users.stream() .filter( user -> { - final double limit = getUserFreeTierDollarLimit(user); + final double limit = getUserInitialCreditsLimit(user); final double userLiveCost = Optional.ofNullable(liveCostByCreator.get(user.getUserId())) .orElse(0.0); @@ -177,8 +176,8 @@ private Set filterUsersHigherThanTheLowestThreshold( } /** - * Retrieve the user's total free tier usage from the DB by summing across the Workspaces they - * have created. This is NOT live BigQuery data: it is only as recent as the last + * Retrieve the user's total initial credits usage from the DB by summing across the Workspaces + * they have created. This is NOT live BigQuery data: it is only as recent as the last * checkFreeTierBillingUsage cron job, recorded as last_update_time in the DB. * *

Note: return value may be null, to enable direct assignment to the nullable Profile field @@ -187,36 +186,36 @@ private Set filterUsersHigherThanTheLowestThreshold( * @return the total USD amount spent in workspaces created by this user, represented as a double */ @Nullable - public Double getCachedFreeTierUsage(DbUser user) { + public Double getCachedInitialCreditsUsage(DbUser user) { return workspaceFreeTierUsageDao.totalCostByUser(user); } /** - * Does this user have remaining free tier credits? Compare the user-specific free tier limit (may - * be the system default) to the amount they have used. + * Does this user have remaining initial credits? Compare the user-specific initial credits limit + * (may be the system default) to the amount they have used. * * @param user the user as represented in our database * @return whether the user has remaining credits */ - public boolean userHasRemainingFreeTierCredits(DbUser user) { - final double usage = Optional.ofNullable(getCachedFreeTierUsage(user)).orElse(0.0); + public boolean userHasRemainingInitialCredits(DbUser user) { + final double usage = Optional.ofNullable(getCachedInitialCreditsUsage(user)).orElse(0.0); return !costAboveLimit(user, usage); } /** - * Retrieve the Free Tier dollar limit actually applicable to this user: this user's override if + * Retrieve the Initial Credits limit actually applicable to this user: this user's override if * present, the environment's default if not * * @param user the user as represented in our database * @return the US dollar amount, represented as a double */ - public double getUserFreeTierDollarLimit(DbUser user) { - return CostComparisonUtils.getUserFreeTierDollarLimit( + public double getUserInitialCreditsLimit(DbUser user) { + return CostComparisonUtils.getUserInitialCreditsLimit( user, workbenchConfigProvider.get().billing.defaultFreeCreditsDollarLimit); } /** - * Set a Free Tier dollar limit override value for this user, but only if the value to set differs + * Set an Initial Credits limit override value for this user, but only if the value to set differs * from the system default or the user has an existing override. If the user has no override and * the value to set it equal to the system default, retain the system default so this user's quota * continues to track it. @@ -230,7 +229,7 @@ public double getUserFreeTierDollarLimit(DbUser user) { * @return whether an override was set */ public boolean maybeSetDollarLimitOverride(DbUser user, double newDollarLimit) { - final Double previousLimitMaybe = user.getFreeTierCreditsLimitDollarsOverride(); + final Double previousLimitMaybe = user.getInitialCreditsLimitOverride(); if (!areUserCreditsExpired(user) && (previousLimitMaybe != null @@ -239,15 +238,14 @@ public boolean maybeSetDollarLimitOverride(DbUser user, double newDollarLimit) { workbenchConfigProvider.get().billing.defaultFreeCreditsDollarLimit))) { // TODO: prevent setting this limit directly except in this method? - user.setFreeTierCreditsLimitDollarsOverride(newDollarLimit); - user = userDao.save(user); + user = userDao.save(user.setInitialCreditsLimitOverride(newDollarLimit)); - if (userHasRemainingFreeTierCredits(user)) { + if (userHasRemainingInitialCredits(user)) { // may be redundant: enable anyway setAllToUnexhausted(user); } - userServiceAuditor.fireSetFreeTierDollarLimitOverride( + userServiceAuditor.fireSetInitialCreditsOverride( user.getUserId(), previousLimitMaybe, newDollarLimit); return true; } @@ -256,20 +254,18 @@ public boolean maybeSetDollarLimitOverride(DbUser user, double newDollarLimit) { } /** - * Given a workspace, find the amount of free credits that the workspace creator has left. + * Given a workspace, find the amount of initial credits that the workspace creator has left. * - * @param dbWorkspace The workspace for which to find its creator's free credits remaining - * @return The amount of free credits in USD the workspace creator has left, represented as a + * @param dbWorkspace The workspace for which to find its creator's initial credits remaining + * @return The amount of initial credits in USD the workspace creator has left, represented as a * double */ - public double getWorkspaceCreatorFreeCreditsRemaining(DbWorkspace dbWorkspace) { - Double creatorCachedFreeTierUsage = this.getCachedFreeTierUsage(dbWorkspace.getCreator()); - Double creatorFreeTierDollarLimit = this.getUserFreeTierDollarLimit(dbWorkspace.getCreator()); - double creatorFreeCreditsRemaining = - creatorCachedFreeTierUsage == null - ? creatorFreeTierDollarLimit - : creatorFreeTierDollarLimit - creatorCachedFreeTierUsage; - return Math.max(creatorFreeCreditsRemaining, 0); + public double getWorkspaceCreatorInitialCreditsRemaining(DbWorkspace dbWorkspace) { + Double creatorCachedUsage = this.getCachedInitialCreditsUsage(dbWorkspace.getCreator()); + Double creatorLimit = this.getUserInitialCreditsLimit(dbWorkspace.getCreator()); + double creatorCreditsRemaining = + creatorCachedUsage == null ? creatorLimit : creatorLimit - creatorCachedUsage; + return Math.max(creatorCreditsRemaining, 0); } /** @@ -425,7 +421,7 @@ public boolean checkInitialCreditsExtensionEligibility(DbUser dbUser) { Instant now = clock.instant(); WorkbenchConfig.BillingConfig billingConfig = workbenchConfigProvider.get().billing; - return userHasRemainingFreeTierCredits(dbUser) + return userHasRemainingInitialCredits(dbUser) && initialCreditsExpiration != null && initialCreditsExpiration.getExtensionTime() == null && initialCreditsExpiration.getCreditStartTime() != null @@ -508,7 +504,7 @@ private List getAllCostsInDbForUsers(Set users) { List allCostsInDbForUsers = workspaceDao.getWorkspaceCostViews(users); allCostsInDbForUsers = - findWorkspaceFreeTierUsagesThatWereNotRecentlyUpdated(allCostsInDbForUsers); + findWorkspaceInitialCreditsUsagesThatWereNotRecentlyUpdated(allCostsInDbForUsers); return allCostsInDbForUsers; } @@ -526,27 +522,27 @@ private void setAllToUnexhausted(final DbUser user) { } /** - * Filter the costs further by getting the workspaces that are active, or deleted but their free - * tier last updated time is before the workspace last updated time. This filtration ensures that - * BQ will not be queried unnecessarily for the costs of deleted workspaces that we already have - * their latest costs The method will return the workspace in either of these cases: + * Filter the costs further by getting the workspaces that are active, or deleted but their + * initial credits last updated time is before the workspace last updated time. This filtration + * ensures that BQ will not be queried unnecessarily for the costs of deleted workspaces that we + * already have their latest costs The method will return the workspace in either of these cases: * *

    *
  1. The workspace is active *
  2. The workspace is deleted within the past 6 months and any of the following is true. *
      - *
    1. Free Tier Usage last updated time is null. This means that it wasn't calculated - * before + *
    2. Initial Credits Usage last updated time is null. This means that it wasn't + * calculated before *
    3. Workspace last updated time is null. This means we don't have enough info about the * workspace, so we need to get its cost from BQ - *
    4. Free Tier Usage time is before the Workspace last updated time (Here the workspace - * last updated time will be the time that the workspace was deleted). This means that - * the workspace got changed some time after our last calculation, so we need to - * recalculate its usage - *
    5. Free Tier Usage time is after the Workspace last updated time (Here the workspace - * last updated time will be the time that the workspace was deleted), but the - * difference is smaller than a certain value. This case to account for charges that - * may occur after the workspace gets deleted and after the last cron had run. + *
    6. Initial Credits Usage time is before the Workspace last updated time (Here the + * workspace last updated time will be the time that the workspace was deleted). This + * means that the workspace got changed some time after our last calculation, so we + * need to recalculate its usage + *
    7. Initial Credits Usage time is after the Workspace last updated time (Here the + * workspace last updated time will be the time that the workspace was deleted), but + * the difference is smaller than a certain value. This case to account for charges + * that may occur after the workspace gets deleted and after the last cron had run. *
    *
* @@ -567,11 +563,13 @@ private Map findWorkspacesThatRequireUpdates( || c.getWorkspaceLastUpdated() .toLocalDateTime() .isAfter(LocalDateTime.now().minusMonths(6))) - && (c.getFreeTierLastUpdated() == null + && (c.getInitialCreditsLastUpdated() == null || c.getWorkspaceLastUpdated() == null - || c.getFreeTierLastUpdated().before(c.getWorkspaceLastUpdated()) - || (c.getFreeTierLastUpdated().after(c.getWorkspaceLastUpdated()) - && c.getFreeTierLastUpdated().getTime() + || c.getInitialCreditsLastUpdated() + .before(c.getWorkspaceLastUpdated()) + || (c.getInitialCreditsLastUpdated() + .after(c.getWorkspaceLastUpdated()) + && c.getInitialCreditsLastUpdated().getTime() - c.getWorkspaceLastUpdated().getTime() < Duration.ofDays( workbenchConfigProvider.get() @@ -590,34 +588,32 @@ private Map findWorkspacesThatRequireUpdates( } /** - * Get only the workspaces that their free tier were updated before the configured time. This - * insures that we don't calculate the costs unnecessarily again if we run the job manually to - * clear up the backlog + * Get only the workspaces whose initial credits usages were updated before the configured time. + * This insures that we don't calculate the costs unnecessarily again if we run the job manually + * to clear up the backlog * * @param allCostsInDbForUsers a List of {@link WorkspaceCostView} which contains all info about * workspaces including when they were last updated. - * @return A filtered list containing the workspaces free tier usage entries that were not last - * updated in the last 60 minutes. + * @return A filtered list containing the workspaces' initial credits usage entries that were not + * last updated in the last 60 minutes. */ @NotNull - private List findWorkspaceFreeTierUsagesThatWereNotRecentlyUpdated( + private List findWorkspaceInitialCreditsUsagesThatWereNotRecentlyUpdated( List allCostsInDbForUsers) { Timestamp minusMinutes = - Timestamp.valueOf(LocalDateTime.now().minusMinutes(getMinutesBeforeLastFreeTierJob())); + Timestamp.valueOf( + LocalDateTime.now().minusMinutes(getMinutesBeforeLastInitialCreditsJob())); - List filteredCostsInDbForUsers = - allCostsInDbForUsers.stream() - .filter( - c -> - c.getFreeTierLastUpdated() == null - || c.getFreeTierLastUpdated().before(minusMinutes)) - .collect(Collectors.toList()); - - return filteredCostsInDbForUsers; + return allCostsInDbForUsers.stream() + .filter( + c -> + c.getInitialCreditsLastUpdated() == null + || c.getInitialCreditsLastUpdated().before(minusMinutes)) + .toList(); } /** - * Use the live cost from BQ to update the workspace free tier usage in the DB. + * Use the live cost from BQ to update the workspace initial credits usage in the DB. * * @param workspaceCostViews List of {@link WorkspaceCostView} containing all workspaces for the * current batch of users @@ -625,7 +621,7 @@ private List findWorkspaceFreeTierUsagesThatWereNotRecentlyUp * workspaces and the values are the workspace IDs. * @return a Map of all live costs. */ - private void updateFreeTierUsageInDb( + private void updateInitialCreditsUsageInDb( List workspaceCostViews, Map allCostsInBqByProject, Map workspaceByProject) { @@ -636,9 +632,9 @@ private void updateFreeTierUsageInDb( .collect( Collectors.toMap( WorkspaceCostView::getWorkspaceId, - v -> Optional.ofNullable(v.getFreeTierCost()).orElse(0.0))); + v -> Optional.ofNullable(v.getInitialCreditsCost()).orElse(0.0))); - workspaceInitialCreditUsageService.updateWorkspaceFreeTierUsageInDB( + workspaceInitialCreditUsageService.updateWorkspaceInitialCreditsUsageInDB( dbCostByWorkspace, allCostsInBqByProject, workspaceByProject); } @@ -647,7 +643,7 @@ private boolean costAboveLimit(final DbUser user, final double currentCost) { user, currentCost, workbenchConfigProvider.get().billing.defaultFreeCreditsDollarLimit); } - private Integer getMinutesBeforeLastFreeTierJob() { + private Integer getMinutesBeforeLastInitialCreditsJob() { return Optional.ofNullable(workbenchConfigProvider.get().billing.minutesBeforeLastFreeTierJob) .orElse(120); } @@ -664,7 +660,7 @@ private Map getWorkspaceByProjectCache( logger.info( String.format( - "FreeTierBillingUsage: Workspace IDs that require updates: %s", + "InitialCreditsService: Workspace IDs that require updates: %s", workspaceByProject.values().stream() .sorted() .map(workspaceId -> Long.toString(workspaceId)) @@ -703,7 +699,7 @@ private static Map getDbCostByCreatorCache( Collectors.groupingBy( WorkspaceCostView::getCreatorId, Collectors.summingDouble( - v -> Optional.ofNullable(v.getFreeTierCost()).orElse(0.0)))); + v -> Optional.ofNullable(v.getInitialCreditsCost()).orElse(0.0)))); return dbCostByCreator; } diff --git a/api/src/main/java/org/pmiops/workbench/initialcredits/WorkspaceInitialCreditUsageService.java b/api/src/main/java/org/pmiops/workbench/initialcredits/WorkspaceInitialCreditUsageService.java index 6a998fbc2ec..b7db62131ea 100644 --- a/api/src/main/java/org/pmiops/workbench/initialcredits/WorkspaceInitialCreditUsageService.java +++ b/api/src/main/java/org/pmiops/workbench/initialcredits/WorkspaceInitialCreditUsageService.java @@ -42,7 +42,7 @@ public WorkspaceInitialCreditUsageService( * @param workspaceByProject */ @Transactional(propagation = Propagation.REQUIRES_NEW) - public void updateWorkspaceFreeTierUsageInDB( + public void updateWorkspaceInitialCreditsUsageInDB( Map dbCostByWorkspace, Map liveCostByProject, Map workspaceByProject) { @@ -64,25 +64,22 @@ public void updateWorkspaceFreeTierUsageInDB( .collect(Collectors.toList()); final Iterable workspaceList = workspaceDao.findAllById(workspacesIdsToUpdate); - final Iterable workspaceFreeTierUsages = + final Iterable initialCreditsUsages = workspaceFreeTierUsageDao.findAllByWorkspaceIn(workspaceList); - // Prepare cache of workspace ID to the free tier use entity - Map workspaceIdToFreeTierUsageCache = - StreamSupport.stream(workspaceFreeTierUsages.spliterator(), false) + // Prepare cache of workspace ID to the initial credits usage entity + Map workspaceIdToUsageCache = + StreamSupport.stream(initialCreditsUsages.spliterator(), false) .collect( Collectors.toMap( wftu -> wftu.getWorkspace().getWorkspaceId(), Function.identity())); + // TODO updateCost queries for each workspace, can be optimized by getting all needed workspaces + // in one query workspaceList.forEach( w -> workspaceFreeTierUsageDao.updateCost( - workspaceIdToFreeTierUsageCache, - w, - liveCostByProject.get( - w.getGoogleProject()))); // TODO updateCost queries for each workspace, can be - // optimized by getting all needed workspaces in one - // query + workspaceIdToUsageCache, w, liveCostByProject.get(w.getGoogleProject()))); logger.info( String.format( diff --git a/api/src/main/java/org/pmiops/workbench/profile/ProfileService.java b/api/src/main/java/org/pmiops/workbench/profile/ProfileService.java index d916cdbf2d4..4eeb8c51bdc 100644 --- a/api/src/main/java/org/pmiops/workbench/profile/ProfileService.java +++ b/api/src/main/java/org/pmiops/workbench/profile/ProfileService.java @@ -125,9 +125,9 @@ public Profile getProfile(DbUser userLite) { final DbUser user = userService.findUserWithAuthoritiesAndPageVisits(userLite.getUserId()).orElse(userLite); - final @Nullable Double freeTierUsage = initialCreditsService.getCachedFreeTierUsage(user); + final @Nullable Double freeTierUsage = initialCreditsService.getCachedInitialCreditsUsage(user); final @Nullable Double freeTierDollarQuota = - initialCreditsService.getUserFreeTierDollarLimit(user); + initialCreditsService.getUserInitialCreditsLimit(user); final @Nullable VerifiedInstitutionalAffiliation verifiedInstitutionalAffiliation = verifiedInstitutionalAffiliationDao .findFirstByUser(user) diff --git a/api/src/main/java/org/pmiops/workbench/utils/CostComparisonUtils.java b/api/src/main/java/org/pmiops/workbench/utils/CostComparisonUtils.java index 9fc9c4e5b00..a9fc1ff777c 100644 --- a/api/src/main/java/org/pmiops/workbench/utils/CostComparisonUtils.java +++ b/api/src/main/java/org/pmiops/workbench/utils/CostComparisonUtils.java @@ -25,23 +25,21 @@ public static int compareCostFractions(final double a, final double b) { } public static boolean costAboveLimit( - final DbUser user, final double currentCost, Double defaultFreeCreditsDollarLimit) { - return compareCosts( - currentCost, getUserFreeTierDollarLimit(user, defaultFreeCreditsDollarLimit)) + final DbUser user, final double currentCost, Double defaultInitialCreditsLimit) { + return compareCosts(currentCost, getUserInitialCreditsLimit(user, defaultInitialCreditsLimit)) > 0; } /** - * Retrieve the Free Tier dollar limit actually applicable to this user: this user's override if + * Retrieve the intial credits limit actually applicable to this user: this user's override if * present, the environment's default if not * * @param user the user as represented in our database - * @param defaultFreeCreditsDollarLimit + * @param defaultInitialCreditsLimit * @return the US dollar amount, represented as a double */ - public static double getUserFreeTierDollarLimit( - DbUser user, Double defaultFreeCreditsDollarLimit) { - return Optional.ofNullable(user.getFreeTierCreditsLimitDollarsOverride()) - .orElse(defaultFreeCreditsDollarLimit); + public static double getUserInitialCreditsLimit(DbUser user, Double defaultInitialCreditsLimit) { + return Optional.ofNullable(user.getInitialCreditsLimitOverride()) + .orElse(defaultInitialCreditsLimit); } } diff --git a/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceAuthService.java b/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceAuthService.java index ae5d725b3d8..26097045169 100644 --- a/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceAuthService.java +++ b/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceAuthService.java @@ -61,7 +61,7 @@ public WorkspaceAuthService( /* * This function will check if a workspace is eligible to be using initial credits. - * This involves checking whether they have a free tier billing account + * This involves checking whether it's using the initial credits billing account * and that their initial credits have not been exhausted or expired. */ public void validateInitialCreditUsage(String workspaceNamespace, String workspaceTerraName) diff --git a/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceServiceImpl.java b/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceServiceImpl.java index c86de137f60..dec4db39e55 100644 --- a/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceServiceImpl.java +++ b/api/src/main/java/org/pmiops/workbench/workspaces/WorkspaceServiceImpl.java @@ -502,7 +502,7 @@ public void updateWorkspaceBillingAccount(DbWorkspace workspace, String newBilli if (isInitialCredits(newBillingAccountName, workbenchConfigProvider.get())) { DbUser creator = workspace.getCreator(); boolean hasInitialCreditsRemaining = - initialCreditsService.userHasRemainingFreeTierCredits(creator); + initialCreditsService.userHasRemainingInitialCredits(creator); workspace.setInitialCreditsExhausted(!hasInitialCreditsRemaining); } } diff --git a/api/src/main/webapp/WEB-INF/queue.yaml b/api/src/main/webapp/WEB-INF/queue.yaml index 71cbc59b14e..05850424513 100644 --- a/api/src/main/webapp/WEB-INF/queue.yaml +++ b/api/src/main/webapp/WEB-INF/queue.yaml @@ -93,7 +93,7 @@ queue: task_retry_limit: 1 task_age_limit: 5m -- name: freeTierBillingQueue +- name: initialCreditsUsageQueue target: api # rate parameters diff --git a/api/src/test/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorTest.java b/api/src/test/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorTest.java index 22cfabfe99d..508a534ebe5 100644 --- a/api/src/test/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorTest.java +++ b/api/src/test/java/org/pmiops/workbench/actionaudit/auditors/UserServiceAuditorTest.java @@ -113,9 +113,9 @@ private Iterable split(String input) { } @Test - public void testSetFreeTierDollarQuota_initial() { + public void testSetInitialCreditsOverride_initial() { DbUser user = createUser(); - userServiceAuditor.fireSetFreeTierDollarLimitOverride(user.getUserId(), null, 123.45); + userServiceAuditor.fireSetInitialCreditsOverride(user.getUserId(), null, 123.45); verify(mockActionAuditService).send(eventArg.capture()); ActionAuditEvent eventSent = eventArg.getValue(); @@ -124,15 +124,15 @@ public void testSetFreeTierDollarQuota_initial() { assertThat(eventSent.targetType()).isEqualTo(TargetType.ACCOUNT); assertThat(eventSent.targetIdMaybe()).isEqualTo(user.getUserId()); assertThat(eventSent.targetPropertyMaybe()) - .isEqualTo(AccountTargetProperty.FREE_TIER_DOLLAR_QUOTA.getPropertyName()); + .isEqualTo(AccountTargetProperty.INITIAL_CREDITS_OVERRIDE.getPropertyName()); assertThat(eventSent.previousValueMaybe()).isNull(); assertThat(eventSent.newValueMaybe()).isEqualTo("123.45"); } @Test - public void testSetFreeTierDollarQuota_chnge() { + public void testSetInitialCreditsOverride_change() { DbUser user = createUser(); - userServiceAuditor.fireSetFreeTierDollarLimitOverride(user.getUserId(), 123.45, 500.0); + userServiceAuditor.fireSetInitialCreditsOverride(user.getUserId(), 123.45, 500.0); verify(mockActionAuditService).send(eventArg.capture()); ActionAuditEvent eventSent = eventArg.getValue(); @@ -141,7 +141,7 @@ public void testSetFreeTierDollarQuota_chnge() { assertThat(eventSent.targetType()).isEqualTo(TargetType.ACCOUNT); assertThat(eventSent.targetIdMaybe()).isEqualTo(user.getUserId()); assertThat(eventSent.targetPropertyMaybe()) - .isEqualTo(AccountTargetProperty.FREE_TIER_DOLLAR_QUOTA.getPropertyName()); + .isEqualTo(AccountTargetProperty.INITIAL_CREDITS_OVERRIDE.getPropertyName()); assertThat(eventSent.previousValueMaybe()).isEqualTo("123.45"); assertThat(eventSent.newValueMaybe()).isEqualTo("500.0"); } diff --git a/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java index 28e11c4182a..29ebe5fd4af 100644 --- a/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/CloudTaskInitialCreditsExhaustionControllerTest.java @@ -158,8 +158,9 @@ public void tearDown() { } @Test - public void handleInitialCreditsExpiry_alertsAndDeletesResources_whenDollarThresholdIsExceeded() - throws MessagingException { + public void + handleInitialCreditsExhaustionBatch_alertsAndDeletesResources_whenDollarThresholdIsExceeded() + throws MessagingException { final double limit = 100.0; final double costUnderThreshold = 49.5; @@ -231,7 +232,7 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_whenDollarThres @Test public void - handleInitialCreditsExpiry_alertsAndDeletesResources_whenAltDollarThresholdIsExceeded() + handleInitialCreditsExhaustionBatch_alertsAndDeletesResources_whenAltDollarThresholdIsExceeded() throws MessagingException { // set alert thresholds at 30% and 65% instead @@ -312,7 +313,7 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_whenDollarThres } @Test - public void handleInitialCreditsExpiry_alertsAndDeletesResources_evenWhenUserIsDisabled() + public void handleInitialCreditsExhaustionBatch_alertsAndDeletesResources_evenWhenUserIsDisabled() throws MessagingException { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; @@ -336,8 +337,9 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_evenWhenUserIsD } @Test - public void handleInitialCreditsExpiry_alertsAndDeletesResources_evenWhenWorkspaceIsDeleted() - throws MessagingException { + public void + handleInitialCreditsExhaustionBatch_alertsAndDeletesResources_evenWhenWorkspaceIsDeleted() + throws MessagingException { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); @@ -360,7 +362,7 @@ public void handleInitialCreditsExpiry_alertsAndDeletesResources_evenWhenWorkspa } @Test - public void handleInitialCreditsExpiry_noAlert_ifCostIsBelowLowestThreshold() { + public void handleInitialCreditsExhaustionBatch_noAlert_ifCostIsBelowLowestThreshold() { // set limit so usage is just under the 50% threshold workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; @@ -379,7 +381,7 @@ public void handleInitialCreditsExpiry_noAlert_ifCostIsBelowLowestThreshold() { } @Test - public void handleInitialCreditsExpiry_doesntThrowNPE_whenWorkspaceIsMissingCreator() { + public void handleInitialCreditsExhaustionBatch_doesntThrowNPE_whenWorkspaceIsMissingCreator() { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); @@ -399,7 +401,7 @@ public void handleInitialCreditsExpiry_doesntThrowNPE_whenWorkspaceIsMissingCrea } @Test - public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenToOverUse() + public void handleInitialCreditsExhaustionBatch_doesntAlert_ifDollarLimitWasOverriddenToOverUse() throws MessagingException { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; @@ -431,7 +433,7 @@ public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenTo // do not reactivate workspaces if the new dollar limit is still below the usage @Test - public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenToUnderUse() + public void handleInitialCreditsExhaustionBatch_doesntAlert_ifDollarLimitWasOverriddenToUnderUse() throws MessagingException { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; @@ -460,8 +462,9 @@ public void handleInitialCreditsExpiry_doesntaAlert_ifDollarLimitWasOverriddenTo } @Test - public void handleInitialCreditsExpiry_disabledAllWorkspaces_ifTheCombinedCostExceedsLimit() - throws MessagingException { + public void + handleInitialCreditsExhaustionBatch_disabledAllWorkspaces_ifTheCombinedCostExceedsLimit() + throws MessagingException { final String proj1 = "proj-1"; final String proj2 = "proj-2"; final double cost1 = 123.45; @@ -492,7 +495,7 @@ public void handleInitialCreditsExpiry_disabledAllWorkspaces_ifTheCombinedCostEx } @Test - public void handleInitialCreditsExpiry_alertsAllUsers_ifTheyExceedFreeTierLimit() + public void handleInitialCreditsExhaustionBatch_alertsAllUsers_ifTheyExceedFreeTierLimit() throws MessagingException { final String proj1 = "proj-1"; final String proj2 = "proj-2"; @@ -526,8 +529,9 @@ public void handleInitialCreditsExpiry_alertsAllUsers_ifTheyExceedFreeTierLimit( } @Test - public void handleInitialCreditsExpiry_alertsOnlyOnce_ifCostKeepIncreasingAboveThreshold() - throws MessagingException { + public void + handleInitialCreditsExhaustionBatch_alertsOnlyOnce_ifCostKeepIncreasingAboveThreshold() + throws MessagingException { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); @@ -560,7 +564,8 @@ public void handleInitialCreditsExpiry_alertsOnlyOnce_ifCostKeepIncreasingAboveT // Regression test coverage for RW-8328. @Test - public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() throws Exception { + public void handleInitialCreditsExhaustionBatch_singleAlert_forExhaustedAndByoBilling() + throws Exception { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); @@ -585,7 +590,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t @Test public void - handleInitialCreditsExpiry_disableFreeTierWorkspacesOnly_whenUserHasMultipleWorkspaces() + handleInitialCreditsExhaustionBatch_disableFreeTierWorkspacesOnly_whenUserHasMultipleWorkspaces() throws MessagingException { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; @@ -615,7 +620,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t @Test public void - handleInitialCreditsExpiry_deletedWorkspaceUsageIsConsidered_whenChargeIsPostedAfterCron() + handleInitialCreditsExhaustionBatch_deletedWorkspaceUsageIsConsidered_whenChargeIsPostedAfterCron() throws MessagingException { final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); DbWorkspace workspace = createWorkspace(user, SINGLE_WORKSPACE_TEST_PROJECT); @@ -642,7 +647,7 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t @Test public void - handleInitialCreditsExpiry_deletedWorkspaceUsageIsConsidered_whenAnotherWorkspaceExceedsLimitAfterCron() + handleInitialCreditsExhaustionBatch_deletedWorkspaceUsageIsConsidered_whenAnotherWorkspaceExceedsLimitAfterCron() throws MessagingException { final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); DbWorkspace workspace = createWorkspace(user, SINGLE_WORKSPACE_TEST_PROJECT); @@ -675,7 +680,8 @@ public void handleInitialCreditsExpiry_singleAlert_forExhaustedAndByoBilling() t } @Test - public void handleInitialCreditsExpiry_withMissingUsersInRequest_NoNPE() throws Exception { + public void handleInitialCreditsExhaustionBatch_withMissingUsersInRequest_NoNPE() + throws Exception { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 300.0; diff --git a/api/src/test/java/org/pmiops/workbench/api/CloudTaskUserControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/CloudTaskUserControllerTest.java index 9deea34ffca..09152fd93e8 100644 --- a/api/src/test/java/org/pmiops/workbench/api/CloudTaskUserControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/CloudTaskUserControllerTest.java @@ -44,7 +44,7 @@ public class CloudTaskUserControllerTest { @Autowired private CloudTaskUserController controller; @MockBean private AccessModuleService mockAccessModuleService; - @MockBean private InitialCreditsBatchUpdateService mockFreeTierBillingUpdateService; + @MockBean private InitialCreditsBatchUpdateService mockInitialCreditsBatchUpdateService; @MockBean private InitialCreditsService mockInitialCreditsService; @MockBean private UserService mockUserService; @@ -126,19 +126,19 @@ public void testSynchronizeAccess() { public void testCheckAndAlertFreeTierBillingUsage() { List userIdList = List.of(1L, 2L, 3L); controller.checkAndAlertFreeTierBillingUsageBatch(userIdList); - verify(mockFreeTierBillingUpdateService).checkAndAlertFreeTierBillingUsage(userIdList); + verify(mockInitialCreditsBatchUpdateService).checkAndAlertInitialCreditsUsage(userIdList); } @Test public void testCheckAndAlertFreeTierBillingUsage_noUserListPassedFromTask() { controller.checkAndAlertFreeTierBillingUsageBatch(Collections.emptyList()); - verify(mockFreeTierBillingUpdateService, never()).checkAndAlertFreeTierBillingUsage(any()); + verify(mockInitialCreditsBatchUpdateService, never()).checkAndAlertInitialCreditsUsage(any()); } @Test public void testCheckAndAlertFreeTierBillingUsage_nullPassedFromTask() { controller.checkAndAlertFreeTierBillingUsageBatch(null); - verify(mockFreeTierBillingUpdateService, never()).checkAndAlertFreeTierBillingUsage(any()); + verify(mockInitialCreditsBatchUpdateService, never()).checkAndAlertInitialCreditsUsage(any()); } @Test diff --git a/api/src/test/java/org/pmiops/workbench/api/OfflineBillingControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/OfflineBillingControllerTest.java index 7090c43e04a..0e89496303f 100644 --- a/api/src/test/java/org/pmiops/workbench/api/OfflineBillingControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/OfflineBillingControllerTest.java @@ -22,7 +22,7 @@ @DataJpaTest public class OfflineBillingControllerTest { - @Autowired private InitialCreditsBatchUpdateService mockFreeTierBillingUpdateService; + @Autowired private InitialCreditsBatchUpdateService mockInitialCreditsBatchUpdateService; @Autowired private OfflineBillingController offlineBillingController; @Autowired private TaskQueueService mockTaskQueueService; @Autowired private UserService mockUserService; @@ -38,12 +38,12 @@ public class OfflineBillingControllerTest { }) static class Configuration {} - Map freeTierForAllWorkspace = new HashMap<>(); + Map workspaceCosts = new HashMap<>(); @Test public void testCheckFreeTierBillingUsage() { mockUserId(); - mockFreeTierCostForGP(); + mockInitialCreditCosts(); offlineBillingController.checkFreeTierBillingUsage(); // Confirm the database is cleared and saved with new value @@ -51,20 +51,19 @@ public void testCheckFreeTierBillingUsage() { verify(mockGoogleProjectPerCostDao).batchInsertProjectPerCost(anyList()); // Confirm that task as pushed with User Id List - verify(mockTaskQueueService).groupAndPushFreeTierBilling(Arrays.asList(1L, 2L, 3L)); + verify(mockTaskQueueService).groupAndPushInitialCreditsUsage(Arrays.asList(1L, 2L, 3L)); } private void mockUserId() { when(mockUserService.getAllUserIds()).thenReturn(Arrays.asList(1L, 2L, 3L)); } - private void mockFreeTierCostForGP() { + private void mockInitialCreditCosts() { // Key: Google Project Value: Cost - freeTierForAllWorkspace.put("1", 0.019); - freeTierForAllWorkspace.put("2", 0.4); - freeTierForAllWorkspace.put("3", 1d); - freeTierForAllWorkspace.put("4", 0.34); - when(mockFreeTierBillingUpdateService.getFreeTierWorkspaceCostsFromBQ()) - .thenReturn(freeTierForAllWorkspace); + workspaceCosts.put("1", 0.019); + workspaceCosts.put("2", 0.4); + workspaceCosts.put("3", 1d); + workspaceCosts.put("4", 0.34); + when(mockInitialCreditsBatchUpdateService.getWorkspaceCostsFromBQ()).thenReturn(workspaceCosts); } } diff --git a/api/src/test/java/org/pmiops/workbench/api/OfflineEnvironmentsControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/OfflineEnvironmentsControllerTest.java index 5003b1ca7bd..f7ab7766a60 100644 --- a/api/src/test/java/org/pmiops/workbench/api/OfflineEnvironmentsControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/OfflineEnvironmentsControllerTest.java @@ -410,7 +410,7 @@ public void testCheckPersistentDisksFreeTier() throws MessagingException { stubDisks(List.of(idleDisk(Duration.ofDays(14L)))); workspace.setBillingAccountName(config.billing.initialCreditsBillingAccountName()); - when(mockInitialCreditsService.getWorkspaceCreatorFreeCreditsRemaining(workspace)) + when(mockInitialCreditsService.getWorkspaceCreatorInitialCreditsRemaining(workspace)) .thenReturn(123.0); assertThat(controller.checkPersistentDisks().getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); diff --git a/api/src/test/java/org/pmiops/workbench/api/ProfileControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/ProfileControllerTest.java index fcd207ba6f8..6591a2bc300 100644 --- a/api/src/test/java/org/pmiops/workbench/api/ProfileControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/ProfileControllerTest.java @@ -1547,7 +1547,7 @@ public void test_updateAccountProperties_bypass_requests() { public void test_updateAccountProperties_free_tier_quota() { createAccountAndDbUserWithAffiliation(); - final Double originalQuota = dbUser.getFreeTierCreditsLimitDollarsOverride(); + final Double originalQuota = dbUser.getInitialCreditsLimitOverride(); final Double newQuota = 123.4; final AccountPropertyUpdate request = @@ -1557,7 +1557,7 @@ public void test_updateAccountProperties_free_tier_quota() { assertThat(retrieved.getFreeTierDollarQuota()).isWithin(0.01).of(newQuota); verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(dbUser.getUserId(), originalQuota, newQuota); + .fireSetInitialCreditsOverride(dbUser.getUserId(), originalQuota, newQuota); } @Test @@ -1571,7 +1571,7 @@ public void test_updateAccountProperties_free_tier_quota_no_change() { profileService.updateAccountProperties(request); verify(mockUserServiceAuditor, never()) - .fireSetFreeTierDollarLimitOverride(anyLong(), anyDouble(), anyDouble()); + .fireSetInitialCreditsOverride(anyLong(), anyDouble(), anyDouble()); } // don't set an override if the value to set is equal to the system default @@ -1599,7 +1599,7 @@ public void test_updateAccountProperties_free_tier_quota_no_override() { .freeCreditsLimit(config.billing.defaultFreeCreditsDollarLimit); profileService.updateAccountProperties(request); verify(mockUserServiceAuditor, never()) - .fireSetFreeTierDollarLimitOverride(anyLong(), anyDouble(), anyDouble()); + .fireSetInitialCreditsOverride(anyLong(), anyDouble(), anyDouble()); // the user's profile continues to track default changes diff --git a/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java index df66cb85eff..aa0167cbd9e 100644 --- a/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/UserControllerTest.java @@ -349,10 +349,10 @@ public void testUserSort() { } // Combinatorial tests for listBillingAccounts: - // free tier available vs. exhausted + // initial credits available vs. exhausted // cloud accounts available vs. none - static final String INITIAL_CREDITS_ID = "free-tier"; + static final String INITIAL_CREDITS_ID = "initial-credits-acct-name"; static final BillingAccount INITIAL_CREDITS_BILLING_ACCOUNT = new BillingAccount() .freeTier(true) @@ -384,13 +384,13 @@ public void testUserSort() { .freeTier(false) .open(true)); - // billing upgrade is true, free tier is available, cloud accounts exist + // initial credits available, cloud accounts exist @Test - public void listBillingAccounts_upgradeYES_freeYES_cloudYES() throws IOException { + public void listBillingAccounts_initialYES_cloudYES() throws IOException { config.billing.accountId = INITIAL_CREDITS_ID; - when(mockInitialCreditsService.userHasRemainingFreeTierCredits(any())).thenReturn(true); + when(mockInitialCreditsService.userHasRemainingInitialCredits(any())).thenReturn(true); when(testCloudbilling.billingAccounts().list().execute()) .thenReturn(new ListBillingAccountsResponse().setBillingAccounts(cloudbillingAccounts)); @@ -404,13 +404,13 @@ public void listBillingAccounts_upgradeYES_freeYES_cloudYES() throws IOException assertThat(response.getBillingAccounts()).isEqualTo(expectedWorkbenchBillingAccounts); } - // billing upgrade is true, free tier is available, no cloud accounts + // initial credits are available, no cloud accounts @Test - public void listBillingAccounts_upgradeYES_freeYES_cloudNO() throws IOException { + public void listBillingAccounts_initialYES_cloudNO() throws IOException { config.billing.accountId = INITIAL_CREDITS_ID; - when(mockInitialCreditsService.userHasRemainingFreeTierCredits(any())).thenReturn(true); + when(mockInitialCreditsService.userHasRemainingInitialCredits(any())).thenReturn(true); when(testCloudbilling.billingAccounts().list().execute()) .thenReturn(new ListBillingAccountsResponse().setBillingAccounts(null)); @@ -423,13 +423,13 @@ public void listBillingAccounts_upgradeYES_freeYES_cloudNO() throws IOException assertThat(response.getBillingAccounts()).isEqualTo(expectedWorkbenchBillingAccounts); } - // billing upgrade is true, free tier is exhausted, cloud accounts exist + // initial credits are exhausted, cloud accounts exist @Test - public void listBillingAccounts_upgradeYES_freeNO_cloudYES() throws IOException { + public void listBillingAccounts_initialNO_cloudYES() throws IOException { config.billing.accountId = INITIAL_CREDITS_ID; - when(mockInitialCreditsService.userHasRemainingFreeTierCredits(any())).thenReturn(false); + when(mockInitialCreditsService.userHasRemainingInitialCredits(any())).thenReturn(false); when(testCloudbilling.billingAccounts().list().execute()) .thenReturn(new ListBillingAccountsResponse().setBillingAccounts(cloudbillingAccounts)); @@ -441,13 +441,13 @@ public void listBillingAccounts_upgradeYES_freeNO_cloudYES() throws IOException assertThat(response.getBillingAccounts()).isEqualTo(expectedWorkbenchBillingAccounts); } - // billing upgrade is true, free tier is exhausted, no cloud accounts + // initial credits are exhausted, no cloud accounts @Test - public void listBillingAccounts_upgradeYES_freeNO_cloudNO() throws IOException { + public void listBillingAccounts_initialNO_cloudNO() throws IOException { config.billing.accountId = INITIAL_CREDITS_ID; - when(mockInitialCreditsService.userHasRemainingFreeTierCredits(any())).thenReturn(false); + when(mockInitialCreditsService.userHasRemainingInitialCredits(any())).thenReturn(false); when(testCloudbilling.billingAccounts().list().execute()) .thenReturn(new ListBillingAccountsResponse().setBillingAccounts(null)); diff --git a/api/src/test/java/org/pmiops/workbench/api/WorkspacesControllerTest.java b/api/src/test/java/org/pmiops/workbench/api/WorkspacesControllerTest.java index 4a342070e18..f69a33bcda7 100644 --- a/api/src/test/java/org/pmiops/workbench/api/WorkspacesControllerTest.java +++ b/api/src/test/java/org/pmiops/workbench/api/WorkspacesControllerTest.java @@ -1147,7 +1147,7 @@ public void testUpdateWorkspace_freeTierBilling_noCreditsRemaining() { doReturn(false) .when(mockInitialCreditsService) - .userHasRemainingFreeTierCredits( + .userHasRemainingInitialCredits( argThat(dbUser -> dbUser.getUserId() == currentUser.getUserId())); UpdateWorkspaceRequest request = new UpdateWorkspaceRequest(); @@ -1174,7 +1174,7 @@ public void testUpdateWorkspace_freeTierBilling_hasCreditsRemaining() { DbStorageEnums.workspaceActiveStatusToStorage(WorkspaceActiveStatus.ACTIVE)); doReturn(true) .when(mockInitialCreditsService) - .userHasRemainingFreeTierCredits( + .userHasRemainingInitialCredits( argThat(dbUser -> dbUser.getUserId() == currentUser.getUserId())); UpdateWorkspaceRequest request = new UpdateWorkspaceRequest(); @@ -2843,7 +2843,7 @@ public void testGetBillingUsage() { ws.getTerraName(), ws.getCreatorUser().getUserName(), WorkspaceAccessLevel.OWNER); - when(mockInitialCreditsService.getWorkspaceFreeTierBillingUsage(any())).thenReturn(cost); + when(mockInitialCreditsService.getWorkspaceInitialCreditsUsage(any())).thenReturn(cost); WorkspaceBillingUsageResponse workspaceBillingUsageResponse = workspacesController.getBillingUsage(ws.getNamespace(), ws.getTerraName()).getBody(); diff --git a/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateServiceTest.java b/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateServiceTest.java index 29b3753f4da..4127bb4c2f4 100644 --- a/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateServiceTest.java +++ b/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsBatchUpdateServiceTest.java @@ -62,13 +62,13 @@ public void init() throws Exception { @Test public void testFreeTierBillingBatchUpdateService() { - initialCreditsBatchUpdateService.checkAndAlertFreeTierBillingUsage(Arrays.asList(1L, 2L, 3L)); + initialCreditsBatchUpdateService.checkAndAlertInitialCreditsUsage(Arrays.asList(1L, 2L, 3L)); verify(mockWorkspaceDao, times(1)).getGoogleProjectForUserList(Arrays.asList(1L, 2L, 3L)); verify(mockGoogleProjectPerCostDao, times(1)).findAllByGoogleProjectId(googleProjectIdsSet); verify(mockInitialCreditsService) - .checkFreeTierBillingUsageForUsers(mockDbuserSet, getUserCostMap()); + .checkInitialCreditsUsageForUsers(mockDbuserSet, getUserCostMap()); } private void mockDbUser() { diff --git a/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsServiceTest.java b/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsServiceTest.java index 382aa6bc10f..22a281e03b5 100644 --- a/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsServiceTest.java +++ b/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsServiceTest.java @@ -15,7 +15,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import static org.pmiops.workbench.initialcredits.InitialCreditsExpiryTaskMatchers.MapMatcher; +import static org.pmiops.workbench.utils.ArgumentMatchers.MapMatcher; import static org.pmiops.workbench.utils.BillingUtils.fullBillingAccountName; import com.google.common.collect.ImmutableMap; @@ -53,12 +53,12 @@ import org.pmiops.workbench.db.model.DbWorkspaceFreeTierUsage; import org.pmiops.workbench.exceptions.BadRequestException; import org.pmiops.workbench.firecloud.FireCloudService; -import org.pmiops.workbench.initialcredits.InitialCreditsExpiryTaskMatchers.UserListMatcher; import org.pmiops.workbench.institution.InstitutionService; import org.pmiops.workbench.leonardo.LeonardoApiClient; import org.pmiops.workbench.mail.MailService; import org.pmiops.workbench.model.WorkspaceActiveStatus; import org.pmiops.workbench.test.FakeClock; +import org.pmiops.workbench.utils.ArgumentMatchers.UserListMatcher; import org.pmiops.workbench.utils.mappers.WorkspaceMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -186,13 +186,13 @@ public void checkFreeTierBillingUsage_exceedsDollarThresholds() { Map allBQCosts = Maps.newHashMap(); allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costUnderThreshold); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verifyNoInteractions(taskQueueService); // check that we alert for the 50% threshold double costOverThreshold = 50.5; allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costOverThreshold); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -203,7 +203,7 @@ public void checkFreeTierBillingUsage_exceedsDollarThresholds() { costOverThreshold = 75.3; allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costOverThreshold); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -216,7 +216,7 @@ public void checkFreeTierBillingUsage_exceedsDollarThresholds() { allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costToTriggerExpiration); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -245,12 +245,12 @@ public void checkFreeTierBillingUsage_altDollarThresholds() { Map allBQCosts = Maps.newHashMap(); allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costUnderThreshold); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verifyNoInteractions(taskQueueService); // check that we detect the 30% threshold allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costOverThreshold); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -260,7 +260,7 @@ public void checkFreeTierBillingUsage_altDollarThresholds() { costOverThreshold = 65.01; allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costOverThreshold); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -272,7 +272,7 @@ public void checkFreeTierBillingUsage_altDollarThresholds() { final double costToTriggerExpiration = 100.01; allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT, costToTriggerExpiration); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -293,7 +293,7 @@ public void checkFreeTierBillingUsage_disabledUserNotIgnored() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -317,7 +317,7 @@ public void checkFreeTierBillingUsage_deletedWorkspaceNotIgnored() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -340,7 +340,7 @@ public void checkFreeTierBillingUsage_noAlert() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verifyNoInteractions(taskQueueService); // No tasks have been added to the queue assertSingleWorkspaceTestDbState(user, workspace, 49.99); @@ -360,7 +360,7 @@ public void checkFreeTierBillingUsage_workspaceMissingCreatorNoNPE() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verifyNoInteractions(taskQueueService); assertSingleWorkspaceTestDbState(user, workspace, 49.99); @@ -374,16 +374,14 @@ public void maybeSetDollarLimitOverride_true() { commitTransaction(); assertThat(initialCreditsService.maybeSetDollarLimitOverride(user, 200.0)).isTrue(); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), null, 200.0); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 200.0); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), null, 200.0); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 200.0); DbUser currentUser = userDao.findUserByUserId(user.getUserId()); assertThat(initialCreditsService.maybeSetDollarLimitOverride(currentUser, 100.0)).isTrue(); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), 200.0, 100.0); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), 200.0, 100.0); assertWithinBillingTolerance( - initialCreditsService.getUserFreeTierDollarLimit(currentUser), 100.0); + initialCreditsService.getUserInitialCreditsLimit(currentUser), 100.0); } @Test @@ -393,16 +391,16 @@ public void maybeSetDollarLimitOverride_false() { assertThat(initialCreditsService.maybeSetDollarLimitOverride(user, 100.0)).isFalse(); verify(mockUserServiceAuditor, never()) - .fireSetFreeTierDollarLimitOverride(anyLong(), anyDouble(), anyDouble()); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 100.0); + .fireSetInitialCreditsOverride(anyLong(), anyDouble(), anyDouble()); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 100.0); workbenchConfig.billing.defaultFreeCreditsDollarLimit = 200.0; - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 200.0); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 200.0); assertThat(initialCreditsService.maybeSetDollarLimitOverride(user, 200.0)).isFalse(); verify(mockUserServiceAuditor, never()) - .fireSetFreeTierDollarLimitOverride(anyLong(), anyDouble(), anyDouble()); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 200.0); + .fireSetInitialCreditsOverride(anyLong(), anyDouble(), anyDouble()); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 200.0); } @Test @@ -415,12 +413,12 @@ public void maybeSetDollarLimitOverride_above_usage() { final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); final DbWorkspace workspace = createWorkspace(user, SINGLE_WORKSPACE_TEST_PROJECT); - assertThat(initialCreditsService.getCachedFreeTierUsage(user)).isNull(); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 100.0); + assertThat(initialCreditsService.getCachedInitialCreditsUsage(user)).isNull(); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 100.0); commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -428,15 +426,14 @@ public void maybeSetDollarLimitOverride_above_usage() { argThat(new MapMatcher(Map.of(user.getUserId(), 150.0)))); assertSingleWorkspaceTestDbState(user, workspace, 150.0); - assertWithinBillingTolerance(initialCreditsService.getCachedFreeTierUsage(user), 150.0); + assertWithinBillingTolerance(initialCreditsService.getCachedInitialCreditsUsage(user), 150.0); initialCreditsService.maybeSetDollarLimitOverride(user, 200.0); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), null, 200.0); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 200.0); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), null, 200.0); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 200.0); assertSingleWorkspaceTestDbState(user, workspace, 150.0); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); assertSingleWorkspaceTestDbState(user, workspace, 150.0); } @@ -454,29 +451,27 @@ public void setFreeTierDollarOverride_under_usage() { commitTransaction(); - assertThat(initialCreditsService.getCachedFreeTierUsage(user)).isNull(); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 100.0); + assertThat(initialCreditsService.getCachedInitialCreditsUsage(user)).isNull(); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 100.0); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), argThat(new MapMatcher(Map.of(user.getUserId(), 0.0d))), argThat(new MapMatcher(Map.of(user.getUserId(), 300.0)))); assertSingleWorkspaceTestDbState(user, workspace, 300.0); - assertWithinBillingTolerance(initialCreditsService.getCachedFreeTierUsage(user), 300.0); + assertWithinBillingTolerance(initialCreditsService.getCachedInitialCreditsUsage(user), 300.0); initialCreditsService.maybeSetDollarLimitOverride(user, 200.0); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), null, 200.0); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), 200.0); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), null, 200.0); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), 200.0); assertSingleWorkspaceTestDbState(user, workspace, 300.0); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); assertSingleWorkspaceTestDbState(user, workspace, 300.0); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), null, 200.0); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), null, 200.0); } @Test @@ -499,7 +494,7 @@ public void checkFreeTierBillingUsage_combinedProjectsExceedsLimit() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -536,7 +531,7 @@ public void checkFreeTierBillingUsage_twoUsers() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers( + initialCreditsService.checkInitialCreditsUsageForUsers( Sets.newHashSet(user1, user2), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( @@ -567,7 +562,7 @@ public void checkFreeTierBillingUsage_dbUpdate() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -584,7 +579,7 @@ public void checkFreeTierBillingUsage_dbUpdate() { // we do not alert again, but the cost field is updated in the DB - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService, times(1)) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -612,7 +607,7 @@ public void checkFreeTierBillingUsage_singleAlertForExhaustedAndByoBilling() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -627,7 +622,7 @@ public void checkFreeTierBillingUsage_singleAlertForExhaustedAndByoBilling() { } @Test - public void getUserFreeTierDollarLimit_default() { + public void getUserInitialCreditsLimit_default() { final DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); commitTransaction(); @@ -635,16 +630,16 @@ public void getUserFreeTierDollarLimit_default() { final double initialFreeCreditsDollarLimit = 1.0; workbenchConfig.billing.defaultFreeCreditsDollarLimit = initialFreeCreditsDollarLimit; assertWithinBillingTolerance( - initialCreditsService.getUserFreeTierDollarLimit(user), initialFreeCreditsDollarLimit); + initialCreditsService.getUserInitialCreditsLimit(user), initialFreeCreditsDollarLimit); final double fractionalFreeCreditsDollarLimit = 123.456; workbenchConfig.billing.defaultFreeCreditsDollarLimit = fractionalFreeCreditsDollarLimit; assertWithinBillingTolerance( - initialCreditsService.getUserFreeTierDollarLimit(user), fractionalFreeCreditsDollarLimit); + initialCreditsService.getUserInitialCreditsLimit(user), fractionalFreeCreditsDollarLimit); } @Test - public void getUserFreeTierDollarLimit_override() { + public void getUserInitialCreditsLimit_override() { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 123.456; DbUser user = createUser(SINGLE_WORKSPACE_TEST_USER); @@ -653,18 +648,16 @@ public void getUserFreeTierDollarLimit_override() { final double limit1 = 100.0; initialCreditsService.maybeSetDollarLimitOverride(user, limit1); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), null, limit1); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), limit1); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), null, limit1); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), limit1); final double limit2 = 200.0; user = userDao.findUserByUserId(user.getUserId()); initialCreditsService.maybeSetDollarLimitOverride(user, limit2); - verify(mockUserServiceAuditor) - .fireSetFreeTierDollarLimitOverride(user.getUserId(), limit1, limit2); - assertWithinBillingTolerance(initialCreditsService.getUserFreeTierDollarLimit(user), limit2); + verify(mockUserServiceAuditor).fireSetInitialCreditsOverride(user.getUserId(), limit1, limit2); + assertWithinBillingTolerance(initialCreditsService.getUserInitialCreditsLimit(user), limit2); } @Test @@ -684,13 +677,13 @@ public void getUserCachedFreeTierUsage() { commitTransaction(); // we have not yet had a chance to cache this usage - assertThat(initialCreditsService.getCachedFreeTierUsage(user1)).isNull(); - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user1)).isTrue(); + assertThat(initialCreditsService.getCachedInitialCreditsUsage(user1)).isNull(); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user1)).isTrue(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user1), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user1), allBQCosts); - assertWithinBillingTolerance(initialCreditsService.getCachedFreeTierUsage(user1), 100.01); - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user1)).isFalse(); + assertWithinBillingTolerance(initialCreditsService.getCachedInitialCreditsUsage(user1), 100.01); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user1)).isFalse(); TestTransaction.start(); createWorkspace(user1, "another project"); @@ -700,31 +693,34 @@ public void getUserCachedFreeTierUsage() { ImmutableMap.of(SINGLE_WORKSPACE_TEST_PROJECT, 1000.0, "another project", 200.0); // we have not yet cached the new workspace costs - assertWithinBillingTolerance(initialCreditsService.getCachedFreeTierUsage(user1), 100.01); + assertWithinBillingTolerance(initialCreditsService.getCachedInitialCreditsUsage(user1), 100.01); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user1), costs); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user1), costs); final double expectedTotalCachedFreeTierUsage = 1000.0 + 200.0; assertWithinBillingTolerance( - initialCreditsService.getCachedFreeTierUsage(user1), expectedTotalCachedFreeTierUsage); + initialCreditsService.getCachedInitialCreditsUsage(user1), + expectedTotalCachedFreeTierUsage); - initialCreditsService.checkFreeTierBillingUsageForUsers( + initialCreditsService.checkInitialCreditsUsageForUsers( Sets.newHashSet(user1, user2), ImmutableMap.of("project 3", user2Costs)); assertWithinBillingTolerance( - initialCreditsService.getCachedFreeTierUsage(user1), expectedTotalCachedFreeTierUsage); - assertWithinBillingTolerance(initialCreditsService.getCachedFreeTierUsage(user2), user2Costs); - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user2)).isFalse(); + initialCreditsService.getCachedInitialCreditsUsage(user1), + expectedTotalCachedFreeTierUsage); + assertWithinBillingTolerance( + initialCreditsService.getCachedInitialCreditsUsage(user2), user2Costs); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user2)).isFalse(); } @Test public void userHasRemainingFreeTierCredits_newUser() { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; final DbUser user1 = createUser(SINGLE_WORKSPACE_TEST_USER); - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user1)).isTrue(); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user1)).isTrue(); } @Test - public void userHasRemainingFreeTierCredits() { + public void userHasRemainingInitialCredits() { workbenchConfig.billing.defaultFreeCreditsDollarLimit = 100.0; final DbUser user1 = createUser(SINGLE_WORKSPACE_TEST_USER); @@ -735,17 +731,17 @@ public void userHasRemainingFreeTierCredits() { // 99.99 < 100.0 Map allBQCosts = ImmutableMap.of(SINGLE_WORKSPACE_TEST_PROJECT, 99.99); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user1), allBQCosts); - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user1)).isTrue(); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user1), allBQCosts); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user1)).isTrue(); // 100.01 > 100.0 allBQCosts = ImmutableMap.of(SINGLE_WORKSPACE_TEST_PROJECT, 100.01); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user1), allBQCosts); - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user1)).isFalse(); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user1), allBQCosts); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user1)).isFalse(); // 100.01 < 200.0 workbenchConfig.billing.defaultFreeCreditsDollarLimit = 200.0; - assertThat(initialCreditsService.userHasRemainingFreeTierCredits(user1)).isTrue(); + assertThat(initialCreditsService.userHasRemainingInitialCredits(user1)).isTrue(); } @Test @@ -766,7 +762,7 @@ public void test_disableOnlyFreeTierWorkspaces() { commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -797,11 +793,11 @@ public void test_deletedWorkspaceUsageIsConsidered_whenChargeIsPostedAfterCron() workspaceDao.save(workspace); commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); allBQCosts = ImmutableMap.of(SINGLE_WORKSPACE_TEST_PROJECT, 100.1); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), @@ -826,7 +822,7 @@ public void test_deletedWorkspaceUsageIsConsidered_whenAnotherWorkspaceExceedsLi workspaceDao.save(workspace); commitTransaction(); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); TestTransaction.start(); DbWorkspace anotherWorkspace = createWorkspace(user, SINGLE_WORKSPACE_TEST_PROJECT + "4"); @@ -836,7 +832,7 @@ public void test_deletedWorkspaceUsageIsConsidered_whenAnotherWorkspaceExceedsLi allBQCosts.put(SINGLE_WORKSPACE_TEST_PROJECT + "4", 100.1); - initialCreditsService.checkFreeTierBillingUsageForUsers(Sets.newHashSet(user), allBQCosts); + initialCreditsService.checkInitialCreditsUsageForUsers(Sets.newHashSet(user), allBQCosts); verify(taskQueueService) .pushInitialCreditsExhaustionTask( argThat(new UserListMatcher(List.of(user.getUserId()))), diff --git a/api/src/test/java/org/pmiops/workbench/testconfig/fixtures/ReportingUserFixture.java b/api/src/test/java/org/pmiops/workbench/testconfig/fixtures/ReportingUserFixture.java index 0510f0053d6..0bb4e165eb6 100644 --- a/api/src/test/java/org/pmiops/workbench/testconfig/fixtures/ReportingUserFixture.java +++ b/api/src/test/java/org/pmiops/workbench/testconfig/fixtures/ReportingUserFixture.java @@ -160,7 +160,7 @@ public DbUser createEntity() { user.setDisabled(USER__DISABLED); user.setFamilyName(USER__FAMILY_NAME); user.setFirstSignInTime(USER__FIRST_SIGN_IN_TIME); - user.setFreeTierCreditsLimitDollarsOverride(USER__FREE_TIER_CREDITS_LIMIT_DOLLARS_OVERRIDE); + user.setInitialCreditsLimitOverride(USER__FREE_TIER_CREDITS_LIMIT_DOLLARS_OVERRIDE); user.setGivenName(USER__GIVEN_NAME); user.setLastModifiedTime(USER__LAST_MODIFIED_TIME); user.setProfessionalUrl(USER__PROFESSIONAL_URL); diff --git a/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsExpiryTaskMatchers.java b/api/src/test/java/org/pmiops/workbench/utils/ArgumentMatchers.java similarity index 77% rename from api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsExpiryTaskMatchers.java rename to api/src/test/java/org/pmiops/workbench/utils/ArgumentMatchers.java index 264f36a5f74..daa69e08914 100644 --- a/api/src/test/java/org/pmiops/workbench/initialcredits/InitialCreditsExpiryTaskMatchers.java +++ b/api/src/test/java/org/pmiops/workbench/utils/ArgumentMatchers.java @@ -1,4 +1,4 @@ -package org.pmiops.workbench.initialcredits; +package org.pmiops.workbench.utils; import java.util.HashSet; import java.util.List; @@ -6,9 +6,9 @@ import java.util.Set; import org.mockito.ArgumentMatcher; -public class InitialCreditsExpiryTaskMatchers { +public class ArgumentMatchers { - static class UserListMatcher implements ArgumentMatcher> { + public static class UserListMatcher implements ArgumentMatcher> { private final List expectedUserIds; public UserListMatcher(List expectedUserIds) { @@ -23,7 +23,7 @@ public boolean matches(List userIds) { } } - static class MapMatcher implements ArgumentMatcher> { + public static class MapMatcher implements ArgumentMatcher> { private final Map expectedMap;