Skip to content

Commit 1802088

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 9783a3e commit 1802088

21 files changed

Lines changed: 1083 additions & 100 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: 38 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,28 @@ 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+
/// Such a subscribe is always `internal`, because there is no session to
104+
/// attribute a `mz_subscriptions` row to. `pack_subscribe_update` depends on
105+
/// that: it needs a session uuid, and is only reached for subscribes that
106+
/// are not internal.
107+
Background,
108+
}
109+
93110
/// A description of an active subscribe from coord's perspective
94111
#[derive(Debug)]
95112
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,
113+
/// The owner responsible for retiring the subscribe.
114+
pub owner: ActiveSubscribeOwner,
100115
/// The ID of the cluster on which the subscribe is running.
101116
pub cluster_id: ClusterId,
102117
/// The IDs of the objects on which the subscribe depends.
@@ -121,6 +136,22 @@ pub struct ActiveSubscribe {
121136
}
122137

123138
impl ActiveSubscribe {
139+
/// Returns the owning connection, if this is a session subscribe.
140+
pub fn connection_id(&self) -> Option<&ConnectionId> {
141+
match &self.owner {
142+
ActiveSubscribeOwner::Session { conn_id, .. } => Some(conn_id),
143+
ActiveSubscribeOwner::Background => None,
144+
}
145+
}
146+
147+
/// Returns the owning session UUID, if this is a session subscribe.
148+
pub fn session_uuid(&self) -> Option<Uuid> {
149+
match self.owner {
150+
ActiveSubscribeOwner::Session { session_uuid, .. } => Some(session_uuid),
151+
ActiveSubscribeOwner::Background => None,
152+
}
153+
}
154+
124155
/// Initializes the subscription.
125156
///
126157
/// This method must be called exactly once, after constructing an

src/adapter/src/catalog/builtin_table_updates.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,7 +931,11 @@ impl CatalogState {
931931
let mut row = Row::default();
932932
let mut packer = row.packer();
933933
packer.push(Datum::String(&id.to_string()));
934-
packer.push(Datum::Uuid(subscribe.session_uuid));
934+
packer.push(Datum::Uuid(
935+
subscribe
936+
.session_uuid()
937+
.expect("a subscribe with an introspection row is session-owned"),
938+
));
935939
packer.push(Datum::String(&subscribe.cluster_id.to_string()));
936940

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

src/adapter/src/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ use crate::frontend_read_then_write::{
7272
use crate::metrics::Metrics;
7373
use crate::optimize::dataflows::{EvalTime, ExprPrepOneShot};
7474
use crate::optimize::{self, Optimize, OptimizerError};
75-
use crate::peek_client::{ExecutionLogging, TakeOver};
75+
use crate::peek_client::{CoordinatorClient, ExecutionLogging, TakeOver};
7676
use crate::session::{
7777
EndTransactionAction, PreparedStatement, Session, SessionConfig, StateRevision, TransactionId,
7878
TransactionStatus,
@@ -313,7 +313,7 @@ impl Client {
313313
} = response;
314314

315315
let peek_client = PeekClient::new(
316-
self.clone(),
316+
CoordinatorClient::Session(self.clone()),
317317
&catalog,
318318
storage_collections,
319319
transient_id_gen,

src/adapter/src/command.rs

Lines changed: 6 additions & 6 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,10 +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+
/// Connection originating the write, so the coordinator can cancel this
441+
/// pending write if the connection is cancelled before it commits.
442+
/// `None` for coordinator background work, which has no connection.
443+
conn_id: Option<ConnectionId>,
444444
target_id: CatalogItemId,
445445
target_global_id: GlobalId,
446446
diffs: Vec<(Row, Diff)>,

src/adapter/src/coord.rs

Lines changed: 13 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,10 @@ 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+
/// The in-flight hydration-history sweep. Aborted when we are dropped.
2075+
hydration_history_sweep: Option<AbortOnDropHandle<()>>,
20672076

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

39003909
self.schedule_storage_usage_collection().await;
39013910
self.schedule_arrangement_sizes_collection().await;
3911+
self.schedule_hydration_history_collection();
39023912
self.spawn_privatelink_vpc_endpoints_watch_task();
39033913
self.spawn_statement_logging_task();
39043914
self.spawn_catalog_info_metrics_task();
@@ -5104,6 +5114,8 @@ pub fn serve(
51045114
active_copies: BTreeMap::new(),
51055115
connection_cancel_watches: BTreeMap::new(),
51065116
introspection_subscribes: BTreeMap::new(),
5117+
hydration_history_replica_cursor: None,
5118+
hydration_history_sweep: None,
51075119
write_locks: BTreeMap::new(),
51085120
deferred_write_ops: BTreeMap::new(),
51095121
pending_writes: Vec::new(),

src/adapter/src/coord/command_handler.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -638,25 +638,14 @@ impl Coordinator {
638638
as_of,
639639
arity,
640640
sink_id,
641-
conn_id,
642-
session_uuid,
641+
owner,
643642
start_time,
644643
read_holds,
645644
tx,
646645
} => {
647646
self.handle_create_internal_subscribe(
648-
*df_desc,
649-
cluster_id,
650-
replica_id,
651-
depends_on,
652-
as_of,
653-
arity,
654-
sink_id,
655-
conn_id,
656-
session_uuid,
657-
start_time,
658-
read_holds,
659-
tx,
647+
*df_desc, cluster_id, replica_id, depends_on, as_of, arity, sink_id, owner,
648+
start_time, read_holds, tx,
660649
)
661650
.await;
662651
}

0 commit comments

Comments
 (0)