Skip to content

chore(tests): harden migration-test connection reset and fix cohort optimize ordering - #73267

Merged
Radu-Raicea merged 7 commits into
masterfrom
posthog-code/deflake-migration-conn-and-cohort-optimize
Jul 24, 2026
Merged

chore(tests): harden migration-test connection reset and fix cohort optimize ordering#73267
Radu-Raicea merged 7 commits into
masterfrom
posthog-code/deflake-migration-conn-and-cohort-optimize

Conversation

@Radu-Raicea

@Radu-Raicea Radu-Raicea commented Jul 23, 2026

Copy link
Copy Markdown
Member

Problem

Two unrelated flaky test failures on master, both test-harness issues (no product bug):

  • products/warehouse_sources/.../test_migration_0052.py::...missing_a_lookback fails with django.db.utils.OperationalError: the connection is closed. The test never runs its own assertions — its DB connection is dead before setUp's first query.
  • ee/clickhouse/queries/test/test_cohort_query.py::test_cohort_filter_with_extra intermittently returns the wrong set of people from a cohort query.

Follow-up to #72758, which added a first mitigation for the connection-closed case but did not stop it.

Changes

Migration connection is closed — root cause. The connection wrapper ends up holding a psycopg handle that was closed while inside a transaction (closed_in_transaction=True). In that state connection.close() is a no-op and Django never reconnects, so setUp's MigrationExecutor query hits the dead handle. A pre-test reset can't recover this — is_usable() fails, close() no-ops. The reason the dead-handle state survives into setUp at all is that the test runs inside a TestCase class-level transaction.

Fix: run the test on NonAtomicTestMigrations (already used by several other migration tests) so there is no class transaction. Without it, a connection severed mid-test is simply reconnected on next use instead of getting stuck. (Earlier commits in this PR tried a connection reset and then a signal-disconnect; CI showed neither stopped it, which is what pointed at the class-transaction as the structural cause — those attempts are reverted.)

Kept as a smaller, independent hardening: the setUpClass reset extracted into a reusable reset_unusable_db_connections() helper (now also catching errored connections), which still covers the separate case of a prior test leaving a dead connection behind; and the warehouse conftest now resets the connection after each test as well as before, containing the direct close_old_connections() calls the transaction=True v3 duckgres backfill suites make.

Cohort query flake. CI showed the real cause: the OR cohort query intermittently returned a matched person twice (a duplicate row), not stale or missing membership. That query is built with UNION DISTINCT, so it returns distinct persons by design — a duplicate row is a ClickHouse merge/dedup timing artifact that doesn't change which persons match. The test now asserts on the set of matched persons rather than a sorted list (the pattern several sibling tests already use); it verifies membership, not result cardinality. An earlier commit's person-table optimize (a membership-staleness theory) was reverted, since the observed failure had both persons present.

How did you test this code?

No database or flox stack is available in the agent environment, so I could not run the DB-backed tests locally — validation is by CI on this branch plus lint/format/syntax checks. The first two pushes reproduced the exact failure again on the warehouse-sources shard and, by elimination, established that the dead-handle state is tied to the class transaction; this push removes that transaction for the test. Recommend confirming with an N-run loop under CI shard conditions before relying on it. The cohort test passed on these CI runs (only the warehouse-sources shard failed), so its fix is validated analytically.

Automatic notifications

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

Docs update

Not needed, test infrastructure only.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Diagnosed and authored via PostHog Code while triaging two flaky failures reported in a Slack thread, using the /fixing-flaky-tests skill. Two hypotheses were tried and falsified by CI (reset dead connections; disconnect the close_old_connections request-signal receiver) before reading the Django connection internals showed the failing state is a handle closed while in_atomic_block, which only persists because of the TestCase class transaction. Settled on the structural fix — the existing non-atomic migration base — rather than fighting Django's transaction bookkeeping. The cohort fix reorders the settle to before membership computation rather than loosening the assertion.


Created with PostHog from a Slack thread

@github-actions

Copy link
Copy Markdown
Contributor

Hey @Radu-Raicea! 👋

