@@ -26,8 +26,11 @@ export const OAUTH_TOKEN_RETENTION_DAYS = 7
2626 * batch small independently of direct access-token deletion.
2727 */
2828const OAUTH_FAMILY_SWEEP_LIMIT = 10
29+ const OAUTH_FAMILY_SWEEP_MAX_PAGES = 5_000
2930const OAUTH_ACCESS_TOKEN_SWEEP_LIMIT = 5_000
30- const OAUTH_TOKEN_SWEEP_MAX_PAGES = 10
31+ const OAUTH_ACCESS_TOKEN_SWEEP_MAX_PAGES = 10
32+ /** Stops admitting batches before the Helm cron's 60-second request timeout. */
33+ const OAUTH_TOKEN_SWEEP_BUDGET_MS = 45_000
3134
3235export interface CleanupOAuthTokensResult {
3336 tokenFamilies : number
@@ -117,60 +120,79 @@ async function deleteExpiredFamilyBatch(
117120 * later reuse go unnoticed while descendants remained active. The family row
118121 * therefore owns retention and cascades every generation when it expires.
119122 *
120- * Families go first and their refresh/access tokens follow by cascade. The
121- * second pass catches expired access tokens for still-live families and access
122- * tokens issued without a refresh grant. Both passes use bounded indexed pages.
123+ * Interleaves family and access-token pages so neither backlog prevents the
124+ * other from making progress before the deadline. Family deletion cascades its
125+ * remaining tokens; access pages also catch tokens from still-live families or
126+ * grants without refresh tokens. Both sweeps retain a 50,000-row run cap while
127+ * family transactions stay small enough to bound their descendant cascades.
123128 */
124129export async function runCleanupOAuthTokens ( ) : Promise < CleanupOAuthTokensResult > {
125- const cutoff = new Date ( Date . now ( ) - OAUTH_TOKEN_RETENTION_DAYS * 24 * 60 * 60 * 1000 )
130+ const startedAt = Date . now ( )
131+ const deadline = startedAt + OAUTH_TOKEN_SWEEP_BUDGET_MS
132+ const cutoff = new Date ( startedAt - OAUTH_TOKEN_RETENTION_DAYS * 24 * 60 * 60 * 1000 )
126133 let tokenFamilies = 0
127134 let accessTokens = 0
135+ let moreFamilies = true
136+ let moreAccessTokens = true
128137
129- for ( let page = 0 ; page < OAUTH_TOKEN_SWEEP_MAX_PAGES ; page += 1 ) {
130- const staleFamilies = await db
131- . select ( {
132- id : oauthTokenFamily . id ,
133- clientId : oauthTokenFamily . clientId ,
134- sessionId : oauthTokenFamily . sessionId ,
135- userId : oauthTokenFamily . userId ,
136- consentId : oauthTokenFamily . consentId ,
137- } )
138- . from ( oauthTokenFamily )
139- . where ( lt ( oauthTokenFamily . expiresAt , cutoff ) )
140- . orderBy ( asc ( oauthTokenFamily . expiresAt ) , asc ( oauthTokenFamily . id ) )
141- . limit ( OAUTH_FAMILY_SWEEP_LIMIT )
142- if ( staleFamilies . length === 0 ) break
138+ for ( let page = 0 ; page < OAUTH_FAMILY_SWEEP_MAX_PAGES ; page += 1 ) {
139+ if ( Date . now ( ) >= deadline || ( ! moreFamilies && ! moreAccessTokens ) ) break
143140
144- tokenFamilies += await deleteExpiredFamilyBatch ( staleFamilies , cutoff )
145- if ( staleFamilies . length < OAUTH_FAMILY_SWEEP_LIMIT ) break
146- }
141+ if ( moreFamilies ) {
142+ const staleFamilies = await db
143+ . select ( {
144+ id : oauthTokenFamily . id ,
145+ clientId : oauthTokenFamily . clientId ,
146+ sessionId : oauthTokenFamily . sessionId ,
147+ userId : oauthTokenFamily . userId ,
148+ consentId : oauthTokenFamily . consentId ,
149+ } )
150+ . from ( oauthTokenFamily )
151+ . where ( lt ( oauthTokenFamily . expiresAt , cutoff ) )
152+ . orderBy ( asc ( oauthTokenFamily . expiresAt ) , asc ( oauthTokenFamily . id ) )
153+ . limit ( OAUTH_FAMILY_SWEEP_LIMIT )
154+ if ( Date . now ( ) >= deadline ) break
147155
148- for ( let page = 0 ; page < OAUTH_TOKEN_SWEEP_MAX_PAGES ; page += 1 ) {
149- const staleAccess = await db
150- . select ( { id : oauthAccessToken . id } )
151- . from ( oauthAccessToken )
152- . where ( lt ( oauthAccessToken . expiresAt , cutoff ) )
153- . orderBy ( asc ( oauthAccessToken . expiresAt ) , asc ( oauthAccessToken . id ) )
154- . limit ( OAUTH_ACCESS_TOKEN_SWEEP_LIMIT )
155- if ( staleAccess . length === 0 ) break
156+ if ( staleFamilies . length > 0 ) {
157+ tokenFamilies += await deleteExpiredFamilyBatch ( staleFamilies , cutoff )
158+ }
159+ moreFamilies = staleFamilies . length === OAUTH_FAMILY_SWEEP_LIMIT
160+ }
156161
157- const deleted = await db
158- . delete ( oauthAccessToken )
159- . where (
160- inArray (
161- oauthAccessToken . id ,
162- staleAccess . map ( ( row ) => row . id )
163- )
164- )
165- . returning ( { id : oauthAccessToken . id } )
166- accessTokens += deleted . length
167- if ( staleAccess . length < OAUTH_ACCESS_TOKEN_SWEEP_LIMIT ) break
162+ if ( Date . now ( ) >= deadline ) break
163+ if ( moreAccessTokens && page < OAUTH_ACCESS_TOKEN_SWEEP_MAX_PAGES ) {
164+ const staleAccess = await db
165+ . select ( { id : oauthAccessToken . id } )
166+ . from ( oauthAccessToken )
167+ . where ( lt ( oauthAccessToken . expiresAt , cutoff ) )
168+ . orderBy ( asc ( oauthAccessToken . expiresAt ) , asc ( oauthAccessToken . id ) )
169+ . limit ( OAUTH_ACCESS_TOKEN_SWEEP_LIMIT )
170+ if ( Date . now ( ) >= deadline ) break
171+
172+ if ( staleAccess . length > 0 ) {
173+ const deleted = await db
174+ . delete ( oauthAccessToken )
175+ . where (
176+ inArray (
177+ oauthAccessToken . id ,
178+ staleAccess . map ( ( row ) => row . id )
179+ )
180+ )
181+ . returning ( { id : oauthAccessToken . id } )
182+ accessTokens += deleted . length
183+ }
184+ moreAccessTokens =
185+ staleAccess . length === OAUTH_ACCESS_TOKEN_SWEEP_LIMIT &&
186+ page + 1 < OAUTH_ACCESS_TOKEN_SWEEP_MAX_PAGES
187+ }
168188 }
169189
170190 const result = { tokenFamilies, accessTokens }
171191 logger . info ( 'Swept expired OAuth tokens' , {
172192 ...result ,
173193 retentionDays : OAUTH_TOKEN_RETENTION_DAYS ,
194+ elapsedMs : Date . now ( ) - startedAt ,
195+ deadlineReached : Date . now ( ) >= deadline ,
174196 } )
175197 return result
176198}
0 commit comments