Skip to content

Commit a01f73c

Browse files
committed
feat(pgwire): add nodedb.read_consistency session parameter
Introduces `session/read_consistency.rs` with parsing and validation for the `nodedb.read_consistency` SET parameter. Accepted values are `strong`, `bounded_staleness:<secs>`, and `eventual`. Invalid values are rejected at SET time with SQLSTATE 22023 and a descriptive error message rather than silently ignored.
1 parent a2e87e5 commit a01f73c

3 files changed

Lines changed: 169 additions & 0 deletions

File tree

nodedb/src/control/server/pgwire/handler/session_cmds.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ impl NodeDbPgHandler {
6767
}
6868
}
6969

70+
if key == super::super::session::read_consistency::PARAM_KEY
71+
&& super::super::session::read_consistency::parse_value(&value).is_none()
72+
{
73+
return Err(PgWireError::UserError(Box::new(ErrorInfo::new(
74+
"ERROR".to_owned(),
75+
"22023".to_owned(),
76+
format!(
77+
"invalid value for {}: '{value}'. Valid: strong, bounded_staleness:<secs>, eventual",
78+
super::super::session::read_consistency::PARAM_KEY
79+
),
80+
))));
81+
}
82+
7083
if key == "nodedb.tenant_id" && value.parse::<u32>().is_err() {
7184
return Err(PgWireError::UserError(Box::new(ErrorInfo::new(
7285
"ERROR".to_owned(),

nodedb/src/control/server/pgwire/session/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod cursor;
22
pub mod cursor_spill;
33
mod live;
44
mod params;
5+
pub mod read_consistency;
56
mod state;
67
mod store;
78
pub mod temp_tables;
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//! Session-level `ReadConsistency` — wire `SET` / `SHOW` for the
2+
//! `default_read_consistency` session parameter.
3+
//!
4+
//! Accepted values:
5+
//!
6+
//! - `'strong'`
7+
//! - `'bounded_staleness:<secs>'` or `'bounded_staleness:<secs>s'`
8+
//! - `'eventual'`
9+
//!
10+
//! The value is stored as a plain string in the session parameter
11+
//! map. This module provides the typed parse + accessor.
12+
13+
use std::net::SocketAddr;
14+
use std::time::Duration;
15+
16+
use crate::types::ReadConsistency;
17+
18+
use super::store::SessionStore;
19+
20+
/// Session parameter key.
21+
pub const PARAM_KEY: &str = "default_read_consistency";
22+
23+
/// Parse a user-supplied string into a `ReadConsistency`. Returns
24+
/// `None` on unrecognised input so the caller can return a helpful
25+
/// error message.
26+
pub fn parse_value(value: &str) -> Option<ReadConsistency> {
27+
let lower = value.trim().to_lowercase();
28+
match lower.as_str() {
29+
"strong" => Some(ReadConsistency::Strong),
30+
"eventual" => Some(ReadConsistency::Eventual),
31+
_ => {
32+
let stripped = lower.strip_prefix("bounded_staleness:")?;
33+
let secs_str = stripped.trim_end_matches('s').trim();
34+
let secs: f64 = secs_str.parse().ok()?;
35+
if secs <= 0.0 {
36+
return None;
37+
}
38+
Some(ReadConsistency::BoundedStaleness(Duration::from_secs_f64(
39+
secs,
40+
)))
41+
}
42+
}
43+
}
44+
45+
/// Format a `ReadConsistency` back into the canonical string form
46+
/// so `SHOW default_read_consistency` returns something parseable.
47+
pub fn format_value(rc: &ReadConsistency) -> String {
48+
match rc {
49+
ReadConsistency::Strong => "strong".to_string(),
50+
ReadConsistency::Eventual => "eventual".to_string(),
51+
ReadConsistency::BoundedStaleness(d) => {
52+
format!("bounded_staleness:{}s", d.as_secs_f64())
53+
}
54+
}
55+
}
56+
57+
impl SessionStore {
58+
/// Resolve the effective `ReadConsistency` for a session. Falls
59+
/// back to `Strong` if the parameter is unset or unparseable.
60+
pub fn read_consistency(&self, addr: &SocketAddr) -> ReadConsistency {
61+
self.get_parameter(addr, PARAM_KEY)
62+
.and_then(|v| parse_value(&v))
63+
.unwrap_or_default()
64+
}
65+
}
66+
67+
#[cfg(test)]
68+
mod tests {
69+
use super::*;
70+
71+
#[test]
72+
fn parse_strong() {
73+
assert_eq!(parse_value("strong"), Some(ReadConsistency::Strong));
74+
assert_eq!(parse_value("STRONG"), Some(ReadConsistency::Strong));
75+
}
76+
77+
#[test]
78+
fn parse_eventual() {
79+
assert_eq!(parse_value("eventual"), Some(ReadConsistency::Eventual));
80+
}
81+
82+
#[test]
83+
fn parse_bounded_staleness_seconds() {
84+
let rc = parse_value("bounded_staleness:5").unwrap();
85+
assert_eq!(
86+
rc,
87+
ReadConsistency::BoundedStaleness(Duration::from_secs(5))
88+
);
89+
}
90+
91+
#[test]
92+
fn parse_bounded_staleness_with_s_suffix() {
93+
let rc = parse_value("bounded_staleness:5s").unwrap();
94+
assert_eq!(
95+
rc,
96+
ReadConsistency::BoundedStaleness(Duration::from_secs(5))
97+
);
98+
}
99+
100+
#[test]
101+
fn parse_bounded_staleness_fractional() {
102+
let rc = parse_value("bounded_staleness:0.5s").unwrap();
103+
assert_eq!(
104+
rc,
105+
ReadConsistency::BoundedStaleness(Duration::from_millis(500))
106+
);
107+
}
108+
109+
#[test]
110+
fn parse_rejects_zero_staleness() {
111+
assert!(parse_value("bounded_staleness:0").is_none());
112+
}
113+
114+
#[test]
115+
fn parse_rejects_garbage() {
116+
assert!(parse_value("foobar").is_none());
117+
assert!(parse_value("").is_none());
118+
}
119+
120+
#[test]
121+
fn format_roundtrip_strong() {
122+
let s = format_value(&ReadConsistency::Strong);
123+
assert_eq!(parse_value(&s), Some(ReadConsistency::Strong));
124+
}
125+
126+
#[test]
127+
fn format_roundtrip_bounded() {
128+
let rc = ReadConsistency::BoundedStaleness(Duration::from_secs(10));
129+
let s = format_value(&rc);
130+
assert_eq!(parse_value(&s), Some(rc));
131+
}
132+
133+
#[test]
134+
fn format_roundtrip_eventual() {
135+
let s = format_value(&ReadConsistency::Eventual);
136+
assert_eq!(parse_value(&s), Some(ReadConsistency::Eventual));
137+
}
138+
139+
#[test]
140+
fn session_store_defaults_to_strong() {
141+
let store = SessionStore::new();
142+
let addr: SocketAddr = "127.0.0.1:5432".parse().unwrap();
143+
store.ensure_session(addr);
144+
assert_eq!(store.read_consistency(&addr), ReadConsistency::Strong);
145+
}
146+
147+
#[test]
148+
fn session_store_reads_set_value() {
149+
let store = SessionStore::new();
150+
let addr: SocketAddr = "127.0.0.1:5432".parse().unwrap();
151+
store.ensure_session(addr);
152+
store.set_parameter(&addr, PARAM_KEY.to_string(), "eventual".to_string());
153+
assert_eq!(store.read_consistency(&addr), ReadConsistency::Eventual);
154+
}
155+
}

0 commit comments

Comments
 (0)