It looks like your git author email on this PR isn't your @posthog.com address (radu@raicea.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@Radu-Raicea
Radu-Raicea marked this pull request as ready for review July 23, 2026 15:08
@Radu-Raicea
Radu-Raicea requested a review from a team July 23, 2026 15:09
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 23, 2026 15:09
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "chore(tests): harden migration-test conn..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 66.0% of changed backend lines covered — 5 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): █████████████░░░░░░░ 66.0% (10 / 15)

File Patch Uncovered changed lines
posthog/test/base.py 16.7% 1557–1559, 1579, 1587

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 30103698640 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.6% 8,415 / 21,265
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks ██████████████░░░░░░ 69.4% 30,040 / 43,284
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 80.2% 22,273 / 27,784
cdp ████████████████░░░░ 81.0% 3,146 / 3,883
data_modeling █████████████████░░░ 82.7% 5,556 / 6,722
wizard █████████████████░░░ 84.8% 935 / 1,102
notebooks █████████████████░░░ 85.6% 7,478 / 8,736
agent_platform █████████████████░░░ 86.4% 3,807 / 4,405
actions █████████████████░░░ 86.6% 717 / 828
cohorts ██████████████████░░ 87.8% 4,488 / 5,114
product_tours ██████████████████░░ 87.9% 1,303 / 1,482
exports ██████████████████░░ 88.4% 6,951 / 7,863
data_warehouse ██████████████████░░ 88.9% 11,905 / 13,396
business_knowledge ██████████████████░░ 89.0% 4,391 / 4,936
conversations ██████████████████░░ 89.3% 16,875 / 18,894
engineering_analytics ██████████████████░░ 89.4% 6,386 / 7,145
dashboards ██████████████████░░ 89.4% 5,983 / 6,693
visual_review ██████████████████░░ 89.4% 5,818 / 6,505
error_tracking ██████████████████░░ 89.7% 10,174 / 11,343
alerts ██████████████████░░ 90.0% 4,056 / 4,508
early_access_features ██████████████████░░ 90.1% 1,031 / 1,144
mcp_analytics ██████████████████░░ 90.3% 2,815 / 3,116
streamlit_apps ██████████████████░░ 90.4% 2,501 / 2,767
links ██████████████████░░ 90.6% 183 / 202
slack_app ██████████████████░░ 90.7% 9,028 / 9,951
marketing_analytics ██████████████████░░ 91.1% 11,903 / 13,072
stamphog ██████████████████░░ 91.1% 4,056 / 4,450
mcp_store ██████████████████░░ 91.9% 4,257 / 4,634
product_analytics ███████████████████░ 92.5% 5,848 / 6,319
notifications ███████████████████░ 92.6% 1,012 / 1,093
ai_observability ███████████████████░ 92.8% 15,132 / 16,313
surveys ███████████████████░ 93.0% 5,734 / 6,167
managed_migrations ███████████████████░ 93.1% 1,424 / 1,530
web_analytics ███████████████████░ 93.2% 14,671 / 15,744
posthog_ai ███████████████████░ 93.2% 1,326 / 1,422
approvals ███████████████████░ 93.3% 3,437 / 3,682
reminders ███████████████████░ 93.4% 468 / 501
workflows ███████████████████░ 93.6% 6,512 / 6,959
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,640 / 9,177
messaging ███████████████████░ 94.3% 2,682 / 2,845
tracing ███████████████████░ 94.5% 2,670 / 2,826
review_hog ███████████████████░ 94.6% 6,898 / 7,289
skills ███████████████████░ 94.6% 3,158 / 3,337
logs ███████████████████░ 95.4% 10,012 / 10,498
experiments ███████████████████░ 95.8% 25,324 / 26,444
growth ███████████████████░ 96.1% 3,245 / 3,376
annotations ███████████████████░ 96.2% 732 / 761
replay_vision ███████████████████░ 96.2% 15,465 / 16,069
revenue_analytics ███████████████████░ 96.3% 1,887 / 1,960
feature_flags ███████████████████░ 96.5% 17,144 / 17,768
user_interviews ███████████████████░ 96.5% 2,638 / 2,734
access_control ███████████████████░ 96.9% 870 / 898
customer_analytics ███████████████████░ 97.1% 9,093 / 9,364
warehouse_sources ███████████████████░ 97.2% 340,184 / 350,022
data_catalog ███████████████████░ 97.5% 2,368 / 2,429
analytics_platform ████████████████████ 98.0% 2,153 / 2,197
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

