@@ -979,6 +979,84 @@ describe('legacy tests', () => {
979979 assert . notEqual ( sentinelNode ! . port , newSentinel . port ) ;
980980 } ) ;
981981
982+ it ( 'Should recover after full outage' , async function ( ) {
983+ this . timeout ( 120000 ) ;
984+
985+ const allSentinelPorts = frame . getAllSentinelsPort ( ) ;
986+ const primarySentinelPort = allSentinelPorts [ 0 ] ;
987+ const extraSentinelPorts = allSentinelPorts . slice ( 1 ) ;
988+
989+ // Keep only one sentinel reachable for the test.
990+ await Promise . all ( extraSentinelPorts . map ( port => frame . stopSentinel ( port . toString ( ) ) ) ) ;
991+ await setTimeout ( 1500 ) ;
992+
993+ sentinel = RedisSentinel . create ( {
994+ name : config . sentinelName ,
995+ sentinelRootNodes : [ { host : '127.0.0.1' , port : primarySentinelPort } ] ,
996+ RESP : 3 ,
997+ scanInterval : 250
998+ } ) ;
999+ sentinel . setTracer ( tracer ) ;
1000+ sentinel . on ( "error" , ( ) => { } ) ;
1001+ await sentinel . connect ( ) ;
1002+
1003+ await sentinel . set ( 'some-key' , 'value' ) ;
1004+ assert . equal ( await sentinel . get ( 'some-key' ) , 'value' ) ;
1005+
1006+ const allNodePorts = frame . getAllNodesPort ( ) ;
1007+ // Simulate full outage (all Redis nodes + the single configured sentinel).
1008+ await Promise . all ( allNodePorts . map ( port => frame . stopNode ( port . toString ( ) ) ) ) ;
1009+ await frame . stopSentinel ( primarySentinelPort . toString ( ) ) ;
1010+
1011+ const timedGet = async ( ) => {
1012+ const getPromise = sentinel ! . get ( 'some-key' ) ;
1013+ void getPromise . catch ( ( ) => undefined ) ; // Promise.race may timeout first.
1014+
1015+ return Promise . race ( [
1016+ getPromise ,
1017+ setTimeout ( 1000 ) . then ( ( ) => {
1018+ throw new Error ( '1s Timeout' ) ;
1019+ } )
1020+ ] ) ;
1021+ } ;
1022+
1023+ const pollResults : Array < { phase : 'outage' | 'recovery' ; status : 'success' | 'timeout' | 'error' } > = [ ] ;
1024+ const pollLoop = async ( phase : 'outage' | 'recovery' , rounds : number ) => {
1025+ for ( let i = 0 ; i < rounds ; i ++ ) {
1026+ try {
1027+ await timedGet ( ) ;
1028+ pollResults . push ( { phase, status : 'success' } ) ;
1029+ } catch ( err : any ) {
1030+ pollResults . push ( {
1031+ phase,
1032+ status : err ?. message === '1s Timeout' ? 'timeout' : 'error'
1033+ } ) ;
1034+ }
1035+ await setTimeout ( 3000 ) ;
1036+ }
1037+ } ;
1038+
1039+ // Match the issue's periodic GET calls while outage is active.
1040+ await pollLoop ( 'outage' , 3 ) ;
1041+
1042+ // Bring only the single configured sentinel back; keep extra sentinels down.
1043+ await Promise . all ( allNodePorts . map ( port => frame . restartNode ( port . toString ( ) ) ) ) ;
1044+ await frame . restartSentinel ( primarySentinelPort . toString ( ) ) ;
1045+
1046+ // Continue periodic GET loop and assert recovery.
1047+ await pollLoop ( 'recovery' , 5 ) ;
1048+
1049+ const sawOutageFailure = pollResults . some ( result =>
1050+ result . phase === 'outage' && result . status !== 'success'
1051+ ) ;
1052+ assert . equal ( sawOutageFailure , true , 'expected GET failures during outage' ) ;
1053+
1054+ const sawRecoverySuccess = pollResults . some ( result =>
1055+ result . phase === 'recovery' && result . status === 'success'
1056+ ) ;
1057+ assert . equal ( sawRecoverySuccess , true , 'expected periodic GET to recover after restart' ) ;
1058+ } ) ;
1059+
9821060 it ( 'timer works, and updates sentinel list' , async function ( ) {
9831061 this . timeout ( 60000 ) ;
9841062
0 commit comments