Skip to content

Commit 62e8f4a

Browse files
committed
feat(sql): return the read pin from SHOW SNAPSHOT
A reader that pages with a fresh snapshot per page can land between two producer transactions and return a half-applied batch, with no token to tell a complete page from an incomplete one. SHOW SNAPSHOT returns the monotonic WAL sequence at the moment of the call, with the node id and the build version. Capture the pin once, and a producer that records the same pin in its commit marker lets the reader decide whether the batch it paged is complete. Not tenant-admin gated: it exposes no tenant data, and a reader needs the pin before it has paged anything.
1 parent aa6e91b commit 62e8f4a

3 files changed

Lines changed: 85 additions & 0 deletions

File tree

‎nodedb/src/control/server/shared/ddl/neutral/observability.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,28 @@ pub fn show_server_stats(
158158
key_value_result(server_stats_rows(state))
159159
}
160160

161+
/// SHOW SNAPSHOT — the current read pin as `(name, value)` rows.
162+
///
163+
/// A reader pages with a fresh snapshot per page, so a page can land between
164+
/// two producer transactions and return a half-applied batch. The pin below
165+
/// is the monotonic WAL sequence at the moment of the call: capture it once,
166+
/// and a producer that records the same pin in its commit marker lets the
167+
/// reader tell whether the batch it paged is complete.
168+
///
169+
/// Not tenant-admin gated: it exposes no tenant data, and a reader needs it
170+
/// before it has paged anything.
171+
pub fn show_snapshot(state: &SharedState) -> Result<Vec<DdlResult>, DdlError> {
172+
key_value_result(vec![
173+
("snapshot_pin".to_string(), "wal_lsn".to_string()),
174+
(
175+
"wal_next_lsn".to_string(),
176+
state.wal.next_lsn().as_u64().to_string(),
177+
),
178+
("node_id".to_string(), state.node_id.to_string()),
179+
("version".to_string(), crate::version::VERSION.to_string()),
180+
])
181+
}
182+
161183
/// SHOW METRICS — `(name, value)` projection of the same source as
162184
/// SHOW STATS, with histogram percentiles appended so latency-style
163185
/// metrics are visible from the SQL surface.

‎nodedb/src/control/server/shared/ddl/neutral/router/string_introspection.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ pub(super) async fn try_string(
206206
if upper == "SHOW SERVER STATS" || upper.starts_with("SHOW SERVER STATS ") {
207207
return Some(observability::show_server_stats(state, identity));
208208
}
209+
if upper == "SHOW SNAPSHOT" || upper.starts_with("SHOW SNAPSHOT ") {
210+
return Some(observability::show_snapshot(state));
211+
}
209212
if upper == "SHOW STATS" || upper.starts_with("SHOW STATS ") {
210213
return Some(observability::show_server_stats(state, identity));
211214
}

‎nodedb/tests/wire/cases/pgwire_show_dispatch.rs‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,63 @@ async fn show_session_set_parameter_round_trips() {
460460
.expect("SHOW application_name must succeed");
461461
assert_eq!(rows, vec!["mae8_bootstrap".to_string()]);
462462
}
463+
464+
// ── SHOW SNAPSHOT ─────────────────────────────────────────────────────
465+
466+
/// `SHOW SNAPSHOT` must reach its handler (not the session-parameter
467+
/// fallback) and return the WAL pin a reader captures before paging. The pin
468+
/// must not move backwards across two reads.
469+
#[tokio::test]
470+
async fn show_snapshot_returns_the_wal_pin() {
471+
let server = TestServer::start().await;
472+
473+
assert!(
474+
!is_session_param_fallback(&server, "SHOW SNAPSHOT", "snapshot").await,
475+
"SHOW SNAPSHOT must not fall through to the session-parameter fallback"
476+
);
477+
478+
let rows = server
479+
.query_named_rows("SHOW SNAPSHOT")
480+
.await
481+
.expect("SHOW SNAPSHOT must not error");
482+
let first: std::collections::BTreeMap<String, String> = rows
483+
.into_iter()
484+
.map(|r| {
485+
(
486+
r.get("name").cloned().unwrap_or_default(),
487+
r.get("value").cloned().unwrap_or_default(),
488+
)
489+
})
490+
.collect();
491+
assert_eq!(
492+
first.get("snapshot_pin").map(String::as_str),
493+
Some("wal_lsn"),
494+
"the pin kind must be named: {first:?}"
495+
);
496+
let before: u64 = first
497+
.get("wal_next_lsn")
498+
.expect("wal_next_lsn row")
499+
.parse()
500+
.expect("wal_next_lsn is a decimal integer");
501+
502+
// A write advances the WAL; the pin must reflect it and never regress.
503+
server
504+
.exec("CREATE COLLECTION show_snapshot_probe")
505+
.await
506+
.expect("seed write");
507+
let rows = server
508+
.query_named_rows("SHOW SNAPSHOT")
509+
.await
510+
.expect("SHOW SNAPSHOT after a write");
511+
let after_row = rows
512+
.iter()
513+
.find(|r| r.get("name").map(|n| n == "wal_next_lsn").unwrap_or(false))
514+
.expect("wal_next_lsn row after a write");
515+
let after: u64 = after_row["value"]
516+
.parse()
517+
.expect("wal_next_lsn stays a decimal integer");
518+
assert!(
519+
after >= before,
520+
"the pin must not regress: {before} -> {after}"
521+
);
522+
}

0 commit comments

Comments
 (0)