@@ -22,8 +22,6 @@ const provider = new ethers.providers.StaticJsonRpcProvider(rpcUrl, {
22
22
name : chainId ,
23
23
} ) ;
24
24
25
- jest . mock ( '../state' ) ;
26
-
27
25
describe ( updateFeedsModule . startUpdateFeedsLoops . name , ( ) => {
28
26
it ( 'starts staggered update loops for a chain' , async ( ) => {
29
27
jest . spyOn ( stateModule , 'getState' ) . mockReturnValue (
@@ -41,6 +39,7 @@ describe(updateFeedsModule.startUpdateFeedsLoops.name, () => {
41
39
} ,
42
40
} )
43
41
) ;
42
+ jest . spyOn ( stateModule , 'updateState' ) . mockImplementation ( ) ;
44
43
jest . spyOn ( updateFeedsModule , 'runUpdateFeeds' ) . mockImplementation ( ) ;
45
44
const intervalCalls = [ ] as number [ ] ;
46
45
jest . spyOn ( global , 'setInterval' ) . mockImplementation ( ( ( ) => {
@@ -100,6 +99,7 @@ describe(updateFeedsModule.startUpdateFeedsLoops.name, () => {
100
99
} ,
101
100
} )
102
101
) ;
102
+ jest . spyOn ( stateModule , 'updateState' ) . mockImplementation ( ) ;
103
103
jest . spyOn ( updateFeedsModule , 'runUpdateFeeds' ) . mockImplementation ( ) ;
104
104
const intervalCalls = [ ] as number [ ] ;
105
105
jest . spyOn ( global , 'setInterval' ) . mockImplementation ( ( ( ) => {
@@ -171,7 +171,7 @@ describe(updateFeedsModule.runUpdateFeeds.name, () => {
171
171
expect ( logger . error ) . toHaveBeenCalledWith ( 'Failed to get first active dAPIs batch' , new Error ( 'provider-error' ) ) ;
172
172
} ) ;
173
173
174
- it ( 'fetches other batches in a staggered way and logs errors' , async ( ) => {
174
+ it ( 'fetches and processes other batches in a staggered way and logs errors' , async ( ) => {
175
175
// Prepare the mocked contract so it returns three batches (of size 1) of dAPIs and the second batch fails to load.
176
176
const firstDapi = generateReadDapiWithIndexResponse ( ) ;
177
177
const thirdDapi = generateReadDapiWithIndexResponse ( ) ;
@@ -206,13 +206,21 @@ describe(updateFeedsModule.runUpdateFeeds.name, () => {
206
206
gasPriceStore : { } ,
207
207
} )
208
208
) ;
209
+ jest . spyOn ( stateModule , 'updateState' ) . mockImplementation ( ) ;
209
210
jest
210
211
. spyOn ( checkFeedsModule , 'getUpdatableFeeds' )
211
- . mockResolvedValue ( [
212
- allowPartial < updateTransactionModule . UpdatableDapi > ( { dapiInfo : firstDapi } ) ,
213
- allowPartial < updateTransactionModule . UpdatableDapi > ( { dapiInfo : thirdDapi } ) ,
214
- ] ) ;
212
+ . mockResolvedValueOnce ( [ allowPartial < updateTransactionModule . UpdatableDapi > ( { dapiInfo : firstDapi } ) ] )
213
+ . mockResolvedValueOnce ( [ allowPartial < updateTransactionModule . UpdatableDapi > ( { dapiInfo : thirdDapi } ) ] ) ;
215
214
jest . spyOn ( updateTransactionModule , 'updateFeeds' ) . mockResolvedValue ( [ null , null ] ) ;
215
+ const processBatchCalls = [ ] as number [ ] ;
216
+ // eslint-disable-next-line @typescript-eslint/require-await
217
+ const originalProcessBatch = updateFeedsModule . processBatch ;
218
+ jest
219
+ . spyOn ( updateFeedsModule , 'processBatch' )
220
+ . mockImplementation ( async ( ...args : Parameters < typeof originalProcessBatch > ) => {
221
+ processBatchCalls . push ( Date . now ( ) ) ;
222
+ return originalProcessBatch ( ...args ) ;
223
+ } ) ;
216
224
217
225
await updateFeedsModule . runUpdateFeeds (
218
226
'provider-name' ,
@@ -231,7 +239,11 @@ describe(updateFeedsModule.runUpdateFeeds.name, () => {
231
239
expect ( utilsModule . sleep ) . toHaveBeenCalledTimes ( 3 ) ;
232
240
expect ( sleepCalls [ 0 ] ) . toBeGreaterThan ( 0 ) ; // The first stagger time is computed dynamically (the execution time is subtracted from the interval time) which is slow on CI, so we just check it's non-zero.
233
241
expect ( sleepCalls [ 1 ] ) . toBe ( 0 ) ;
234
- expect ( sleepCalls [ 2 ] ) . toBe ( 49.999_999_999_999_99 ) ; // Stagger time is actually 150 / 3 = 50, but there is a rounding error.
242
+ expect ( sleepCalls [ 2 ] ) . toBe ( 50 ) ;
243
+
244
+ // Expect the call times of processBatch to be staggered as well.
245
+ expect ( updateFeedsModule . processBatch ) . toHaveBeenCalledTimes ( 2 ) ;
246
+ expect ( processBatchCalls [ 1 ] ! - processBatchCalls [ 0 ] ! ) . toBeGreaterThanOrEqual ( 100 ) ; // The stagger time is 50ms, but second batch fails to load which means the third second processBatch call needs to happen after we wait for 2 stagger times.
235
247
236
248
// Expect the logs to be called with the correct context.
237
249
expect ( logger . error ) . toHaveBeenCalledTimes ( 1 ) ;
@@ -244,7 +256,7 @@ describe(updateFeedsModule.runUpdateFeeds.name, () => {
244
256
expect ( logger . debug ) . toHaveBeenNthCalledWith ( 2 , 'Processing batch of active dAPIs' , expect . anything ( ) ) ;
245
257
expect ( logger . debug ) . toHaveBeenNthCalledWith ( 3 , 'Fetching batches of active dAPIs' , {
246
258
batchesCount : 3 ,
247
- staggerTime : 49.999_999_999_999_99 ,
259
+ staggerTime : 50 ,
248
260
} ) ;
249
261
expect ( logger . debug ) . toHaveBeenNthCalledWith ( 4 , 'Fetching batch of active dAPIs' , {
250
262
batchIndex : 1 ,
@@ -254,9 +266,9 @@ describe(updateFeedsModule.runUpdateFeeds.name, () => {
254
266
} ) ;
255
267
expect ( logger . debug ) . toHaveBeenNthCalledWith ( 6 , 'Processing batch of active dAPIs' , expect . anything ( ) ) ;
256
268
expect ( logger . debug ) . toHaveBeenNthCalledWith ( 7 , 'Finished processing batches of active dAPIs' , {
257
- batchesCount : 3 ,
258
- errorCount : 4 ,
259
- successCount : 0 ,
269
+ dapiUpdateFailures : 2 ,
270
+ dapiUpdates : 0 ,
271
+ skippedBatchesCount : 1 ,
260
272
} ) ;
261
273
} ) ;
262
274
@@ -280,6 +292,7 @@ describe(updateFeedsModule.runUpdateFeeds.name, () => {
280
292
gasPriceStore : { } ,
281
293
} )
282
294
) ;
295
+ jest . spyOn ( stateModule , 'updateState' ) . mockImplementation ( ) ;
283
296
jest . spyOn ( logger , 'error' ) ;
284
297
jest . spyOn ( checkFeedsModule , 'getUpdatableFeeds' ) . mockRejectedValueOnce ( new Error ( 'unexpected-unhandled-error' ) ) ;
285
298
@@ -364,3 +377,35 @@ describe(updateFeedsModule.processBatch.name, () => {
364
377
await expect ( feeds ) . resolves . toStrictEqual ( [ ] ) ;
365
378
} ) ;
366
379
} ) ;
380
+
381
+ describe ( updateFeedsModule . calculateStaggerTime . name , ( ) => {
382
+ it ( 'calculates zero stagger time for specific edge cases' , ( ) => {
383
+ expect ( updateFeedsModule . calculateStaggerTime ( 1 , 10_000 , 60_000 ) ) . toBe ( 0 ) ; // When there is only a single batch.
384
+ expect ( updateFeedsModule . calculateStaggerTime ( 2 , 25_000 , 30_000 ) ) . toBe ( 0 ) ; // When there are just two batches and fetching the first batch takes too long.
385
+ } ) ;
386
+
387
+ it ( 'uses remaining time to calculate stagger time when fetching batch takes too long' , ( ) => {
388
+ expect ( updateFeedsModule . calculateStaggerTime ( 3 , 15_000 , 30_000 ) ) . toBe ( 7500 ) ;
389
+ expect ( updateFeedsModule . calculateStaggerTime ( 10 , 10_000 , 50_000 ) ) . toBe ( 4444 ) ;
390
+ expect ( updateFeedsModule . calculateStaggerTime ( 10 , 20_000 , 20_000 ) ) . toBe ( 0 ) ;
391
+ } ) ;
392
+
393
+ it ( 'staggers the batches evenly' , ( ) => {
394
+ const firstBatchDuration = 10_000 ;
395
+ const batchCount = 11 ;
396
+ const staggerTime = updateFeedsModule . calculateStaggerTime ( batchCount , firstBatchDuration , 50_000 ) ;
397
+
398
+ const fetchTimes = [ 0 , firstBatchDuration ] ;
399
+ for ( let i = 1 ; i < batchCount - 1 ; i ++ ) {
400
+ fetchTimes . push ( fetchTimes [ 1 ] ! + staggerTime * i ) ;
401
+ }
402
+
403
+ expect ( fetchTimes ) . toStrictEqual ( [
404
+ 0 , 10_000 , 14_000 , 18_000 , 22_000 , 26_000 , 30_000 , 34_000 , 38_000 , 42_000 , 46_000 ,
405
+ ] ) ;
406
+ } ) ;
407
+
408
+ it ( 'returns zero if first batch takes more than the full update interval' , ( ) => {
409
+ expect ( updateFeedsModule . calculateStaggerTime ( 3 , 60_000 , 30_000 ) ) . toBe ( 0 ) ;
410
+ } ) ;
411
+ } ) ;
0 commit comments