Skip to content

Commit 5e42580

Browse files
committed
adapter: collect durable object hydration history
Adds the collector that fills `mz_internal.mz_object_hydration_history`, and the background read-then-write plumbing it needs. A sweep visits one managed user replica per interval and installs an internal subscribe on that replica which aggregates complete per-worker episodes, maps runtime export ids to catalog objects, anti-joins against the history table, and writes the missing rows through the timestamped OCC path. Retention retracts a bounded batch per sweep on the catalog server, independently of whether that sweep's collection succeeded. Reading the target table inside the subscribe is what makes the write idempotent across concurrent environmentd processes. Two collectors that compute the same row race for one write timestamp, and the loser observes the winner's append through its own subscribe and finds nothing left to write. The read-then-write path grows an `RtwCaller`, because replica pinning, dependency validation, and write cancellation all differ for a background caller and have to move together. Background subscribes are owned by the coordinator rather than a session, so they write no `mz_subscriptions` row, and background writes take no OCC permit: the sweep has no statement timeout, and its subscribe must first hydrate a dataflow on a user replica, so holding a permit would let it stall user DML for as long as that takes. Collection is off by default and enabled in the mzcompose configuration. `test/testdrive/hydration-status.td` covers lifecycle ordering, several replicas, deduplication across sweeps, replica removal, retention, and the deliberate absence of an object that never reports a completion time. `test/restart/mzcompose.py` covers survival of an environmentd restart without duplication, on a two-worker replica. Closes: SQL-644
1 parent 025d91f commit 5e42580

23 files changed

Lines changed: 1261 additions & 169 deletions

File tree

doc/user/data/metrics.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ metrics:
9797
source: src/adapter/src/metrics.rs
9898
visibility: internal
9999
- name: mz_active_internal_subscribes
100-
help: The number of active internal subscribes, which serve frontend-sequenced read-then-write.
100+
help: The number of active internal subscribes used by read-then-write operations and background maintenance.
101101
labels:
102102
- session_type
103103
source: src/adapter/src/metrics.rs

