feat(studio): show table WAL retention headroom (#50499)

## 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 |
| --- |
| <img width="1862" height="966" alt="CleanShot 2026-09-17 at 15 11
26@2x"
src="https://github.com/user-attachments/assets/8475bc8a-792c-46fc-b776-1ea20b7dfb89"
/> |

## To test

1. Open `/project/<ref>/database/replication/<pipeline-id>` 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.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-09-21 10:48:19 +10:00
1 parent e3c677fc5a
commit 8511f92314
2 files changed
+54 -7

No files matched your search

@@ -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([])
})
})
@@ -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')