diff --git a/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/taboola.py b/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/taboola.py index 1f5937784c95..29aa9219d414 100644 --- a/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/taboola.py +++ b/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/taboola.py @@ -101,7 +101,22 @@ def get_rows( ) -> Iterator[list[dict[str, Any]]]: config = TABOOLA_ENDPOINTS[endpoint] session = _get_session(client_secret) - token = _mint_token(session, client_id, client_secret) + + # Unlike the mid-sync re-mint below, nothing else retries this initial mint, so a + # transient token-endpoint failure here needs its own backoff instead of failing the + # sync outright. Don't reuse `fetch`'s retry for this: it also retries on + # TaboolaRetryableError, and re-mint failures already propagate into that retry via the + # 401 branch below, so decorating the mint itself too would let the two retries compound. + @retry( + retry=retry_if_exception_type(TaboolaRetryableError), + stop=stop_after_attempt(MAX_RETRY_ATTEMPTS), + wait=wait_exponential_jitter(initial=2, max=90), + reraise=True, + ) + def mint_initial_token() -> str: + return _mint_token(session, client_id, client_secret) + + token = mint_initial_token() account_base = f"{TABOOLA_API_BASE_URL}/{_encode_path_segment(account_id)}" @retry( @@ -114,7 +129,8 @@ def fetch(url: str) -> dict[str, Any]: nonlocal token response = session.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=REQUEST_TIMEOUT_SECONDS) - # Access tokens are short-lived; re-mint once if one expires mid-sync. + # Access tokens are short-lived; re-mint once if one expires mid-sync. A retryable + # failure here isn't retried separately — it propagates to this function's own retry. if response.status_code == 401: token = _mint_token(session, client_id, client_secret) response = session.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=REQUEST_TIMEOUT_SECONDS) diff --git a/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/tests/test_taboola.py b/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/tests/test_taboola.py index 6c2f6cc8aa32..623375bf4846 100644 --- a/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/tests/test_taboola.py +++ b/products/warehouse_sources/backend/temporal/data_imports/sources/taboola/tests/test_taboola.py @@ -100,6 +100,20 @@ def test_mid_sync_401_re_mints_token(self, mock_session): assert batches == [[{"id": "1"}]] assert mock_session.return_value.post.call_count == 2 + @mock.patch("tenacity.nap.time.sleep", return_value=None) + @mock.patch(f"{_MODULE}.make_tracked_session") + def test_initial_token_mint_retries_on_transient_error(self, mock_session, _sleep): + mock_session.return_value.post.side_effect = [ + _response({}, status_code=503), + _token_response(), + ] + mock_session.return_value.get.return_value = _response({"results": [{"id": "1"}]}) + + batches = list(get_rows("cid", "sec", "acct", "campaigns", mock.MagicMock(), _make_manager())) + + assert batches == [[{"id": "1"}]] + assert mock_session.return_value.post.call_count == 2 + class TestCampaignItemsFanOut: @mock.patch(f"{_MODULE}.make_tracked_session")