Skip to content

Commit e6d86b9

Browse files
committed
improvement(billing): skip locked rows in the identity purge, share the auth-request lookup
Concurrent account deletions no longer wait on each other's purge batch. syncUsageLimitsFromSubscription reads in parallel again, and email canonicalization reuses normalizeEmail.
1 parent a5be6a6 commit e6d86b9

2 files changed

Lines changed: 26 additions & 25 deletions

File tree

‎apps/sim/lib/billing/core/usage.ts‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ function calculateOrgUsageLimit(
191191
*/
192192
const usersCreatedInAuthRequest = new WeakMap<object, string>()
193193

194+
function isUserCreatedInAuthRequest(
195+
userId: string,
196+
authRequest: object | null | undefined
197+
): boolean {
198+
return authRequest ? usersCreatedInAuthRequest.get(authRequest) === userId : false
199+
}
200+
194201
/**
195202
* Handle new user setup when they join the platform.
196203
* Creates the userStats record with the free allowance, which is zero when the
@@ -253,7 +260,7 @@ export async function handleNewSignInAccount(
253260
},
254261
authRequest: object | null | undefined
255262
): Promise<void> {
256-
if (!authRequest || usersCreatedInAuthRequest.get(authRequest) !== userId) return
263+
if (!isUserCreatedInAuthRequest(userId, authRequest)) return
257264
if (!(await isDeletedAccountIdentity({ oauthSubjects: [{ providerId, accountId }] }))) return
258265
await forfeitFreeTierAllowance(userId)
259266
}
@@ -289,9 +296,7 @@ export async function ensureUserStatsExists(
289296
if (created.length === 0) return
290297

291298
try {
292-
const includeSignIns = authRequest
293-
? usersCreatedInAuthRequest.get(authRequest) === userId
294-
: false
299+
const includeSignIns = isUserCreatedInAuthRequest(userId, authRequest)
295300
if (await userHasDeletedAccountIdentity(userId, { includeSignIns })) {
296301
await forfeitFreeTierAllowance(userId)
297302
}
@@ -705,15 +710,10 @@ export async function syncUsageLimitsFromSubscription(
705710
userId: string,
706711
executor: DbOrTx = db
707712
): Promise<void> {
708-
const subscription = await getHighestPriorityPersonalSubscription(userId, {
709-
onError: 'throw',
710-
executor,
711-
})
712-
const currentUserStats = await executor
713-
.select()
714-
.from(userStats)
715-
.where(eq(userStats.userId, userId))
716-
.limit(1)
713+
const [subscription, currentUserStats] = await Promise.all([
714+
getHighestPriorityPersonalSubscription(userId, { onError: 'throw', executor }),
715+
executor.select().from(userStats).where(eq(userStats.userId, userId)).limit(1),
716+
])
717717

718718
if (currentUserStats.length === 0) {
719719
throw new Error(`User stats not found for userId: ${userId}`)

‎apps/sim/lib/users/deleted-identity.ts‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { account, deletedAccountIdentity, user } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { hmacSha256Hex } from '@sim/security/hmac'
55
import { getErrorMessage } from '@sim/utils/errors'
6+
import { normalizeEmail } from '@sim/utils/string'
67
import { and, eq, gt, inArray, lte, sql } from 'drizzle-orm'
78
import { env } from '@/lib/core/config/env'
89
import { isBillingEnabled } from '@/lib/core/config/env-flags'
@@ -102,7 +103,7 @@ function getIdentityKey(): string {
102103
* `googlemail.com` alias.
103104
*/
104105
function canonicalizeEmail(email: string): string | null {
105-
const normalized = email.trim().toLowerCase()
106+
const normalized = normalizeEmail(email)
106107
const at = normalized.lastIndexOf('@')
107108
if (at <= 0 || at === normalized.length - 1) return null
108109
let local = normalized.slice(0, at)
@@ -164,18 +165,18 @@ export async function recordDeletedAccountIdentities(tx: DbOrTx, userId: string)
164165
const hashes = identityHashes(await loadSignInIdentity(savepoint, userId))
165166
if (hashes.length === 0) return
166167

167-
await savepoint
168-
.delete(deletedAccountIdentity)
169-
.where(
170-
inArray(
171-
deletedAccountIdentity.identityHash,
172-
savepoint
173-
.select({ identityHash: deletedAccountIdentity.identityHash })
174-
.from(deletedAccountIdentity)
175-
.where(lte(deletedAccountIdentity.expiresAt, sql`now()`))
176-
.limit(EXPIRED_PURGE_BATCH)
177-
)
168+
await savepoint.delete(deletedAccountIdentity).where(
169+
inArray(
170+
deletedAccountIdentity.identityHash,
171+
savepoint
172+
.select({ identityHash: deletedAccountIdentity.identityHash })
173+
.from(deletedAccountIdentity)
174+
.where(lte(deletedAccountIdentity.expiresAt, sql`now()`))
175+
.limit(EXPIRED_PURGE_BATCH)
176+
// Concurrent deletions skip each other's purge batch instead of waiting on its commit.
177+
.for('update', { skipLocked: true })
178178
)
179+
)
179180

180181
const expiresAt = new Date(Date.now() + DELETED_IDENTITY_RETENTION_MS)
181182
await savepoint

0 commit comments

Comments
 (0)