From 8511f92314536b52f085065a6b679fe312a13f18 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:48:19 +1000 Subject: [PATCH] feat(studio): show table WAL retention headroom (#50499) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What kind of change does this PR introduce? Feature ## What is the current behavior? Replicated table rows show their temporary sync slot's pending bytes, WAL risk, and last check-in, but omit the amount of WAL retention remaining. ## What is the new behavior? Replicated table rows include the temporary slot's WAL retention headroom alongside the existing sync details. Finite values use a compact byte value, an explicit unlimited value is labelled `Unlimited WAL retention`, and absent values remain omitted. | After | | --- | | CleanShot 2026-09-17 at 15 11
26@2x | ## To test 1. Open `/project//database/replication/` for a BigQuery pipeline while at least one table is completing its initial sync (or was just restarted). 2. Find that table under **Replicated tables**. 3. Confirm its **Details** cell reads in this order when all values are available: `720 MB waiting to sync · 1.3 GB WAL retention remaining · Last check-in 2 min`. 4. Confirm a temporary slot with unlimited retention shows `Unlimited WAL retention`, and a missing retention value adds no placeholder. 5. Confirm WAL warnings such as `Some changes at risk` or `Some changes lost` still appear alongside the retention value. ## Summary by CodeRabbit - **New Features** - Replication status now displays remaining WAL retention. - Shows unlimited retention when applicable or presents finite capacity in a readable format. - Reports when retention is exhausted and changes may be at risk. - **Bug Fixes** - Invalid retention values no longer produce misleading WAL retention messages. - Replication pipeline status more clearly identifies conditions where changes may be at risk due to limited or exhausted retention. --- .../ReplicationPipelineStatus.utils.test.ts | 49 ++++++++++++++++--- .../ReplicationPipelineStatus.utils.tsx | 12 +++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts index d614e0badd3..3c710816514 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest' import { getTableSyncLagLabel } from './ReplicationPipelineStatus.utils' describe('getTableSyncLagLabel', () => { - test('omits healthy slot details when the table has caught up', () => { + test('reports unlimited WAL retention for a healthy caught-up table slot', () => { expect( getTableSyncLagLabel({ active: true, @@ -12,20 +12,20 @@ describe('getTableSyncLagLabel', () => { confirmed_flush_lsn_bytes: 0, safe_wal_size_bytes: null, }) - ).toEqual([]) + ).toEqual(['Unlimited WAL retention']) }) - test('reports the backlog and last check-in', () => { + test('reports the backlog, finite WAL retention, and last check-in', () => { expect( getTableSyncLagLabel({ active: true, wal_status: 'reserved', restart_lsn_bytes: 4096, confirmed_flush_lsn_bytes: 2048, - safe_wal_size_bytes: null, + safe_wal_size_bytes: 1_363_148_800, reply_time_lag: 4800, }) - ).toEqual(['2 KB waiting to sync', 'Last check-in 4.80 s']) + ).toEqual(['2 KB waiting to sync', '1.3 GB WAL retention remaining', 'Last check-in 4.80 s']) }) test('reports slot risk without treating the expected inactive connection as a fault', () => { @@ -35,8 +35,43 @@ describe('getTableSyncLagLabel', () => { wal_status: 'unreserved', restart_lsn_bytes: 0, confirmed_flush_lsn_bytes: 0, - safe_wal_size_bytes: null, + safe_wal_size_bytes: 0, }) - ).toEqual(['Some changes at risk']) + ).toEqual(['0 bytes WAL retention remaining', 'Some changes at risk']) + }) + + test('omits WAL retention when the API does not provide it', () => { + expect( + getTableSyncLagLabel({ + active: true, + wal_status: 'reserved', + restart_lsn_bytes: 0, + confirmed_flush_lsn_bytes: 0, + }) + ).toEqual([]) + }) + + test('omits an invalid WAL retention value', () => { + expect( + getTableSyncLagLabel({ + active: true, + wal_status: 'reserved', + restart_lsn_bytes: 0, + confirmed_flush_lsn_bytes: 0, + safe_wal_size_bytes: Number.NaN, + }) + ).toEqual([]) + }) + + test('omits a negative WAL retention value', () => { + expect( + getTableSyncLagLabel({ + active: true, + wal_status: 'reserved', + restart_lsn_bytes: 0, + confirmed_flush_lsn_bytes: 0, + safe_wal_size_bytes: -1, + }) + ).toEqual([]) }) }) diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx index 29e15b43e3d..32401d89afb 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.utils.tsx @@ -276,6 +276,18 @@ export const getTableSyncLagLabel = (metrics: SlotLagMetrics): string[] => { parts.push(`${formatBytes(pendingBytes, pendingBytes < 1024 ? 0 : 1)} waiting to sync`) } + const safeWalSizeBytes = metrics.safe_wal_size_bytes + if (safeWalSizeBytes === null) { + parts.push('Unlimited WAL retention') + } else if ( + typeof safeWalSizeBytes === 'number' && + Number.isFinite(safeWalSizeBytes) && + safeWalSizeBytes >= 0 + ) { + const formattedBytes = formatBytes(safeWalSizeBytes, safeWalSizeBytes < 1024 ? 0 : 1) + parts.push(`${formattedBytes} WAL retention remaining`) + } + if (metrics.wal_status === 'unreserved') parts.push('Some changes at risk') if (metrics.wal_status === 'lost') parts.push('Some changes lost')