@@ -413,7 +413,7 @@ def test_sync_client_does_not_recreate_caller_owned_channel():
413413
414414 with patch ("durabletask.client.shared.get_grpc_channel" ) as mock_get_channel , patch (
415415 "durabletask.client.stubs.TaskHubSidecarServiceStub" , return_value = stub
416- ) as mock_stub :
416+ ) as mock_stub , patch ( "threading.Timer" ) as mock_timer :
417417 client = TaskHubGrpcClient (
418418 channel = provided_channel ,
419419 resiliency_options = GrpcClientResiliencyOptions (channel_recreate_failure_threshold = 1 ),
@@ -426,6 +426,74 @@ def test_sync_client_does_not_recreate_caller_owned_channel():
426426 assert client ._channel is provided_channel
427427 mock_get_channel .assert_not_called ()
428428 mock_stub .assert_called_once_with (provided_channel )
429+ mock_timer .assert_not_called ()
430+
431+
432+ def test_sync_client_recreate_cooldown_prevents_immediate_repeated_recreation ():
433+ first_channel = MagicMock (name = "first-channel" )
434+ second_channel = MagicMock (name = "second-channel" )
435+ third_channel = MagicMock (name = "third-channel" )
436+ first_stub = MagicMock ()
437+ second_stub = MagicMock ()
438+ third_stub = MagicMock ()
439+ first_stub .GetInstance .side_effect = FakeRpcError (grpc .StatusCode .UNAVAILABLE )
440+ second_stub .GetInstance .side_effect = [
441+ FakeRpcError (grpc .StatusCode .UNAVAILABLE ),
442+ FakeRpcError (grpc .StatusCode .UNAVAILABLE ),
443+ ]
444+ timer1 = MagicMock (name = "close-timer-1" )
445+ timer2 = MagicMock (name = "close-timer-2" )
446+
447+ with patch (
448+ "durabletask.client.shared.get_grpc_channel" ,
449+ side_effect = [first_channel , second_channel , third_channel ],
450+ ) as mock_get_channel , patch (
451+ "durabletask.client.stubs.TaskHubSidecarServiceStub" ,
452+ side_effect = [first_stub , second_stub , third_stub ],
453+ ), patch (
454+ "durabletask.client.time.monotonic" , side_effect = [100.0 , 101.0 , 131.0 ]
455+ ), patch ("threading.Timer" , side_effect = [timer1 , timer2 ]) as mock_timer :
456+ client = TaskHubGrpcClient (
457+ host_address = HOST_ADDRESS ,
458+ resiliency_options = GrpcClientResiliencyOptions (
459+ channel_recreate_failure_threshold = 1 ,
460+ min_recreate_interval_seconds = 30.0 ,
461+ ),
462+ )
463+ with pytest .raises (FakeRpcError ):
464+ client .get_orchestration_state ("abc" )
465+ assert client ._channel is second_channel
466+ assert mock_get_channel .call_count == 2
467+
468+ with pytest .raises (FakeRpcError ):
469+ client .get_orchestration_state ("abc" )
470+ assert client ._channel is second_channel
471+ assert mock_get_channel .call_count == 2
472+ mock_timer .assert_called_once_with (30.0 , first_channel .close )
473+
474+ with pytest .raises (FakeRpcError ):
475+ client .get_orchestration_state ("abc" )
476+ assert client ._channel is third_channel
477+
478+ expected_channel_call = call (
479+ host_address = HOST_ADDRESS ,
480+ secure_channel = False ,
481+ interceptors = None ,
482+ channel_options = None ,
483+ )
484+ assert mock_get_channel .call_args_list == [
485+ expected_channel_call ,
486+ expected_channel_call ,
487+ expected_channel_call ,
488+ ]
489+ assert mock_timer .call_args_list == [
490+ call (30.0 , first_channel .close ),
491+ call (30.0 , second_channel .close ),
492+ ]
493+ assert timer1 .daemon is True
494+ assert timer2 .daemon is True
495+ timer1 .start .assert_called_once_with ()
496+ timer2 .start .assert_called_once_with ()
429497
430498
431499def test_sync_client_resets_failure_tracking_after_success ():
0 commit comments