From 478729cab88bee72fba332ae46ead66ed1d1119c Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Sun, 2 Aug 2026 14:52:31 +0100 Subject: [PATCH 1/2] fix(taboola): retry the initial token mint on transient errors The initial access-token mint in `get_rows` ran before the `@retry`-decorated `fetch` closure existed, so a transient 429/5xx from the token endpoint raised `TaboolaRetryableError` straight out of the sync activity instead of backing off, even though the mid-sync re-mint on a 401 already benefited from that retry. Wrap both call sites in the same retry policy via a shared `mint_token` closure. Generated-By: PostHog Code Task-Id: 34f1efbd-102b-40cc-a9a6-655f9adb71b3 --- .../data_imports/sources/taboola/taboola.py | 16 ++++++++++++++-- .../sources/taboola/tests/test_taboola.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) 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..e0a51d2dd2c1 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,19 @@ 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) + + # The initial mint below and the mid-sync re-mint on 401 (inside `fetch`) share this + # retry so a transient token-endpoint failure backs off instead of failing the sync. + @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_token() -> str: + return _mint_token(session, client_id, client_secret) + + token = mint_token() account_base = f"{TABOOLA_API_BASE_URL}/{_encode_path_segment(account_id)}" @retry( @@ -116,7 +128,7 @@ def fetch(url: str) -> dict[str, Any]: # Access tokens are short-lived; re-mint once if one expires mid-sync. if response.status_code == 401: - token = _mint_token(session, client_id, client_secret) + token = mint_token() response = session.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=REQUEST_TIMEOUT_SECONDS) if response.status_code == 429 or response.status_code >= 500: 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") From 45268ed9c411cd6c192e893a7b9554a77b802265 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Sun, 2 Aug 2026 14:59:05 +0100 Subject: [PATCH 2/2] fix(taboola): avoid compounding retries between the initial mint and fetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only the initial token mint needs its own retry decorator — it's the one call site nothing else covers. The mid-sync re-mint on a 401 already propagates into fetch's own retry, so decorating it too let both retries stack (up to 25 attempts in the worst case). Address Copilot review feedback on the PR. Generated-By: PostHog Code Task-Id: 34f1efbd-102b-40cc-a9a6-655f9adb71b3 --- .../data_imports/sources/taboola/taboola.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 e0a51d2dd2c1..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 @@ -102,18 +102,21 @@ def get_rows( config = TABOOLA_ENDPOINTS[endpoint] session = _get_session(client_secret) - # The initial mint below and the mid-sync re-mint on 401 (inside `fetch`) share this - # retry so a transient token-endpoint failure backs off instead of failing the sync. + # 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_token() -> str: + def mint_initial_token() -> str: return _mint_token(session, client_id, client_secret) - token = mint_token() + token = mint_initial_token() account_base = f"{TABOOLA_API_BASE_URL}/{_encode_path_segment(account_id)}" @retry( @@ -126,9 +129,10 @@ 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() + token = _mint_token(session, client_id, client_secret) response = session.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=REQUEST_TIMEOUT_SECONDS) if response.status_code == 429 or response.status_code >= 500: