Skip to content

fix(data-imports): retry transient DB connection drops on two import reads - #75737

Draft
posthog[bot] wants to merge 2 commits into
masterfrom
posthog-code/data-imports-retry-db-connection-drop
Draft

fix(data-imports): retry transient DB connection drops on two import reads#75737
posthog[bot] wants to merge 2 commits into
masterfrom
posthog-code/data-imports-retry-db-connection-drop

Conversation

@posthog

@posthog posthog Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

  • A ~2 minute pooler blip threw a burst of uncaught OperationalErrors out of the data warehouse import path, straight into error tracking for the data imports team.
  • Two reads of PostHog's own Postgres go through database_sync_to_async_pool with no retry and no error classification: CDPProducer.get_dot_notated_table_name() and update_last_synced_at().
  • The worse tail risk is misclassification. A raw OperationalError from our DB stringifies the same way a customer's misconfigured source host does, so a source's get_non_retryable_errors can match it and permanently stop a healthy sync.
  • The guard already exists one method over: should_produce_table() catches OperationalError and re-raises PostHogInternalDatabaseError for exactly this reason. get_dot_notated_table_name() is its prerequisite and never got it.

Changes

Call site Before After
cdp_producer.get_dot_notated_table_name raw ORM read aretry_on_db_connection_drop + re-raise as PostHogInternalDatabaseError
pipeline_sync.update_last_synced_at raw ORM read/write aretry_on_db_connection_drop
cdp_producer.should_produce_table already guarded unchanged
  • Retry uses the existing helper in posthog/temporal/common/utils.py (evict the stale connection, retry once), same as repartition_controller.py:80. Nothing hand-rolled.
  • A second consecutive failure still propagates, but now as PostHogInternalDatabaseError, clear of the substrings source classification keys on.

Note

update_last_synced_at retries a small read-then-save, which is idempotent, so the retry is safe there.

How did you test this code?

Added two tests in test_cdp_producer.py, mirroring the existing should_produce_table connection-failure test:

  • test_get_dot_notated_table_name_retries_once_on_connection_drop – the first ORM read raises, the second succeeds, and the resolved name still comes back. Catches removal of the retry wrapper, which no existing test covers.
  • test_get_dot_notated_table_name_database_failure_stays_retryable – parameterized over OperationalError and InterfaceError; asserts a persistent failure surfaces as PostHogInternalDatabaseError whose message matches none of PostgresSource().get_non_retryable_errors(). This is the misclassification regression.
What I could and could not run
  • ruff check / ruff format – clean
  • tach check --dependencies --interfaces – clean
  • mypy --cache-fine-grained on both changed modules – clean
  • The new tests need Postgres (django_db(transaction=True)) and no database was available in this environment, so I did not execute them. No manual testing was done.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

N/A, internal retry and error-classification change with no user-facing behavior change.

🤖 Agent context

Autonomy: Fully autonomous

I (Claude) worked from an inbox report on the error burst. I checked for competing work first: #72062 added the sync retry_on_db_connection_drop helper and applied it to mark_initial_sync_complete, and #73386 broadens transient classification in pre-write maintenance. Neither touches these two call sites, so this is additive on top of the same pattern.

I considered also wrapping should_produce_table's _check() in the retry, but kept the change to the two unguarded reads the report identified rather than widening scope. I invoked /writing-tests and /writing-code-comments.


Created with PostHog Desktop from this inbox report.

…reads

get_dot_notated_table_name and update_last_synced_at read PostHog's own
Postgres through database_sync_to_async_pool with no retry, so a pooler
blip surfaced as an uncaught OperationalError.

Wrap both in the existing aretry_on_db_connection_drop helper, and extend
the DjangoOperationalError -> PostHogInternalDatabaseError re-raise from
should_produce_table to get_dot_notated_table_name so a persistent failure
escapes clear of the substrings source classification keys on.

Generated-By: PostHog Code
Task-Id: 9b03d3e4-5b15-40d7-b569-de22408ef1fe
@trunk-io

trunk-io Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

…ion-drop

Generated-By: PostHog Code
Task-Id: 9b03d3e4-5b15-40d7-b569-de22408ef1fe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants