{"id":"4235c1c5ca0abc2a","repo":"redis/node-redis","slug":"scan-iteration-was-interrupted-by-a-sentinel-maste","errorCode":null,"errorMessage":"Scan iteration was interrupted by a Sentinel master change","messagePattern":"Scan iteration was interrupted by a Sentinel master change","errorType":"exception","errorClass":"ScanIteratorInterruptedError","httpStatus":null,"severity":"warning","filePath":"packages/client/lib/sentinel/index.ts","lineNumber":723,"sourceCode":"   * @throws {ScanIteratorInterruptedError} On observed `MASTER_CHANGE`.\n   */\n  async *scanIterator(\n    this: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>,\n    options?: ScanOptions & ScanIteratorOptions\n  ) {\n    let cursor: RedisArgument = options?.cursor ?? '0';\n    let masterChanged = false;\n\n    const handleTopologyChange = (event: RedisSentinelEvent) => {\n      if (event.type === 'MASTER_CHANGE') {\n        masterChanged = true;\n      }\n    };\n    this.on('topology-change', handleTopologyChange);\n\n    try {\n      do {\n        if (masterChanged) throw new ScanIteratorInterruptedError();\n\n        // Route through _execute so reserveClient:true reuses the reserved\n        // lease (instead of waiting forever on an empty master pool), and the\n        // lease is released before yielding — consumers can issue other\n        // commands inside the for-await loop without exhausting the pool.\n        let reply;\n        try {\n          reply = await this._execute(\n            false,\n            client => {\n              // Re-check after the lease resolves: a failover may have landed\n              // while waiting on an empty master pool, in which case the lease\n              // now points to a fresh client on the new master and SCAN would\n              // resume with a cursor from the old master.\n              if (masterChanged) throw new ScanIteratorInterruptedError();\n              return (client as RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>).scan(cursor, options);\n            }\n          );","sourceCodeStart":705,"sourceCodeEnd":741,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/sentinel/index.ts#L705-L741","documentation":"The first of three throw sites in RedisSentinel.scanIterator: at the top of each loop iteration it checks the masterChanged flag set by the 'topology-change' MASTER_CHANGE listener. If a failover was observed between yielding one page and starting the next SCAN, it throws ScanIteratorInterruptedError because the cursor from the old master no longer applies.","triggerScenarios":"Iterating for await (const keys of sentinel.scanIterator()) and a Sentinel master failover event arrives between pages; the next iteration's pre-check sees masterChanged=true and aborts before issuing SCAN.","commonSituations":"Long-running full-key scans over a Sentinel-managed topology that fails over (maintenance, crash, manual failover) mid-iteration; scans running during a scheduled failover window.","solutions":["Catch ScanIteratorInterruptedError and restart the scan from the beginning (fresh scanIterator call).","Run large scans outside failover windows or with idempotent dedup of keys.","Reduce per-page processing time to shrink the window in which a failover can interrupt.","Use the iterator's options.cursor only within a single uninterrupted run."],"exampleFix":"// before\nfor await (const keys of sentinel.scanIterator()) { process(keys); } // throws mid-scan\n\n// after\nasync function fullScan() {\n  for (;;) {\n    try { for await (const keys of sentinel.scanIterator()) { process(keys); } return; }\n    catch (e) { if (e instanceof ScanIteratorInterruptedError) continue; throw e; }\n  }\n}","handlingStrategy":"retry","validationCode":"// Cannot pre-validate an asynchronous failover. Restart-on-interrupt:\nasync function robustScan(sentinel, process) { for (;;) { try { for await (const k of sentinel.scanIterator()) process(k); return; } catch (e) { if (!(e instanceof ScanIteratorInterruptedError)) throw e; } } }","typeGuard":"function isScanInterrupted(e) { return e instanceof Error && /interrupted by a Sentinel master change/.test(e.message); }","tryCatchPattern":"try { for await (const keys of sentinel.scanIterator()) process(keys); } catch (e) { if (isScanInterrupted(e)) { /* restart from new scanIterator */ } else throw e; }","preventionTips":["Catch ScanIteratorInterruptedError and restart the scan.","Make per-key work idempotent and dedupe across restarts.","Run big scans outside failover windows."],"tags":["sentinel","scan","failover","interrupted","topology"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}