Skip to content

Commit f82b38b

Browse files
committed
catalog: add mz_object_hydration_history
Adds the durable table that hydration episodes are recorded into, along with the index that serves the question users ask of it, how long a given object took to hydrate. Nothing writes the table yet. The table is in `mz_internal` because its contents are best effort and its `status` column will gain values as more hydration events become observable. An episode is keyed by `(object_id, replica_id, installed_at)`, using the replica-stamped installation time because it is stable across an environmentd restart. Contents are exempt from the bootstrap reset and from forced schema migrations, since a sampled history cannot be rebuilt from anything else once it is gone. Durability is best effort in both directions, and the assert added here is a tripwire so that clearing the table is chosen rather than stumbled into. Adding a builtin index changes the fingerprint of the `mz_indexes` materialized view, which inlines the builtin index set as VALUES for exactly that reason, so this also declares the required replacement migration step. Ref: SQL-644
1 parent 703a394 commit f82b38b

17 files changed

Lines changed: 215 additions & 31 deletions

File tree

‎doc/user/content/reference/system-catalog/mz_internal.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,28 @@ The `mz_object_history` view enriches the [`mz_catalog.mz_objects`](/reference/s
688688
| `created_at` | [`timestamp with time zone`] | Wall-clock timestamp of when the object was created. `NULL` for built in system objects. |
689689
| `dropped_at` | [`timestamp with time zone`] | Wall-clock timestamp of when the object was dropped. `NULL` for built in system objects or if the object hasn't been dropped. |
690690

691+
## `mz_object_hydration_history`
692+
693+
The `mz_object_hydration_history` table records completed hydration of indexes and
694+
materialized views, one row per object, replica, and installation. Rows are retained
695+
for 30 days, and `object_id`, `cluster_id`, and `replica_id` may name objects that no
696+
longer exist.
697+
698+
Recording is best effort. Only successful hydration is recorded, an episode can be
699+
missed if the object or its replica goes away before the episode is recorded, and a
700+
schema change to this table in a future release may clear its contents.
701+
702+
<!-- RELATION_SPEC mz_internal.mz_object_hydration_history -->
703+
| Field | Type | Meaning |
704+
| -------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
705+
| `object_id` | [`text`] | The ID of the index or materialized view. May name an object that no longer exists. |
706+
| `cluster_id` | [`text`] | The ID of the object's cluster. |
707+
| `replica_id` | [`text`] | The ID of the cluster replica. May name a replica that no longer exists. |
708+
| `installed_at` | [`timestamp with time zone`] | When the object's dataflow was installed on the replica. |
709+
| `started_at` | [`timestamp with time zone`] | When hydration work began, or `NULL` if the replica did not observe a start for this episode. |
710+
| `finished_at` | [`timestamp with time zone`] | When hydration finished. |
711+
| `status` | [`text`] | The terminal status. Currently always `hydrated`. |
712+
691713
## `mz_object_transitive_dependencies`
692714

693715
The `mz_object_transitive_dependencies` view describes the transitive dependency structure between

‎src/adapter/src/catalog/open/builtin_schema_migration.rs‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ use futures::future::BoxFuture;
3737
use mz_build_info::{BuildInfo, DUMMY_BUILD_INFO};
3838
use mz_catalog::builtin::{
3939
BUILTIN_LOOKUP, Builtin, Fingerprint, MZ_CATALOG_RAW, MZ_CATALOG_RAW_DESCRIPTION,
40-
MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, MZ_STORAGE_USAGE_BY_SHARD,
40+
MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION, MZ_OBJECT_HYDRATION_HISTORY,
41+
MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, MZ_STORAGE_USAGE_BY_SHARD,
4142
MZ_STORAGE_USAGE_BY_SHARD_DESCRIPTION, RUNTIME_ALTERABLE_FINGERPRINT_SENTINEL,
4243
};
4344
use mz_catalog::config::BuiltinItemMigrationConfig;
@@ -390,6 +391,17 @@ static MIGRATIONS: LazyLock<Vec<MigrationStep>> = LazyLock::new(|| {
390391
MZ_CATALOG_SCHEMA,
391392
"mz_audit_events",
392393
),
394+
// Required because we added the `mz_object_hydration_history_ind`
395+
// builtin index. make_mz_indexes inlines the builtin-index set as
396+
// VALUES, so any add or remove changes its SQL fingerprint and requires
397+
// an explicit replacement step. See the NOTE above: this version must
398+
// stay at the workspace's current dev version until the change ships.
399+
MigrationStep::replacement(
400+
"26.39.0-dev.0",
401+
CatalogItemType::MaterializedView,
402+
MZ_CATALOG_SCHEMA,
403+
"mz_indexes",
404+
),
393405
]
394406
});
395407