misc/python/materialize/mzcompose/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ def get_minimal_system_parameters(
137137
if version < MzVersion.parse_mz("v26.25.0-dev"):
138138
config["enable_multi_replica_sources"] = "true"
139139

140+
if version >= MzVersion.parse_mz("v26.39.0-dev"):
141+
config["hydration_history_collection_interval"] = "60s"
142+
140143
if sanitizer_enabled():
141144
config["with_0dt_deployment_max_wait"] = "18000s"
142145

@@ -461,6 +464,9 @@ def get_variable_system_parameters(
461464
VariableSystemParameter(
462465
"arrangement_size_history_retention_period", "7d", ["1min", "1h", "7d"]
463466
),
467+
VariableSystemParameter(
468+
"hydration_history_retention_period", "30d", ["1min", "1h", "30d"]
469+
),
464470
VariableSystemParameter(
465471
"persist_validate_part_bounds_on_read", "false", ["true", "false"]
466472
),

misc/python/materialize/parallel_workload/action.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2949,6 +2949,16 @@ def __init__(
29492949
"'1h'",
29502950
"'7d'",
29512951
]
2952+
self.flags_with_values["hydration_history_collection_interval"] = [
2953+
"'0s'",
2954+
"'1s'",
2955+
"'1min'",
2956+
]
2957+
self.flags_with_values["hydration_history_retention_period"] = [
2958+
"'1min'",
2959+
"'1h'",
2960+
"'30d'",
2961+
]
29522962
# Keep these generous: a tight timeout would abort the oracle's own
29532963
# queries (they are retried, but it adds noise). "0s" leaves it unset.
29542964
self.flags_with_values["pg_timestamp_oracle_statement_timeout"] = [

src/adapter-types/src/dyncfgs.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,20 @@ pub const ARRANGEMENT_SIZE_HISTORY_RETENTION_PERIOD: Config<Duration> = Config::
344344
"How long to retain rows in mz_internal.mz_object_arrangement_size_history.",
345345
);
346346

347+
/// How often to sweep replicas for completed object hydration episodes.
348+
pub const HYDRATION_HISTORY_COLLECTION_INTERVAL: Config<Duration> = Config::new(
349+
"hydration_history_collection_interval",
350+
Duration::ZERO,
351+
"How often to record completed object hydration episodes. A zero duration disables collection.",
352+
);
353+
354+
/// How long to retain completed object hydration episodes.
355+
pub const HYDRATION_HISTORY_RETENTION_PERIOD: Config<Duration> = Config::new(
356+
"hydration_history_retention_period",
357+
Duration::from_hours(30 * 24),
358+
"How long to retain rows in mz_internal.mz_object_hydration_history.",
359+
);
360+
347361
/// How frequently the catalog `*_info` metrics (`mz_object_info`,
348362
/// `mz_cluster_info`, …) are reconciled with the catalog. A zero duration
349363
/// disables reconciliation.
@@ -470,6 +484,8 @@ pub fn all_dyncfgs(configs: ConfigSet) -> ConfigSet {
470484
.add(&CONSOLE_OIDC_SCOPES)
471485
.add(&ARRANGEMENT_SIZE_HISTORY_COLLECTION_INTERVAL)
472486
.add(&ARRANGEMENT_SIZE_HISTORY_RETENTION_PERIOD)
487+
.add(&HYDRATION_HISTORY_COLLECTION_INTERVAL)
488+
.add(&HYDRATION_HISTORY_RETENTION_PERIOD)
473489
.add(&CATALOG_INFO_METRICS_RECONCILE_INTERVAL)
474490
.add(&PG_TIMESTAMP_ORACLE_STATEMENT_TIMEOUT)
475491
.add(&FRONTEND_READ_THEN_WRITE)

src/adapter/src/active_compute_sink.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ impl ActiveComputeSink {
5050
}
5151

5252
/// Reports the ID of the connection which created the sink.
53-
pub fn connection_id(&self) -> &ConnectionId {
53+
pub fn connection_id(&self) -> Option<&ConnectionId> {
5454
match &self {
55-
ActiveComputeSink::Subscribe(subscribe) => &subscribe.conn_id,
56-
ActiveComputeSink::CopyTo(copy_to) => &copy_to.conn_id,
55+
ActiveComputeSink::Subscribe(subscribe) => subscribe.connection_id(),
56+
ActiveComputeSink::CopyTo(copy_to) => Some(&copy_to.conn_id),
5757
}
5858
}
5959

@@ -90,13 +90,26 @@ pub enum ActiveComputeSinkRetireReason {
9090
DependencyDropped(DroppedDependency),
9191
}
9292

93+
/// Ownership and cleanup scope of an active subscribe.
94+
#[derive(Debug)]
95+
pub enum ActiveSubscribeOwner {
96+
/// The subscribe belongs to a SQL session.
97+
Session {
98+
conn_id: ConnectionId,
99+
session_uuid: Uuid,
100+
},
101+
/// The subscribe belongs to a coordinator background task.
102+
///
103+
/// Always `internal`, since there is no session to attribute a
104+
/// `mz_subscriptions` row to.
105+
Background,
106+
}
107+
93108
/// A description of an active subscribe from coord's perspective
94109
#[derive(Debug)]
95110
pub struct ActiveSubscribe {
96-
/// The ID of the connection which created the subscribe.
97-
pub conn_id: ConnectionId,
98-
/// The UUID of the session which created the subscribe.
99-
pub session_uuid: Uuid,
111+
/// The owner responsible for retiring the subscribe.
112+
pub owner: ActiveSubscribeOwner,
100113
/// The ID of the cluster on which the subscribe is running.
101114
pub cluster_id: ClusterId,
102115
/// The IDs of the objects on which the subscribe depends.
@@ -121,6 +134,33 @@ pub struct ActiveSubscribe {
121134
}
122135

123136
impl ActiveSubscribe {
137+
/// The session uuid for this subscribe's `mz_subscriptions` row, or `None`
138+
/// if it does not appear there.
139+
pub fn introspection_session_uuid(&self) -> Option<Uuid> {
140+
match &self.owner {
141+
ActiveSubscribeOwner::Session { session_uuid, .. } if !self.internal => {
142+
Some(*session_uuid)
143+
}
144+
_ => None,
145+
}
146+
}
147+
148+
/// Returns the owning connection, if this is a session subscribe.
149+
pub fn connection_id(&self) -> Option<&ConnectionId> {
150+
match &self.owner {
151+
ActiveSubscribeOwner::Session { conn_id, .. } => Some(conn_id),
152+
ActiveSubscribeOwner::Background => None,
153+
}
154+
}
155+
156+
/// Returns the owning session UUID, if this is a session subscribe.
157+
pub fn session_uuid(&self) -> Option<Uuid> {
158+
match self.owner {
159+
ActiveSubscribeOwner::Session { session_uuid, .. } => Some(session_uuid),
160+
ActiveSubscribeOwner::Background => None,
161+
}
162+
}
163+
124164
/// Initializes the subscription.
125165
///
126166
/// This method must be called exactly once, after constructing an

src/adapter/src/catalog/builtin_table_updates.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ use mz_sql::names::SchemaSpecifier;
5151
use mz_sql_parser::ast::display::AstDisplay;
5252
use mz_storage_client::client::TableData;
5353
use smallvec::smallvec;
54+
use uuid::Uuid;
5455

5556
// DO NOT add any more imports from `crate` outside of `crate::catalog`.
5657
use crate::active_compute_sink::ActiveSubscribe;
@@ -926,12 +927,13 @@ impl CatalogState {
926927
&self,
927928
id: GlobalId,
928929
subscribe: &ActiveSubscribe,
930+
session_uuid: Uuid,
929931
diff: Diff,
930932
) -> BuiltinTableUpdate<&'static BuiltinTable> {
931933
let mut row = Row::default();
932934
let mut packer = row.packer();
933935
packer.push(Datum::String(&id.to_string()));
934-
packer.push(Datum::Uuid(subscribe.session_uuid));
936+
packer.push(Datum::Uuid(session_uuid));
935937
packer.push(Datum::String(&subscribe.cluster_id.to_string()));
936938

937939
let start_dt = mz_ore::now::to_datetime(subscribe.start_time);

src/adapter/src/catalog/state.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3127,7 +3127,9 @@ mod tests {
31273127
#[mz_ore::test(tokio::test)]
31283128
#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
31293129
async fn validate_read_then_write_deep_chain_no_stack_overflow() {
3130-
use crate::coord::read_then_write::validate_read_then_write_dependencies;
3130+
use crate::coord::read_then_write::{
3131+
DependencyPolicy, validate_read_then_write_dependencies,
3132+
};
31313133

31323134
Catalog::with_debug(|mut catalog| async move {
31333135
// Deep enough that the previous recursive implementation overflowed
@@ -3142,6 +3144,7 @@ mod tests {
31423144
&catalog,
31433145
[CatalogItemId::User(BASE)],
31443146
usize::MAX,
3147+
DependencyPolicy::UserDml,
31453148
)
31463149
.expect("deep chain of user views is valid for read-then-write");
31473150

@@ -3156,7 +3159,9 @@ mod tests {
31563159
#[mz_ore::test(tokio::test)]
31573160
#[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `TLS_client_method`
31583161
async fn validate_read_then_write_dependency_limit() {
3159-
use crate::coord::read_then_write::validate_read_then_write_dependencies;
3162+
use crate::coord::read_then_write::{
3163+
DependencyPolicy, validate_read_then_write_dependencies,
3164+
};
31603165
use crate::error::AdapterError;
31613166

31623167
Catalog::with_debug(|mut catalog| async move {
@@ -3168,14 +3173,20 @@ mod tests {
31683173
const OBJECTS: usize = DEPTH + 1;
31693174

31703175
// Exactly at the limit is allowed.
3171-
validate_read_then_write_dependencies(&catalog, [CatalogItemId::User(BASE)], OBJECTS)
3172-
.expect("chain at the limit is valid");
3176+
validate_read_then_write_dependencies(
3177+
&catalog,
3178+
[CatalogItemId::User(BASE)],
3179+
OBJECTS,
3180+
DependencyPolicy::UserDml,
3181+
)
3182+
.expect("chain at the limit is valid");
31733183

31743184
// One below the limit is rejected with a clean error.
31753185
let err = validate_read_then_write_dependencies(
31763186
&catalog,
31773187
[CatalogItemId::User(BASE)],
31783188
OBJECTS - 1,
3189+
DependencyPolicy::UserDml,
31793190
)
31803191
.expect_err("chain over the limit is rejected");
31813192
assert!(matches!(

src/adapter/src/client.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ use crate::command::{
6363
SASLChallengeResponse, SASLVerifyProofResponse, SuperuserAttribute,
6464
};
6565
use crate::config::{ScopedParameters, ScopedParametersScope, SystemParameterFrontend};
66+
use crate::coord::read_then_write::DependencyPolicy;
6667
use crate::coord::{Coordinator, ExecuteContextGuard};
6768
use crate::error::AdapterError;
6869
use crate::frontend_read_then_write::{
@@ -72,7 +73,7 @@ use crate::frontend_read_then_write::{
7273
use crate::metrics::Metrics;
7374
use crate::optimize::dataflows::{EvalTime, ExprPrepOneShot};
7475
use crate::optimize::{self, Optimize, OptimizerError};
75-
use crate::peek_client::{ExecutionLogging, TakeOver};
76+
use crate::peek_client::{CoordinatorClient, ExecutionLogging, TakeOver};
7677
use crate::session::{
7778
EndTransactionAction, PreparedStatement, Session, SessionConfig, StateRevision, TransactionId,
7879
TransactionStatus,
@@ -313,7 +314,7 @@ impl Client {
313314
} = response;
314315

315316
let peek_client = PeekClient::new(
316-
self.clone(),
317+
CoordinatorClient::Session(self.clone()),
317318
&catalog,
318319
storage_collections,
319320
transient_id_gen,
@@ -1973,7 +1974,7 @@ impl SessionClient {
19731974
"calls to mz_now in write statements",
19741975
));
19751976
}
1976-
validate_selection_dependencies(&catalog, &depends_on)?;
1977+
validate_selection_dependencies(&catalog, &depends_on, DependencyPolicy::UserDml)?;
19771978
return Err(prohibited_in_transaction(&stmt));
19781979
}
19791980
}

src/adapter/src/command.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use mz_timestamp_oracle::TimestampOracle;
4545
use tokio::sync::{Semaphore, mpsc, oneshot, watch};
4646
use uuid::Uuid;
4747

48+
use crate::active_compute_sink::ActiveSubscribeOwner;
4849
use crate::catalog::Catalog;
4950
use crate::config::{ScopedParameters, ScopedParametersScope, SystemParameterFrontend};
5051
use crate::coord::appends::{BuiltinTableAppendNotify, WriteResult};
@@ -419,8 +420,7 @@ pub enum Command {
419420
as_of: mz_repr::Timestamp,
420421
arity: usize,
421422
sink_id: GlobalId,
422-
conn_id: ConnectionId,
423-
session_uuid: Uuid,
423+
owner: ActiveSubscribeOwner,
424424
start_time: mz_ore::now::EpochMillis,
425425
read_holds: ReadHolds,
426426
tx: oneshot::Sender<Result<mpsc::UnboundedReceiver<PeekResponseUnary>, AdapterError>>,
@@ -437,14 +437,10 @@ pub enum Command {
437437
/// including read-only, a changed target and cancellation, is reported the
438438
/// same way in both modes.
439439
AttemptWrite {
440-
/// Connection originating the write. Used so the coordinator can
441-
/// cancel this pending write if the connection is cancelled before
442-
/// the write commits.
443-
conn_id: ConnectionId,
440+
attempt: WriteAttempt,
444441
target_id: CatalogItemId,
445442
target_global_id: GlobalId,
446443
diffs: Vec<(Row, Diff)>,
447-
write_ts: Option<mz_repr::Timestamp>,
448444
tx: oneshot::Sender<WriteResult>,
449445
},
450446

@@ -455,6 +451,24 @@ pub enum Command {
455451
},
456452
}
457453

454+
/// Who a read-then-write commits on behalf of, and how its timestamp is chosen.
455+
///
456+
/// Group commit picking the timestamp requires a connection to answer through,
457+
/// so that combination is only reachable from a session.
458+
#[derive(Debug)]
459+
pub enum WriteAttempt {
460+
/// A session's write, cancelled with `conn_id` if the connection goes away
461+
/// before it commits. A `write_ts` of `None` lets group commit pick the
462+
/// timestamp, which then cannot be reported as passed.
463+
Session {
464+
conn_id: ConnectionId,
465+
write_ts: Option<mz_repr::Timestamp>,
466+
},
467+
/// Coordinator background work. There is no connection to cancel with, so
468+
/// the caller names the timestamp and handles `TimestampPassed` itself.
469+
Background { write_ts: mz_repr::Timestamp },
470+
}
471+
458472
impl Command {
459473
pub fn session(&self) -> Option<&Session> {
460474
match self {

src/adapter/src/coord.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ use mz_ore::channel::trigger::Trigger;
130130
use mz_ore::future::TimeoutError;
131131
use mz_ore::metrics::MetricsRegistry;
132132
use mz_ore::now::{EpochMillis, NowFn};
133-
use mz_ore::task::{JoinHandle, spawn};
133+
use mz_ore::task::{AbortOnDropHandle, JoinHandle, spawn};
134134
use mz_ore::thread::JoinHandleExt;
135135
use mz_ore::tracing::{OpenTelemetryContext, TracingHandle};
136136
use mz_ore::url::SensitiveUrl;
@@ -236,6 +236,7 @@ mod caught_up;
236236
mod command_handler;
237237
mod ddl;
238238
pub(crate) mod group_sync;
239+
mod hydration_history;
239240
mod indexes;
240241
mod info_metrics;
241242
mod introspection;
@@ -386,6 +387,8 @@ pub enum Message {
386387
ArrangementSizesSnapshot,
387388
ArrangementSizesWrite(Vec<ArrangementSizeRecord>),
388389
ArrangementSizesPrune(Vec<BuiltinTableUpdate>),
390+
HydrationHistorySchedule,
391+
HydrationHistoryRun,
389392
/// Performs any cleanup and logging actions necessary for
390393
/// finalizing a statement execution.
391394
RetireExecute {
@@ -536,6 +539,8 @@ impl Message {
536539
Message::ArrangementSizesSnapshot => "arrangement_sizes_snapshot",
537540
Message::ArrangementSizesWrite(_) => "arrangement_sizes_write",
538541
Message::ArrangementSizesPrune(_) => "arrangement_sizes_prune",
542+
Message::HydrationHistorySchedule => "hydration_history_schedule",
543+
Message::HydrationHistoryRun => "hydration_history_run",
539544
Message::RetireExecute { .. } => "retire_execute",
540545
Message::ExecuteSingleStatementTransaction { .. } => {
541546
"execute_single_statement_transaction"
@@ -2064,6 +2069,11 @@ pub struct Coordinator {
20642069
connection_cancel_watches: BTreeMap<ConnectionId, (watch::Sender<bool>, watch::Receiver<bool>)>,
20652070
/// Active introspection subscribes.
20662071
introspection_subscribes: BTreeMap<GlobalId, IntrospectionSubscribe>,
2072+
/// The last replica visited by the sequential hydration-history sweep.
2073+
hydration_history_replica_cursor: Option<ReplicaId>,
2074+
/// Hydration-history sweep, while one is in flight. Aborted when we are
2075+
/// dropped.
2076+
hydration_history_sweep: Option<AbortOnDropHandle<()>>,
20672077

20682078
/// Locks that grant access to a specific object, populated lazily as objects are written to.
20692079
write_locks: BTreeMap<CatalogItemId, Arc<tokio::sync::Mutex<()>>>,
@@ -3899,6 +3909,7 @@ impl Coordinator {
38993909

39003910
self.schedule_storage_usage_collection().await;
39013911
self.schedule_arrangement_sizes_collection().await;
3912+
self.schedule_hydration_history_collection();
39023913
self.spawn_privatelink_vpc_endpoints_watch_task();
39033914
self.spawn_statement_logging_task();
39043915
self.spawn_catalog_info_metrics_task();
@@ -5104,6 +5115,8 @@ pub fn serve(
51045115
active_copies: BTreeMap::new(),
51055116
connection_cancel_watches: BTreeMap::new(),
51065117
introspection_subscribes: BTreeMap::new(),
5118+
hydration_history_replica_cursor: None,
5119+
hydration_history_sweep: None,
51075120
write_locks: BTreeMap::new(),
51085121
deferred_write_ops: BTreeMap::new(),
51095122
pending_writes: Vec::new(),

0 commit comments

Comments
 (0)