@@ -55,13 +55,13 @@ def test_sync_replication_slots(self):
5555 self .p .set_role (PostgresqlRole .STANDBY_LEADER )
5656 with patch .object (SlotsHandler , 'drop_replication_slot' , Mock (return_value = (True , False ))), \
5757 patch .object (global_config .__class__ , 'is_standby_cluster' , PropertyMock (return_value = True )), \
58- patch ('patroni.postgresql.slots.logger.debug ' ) as mock_debug :
58+ patch ('patroni.postgresql.slots.logger.warning ' ) as mock_warning :
5959 self .s .sync_replication_slots (cluster , self .tags )
60- mock_debug . assert_called_once ( )
60+ mock_warning . assert_called_once_with ( "Unable to drop replication slot '%s', slot is active" , 'foobar' )
6161 self .p .set_role (PostgresqlRole .REPLICA )
6262 with patch .object (Postgresql , 'is_primary' , Mock (return_value = False )), \
6363 patch .object (global_config .__class__ , 'is_paused' , PropertyMock (return_value = True )), \
64- patch .object (SlotsHandler , 'drop_replication_slot ' ) as mock_drop :
64+ patch .object (SlotsHandler , '_drop_physical_slot ' ) as mock_drop :
6565 config .data ['slots' ].pop ('ls' )
6666 self .s .sync_replication_slots (cluster , self .tags )
6767 mock_drop .assert_not_called ()
@@ -343,6 +343,7 @@ def test_advance_physical_slots(self):
343343 [self .me , self .other , self .leadermem ], None , SyncState .empty (), None , None )
344344 global_config .update (cluster )
345345 self .s .sync_replication_slots (cluster , self .tags )
346+
346347 with patch .object (SlotsHandler , '_query' , Mock (side_effect = [[('blabla' , 'physical' , None , 12345 , None , None ,
347348 None , None , None )], Exception ])) as mock_query , \
348349 patch ('patroni.postgresql.slots.logger.error' ) as mock_error :
@@ -354,7 +355,7 @@ def test_advance_physical_slots(self):
354355
355356 with patch .object (SlotsHandler , '_query' , Mock (side_effect = [[('test_1' , 'physical' , 1 , 12345 , None , None ,
356357 None , None , None )], Exception ])), \
357- patch .object (SlotsHandler , 'drop_replication_slot ' , Mock (return_value = (False , True ))):
358+ patch .object (SlotsHandler , '_drop_physical_slot ' , Mock (return_value = (True ))):
358359 self .s .sync_replication_slots (cluster , self .tags )
359360
360361 with patch .object (SlotsHandler , '_query' , Mock (side_effect = [[('test_1' , 'physical' , 1 , 12345 , None , None ,
@@ -367,16 +368,31 @@ def test_advance_physical_slots(self):
367368
368369 with patch .object (SlotsHandler , '_query' , Mock (side_effect = [[('test_1' , 'physical' , 1 , 12345 , None , None ,
369370 None , None , None )], Exception ])), \
370- patch .object (SlotsHandler , 'drop_replication_slot ' , Mock (return_value = (False , False ))):
371+ patch .object (SlotsHandler , '_drop_physical_slot ' , Mock (return_value = (False ))):
371372 self .s .sync_replication_slots (cluster , self .tags )
372373
373374 with patch .object (SlotsHandler , '_query' , Mock (side_effect = [[('test_1' , 'physical' , 1 , 12345 , None , None ,
374375 None , None , None )], Exception ])), \
375376 patch .object (Cluster , 'is_unlocked' , Mock (return_value = True )), \
376- patch .object (SlotsHandler , 'drop_replication_slot ' ) as mock_drop :
377+ patch .object (SlotsHandler , '_drop_physical_slot ' ) as mock_drop :
377378 self .s .sync_replication_slots (cluster , self .tags )
378379 mock_drop .assert_not_called ()
379380
381+ # If the slot has no restart_lsn, we should not try to advance it, and only warn the user that this is not an
382+ # expected situation.
383+ with patch .object (SlotsHandler , '_query' , Mock (side_effect = [[('blabla' , 'physical' , None , None , None , None ,
384+ None , None , None )], Exception ])) as mock_query , \
385+ patch ('patroni.postgresql.slots.logger.warning' ) as mock_warning , \
386+ patch .object (SlotsHandler , '_drop_physical_slot' ) as mock_drop :
387+ self .s .sync_replication_slots (cluster , self .tags )
388+ for mock_call in mock_query .call_args_list :
389+ self .assertNotIn ("pg_catalog.pg_replication_slot_advance" , mock_call [0 ][0 ])
390+ self .assertEqual (mock_warning .call_args [0 ][0 ],
391+ 'Dropping physical replication slot %s because it has no restart_lsn. '
392+ 'This slot was probably not created by Patroni, but by an external agent.' )
393+ self .assertEqual (mock_warning .call_args [0 ][1 ], 'blabla' )
394+ mock_drop .assert_called_once_with ('blabla' )
395+
380396 @patch .object (Postgresql , 'is_primary' , Mock (return_value = False ))
381397 @patch .object (Postgresql , 'role' , PropertyMock (return_value = PostgresqlRole .REPLICA ))
382398 @patch .object (TestTags , 'tags' , PropertyMock (return_value = {'nofailover' : True }))
@@ -390,3 +406,50 @@ def test_slots_nofailover_tag(self):
390406 None , None , None )], Exception ])) as mock_query :
391407 self .s .sync_replication_slots (cluster , self .tags )
392408 self .assertTrue (mock_query .call_args [0 ][0 ].startswith ('SELECT slot_name, slot_type, xmin, ' ))
409+
410+ def test__drop_physical_slot (self ):
411+ """Test the :meth:~SlotsHandler._drop_physical_slot` method."""
412+ # Should log info and remove the slot from the list when the slot is dropped
413+ self .s ._replication_slots ['testslot' ] = {'type' : 'physical' }
414+ self .s ._schedule_load_slots = False
415+ with patch .object (self .s , 'drop_replication_slot' , return_value = (False , True )) as mock_drop , \
416+ patch ('patroni.postgresql.slots.logger.info' ) as mock_info , \
417+ patch ('patroni.postgresql.slots.logger.warning' ) as mock_warning , \
418+ patch ('patroni.postgresql.slots.logger.error' ) as mock_error :
419+ self .s ._drop_physical_slot ('testslot' )
420+ mock_drop .assert_called_once_with ('testslot' )
421+ mock_info .assert_called_once_with ("Dropped physical replication slot '%s'" , 'testslot' )
422+ mock_warning .assert_not_called ()
423+ mock_error .assert_not_called ()
424+ self .assertFalse (self .s ._schedule_load_slots )
425+ self .assertNotIn ('testslot' , self .s ._replication_slots )
426+
427+ # Should log warning and keep slot in the list when the slot is active and not dropped
428+ self .s ._replication_slots ['testslot' ] = {'type' : 'physical' }
429+ self .s ._schedule_load_slots = False
430+ with patch .object (self .s , 'drop_replication_slot' , return_value = (True , False )) as mock_drop , \
431+ patch ('patroni.postgresql.slots.logger.info' ) as mock_info , \
432+ patch ('patroni.postgresql.slots.logger.warning' ) as mock_warning , \
433+ patch ('patroni.postgresql.slots.logger.error' ) as mock_error :
434+ self .s ._drop_physical_slot ('testslot' )
435+ mock_drop .assert_called_once_with ('testslot' )
436+ mock_info .assert_not_called ()
437+ mock_warning .assert_called_once_with ("Unable to drop replication slot '%s', slot is active" , 'testslot' )
438+ mock_error .assert_not_called ()
439+ self .assertTrue (self .s ._schedule_load_slots )
440+ self .assertIn ('testslot' , self .s ._replication_slots )
441+
442+ # Should log error and keep the slot in the list when the slot is not active and not dropped
443+ self .s ._replication_slots ['testslot' ] = {'type' : 'physical' }
444+ self .s ._schedule_load_slots = False
445+ with patch .object (self .s , 'drop_replication_slot' , return_value = (False , False )) as mock_drop , \
446+ patch ('patroni.postgresql.slots.logger.info' ) as mock_info , \
447+ patch ('patroni.postgresql.slots.logger.warning' ) as mock_warning , \
448+ patch ('patroni.postgresql.slots.logger.error' ) as mock_error :
449+ self .s ._drop_physical_slot ('testslot' )
450+ mock_drop .assert_called_once_with ('testslot' )
451+ mock_info .assert_not_called ()
452+ mock_warning .assert_not_called ()
453+ mock_error .assert_called_once_with ("Failed to drop replication slot '%s'" , 'testslot' )
454+ self .assertTrue (self .s ._schedule_load_slots )
455+ self .assertIn ('testslot' , self .s ._replication_slots )
0 commit comments