@@ -725,6 +737,17 @@ impl Migration {
725737
"mz_object_arrangement_size_history cannot be migrated or else the table will be truncated"
726738
);
727739

740+
// Unlike the two tables above, there is no correctness hazard here, a
741+
// truncation would only lose history. This is a tripwire so that the
742+
// loss is chosen rather than stumbled into: if a schema change to
743+
// this table is worth clearing it for, remove this assert along with
744+
// the exemption in `plan_forced_migration`, and say in the release
745+
// notes that the history restarts.
746+
assert_ne!(
747+
&*MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION, object,
748+
"migrating mz_object_hydration_history clears it, see the comment above"
749+
);
750+
728751
// `mz_catalog_raw` cannot be migrated because it contains the durable catalog and it
729752
// wouldn't be very durable if we allowed it to be truncated.
730753
assert_ne!(
@@ -790,9 +813,17 @@ impl Migration {
790813
.filter(|(_, info)| {
791814
use Builtin::*;
792815
match info.builtin {
793-
// Filter out the 'mz_storage_usage_by_shard' table since we need to retain
794-
// that info for billing purposes.
795-
Table(table) => **table != *MZ_STORAGE_USAGE_BY_SHARD,
816+
// A forced migration allocates a fresh shard, which discards
817+
// the table's contents. Exclude the tables whose contents
818+
// are the point: storage usage is retained for billing, and
819+
// hydration history cannot be rebuilt from any other source.
820+
// The hydration exemption is best effort, not a guarantee.
821+
// See the tripwire in `plan_migration` for how to give it up
822+
// deliberately.
823+
Table(table) => {
824+
**table != *MZ_STORAGE_USAGE_BY_SHARD
825+
&& **table != *MZ_OBJECT_HYDRATION_HISTORY
826+
}
796827
MaterializedView(..) => true,
797828
Source(source) => **source != *MZ_CATALOG_RAW,
798829
Log(..) | View(..) | Type(..) | Func(..) | Index(..) | Connection(..) => false,

‎src/adapter/src/coord.rs‎

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ use mz_adapter_types::dyncfgs::{
9898
use mz_auth::password::Password;
9999
use mz_build_info::BuildInfo;
100100
use mz_catalog::builtin::{
101-
BUILTINS, BUILTINS_STATIC, MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY, MZ_STORAGE_USAGE_BY_SHARD,
101+
BUILTINS, BUILTINS_STATIC, MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY, MZ_OBJECT_HYDRATION_HISTORY,
102+
MZ_STORAGE_USAGE_BY_SHARD,
102103
};
103104
use mz_catalog::config::{AwsPrincipalContext, BuiltinItemMigrationConfig, ClusterReplicaSizeMap};
104105
use mz_catalog::durable::OpenableDurableCatalogState;
@@ -149,7 +150,7 @@ use mz_secrets::cache::CachingSecretsReader;
149150
use mz_secrets::{SecretsController, SecretsReader};
150151
use mz_sql::ast::{Raw, Statement};
151152
use mz_sql::catalog::{CatalogCluster, EnvironmentId};
152-
use mz_sql::names::{QualifiedItemName, ResolvedIds, SchemaSpecifier};
153+
use mz_sql::names::{QualifiedItemName, ResolvedIds};
153154
use mz_sql::optimizer_metrics::OptimizerMetrics;
154155
use mz_sql::plan::{
155156
self, AlterSinkPlan, ConnectionDetails, CreateConnectionPlan, HirRelationExpr,
@@ -3064,29 +3065,19 @@ impl Coordinator {
30643065
debug!("coordinator init: resetting system tables");
30653066
let read_ts = self.get_local_read_ts().await;
30663067

3067-
// Filter out tables whose contents must survive restarts:
3068-
// 'mz_storage_usage_by_shard' for billing, and
3069-
// 'mz_object_arrangement_size_history', which accumulates history that
3070-
// is pruned by its own retention period instead.
3071-
let mz_storage_usage_by_shard_schema: SchemaSpecifier = self
3072-
.catalog()
3073-
.resolve_system_schema(MZ_STORAGE_USAGE_BY_SHARD.schema)
3074-
.into();
3075-
let arrangement_size_history_schema: SchemaSpecifier = self
3076-
.catalog()
3077-
.resolve_system_schema(MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY.schema)
3078-
.into();
3079-
let is_retained_across_restarts = |meta: &TableMetadata| -> bool {
3080-
(meta.name.item == MZ_STORAGE_USAGE_BY_SHARD.name
3081-
&& meta.name.qualifiers.schema_spec == mz_storage_usage_by_shard_schema)
3082-
|| (meta.name.item == MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY.name
3083-
&& meta.name.qualifiers.schema_spec == arrangement_size_history_schema)
3084-
};
3068+
let retained_across_restarts = BTreeSet::from([
3069+
self.catalog()
3070+
.resolve_builtin_table(&MZ_STORAGE_USAGE_BY_SHARD),
3071+
self.catalog()
3072+
.resolve_builtin_table(&MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY),
3073+
self.catalog()
3074+
.resolve_builtin_table(&MZ_OBJECT_HYDRATION_HISTORY),
3075+
]);
30853076

30863077
let mut retraction_tasks = Vec::new();
30873078
let system_tables: Vec<_> = table_metas
30883079
.iter()
3089-
.filter(|meta| meta.id.is_system() && !is_retained_across_restarts(meta))
3080+
.filter(|meta| meta.id.is_system() && !retained_across_restarts.contains(&meta.id))
30903081
.collect();
30913082

30923083
for system_table in system_tables {

‎src/catalog/src/builtin.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,14 @@ pub static MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_DESCRIPTION: LazyLock<SystemObject
836836
object_type: CatalogItemType::Table,
837837
object_name: MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY.name.to_string(),
838838
});
839+
840+
/// Identifies [`MZ_OBJECT_HYDRATION_HISTORY`] for the schema-migration guard.
841+
pub static MZ_OBJECT_HYDRATION_HISTORY_DESCRIPTION: LazyLock<SystemObjectDescription> =
842+
LazyLock::new(|| SystemObjectDescription {
843+
schema_name: MZ_OBJECT_HYDRATION_HISTORY.schema.to_string(),
844+
object_type: CatalogItemType::Table,
845+
object_name: MZ_OBJECT_HYDRATION_HISTORY.name.to_string(),
846+
});
839847
pub const MZ_SYSTEM_ROLE: BuiltinRole = BuiltinRole {
840848
id: MZ_SYSTEM_ROLE_ID,
841849
name: SYSTEM_USER_NAME,
@@ -1463,6 +1471,13 @@ pub static BUILTINS_STATIC: LazyLock<Vec<Builtin<NameReference>>> = LazyLock::ne
14631471
Builtin::View(&MZ_INDEX_ADVICE),
14641472
Builtin::View(&MZ_MCP_DATA_PRODUCTS),
14651473
Builtin::View(&MZ_MCP_DATA_PRODUCT_DETAILS),
1474+
// NOTE: This list is a dependency order, and in a fresh environment it
1475+
// is also the order builtin ids are handed out in. Appending keeps a new
1476+
// builtin from shifting the ids of the ones before it, which is what the
1477+
// catalog goldens record. An existing environment is unaffected either
1478+
// way: its builtins keep the ids stored under their names.
1479+
Builtin::Table(&MZ_OBJECT_HYDRATION_HISTORY),
1480+
Builtin::Index(&MZ_OBJECT_HYDRATION_HISTORY_IND),
14661481
];
14671482

14681483
builtin_items.extend(notice::builtins());

‎src/catalog/src/builtin/mz_internal.rs‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4965,6 +4965,83 @@ pub static MZ_OBJECT_ARRANGEMENT_SIZE_HISTORY_TS_IND: LazyLock<BuiltinIndex> =
49654965
is_retained_metrics_object: true,
49664966
});
49674967

4968+
/// Completed hydration episodes, one row per object, replica, and installation.
4969+
///
4970+
/// Durability here is best effort, in both directions. We keep the contents out
4971+
/// of the bootstrap reset and out of forced schema migrations, because a sampled
4972+
/// history cannot be rebuilt from anything else once it is gone. But we do not
4973+
/// promise to preserve it forever: a future schema change may be worth more than
4974+
/// the accumulated rows, and clearing the table is an acceptable price to pay for
4975+
/// one. Nothing may depend on a row still being here.
4976+
pub static MZ_OBJECT_HYDRATION_HISTORY: LazyLock<BuiltinTable> = LazyLock::new(|| BuiltinTable {
4977+
name: "mz_object_hydration_history",
4978+
schema: MZ_INTERNAL_SCHEMA,
4979+
oid: oid::TABLE_MZ_OBJECT_HYDRATION_HISTORY_OID,
4980+
desc: RelationDesc::builder()
4981+
.with_column("object_id", SqlScalarType::String.nullable(false))
4982+
.with_column("cluster_id", SqlScalarType::String.nullable(false))
4983+
.with_column("replica_id", SqlScalarType::String.nullable(false))
4984+
.with_column(
4985+
"installed_at",
4986+
SqlScalarType::TimestampTz { precision: None }.nullable(false),
4987+
)
4988+
.with_column(
4989+
"started_at",
4990+
SqlScalarType::TimestampTz { precision: None }.nullable(true),
4991+
)
4992+
.with_column(
4993+
"finished_at",
4994+
SqlScalarType::TimestampTz { precision: None }.nullable(true),
4995+
)
4996+
.with_column("status", SqlScalarType::String.nullable(false))
4997+
.with_key(vec![0, 2, 3])
4998+
.finish(),
4999+
column_comments: BTreeMap::from_iter([
5000+
(
5001+
"object_id",
5002+
"The ID of the index or materialized view. May name an object that no longer exists.",
5003+
),
5004+
("cluster_id", "The ID of the object's cluster."),
5005+
(
5006+
"replica_id",
5007+
"The ID of the cluster replica. May name a replica that no longer exists.",
5008+
),
5009+
(
5010+
"installed_at",
5011+
"When the object's dataflow was installed on the replica.",
5012+
),
5013+
(
5014+
"started_at",
5015+
"When hydration work began, or `NULL` if the replica did not observe a start for this episode.",
5016+
),
5017+
("finished_at", "When hydration finished."),
5018+
(
5019+
"status",
5020+
"The terminal status. Currently always `hydrated`.",
5021+
),
5022+
]),
5023+
is_retained_metrics_object: true,
5024+
access: vec![PUBLIC_SELECT],
5025+
// No ontology links: a history row deliberately outlives the object and the
5026+
// replica it describes, so a foreign key to either would dangle for exactly
5027+
// the rows that make this table worth keeping.
5028+
ontology: None,
5029+
});
5030+
5031+
pub static MZ_OBJECT_HYDRATION_HISTORY_IND: LazyLock<BuiltinIndex> =
5032+
LazyLock::new(|| BuiltinIndex {
5033+
name: "mz_object_hydration_history_ind",
5034+
schema: MZ_INTERNAL_SCHEMA,
5035+
oid: oid::INDEX_MZ_OBJECT_HYDRATION_HISTORY_IND_OID,
5036+
// Keyed for the question users ask of this table, "how long did this
5037+
// object take to hydrate". The collector's own anti-join looks the same
5038+
// but cannot use this arrangement: its subscribe runs on the targeted
5039+
// user replica, where the index does not exist.
5040+
sql: "IN CLUSTER mz_catalog_server
5041+
ON mz_internal.mz_object_hydration_history (object_id)",
5042+
is_retained_metrics_object: true,
5043+
});
5044+
49685045
pub static MZ_COMPUTE_HYDRATION_STATUSES: LazyLock<BuiltinView> = LazyLock::new(|| BuiltinView {
49695046
name: "mz_compute_hydration_statuses",
49705047
schema: MZ_INTERNAL_SCHEMA,

‎src/pgrepr-consts/src/oid.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,3 +822,5 @@ pub const FUNC_PARSE_CONNECTION_DETAILS_OID: u32 = 17112;
822822
pub const FUNC_MZ_AWS_ACCOUNT_ID_OID: u32 = 17113;
823823
pub const FUNC_MZ_AWS_EXTERNAL_ID_PREFIX_OID: u32 = 17114;
824824
pub const FUNC_MZ_AWS_CONNECTION_ROLE_ARN_OID: u32 = 17115;
825+
pub const TABLE_MZ_OBJECT_HYDRATION_HISTORY_OID: u32 = 17116;
826+
pub const INDEX_MZ_OBJECT_HYDRATION_HISTORY_IND_OID: u32 = 17117;

‎test/sqllogictest/autogenerated/mz_internal.slt‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,17 @@ object_type text The␠type␠of␠the␠object:␠one␠of␠`table`,␠`sour
382382
created_at timestamp␠with␠time␠zone Wall-clock␠timestamp␠of␠when␠the␠object␠was␠created.␠`NULL`␠for␠built␠in␠system␠objects.
383383
dropped_at timestamp␠with␠time␠zone Wall-clock␠timestamp␠of␠when␠the␠object␠was␠dropped.␠`NULL`␠for␠built␠in␠system␠objects␠or␠if␠the␠object␠hasn't␠been␠dropped.
384384

385+
query TTT
386+
SELECT name, type, comment FROM objects WHERE schema = 'mz_internal' AND object = 'mz_object_hydration_history' ORDER BY position
387+
----
388+
object_id text The␠ID␠of␠the␠index␠or␠materialized␠view.␠May␠name␠an␠object␠that␠no␠longer␠exists.
389+
cluster_id text The␠ID␠of␠the␠object's␠cluster.
390+
replica_id text The␠ID␠of␠the␠cluster␠replica.␠May␠name␠a␠replica␠that␠no␠longer␠exists.
391+
installed_at timestamp␠with␠time␠zone When␠the␠object's␠dataflow␠was␠installed␠on␠the␠replica.
392+
started_at timestamp␠with␠time␠zone When␠hydration␠work␠began,␠or␠`NULL`␠if␠the␠replica␠did␠not␠observe␠a␠start␠for␠this␠episode.
393+
finished_at timestamp␠with␠time␠zone When␠hydration␠finished.
394+
status text The␠terminal␠status.␠Currently␠always␠`hydrated`.
395+
385396
query TTT
386397
SELECT name, type, comment FROM objects WHERE schema = 'mz_internal' AND object = 'mz_object_transitive_dependencies' ORDER BY position
387398
----
@@ -830,6 +841,7 @@ mz_object_dependencies
830841
mz_object_fully_qualified_names
831842
mz_object_global_ids
832843
mz_object_history
844+
mz_object_hydration_history
833845
mz_object_lifetimes
834846
mz_object_oid_alias
835847
mz_object_transitive_dependencies

‎test/sqllogictest/catalog_server_explain.slt‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1664,6 +1664,19 @@ Target cluster: mz_catalog_server
16641664

16651665
EOF
16661666

1667+
query T multiline
1668+
EXPLAIN INDEX "mz_internal"."mz_object_hydration_history_ind";
1669+
----
1670+
mz_internal.mz_object_hydration_history_ind:
1671+
→Arrange (#0{object_id})
1672+
→Stream mz_internal.mz_object_hydration_history
1673+
1674+
Source mz_internal.mz_object_hydration_history
1675+
1676+
Target cluster: mz_catalog_server
1677+
1678+
EOF
1679+
16671680
query T multiline
16681681
EXPLAIN INDEX "mz_internal"."mz_object_lifetimes_ind";
16691682
----
@@ -4811,7 +4824,7 @@ mz_catalog.mz_indexes:
48114824
Project: #2, #0, #1, #3, #4, #7, #5, #6
48124825
Map: "s1"
48134826
→Arrange (#1{name}) (#2{on_schema}, #3{on_name})
4814-
→Constant (80 rows)
4827+
→Constant (81 rows)
48154828
→Arrange (empty key) (#1{name})
48164829
→Fused with Child Map/Filter/Project
48174830
Project: #4, #3

‎test/sqllogictest/cluster.slt‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,15 +420,15 @@ CREATE CLUSTER test REPLICAS (foo (SIZE 'scale=1,workers=1'));
420420
query I
421421
SELECT COUNT(name) FROM mz_indexes;
422422
----
423-
304
423+
305
424424

425425
statement ok
426426
DROP CLUSTER test CASCADE
427427

428428
query T
429429
SELECT COUNT(name) FROM mz_indexes;
430430
----
431-
272
431+
273
432432

433433
simple conn=mz_system,user=mz_system
434434
ALTER CLUSTER quickstart OWNER TO materialize

‎test/sqllogictest/cockroach/srfs.slt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1473,6 +1473,7 @@ mz_object_arrangement_size_history 4
14731473
mz_object_arrangement_sizes 1
14741474
mz_object_dependencies 1
14751475
mz_object_history 1
1476+
mz_object_hydration_history 1
14761477
mz_object_lifetimes 1
14771478
mz_object_transitive_dependencies 1
14781479
mz_objects 3

0 commit comments

Comments
 (0)