@@ -44,10 +44,12 @@ describe('retryOnLockTimeout', () => {
4444 expect ( clock . sleep ) . toHaveBeenCalledTimes ( 19 )
4545 } )
4646
47- it ( 'throws the last lock timeout once the next retry would end past the budget ' , async ( ) => {
47+ it ( 'starts no attempt after the budget and throws the last lock timeout ' , async ( ) => {
4848 const clock = fakeClock ( )
4949 const onRetry = vi . fn ( )
50+ const startedAt : number [ ] = [ ]
5051 const attempt = vi . fn ( async ( ) => {
52+ startedAt . push ( clock . now ( ) )
5153 clock . advance ( 5_000 )
5254 throw pgError ( '55P03' )
5355 } )
@@ -62,13 +64,33 @@ describe('retryOnLockTimeout', () => {
6264 } )
6365 ) . rejects . toMatchObject ( { code : '55P03' } )
6466
65- expect ( clock . now ( ) ) . toBeLessThan ( 2 * 60_000 )
66- for ( const [ { elapsedMs, delayMs } ] of onRetry . mock . calls ) {
67- expect ( elapsedMs + delayMs ) . toBeLessThan ( 2 * 60_000 )
68- }
67+ for ( const start of startedAt ) expect ( start ) . toBeLessThan ( 2 * 60_000 )
68+ /** The last attempt may run one lock timeout past the budget, never more. */
69+ expect ( clock . now ( ) ) . toBeLessThan ( 2 * 60_000 + 5_000 )
6970 expect ( attempt ) . toHaveBeenCalledTimes ( onRetry . mock . calls . length + 1 )
7071 } )
7172
73+ it ( 'does not start an attempt when a timer resolves after the budget' , async ( ) => {
74+ let nowMs = 0
75+ const attempt = vi . fn ( async ( ) => {
76+ nowMs += 1_000
77+ throw pgError ( '55P03' )
78+ } )
79+
80+ await expect (
81+ retryOnLockTimeout ( attempt , {
82+ budgetMs : 60_000 ,
83+ backoff : BACKOFF ,
84+ now : ( ) => nowMs ,
85+ /** The process stalls: the timer fires long after its delay. */
86+ sleep : async ( ) => {
87+ nowMs += 120_000
88+ } ,
89+ } )
90+ ) . rejects . toMatchObject ( { code : '55P03' } )
91+ expect ( attempt ) . toHaveBeenCalledOnce ( )
92+ } )
93+
7294 it ( 'finds a lock timeout wrapped in a cause chain' , async ( ) => {
7395 const clock = fakeClock ( )
7496 let calls = 0
0 commit comments