Skip to content

fix(duckdbservice): wipe user-attached catalogs at session create on shared-warm workers - #1200

Closed
fuziontech wants to merge 2 commits into
mainfrom
fix/session-catalog-wipe
Closed

fuziontech wants to merge 2 commits into
mainfrom
fix/session-catalog-wipe

Conversation

@fuziontech

Copy link
Copy Markdown
Member

Problem

DuckDB ATTACH is instance-global, and a hot-idle worker pod is reused across sessions of an org. CreateSession wiped user secrets at session create (wipeUserSecrets) but never detached user catalogs — so a catalog attached by one session (e.g. an external Postgres source, whose postgres_scanner pool holds live, authenticated upstream connections) was silently inherited by the next session on that worker.

That is the same cross-user isolation boundary as the secret wipe, one level up. It also has a second-order correctness effect that surfaced in production: postgres_scanner's pg_pool_max_connections SET has no set-callback and is only read when a catalog's pool is created — a session that inherits a stale attach (ATTACH IF NOT EXISTS no-ops) cannot reconfigure the pool, the parallel-scan pg_export_snapshot/SET TRANSACTION SNAPSHOT dance resumes across a load-balanced multi-replica Postgres frontend, and imports intermittently fail with snapshot "..." does not exist.

Fix

CreateSession (shared-warm mode, same max_sessions=1 guard as the secret wipe) now detaches every non-internal attached catalog before the secret wipe, preserving the system-managed reserved set (ducklake, delta, memory — activation and the pg_catalog compat layer own those). A wipe failure fails the session, exactly like secrets. The catalog wipe runs first because attached pools can reference the secrets about to be dropped.

Tests

  • duckdbservice/user_catalogs_test.go — unit: wipe detaches user catalogs (incl. quote-unsafe names), preserves reserved/internal ones; wiring: a catalog attached by session A is gone after session B's CreateSession on the same worker (red-green verified against the unwired code).
  • tests/mw-dev/e2e/harness.sh — new user_catalog_wipe assertion: attach visible in-session, absent on the next fresh session, ducklake+memory preserved.
  • CLAUDE.md wipe-boundary invariant updated.

Co-authored-by: Shelley shelley@exe.dev

…shared-warm workers

DuckDB ATTACH is instance-global and a hot-idle worker is reused across
sessions of an org, but CreateSession only wiped user SECRETS — an
attached catalog from a previous session (e.g. an external Postgres
source, whose postgres_scanner pool holds live authenticated upstream
connections) was silently inherited by the next session. That is the
same cross-user isolation boundary as wipeUserSecrets, one level up, and
it has a second-order correctness effect: postgres_scanner's
pg_pool_max_connections SET has no set-callback and applies only at pool
creation, so a session inheriting a stale attach cannot reconfigure the
pool it inherits (ATTACH IF NOT EXISTS no-ops), which re-enables the
parallel-scan snapshot dance behind load-balanced Postgres replicas and
fails intermittently with 'snapshot ... does not exist'.

CreateSession now detaches every non-internal attached catalog except
the system-managed reserved set (ducklake, delta, memory) before the
secret wipe, failing the session on wipe error exactly like secrets.

Tests: duckdbservice/user_catalogs_test.go (unit: wipe drops user
catalogs incl. quoted names, preserves ducklake/delta/memory/system/
temp; wiring: a catalog attached by session A is gone after session B's
CreateSession on the same worker). E2E: user_catalog_wipe assertion in
tests/mw-dev/e2e/harness.sh.

Co-authored-by: Shelley <shelley@exe.dev>
@fuziontech
fuziontech requested a review from a team September 18, 2026 05:45
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown

Test Impact Plan

Deterministic summary of how this PR changes tests, CI runners, and coverage-risk signals.

Summary

Area Added Changed Deleted
Test files 1 1 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +2 / -0
  • Assertions: +21 / -0
  • Skips or known failures added: 0
  • Workflow continue-on-error added: 0
  • Workflow path filters added: 0
  • Test commands removed from justfile: 0
  • E2E/journey retry lines added: 0

Coverage risk: neutral or increased

No coverage-reduction warnings detected.

@bill-ph bill-ph left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit a6e104a.

No P0 blockers found. The shared-warm session boundary now detaches user catalogs before wiping secrets, preserves reserved/internal catalogs, quotes user-controlled identifiers safely, fails session creation on wipe errors, and adds unit, wiring, and mw-dev E2E coverage.

— Robo Bill

@benben benben left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review generated on behalf of @benben.

Approve. Adds a session-create DETACH of every non-internal, non-reserved catalog on shared-warm workers, mirroring the secret wipe. The mechanism and tests are sound for user attaches, but the reserved set does not name DuckLake's Postgres-typed metadata catalog (__ducklake_metadata_ducklake); if duckdb_databases() does not flag it internal, the wipe detaches the org's own metadata catalog on every fresh session, and the unit tests (all :memory: attaches) cannot detect that.

  • major duckdbservice/user_catalogs.go:48 DuckLake metadata catalog not reserved; wiped if not flagged internal. Confirm SELECT internal FROM duckdb_databases() WHERE database_name='__ducklake_metadata_ducklake' is true on a real worker; otherwise every CreateSession detaches it and the reaper-configured pg pool from configureDuckLakeMetadataPool is lost.
  • minor duckdbservice/service.go:1472 DestroySession still leaves user catalogs (live upstream pools) on hot-idle worker. Secrets get a best-effort wipe at destroy but attached postgres_scanner pools stay open for the whole hot-idle TTL.
  • minor duckdbservice/user_catalogs_test.go:48 Tests only attach :memory: catalogs; no real DuckLake/type catalog covered. A regression where a system-owned non-internal catalog gets detached passes both unit tests.

Model: fable.

…nection

Each pg/psql -c invocation is its own connection (and session), so the
'attached in its own session' probe ran in a SECOND session — whose
CreateSession wipe had already detached the probe (ci-pr-21200-cnpg:
'probe catalog not attached in its own session (count=0)'). Feed both
statements to psql via stdin so they ride one connection; also make the
probe catalog name run-unique.

Co-authored-by: Shelley <shelley@exe.dev>

@benben benben left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review generated on behalf of @benben.

Approve (re-review of new commits). The new commit only reworks the harness's same-session check to ride one psql connection; that part is correct, but the "fresh session" leak assertion still cannot land on the worker holding the attach under the e2e's exploratory tier.

  • major tests/mw-dev/e2e/harness.sh:2582 Leak check passes trivially: fresh SELECT never reuses the pinned worker. mw-dev sets DUCKGRES_EXPLORATORY_TIER_ENABLED=true and findIdleAssignedWorkerLocked matches on profile, so the ATTACH (DDL, pinned standard) and the plain SELECT (exploratory) land on different-shaped workers.
  • minor tests/mw-dev/e2e/harness.sh:2579 Inline psql bypasses pg_script backpressure retry; hard-fails on transient errors. A 'no Duckgres worker'/'capacity exhausted' response on the attach connection aborts the whole harness instead of retrying like every other assertion.

Model: fable.

@fuziontech

Copy link
Copy Markdown
Member Author

Superseded by #1201 (merged as 261f430), which lands the same session-boundary catalog detach. #1201 adopted this PR's single-connection harness technique, run-unique catalog name and system-catalogs-preserved assertion, and additionally resolves the primary database by oid rather than reserving memory by name, catalog-qualifies the listing, gives the detach its own deadline, and detaches best-effort at session destroy.

@fuziontech fuziontech closed this Sep 18, 2026
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