From ddbad2218be016f3ce9e833fe65170976e55e0dc Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:07:11 -0400 Subject: [PATCH] chore(relay): drop the live-basis partial index that the planner never picks (#21305) Added in #21301 on the reasoning that the composite (active, deadline) index spans all 6.65M rows to find a few hundred live ones. Measured post-merge against production-shaped history, that reasoning does not hold: a basis is inserted active and flipped to 0, so the partial index accumulates one dead entry per deactivation exactly as the composite one does. Scan buffers are identical to composite-only in every state, 11,099 cold, 2,522 warm, 9 after VACUUM, and the planner picks the composite index throughout. The extra index costs ~65 bytes of WAL per basis insert, about 22% more. The relief is the reaper plus vacuum, which #21301 already ships. Removes the statement from SCHEMA, restores the plan test to pinning the composite index by name, and records why a narrower index is not the cure so the next reader does not re-derive it. --- .../src/credential-cleanup-sweep-postgres.test.ts | 9 +++++---- cloud/apps/relay/src/database.ts | 12 +++++------- .../src/postgres-maintenance-sweep-plans.test.ts | 5 +---- .../relay/src/relay-schema-lock-targets.test.ts | 13 ------------- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts b/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts index aa720a67b2d..45f38326d3a 100644 --- a/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts +++ b/cloud/apps/relay/src/credential-cleanup-sweep-postgres.test.ts @@ -120,10 +120,11 @@ describePostgres('credential cleanup against PostgreSQL', () => { expect(expiry).toContain('relay_invites_sweep_expiry') }) - it('plans the live-basis sweep off an index rather than the 1.5 GB heap', async () => { + it('plans the basis sweep off the composite index rather than the 1.5 GB heap', async () => { // The shape that made this the most expensive statement in the sweep: 20,000 settled bases to - // 50 live ones, so the composite (active, deadline) index spans 400x the rows the sweep wants. - // Which index serves it is the planner's call, the same as for the two invite sweeps below. + // 50 live ones. A partial index on active = 1 looks like the answer to that ratio and is not: + // a basis is inserted active and flipped to 0, so it accumulates the same dead entries, and + // the planner picks the composite index anyway. See the schema comment beside it. await database.query( `INSERT INTO relay_connection_bases (basis_conn_id, user_id, relay_host_id, relay_device_id, owning_control_generation, @@ -148,7 +149,7 @@ describePostgres('credential cleanup against PostgreSQL', () => { ) expect(sweep).not.toContain('Seq Scan on relay_connection_bases') - expect(sweep).toMatch(/using relay_connection_bases_(active|live)_deadline/) + expect(sweep).toContain('using relay_connection_bases_active_deadline') }) it('plans the drained basis reaper off the composite index, not the heap', async () => { diff --git a/cloud/apps/relay/src/database.ts b/cloud/apps/relay/src/database.ts index a62bb35798f..972f3ce2aa1 100644 --- a/cloud/apps/relay/src/database.ts +++ b/cloud/apps/relay/src/database.ts @@ -172,16 +172,14 @@ CREATE TABLE IF NOT EXISTS relay_connection_bases ( -- accumulate unboundedly. Unindexed it seq-scans millions of rows every cycle -- and holds the maintenance transaction open long enough to time out -- assignment lock waits. +-- Why not a partial index on active = 1: a basis is inserted active and flipped to 0, so each +-- deactivation leaves a dead entry in that index too. Measured on production-shaped history it +-- carries the same dead entries as this one, the planner picks this one in every state, and it +-- costs ~65 bytes of WAL per insert. Bloat here is cured by reaping and vacuum, not by a narrower +-- index. CREATE INDEX IF NOT EXISTS relay_connection_bases_active_deadline ON relay_connection_bases(active, deadline); --- schema-deferrable: created out of band, so a boot that cannot take the lock must retry --- Why: the index above spans every row, and inactive bases outnumber live ones by ~6.6M to a few --- hundred, so the sweep still walked ~283 MB of index to find them. This one holds only the rows --- the sweep can act on. Keeping both: the composite is also what makes the reaper an index range. -CREATE INDEX IF NOT EXISTS relay_connection_bases_live_deadline - ON relay_connection_bases(deadline) WHERE active = 1; - CREATE TABLE IF NOT EXISTS relay_direct_authorizations ( direct_auth_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, diff --git a/cloud/apps/relay/src/postgres-maintenance-sweep-plans.test.ts b/cloud/apps/relay/src/postgres-maintenance-sweep-plans.test.ts index de8573a0d6c..a8d0e1e3bf9 100644 --- a/cloud/apps/relay/src/postgres-maintenance-sweep-plans.test.ts +++ b/cloud/apps/relay/src/postgres-maintenance-sweep-plans.test.ts @@ -56,9 +56,6 @@ describePostgres('PostgreSQL maintenance sweep plans', () => { const plan = result.rows.map((row) => String(row['QUERY PLAN'])).join('\n') expect(plan).not.toMatch(/Seq Scan on relay_connection_bases/) - // Either index keeps the sweep off the table. It used to be the composite one; the partial - // relay_connection_bases_live_deadline now wins on cost, because it spans only the live rows - // rather than all ~6.6M, and that is the improvement, not a regression in this invariant. - expect(plan).toMatch(/relay_connection_bases_(active|live)_deadline/) + expect(plan).toMatch(/relay_connection_bases_active_deadline/) }) }) diff --git a/cloud/apps/relay/src/relay-schema-lock-targets.test.ts b/cloud/apps/relay/src/relay-schema-lock-targets.test.ts index 26348bb7d9c..68a7c6f0216 100644 --- a/cloud/apps/relay/src/relay-schema-lock-targets.test.ts +++ b/cloud/apps/relay/src/relay-schema-lock-targets.test.ts @@ -27,12 +27,6 @@ const GOLDEN_LOCK_TAKING: SchemaLockTarget[] = [ { kind: 'index', table: 'relay_devices', name: 'relay_devices_current_hash', skipWhen: 'present' }, { kind: 'index', table: 'relay_devices', name: 'relay_devices_grace_hash', skipWhen: 'present' }, { kind: 'index', table: 'relay_connection_bases', name: 'relay_connection_bases_active_deadline', skipWhen: 'present' }, - { - kind: 'index', - table: 'relay_connection_bases', - name: 'relay_connection_bases_live_deadline', - skipWhen: 'present' - }, { kind: 'index', table: 'relay_direct_authorizations', @@ -251,7 +245,6 @@ describe('relay boot-time lock targets', () => { expect(deferrable.map((statement) => sqlWithoutComments(statement).replace(/\s+/g, ' '))).toEqual([ "CREATE INDEX IF NOT EXISTS relay_invites_sweep_expiry ON relay_invites(expires_at) WHERE state IN ('available', 'reserved', 'cooldown')", "CREATE INDEX IF NOT EXISTS relay_invites_sweep_reservation ON relay_invites(reservation_expires_at) WHERE state = 'reserved'", - 'CREATE INDEX IF NOT EXISTS relay_connection_bases_live_deadline ON relay_connection_bases(deadline) WHERE active = 1', 'CREATE INDEX IF NOT EXISTS relay_direct_authorizations_pending_deadline ON relay_direct_authorizations(deadline) WHERE consumed_at IS NULL', 'CREATE INDEX IF NOT EXISTS relay_rate_windows_started ON relay_rate_windows(window_started_at)', 'DROP INDEX IF EXISTS relay_assignment_activity_expiry', @@ -274,12 +267,6 @@ describe('relay boot-time lock targets', () => { name: 'relay_invites_sweep_reservation', skipWhen: 'present' }, - { - kind: 'index', - table: 'relay_connection_bases', - name: 'relay_connection_bases_live_deadline', - skipWhen: 'present' - }, { kind: 'index', table: 'relay_direct_authorizations',