@@ -31,6 +31,15 @@ const IUniswapV2Router02 = require('../lib/abis/IUniswapV2Router02.json');
3131const IUniswapV2Factory = require ( '../lib/abis/IUniswapV2Factory.json' ) ;
3232const MAX_RPC_ATTEMPTS = 3 ;
3333
34+ // Sync failure auto-disable configuration
35+ const SYNC_FAILURE_THRESHOLD = 3 ;
36+ const RECOVERY_BACKOFF_SCHEDULE = [
37+ 5 * 60 * 1000 , // 5 minutes
38+ 15 * 60 * 1000 , // 15 minutes
39+ 60 * 60 * 1000 , // 1 hour
40+ 6 * 60 * 60 * 1000 // 6 hours (max)
41+ ] ;
42+
3443module . exports = ( sequelize , DataTypes ) => {
3544 class Explorer extends Model {
3645 /**
@@ -299,10 +308,18 @@ module.exports = (sequelize, DataTypes) => {
299308
300309 /**
301310 * Starts block synchronization for the explorer.
311+ * Resets all failure tracking when manually enabling sync.
302312 * @returns {Promise<Explorer> } Updated explorer
303313 */
304- startSync ( ) {
305- return this . update ( { shouldSync : true } ) ;
314+ async startSync ( ) {
315+ await this . update ( {
316+ shouldSync : true ,
317+ syncFailedAttempts : 0 ,
318+ syncDisabledAt : null ,
319+ syncDisabledReason : null ,
320+ nextRecoveryCheckAt : null
321+ } ) ;
322+ return this ;
306323 }
307324
308325 /**
@@ -313,6 +330,101 @@ module.exports = (sequelize, DataTypes) => {
313330 return this . update ( { shouldSync : false } ) ;
314331 }
315332
333+ /**
334+ * Increments the sync failure counter and auto-disables if threshold reached.
335+ * @param {string } [reason='rpc_unreachable'] - Reason for the failure
336+ * @returns {Promise<{disabled: boolean, attempts: number}> } Result with disable status
337+ */
338+ async incrementSyncFailures ( reason = 'rpc_unreachable' ) {
339+ const newCount = ( this . syncFailedAttempts || 0 ) + 1 ;
340+ await this . update ( { syncFailedAttempts : newCount } ) ;
341+
342+ if ( newCount >= SYNC_FAILURE_THRESHOLD ) {
343+ await this . autoDisableSync ( reason ) ;
344+ return { disabled : true , attempts : newCount } ;
345+ }
346+ return { disabled : false , attempts : newCount } ;
347+ }
348+
349+ /**
350+ * Auto-disables sync and schedules first recovery check.
351+ * @param {string } reason - Reason for disabling (e.g., 'rpc_unreachable')
352+ * @returns {Promise<Explorer> } Updated explorer
353+ */
354+ async autoDisableSync ( reason ) {
355+ const nextCheck = new Date ( Date . now ( ) + RECOVERY_BACKOFF_SCHEDULE [ 0 ] ) ;
356+ await this . update ( {
357+ shouldSync : false ,
358+ syncDisabledAt : new Date ( ) ,
359+ syncDisabledReason : reason ,
360+ nextRecoveryCheckAt : nextCheck
361+ } ) ;
362+ return this ;
363+ }
364+
365+ /**
366+ * Resets all sync failure tracking state.
367+ * @returns {Promise<Explorer> } Updated explorer
368+ */
369+ async resetSyncState ( ) {
370+ await this . update ( {
371+ syncFailedAttempts : 0 ,
372+ syncDisabledAt : null ,
373+ syncDisabledReason : null ,
374+ nextRecoveryCheckAt : null
375+ } ) ;
376+ return this ;
377+ }
378+
379+ /**
380+ * Schedules the next recovery check using exponential backoff.
381+ * Backoff schedule: 5m -> 15m -> 1h -> 6h (max)
382+ * @returns {Promise<Explorer> } Updated explorer
383+ */
384+ async scheduleNextRecoveryCheck ( ) {
385+ if ( ! this . syncDisabledAt ) {
386+ return this ;
387+ }
388+
389+ const timeSinceDisabled = Date . now ( ) - new Date ( this . syncDisabledAt ) . getTime ( ) ;
390+ let cumulativeTime = 0 ;
391+ let backoffIndex = 0 ;
392+
393+ // Find which backoff interval we should use based on time since disabled
394+ for ( let i = 0 ; i < RECOVERY_BACKOFF_SCHEDULE . length ; i ++ ) {
395+ cumulativeTime += RECOVERY_BACKOFF_SCHEDULE [ i ] ;
396+ if ( timeSinceDisabled < cumulativeTime ) {
397+ backoffIndex = i ;
398+ break ;
399+ }
400+ backoffIndex = i ;
401+ }
402+
403+ // Cap at max backoff (last element)
404+ if ( backoffIndex >= RECOVERY_BACKOFF_SCHEDULE . length ) {
405+ backoffIndex = RECOVERY_BACKOFF_SCHEDULE . length - 1 ;
406+ }
407+
408+ const nextCheck = new Date ( Date . now ( ) + RECOVERY_BACKOFF_SCHEDULE [ backoffIndex ] ) ;
409+ await this . update ( { nextRecoveryCheckAt : nextCheck } ) ;
410+ return this ;
411+ }
412+
413+ /**
414+ * Re-enables sync after successful recovery check.
415+ * @returns {Promise<Explorer> } Updated explorer
416+ */
417+ async enableSyncAfterRecovery ( ) {
418+ await this . update ( {
419+ shouldSync : true ,
420+ syncFailedAttempts : 0 ,
421+ syncDisabledAt : null ,
422+ syncDisabledReason : null ,
423+ nextRecoveryCheckAt : null
424+ } ) ;
425+ return this ;
426+ }
427+
316428 /**
317429 * Creates a Uniswap V2 compatible DEX for the explorer.
318430 * @param {string } routerAddress - DEX router contract address
@@ -690,7 +802,11 @@ module.exports = (sequelize, DataTypes) => {
690802 shouldEnforceQuota : DataTypes . BOOLEAN ,
691803 isDemo : DataTypes . BOOLEAN ,
692804 gasAnalyticsEnabled : DataTypes . BOOLEAN ,
693- displayTopAccounts : DataTypes . BOOLEAN
805+ displayTopAccounts : DataTypes . BOOLEAN ,
806+ syncFailedAttempts : DataTypes . INTEGER ,
807+ syncDisabledAt : DataTypes . DATE ,
808+ syncDisabledReason : DataTypes . STRING ,
809+ nextRecoveryCheckAt : DataTypes . DATE
694810 } , {
695811 hooks : {
696812 afterCreate ( explorer , options ) {
0 commit comments