-
Notifications
You must be signed in to change notification settings - Fork 482
persist: register schema at write time, not writer open time #33902
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,7 @@ use differential_dataflow::lattice::Lattice; | |
| use itertools::Itertools; | ||
| use mz_build_info::{BuildInfo, build_info}; | ||
| use mz_dyncfg::ConfigSet; | ||
| use mz_ore::{instrument, soft_assert_or_log}; | ||
| use mz_ore::instrument; | ||
| use mz_persist::location::{Blob, Consensus, ExternalError}; | ||
| use mz_persist_types::schema::SchemaId; | ||
| use mz_persist_types::{Codec, Codec64, Opaque}; | ||
|
|
@@ -490,10 +490,6 @@ impl PersistClient { | |
| /// | ||
| /// Use this to save latency and a bit of persist traffic if you're just | ||
| /// going to immediately drop or expire the [ReadHandle]. | ||
| /// | ||
| /// The `_schema` parameter is currently unused, but should be an object | ||
| /// that represents the schema of the data in the shard. This will be required | ||
| /// in the future. | ||
| #[instrument(level = "debug", fields(shard = %shard_id))] | ||
| pub async fn open_writer<K, V, T, D>( | ||
| &self, | ||
|
|
@@ -511,23 +507,11 @@ impl PersistClient { | |
| let machine = self.make_machine(shard_id, diagnostics.clone()).await?; | ||
| let gc = GarbageCollector::new(machine.clone(), Arc::clone(&self.isolated_runtime)); | ||
|
|
||
| // TODO: Because schemas are ordered, as part of the persist schema | ||
| // changes work, we probably want to build some way to allow persist | ||
| // users to control the order. For example, maybe a | ||
| // `PersistClient::compare_and_append_schema(current_schema_id, | ||
| // next_schema)`. Presumably this would then be passed in to open_writer | ||
| // instead of us implicitly registering it here. | ||
| // NB: The overwhelming common case is that this schema is already | ||
| // registered. In this case, the cmd breaks early and nothing is | ||
| // written to (or read from) CRDB. | ||
| let (schema_id, maintenance) = machine.register_schema(&*key_schema, &*val_schema).await; | ||
| maintenance.start_performing(&machine, &gc); | ||
| soft_assert_or_log!( | ||
| schema_id.is_some(), | ||
| "unable to register schemas {:?} {:?}", | ||
| key_schema, | ||
| val_schema, | ||
| ); | ||
| // We defer registering the schema until write time, to allow opening | ||
| // write handles in a "read-only" mode where they don't implicitly | ||
| // modify persist state. But it might already be registered, in which | ||
| // case we can fetch its ID. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was a little nervous about how this changed the semantics of the |
||
| let schema_id = machine.find_schema(&*key_schema, &*val_schema); | ||
|
|
||
| let writer_id = WriterId::new(); | ||
| let schemas = Schemas { | ||
|
|
@@ -1992,7 +1976,6 @@ mod tests { | |
| #[mz_persist_proc::test(tokio::test)] | ||
| #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented | ||
| async fn finalize_empty_shard(dyncfgs: ConfigUpdates) { | ||
| const EMPTY: &[(((), ()), u64, i64)] = &[]; | ||
| let persist_client = new_test_client(&dyncfgs).await; | ||
|
|
||
| let shard_id = ShardId::new(); | ||
|
|
@@ -2006,11 +1989,7 @@ mod tests { | |
| // Advance since and upper to empty, which is a pre-requisite for | ||
| // finalization/tombstoning. | ||
| let () = read.downgrade_since(&Antichain::new()).await; | ||
| let () = write | ||
| .compare_and_append(EMPTY, Antichain::from_elem(0), Antichain::new()) | ||
| .await | ||
| .expect("usage should be valid") | ||
| .expect("upper should match"); | ||
| let () = write.advance_upper(&Antichain::new()).await; | ||
|
|
||
| let mut since_handle: SinceHandle<(), (), u64, i64, u64> = persist_client | ||
| .open_critical_since(shard_id, CRITICAL_SINCE, Diagnostics::for_tests()) | ||
|
|
@@ -2047,7 +2026,6 @@ mod tests { | |
| #[mz_persist_proc::test(tokio::test)] | ||
| #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented | ||
| async fn finalize_shard(dyncfgs: ConfigUpdates) { | ||
| const EMPTY: &[(((), ()), u64, i64)] = &[]; | ||
| const DATA: &[(((), ()), u64, i64)] = &[(((), ()), 0, 1)]; | ||
| let persist_client = new_test_client(&dyncfgs).await; | ||
|
|
||
|
|
@@ -2069,11 +2047,7 @@ mod tests { | |
| // Advance since and upper to empty, which is a pre-requisite for | ||
| // finalization/tombstoning. | ||
| let () = read.downgrade_since(&Antichain::new()).await; | ||
| let () = write | ||
| .compare_and_append(EMPTY, Antichain::from_elem(1), Antichain::new()) | ||
| .await | ||
| .expect("usage should be valid") | ||
| .expect("upper should match"); | ||
| let () = write.advance_upper(&Antichain::new()).await; | ||
|
|
||
| let mut since_handle: SinceHandle<(), (), u64, i64, u64> = persist_client | ||
| .open_critical_since(shard_id, CRITICAL_SINCE, Diagnostics::for_tests()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,6 +221,31 @@ where | |
| self.write_schemas.id | ||
| } | ||
|
|
||
| /// Registers the write schema, if it isn't already registered. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// This method expects that either the shard doesn't yet have any schema registered, or one of | ||
| /// the registered schemas is the same as the write schema. If all registered schemas are | ||
| /// different from the write schema, it panics. | ||
| pub async fn ensure_schema_registered(&mut self) -> SchemaId { | ||
| let Schemas { id, key, val } = &self.write_schemas; | ||
|
|
||
| if let Some(id) = id { | ||
| return *id; | ||
| } | ||
|
|
||
| let (schema_id, maintenance) = self.machine.register_schema(key, val).await; | ||
| maintenance.start_performing(&self.machine, &self.gc); | ||
|
|
||
| let Some(schema_id) = schema_id else { | ||
| panic!("unable to register schemas: {key:?} {val:?}"); | ||
| }; | ||
|
Comment on lines
+241
to
+243
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that previously in Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Maybe! Note that prior to this stuff being added, the schema could change ~arbitrarily without Persist noticing, so it was a strict improvement in safety. But anyways I don't think this assert has tripped at all in the year or so it's been in prod, so it seems safe to strengthen it now. |
||
|
|
||
| self.write_schemas.id = Some(schema_id); | ||
| schema_id | ||
| } | ||
|
|
||
| /// A cached version of the shard-global `upper` frontier. | ||
| /// | ||
| /// This is the most recent upper discovered by this handle. It is | ||
|
|
@@ -256,6 +281,50 @@ where | |
| &self.upper | ||
| } | ||
|
|
||
| /// Advance the shard's upper by the given frontier. | ||
| /// | ||
| /// If the provided `target` is less than or equal to the shard's upper, this is a no-op. | ||
| /// | ||
| /// In contrast to the various compare-and-append methods, this method does not require the | ||
| /// handle's write schema to be registered with the shard. That is, it is fine to use a dummy | ||
| /// schema when creating a writer just to advance a shard upper. | ||
| pub async fn advance_upper(&mut self, target: &Antichain<T>) { | ||
| // We avoid `fetch_recent_upper` here, to avoid a consensus roundtrip if the known upper is | ||
| // already beyond the target. | ||
| let mut lower = self.shared_upper().clone(); | ||
|
|
||
| while !PartialOrder::less_equal(target, &lower) { | ||
| let since = Antichain::from_elem(T::minimum()); | ||
| let desc = Description::new(lower.clone(), target.clone(), since); | ||
| let batch = HollowBatch::empty(desc); | ||
|
|
||
| let heartbeat_timestamp = (self.cfg.now)(); | ||
| let res = self | ||
| .machine | ||
| .compare_and_append( | ||
| &batch, | ||
| &self.writer_id, | ||
| &self.debug_state, | ||
| heartbeat_timestamp, | ||
| ) | ||
| .await; | ||
|
|
||
| use CompareAndAppendRes::*; | ||
| let new_upper = match res { | ||
| Success(_seq_no, maintenance) => { | ||
| maintenance.start_performing(&self.machine, &self.gc, self.compact.as_ref()); | ||
| batch.desc.upper().clone() | ||
| } | ||
| UpperMismatch(_seq_no, actual_upper) => actual_upper, | ||
| InvalidUsage(_invalid_usage) => unreachable!("batch bounds checked above"), | ||
| InlineBackpressure => unreachable!("batch was empty"), | ||
| }; | ||
|
|
||
| self.upper.clone_from(&new_upper); | ||
| lower = new_upper; | ||
| } | ||
| } | ||
|
|
||
| /// Applies `updates` to this shard and downgrades this handle's upper to | ||
| /// `upper`. | ||
| /// | ||
|
|
@@ -507,6 +576,9 @@ where | |
| } | ||
| } | ||
|
|
||
| // Before we append any data, we require a registered write schema. | ||
| self.ensure_schema_registered().await; | ||
|
|
||
| let lower = expected_upper.clone(); | ||
| let upper = new_upper; | ||
| let since = Antichain::from_elem(T::minimum()); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this TODO because we need the
Optionnow to supportWriteHandles who haven't yet registered their schema.