|
3 | 3 | import os
|
4 | 4 | import socket
|
5 | 5 | import sys
|
| 6 | +import asyncio |
| 7 | +import threading |
6 | 8 | from collections import defaultdict
|
7 | 9 | from datetime import datetime, timedelta, timezone
|
8 | 10 | from unittest import mock
|
@@ -153,6 +155,7 @@ def test_transport_works(
|
153 | 155 | @pytest.mark.parametrize("use_pickle", (True, False))
|
154 | 156 | @pytest.mark.parametrize("compression_level", (0, 9, None))
|
155 | 157 | @pytest.mark.parametrize("compression_algo", ("gzip", "br", "<invalid>", None))
|
| 158 | +@pytest.mark.skipif(not PY38, reason="Async transport only supported in Python 3.8+") |
156 | 159 | async def test_transport_works_async(
|
157 | 160 | capturing_server,
|
158 | 161 | request,
|
@@ -753,3 +756,118 @@ def close(self):
|
753 | 756 | client.flush()
|
754 | 757 |
|
755 | 758 | assert seen == ["status_500"]
|
| 759 | + |
| 760 | + |
| 761 | +@pytest.mark.asyncio |
| 762 | +@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+") |
| 763 | +async def test_async_transport_background_thread_capture( |
| 764 | + capturing_server, make_client, caplog |
| 765 | +): |
| 766 | + """Test capture_envelope from background threads uses run_coroutine_threadsafe""" |
| 767 | + caplog.set_level(logging.DEBUG) |
| 768 | + experiments = {"transport_async": True} |
| 769 | + client = make_client(_experiments=experiments) |
| 770 | + assert isinstance(client.transport, AsyncHttpTransport) |
| 771 | + sentry_sdk.get_global_scope().set_client(client) |
| 772 | + captured_from_thread = [] |
| 773 | + exception_from_thread = [] |
| 774 | + |
| 775 | + def background_thread_work(): |
| 776 | + try: |
| 777 | + # This should use run_coroutine_threadsafe path |
| 778 | + capture_message("from background thread") |
| 779 | + captured_from_thread.append(True) |
| 780 | + except Exception as e: |
| 781 | + exception_from_thread.append(e) |
| 782 | + |
| 783 | + thread = threading.Thread(target=background_thread_work) |
| 784 | + thread.start() |
| 785 | + thread.join() |
| 786 | + assert not exception_from_thread |
| 787 | + assert captured_from_thread |
| 788 | + await client.close(timeout=2.0) |
| 789 | + assert capturing_server.captured |
| 790 | + |
| 791 | + |
| 792 | +@pytest.mark.asyncio |
| 793 | +@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+") |
| 794 | +async def test_async_transport_event_loop_closed_scenario( |
| 795 | + capturing_server, make_client, caplog |
| 796 | +): |
| 797 | + """Test behavior when trying to capture after event loop context ends""" |
| 798 | + caplog.set_level(logging.DEBUG) |
| 799 | + experiments = {"transport_async": True} |
| 800 | + client = make_client(_experiments=experiments) |
| 801 | + sentry_sdk.get_global_scope().set_client(client) |
| 802 | + original_loop = client.transport.loop |
| 803 | + |
| 804 | + with mock.patch("asyncio.get_running_loop", side_effect=RuntimeError("no loop")): |
| 805 | + with mock.patch.object(client.transport.loop, "is_running", return_value=False): |
| 806 | + with mock.patch("sentry_sdk.transport.logger") as mock_logger: |
| 807 | + # This should trigger the "no_async_context" path |
| 808 | + capture_message("after loop closed") |
| 809 | + |
| 810 | + mock_logger.warning.assert_called_with( |
| 811 | + "Async Transport is not running in an event loop." |
| 812 | + ) |
| 813 | + |
| 814 | + client.transport.loop = original_loop |
| 815 | + await client.close(timeout=2.0) |
| 816 | + |
| 817 | + |
| 818 | +@pytest.mark.asyncio |
| 819 | +@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+") |
| 820 | +async def test_async_transport_concurrent_requests( |
| 821 | + capturing_server, make_client, caplog |
| 822 | +): |
| 823 | + """Test multiple simultaneous envelope submissions""" |
| 824 | + caplog.set_level(logging.DEBUG) |
| 825 | + experiments = {"transport_async": True} |
| 826 | + client = make_client(_experiments=experiments) |
| 827 | + assert isinstance(client.transport, AsyncHttpTransport) |
| 828 | + sentry_sdk.get_global_scope().set_client(client) |
| 829 | + |
| 830 | + num_messages = 15 |
| 831 | + |
| 832 | + async def send_message(i): |
| 833 | + capture_message(f"concurrent message {i}") |
| 834 | + |
| 835 | + tasks = [send_message(i) for i in range(num_messages)] |
| 836 | + await asyncio.gather(*tasks) |
| 837 | + transport = client.transport |
| 838 | + await client.close(timeout=2.0) |
| 839 | + assert len(transport.background_tasks) == 0 |
| 840 | + assert len(capturing_server.captured) == num_messages |
| 841 | + |
| 842 | + |
| 843 | +@pytest.mark.asyncio |
| 844 | +@pytest.mark.skipif(not PY38, reason="Async transport requires Python 3.8+") |
| 845 | +async def test_async_transport_rate_limiting_with_concurrency( |
| 846 | + capturing_server, make_client, request |
| 847 | +): |
| 848 | + """Test async transport rate limiting with concurrent requests""" |
| 849 | + experiments = {"transport_async": True} |
| 850 | + client = make_client(_experiments=experiments) |
| 851 | + |
| 852 | + assert isinstance(client.transport, AsyncHttpTransport) |
| 853 | + sentry_sdk.get_global_scope().set_client(client) |
| 854 | + request.addfinalizer(lambda: sentry_sdk.get_global_scope().set_client(None)) |
| 855 | + capturing_server.respond_with( |
| 856 | + code=429, headers={"X-Sentry-Rate-Limits": "60:error:organization"} |
| 857 | + ) |
| 858 | + |
| 859 | + # Send one request first to trigger rate limiting |
| 860 | + capture_message("initial message") |
| 861 | + await asyncio.sleep(0.1) # Wait for request to execute |
| 862 | + assert client.transport._check_disabled("error") is True |
| 863 | + capturing_server.clear_captured() |
| 864 | + |
| 865 | + async def send_message(i): |
| 866 | + capture_message(f"message {i}") |
| 867 | + await asyncio.sleep(0.01) |
| 868 | + |
| 869 | + await asyncio.gather(*[send_message(i) for i in range(5)]) |
| 870 | + await asyncio.sleep(0.1) |
| 871 | + # New request should be dropped due to rate limiting |
| 872 | + assert len(capturing_server.captured) == 0 |
| 873 | + await client.close(timeout=2.0) |
0 commit comments