1use std::collections::BTreeMap;
88use std::rc::Rc;
89use std::time::Duration;
90
91use itertools::Itertools as _;
92use mz_expr::EvalError;
93use mz_ore::error::ErrorExt;
94use mz_postgres_util::desc::PostgresTableDesc;
95use mz_postgres_util::schema_change::SchemaChangeError;
96use mz_postgres_util::{
97 Client, PostgresError, Sql, query_opt, simple_query, simple_query_opt, sql,
98};
99use mz_repr::{Datum, GlobalId, Row};
100use mz_storage_types::errors::{DataflowError, SourceError, SourceErrorDetails};
101use mz_storage_types::sources::casts::StorageScalarExpr;
102use mz_storage_types::sources::postgres::CastType;
103use mz_storage_types::sources::{
104 MzOffset, PostgresSourceConnection, SourceExport, SourceExportDetails, SourceTimestamp,
105};
106use mz_timely_util::builder_async::PressOnDropButton;
107use serde::{Deserialize, Serialize};
108use timely::dataflow::operators::Concat;
109use timely::dataflow::operators::vec::{Map, ToStream};
110use timely::dataflow::{Scope, StreamVec};
111use timely::progress::{Antichain, Timestamp};
112use tokio_postgres::SimpleQueryRow;
113use tokio_postgres::error::SqlState;
114use tokio_postgres::types::PgLsn;
115
116use crate::healthcheck::{HealthStatusMessage, HealthStatusUpdate, StatusNamespace};
117use crate::source::types::{Probe, ResumeUppers, SourceRender, StackedCollection};
118use crate::source::{RawSourceCreationConfig, SourceMessage};
119
120mod replication;
121mod snapshot;
122
123impl SourceRender for PostgresSourceConnection {
124 type Time = MzOffset;
125
126 const STATUS_NAMESPACE: StatusNamespace = StatusNamespace::Postgres;
127
128 fn render<'scope>(
131 self,
132 scope: Scope<'scope, MzOffset>,
133 config: &RawSourceCreationConfig,
134 resume_uppers: impl futures::Stream<Item = ResumeUppers<MzOffset>> + 'static,
135 _start_signal: impl std::future::Future<Output = ()> + 'static,
136 ) -> (
137 BTreeMap<
138 GlobalId,
139 StackedCollection<'scope, MzOffset, Result<SourceMessage, DataflowError>>,
140 >,
141 StreamVec<'scope, MzOffset, HealthStatusMessage>,
142 StreamVec<'scope, MzOffset, Probe<MzOffset>>,
143 Vec<PressOnDropButton>,
144 ) {
145 let mut table_info = BTreeMap::new();
147 for (idx, (id, export)) in config.source_exports.iter().enumerate() {
148 let SourceExport {
149 details,
150 storage_metadata: _,
151 data_config: _,
152 } = export;
153 let details = match details {
154 SourceExportDetails::Postgres(details) => details,
155 SourceExportDetails::None => continue,
157 _ => panic!("unexpected source export details: {:?}", details),
158 };
159 let desc = details.table.clone();
160 let casts = details.column_casts.clone();
161 let resume_upper = Antichain::from_iter(
162 config
163 .source_resume_uppers
164 .get(id)
165 .expect("all source exports must be present in source resume uppers")
166 .iter()
167 .map(MzOffset::decode_row),
168 );
169 let output = SourceOutputInfo {
170 desc,
171 projection: None,
172 casts,
173 resume_upper,
174 export_id: id.clone(),
175 initial_lsn: details.initial_lsn.unwrap_or_else(MzOffset::minimum),
176 };
177 table_info
178 .entry(output.desc.oid)
179 .or_insert_with(BTreeMap::new)
180 .insert(idx, output);
181 }
182
183 let metrics = config.metrics.get_postgres_source_metrics(config.id);
184
185 let (snapshot_updates, rewinds, slot_ready, snapshot_err, snapshot_token) =
186 snapshot::render(
187 scope.clone(),
188 config.clone(),
189 self.clone(),
190 table_info.clone(),
191 metrics.snapshot_metrics.clone(),
192 );
193
194 let (repl_updates, probe_stream, repl_err, repl_token) = replication::render(
195 scope.clone(),
196 config.clone(),
197 self,
198 table_info,
199 rewinds,
200 slot_ready,
201 resume_uppers,
202 metrics,
203 );
204
205 let mut data_collections = BTreeMap::new();
208 for ((id, snapshot_updates), repl_updates) in config
209 .source_exports
210 .keys()
211 .zip_eq(snapshot_updates)
212 .zip_eq(repl_updates)
213 {
214 data_collections.insert(*id, snapshot_updates.concat(repl_updates));
215 }
216
217 let export_ids = config.source_exports.keys().copied();
218 let health_init = export_ids
219 .map(Some)
220 .chain(std::iter::once(None))
221 .map(|id| HealthStatusMessage {
222 id,
223 namespace: Self::STATUS_NAMESPACE,
224 update: HealthStatusUpdate::Running,
225 })
226 .collect::<Vec<_>>()
227 .to_stream(scope);
228
229 let errs = snapshot_err.concat(repl_err).map(move |err| {
233 let err_string = err.display_with_causes().to_string();
235 let hint = match &err {
236 ReplicationError::Definite(err) => err.hint(),
237 ReplicationError::Transient(_) => None,
238 };
239 let update = HealthStatusUpdate::halting(err_string.clone(), hint);
240
241 let namespace = match err {
242 ReplicationError::Transient(err)
243 if matches!(
244 &*err,
245 TransientError::PostgresError(PostgresError::Ssh(_))
246 | TransientError::PostgresError(PostgresError::SshIo(_))
247 ) =>
248 {
249 StatusNamespace::Ssh
250 }
251 _ => Self::STATUS_NAMESPACE,
252 };
253
254 HealthStatusMessage {
255 id: None,
256 namespace: namespace.clone(),
257 update,
258 }
259 });
260
261 let health = health_init.concat(errs);
262
263 (
264 data_collections,
265 health,
266 probe_stream,
267 vec![snapshot_token, repl_token],
268 )
269 }
270}
271
272#[derive(Clone, Debug)]
273struct SourceOutputInfo {
274 desc: PostgresTableDesc,
276 projection: Option<Vec<usize>>,
280 casts: Vec<(CastType, StorageScalarExpr)>,
281 resume_upper: Antichain<MzOffset>,
282 export_id: GlobalId,
283 initial_lsn: MzOffset,
287}
288
289impl SourceOutputInfo {
290 fn ignores(&self, commit_lsn: MzOffset) -> bool {
301 commit_lsn < self.initial_lsn
302 }
303}
304
305#[derive(Clone, Debug, thiserror::Error)]
306pub enum ReplicationError {
307 #[error(transparent)]
308 Transient(#[from] Rc<TransientError>),
309 #[error(transparent)]
310 Definite(#[from] Rc<DefiniteError>),
311}
312
313#[derive(Debug, thiserror::Error)]
315pub enum TransientError {
316 #[error("replication slot mysteriously missing")]
317 MissingReplicationSlot,
318 #[error(
319 "slot overcompacted. Requested LSN {requested_lsn} but only LSNs >= {available_lsn} are available"
320 )]
321 OvercompactedReplicationSlot {
322 requested_lsn: MzOffset,
323 available_lsn: MzOffset,
324 },
325 #[error("replication slot already exists")]
326 ReplicationSlotAlreadyExists,
327 #[error(
328 "creating replication slot {slot} timed out after {timeout:?}. Slot creation waits for \
329 in-progress upstream transactions to end"
330 )]
331 ReplicationSlotCreationTimeout { slot: String, timeout: Duration },
332 #[error("stream ended prematurely")]
333 ReplicationEOF,
334 #[error("unexpected replication message")]
335 UnknownReplicationMessage,
336 #[error("unexpected logical replication message")]
337 UnknownLogicalReplicationMessage,
338 #[error("received replication event outside of transaction")]
339 BareTransactionEvent,
340 #[error("lsn mismatch between BEGIN and COMMIT")]
341 InvalidTransaction,
342 #[error("BEGIN within existing BEGIN stream")]
343 NestedTransaction,
344 #[error("recoverable errors should crash the process during snapshots")]
345 SyntheticError,
346 #[error("sql client error")]
347 SQLClient(#[from] tokio_postgres::Error),
348 #[error(transparent)]
349 PostgresError(#[from] PostgresError),
350 #[error(transparent)]
351 Generic(#[from] anyhow::Error),
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
356pub enum DefiniteError {
357 #[error("slot compacted past snapshot point. snapshot consistent point={0} resume_lsn={1}")]
358 SlotCompactedPastResumePoint(MzOffset, MzOffset),
359 #[error("table was truncated")]
360 TableTruncated,
361 #[error("table was dropped")]
362 TableDropped,
363 #[error("publication {0:?} does not exist")]
364 PublicationDropped(String),
365 #[error("replication slot has been invalidated because it exceeded the maximum reserved size")]
366 InvalidReplicationSlot,
367 #[error("unexpected number of columns while parsing COPY output")]
368 MissingColumn,
369 #[error("failed to parse COPY protocol")]
370 InvalidCopyInput,
371 #[error(
372 "unsupported action: database restored from point-in-time backup. Expected timeline ID {expected} but got {actual}"
373 )]
374 InvalidTimelineId { expected: u64, actual: u64 },
375 #[error(
376 "unsupported action: upstream went back in time. Expected a snapshot at or after LSN {initial_lsn} but the snapshot was taken at {snapshot_lsn}"
377 )]
378 InvalidSnapshotLsn {
379 initial_lsn: MzOffset,
380 snapshot_lsn: MzOffset,
381 },
382 #[error(
383 "unsupported action: upstream physical replica status changed (e.g. a physical replica was promoted to a primary). Expected pg_is_in_recovery()={expected} but got {actual}"
384 )]
385 InvalidPhysicalReplica { expected: bool, actual: bool },
386 #[error(
387 "TOASTed value missing from old row. Did you forget to set REPLICA IDENTITY to FULL for your table?"
388 )]
389 MissingToast,
390 #[error(
391 "old row missing from replication stream. Did you forget to set REPLICA IDENTITY to FULL for your table?"
392 )]
393 DefaultReplicaIdentity,
394 #[error(transparent)]
395 IncompatibleSchema(SchemaChangeError),
396 #[error("invalid UTF8 string: {0:?}")]
397 InvalidUTF8(Vec<u8>),
398 #[error("failed to cast raw column: {0}")]
399 CastError(#[source] EvalError),
400 #[error("unexpected binary data in replication stream")]
401 UnexpectedBinaryData,
402}
403
404impl DefiniteError {
405 fn hint(&self) -> Option<String> {
406 match self {
407 DefiniteError::IncompatibleSchema(err) => err.hint(),
408 _ => None,
409 }
410 }
411}
412
413impl From<DefiniteError> for DataflowError {
414 fn from(err: DefiniteError) -> Self {
415 let m = err.to_string().into();
416 DataflowError::SourceError(Box::new(SourceError {
417 hint: err.hint().map(Into::into),
418 error: match &err {
419 DefiniteError::SlotCompactedPastResumePoint(_, _) => SourceErrorDetails::Other(m),
420 DefiniteError::TableTruncated => SourceErrorDetails::Other(m),
421 DefiniteError::TableDropped => SourceErrorDetails::Other(m),
422 DefiniteError::PublicationDropped(_) => SourceErrorDetails::Initialization(m),
423 DefiniteError::InvalidReplicationSlot => SourceErrorDetails::Initialization(m),
424 DefiniteError::MissingColumn => SourceErrorDetails::Other(m),
425 DefiniteError::InvalidCopyInput => SourceErrorDetails::Other(m),
426 DefiniteError::InvalidTimelineId { .. } => SourceErrorDetails::Initialization(m),
427 DefiniteError::InvalidSnapshotLsn { .. } => SourceErrorDetails::Initialization(m),
428 DefiniteError::InvalidPhysicalReplica { .. } => {
429 SourceErrorDetails::Initialization(m)
430 }
431 DefiniteError::MissingToast => SourceErrorDetails::Other(m),
432 DefiniteError::DefaultReplicaIdentity => SourceErrorDetails::Other(m),
433 DefiniteError::IncompatibleSchema(_) => SourceErrorDetails::Other(m),
434 DefiniteError::InvalidUTF8(_) => SourceErrorDetails::Other(m),
435 DefiniteError::CastError(_) => SourceErrorDetails::Other(m),
436 DefiniteError::UnexpectedBinaryData => SourceErrorDetails::Other(m),
437 },
438 }))
439 }
440}
441
442async fn ensure_replication_slot(
443 client: &Client,
444 slot: &str,
445 timeout: Duration,
446) -> Result<(), TransientError> {
447 let query = sql!(
448 "CREATE_REPLICATION_SLOT {} LOGICAL \"pgoutput\" NOEXPORT_SNAPSHOT",
449 Sql::ident(slot)
450 );
451 match create_replication_slot(client, slot, query, timeout).await {
452 Ok(_) => Ok(()),
453 Err(TransientError::ReplicationSlotAlreadyExists) => {
454 tracing::trace!(%slot, "replication slot already existed");
455 Ok(())
456 }
457 Err(err) => Err(err),
458 }
459}
460
461async fn create_replication_slot(
467 client: &Client,
468 slot: &str,
469 query: Sql,
470 timeout: Duration,
471) -> Result<Option<SimpleQueryRow>, TransientError> {
472 set_timeout(client, "lock_timeout", timeout).await?;
481 let result = simple_query_opt(client, query).await;
482 let reset = simple_query(client, sql!("RESET lock_timeout")).await;
485 let row = match result {
486 Ok(row) => row,
487 Err(PostgresError::Postgres(err)) if err.code() == Some(&SqlState::DUPLICATE_OBJECT) => {
488 return Err(TransientError::ReplicationSlotAlreadyExists);
489 }
490 Err(PostgresError::Postgres(err)) if err.code() == Some(&SqlState::LOCK_NOT_AVAILABLE) => {
491 return Err(TransientError::ReplicationSlotCreationTimeout {
492 slot: slot.to_owned(),
493 timeout,
494 });
495 }
496 Err(err) => return Err(err.into()),
497 };
498 reset?;
499 Ok(row)
500}
501
502async fn set_timeout(
504 client: &Client,
505 parameter: &'static str,
506 timeout: Duration,
507) -> Result<(), TransientError> {
508 let millis = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
512 let query = sql!("SET {} = {}", Sql::new(parameter), Sql::from(millis));
513 simple_query(client, query).await?;
514 Ok(())
515}
516
517struct SlotMetadata {
519 active_pid: Option<i32>,
522 confirmed_flush_lsn: MzOffset,
525 invalidated: bool,
528}
529
530async fn fetch_slot_metadata(
532 client: &Client,
533 slot: &str,
534 interval: Duration,
535) -> Result<SlotMetadata, TransientError> {
536 loop {
537 let Some(row) = query_opt(
541 &**client,
542 sql!(
543 "SELECT active_pid, confirmed_flush_lsn, \
544 to_jsonb(s) ->> 'wal_status' = 'lost' AS invalidated \
545 FROM pg_replication_slots s WHERE slot_name = $1"
546 ),
547 &[&slot],
548 )
549 .await?
550 else {
551 return Err(TransientError::MissingReplicationSlot);
552 };
553
554 match row.get::<_, Option<PgLsn>>("confirmed_flush_lsn") {
555 Some(lsn) => {
559 return Ok(SlotMetadata {
560 confirmed_flush_lsn: MzOffset::from(lsn),
561 active_pid: row.get("active_pid"),
562 invalidated: row.get::<_, Option<bool>>("invalidated").unwrap_or(false),
563 });
564 }
565 None => tokio::time::sleep(interval).await,
569 };
570 }
571}
572
573fn verify_schema(
576 oid: u32,
577 info: &SourceOutputInfo,
578 upstream_info: &BTreeMap<u32, PostgresTableDesc>,
579) -> Result<(), DefiniteError> {
580 let current_desc = upstream_info.get(&oid).ok_or(DefiniteError::TableDropped)?;
581
582 let allow_oids_to_change_by_col_num = info
583 .desc
584 .columns
585 .iter()
586 .zip_eq(info.casts.iter())
587 .flat_map(|(col, (cast_type, _))| match cast_type {
588 CastType::Text => Some(col.col_num),
589 CastType::Natural => None,
590 })
591 .collect();
592
593 match info
594 .desc
595 .determine_compatibility(current_desc, &allow_oids_to_change_by_col_num)
596 {
597 Ok(()) => Ok(()),
598 Err(err) => Err(DefiniteError::IncompatibleSchema(err)),
599 }
600}
601
602fn cast_row(
604 casts: &[(CastType, StorageScalarExpr)],
605 datums: &[Datum<'_>],
606 row: &mut Row,
607) -> Result<(), DefiniteError> {
608 let arena = mz_repr::RowArena::new();
609 let mut packer = row.packer();
610 for (_, column_cast) in casts {
611 let datum = column_cast
612 .eval(datums, &arena)
613 .map_err(DefiniteError::CastError)?;
614 packer.push(datum);
615 }
616 Ok(())
617}
618
619fn decode_utf8_text(bytes: &[u8]) -> Result<Datum<'_>, DefiniteError> {
621 match std::str::from_utf8(bytes) {
622 Ok(text) => Ok(Datum::String(text)),
623 Err(_) => Err(DefiniteError::InvalidUTF8(bytes.to_vec())),
624 }
625}