@posthog-bot-comment-resolver

posthog-bot-comment-resolver Bot commented Jul 23, 2026

Copy link
Copy Markdown

🔀 Tried to auto-resolve conflicts with master but this one needs a human.

I won't retry until the branch or master moves.

…ptimize ordering

Two independent test-harness flakes.

1. TestMigrations "the connection is closed": a prior transaction=True test (the
   warehouse duckgres backfill suites) calls close_old_connections() on the main
   thread and severs the shared connection. Extract the setUpClass reset into a
   reusable reset_unusable_db_connections() helper, broaden detection to also cover
   connections left in an errored state, and contain the poison at its source by
   resetting after each warehouse_sources test as well as before — so it can't reach
   a victim in another shard or survive into a rerun.

2. Cohort query flake: the test computed cohort membership (calculate_people_ch,
   which reads the person table) before optimizing it, so membership could be built
   from unmerged person data. Optimizing only at read time is too late. Merge the
   person table before membership is computed.

Generated-By: PostHog Code
Task-Id: 1a2c3c38-f9f5-40e7-8c84-6f0d17193560
…tion-test connection

The setUpClass reset alone can't win the race: with CONN_MAX_AGE=0 every connection
is instantly obsolete, and inside the class-level test transaction autocommit is
disabled (!= the AUTOCOMMIT setting), so close_old_connections — wired to Django's
request_started/request_finished signals — closes the connection out from under the
open transaction if any request signal fires between setUpClass and the test's own DB
work. setUp's MigrationExecutor then fails with "the connection is closed", after the
reset already ran.

Detach the close_old_connections receiver for the life of every migration test class
(after super().setUpClass() so a failure there can't leak the disconnect process-wide)
and re-arm it in tearDownClass. Migration tests manage their connections explicitly
and don't rely on signal-driven cycling.

Generated-By: PostHog Code
Task-Id: 1a2c3c38-f9f5-40e7-8c84-6f0d17193560
…omically

Revert the signal-disconnect approach — it didn't stop the failure, because the
connection isn't severed by a request signal but is closed while in_atomic_block
(closed_in_transaction=True), a state conn.close() no-ops on and Django never
reconnects from. That dead-handle state only persists into setUp because the test
runs inside a TestCase class-level transaction.

Fix structurally: run the test on NonAtomicTestMigrations (already used by other
migration tests) so there is no class transaction. A connection severed mid-test is
then simply reconnected on next use instead of getting stuck. Keep the extracted
reset_unusable_db_connections() helper (still used by the warehouse conftest) and the
setUpClass reset as a belt for the separate prior-test-poison case.

Generated-By: PostHog Code
Task-Id: 1a2c3c38-f9f5-40e7-8c84-6f0d17193560
CI showed the real cohort flake: the OR query intermittently returned a matched
person twice (p2 appeared twice in results), not stale/missing membership. The OR
cohort query is built with UNION DISTINCT, so it returns distinct persons by design;
a duplicate row is a ClickHouse merge/dedup timing artifact that doesn't change which
persons match. Assert on the set of matched persons rather than a sorted list — the
test verifies membership, not result cardinality, and other tests in this file use the
same set-based pattern. Revert the earlier person-table optimize, which targeted a
membership-staleness theory the observed failure disproved (both persons were present).

Generated-By: PostHog Code
Task-Id: 1a2c3c38-f9f5-40e7-8c84-6f0d17193560
@Radu-Raicea
Radu-Raicea force-pushed the posthog-code/deflake-migration-conn-and-cohort-optimize branch from 79dca51 to a4a049c Compare July 23, 2026 19:20
@Radu-Raicea
Radu-Raicea enabled auto-merge (squash) July 24, 2026 15:04
@Radu-Raicea
Radu-Raicea merged commit 222a670 into master Jul 24, 2026
246 checks passed
@Radu-Raicea
Radu-Raicea deleted the posthog-code/deflake-migration-conn-and-cohort-optimize branch July 24, 2026 15:37
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-24 16:24 UTC Run
prod-us ✅ Deployed 2026-07-24 16:40 UTC Run
prod-eu ✅ Deployed 2026-07-24 16:44 UTC Run

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.

3 participants