@@ -25,21 +25,13 @@ export const PROJECTION_SOURCE_ACL_PAGE_TIMEOUT_MS = 60_000
2525/**
2626 * How many times in a row one page may time out before the run fails. Index maintenance was
2727 * observed holding the page for several minutes; with the pauses below a page waits roughly
28- * twelve minutes, so the budget covers one such pass.
28+ * twelve minutes, about fourteen at the jitter's worst, so the budget covers one such pass.
2929 */
3030export const PROJECTION_SOURCE_ACL_PAGE_RETRIES = 12
3131
32- /** Pause before a page is retried: 10 s, doubling to 60 s, with jitter. */
32+ /** Pause before a page is retried: 10 s, doubling to a 60 s base with up to 20% jitter (about 72 s) . */
3333const PAGE_RETRY_PAUSE = { baseMs : 10_000 , maxMs : 60_000 } as const
3434
35- /**
36- * The two ways the database cancels a page: `lock_timeout` (55P03) while the page's index write
37- * waits on a lock the index's background maintenance holds, and `statement_timeout` (57014) when
38- * the page itself runs past {@link PROJECTION_SOURCE_ACL_PAGE_TIMEOUT_MS}. Both pass once the
39- * maintenance moves on, so both are retried the same way.
40- */
41- const PAGE_TIMEOUT_CODES : ReadonlySet < string > = new Set ( [ '55P03' , '57014' ] )
42-
4335/** The SQLSTATE on a driver error, or on the error it wraps. */
4436function postgresErrorCode ( error : unknown ) : string | undefined {
4537 if ( typeof error !== 'object' || error === null ) return undefined
@@ -48,6 +40,28 @@ function postgresErrorCode(error: unknown): string | undefined {
4840 return postgresErrorCode ( ( error as { cause ?: unknown } ) . cause )
4941}
5042
43+ /** The message on a driver error, or on the error it wraps. */
44+ function postgresErrorMessage ( error : unknown ) : string | undefined {
45+ if ( typeof error !== 'object' || error === null ) return undefined
46+ const message = ( error as { message ?: unknown } ) . message
47+ if ( typeof message === 'string' ) return message
48+ return postgresErrorMessage ( ( error as { cause ?: unknown } ) . cause )
49+ }
50+
51+ /**
52+ * The two ways the database cancels a page: `lock_timeout` (55P03) while the page's index write
53+ * waits on a lock the index's background maintenance holds, and `statement_timeout` (57014) when
54+ * the page itself runs past {@link PROJECTION_SOURCE_ACL_PAGE_TIMEOUT_MS}. Both pass once the
55+ * maintenance moves on, so both are retried the same way. 57014 is also what an explicit
56+ * cancellation raises, and that is not retried: only the message tells the two apart.
57+ */
58+ function isPageTimeout ( error : unknown ) : boolean {
59+ const code = postgresErrorCode ( error )
60+ if ( code === '55P03' ) return true
61+ if ( code !== '57014' ) return false
62+ return postgresErrorMessage ( error ) ?. includes ( 'statement timeout' ) ?? false
63+ }
64+
5165/** Pages between progress log lines. */
5266const PROGRESS_EVERY_PAGES = 100
5367
@@ -211,8 +225,8 @@ export async function backfillProjectionSourceAcl(
211225 return row
212226 } )
213227 } catch ( error ) {
228+ if ( ! isPageTimeout ( error ) ) throw error
214229 const code = postgresErrorCode ( error )
215- if ( code === undefined || ! PAGE_TIMEOUT_CODES . has ( code ) ) throw error
216230 timeouts += 1
217231 if ( timeouts > PROJECTION_SOURCE_ACL_PAGE_RETRIES ) throw error
218232 if ( Date . now ( ) >= deadline ) break
@@ -225,6 +239,8 @@ export async function backfillProjectionSourceAcl(
225239 retryInMs : Math . round ( pauseMs ) ,
226240 } )
227241 await sleep ( pauseMs )
242+ /** Checked again after the pause, so a timeout at the budget cannot start another page. */
243+ if ( Date . now ( ) >= deadline ) break
228244 continue
229245 }
230246 timeouts = 0
0 commit comments