diff --git a/components/gitpod-protocol/src/util/garbage-collected-cache.ts b/components/gitpod-protocol/src/util/garbage-collected-cache.ts index 303759e46914f2..3136be4a9a15c1 100644 --- a/components/gitpod-protocol/src/util/garbage-collected-cache.ts +++ b/components/gitpod-protocol/src/util/garbage-collected-cache.ts @@ -37,6 +37,11 @@ export class GarbageCollectedCache { if (!entry) { return undefined; } + // Still valid? + if (entry.expiryDate < Date.now()) { + this.store.delete(entry.key); + return undefined; + } return entry.value; } diff --git a/components/server/ee/src/billing/entitlement-service-chargebee.ts b/components/server/ee/src/billing/entitlement-service-chargebee.ts index 9c7386eab5f3ea..c35f5866e07e4e 100644 --- a/components/server/ee/src/billing/entitlement-service-chargebee.ts +++ b/components/server/ee/src/billing/entitlement-service-chargebee.ts @@ -57,12 +57,7 @@ export class EntitlementServiceChargebee implements EntitlementService { hasHitParallelWorkspaceLimit(), ]); - const result = enoughCredits && !hitParallelWorkspaceLimit; - - console.log("mayStartWorkspace > hitParallelWorkspaceLimit " + hitParallelWorkspaceLimit); - return { - mayStart: result, oufOfCredits: !enoughCredits, hitParallelWorkspaceLimit, }; @@ -84,10 +79,9 @@ export class EntitlementServiceChargebee implements EntitlementService { runningInstances: Promise, ): Promise { // As retrieving a full AccountStatement is expensive we want to cache it as much as possible. - const cachedAccountStatement = this.accountStatementProvider.getCachedStatement(); + const cachedAccountStatement = this.accountStatementProvider.getCachedStatement(userId); const lowerBound = this.getRemainingUsageHoursLowerBound(cachedAccountStatement, date.toISOString()); if (lowerBound && (lowerBound === "unlimited" || lowerBound > Accounting.MINIMUM_CREDIT_FOR_OPEN_IN_HOURS)) { - console.log("checkEnoughCreditForWorkspaceStart > unlimited"); return true; } @@ -96,7 +90,6 @@ export class EntitlementServiceChargebee implements EntitlementService { date.toISOString(), runningInstances, ); - console.log("checkEnoughCreditForWorkspaceStart > remainingUsageHours " + remainingUsageHours); return remainingUsageHours > Accounting.MINIMUM_CREDIT_FOR_OPEN_IN_HOURS; } @@ -115,7 +108,7 @@ export class EntitlementServiceChargebee implements EntitlementService { return "unlimited"; } - const diffInMillis = new Date(cachedStatement.endDate).getTime() - new Date(date).getTime(); + const diffInMillis = Math.max(0, new Date(cachedStatement.endDate).getTime() - new Date(date).getTime()); const maxPossibleUsage = millisecondsToHours(diffInMillis) * MAX_PARALLEL_WORKSPACES; return cachedStatement.remainingHours - maxPossibleUsage; } diff --git a/components/server/ee/src/billing/entitlement-service-license.ts b/components/server/ee/src/billing/entitlement-service-license.ts index 86eb96e58ca853..ac34745d0fd624 100644 --- a/components/server/ee/src/billing/entitlement-service-license.ts +++ b/components/server/ee/src/billing/entitlement-service-license.ts @@ -30,7 +30,7 @@ export class EntitlementServiceLicense implements EntitlementService { runningInstances: Promise, ): Promise { // if payment is not enabled users can start as many parallel workspaces as they want - return { mayStart: true }; + return {}; } async maySetTimeout(user: User, date: Date): Promise { diff --git a/components/server/ee/src/billing/entitlement-service-ubp.ts b/components/server/ee/src/billing/entitlement-service-ubp.ts index ef7a275efd6158..93d2420b2881e7 100644 --- a/components/server/ee/src/billing/entitlement-service-ubp.ts +++ b/components/server/ee/src/billing/entitlement-service-ubp.ts @@ -57,9 +57,7 @@ export class EntitlementServiceUBP implements EntitlementService { this.checkSpendingLimitReached(user, date), hasHitParallelWorkspaceLimit(), ]); - const result = !spendingLimitReachedOnCostCenter && !hitParallelWorkspaceLimit; return { - mayStart: result, spendingLimitReachedOnCostCenter, hitParallelWorkspaceLimit, }; diff --git a/components/server/ee/src/billing/entitlement-service.ts b/components/server/ee/src/billing/entitlement-service.ts index d7537b7af6c4e1..61eb0269f9e96f 100644 --- a/components/server/ee/src/billing/entitlement-service.ts +++ b/components/server/ee/src/billing/entitlement-service.ts @@ -43,29 +43,23 @@ export class EntitlementServiceImpl implements EntitlementService { const verification = await this.verificationService.needsVerification(user); if (verification) { return { - mayStart: false, needsVerification: true, }; } const billingMode = await this.billingModes.getBillingModeForUser(user, date); - let result; switch (billingMode.mode) { case "none": - result = await this.license.mayStartWorkspace(user, date, runningInstances); - break; + return this.license.mayStartWorkspace(user, date, runningInstances); case "chargebee": - result = await this.chargebee.mayStartWorkspace(user, date, runningInstances); - break; + return this.chargebee.mayStartWorkspace(user, date, runningInstances); case "usage-based": - result = await this.ubp.mayStartWorkspace(user, date, runningInstances); - break; + return this.ubp.mayStartWorkspace(user, date, runningInstances); default: throw new Error("Unsupported billing mode: " + (billingMode as any).mode); // safety net } - return result; } catch (err) { log.error({ userId: user.id }, "EntitlementService error: mayStartWorkspace", err); - throw err; + return {}; // When there is an EntitlementService error, we never want to break workspace starts } } diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index 93d8fa1a72a747..299a1e2ae8fc4d 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -94,13 +94,13 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is // GitpodServerImpl (stateful per user) rebind(GitpodServerImpl).to(GitpodServerEEImpl).inRequestScope(); bind(EligibilityService).toSelf().inRequestScope(); - bind(AccountStatementProvider).toSelf().inRequestScope(); // various rebind(HostContainerMapping).to(HostContainerMappingEE).inSingletonScope(); bind(EMailDomainService).to(EMailDomainServiceImpl).inSingletonScope(); rebind(BlockedUserFilter).toService(EMailDomainService); bind(SnapshotService).toSelf().inSingletonScope(); + bind(AccountStatementProvider).toSelf().inSingletonScope(); bind(UserDeletionServiceEE).toSelf().inSingletonScope(); rebind(UserDeletionService).to(UserDeletionServiceEE).inSingletonScope(); diff --git a/components/server/ee/src/user/account-statement-provider.ts b/components/server/ee/src/user/account-statement-provider.ts index c6388710793c8d..42e97babed8fee 100644 --- a/components/server/ee/src/user/account-statement-provider.ts +++ b/components/server/ee/src/user/account-statement-provider.ts @@ -8,6 +8,7 @@ import { injectable, inject } from "inversify"; import { WorkspaceInstance } from "@gitpod/gitpod-protocol"; import { AccountStatement } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; import { AccountService } from "@gitpod/gitpod-payment-endpoint/lib/accounting"; +import { GarbageCollectedCache } from "@gitpod/gitpod-protocol/lib/util/garbage-collected-cache"; export type CachedAccountStatement = Pick; @@ -19,20 +20,18 @@ export type CachedAccountStatement = Pick(5 * 60, 10 * 60); - setCachedStatement(cachedStatement: CachedAccountStatement) { - this.cachedStatement = cachedStatement; - } - - getCachedStatement(): CachedAccountStatement | undefined { - return this.cachedStatement; + getCachedStatement(userId: string): CachedAccountStatement | undefined { + return this.cachedStatements.get(userId); } async getAccountStatement(userId: string, date: string): Promise { const statement = await this.accountService.getAccountStatement(userId, date); - // Fill cache - this.setCachedStatement({ + this.cachedStatements.set(userId, { remainingHours: statement.remainingHours, endDate: statement.endDate, }); diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index ae7ebfd87486a2..301bb7ba413899 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -108,7 +108,7 @@ import { UserCounter } from "../user/user-counter"; import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { CachingUsageServiceClientProvider, UsageService } from "@gitpod/usage-api/lib/usage/v1/sugar"; import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; -import { EntitlementService } from "../../../src/billing/entitlement-service"; +import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billing/entitlement-service"; import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-mode"; import { BillingModes } from "../billing/billing-mode"; import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server"; @@ -256,15 +256,14 @@ export class GitpodServerEEImpl extends GitpodServerImpl { ): Promise { await super.mayStartWorkspace(ctx, user, runningInstances); - let result; + let result: MayStartWorkspaceResult = {}; try { result = await this.entitlementService.mayStartWorkspace(user, new Date(), runningInstances); - } catch (error) { - throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, `Error in Entitlement Service.`); - } - log.info("mayStartWorkspace", { result }); - if (result.mayStart) { - return; // green light from entitlement service + TraceContext.addNestedTags(ctx, { mayStartWorkspace: { result } }); + } catch (err) { + log.error({ userId: user.id }, "EntitlementSerivce.mayStartWorkspace error", err); + TraceContext.setError(ctx, err); + return; // we don't want to block workspace starts because of internal errors } if (!!result.needsVerification) { throw new ResponseError(ErrorCodes.NEEDS_VERIFICATION, `Please verify your account.`); diff --git a/components/server/src/billing/entitlement-service.ts b/components/server/src/billing/entitlement-service.ts index a77b4adbe7aed3..be233bb056abf6 100644 --- a/components/server/src/billing/entitlement-service.ts +++ b/components/server/src/billing/entitlement-service.ts @@ -14,10 +14,7 @@ import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution"; import { injectable } from "inversify"; export interface MayStartWorkspaceResult { - mayStart: boolean; - hitParallelWorkspaceLimit?: HitParallelWorkspaceLimit; - oufOfCredits?: boolean; needsVerification?: boolean; @@ -83,7 +80,7 @@ export class CommunityEntitlementService implements EntitlementService { date: Date, runningInstances: Promise, ): Promise { - return { mayStart: true }; + return {}; } async maySetTimeout(user: User, date: Date): Promise {