1use crate::metrics::{Metrics, QueryOutcome, SyncOperation, SyncOutcome, SyncRepairLabel};
2use crate::protocol::{Message, WireParam, WireRetainedUnit, WireSyncRepairAction, WireSyncStatus};
3use powdb_auth::{Permission, Role, UserStore};
4use powdb_query::executor::{is_read_only_statement, Engine, WalDurabilityTicket};
5use powdb_query::parser;
6use powdb_query::result::{QueryError, QueryResult};
7use powdb_query::sql;
8use powdb_storage::types::Value;
9use powdb_sync::{
10 acknowledge_replica_apply, read_identity, read_units_through, replica_sync_status,
11 retained_segments_dir, validate_retained_tail_available, validate_v1_retained_units_applyable,
12 ReplicaSyncStatus, RetainedUnit, SyncRepairAction, RETAINED_SEGMENT_FORMAT_VERSION,
13};
14use std::collections::HashMap;
15use std::net::IpAddr;
16use std::path::{Path, PathBuf};
17use std::sync::{Arc, Mutex, RwLock};
18use std::time::{Duration, Instant};
19use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
20use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
21use tracing::{debug, error, info, warn};
22use zeroize::Zeroizing;
23
24pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
26
27pub type TxGate = Arc<Semaphore>;
32
33pub fn new_tx_gate() -> TxGate {
35 Arc::new(Semaphore::new(1))
36}
37
38const MAX_QUERY_LENGTH: usize = 1024 * 1024;
40
41const MAX_SYNC_PULL_UNITS: u32 = 4096;
43
44const MAX_SYNC_PULL_BYTES: u64 = 16 * 1024 * 1024;
46
47#[cfg(not(test))]
51const MAX_RESPONSE_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
52#[cfg(test)]
53const MAX_RESPONSE_PAYLOAD_SIZE: usize = 1024;
54
55const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
58
59const MAX_AUTH_FAILURES: u32 = 5;
61
62const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
64
65pub fn new_rate_limiter() -> AuthRateLimiter {
67 Arc::new(Mutex::new(HashMap::new()))
68}
69
70fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
73 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
74 let now = Instant::now();
76 map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
77
78 if let Some((count, _)) = map.get(&ip) {
79 *count >= MAX_AUTH_FAILURES
80 } else {
81 false
82 }
83}
84
85fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
87 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
88 let now = Instant::now();
89 let entry = map.entry(ip).or_insert((0, now));
90 if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
92 *entry = (1, now);
93 } else {
94 entry.0 += 1;
95 }
96}
97
98fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
100 let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
101 map.remove(&ip);
102}
103
104fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
107 use sha2::{Digest, Sha256};
108 let ha = Sha256::digest(a);
109 let hb = Sha256::digest(b);
110 let mut diff = 0u8;
111 for (x, y) in ha.iter().zip(hb.iter()) {
112 diff |= x ^ y;
113 }
114 diff == 0
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Principal {
122 pub name: String,
123 pub role: String,
124}
125
126fn is_ddl_statement(stmt: &powdb_query::ast::Statement) -> bool {
132 use powdb_query::ast::Statement;
133 let inner = match stmt {
134 Statement::Explain(inner) => inner.as_ref(),
135 other => other,
136 };
137 matches!(
138 inner,
139 Statement::CreateType(_)
140 | Statement::AlterTable(_)
141 | Statement::DropTable(_)
142 | Statement::CreateView(_)
143 | Statement::DropView(_)
144 )
145}
146
147fn required_permission(stmt: &powdb_query::ast::Statement) -> Permission {
153 if is_read_only_statement(stmt) {
154 Permission::Read
155 } else if is_ddl_statement(stmt) {
156 Permission::Ddl
157 } else {
158 Permission::Write
159 }
160}
161
162fn check_statement_permitted(
174 principal: Option<&Principal>,
175 stmt: &powdb_query::ast::Statement,
176) -> Result<(), QueryError> {
177 let Some(p) = principal else {
178 return Ok(());
181 };
182 if is_read_only_statement(stmt) {
185 return Ok(());
186 }
187 let needed = required_permission(stmt);
188 if Role::builtin(&p.role).is_some_and(|r| r.allows(needed)) {
189 return Ok(());
190 }
191 let kind = if needed == Permission::Ddl {
192 "schema-definition"
193 } else {
194 "write"
195 };
196 Err(QueryError::Execution(format!(
197 "permission denied: role '{}' cannot execute {kind} statements",
198 p.role
199 )))
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum AuthOutcome {
205 Authenticated { principal: Option<Principal> },
209 Rejected,
212}
213
214pub fn authenticate_connect(
226 users: &UserStore,
227 expected_password: Option<&str>,
228 username: Option<&str>,
229 password: Option<&str>,
230) -> AuthOutcome {
231 if !users.is_empty() {
232 let Some(name) = username else {
234 return AuthOutcome::Rejected;
235 };
236 let Some(candidate) = password else {
237 return AuthOutcome::Rejected;
238 };
239 match users.authenticate(name, candidate) {
240 Some(user) => AuthOutcome::Authenticated {
241 principal: Some(Principal {
242 name: user.name.clone(),
243 role: user.role.clone(),
244 }),
245 },
246 None => AuthOutcome::Rejected,
247 }
248 } else {
249 match expected_password {
251 Some(expected) => {
252 if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
253 AuthOutcome::Authenticated { principal: None }
254 } else {
255 AuthOutcome::Rejected
256 }
257 }
258 None => AuthOutcome::Authenticated { principal: None },
259 }
260 }
261}
262
263const DEFAULT_DB_NAME: &str = "default";
267
268fn check_db_name(configured: Option<&str>, requested: &str) -> Result<(), String> {
278 if requested.is_empty() || requested == DEFAULT_DB_NAME {
279 return Ok(());
280 }
281 match configured {
282 None => Ok(()),
283 Some(name) if requested == name => Ok(()),
284 Some(name) => Err(format!(
285 "unknown database '{requested}'; this server serves '{name}'"
286 )),
287 }
288}
289
290const SAFE_ERROR_PREFIXES: &[&str] = &[
292 "table not found",
293 "table '",
297 "type '",
298 "column not found",
299 "at position",
302 "parse error",
303 "type mismatch",
304 "unknown table",
305 "unknown column",
306 "unknown function",
307 "syntax error",
308 "expected",
309 "unexpected",
310 "missing",
311 "duplicate",
312 "invalid",
313 "cannot",
314 "no such",
315 "already exists",
316 "permission denied",
317 "row too large",
318 "unique constraint violation",
319 "sort input exceeds",
324 "join result exceeds",
325 "query exceeded memory budget",
326 "result too large",
327 "wal durability sync failed",
332];
333
334fn sanitize_error(e: &str) -> String {
338 let lower = e.to_lowercase();
339 for prefix in SAFE_ERROR_PREFIXES {
340 if lower.starts_with(prefix) {
341 return e.to_string();
342 }
343 }
344 "query execution error".into()
345}
346
347async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
350 let write_fut = async {
351 if msg.write_to(writer).await.is_err() {
352 return false;
353 }
354 writer.flush().await.is_ok()
355 };
356 tokio::time::timeout(WRITE_TIMEOUT, write_fut)
357 .await
358 .unwrap_or_default()
359}
360
361pub struct ConnOpts<'a> {
364 pub engine: Arc<RwLock<Engine>>,
365 pub tx_gate: TxGate,
366 pub expected_password: Option<Zeroizing<String>>,
369 pub users: Arc<UserStore>,
373 pub shutdown_rx: &'a mut watch::Receiver<bool>,
374 pub idle_timeout: Duration,
375 pub query_timeout: Duration,
376 pub rate_limiter: Option<&'a AuthRateLimiter>,
377 pub peer_addr: Option<std::net::SocketAddr>,
378 pub metrics: Arc<Metrics>,
380 pub tx_wait_timeout: Duration,
384 pub db_name: Option<String>,
388}
389
390type DispatchOutcome = (Result<QueryResult, QueryError>, Option<WalDurabilityTicket>);
403
404fn execute_write_deferred(
411 engine: &Arc<RwLock<Engine>>,
412 f: impl FnOnce(&mut Engine) -> Result<QueryResult, QueryError>,
413) -> DispatchOutcome {
414 let mut eng = match engine.write() {
415 Ok(eng) => eng,
416 Err(e) => {
417 return (
418 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
419 None,
420 )
421 }
422 };
423 eng.run_with_deferred_durability(f)
424}
425
426fn dispatch_query(
427 engine: &Arc<RwLock<Engine>>,
428 query: &str,
429 principal: Option<&Principal>,
430) -> DispatchOutcome {
431 let stmt_result = parser::parse(query).map_err(|e| e.to_string());
432
433 if let Ok(stmt) = &stmt_result {
437 if let Err(e) = check_statement_permitted(principal, stmt) {
438 return (Err(e), None);
439 }
440 }
441
442 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
443 if can_try_read {
444 let res = {
445 let eng = match engine.read() {
446 Ok(eng) => eng,
447 Err(e) => {
448 return (
449 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
450 None,
451 )
452 }
453 };
454 eng.execute_powql_readonly(query)
455 };
456 match res {
457 Ok(r) => return (Ok(r), None),
458 Err(QueryError::ReadonlyNeedsWrite) => {
459 }
461 Err(e) => return (Err(e), None),
462 }
463 }
464
465 if matches!(
466 parsed_transaction_control(&stmt_result),
467 Some(TransactionControl::Rollback)
468 ) {
469 let mut eng = match engine.write() {
470 Ok(eng) => eng,
471 Err(e) => {
472 return (
473 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
474 None,
475 )
476 }
477 };
478 return (execute_rollback_preserving_sync_if_needed(&mut eng), None);
479 }
480 execute_write_deferred(engine, |eng| eng.execute_powql(query))
481}
482
483fn dispatch_sql_query(
484 engine: &Arc<RwLock<Engine>>,
485 query: &str,
486 principal: Option<&Principal>,
487) -> DispatchOutcome {
488 let stmt_result = sql::parse_sql(query).map_err(|e| e.to_string());
489
490 if let Ok(stmt) = &stmt_result {
491 if let Err(e) = check_statement_permitted(principal, stmt) {
492 return (Err(e), None);
493 }
494 }
495
496 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
497 if can_try_read {
498 let res = {
499 let eng = match engine.read() {
500 Ok(eng) => eng,
501 Err(e) => {
502 return (
503 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
504 None,
505 )
506 }
507 };
508 eng.execute_sql_readonly(query)
509 };
510 match res {
511 Ok(r) => return (Ok(r), None),
512 Err(QueryError::ReadonlyNeedsWrite) => {}
513 Err(e) => return (Err(e), None),
514 }
515 }
516
517 if matches!(
518 parsed_transaction_control(&stmt_result),
519 Some(TransactionControl::Rollback)
520 ) {
521 let mut eng = match engine.write() {
522 Ok(eng) => eng,
523 Err(e) => {
524 return (
525 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
526 None,
527 )
528 }
529 };
530 return (execute_rollback_preserving_sync_if_needed(&mut eng), None);
531 }
532 execute_write_deferred(engine, |eng| eng.execute_sql(query))
533}
534
535fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
538 use powdb_query::ast::ParamValue;
539 match p {
540 WireParam::Null => ParamValue::Null,
541 WireParam::Int(v) => ParamValue::Int(*v),
542 WireParam::Float(v) => ParamValue::Float(*v),
543 WireParam::Bool(v) => ParamValue::Bool(*v),
544 WireParam::Str(s) => ParamValue::Str(s.clone()),
545 }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554enum TransactionControl {
555 Begin,
556 Commit,
557 Rollback,
558}
559
560fn transaction_control(stmt: &powdb_query::ast::Statement) -> Option<TransactionControl> {
561 use powdb_query::ast::Statement;
562 match stmt {
563 Statement::Begin => Some(TransactionControl::Begin),
564 Statement::Commit => Some(TransactionControl::Commit),
565 Statement::Rollback => Some(TransactionControl::Rollback),
566 _ => None,
567 }
568}
569
570fn classify_query_transaction_control(query: &str) -> Option<TransactionControl> {
571 parser::parse(query)
572 .ok()
573 .and_then(|stmt| transaction_control(&stmt))
574}
575
576fn classify_sql_transaction_control(query: &str) -> Option<TransactionControl> {
577 sql::parse_sql(query)
578 .ok()
579 .and_then(|stmt| transaction_control(&stmt))
580}
581
582fn classify_params_transaction_control(
583 query: &str,
584 params: &[WireParam],
585) -> Option<TransactionControl> {
586 let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
587 parser::parse_with_params(query, &bound)
588 .ok()
589 .and_then(|stmt| transaction_control(&stmt))
590}
591
592fn parsed_transaction_control(
593 stmt_result: &Result<powdb_query::ast::Statement, String>,
594) -> Option<TransactionControl> {
595 stmt_result.as_ref().ok().and_then(transaction_control)
596}
597
598fn execute_rollback_preserving_sync_if_needed(
599 engine: &mut Engine,
600) -> Result<QueryResult, QueryError> {
601 engine.rollback_transaction_preserving_wal_archive()
602}
603
604fn dispatch_query_with_params(
605 engine: &Arc<RwLock<Engine>>,
606 query: &str,
607 params: &[WireParam],
608 principal: Option<&Principal>,
609) -> DispatchOutcome {
610 let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
611
612 let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
615
616 if let Ok(stmt) = &stmt_result {
617 if let Err(e) = check_statement_permitted(principal, stmt) {
618 return (Err(e), None);
619 }
620 }
621
622 let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
623 if can_try_read {
624 let res = {
625 let eng = match engine.read() {
626 Ok(eng) => eng,
627 Err(e) => {
628 return (
629 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
630 None,
631 )
632 }
633 };
634 eng.execute_powql_readonly_with_params(query, &bound)
635 };
636 match res {
637 Ok(r) => return (Ok(r), None),
638 Err(QueryError::ReadonlyNeedsWrite) => {
639 }
641 Err(e) => return (Err(e), None),
642 }
643 }
644
645 if matches!(
646 parsed_transaction_control(&stmt_result),
647 Some(TransactionControl::Rollback)
648 ) {
649 let mut eng = match engine.write() {
650 Ok(eng) => eng,
651 Err(e) => {
652 return (
653 Err(QueryError::Execution(format!("lock poisoned: {e}"))),
654 None,
655 )
656 }
657 };
658 return (execute_rollback_preserving_sync_if_needed(&mut eng), None);
659 }
660 execute_write_deferred(engine, |eng| eng.execute_powql_with_params(query, &bound))
661}
662
663#[derive(Debug, Clone)]
664struct SyncPullRequest {
665 replica_id: String,
666 since_lsn: u64,
667 max_units: u32,
668 max_bytes: u64,
669 database_id: [u8; 16],
670 primary_generation: u64,
671 wal_format_version: u16,
672 catalog_version: u16,
673 segment_format_version: u16,
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
677enum SyncErrorClass {
678 AuthRequired,
679 PermissionDenied,
680 InvalidReplicaId,
681 ActiveTransaction,
682 QueryExecution,
683 InvalidMaxUnits,
684 InvalidMaxBytes,
685 SyncContext,
686 StatusRead,
687 CursorLsnMismatch,
688 IdentityRead,
689 IdentityOrFormatMismatch,
690 RetainedRead,
691 RetainedUnitEncoding,
692 RetainedChunkNotApplyable,
693 LsnAheadOfRemote,
694 AckValidation,
695 AckUpdate,
696 Internal,
697}
698
699impl SyncErrorClass {
700 const fn as_label(self) -> &'static str {
701 match self {
702 Self::AuthRequired => "auth_required",
703 Self::PermissionDenied => "permission_denied",
704 Self::InvalidReplicaId => "invalid_replica_id",
705 Self::ActiveTransaction => "active_transaction",
706 Self::QueryExecution => "query_execution",
707 Self::InvalidMaxUnits => "invalid_max_units",
708 Self::InvalidMaxBytes => "invalid_max_bytes",
709 Self::SyncContext => "sync_context",
710 Self::StatusRead => "status_read",
711 Self::CursorLsnMismatch => "cursor_lsn_mismatch",
712 Self::IdentityRead => "identity_read",
713 Self::IdentityOrFormatMismatch => "identity_or_format_mismatch",
714 Self::RetainedRead => "retained_read",
715 Self::RetainedUnitEncoding => "retained_unit_encoding",
716 Self::RetainedChunkNotApplyable => "retained_chunk_not_applyable",
717 Self::LsnAheadOfRemote => "lsn_ahead_of_remote",
718 Self::AckValidation => "ack_validation",
719 Self::AckUpdate => "ack_update",
720 Self::Internal => "internal",
721 }
722 }
723}
724
725#[derive(Debug, Clone)]
726struct SyncDecision {
727 message: Message,
728 error_class: Option<SyncErrorClass>,
729}
730
731impl SyncDecision {
732 fn ok(message: Message) -> Self {
733 Self {
734 message,
735 error_class: None,
736 }
737 }
738
739 fn error(class: SyncErrorClass, message: impl Into<String>) -> Self {
740 Self {
741 message: Message::Error {
742 message: message.into(),
743 },
744 error_class: Some(class),
745 }
746 }
747}
748
749fn check_sync_protocol_permitted(
750 credential_authenticated: bool,
751 principal: Option<&Principal>,
752) -> Result<(), (SyncErrorClass, String)> {
753 if !credential_authenticated {
754 return Err((
755 SyncErrorClass::AuthRequired,
756 "sync protocol requires authentication".to_string(),
757 ));
758 }
759 if let Some(principal) = principal {
760 let allowed =
761 Role::builtin(&principal.role).is_some_and(|role| role.allows(Permission::Write));
762 if !allowed {
763 return Err((
764 SyncErrorClass::PermissionDenied,
765 format!(
766 "permission denied: role '{}' cannot use sync protocol",
767 principal.role
768 ),
769 ));
770 }
771 }
772 Ok(())
773}
774
775fn validate_wire_replica_id(replica_id: &str) -> Result<(), String> {
776 if replica_id.is_empty() {
777 return Err("replica id must be non-empty".to_string());
778 }
779 if replica_id.len() > 128 {
780 return Err("replica id must be at most 128 bytes".to_string());
781 }
782 if !replica_id
783 .bytes()
784 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b':'))
785 {
786 return Err("replica id contains unsupported characters".to_string());
787 }
788 Ok(())
789}
790
791fn sync_context(engine: &Arc<RwLock<Engine>>) -> Result<(PathBuf, u64), String> {
792 let engine = engine
793 .read()
794 .map_err(|e| format!("lock poisoned while reading sync context: {e}"))?;
795 let catalog = engine.catalog();
796 Ok((catalog.data_dir().to_path_buf(), catalog.max_lsn()))
797}
798
799fn wire_repair_action(action: SyncRepairAction) -> WireSyncRepairAction {
800 match action {
801 SyncRepairAction::None => WireSyncRepairAction::None,
802 SyncRepairAction::Pull => WireSyncRepairAction::Pull,
803 SyncRepairAction::AwaitArchive => WireSyncRepairAction::AwaitArchive,
804 SyncRepairAction::Rebootstrap => WireSyncRepairAction::Rebootstrap,
805 }
806}
807
808fn wire_sync_status(status: ReplicaSyncStatus) -> WireSyncStatus {
809 WireSyncStatus {
810 replica_id: status.replica_id,
811 active: status.active,
812 last_applied_lsn: status.last_applied_lsn,
813 remote_lsn: status.remote_lsn,
814 servable_lsn: status.servable_lsn,
815 unarchived_lsn: status.unarchived_lsn,
816 lag_lsn: status.lag_lsn,
817 lag_bytes: status.lag_bytes,
818 lag_ms: status.lag_ms,
819 stale: status.stale,
820 repair_action: wire_repair_action(status.repair_action),
821 last_sync_error: status.last_sync_error,
822 }
823}
824
825fn wire_retained_unit(unit: RetainedUnit) -> WireRetainedUnit {
826 WireRetainedUnit {
827 tx_id: unit.tx_id,
828 record_type: unit.record_type,
829 lsn: unit.lsn,
830 data: unit.data,
831 }
832}
833
834fn sync_operation_outcome(message: &Message) -> SyncOutcome {
835 match message {
836 Message::SyncStatusResult { .. }
837 | Message::SyncPullResult { .. }
838 | Message::SyncAckResult { .. } => SyncOutcome::Ok,
839 _ => SyncOutcome::Error,
840 }
841}
842
843fn sync_operation_label(operation: SyncOperation) -> &'static str {
844 match operation {
845 SyncOperation::Status => "status",
846 SyncOperation::Pull => "pull",
847 SyncOperation::Ack => "ack",
848 }
849}
850
851fn sync_pull_payload_bytes(units: &[WireRetainedUnit]) -> u64 {
852 units.iter().fold(0, |total, unit| {
853 total.saturating_add(unit.encoded_len().unwrap_or(0))
854 })
855}
856
857fn sync_repair_label(action: WireSyncRepairAction) -> SyncRepairLabel {
858 match action {
859 WireSyncRepairAction::None => SyncRepairLabel::None,
860 WireSyncRepairAction::Pull => SyncRepairLabel::Pull,
861 WireSyncRepairAction::AwaitArchive => SyncRepairLabel::AwaitArchive,
862 WireSyncRepairAction::Rebootstrap => SyncRepairLabel::Rebootstrap,
863 }
864}
865
866fn wire_repair_action_label(action: WireSyncRepairAction) -> &'static str {
867 match action {
868 WireSyncRepairAction::None => "none",
869 WireSyncRepairAction::Pull => "pull",
870 WireSyncRepairAction::AwaitArchive => "await_archive",
871 WireSyncRepairAction::Rebootstrap => "rebootstrap",
872 }
873}
874
875const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;
876const FNV1A64_PRIME: u64 = 0x100000001b3;
877const INVALID_REPLICA_FINGERPRINT: &str = "invalid";
878
879fn replica_fingerprint(replica_id: &str) -> String {
880 let mut hash = FNV1A64_OFFSET;
881 for byte in replica_id.as_bytes() {
882 hash ^= u64::from(*byte);
883 hash = hash.wrapping_mul(FNV1A64_PRIME);
884 }
885 format!("{hash:016x}")
886}
887
888fn log_replica_fingerprint(replica_id: &str) -> String {
889 if validate_wire_replica_id(replica_id).is_ok() {
890 replica_fingerprint(replica_id)
891 } else {
892 INVALID_REPLICA_FINGERPRINT.to_string()
893 }
894}
895
896#[derive(Debug, Clone)]
897struct SyncLogContext {
898 replica_fingerprint: String,
899 since_lsn: Option<u64>,
900 applied_lsn: Option<u64>,
901 observed_remote_lsn: Option<u64>,
902 max_units: Option<u32>,
903 max_bytes: Option<u64>,
904}
905
906impl SyncLogContext {
907 fn status(replica_id: &str) -> Self {
908 Self::base(replica_id)
909 }
910
911 fn pull(request: &SyncPullRequest) -> Self {
912 Self {
913 replica_fingerprint: log_replica_fingerprint(&request.replica_id),
914 since_lsn: Some(request.since_lsn),
915 applied_lsn: None,
916 observed_remote_lsn: None,
917 max_units: Some(request.max_units),
918 max_bytes: Some(request.max_bytes),
919 }
920 }
921
922 fn ack(replica_id: &str, applied_lsn: u64, observed_remote_lsn: u64) -> Self {
923 Self {
924 replica_fingerprint: log_replica_fingerprint(replica_id),
925 since_lsn: None,
926 applied_lsn: Some(applied_lsn),
927 observed_remote_lsn: Some(observed_remote_lsn),
928 max_units: None,
929 max_bytes: None,
930 }
931 }
932
933 fn base(replica_id: &str) -> Self {
934 Self {
935 replica_fingerprint: log_replica_fingerprint(replica_id),
936 since_lsn: None,
937 applied_lsn: None,
938 observed_remote_lsn: None,
939 max_units: None,
940 max_bytes: None,
941 }
942 }
943}
944
945struct SyncExecutionContext<'a> {
946 tx_gate: TxGate,
947 connection_has_transaction: bool,
948 operation: SyncOperation,
949 log_context: SyncLogContext,
950 metrics: &'a Arc<Metrics>,
951 query_timeout: Duration,
952}
953
954fn log_sync_decision(
955 operation: SyncOperation,
956 context: &SyncLogContext,
957 elapsed: Duration,
958 decision: &SyncDecision,
959) {
960 let operation = sync_operation_label(operation);
961 let elapsed_ms = elapsed.as_secs_f64() * 1000.0;
962 match &decision.message {
963 Message::SyncStatusResult { status } => {
964 let repair_action = wire_repair_action_label(status.repair_action);
965 if status.stale || status.repair_action != WireSyncRepairAction::None {
966 info!(
967 operation = operation,
968 replica_fingerprint = %context.replica_fingerprint,
969 remote_lsn = status.remote_lsn,
970 last_applied_lsn = ?status.last_applied_lsn,
971 servable_lsn = ?status.servable_lsn,
972 unarchived_lsn = ?status.unarchived_lsn,
973 lag_lsn = ?status.lag_lsn,
974 lag_bytes = ?status.lag_bytes,
975 lag_ms = ?status.lag_ms,
976 stale = status.stale,
977 repair_action,
978 elapsed_ms,
979 "sync decision"
980 );
981 } else {
982 debug!(
983 operation = operation,
984 replica_fingerprint = %context.replica_fingerprint,
985 remote_lsn = status.remote_lsn,
986 last_applied_lsn = ?status.last_applied_lsn,
987 repair_action,
988 elapsed_ms,
989 "sync decision"
990 );
991 }
992 }
993 Message::SyncPullResult {
994 status,
995 units,
996 has_more,
997 } => {
998 let repair_action = wire_repair_action_label(status.repair_action);
999 let units_len = units.len();
1000 let payload_bytes = sync_pull_payload_bytes(units);
1001 if *has_more || status.stale || status.repair_action != WireSyncRepairAction::None {
1002 info!(
1003 operation = operation,
1004 replica_fingerprint = %context.replica_fingerprint,
1005 since_lsn = ?context.since_lsn,
1006 max_units = ?context.max_units,
1007 max_bytes = ?context.max_bytes,
1008 units = units_len,
1009 payload_bytes,
1010 has_more = *has_more,
1011 remote_lsn = status.remote_lsn,
1012 last_applied_lsn = ?status.last_applied_lsn,
1013 servable_lsn = ?status.servable_lsn,
1014 unarchived_lsn = ?status.unarchived_lsn,
1015 lag_lsn = ?status.lag_lsn,
1016 stale = status.stale,
1017 repair_action,
1018 elapsed_ms,
1019 "sync decision"
1020 );
1021 } else {
1022 debug!(
1023 operation = operation,
1024 replica_fingerprint = %context.replica_fingerprint,
1025 since_lsn = ?context.since_lsn,
1026 units = units_len,
1027 payload_bytes,
1028 has_more = *has_more,
1029 remote_lsn = status.remote_lsn,
1030 repair_action,
1031 elapsed_ms,
1032 "sync decision"
1033 );
1034 }
1035 }
1036 Message::SyncAckResult {
1037 previous_applied_lsn,
1038 applied_lsn,
1039 remote_lsn,
1040 advanced,
1041 status,
1042 } => {
1043 let repair_action = wire_repair_action_label(status.repair_action);
1044 if *advanced || status.stale || status.repair_action != WireSyncRepairAction::None {
1045 info!(
1046 operation = operation,
1047 replica_fingerprint = %context.replica_fingerprint,
1048 requested_applied_lsn = ?context.applied_lsn,
1049 observed_remote_lsn = ?context.observed_remote_lsn,
1050 previous_applied_lsn = *previous_applied_lsn,
1051 applied_lsn = *applied_lsn,
1052 remote_lsn = *remote_lsn,
1053 advanced = *advanced,
1054 stale = status.stale,
1055 repair_action,
1056 elapsed_ms,
1057 "sync decision"
1058 );
1059 } else {
1060 debug!(
1061 operation = operation,
1062 replica_fingerprint = %context.replica_fingerprint,
1063 requested_applied_lsn = ?context.applied_lsn,
1064 previous_applied_lsn = *previous_applied_lsn,
1065 applied_lsn = *applied_lsn,
1066 remote_lsn = *remote_lsn,
1067 advanced = *advanced,
1068 repair_action,
1069 elapsed_ms,
1070 "sync decision"
1071 );
1072 }
1073 }
1074 Message::Error { .. } => {
1075 warn!(
1076 operation = operation,
1077 replica_fingerprint = %context.replica_fingerprint,
1078 since_lsn = ?context.since_lsn,
1079 applied_lsn = ?context.applied_lsn,
1080 observed_remote_lsn = ?context.observed_remote_lsn,
1081 max_units = ?context.max_units,
1082 max_bytes = ?context.max_bytes,
1083 error_class = decision
1084 .error_class
1085 .unwrap_or(SyncErrorClass::Internal)
1086 .as_label(),
1087 elapsed_ms,
1088 "sync decision rejected"
1089 );
1090 }
1091 _ => {
1092 debug!(
1093 operation = operation,
1094 replica_fingerprint = %context.replica_fingerprint,
1095 elapsed_ms,
1096 "unexpected sync decision response"
1097 );
1098 }
1099 }
1100}
1101
1102fn trim_to_applyable_v1_prefix(
1103 raw_units: &mut Vec<RetainedUnit>,
1104 wire_units: &mut Vec<WireRetainedUnit>,
1105) -> Result<(), String> {
1106 let mut last_error = None;
1107 while !raw_units.is_empty() {
1108 match validate_v1_retained_units_applyable(raw_units) {
1109 Ok(()) => return Ok(()),
1110 Err(err) => {
1111 last_error = Some(err.to_string());
1112 raw_units.pop();
1113 wire_units.pop();
1114 }
1115 }
1116 }
1117 if let Some(error) = last_error {
1118 return Err(format!(
1119 "sync pull cannot serve an applyable V1 retained chunk with current limits: {error}"
1120 ));
1121 }
1122 Ok(())
1123}
1124
1125fn validate_sync_ack_applyable_boundary(
1126 data_dir: &Path,
1127 replica_id: &str,
1128 applied_lsn: u64,
1129 remote_lsn: u64,
1130) -> Result<(), String> {
1131 let status =
1132 replica_sync_status(data_dir, replica_id, remote_lsn).map_err(|err| err.to_string())?;
1133 let Some(previous_lsn) = status.last_applied_lsn else {
1134 return Ok(());
1135 };
1136 if applied_lsn <= previous_lsn {
1137 return Ok(());
1138 }
1139 let range_len = applied_lsn - previous_lsn;
1140 if range_len > u64::from(MAX_SYNC_PULL_UNITS) {
1141 return Err(format!(
1142 "sync ack range contains {range_len} units; acknowledge ranges no larger than {MAX_SYNC_PULL_UNITS}"
1143 ));
1144 }
1145 let max_units =
1146 usize::try_from(range_len).map_err(|_| "sync ack range is too large to validate")?;
1147 let identity = read_identity(data_dir).map_err(|err| err.to_string())?;
1148 let segment_dir = retained_segments_dir(data_dir);
1149 let units = read_units_through(
1150 &segment_dir,
1151 identity.segment_identity(),
1152 previous_lsn,
1153 applied_lsn,
1154 max_units,
1155 )
1156 .map_err(|err| err.to_string())?;
1157 if units.len() != max_units || units.last().map(|unit| unit.lsn) != Some(applied_lsn) {
1158 return Err(
1159 "sync ack does not cover a complete retained-unit range; rebootstrap required".into(),
1160 );
1161 }
1162 validate_v1_retained_units_applyable(&units).map_err(|err| err.to_string())
1163}
1164
1165#[cfg(test)]
1166fn dispatch_sync_status(
1167 engine: &Arc<RwLock<Engine>>,
1168 replica_id: String,
1169 credential_authenticated: bool,
1170 principal: Option<&Principal>,
1171) -> Message {
1172 dispatch_sync_status_decision(engine, replica_id, credential_authenticated, principal).message
1173}
1174
1175fn dispatch_sync_status_decision(
1176 engine: &Arc<RwLock<Engine>>,
1177 replica_id: String,
1178 credential_authenticated: bool,
1179 principal: Option<&Principal>,
1180) -> SyncDecision {
1181 if let Err((class, message)) =
1182 check_sync_protocol_permitted(credential_authenticated, principal)
1183 {
1184 return SyncDecision::error(class, message);
1185 }
1186 if let Err(message) = validate_wire_replica_id(&replica_id) {
1187 return SyncDecision::error(SyncErrorClass::InvalidReplicaId, message);
1188 }
1189 let (data_dir, remote_lsn) = match sync_context(engine) {
1190 Ok(context) => context,
1191 Err(message) => return SyncDecision::error(SyncErrorClass::SyncContext, message),
1192 };
1193 match replica_sync_status(&data_dir, &replica_id, remote_lsn) {
1194 Ok(status) => SyncDecision::ok(Message::SyncStatusResult {
1195 status: wire_sync_status(status),
1196 }),
1197 Err(err) => SyncDecision::error(SyncErrorClass::StatusRead, err.to_string()),
1198 }
1199}
1200
1201#[cfg(test)]
1202fn dispatch_sync_pull(
1203 engine: &Arc<RwLock<Engine>>,
1204 request: SyncPullRequest,
1205 credential_authenticated: bool,
1206 principal: Option<&Principal>,
1207) -> Message {
1208 dispatch_sync_pull_decision(engine, request, credential_authenticated, principal).message
1209}
1210
1211fn dispatch_sync_pull_decision(
1212 engine: &Arc<RwLock<Engine>>,
1213 request: SyncPullRequest,
1214 credential_authenticated: bool,
1215 principal: Option<&Principal>,
1216) -> SyncDecision {
1217 if let Err((class, message)) =
1218 check_sync_protocol_permitted(credential_authenticated, principal)
1219 {
1220 return SyncDecision::error(class, message);
1221 }
1222 if let Err(message) = validate_wire_replica_id(&request.replica_id) {
1223 return SyncDecision::error(SyncErrorClass::InvalidReplicaId, message);
1224 }
1225 if request.max_units == 0 || request.max_units > MAX_SYNC_PULL_UNITS {
1226 return SyncDecision::error(
1227 SyncErrorClass::InvalidMaxUnits,
1228 format!("sync pull maxUnits must be between 1 and {MAX_SYNC_PULL_UNITS}"),
1229 );
1230 }
1231 if request.max_bytes == 0 || request.max_bytes > MAX_SYNC_PULL_BYTES {
1232 return SyncDecision::error(
1233 SyncErrorClass::InvalidMaxBytes,
1234 format!("sync pull maxBytes must be between 1 and {MAX_SYNC_PULL_BYTES}"),
1235 );
1236 }
1237
1238 let (data_dir, remote_lsn) = match sync_context(engine) {
1239 Ok(context) => context,
1240 Err(message) => return SyncDecision::error(SyncErrorClass::SyncContext, message),
1241 };
1242 let status = match replica_sync_status(&data_dir, &request.replica_id, remote_lsn) {
1243 Ok(status) => status,
1244 Err(err) => {
1245 return SyncDecision::error(SyncErrorClass::StatusRead, err.to_string());
1246 }
1247 };
1248 let Some(cursor_lsn) = status.last_applied_lsn else {
1249 return SyncDecision::ok(Message::SyncPullResult {
1250 status: wire_sync_status(status),
1251 units: Vec::new(),
1252 has_more: false,
1253 });
1254 };
1255 if status.repair_action != SyncRepairAction::Pull {
1256 return SyncDecision::ok(Message::SyncPullResult {
1257 status: wire_sync_status(status),
1258 units: Vec::new(),
1259 has_more: false,
1260 });
1261 }
1262 if request.since_lsn != cursor_lsn {
1263 return SyncDecision::error(
1264 SyncErrorClass::CursorLsnMismatch,
1265 format!(
1266 "sync pull sinceLsn {} does not match primary cursor LSN {cursor_lsn}",
1267 request.since_lsn
1268 ),
1269 );
1270 }
1271
1272 let identity = match read_identity(&data_dir) {
1273 Ok(identity) => identity,
1274 Err(err) => {
1275 return SyncDecision::error(SyncErrorClass::IdentityRead, err.to_string());
1276 }
1277 };
1278 let expected = identity.segment_identity();
1279 if request.database_id != expected.database_id
1280 || request.primary_generation != expected.primary_generation
1281 || request.wal_format_version != expected.wal_format_version
1282 || request.catalog_version != expected.catalog_version
1283 || request.segment_format_version != RETAINED_SEGMENT_FORMAT_VERSION
1284 {
1285 return SyncDecision::error(
1286 SyncErrorClass::IdentityOrFormatMismatch,
1287 "sync pull identity or format version mismatch; rebootstrap required",
1288 );
1289 }
1290
1291 let effective_max_units = request.max_units.min(MAX_SYNC_PULL_UNITS) as usize;
1292 let requested_through_lsn = request
1293 .since_lsn
1294 .saturating_add(request.max_units as u64)
1295 .min(remote_lsn)
1296 .min(status.servable_lsn.unwrap_or(request.since_lsn));
1297 let segment_dir = retained_segments_dir(&data_dir);
1298 if requested_through_lsn > request.since_lsn {
1299 if let Err(err) = validate_retained_tail_available(
1300 &segment_dir,
1301 expected,
1302 request.since_lsn,
1303 requested_through_lsn,
1304 ) {
1305 let mut rebootstrap_status = status;
1306 rebootstrap_status.stale = true;
1307 rebootstrap_status.repair_action = SyncRepairAction::Rebootstrap;
1308 rebootstrap_status.last_sync_error = Some(format!(
1309 "retained history is unavailable; rebootstrap required: {err}"
1310 ));
1311 return SyncDecision::ok(Message::SyncPullResult {
1312 status: wire_sync_status(rebootstrap_status),
1313 units: Vec::new(),
1314 has_more: false,
1315 });
1316 }
1317 }
1318
1319 let raw_units = match read_units_through(
1320 &segment_dir,
1321 expected,
1322 request.since_lsn,
1323 requested_through_lsn,
1324 effective_max_units,
1325 ) {
1326 Ok(units) => units,
1327 Err(err) => {
1328 return SyncDecision::error(SyncErrorClass::RetainedRead, err.to_string());
1329 }
1330 };
1331
1332 let mut selected_raw = Vec::new();
1333 let mut selected = Vec::new();
1334 let mut selected_bytes = 0u64;
1335 for unit in raw_units {
1336 let wire_unit = wire_retained_unit(unit.clone());
1337 let unit_bytes = match wire_unit.encoded_len() {
1338 Ok(bytes) => bytes,
1339 Err(message) => {
1340 return SyncDecision::error(SyncErrorClass::RetainedUnitEncoding, message);
1341 }
1342 };
1343 if selected_bytes.saturating_add(unit_bytes) > request.max_bytes {
1344 if selected.is_empty() {
1345 return SyncDecision::error(
1346 SyncErrorClass::InvalidMaxBytes,
1347 "sync pull maxBytes is too small for the next retained unit",
1348 );
1349 }
1350 break;
1351 }
1352 selected_bytes += unit_bytes;
1353 selected_raw.push(unit);
1354 selected.push(wire_unit);
1355 }
1356 if let Err(message) = trim_to_applyable_v1_prefix(&mut selected_raw, &mut selected) {
1357 return SyncDecision::error(SyncErrorClass::RetainedChunkNotApplyable, message);
1358 }
1359
1360 let fetchable_through_lsn = status.servable_lsn.unwrap_or(remote_lsn).min(remote_lsn);
1361 let has_more = selected
1362 .last()
1363 .is_some_and(|unit| unit.lsn < fetchable_through_lsn);
1364 SyncDecision::ok(Message::SyncPullResult {
1365 status: wire_sync_status(status),
1366 units: selected,
1367 has_more,
1368 })
1369}
1370
1371#[cfg(test)]
1372fn dispatch_sync_ack(
1373 engine: &Arc<RwLock<Engine>>,
1374 replica_id: String,
1375 applied_lsn: u64,
1376 observed_remote_lsn: u64,
1377 credential_authenticated: bool,
1378 principal: Option<&Principal>,
1379) -> Message {
1380 dispatch_sync_ack_decision(
1381 engine,
1382 replica_id,
1383 applied_lsn,
1384 observed_remote_lsn,
1385 credential_authenticated,
1386 principal,
1387 )
1388 .message
1389}
1390
1391fn dispatch_sync_ack_decision(
1392 engine: &Arc<RwLock<Engine>>,
1393 replica_id: String,
1394 applied_lsn: u64,
1395 observed_remote_lsn: u64,
1396 credential_authenticated: bool,
1397 principal: Option<&Principal>,
1398) -> SyncDecision {
1399 if let Err((class, message)) =
1400 check_sync_protocol_permitted(credential_authenticated, principal)
1401 {
1402 return SyncDecision::error(class, message);
1403 }
1404 if let Err(message) = validate_wire_replica_id(&replica_id) {
1405 return SyncDecision::error(SyncErrorClass::InvalidReplicaId, message);
1406 }
1407 if applied_lsn > observed_remote_lsn {
1408 return SyncDecision::error(
1409 SyncErrorClass::LsnAheadOfRemote,
1410 format!(
1411 "sync ack appliedLsn {applied_lsn} is ahead of observed remoteLsn {observed_remote_lsn}"
1412 ),
1413 );
1414 }
1415 let (data_dir, remote_lsn) = match sync_context(engine) {
1416 Ok(context) => context,
1417 Err(message) => return SyncDecision::error(SyncErrorClass::SyncContext, message),
1418 };
1419 if observed_remote_lsn > remote_lsn {
1420 return SyncDecision::error(
1421 SyncErrorClass::LsnAheadOfRemote,
1422 format!(
1423 "sync ack remoteLsn {observed_remote_lsn} is ahead of primary LSN {remote_lsn}"
1424 ),
1425 );
1426 }
1427 if let Err(message) =
1428 validate_sync_ack_applyable_boundary(&data_dir, &replica_id, applied_lsn, remote_lsn)
1429 {
1430 return SyncDecision::error(SyncErrorClass::AckValidation, message);
1431 }
1432 match acknowledge_replica_apply(&data_dir, &replica_id, applied_lsn, remote_lsn) {
1433 Ok(summary) => SyncDecision::ok(Message::SyncAckResult {
1434 previous_applied_lsn: summary.previous_applied_lsn,
1435 applied_lsn: summary.applied_lsn,
1436 remote_lsn: summary.remote_lsn,
1437 advanced: summary.advanced,
1438 status: wire_sync_status(summary.status),
1439 }),
1440 Err(err) => SyncDecision::error(SyncErrorClass::AckUpdate, err.to_string()),
1441 }
1442}
1443
1444async fn run_blocking_sync<T, F>(input: T, query_timeout: Duration, f: F) -> SyncDecision
1445where
1446 T: Send + 'static,
1447 F: FnOnce(T) -> SyncDecision + Send + 'static,
1448{
1449 let mut handle = tokio::task::spawn_blocking(move || f(input));
1450 tokio::select! {
1451 result = &mut handle => match result {
1452 Ok(decision) => decision,
1453 Err(err) => SyncDecision::error(SyncErrorClass::Internal, format!("internal error: {err}")),
1454 },
1455 _ = tokio::time::sleep(query_timeout) => match handle.await {
1456 Ok(decision) => decision,
1457 Err(err) => SyncDecision::error(SyncErrorClass::Internal, format!("internal error: {err}")),
1458 },
1459 }
1460}
1461
1462async fn execute_gated_sync<T, F>(context: SyncExecutionContext<'_>, input: T, f: F) -> Message
1463where
1464 T: Send + 'static,
1465 F: FnOnce(T) -> SyncDecision + Send + 'static,
1466{
1467 let SyncExecutionContext {
1468 tx_gate,
1469 connection_has_transaction,
1470 operation,
1471 log_context,
1472 metrics,
1473 query_timeout,
1474 } = context;
1475 let start = Instant::now();
1476 if connection_has_transaction {
1477 let decision = SyncDecision::error(
1478 SyncErrorClass::ActiveTransaction,
1479 "sync protocol is unavailable inside an active transaction",
1480 );
1481 let elapsed = start.elapsed();
1482 log_sync_decision(operation, &log_context, elapsed, &decision);
1483 metrics.record_sync_operation(operation, elapsed, SyncOutcome::Error);
1484 return decision.message;
1485 }
1486
1487 let permit = match tx_gate.acquire_owned().await {
1488 Ok(permit) => permit,
1489 Err(_) => {
1490 let decision =
1491 SyncDecision::error(SyncErrorClass::QueryExecution, "query execution error");
1492 let elapsed = start.elapsed();
1493 log_sync_decision(operation, &log_context, elapsed, &decision);
1494 metrics.record_sync_operation(operation, elapsed, SyncOutcome::Error);
1495 return decision.message;
1496 }
1497 };
1498 let decision = run_blocking_sync(input, query_timeout, f).await;
1499 drop(permit);
1500 match &decision.message {
1501 Message::SyncStatusResult { status } => {
1502 metrics.record_sync_repair_action(operation, sync_repair_label(status.repair_action));
1503 }
1504 Message::SyncPullResult { status, units, .. } => {
1505 metrics.record_sync_repair_action(operation, sync_repair_label(status.repair_action));
1506 metrics.record_sync_pull_payload(units.len() as u64, sync_pull_payload_bytes(units));
1507 }
1508 Message::SyncAckResult {
1509 advanced, status, ..
1510 } => {
1511 metrics.record_sync_repair_action(operation, sync_repair_label(status.repair_action));
1512 if *advanced {
1513 metrics.inc_sync_ack_advanced();
1514 }
1515 }
1516 _ => {}
1517 }
1518 let elapsed = start.elapsed();
1519 log_sync_decision(operation, &log_context, elapsed, &decision);
1520 metrics.record_sync_operation(
1521 operation,
1522 elapsed,
1523 sync_operation_outcome(&decision.message),
1524 );
1525 decision.message
1526}
1527
1528async fn acquire_begin_permit(
1535 tx_gate: &TxGate,
1536 tx_wait_timeout: Duration,
1537 metrics: &Arc<Metrics>,
1538) -> Result<OwnedSemaphorePermit, Message> {
1539 match tokio::time::timeout(tx_wait_timeout, tx_gate.clone().acquire_owned()).await {
1540 Ok(Ok(permit)) => Ok(permit),
1541 Ok(Err(_)) => Err(Message::Error {
1542 message: "query execution error".into(),
1543 }),
1544 Err(_) => {
1545 metrics.inc_tx_gate_timeout();
1546 Err(Message::Error {
1547 message: format!(
1548 "transaction gate timeout after {}ms waiting for concurrent transaction to complete",
1549 tx_wait_timeout.as_millis()
1550 ),
1551 })
1552 }
1553 }
1554}
1555
1556#[allow(clippy::too_many_arguments)]
1562async fn execute_wire_query(
1563 engine: Arc<RwLock<Engine>>,
1564 tx_gate: TxGate,
1565 tx_permit: &mut Option<OwnedSemaphorePermit>,
1566 query: String,
1567 principal: Option<Principal>,
1568 query_timeout: Duration,
1569 tx_wait_timeout: Duration,
1570 metrics: &Arc<Metrics>,
1571) -> (Message, Option<PendingDurability>) {
1572 match classify_query_transaction_control(&query) {
1573 Some(TransactionControl::Begin) => {
1574 if tx_permit.is_some() {
1575 return (
1576 Message::Error {
1577 message: sanitize_error(
1578 "cannot begin: a transaction is already active on this connection",
1579 ),
1580 },
1581 None,
1582 );
1583 }
1584 let permit = match acquire_begin_permit(&tx_gate, tx_wait_timeout, metrics).await {
1585 Ok(permit) => permit,
1586 Err(response) => return (response, None),
1587 };
1588 let (response, ticket) = run_blocking_query(
1589 engine,
1590 query,
1591 principal,
1592 query_timeout,
1593 metrics,
1594 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1595 )
1596 .await;
1597 if is_success_response(&response) {
1598 *tx_permit = Some(permit);
1599 }
1600 (response, ticket)
1601 }
1602 Some(TransactionControl::Commit | TransactionControl::Rollback) => {
1603 let (response, ticket) = run_blocking_query(
1604 engine,
1605 query,
1606 principal,
1607 query_timeout,
1608 metrics,
1609 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1610 )
1611 .await;
1612 if is_success_response(&response) {
1613 tx_permit.take();
1618 }
1619 (response, ticket)
1620 }
1621 None if tx_permit.is_some() => {
1622 run_blocking_query(
1623 engine,
1624 query,
1625 principal,
1626 query_timeout,
1627 metrics,
1628 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1629 )
1630 .await
1631 }
1632 None => {
1633 let permit = match tx_gate.acquire_owned().await {
1634 Ok(permit) => permit,
1635 Err(_) => {
1636 return (
1637 Message::Error {
1638 message: "query execution error".into(),
1639 },
1640 None,
1641 )
1642 }
1643 };
1644 let out = run_blocking_query(
1645 engine,
1646 query,
1647 principal,
1648 query_timeout,
1649 metrics,
1650 |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1651 )
1652 .await;
1653 drop(permit);
1654 out
1655 }
1656 }
1657}
1658
1659#[allow(clippy::too_many_arguments)]
1660async fn execute_wire_query_sql(
1661 engine: Arc<RwLock<Engine>>,
1662 tx_gate: TxGate,
1663 tx_permit: &mut Option<OwnedSemaphorePermit>,
1664 query: String,
1665 principal: Option<Principal>,
1666 query_timeout: Duration,
1667 tx_wait_timeout: Duration,
1668 metrics: &Arc<Metrics>,
1669) -> (Message, Option<PendingDurability>) {
1670 match classify_sql_transaction_control(&query) {
1671 Some(TransactionControl::Begin) => {
1672 if tx_permit.is_some() {
1673 return (
1674 Message::Error {
1675 message: sanitize_error(
1676 "cannot begin: a transaction is already active on this connection",
1677 ),
1678 },
1679 None,
1680 );
1681 }
1682 let permit = match acquire_begin_permit(&tx_gate, tx_wait_timeout, metrics).await {
1683 Ok(permit) => permit,
1684 Err(response) => return (response, None),
1685 };
1686 let (response, ticket) = run_blocking_query(
1687 engine,
1688 query,
1689 principal,
1690 query_timeout,
1691 metrics,
1692 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1693 )
1694 .await;
1695 if is_success_response(&response) {
1696 *tx_permit = Some(permit);
1697 }
1698 (response, ticket)
1699 }
1700 Some(TransactionControl::Commit | TransactionControl::Rollback) => {
1701 let (response, ticket) = run_blocking_query(
1702 engine,
1703 query,
1704 principal,
1705 query_timeout,
1706 metrics,
1707 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1708 )
1709 .await;
1710 if is_success_response(&response) {
1711 tx_permit.take();
1714 }
1715 (response, ticket)
1716 }
1717 None if tx_permit.is_some() => {
1718 run_blocking_query(
1719 engine,
1720 query,
1721 principal,
1722 query_timeout,
1723 metrics,
1724 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1725 )
1726 .await
1727 }
1728 None => {
1729 let permit = match tx_gate.acquire_owned().await {
1730 Ok(permit) => permit,
1731 Err(_) => {
1732 return (
1733 Message::Error {
1734 message: "query execution error".into(),
1735 },
1736 None,
1737 )
1738 }
1739 };
1740 let out = run_blocking_query(
1741 engine,
1742 query,
1743 principal,
1744 query_timeout,
1745 metrics,
1746 |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1747 )
1748 .await;
1749 drop(permit);
1750 out
1751 }
1752 }
1753}
1754
1755#[allow(clippy::too_many_arguments)]
1759async fn execute_wire_query_with_params(
1760 engine: Arc<RwLock<Engine>>,
1761 tx_gate: TxGate,
1762 tx_permit: &mut Option<OwnedSemaphorePermit>,
1763 query: String,
1764 params: Vec<WireParam>,
1765 principal: Option<Principal>,
1766 query_timeout: Duration,
1767 tx_wait_timeout: Duration,
1768 metrics: &Arc<Metrics>,
1769) -> (Message, Option<PendingDurability>) {
1770 match classify_params_transaction_control(&query, ¶ms) {
1771 Some(TransactionControl::Begin) => {
1772 if tx_permit.is_some() {
1773 return (
1774 Message::Error {
1775 message: sanitize_error(
1776 "cannot begin: a transaction is already active on this connection",
1777 ),
1778 },
1779 None,
1780 );
1781 }
1782 let permit = match acquire_begin_permit(&tx_gate, tx_wait_timeout, metrics).await {
1783 Ok(permit) => permit,
1784 Err(response) => return (response, None),
1785 };
1786 let (response, ticket) = run_blocking_query(
1787 engine,
1788 (query, params),
1789 principal,
1790 query_timeout,
1791 metrics,
1792 |engine, (query, params), principal| {
1793 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
1794 },
1795 )
1796 .await;
1797 if is_success_response(&response) {
1798 *tx_permit = Some(permit);
1799 }
1800 (response, ticket)
1801 }
1802 Some(TransactionControl::Commit | TransactionControl::Rollback) => {
1803 let (response, ticket) = run_blocking_query(
1804 engine,
1805 (query, params),
1806 principal,
1807 query_timeout,
1808 metrics,
1809 |engine, (query, params), principal| {
1810 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
1811 },
1812 )
1813 .await;
1814 if is_success_response(&response) {
1815 tx_permit.take();
1818 }
1819 (response, ticket)
1820 }
1821 None if tx_permit.is_some() => {
1822 run_blocking_query(
1823 engine,
1824 (query, params),
1825 principal,
1826 query_timeout,
1827 metrics,
1828 |engine, (query, params), principal| {
1829 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
1830 },
1831 )
1832 .await
1833 }
1834 None => {
1835 let permit = match tx_gate.acquire_owned().await {
1836 Ok(permit) => permit,
1837 Err(_) => {
1838 return (
1839 Message::Error {
1840 message: "query execution error".into(),
1841 },
1842 None,
1843 )
1844 }
1845 };
1846 let out = run_blocking_query(
1847 engine,
1848 (query, params),
1849 principal,
1850 query_timeout,
1851 metrics,
1852 |engine, (query, params), principal| {
1853 dispatch_query_with_params(&engine, &query, ¶ms, principal.as_ref())
1854 },
1855 )
1856 .await;
1857 drop(permit);
1858 out
1859 }
1860 }
1861}
1862
1863struct DeferredQueryMetric {
1868 start: Instant,
1869 outcome: QueryOutcome,
1870 exceeded_timeout: bool,
1871}
1872
1873type PendingDurability = (WalDurabilityTicket, DeferredQueryMetric);
1875
1876async fn run_blocking_query<T, F>(
1877 engine: Arc<RwLock<Engine>>,
1878 input: T,
1879 principal: Option<Principal>,
1880 query_timeout: Duration,
1881 metrics: &Arc<Metrics>,
1882 f: F,
1883) -> (Message, Option<PendingDurability>)
1884where
1885 T: Send + 'static,
1886 F: FnOnce(Arc<RwLock<Engine>>, T, Option<Principal>) -> DispatchOutcome + Send + 'static,
1887{
1888 let _in_flight = metrics.in_flight_guard();
1889 let start = Instant::now();
1890 let mut handle = tokio::task::spawn_blocking(move || f(engine, input, principal));
1891 let mut exceeded_timeout = false;
1892 let join_result = tokio::select! {
1893 result = &mut handle => result,
1894 _ = tokio::time::sleep(query_timeout) => {
1895 exceeded_timeout = true;
1896 handle.await
1901 }
1902 };
1903
1904 let (message, ticket, outcome) = match join_result {
1905 Ok((Ok(result), ticket)) => match query_result_to_message(result) {
1906 Ok(message) => (message, ticket, QueryOutcome::Ok),
1907 Err(e) => (
1908 Message::Error {
1909 message: sanitize_error(&e.to_string()),
1910 },
1911 ticket,
1912 QueryOutcome::Error,
1913 ),
1914 },
1915 Ok((Err(e), ticket)) => {
1916 let outcome = if matches!(e, QueryError::MemoryLimitExceeded { .. }) {
1917 QueryOutcome::MemoryLimit
1918 } else {
1919 QueryOutcome::Error
1920 };
1921 (
1922 Message::Error {
1923 message: sanitize_error(&e.to_string()),
1924 },
1925 ticket,
1926 outcome,
1927 )
1928 }
1929 Err(e) => (
1930 Message::Error {
1931 message: format!("internal error: {e}"),
1932 },
1933 None,
1934 QueryOutcome::Error,
1935 ),
1936 };
1937 match ticket {
1938 Some(ticket) => (
1943 message,
1944 Some((
1945 ticket,
1946 DeferredQueryMetric {
1947 start,
1948 outcome,
1949 exceeded_timeout,
1950 },
1951 )),
1952 ),
1953 None => {
1954 if exceeded_timeout {
1955 metrics.record_query(start.elapsed(), QueryOutcome::Timeout);
1956 } else {
1957 metrics.record_query(start.elapsed(), outcome);
1958 }
1959 (message, None)
1960 }
1961 }
1962}
1963
1964async fn settle_durability_ticket(ticket: WalDurabilityTicket) -> Option<String> {
1972 match tokio::task::spawn_blocking(move || ticket.wait()).await {
1973 Ok(Ok(())) => None,
1974 Ok(Err(e)) => Some(sanitize_error(&format!("WAL durability sync failed: {e}"))),
1975 Err(e) => Some(format!("internal error: {e}")),
1976 }
1977}
1978
1979fn is_success_response(msg: &Message) -> bool {
1980 matches!(
1981 msg,
1982 Message::ResultRows { .. }
1983 | Message::ResultScalar { .. }
1984 | Message::ResultOk { .. }
1985 | Message::ResultMessage { .. }
1986 )
1987}
1988
1989fn rollback_open_transaction(engine: Arc<RwLock<Engine>>, principal: Option<Principal>) {
1990 let (res, ticket) = dispatch_query(&engine, "rollback", principal.as_ref());
1991 let _ = res;
1992 if let Some(ticket) = ticket {
1995 let _ = ticket.wait();
1996 }
1997}
1998
1999pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
2000where
2001 S: AsyncRead + AsyncWrite + Unpin,
2002{
2003 let ConnOpts {
2004 engine,
2005 tx_gate,
2006 expected_password,
2007 users,
2008 shutdown_rx,
2009 idle_timeout,
2010 query_timeout,
2011 rate_limiter,
2012 peer_addr,
2013 metrics,
2014 tx_wait_timeout,
2015 db_name: server_db_name,
2016 } = opts;
2017
2018 let peer = peer_addr
2019 .map(|a| a.to_string())
2020 .unwrap_or_else(|| "unknown".into());
2021 let peer_ip = peer_addr.map(|a| a.ip());
2022
2023 let (reader, writer) = tokio::io::split(stream);
2024 let mut reader = BufReader::new(reader);
2025 let mut writer = BufWriter::new(writer);
2026
2027 let connect_msg = loop {
2032 match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
2033 Ok(Ok(Some(Message::Ping))) => {
2034 debug!(peer = %peer, "pre-auth ping");
2035 if !write_msg(&mut writer, &Message::Pong).await {
2036 return;
2037 }
2038 continue;
2039 }
2040 Ok(Ok(Some(msg))) => break msg,
2041 Ok(Ok(None)) => {
2042 debug!(peer = %peer, "client closed before CONNECT");
2043 return;
2044 }
2045 Ok(Err(e)) => {
2046 error!(peer = %peer, error = %e, "error reading CONNECT");
2047 return;
2048 }
2049 Err(_) => {
2050 warn!(peer = %peer, "idle timeout waiting for CONNECT");
2051 return;
2052 }
2053 }
2054 };
2055
2056 let principal: Option<Principal>;
2059 let credential_auth_configured = !users.is_empty() || expected_password.is_some();
2060 match connect_msg {
2061 Message::Connect {
2062 db_name,
2063 password,
2064 username,
2065 } => {
2066 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
2068 if is_rate_limited(limiter, ip) {
2069 warn!(peer = %peer, "rate limited: too many auth failures");
2070 let err = Message::Error {
2071 message: "too many auth failures, try again later".into(),
2072 };
2073 write_msg(&mut writer, &err).await;
2074 return;
2075 }
2076 }
2077
2078 let outcome = authenticate_connect(
2079 &users,
2080 expected_password.as_ref().map(|p| p.as_str()),
2081 username.as_deref(),
2082 password.as_ref().map(|p| p.as_str()),
2083 );
2084
2085 match outcome {
2086 AuthOutcome::Rejected => {
2087 warn!(peer = %peer, db = %db_name, "auth rejected");
2088 metrics.inc_auth_failure();
2089 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
2091 record_auth_failure(limiter, ip);
2092 }
2093 let err = Message::Error {
2094 message: "authentication failed".into(),
2095 };
2096 write_msg(&mut writer, &err).await;
2097 return;
2098 }
2099 AuthOutcome::Authenticated {
2100 principal: auth_principal,
2101 } => {
2102 if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
2104 clear_auth_failures(limiter, ip);
2105 }
2106 match &auth_principal {
2107 Some(p) => {
2108 info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
2109 }
2110 None => {
2111 info!(peer = %peer, db = %db_name, "client connected");
2112 }
2113 }
2114 principal = auth_principal;
2115 }
2116 }
2117
2118 match check_db_name(server_db_name.as_deref(), &db_name) {
2125 Ok(()) => {
2126 if server_db_name.is_none() && !db_name.is_empty() && db_name != DEFAULT_DB_NAME
2127 {
2128 static NAMED_DB_WARNED: std::sync::atomic::AtomicBool =
2129 std::sync::atomic::AtomicBool::new(false);
2130 if !NAMED_DB_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
2131 warn!(
2132 peer = %peer, db = %db_name,
2133 "client requested a named database but this server serves a single global database; name ignored"
2134 );
2135 }
2136 }
2137 }
2138 Err(msg) => {
2139 warn!(peer = %peer, db = %db_name, "rejected: unknown database");
2140 let err = Message::Error { message: msg };
2141 write_msg(&mut writer, &err).await;
2142 return;
2143 }
2144 }
2145
2146 let ok = Message::ConnectOk {
2147 version: env!("CARGO_PKG_VERSION").into(),
2148 };
2149 if !write_msg(&mut writer, &ok).await {
2150 return;
2151 }
2152 }
2153 _ => {
2154 warn!(peer = %peer, "first message was not CONNECT");
2155 let err = Message::Error {
2156 message: "expected CONNECT".into(),
2157 };
2158 write_msg(&mut writer, &err).await;
2159 return;
2160 }
2161 }
2162
2163 let mut tx_permit: Option<OwnedSemaphorePermit> = None;
2164 let mut carry: Option<Message> = None;
2167
2168 'conn: loop {
2170 let msg = if let Some(m) = carry.take() {
2171 m
2172 } else {
2173 tokio::select! {
2174 result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
2176 match result {
2177 Ok(Ok(Some(msg))) => msg,
2178 Ok(Ok(None)) => break,
2179 Ok(Err(e)) => {
2180 error!(peer = %peer, error = %e, "read error");
2181 break;
2182 }
2183 Err(_) => {
2184 info!(peer = %peer, "idle timeout, closing connection");
2185 let err = Message::Error { message: "idle timeout".into() };
2186 write_msg(&mut writer, &err).await;
2187 break;
2188 }
2189 }
2190 }
2191 _ = shutdown_rx.changed() => {
2193 if *shutdown_rx.borrow() {
2194 info!(peer = %peer, "server shutting down, closing connection");
2195 let err = Message::Error { message: "server shutting down".into() };
2196 write_msg(&mut writer, &err).await;
2197 break;
2198 }
2199 continue;
2200 }
2201 }
2202 };
2203
2204 if matches!(
2210 msg,
2211 Message::Query { .. } | Message::QuerySql { .. } | Message::QueryWithParams { .. }
2212 ) {
2213 const MAX_PIPELINE_BATCH: usize = 128;
2216 const MAX_PIPELINE_BATCH_BYTES: usize = 4 << 20;
2221
2222 enum BatchFatal {
2225 Closed,
2226 ReadError,
2227 }
2228
2229 fn approx_response_bytes(msg: &Message) -> usize {
2233 match msg {
2234 Message::ResultRows { columns, rows } => {
2235 columns.iter().map(|c| c.len() + 4).sum::<usize>()
2236 + rows
2237 .iter()
2238 .map(|r| r.iter().map(|v| v.len() + 4).sum::<usize>())
2239 .sum::<usize>()
2240 }
2241 Message::ResultScalar { value } => value.len(),
2242 Message::ResultMessage { message } | Message::Error { message } => {
2243 message.len()
2244 }
2245 _ => 16,
2246 }
2247 }
2248
2249 fn complete_frame_buffered(buf: &[u8]) -> bool {
2255 buf.len() >= 6 && {
2256 let payload_len =
2257 u32::from_le_bytes(buf[2..6].try_into().expect("4-byte slice")) as usize;
2258 buf.len() - 6 >= payload_len
2259 }
2260 }
2261
2262 let mut responses: Vec<Message> = Vec::new();
2263 let mut response_bytes: usize = 0;
2264 let mut last_ticket: Option<WalDurabilityTicket> = None;
2265 let mut deferred_metrics: Vec<DeferredQueryMetric> = Vec::new();
2266 let mut fatal: Option<BatchFatal> = None;
2267 let mut current = msg;
2268 loop {
2269 let (response, ticket) = match current {
2270 Message::Query { query } => {
2271 if query.len() > MAX_QUERY_LENGTH {
2272 (
2273 Message::Error {
2274 message: format!(
2275 "query too large: {} bytes (max {})",
2276 query.len(),
2277 MAX_QUERY_LENGTH
2278 ),
2279 },
2280 None,
2281 )
2282 } else {
2283 debug!(peer = %peer, query = %query, "received query");
2284 execute_wire_query(
2285 engine.clone(),
2286 tx_gate.clone(),
2287 &mut tx_permit,
2288 query,
2289 principal.clone(),
2290 query_timeout,
2291 tx_wait_timeout,
2292 &metrics,
2293 )
2294 .await
2295 }
2296 }
2297 Message::QuerySql { query } => {
2298 if query.len() > MAX_QUERY_LENGTH {
2299 (
2300 Message::Error {
2301 message: format!(
2302 "query too large: {} bytes (max {})",
2303 query.len(),
2304 MAX_QUERY_LENGTH
2305 ),
2306 },
2307 None,
2308 )
2309 } else {
2310 debug!(peer = %peer, query = %query, "received SQL query");
2311 execute_wire_query_sql(
2312 engine.clone(),
2313 tx_gate.clone(),
2314 &mut tx_permit,
2315 query,
2316 principal.clone(),
2317 query_timeout,
2318 tx_wait_timeout,
2319 &metrics,
2320 )
2321 .await
2322 }
2323 }
2324 Message::QueryWithParams { query, params } => {
2325 if query.len() > MAX_QUERY_LENGTH {
2326 (
2327 Message::Error {
2328 message: format!(
2329 "query too large: {} bytes (max {})",
2330 query.len(),
2331 MAX_QUERY_LENGTH
2332 ),
2333 },
2334 None,
2335 )
2336 } else {
2337 debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
2338 execute_wire_query_with_params(
2339 engine.clone(),
2340 tx_gate.clone(),
2341 &mut tx_permit,
2342 query,
2343 params,
2344 principal.clone(),
2345 query_timeout,
2346 tx_wait_timeout,
2347 &metrics,
2348 )
2349 .await
2350 }
2351 }
2352 _ => unreachable!("batch loop only receives plain query frames"),
2353 };
2354 if let Some((t, m)) = ticket {
2355 last_ticket = Some(t);
2359 deferred_metrics.push(m);
2360 }
2361 response_bytes += approx_response_bytes(&response);
2362 responses.push(response);
2363
2364 if tx_permit.is_some()
2370 || responses.len() >= MAX_PIPELINE_BATCH
2371 || response_bytes >= MAX_PIPELINE_BATCH_BYTES
2372 || !complete_frame_buffered(reader.buffer())
2373 {
2374 break;
2375 }
2376 match tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)).await {
2379 Ok(Ok(Some(
2380 next @ (Message::Query { .. }
2381 | Message::QuerySql { .. }
2382 | Message::QueryWithParams { .. }),
2383 ))) => {
2384 if tx_gate.available_permits() == 0 {
2392 carry = Some(next);
2393 break;
2394 }
2395 current = next;
2396 }
2397 Ok(Ok(Some(other))) => {
2398 carry = Some(other);
2401 break;
2402 }
2403 Ok(Ok(None)) => {
2404 fatal = Some(BatchFatal::Closed);
2405 break;
2406 }
2407 Ok(Err(e)) => {
2408 error!(peer = %peer, error = %e, "read error");
2409 fatal = Some(BatchFatal::ReadError);
2410 break;
2411 }
2412 Err(_) => {
2413 error!(peer = %peer, "timeout decoding fully-buffered frame");
2416 fatal = Some(BatchFatal::ReadError);
2417 break;
2418 }
2419 }
2420 }
2421
2422 let mut durability_failed = false;
2426 if let Some(ticket) = last_ticket {
2427 if let Some(message) = settle_durability_ticket(ticket).await {
2428 durability_failed = true;
2431 for r in responses.iter_mut() {
2432 if is_success_response(r) {
2433 *r = Message::Error {
2434 message: message.clone(),
2435 };
2436 }
2437 }
2438 }
2439 }
2440 for m in deferred_metrics.drain(..) {
2441 let outcome = if m.exceeded_timeout {
2442 QueryOutcome::Timeout
2443 } else if durability_failed && matches!(m.outcome, QueryOutcome::Ok) {
2444 QueryOutcome::Error
2445 } else {
2446 m.outcome
2447 };
2448 metrics.record_query(m.start.elapsed(), outcome);
2449 }
2450
2451 for r in &responses {
2452 if !write_msg(&mut writer, r).await {
2453 break 'conn;
2454 }
2455 }
2456 match fatal {
2457 None => continue,
2458 Some(BatchFatal::Closed | BatchFatal::ReadError) => break,
2459 }
2460 }
2461
2462 let response = match msg {
2463 Message::Ping => {
2464 debug!(peer = %peer, "ping");
2465 Message::Pong
2466 }
2467 Message::SyncStatus { replica_id } => {
2468 let engine = engine.clone();
2469 let principal = principal.clone();
2470 let log_context = SyncLogContext::status(&replica_id);
2471 execute_gated_sync(
2472 SyncExecutionContext {
2473 tx_gate: tx_gate.clone(),
2474 connection_has_transaction: tx_permit.is_some(),
2475 operation: SyncOperation::Status,
2476 log_context,
2477 metrics: &metrics,
2478 query_timeout,
2479 },
2480 (engine, replica_id, credential_auth_configured, principal),
2481 |(engine, replica_id, credential_authenticated, principal)| {
2482 dispatch_sync_status_decision(
2483 &engine,
2484 replica_id,
2485 credential_authenticated,
2486 principal.as_ref(),
2487 )
2488 },
2489 )
2490 .await
2491 }
2492 Message::SyncPull {
2493 replica_id,
2494 since_lsn,
2495 max_units,
2496 max_bytes,
2497 database_id,
2498 primary_generation,
2499 wal_format_version,
2500 catalog_version,
2501 segment_format_version,
2502 } => {
2503 let engine = engine.clone();
2504 let principal = principal.clone();
2505 let request = SyncPullRequest {
2506 replica_id,
2507 since_lsn,
2508 max_units,
2509 max_bytes,
2510 database_id,
2511 primary_generation,
2512 wal_format_version,
2513 catalog_version,
2514 segment_format_version,
2515 };
2516 let log_context = SyncLogContext::pull(&request);
2517 execute_gated_sync(
2518 SyncExecutionContext {
2519 tx_gate: tx_gate.clone(),
2520 connection_has_transaction: tx_permit.is_some(),
2521 operation: SyncOperation::Pull,
2522 log_context,
2523 metrics: &metrics,
2524 query_timeout,
2525 },
2526 (engine, request, credential_auth_configured, principal),
2527 |(engine, request, credential_authenticated, principal)| {
2528 dispatch_sync_pull_decision(
2529 &engine,
2530 request,
2531 credential_authenticated,
2532 principal.as_ref(),
2533 )
2534 },
2535 )
2536 .await
2537 }
2538 Message::SyncAck {
2539 replica_id,
2540 applied_lsn,
2541 remote_lsn,
2542 } => {
2543 let engine = engine.clone();
2544 let principal = principal.clone();
2545 let log_context = SyncLogContext::ack(&replica_id, applied_lsn, remote_lsn);
2546 execute_gated_sync(
2547 SyncExecutionContext {
2548 tx_gate: tx_gate.clone(),
2549 connection_has_transaction: tx_permit.is_some(),
2550 operation: SyncOperation::Ack,
2551 log_context,
2552 metrics: &metrics,
2553 query_timeout,
2554 },
2555 (
2556 engine,
2557 replica_id,
2558 applied_lsn,
2559 remote_lsn,
2560 credential_auth_configured,
2561 principal,
2562 ),
2563 |(
2564 engine,
2565 replica_id,
2566 applied_lsn,
2567 observed_remote_lsn,
2568 credential_authenticated,
2569 principal,
2570 )| {
2571 dispatch_sync_ack_decision(
2572 &engine,
2573 replica_id,
2574 applied_lsn,
2575 observed_remote_lsn,
2576 credential_authenticated,
2577 principal.as_ref(),
2578 )
2579 },
2580 )
2581 .await
2582 }
2583 Message::Disconnect => {
2584 debug!(peer = %peer, "received DISCONNECT");
2585 break;
2586 }
2587 _ => Message::Error {
2588 message: "unexpected message type".into(),
2589 },
2590 };
2591
2592 if !write_msg(&mut writer, &response).await {
2593 break;
2594 }
2595 }
2596
2597 if tx_permit.is_some() {
2604 let engine = engine.clone();
2605 let principal = principal.clone();
2606 let _ =
2607 tokio::task::spawn_blocking(move || rollback_open_transaction(engine, principal)).await;
2608 }
2609 tx_permit.take();
2610
2611 info!(peer = %peer, "client disconnected");
2612}
2613
2614fn charge_response_bytes(total: &mut usize, bytes: usize) -> Result<(), QueryError> {
2615 *total = total.saturating_add(bytes);
2616 if *total > MAX_RESPONSE_PAYLOAD_SIZE {
2617 return Err(QueryError::Execution(format!(
2618 "result too large: encoded response exceeds {} bytes; add a limit or narrower projection",
2619 MAX_RESPONSE_PAYLOAD_SIZE
2620 )));
2621 }
2622 Ok(())
2623}
2624
2625fn query_result_to_message(result: QueryResult) -> Result<Message, QueryError> {
2626 match result {
2627 QueryResult::Rows { columns, rows } => {
2628 let mut encoded_bytes = 2usize; let mut out_columns = Vec::with_capacity(columns.len());
2630 for col in columns {
2631 charge_response_bytes(&mut encoded_bytes, 4 + col.len())?;
2632 out_columns.push(col);
2633 }
2634 charge_response_bytes(&mut encoded_bytes, 4)?; let mut str_rows = Vec::with_capacity(rows.len());
2637 for row in rows {
2638 let mut str_row = Vec::with_capacity(row.len());
2639 for value in row {
2640 let display = value_to_display(&value);
2641 charge_response_bytes(&mut encoded_bytes, 4 + display.len())?;
2642 str_row.push(display);
2643 }
2644 str_rows.push(str_row);
2645 }
2646 Ok(Message::ResultRows {
2647 columns: out_columns,
2648 rows: str_rows,
2649 })
2650 }
2651 QueryResult::Scalar(val) => Ok(Message::ResultScalar {
2652 value: value_to_display(&val),
2653 }),
2654 QueryResult::Modified(n) => Ok(Message::ResultOk { affected: n }),
2655 QueryResult::Created(name) => Ok(Message::ResultMessage {
2656 message: format!("type {name} created"),
2657 }),
2658 QueryResult::Executed { message } => Ok(Message::ResultMessage { message }),
2659 }
2660}
2661
2662fn value_to_display(v: &Value) -> String {
2666 v.to_wire_string()
2667}
2668
2669#[cfg(test)]
2670mod tests {
2671 use super::*;
2672 use powdb_storage::wal::WalRecordType;
2673 use powdb_sync::{
2674 write_identity_snapshot, write_segment_atomic, DatabaseIdentity, IdentitySnapshot,
2675 ReplicaCursor, RetainedSegment, RetainedUnit,
2676 };
2677
2678 #[test]
2681 fn null_serializes_as_null_bareword_on_wire() {
2682 assert_eq!(value_to_display(&Value::Empty), "null");
2683 }
2684
2685 #[test]
2688 fn unique_violation_error_surfaces_to_remote_clients() {
2689 assert_eq!(
2692 sanitize_error("unique constraint violation on User.email"),
2693 "unique constraint violation on User.email"
2694 );
2695 }
2696
2697 #[test]
2698 fn internal_errors_stay_generic() {
2699 assert_eq!(
2700 sanitize_error("some internal io panic detail"),
2701 "query execution error"
2702 );
2703 }
2704
2705 #[test]
2708 fn db_name_unpinned_accepts_any_name() {
2709 for requested in ["", "default", "prod", "anything"] {
2710 assert!(
2711 check_db_name(None, requested).is_ok(),
2712 "rejected {requested}"
2713 );
2714 }
2715 }
2716
2717 #[test]
2718 fn db_name_pinned_accepts_match_empty_and_default_sentinel() {
2719 assert!(check_db_name(Some("prod"), "prod").is_ok());
2722 assert!(check_db_name(Some("prod"), "").is_ok());
2723 assert!(check_db_name(Some("prod"), DEFAULT_DB_NAME).is_ok());
2724 }
2725
2726 #[test]
2727 fn db_name_pinned_rejects_foreign_with_clear_message() {
2728 let err = check_db_name(Some("prod"), "staging").unwrap_err();
2729 assert_eq!(err, "unknown database 'staging'; this server serves 'prod'");
2730 }
2731
2732 #[tokio::test]
2735 async fn begin_permit_acquires_when_gate_is_free() {
2736 let gate = new_tx_gate();
2737 let metrics = Arc::new(Metrics::new());
2738 let permit = acquire_begin_permit(&gate, Duration::from_secs(5), &metrics)
2739 .await
2740 .expect("should acquire a free gate");
2741 assert_eq!(gate.available_permits(), 0, "permit must be held");
2742 drop(permit);
2743 assert_eq!(gate.available_permits(), 1, "permit must release on drop");
2744 }
2745
2746 #[tokio::test]
2747 async fn begin_permit_times_out_with_clear_error_and_truthful_metric() {
2748 let gate = new_tx_gate();
2749 let metrics = Arc::new(Metrics::new());
2750 let _held = gate.clone().acquire_owned().await.unwrap();
2752 let err = acquire_begin_permit(&gate, Duration::from_millis(25), &metrics)
2753 .await
2754 .expect_err("must time out while the gate is held");
2755 match err {
2756 Message::Error { message } => {
2757 assert!(
2758 message.contains("transaction gate timeout after 25ms"),
2759 "unexpected message: {message}"
2760 );
2761 assert!(
2762 message.contains("waiting for concurrent transaction"),
2763 "unexpected message: {message}"
2764 );
2765 }
2766 other => panic!("expected Error, got {other:?}"),
2767 }
2768 let rendered = metrics.render();
2769 assert!(rendered.contains("powdb_tx_gate_timeouts_total 1"));
2770 assert!(rendered.contains("powdb_queries_total{result=\"error\"} 1"));
2772 }
2773
2774 #[test]
2775 fn resource_limit_errors_surface_actionable_hints() {
2776 for msg in [
2781 "sort input exceeds row limit — add a LIMIT clause",
2782 "join result exceeds row limit",
2783 "query exceeded memory budget: requested 100 bytes, limit 50 bytes",
2784 "result too large: encoded response exceeds 1024 bytes; add a limit or narrower projection",
2785 ] {
2786 assert_eq!(sanitize_error(msg), msg, "should pass through verbatim");
2787 }
2788 }
2789
2790 #[test]
2791 fn oversized_result_is_rejected_before_wire_encoding() {
2792 let long = "x".repeat(MAX_RESPONSE_PAYLOAD_SIZE);
2793 let result = QueryResult::Rows {
2794 columns: vec!["payload".into()],
2795 rows: vec![vec![Value::Str(long)]],
2796 };
2797 let err = query_result_to_message(result).unwrap_err();
2798 assert!(
2799 err.to_string().starts_with("result too large"),
2800 "unexpected error: {err}"
2801 );
2802 }
2803
2804 fn parsed(q: &str) -> powdb_query::ast::Statement {
2807 parser::parse(q).unwrap()
2808 }
2809
2810 fn principal(role: &str) -> Option<Principal> {
2811 Some(Principal {
2812 name: "u".into(),
2813 role: role.into(),
2814 })
2815 }
2816
2817 #[test]
2818 fn readonly_can_read_but_not_write() {
2819 let p = principal("readonly");
2820 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
2822 assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
2823 assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
2824 for q in [
2826 r#"insert User { name := "x" }"#,
2827 "User filter .id = 1 update { age := 2 }",
2828 "User filter .id = 1 delete",
2829 "drop User",
2830 "alter User add column c: str",
2831 "type T { required id: int }",
2832 "begin",
2833 "commit",
2834 "rollback",
2835 ] {
2836 let err = check_statement_permitted(p.as_ref(), &parsed(q))
2837 .expect_err(&format!("must deny: {q}"));
2838 assert!(
2839 err.to_string().contains("permission denied"),
2840 "unexpected error for {q}: {err}"
2841 );
2842 }
2843 }
2844
2845 #[test]
2846 fn readwrite_and_admin_have_full_query_access() {
2847 for role in ["readwrite", "admin"] {
2848 let p = principal(role);
2849 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
2850 assert!(check_statement_permitted(
2851 p.as_ref(),
2852 &parsed(r#"insert User { name := "x" }"#)
2853 )
2854 .is_ok());
2855 assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
2856 }
2857 }
2858
2859 #[test]
2860 fn unknown_role_fails_closed_for_writes() {
2861 let p = principal("mystery");
2862 assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
2863 assert!(
2864 check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
2865 .is_err()
2866 );
2867 }
2868
2869 #[test]
2870 fn no_principal_means_full_access() {
2871 assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
2873 assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
2874 }
2875
2876 fn store_with_alice() -> UserStore {
2877 let mut s = UserStore::new();
2878 s.create_user("alice", "pw", "readwrite").unwrap();
2879 s
2880 }
2881
2882 #[test]
2885 fn empty_store_no_password_is_open() {
2886 let s = UserStore::new();
2887 assert_eq!(
2888 authenticate_connect(&s, None, None, None),
2889 AuthOutcome::Authenticated { principal: None }
2890 );
2891 assert_eq!(
2893 authenticate_connect(&s, None, Some("x"), Some("y")),
2894 AuthOutcome::Authenticated { principal: None }
2895 );
2896 }
2897
2898 #[test]
2899 fn empty_store_correct_shared_password_succeeds() {
2900 let s = UserStore::new();
2901 assert_eq!(
2902 authenticate_connect(&s, Some("pw"), None, Some("pw")),
2903 AuthOutcome::Authenticated { principal: None }
2904 );
2905 }
2906
2907 #[test]
2908 fn empty_store_wrong_shared_password_rejected() {
2909 let s = UserStore::new();
2910 assert_eq!(
2911 authenticate_connect(&s, Some("pw"), None, Some("bad")),
2912 AuthOutcome::Rejected
2913 );
2914 }
2915
2916 #[test]
2917 fn empty_store_missing_password_rejected_when_expected() {
2918 let s = UserStore::new();
2919 assert_eq!(
2920 authenticate_connect(&s, Some("pw"), None, None),
2921 AuthOutcome::Rejected
2922 );
2923 }
2924
2925 #[test]
2926 fn empty_store_ignores_username_for_shared_password() {
2927 let s = UserStore::new();
2930 assert_eq!(
2931 authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
2932 AuthOutcome::Authenticated { principal: None }
2933 );
2934 }
2935
2936 #[test]
2939 fn user_auth_success_binds_principal() {
2940 let s = store_with_alice();
2941 assert_eq!(
2942 authenticate_connect(&s, None, Some("alice"), Some("pw")),
2943 AuthOutcome::Authenticated {
2944 principal: Some(Principal {
2945 name: "alice".into(),
2946 role: "readwrite".into(),
2947 })
2948 }
2949 );
2950 }
2951
2952 #[test]
2953 fn user_auth_wrong_password_rejected() {
2954 let s = store_with_alice();
2955 assert_eq!(
2956 authenticate_connect(&s, None, Some("alice"), Some("bad")),
2957 AuthOutcome::Rejected
2958 );
2959 }
2960
2961 #[test]
2962 fn user_auth_unknown_user_rejected() {
2963 let s = store_with_alice();
2964 assert_eq!(
2965 authenticate_connect(&s, None, Some("mallory"), Some("pw")),
2966 AuthOutcome::Rejected
2967 );
2968 }
2969
2970 #[test]
2971 fn user_auth_missing_username_rejected() {
2972 let s = store_with_alice();
2973 assert_eq!(
2974 authenticate_connect(&s, None, None, Some("pw")),
2975 AuthOutcome::Rejected
2976 );
2977 }
2978
2979 #[test]
2980 fn user_auth_missing_password_rejected() {
2981 let s = store_with_alice();
2982 assert_eq!(
2983 authenticate_connect(&s, Some("pw"), Some("alice"), None),
2984 AuthOutcome::Rejected
2985 );
2986 }
2987
2988 #[test]
2989 fn user_auth_ignores_shared_password_when_users_present() {
2990 let s = store_with_alice();
2993 assert_eq!(
2994 authenticate_connect(&s, Some("shared"), None, Some("shared")),
2995 AuthOutcome::Rejected
2996 );
2997 }
2998
2999 #[test]
3000 fn replica_fingerprint_is_stable_and_redacted() {
3001 let replica_id = "customer-prod-replica-a";
3002 let fingerprint = replica_fingerprint(replica_id);
3003 assert_eq!(fingerprint, replica_fingerprint(replica_id));
3004 assert_eq!(fingerprint, log_replica_fingerprint(replica_id));
3005 assert_ne!(fingerprint, replica_fingerprint("customer-prod-replica-b"));
3006 assert_eq!(fingerprint.len(), 16);
3007 assert!(fingerprint.chars().all(|c| c.is_ascii_hexdigit()));
3008 assert!(!fingerprint.contains("customer"));
3009 assert!(!fingerprint.contains("replica"));
3010 assert!(!fingerprint.contains(replica_id));
3011 }
3012
3013 #[test]
3014 fn invalid_replica_ids_use_fixed_log_fingerprint() {
3015 assert_eq!(log_replica_fingerprint(""), INVALID_REPLICA_FINGERPRINT);
3016 assert_eq!(
3017 log_replica_fingerprint("customer/prod/replica"),
3018 INVALID_REPLICA_FINGERPRINT
3019 );
3020 assert_eq!(
3021 log_replica_fingerprint(&"a".repeat(4096)),
3022 INVALID_REPLICA_FINGERPRINT
3023 );
3024 }
3025
3026 #[test]
3027 fn sync_error_classes_use_bounded_labels() {
3028 assert_eq!(SyncErrorClass::AuthRequired.as_label(), "auth_required");
3029 assert_eq!(
3030 SyncErrorClass::PermissionDenied.as_label(),
3031 "permission_denied"
3032 );
3033 assert_eq!(
3034 SyncErrorClass::IdentityOrFormatMismatch.as_label(),
3035 "identity_or_format_mismatch"
3036 );
3037 assert_eq!(SyncErrorClass::AckValidation.as_label(), "ack_validation");
3038 assert_eq!(SyncErrorClass::Internal.as_label(), "internal");
3039 }
3040
3041 fn sync_identity() -> DatabaseIdentity {
3042 DatabaseIdentity {
3043 database_id: *b"server-sync-test",
3044 primary_generation: 1,
3045 }
3046 }
3047
3048 fn retained_unit(lsn: u64) -> RetainedUnit {
3049 RetainedUnit {
3050 tx_id: 1,
3051 record_type: 4,
3052 lsn,
3053 data: lsn.to_le_bytes().to_vec(),
3054 }
3055 }
3056
3057 fn retained_unit_with(tx_id: u64, record_type: WalRecordType, lsn: u64) -> RetainedUnit {
3058 RetainedUnit {
3059 tx_id,
3060 record_type: record_type as u8,
3061 lsn,
3062 data: lsn.to_le_bytes().to_vec(),
3063 }
3064 }
3065
3066 fn write_sync_identity_and_tail(data_dir: &std::path::Path, through_lsn: u64) {
3067 let identity = sync_identity();
3068 write_identity_snapshot(data_dir, &IdentitySnapshot::from_identity(identity, 1)).unwrap();
3069 let units = (1..=through_lsn).map(retained_unit).collect();
3070 let segment = RetainedSegment::new(identity.segment_identity(), units).unwrap();
3071 write_segment_atomic(&retained_segments_dir(data_dir), &segment).unwrap();
3072 }
3073
3074 fn write_sync_identity_and_units(data_dir: &std::path::Path, units: Vec<RetainedUnit>) {
3075 let identity = sync_identity();
3076 write_identity_snapshot(data_dir, &IdentitySnapshot::from_identity(identity, 1)).unwrap();
3077 let segment = RetainedSegment::new(identity.segment_identity(), units).unwrap();
3078 write_segment_atomic(&retained_segments_dir(data_dir), &segment).unwrap();
3079 }
3080
3081 fn write_sync_identity_only(data_dir: &std::path::Path) {
3082 let identity = sync_identity();
3083 write_identity_snapshot(data_dir, &IdentitySnapshot::from_identity(identity, 1)).unwrap();
3084 }
3085
3086 fn admin_principal() -> Principal {
3087 Principal {
3088 name: "admin".into(),
3089 role: "admin".into(),
3090 }
3091 }
3092
3093 #[test]
3094 fn sync_protocol_requires_credential_auth_and_rejects_readonly() {
3095 let dir = tempfile::tempdir().unwrap();
3096 let engine = Arc::new(RwLock::new(Engine::new(dir.path()).unwrap()));
3097
3098 match dispatch_sync_status(&engine, "replica-a".into(), false, None) {
3099 Message::Error { message } => {
3100 assert!(message.contains("requires authentication"));
3101 }
3102 other => panic!("expected auth error, got {other:?}"),
3103 }
3104
3105 let readonly = Principal {
3106 name: "reader".into(),
3107 role: "readonly".into(),
3108 };
3109 match dispatch_sync_status(&engine, "replica-a".into(), true, Some(&readonly)) {
3110 Message::Error { message } => {
3111 assert!(message.contains("permission denied"));
3112 }
3113 other => panic!("expected permission error, got {other:?}"),
3114 }
3115 }
3116
3117 #[test]
3118 fn sync_status_pull_and_ack_use_server_remote_lsn() {
3119 let dir = tempfile::tempdir().unwrap();
3120 let mut engine = Engine::new(dir.path()).unwrap();
3121 engine
3122 .execute_powql("type SyncT { required id: int, v: str }")
3123 .unwrap();
3124 engine
3125 .execute_powql(r#"insert SyncT { id := 1, v := "one" }"#)
3126 .unwrap();
3127 let remote_lsn = engine.catalog().max_lsn();
3128 assert!(remote_lsn > 0);
3129 write_sync_identity_and_tail(dir.path(), remote_lsn);
3130 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3131 .unwrap();
3132
3133 let engine = Arc::new(RwLock::new(engine));
3134 let principal = admin_principal();
3135 let status = match dispatch_sync_status(&engine, "replica-a".into(), true, Some(&principal))
3136 {
3137 Message::SyncStatusResult { status } => status,
3138 other => panic!("expected sync status, got {other:?}"),
3139 };
3140 assert_eq!(status.remote_lsn, remote_lsn);
3141 assert_eq!(status.servable_lsn, Some(remote_lsn));
3142 assert_eq!(status.unarchived_lsn, Some(0));
3143 assert_eq!(status.last_applied_lsn, Some(0));
3144 assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3145 assert!(status.stale);
3146
3147 let identity = sync_identity().segment_identity();
3148 let pull = SyncPullRequest {
3149 replica_id: "replica-a".into(),
3150 since_lsn: 0,
3151 max_units: MAX_SYNC_PULL_UNITS,
3152 max_bytes: MAX_SYNC_PULL_BYTES,
3153 database_id: identity.database_id,
3154 primary_generation: identity.primary_generation,
3155 wal_format_version: identity.wal_format_version,
3156 catalog_version: identity.catalog_version,
3157 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3158 };
3159 let units = match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3160 Message::SyncPullResult {
3161 status,
3162 units,
3163 has_more,
3164 } => {
3165 assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3166 assert!(!has_more);
3167 units
3168 }
3169 other => panic!("expected sync pull result, got {other:?}"),
3170 };
3171 assert_eq!(units.len() as u64, remote_lsn);
3172 assert_eq!(units.last().unwrap().lsn, remote_lsn);
3173
3174 let ack = match dispatch_sync_ack(
3175 &engine,
3176 "replica-a".into(),
3177 remote_lsn,
3178 remote_lsn,
3179 true,
3180 Some(&principal),
3181 ) {
3182 Message::SyncAckResult {
3183 previous_applied_lsn,
3184 applied_lsn,
3185 remote_lsn: ack_remote_lsn,
3186 advanced,
3187 status,
3188 } => {
3189 assert_eq!(previous_applied_lsn, 0);
3190 assert_eq!(applied_lsn, remote_lsn);
3191 assert_eq!(ack_remote_lsn, remote_lsn);
3192 assert!(advanced);
3193 status
3194 }
3195 other => panic!("expected sync ack result, got {other:?}"),
3196 };
3197 assert_eq!(ack.repair_action, WireSyncRepairAction::None);
3198 assert!(!ack.stale);
3199 assert_eq!(ack.lag_lsn, Some(0));
3200 }
3201
3202 #[test]
3203 fn sync_pull_and_ack_reject_transaction_cut_boundaries() {
3204 let dir = tempfile::tempdir().unwrap();
3205 let mut engine = Engine::new(dir.path()).unwrap();
3206 engine
3207 .execute_powql("type SyncT { required id: int }")
3208 .unwrap();
3209 for id in 1..=3 {
3210 engine
3211 .execute_powql(&format!("insert SyncT {{ id := {id} }}"))
3212 .unwrap();
3213 }
3214 let remote_lsn = engine.catalog().max_lsn();
3215 assert!(remote_lsn >= 3);
3216 write_sync_identity_and_units(
3217 dir.path(),
3218 vec![
3219 retained_unit_with(77, WalRecordType::Begin, 1),
3220 retained_unit_with(77, WalRecordType::Insert, 2),
3221 retained_unit_with(77, WalRecordType::Commit, 3),
3222 ],
3223 );
3224 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3225 .unwrap();
3226
3227 let engine = Arc::new(RwLock::new(engine));
3228 let principal = admin_principal();
3229 let identity = sync_identity().segment_identity();
3230 let cut_pull = SyncPullRequest {
3231 replica_id: "replica-a".into(),
3232 since_lsn: 0,
3233 max_units: 2,
3234 max_bytes: MAX_SYNC_PULL_BYTES,
3235 database_id: identity.database_id,
3236 primary_generation: identity.primary_generation,
3237 wal_format_version: identity.wal_format_version,
3238 catalog_version: identity.catalog_version,
3239 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3240 };
3241 match dispatch_sync_pull(&engine, cut_pull, true, Some(&principal)) {
3242 Message::Error { message } => assert!(message.contains("cuts through transaction")),
3243 other => panic!("expected transaction-cut pull error, got {other:?}"),
3244 }
3245
3246 let cut_bytes_pull = SyncPullRequest {
3247 replica_id: "replica-a".into(),
3248 since_lsn: 0,
3249 max_units: 3,
3250 max_bytes: 58,
3251 database_id: identity.database_id,
3252 primary_generation: identity.primary_generation,
3253 wal_format_version: identity.wal_format_version,
3254 catalog_version: identity.catalog_version,
3255 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3256 };
3257 match dispatch_sync_pull(&engine, cut_bytes_pull, true, Some(&principal)) {
3258 Message::Error { message } => assert!(message.contains("cuts through transaction")),
3259 other => panic!("expected byte-capped transaction-cut pull error, got {other:?}"),
3260 }
3261
3262 let full_pull = SyncPullRequest {
3263 replica_id: "replica-a".into(),
3264 since_lsn: 0,
3265 max_units: 3,
3266 max_bytes: MAX_SYNC_PULL_BYTES,
3267 database_id: identity.database_id,
3268 primary_generation: identity.primary_generation,
3269 wal_format_version: identity.wal_format_version,
3270 catalog_version: identity.catalog_version,
3271 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3272 };
3273 match dispatch_sync_pull(&engine, full_pull, true, Some(&principal)) {
3274 Message::SyncPullResult { units, .. } => {
3275 assert_eq!(units.len(), 3);
3276 assert_eq!(units.last().unwrap().lsn, 3);
3277 }
3278 other => panic!("expected complete transaction pull, got {other:?}"),
3279 }
3280
3281 match dispatch_sync_ack(
3282 &engine,
3283 "replica-a".into(),
3284 2,
3285 remote_lsn,
3286 true,
3287 Some(&principal),
3288 ) {
3289 Message::Error { message } => assert!(message.contains("cuts through transaction")),
3290 other => panic!("expected transaction-cut ack error, got {other:?}"),
3291 }
3292 let cursor = powdb_sync::read_replica_cursors(dir.path()).unwrap();
3293 assert_eq!(cursor[0].applied_lsn, 0);
3294
3295 match dispatch_sync_ack(
3296 &engine,
3297 "replica-a".into(),
3298 3,
3299 remote_lsn,
3300 true,
3301 Some(&principal),
3302 ) {
3303 Message::SyncAckResult {
3304 previous_applied_lsn,
3305 applied_lsn,
3306 advanced,
3307 ..
3308 } => {
3309 assert_eq!(previous_applied_lsn, 0);
3310 assert_eq!(applied_lsn, 3);
3311 assert!(advanced);
3312 }
3313 other => panic!("expected complete transaction ack, got {other:?}"),
3314 }
3315 }
3316
3317 #[test]
3318 fn sync_pull_byte_cap_returns_applyable_prefix_with_reused_tx_id() {
3319 let dir = tempfile::tempdir().unwrap();
3320 let mut engine = Engine::new(dir.path()).unwrap();
3321 engine
3322 .execute_powql("type SyncT { required id: int }")
3323 .unwrap();
3324 for id in 1..=6 {
3325 engine
3326 .execute_powql(&format!("insert SyncT {{ id := {id} }}"))
3327 .unwrap();
3328 }
3329 let remote_lsn = engine.catalog().max_lsn();
3330 assert!(remote_lsn >= 6);
3331 write_sync_identity_and_units(
3332 dir.path(),
3333 vec![
3334 retained_unit_with(1, WalRecordType::Begin, 1),
3335 retained_unit_with(1, WalRecordType::Insert, 2),
3336 retained_unit_with(1, WalRecordType::Commit, 3),
3337 retained_unit_with(1, WalRecordType::Begin, 4),
3338 retained_unit_with(1, WalRecordType::Insert, 5),
3339 retained_unit_with(1, WalRecordType::Commit, 6),
3340 ],
3341 );
3342 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3343 .unwrap();
3344
3345 let engine = Arc::new(RwLock::new(engine));
3346 let principal = admin_principal();
3347 let identity = sync_identity().segment_identity();
3348 let pull = SyncPullRequest {
3349 replica_id: "replica-a".into(),
3350 since_lsn: 0,
3351 max_units: 6,
3352 max_bytes: 100,
3353 database_id: identity.database_id,
3354 primary_generation: identity.primary_generation,
3355 wal_format_version: identity.wal_format_version,
3356 catalog_version: identity.catalog_version,
3357 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3358 };
3359
3360 match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3361 Message::SyncPullResult {
3362 status,
3363 units,
3364 has_more,
3365 } => {
3366 assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3367 assert_eq!(units.len(), 3);
3368 assert_eq!(units.last().unwrap().lsn, 3);
3369 assert!(has_more);
3370 }
3371 other => panic!("expected byte-capped applyable prefix, got {other:?}"),
3372 }
3373 }
3374
3375 #[test]
3376 fn sync_pull_never_serves_units_beyond_server_remote_lsn() {
3377 let dir = tempfile::tempdir().unwrap();
3378 let mut engine = Engine::new(dir.path()).unwrap();
3379 engine
3380 .execute_powql("type SyncT { required id: int }")
3381 .unwrap();
3382 engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3383 let remote_lsn = engine.catalog().max_lsn();
3384 assert!(remote_lsn > 0);
3385 write_sync_identity_and_tail(dir.path(), remote_lsn + 2);
3386 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3387 .unwrap();
3388
3389 let engine = Arc::new(RwLock::new(engine));
3390 let principal = admin_principal();
3391 let identity = sync_identity().segment_identity();
3392 let pull = SyncPullRequest {
3393 replica_id: "replica-a".into(),
3394 since_lsn: 0,
3395 max_units: MAX_SYNC_PULL_UNITS,
3396 max_bytes: MAX_SYNC_PULL_BYTES,
3397 database_id: identity.database_id,
3398 primary_generation: identity.primary_generation,
3399 wal_format_version: identity.wal_format_version,
3400 catalog_version: identity.catalog_version,
3401 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3402 };
3403
3404 match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3405 Message::SyncPullResult {
3406 status,
3407 units,
3408 has_more,
3409 } => {
3410 assert_eq!(status.remote_lsn, remote_lsn);
3411 assert_eq!(status.servable_lsn, Some(remote_lsn));
3412 assert_eq!(status.unarchived_lsn, Some(0));
3413 assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3414 assert!(!has_more);
3415 assert_eq!(units.len() as u64, remote_lsn);
3416 assert_eq!(units.last().unwrap().lsn, remote_lsn);
3417 assert!(units.iter().all(|unit| unit.lsn <= remote_lsn));
3418 }
3419 other => panic!("expected capped sync pull result, got {other:?}"),
3420 }
3421 }
3422
3423 #[test]
3424 fn sync_status_reports_await_archive_when_primary_outruns_retained_tail() {
3425 let dir = tempfile::tempdir().unwrap();
3426 let mut engine = Engine::new(dir.path()).unwrap();
3427 engine
3428 .execute_powql("type SyncT { required id: int }")
3429 .unwrap();
3430 engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3431 let remote_lsn = engine.catalog().max_lsn();
3432 assert!(remote_lsn > 0);
3433 write_sync_identity_only(dir.path());
3434 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3435 .unwrap();
3436
3437 let engine = Arc::new(RwLock::new(engine));
3438 let principal = admin_principal();
3439 let identity = sync_identity().segment_identity();
3440 let status = match dispatch_sync_status(&engine, "replica-a".into(), true, Some(&principal))
3441 {
3442 Message::SyncStatusResult { status } => status,
3443 other => panic!("expected sync status, got {other:?}"),
3444 };
3445 assert_eq!(status.remote_lsn, remote_lsn);
3446 assert_eq!(status.servable_lsn, Some(0));
3447 assert_eq!(status.unarchived_lsn, Some(remote_lsn));
3448 assert_eq!(status.repair_action, WireSyncRepairAction::AwaitArchive);
3449 assert!(status
3450 .last_sync_error
3451 .as_deref()
3452 .unwrap()
3453 .contains("not yet archived"));
3454
3455 let pull = SyncPullRequest {
3456 replica_id: "replica-a".into(),
3457 since_lsn: 0,
3458 max_units: MAX_SYNC_PULL_UNITS,
3459 max_bytes: MAX_SYNC_PULL_BYTES,
3460 database_id: identity.database_id,
3461 primary_generation: identity.primary_generation,
3462 wal_format_version: identity.wal_format_version,
3463 catalog_version: identity.catalog_version,
3464 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3465 };
3466 match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3467 Message::SyncPullResult {
3468 status,
3469 units,
3470 has_more,
3471 } => {
3472 assert_eq!(status.repair_action, WireSyncRepairAction::AwaitArchive);
3473 assert!(units.is_empty());
3474 assert!(!has_more);
3475 }
3476 other => panic!("expected await-archive sync pull result, got {other:?}"),
3477 }
3478 }
3479
3480 #[test]
3481 fn sync_pull_serves_partial_retained_prefix_when_archive_lags_remote_lsn() {
3482 let dir = tempfile::tempdir().unwrap();
3483 let mut engine = Engine::new(dir.path()).unwrap();
3484 engine
3485 .execute_powql("type SyncT { required id: int }")
3486 .unwrap();
3487 engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3488 engine.execute_powql("insert SyncT { id := 2 }").unwrap();
3489 let remote_lsn = engine.catalog().max_lsn();
3490 assert!(remote_lsn > 1);
3491 let servable_lsn = remote_lsn - 1;
3492 write_sync_identity_and_tail(dir.path(), servable_lsn);
3493 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3494 .unwrap();
3495
3496 let engine = Arc::new(RwLock::new(engine));
3497 let principal = admin_principal();
3498 let identity = sync_identity().segment_identity();
3499 let pull = SyncPullRequest {
3500 replica_id: "replica-a".into(),
3501 since_lsn: 0,
3502 max_units: MAX_SYNC_PULL_UNITS,
3503 max_bytes: MAX_SYNC_PULL_BYTES,
3504 database_id: identity.database_id,
3505 primary_generation: identity.primary_generation,
3506 wal_format_version: identity.wal_format_version,
3507 catalog_version: identity.catalog_version,
3508 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3509 };
3510
3511 match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3512 Message::SyncPullResult {
3513 status,
3514 units,
3515 has_more,
3516 } => {
3517 assert_eq!(status.remote_lsn, remote_lsn);
3518 assert_eq!(status.servable_lsn, Some(servable_lsn));
3519 assert_eq!(status.unarchived_lsn, Some(1));
3520 assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3521 assert!(!has_more);
3522 assert_eq!(units.len() as u64, servable_lsn);
3523 assert_eq!(units.last().unwrap().lsn, servable_lsn);
3524 assert!(units.iter().all(|unit| unit.lsn <= servable_lsn));
3525 }
3526 other => panic!("expected partial sync pull result, got {other:?}"),
3527 }
3528 }
3529
3530 #[test]
3531 fn sync_pull_rejects_cursor_or_format_mismatch() {
3532 let dir = tempfile::tempdir().unwrap();
3533 let mut engine = Engine::new(dir.path()).unwrap();
3534 engine
3535 .execute_powql("type SyncT { required id: int }")
3536 .unwrap();
3537 engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3538 let remote_lsn = engine.catalog().max_lsn();
3539 write_sync_identity_and_tail(dir.path(), remote_lsn);
3540 powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3541 .unwrap();
3542 let engine = Arc::new(RwLock::new(engine));
3543 let principal = admin_principal();
3544 let identity = sync_identity().segment_identity();
3545
3546 let wrong_cursor = SyncPullRequest {
3547 replica_id: "replica-a".into(),
3548 since_lsn: 1,
3549 max_units: 10,
3550 max_bytes: MAX_SYNC_PULL_BYTES,
3551 database_id: identity.database_id,
3552 primary_generation: identity.primary_generation,
3553 wal_format_version: identity.wal_format_version,
3554 catalog_version: identity.catalog_version,
3555 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3556 };
3557 match dispatch_sync_pull(&engine, wrong_cursor, true, Some(&principal)) {
3558 Message::Error { message } => assert!(message.contains("does not match")),
3559 other => panic!("expected cursor mismatch error, got {other:?}"),
3560 }
3561
3562 let wrong_format = SyncPullRequest {
3563 replica_id: "replica-a".into(),
3564 since_lsn: 0,
3565 max_units: 10,
3566 max_bytes: MAX_SYNC_PULL_BYTES,
3567 database_id: identity.database_id,
3568 primary_generation: identity.primary_generation,
3569 wal_format_version: identity.wal_format_version,
3570 catalog_version: identity.catalog_version,
3571 segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION + 1,
3572 };
3573 match dispatch_sync_pull(&engine, wrong_format, true, Some(&principal)) {
3574 Message::Error { message } => assert!(message.contains("rebootstrap required")),
3575 other => panic!("expected format mismatch error, got {other:?}"),
3576 }
3577 }
3578}