11import { describe , it } from 'node:test' ;
2- import { deepStrictEqual , notStrictEqual , strictEqual , throws } from 'node:assert' ;
2+ import { deepStrictEqual , notStrictEqual , ok , strictEqual , throws } from 'node:assert' ;
33import mock from 'ts-mockito' ;
44
55import { V1Namespace , V1NamespaceList , V1ObjectMeta , V1Pod , V1PodList , V1ListMeta } from './api.js' ;
@@ -1561,6 +1561,8 @@ describe('ListWatchCache', () => {
15611561 delayValues . push ( ms ) ;
15621562 return Promise . resolve ( ) ;
15631563 } ,
1564+ // Pin the jitter off so the growth itself can be asserted.
1565+ randFn : ( ) => 0 ,
15641566 } ,
15651567 ) ;
15661568 await promise ;
@@ -1574,15 +1576,15 @@ describe('ListWatchCache', () => {
15741576
15751577 await doneHandler ( null ) ;
15761578 strictEqual ( watchCalls , 3 ) ;
1577- deepStrictEqual ( delayValues , [ 1000 ] ) ;
1579+ deepStrictEqual ( delayValues , [ 800 ] ) ;
15781580
15791581 await doneHandler ( null ) ;
15801582 strictEqual ( watchCalls , 4 ) ;
1581- deepStrictEqual ( delayValues , [ 1000 , 2000 ] ) ;
1583+ deepStrictEqual ( delayValues , [ 800 , 1600 ] ) ;
15821584
15831585 await doneHandler ( null ) ;
15841586 strictEqual ( watchCalls , 5 ) ;
1585- deepStrictEqual ( delayValues , [ 1000 , 2000 , 4000 ] ) ;
1587+ deepStrictEqual ( delayValues , [ 800 , 1600 , 3200 ] ) ;
15861588 } ) ;
15871589
15881590 it ( 'should reset backoff after receiving a watch event' , async ( ) => {
@@ -1617,6 +1619,7 @@ describe('ListWatchCache', () => {
16171619 delayValues . push ( ms ) ;
16181620 return Promise . resolve ( ) ;
16191621 } ,
1622+ randFn : ( ) => 0 ,
16201623 } ,
16211624 ) ;
16221625 await promise ;
@@ -1625,7 +1628,7 @@ describe('ListWatchCache', () => {
16251628
16261629 await doneHandler ( null ) ;
16271630 await doneHandler ( null ) ;
1628- deepStrictEqual ( delayValues , [ 1000 ] ) ;
1631+ deepStrictEqual ( delayValues , [ 800 ] ) ;
16291632
16301633 watchHandler ( 'ADDED' , {
16311634 metadata : { name : 'reset' , namespace : 'default' , resourceVersion : '99' } as V1ObjectMeta ,
@@ -1636,7 +1639,7 @@ describe('ListWatchCache', () => {
16361639 deepStrictEqual ( delayValues , [ ] ) ;
16371640
16381641 await doneHandler ( null ) ;
1639- deepStrictEqual ( delayValues , [ 1000 ] ) ;
1642+ deepStrictEqual ( delayValues , [ 800 ] ) ;
16401643 } ) ;
16411644
16421645 it ( 'should reconnect on TimeoutError' , async ( ) => {
@@ -1718,6 +1721,7 @@ describe('ListWatchCache', () => {
17181721 delayValues . push ( ms ) ;
17191722 return Promise . resolve ( ) ;
17201723 } ,
1724+ randFn : ( ) => 0 ,
17211725 } ,
17221726 ) ;
17231727 await promise ;
@@ -1737,7 +1741,84 @@ describe('ListWatchCache', () => {
17371741
17381742 // Backoff is still applied to non-timeout reconnects.
17391743 await doneHandler ( null ) ;
1740- deepStrictEqual ( delayValues , [ 1000 ] ) ;
1744+ deepStrictEqual ( delayValues , [ 800 ] ) ;
1745+ } ) ;
1746+
1747+ // Builds a cache whose delay, clock and randomness are all observable, so
1748+ // the backoff can be driven deterministically.
1749+ async function setupBackoffCache ( opts : { delays : number [ ] ; now ?: ( ) => number ; rand ?: ( ) => number } ) {
1750+ const fakeWatch = mock . mock ( Watch ) ;
1751+ const listObj = {
1752+ metadata : { resourceVersion : '12345' } as V1ListMeta ,
1753+ items : [ ] as V1Namespace [ ] ,
1754+ } as V1NamespaceList ;
1755+ const listFn : ListPromise < V1Namespace > = ( ) => Promise . resolve ( listObj ) ;
1756+
1757+ const promise = new Promise ( ( resolve ) => {
1758+ mock . when (
1759+ fakeWatch . watch ( mock . anything ( ) , mock . anything ( ) , mock . anything ( ) , mock . anything ( ) ) ,
1760+ ) . thenCall ( ( ) => {
1761+ resolve ( new AbortController ( ) ) ;
1762+ return Promise . resolve ( new AbortController ( ) ) ;
1763+ } ) ;
1764+ } ) ;
1765+
1766+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1767+ const cache = new ListWatch (
1768+ '/some/path' ,
1769+ mock . instance ( fakeWatch ) ,
1770+ listFn ,
1771+ true ,
1772+ undefined ,
1773+ undefined ,
1774+ {
1775+ delayFn : ( ms : number ) => {
1776+ opts . delays . push ( ms ) ;
1777+ return Promise . resolve ( ) ;
1778+ } ,
1779+ nowFn : opts . now ,
1780+ randFn : opts . rand ,
1781+ } ,
1782+ ) ;
1783+ await promise ;
1784+ const [ , , , doneHandler ] = mock . capture ( fakeWatch . watch ) . last ( ) ;
1785+ return { done : doneHandler } ;
1786+ }
1787+
1788+ it ( 'should spread the backoff over a jittered range' , async ( ) => {
1789+ // Full jitter: each delay lands in [level, 2*level). Two clients that
1790+ // failed together therefore do not retry together.
1791+ const low : number [ ] = [ ] ;
1792+ const high : number [ ] = [ ] ;
1793+ const lowCache = await setupBackoffCache ( { delays : low , rand : ( ) => 0 } ) ;
1794+ const highCache = await setupBackoffCache ( { delays : high , rand : ( ) => 0.999 } ) ;
1795+
1796+ for ( let i = 0 ; i < 3 ; i ++ ) {
1797+ await lowCache . done ( null ) ;
1798+ await highCache . done ( null ) ;
1799+ }
1800+
1801+ deepStrictEqual ( low , [ 800 , 1600 ] ) ;
1802+ ok ( high [ 0 ] > 800 && high [ 0 ] < 1600 , `expected [800, 1600), got ${ high [ 0 ] } ` ) ;
1803+ ok ( high [ 1 ] > 1600 && high [ 1 ] < 3200 , `expected [1600, 3200), got ${ high [ 1 ] } ` ) ;
1804+ } ) ;
1805+
1806+ it ( 'should cap the backoff and reset it once idle' , async ( ) => {
1807+ const delays : number [ ] = [ ] ;
1808+ let now = 0 ;
1809+ const h = await setupBackoffCache ( { delays, now : ( ) => now , rand : ( ) => 0 } ) ;
1810+
1811+ for ( let i = 0 ; i < 12 ; i ++ ) {
1812+ await h . done ( null ) ;
1813+ }
1814+ strictEqual ( delays [ delays . length - 1 ] , 30000 , 'expected the delay to cap at 30s' ) ;
1815+
1816+ // Two idle minutes means the previous failures no longer count.
1817+ now += 120000 ;
1818+ await h . done ( null ) ;
1819+ strictEqual ( delays [ delays . length - 1 ] , 30000 , 'the pending delay is still the capped one' ) ;
1820+ await h . done ( null ) ;
1821+ strictEqual ( delays [ delays . length - 1 ] , 800 , 'expected the backoff to start over' ) ;
17411822 } ) ;
17421823} ) ;
17431824
0 commit comments