3030from enum import Enum
3131
3232import sib_api_v3_sdk
33+ import urllib3
3334
3435logger = logging .getLogger (__name__ )
3536
37+ # (connect, read) timeout in seconds for every Brevo API call. Without this the SDK uses no
38+ # timeout, so when Brevo is unreachable the underlying urllib3 retries hang for a long time while
39+ # holding the caller's DB connection from the pool, which can exhaust the pool and make requests
40+ # appear "stuck". A short connect timeout makes the failure fast and bounded.
41+ BREVO_REQUEST_TIMEOUT = (3.05 , 10 )
42+
43+ # Do not retry on connection failures. The Brevo call runs inside an async FastAPI route (which
44+ # executes on the event loop), so a long retry loop would block the whole API, not just this
45+ # request. With no retries, an unreachable Brevo fails within ~the connect timeout above.
46+ _BREVO_RETRIES = urllib3 .Retry (total = 0 , connect = 0 , read = 0 , redirect = 0 , status = 0 )
47+
3648
3749class BrevoSubscriptionStatus (Enum ):
3850 SUBSCRIBED = "subscribed"
@@ -47,7 +59,10 @@ def _get_contacts_api() -> "sib_api_v3_sdk.ContactsApi":
4759 raise RuntimeError ("BREVO_API_KEY environment variable is not set" )
4860 configuration = sib_api_v3_sdk .Configuration ()
4961 configuration .api_key ["api-key" ] = api_key
50- return sib_api_v3_sdk .ContactsApi (sib_api_v3_sdk .ApiClient (configuration ))
62+ api = sib_api_v3_sdk .ContactsApi (sib_api_v3_sdk .ApiClient (configuration ))
63+ # Disable urllib3 retries so a connection failure fails fast instead of looping.
64+ api .api_client .rest_client .pool_manager .connection_pool_kw ["retries" ] = _BREVO_RETRIES
65+ return api
5166
5267
5368def get_announcements_list_id () -> int :
@@ -71,15 +86,20 @@ def add_contact_to_list(email: str, list_id: int, subscription_id: str) -> None:
7186 attributes = {"MDB_SUBSCRIPTION_ID" : subscription_id },
7287 list_ids = [list_id ],
7388 update_enabled = True ,
74- )
89+ ),
90+ _request_timeout = BREVO_REQUEST_TIMEOUT ,
7591 )
7692
7793
7894def remove_contact_from_list (email : str , list_id : int ) -> None :
7995 """Remove a Brevo contact from the list. No-op if the contact is not on the list."""
8096 api = _get_contacts_api ()
8197 try :
82- api .remove_contact_from_list (list_id , sib_api_v3_sdk .RemoveContactFromList (emails = [email ]))
98+ api .remove_contact_from_list (
99+ list_id ,
100+ sib_api_v3_sdk .RemoveContactFromList (emails = [email ]),
101+ _request_timeout = BREVO_REQUEST_TIMEOUT ,
102+ )
83103 except sib_api_v3_sdk .rest .ApiException as exc :
84104 # 400 "Contact already removed from list" / 404 contact-not-found are idempotent no-ops.
85105 if exc .status in (400 , 404 ):
@@ -107,7 +127,7 @@ def get_contact_subscription_status(
107127 api = _get_contacts_api ()
108128
109129 try :
110- contact = api .get_contact_info (email )
130+ contact = api .get_contact_info (email , _request_timeout = BREVO_REQUEST_TIMEOUT )
111131 except sib_api_v3_sdk .rest .ApiException as exc :
112132 if exc .status == 404 :
113133 return BrevoSubscriptionStatus .NOT_FOUND
0 commit comments