2828from kinesis .utils import Throttler
2929
3030
31+ @pytest .fixture
32+ def fast_asyncio_sleep (monkeypatch ):
33+ """Replace asyncio.sleep with a zero-delay awaitable for retry-path tests."""
34+ orig_sleep = asyncio .sleep
35+
36+ async def fast_sleep (_ ):
37+ await orig_sleep (0 )
38+
39+ monkeypatch .setattr (asyncio , "sleep" , fast_sleep )
40+ return fast_sleep
41+
42+
3143class TestNoOpMetricsCollector :
3244 def test_all_operations_are_noop (self ):
3345 """Test that NoOpMetricsCollector does nothing."""
@@ -501,7 +513,7 @@ def _shard(shard_id="shard-0"):
501513 }
502514
503515 @pytest .mark .asyncio
504- async def test_connection_error_emits_connection_label (self , mock_consumer ):
516+ async def test_connection_error_emits_connection_label (self , mock_consumer , fast_asyncio_sleep ):
505517 collector = InMemoryMetricsCollector ()
506518 consumer = mock_consumer (metrics_collector = collector )
507519 consumer .client = MagicMock ()
@@ -512,9 +524,10 @@ async def test_connection_error_emits_connection_label(self, mock_consumer):
512524
513525 key = f"consumer_errors_total{{error_type=connection,{ _shard_labels ('shard-0' )} }}"
514526 assert collector .counters [key ] == 1
527+ consumer .get_conn .assert_not_awaited ()
515528
516529 @pytest .mark .asyncio
517- async def test_botocore_connection_closed_emits_connection_label (self , mock_consumer ):
530+ async def test_botocore_connection_closed_emits_connection_label (self , mock_consumer , fast_asyncio_sleep ):
518531 """Real AWS commonly raises botocore ConnectionClosedError when an idle
519532 keep-alive connection is dropped mid-response. Prior to #73 fix this fell
520533 through to the catch-all and got mislabeled as error_type=unknown."""
@@ -530,12 +543,12 @@ async def test_botocore_connection_closed_emits_connection_label(self, mock_cons
530543
531544 key = f"consumer_errors_total{{error_type=connection,{ _shard_labels ('shard-0' )} }}"
532545 assert collector .counters [key ] == 1
533- # Must rebuild client, not just sleep — same remediation as ClientConnectionError
534- consumer .get_conn .assert_awaited_once ()
546+ consumer .get_conn .assert_not_awaited ()
535547
536548 @pytest .mark .asyncio
537- async def test_botocore_endpoint_connection_error_emits_connection_label (self , mock_consumer ):
538- """botocore.EndpointConnectionError (DNS/TCP failure before request is sent)."""
549+ async def test_botocore_endpoint_connection_error_emits_connection_label (self , mock_consumer , fast_asyncio_sleep ):
550+ """botocore.EndpointConnectionError (DNS/TCP failure before request is sent)
551+ should be counted as a connection error and return without rebuilding."""
539552 collector = InMemoryMetricsCollector ()
540553 consumer = mock_consumer (metrics_collector = collector )
541554 consumer .client = MagicMock ()
@@ -548,10 +561,10 @@ async def test_botocore_endpoint_connection_error_emits_connection_label(self, m
548561
549562 key = f"consumer_errors_total{{error_type=connection,{ _shard_labels ('shard-0' )} }}"
550563 assert collector .counters [key ] == 1
551- consumer .get_conn .assert_awaited_once ()
564+ consumer .get_conn .assert_not_awaited ()
552565
553566 @pytest .mark .asyncio
554- async def test_botocore_read_timeout_emits_timeout_label (self , mock_consumer ):
567+ async def test_botocore_read_timeout_emits_timeout_label (self , mock_consumer , fast_asyncio_sleep ):
555568 """botocore.ReadTimeoutError lands in the timeout bucket, not unknown."""
556569 collector = InMemoryMetricsCollector ()
557570 consumer = mock_consumer (metrics_collector = collector )
@@ -560,40 +573,25 @@ async def test_botocore_read_timeout_emits_timeout_label(self, mock_consumer):
560573 side_effect = ReadTimeoutError (endpoint_url = "https://kinesis.us-east-1.amazonaws.com/" )
561574 )
562575
563- orig_sleep = asyncio .sleep
564-
565- async def fast_sleep (_ ):
566- await orig_sleep (0 )
567-
568- with pytest .MonkeyPatch ().context () as mp :
569- mp .setattr (asyncio , "sleep" , fast_sleep )
570- await consumer .get_records (self ._shard ())
576+ await consumer .get_records (self ._shard ())
571577
572578 key = f"consumer_errors_total{{error_type=timeout,{ _shard_labels ('shard-0' )} }}"
573579 assert collector .counters [key ] == 1
574580
575581 @pytest .mark .asyncio
576- async def test_timeout_emits_timeout_label (self , mock_consumer ):
582+ async def test_timeout_emits_timeout_label (self , mock_consumer , fast_asyncio_sleep ):
577583 collector = InMemoryMetricsCollector ()
578584 consumer = mock_consumer (metrics_collector = collector )
579585 consumer .client = MagicMock ()
580586 consumer .client .get_records = AsyncMock (side_effect = asyncio .TimeoutError ())
581587
582- # Monkeypatch asyncio.sleep inside the except block to avoid 3s wait.
583- orig_sleep = asyncio .sleep
584-
585- async def fast_sleep (_ ):
586- await orig_sleep (0 )
587-
588- with pytest .MonkeyPatch ().context () as mp :
589- mp .setattr (asyncio , "sleep" , fast_sleep )
590- await consumer .get_records (self ._shard ())
588+ await consumer .get_records (self ._shard ())
591589
592590 key = f"consumer_errors_total{{error_type=timeout,{ _shard_labels ('shard-0' )} }}"
593591 assert collector .counters [key ] == 1
594592
595593 @pytest .mark .asyncio
596- async def test_client_error_emits_raw_code_label (self , mock_consumer ):
594+ async def test_client_error_emits_raw_code_label (self , mock_consumer , fast_asyncio_sleep ):
597595 collector = InMemoryMetricsCollector ()
598596 consumer = mock_consumer (metrics_collector = collector )
599597 err = ClientError (
@@ -603,35 +601,39 @@ async def test_client_error_emits_raw_code_label(self, mock_consumer):
603601 consumer .client = MagicMock ()
604602 consumer .client .get_records = AsyncMock (side_effect = err )
605603
606- orig_sleep = asyncio .sleep
607-
608- async def fast_sleep (_ ):
609- await orig_sleep (0 )
610-
611- with pytest .MonkeyPatch ().context () as mp :
612- mp .setattr (asyncio , "sleep" , fast_sleep )
613- await consumer .get_records (self ._shard ())
604+ await consumer .get_records (self ._shard ())
614605
615606 key = (
616607 "consumer_errors_total{" f"error_type=ProvisionedThroughputExceededException,{ _shard_labels ('shard-0' )} " "}"
617608 )
618609 assert collector .counters [key ] == 1
619610
620611 @pytest .mark .asyncio
621- async def test_unknown_exception_emits_unknown_label (self , mock_consumer ):
612+ async def test_internal_failure_no_longer_rebuilds (self , mock_consumer , fast_asyncio_sleep ):
622613 collector = InMemoryMetricsCollector ()
623614 consumer = mock_consumer (metrics_collector = collector )
615+ err = ClientError (
616+ {"Error" : {"Code" : "InternalFailure" , "Message" : "internal failure" }},
617+ "GetRecords" ,
618+ )
624619 consumer .client = MagicMock ()
625- consumer .client .get_records = AsyncMock (side_effect = RuntimeError ("surprise" ))
620+ consumer .client .get_records = AsyncMock (side_effect = err )
621+ consumer .get_conn = AsyncMock ()
622+
623+ await consumer .get_records (self ._shard ())
626624
627- orig_sleep = asyncio .sleep
625+ key = f"consumer_errors_total{{error_type=InternalFailure,{ _shard_labels ('shard-0' )} }}"
626+ assert collector .counters [key ] == 1
627+ consumer .get_conn .assert_not_awaited ()
628628
629- async def fast_sleep (_ ):
630- await orig_sleep (0 )
629+ @pytest .mark .asyncio
630+ async def test_unknown_exception_emits_unknown_label (self , mock_consumer , fast_asyncio_sleep ):
631+ collector = InMemoryMetricsCollector ()
632+ consumer = mock_consumer (metrics_collector = collector )
633+ consumer .client = MagicMock ()
634+ consumer .client .get_records = AsyncMock (side_effect = RuntimeError ("surprise" ))
631635
632- with pytest .MonkeyPatch ().context () as mp :
633- mp .setattr (asyncio , "sleep" , fast_sleep )
634- await consumer .get_records (self ._shard ())
636+ await consumer .get_records (self ._shard ())
635637
636638 key = f"consumer_errors_total{{error_type=unknown,{ _shard_labels ('shard-0' )} }}"
637639 assert collector .counters [key ] == 1
0 commit comments