Mysql string primary key partitioner - #38093
Conversation
8753eb8 to
50b66ec
Compare
6cdaec2 to
fe76d04
Compare
fe76d04 to
fcfd0ea
Compare
def-
left a comment
There was a problem hiding this comment.
Small extra tests:
diff --git a/src/mysql-util/src/partition.rs b/src/mysql-util/src/partition.rs
index 19fc9e1be2..12e87f759e 100644
--- a/src/mysql-util/src/partition.rs
+++ b/src/mysql-util/src/partition.rs
@@ -187,3 +187,81 @@ async fn children_prefixes(
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use mysql_async::prelude::Queryable;
+ use mz_ore::cast::CastFrom;
+
+ use super::*;
+ use crate::probe::tests::{connect, drop_db, setup_table};
+
+ /// Keys starting below space collate below `''` under a PAD SPACE collation,
+ /// and `utf8mb4_bin` is one that still compares character by character as
+ /// [`partition_table`] requires. Treating `''` as the start of the key space
+ /// hides that region from the walk, so it yields no boundaries and is never
+ /// counted towards sizing the ones it does yield. The snapshot range below
+ /// the first boundary is open-started, so it absorbs the whole region.
+ #[mz_ore::test(tokio::test)]
+ #[cfg_attr(miri, ignore)]
+ async fn test_live_mysql_partition_keys_below_empty_string() -> Result<(), anyhow::Error> {
+ let Some(mut conn) = connect().await? else {
+ return Ok(());
+ };
+ // One table entirely below `''`, one four fifths below it.
+ for (db, below) in [
+ ("mz_partition_all_below_test", 200),
+ ("mz_partition_part_below_test", 160),
+ ] {
+ let mut keys: Vec<String> = (0..below).map(|i| format!("\t{i:03}")).collect();
+ keys.extend((below..200).map(|i| format!("a{i:03}")));
+ let table = setup_table(&mut conn, db, "utf8mb4_bin", &keys).await?;
+ // Table statistics count every row, including those below `''`, so
+ // that is the estimate a caller has to work with.
+ let total = u64::cast_from(keys.len());
+
+ for workers in [2, 4, 8] {
+ let bounds =
+ partition_table(&mut conn, table.clone(), "id", workers, total, 1).await?;
+ let counts = partition_counts(&mut conn, db, &bounds, total).await?;
+ // An even split gives each worker `1 / workers` of the table, so
+ // capping at three fifths is loose. It fails only once the
+ // partitioning has effectively collapsed onto one worker.
+ let largest = counts.iter().copied().max().expect("nonempty");
+ assert!(
+ largest * 5 <= total * 3,
+ "{db} workers={workers} largest={largest} of {total}, counts={counts:?}"
+ );
+ }
+ drop_db(&mut conn, db).await?;
+ }
+ conn.disconnect().await?;
+ Ok(())
+ }
+
+ /// Rows per snapshot partition of `bounds`, i.e. the half-open ranges
+ /// `[..b0), [b0, b1), .., [bn, ..)`. The server counts, so the comparisons
+ /// happen under the column's own collation.
+ async fn partition_counts(
+ conn: &mut mysql_async::Conn,
+ db: &str,
+ bounds: &[String],
+ total: u64,
+ ) -> Result<Vec<u64>, anyhow::Error> {
+ let mut counts = Vec::with_capacity(bounds.len() + 1);
+ let mut below = 0;
+ for bound in bounds {
+ let cumulative: Option<u64> = conn
+ .exec_first(
+ format!("SELECT COUNT(*) FROM {db}.t WHERE id < ?"),
+ (bound.as_str(),),
+ )
+ .await?;
+ let cumulative = cumulative.expect("COUNT returns a row");
+ counts.push(cumulative - below);
+ below = cumulative;
+ }
+ counts.push(total - below);
+ Ok(counts)
+ }
+}
diff --git a/src/mysql-util/src/probe.rs b/src/mysql-util/src/probe.rs
index 676a50368d..c24e16e887 100644
--- a/src/mysql-util/src/probe.rs
+++ b/src/mysql-util/src/probe.rs
@@ -229,8 +229,9 @@ where
Ok(estimate)
}
+/// The live MySQL harness here is shared with [`crate::partition`]'s tests.
#[cfg(test)]
-mod tests {
+pub(crate) mod tests {
use std::collections::BTreeSet;
use mz_ore::cast::CastFrom;
@@ -722,6 +723,34 @@ mod tests {
Ok(())
}
+ #[mz_ore::test(tokio::test)]
+ #[cfg_attr(miri, ignore)]
+ async fn test_live_mysql_keys_below_empty_string() -> Result<(), anyhow::Error> {
+ let Some(mut conn) = connect().await? else {
+ return Ok(());
+ };
+ const DB: &str = "mz_probe_below_empty_test";
+ // `utf8mb4_bin` compares character by character but is PAD SPACE, so
+ // comparing a key against `''` pads `''` out with spaces and every key
+ // starting below space collates below `''`. An empty lower bound has to
+ // mean no lower bound, or that whole region of the key space is
+ // invisible to both `prefix_of_first_key_in_range` and the
+ // `max_key_with_prefix` step that advances off a prefix.
+ let keys = ["\u{1}a", "\u{9}b", "a1", "b1"];
+ let table = setup_table(&mut conn, DB, "utf8mb4_bin", &keys).await?;
+
+ let p = &mut KeyProber::new(&mut conn, table, "id");
+ assert_eq!(
+ prefix_of_first_key_in_range(p, "", None, 1).await,
+ some("\u{1}")
+ );
+ assert_eq!(walk_prefixes(p, 2).await?, keys);
+
+ drop_db(&mut conn, DB).await?;
+ conn.disconnect().await?;
+ Ok(())
+ }
+
#[mz_ore::test(tokio::test)]
#[cfg_attr(miri, ignore)]
async fn test_live_mysql_latin1_charset() -> Result<(), anyhow::Error> {
@@ -974,7 +1003,7 @@ mod tests {
/// Connects to the server named by `MZ_TEST_MYSQL_URL`, or `None` to skip
/// the test when it is unset. Skipping is a local-only convenience, CI
/// must always provide the URL.
- async fn connect() -> Result<Option<mysql_async::Conn>, anyhow::Error> {
+ pub(crate) async fn connect() -> Result<Option<mysql_async::Conn>, anyhow::Error> {
let Ok(url) = std::env::var("MZ_TEST_MYSQL_URL") else {
if mz_ore::env::is_var_truthy("CI") {
panic!("CI is supposed to run this test but something has gone wrong!");
@@ -1002,7 +1031,7 @@ mod tests {
/// Recreates scratch database `db` holding one table `t` whose string
/// primary key `id` is pinned to the given `collation`, containing
/// `keys`, with fresh statistics. Returns a ref for [`KeyProber::new`].
- async fn setup_table<'a>(
+ pub(crate) async fn setup_table<'a>(
conn: &mut mysql_async::Conn,
db: &'a str,
collation: &str,
@@ -1032,7 +1061,10 @@ mod tests {
}
/// Drops the scratch database `db`.
- async fn drop_db(conn: &mut mysql_async::Conn, db: &str) -> Result<(), anyhow::Error> {
+ pub(crate) async fn drop_db(
+ conn: &mut mysql_async::Conn,
+ db: &str,
+ ) -> Result<(), anyhow::Error> {
#[allow(clippy::disallowed_methods)]
conn.query_drop(format!("DROP DATABASE {db}")).await?;
Ok(())Currently both fail:
Details
thread 'probe::tests::test_live_mysql_keys_below_empty_string' (867008) panicked at src/mysql-util/src/probe.rs:743:9:
assertion `left == right` failed
left: Some("a")
right: Some("\u{1}")
stack backtrace:
0: __rustc::rust_begin_unwind
1: core::panicking::panic_fmt
2: core::panicking::assert_failed_inner
3: core::panicking::assert_failed::<core::option::Option<alloc::string::String>, core::option::Option<alloc::string::String>>
4: mz_mysql_util::probe::tests::test_live_mysql_keys_below_empty_string::{closure#0}::test_impl::{closure#0}
5: mz_mysql_util::probe::tests::test_live_mysql_keys_below_empty_string::{closure#0}
6: <core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>> as core::future::future::Future>::poll
7: <tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>> as core::future::future::Future>::poll
8: <tokio::runtime::park::CachedParkThread>::block_on::<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>::{closure#0}
9: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}::{closure#0}::{closure#0}
10: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}::{closure#0}
11: <tokio::runtime::scheduler::current_thread::Context>::enter::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}::{closure#0}>
12: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}
13: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}
14: <tokio::runtime::context::scoped::Scoped<tokio::runtime::scheduler::Context>>::set::<<tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>)>
15: tokio::runtime::context::set_scheduler::<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>::{closure#0}
16: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::try_with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>)>
17: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>)>
18: tokio::runtime::context::set_scheduler::<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>
19: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>
20: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>
21: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>::{closure#0}
22: tokio::runtime::context::runtime::enter_runtime::<<tokio::runtime::scheduler::current_thread::CurrentThread>::block_on<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>::{closure#0}, core::result::Result<(), anyhow::Error>>
23: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>
24: <tokio::runtime::runtime::Runtime>::block_on_inner::<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>
25: <tokio::runtime::runtime::Runtime>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>
26: mz_mysql_util::probe::tests::test_live_mysql_keys_below_empty_string
27: mz_mysql_util::probe::tests::test_live_mysql_keys_below_empty_string::{closure#0}
28: <mz_mysql_util::probe::tests::test_live_mysql_keys_below_empty_string::{closure#0} as core::ops::function::FnOnce<()>>::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Cancelling due to test failure: 1 test still running
FAIL [ 3.262s] mz-mysql-util partition::tests::test_live_mysql_partition_keys_below_empty_string
stdout ───
running 1 test
test partition::tests::test_live_mysql_partition_keys_below_empty_string ... FAILED
failures:
failures:
partition::tests::test_live_mysql_partition_keys_below_empty_string
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 32 filtered out; finished in 3.26s
stderr ───
thread 'partition::tests::test_live_mysql_partition_keys_below_empty_string' (867009) panicked at src/mysql-util/src/partition.rs:231:17:
mz_partition_all_below_test workers=2 largest=200 of 200, counts=[200]
stack backtrace:
0: __rustc::rust_begin_unwind
1: core::panicking::panic_fmt
2: mz_mysql_util::partition::tests::test_live_mysql_partition_keys_below_empty_string::{closure#0}::test_impl::{closure#0}
3: mz_mysql_util::partition::tests::test_live_mysql_partition_keys_below_empty_string::{closure#0}
4: <core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>> as core::future::future::Future>::poll
5: <tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>> as core::future::future::Future>::poll
6: <tokio::runtime::park::CachedParkThread>::block_on::<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>::{closure#0}
7: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}::{closure#0}::{closure#0}
8: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}::{closure#0}
9: <tokio::runtime::scheduler::current_thread::Context>::enter::<core::task::poll::Poll<core::result::Result<(), anyhow::Error>>, <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}::{closure#0}>
10: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}
11: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}
12: <tokio::runtime::context::scoped::Scoped<tokio::runtime::scheduler::Context>>::set::<<tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>)>
13: tokio::runtime::context::set_scheduler::<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>::{closure#0}
14: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::try_with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>)>
15: <std::thread::local::LocalKey<tokio::runtime::context::Context>>::with::<tokio::runtime::context::set_scheduler<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>::{closure#0}, (alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>)>
16: tokio::runtime::context::set_scheduler::<(alloc::boxed::Box<tokio::runtime::scheduler::current_thread::Core>, core::option::Option<core::result::Result<(), anyhow::Error>>), <tokio::runtime::scheduler::current_thread::CoreGuard>::enter<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>::{closure#0}>
17: <tokio::runtime::scheduler::current_thread::CoreGuard>::enter::<<tokio::runtime::scheduler::current_thread::CoreGuard>::block_on<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>::{closure#0}, core::option::Option<core::result::Result<(), anyhow::Error>>>
18: <tokio::runtime::scheduler::current_thread::CoreGuard>::block_on::<core::pin::Pin<&mut tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>>
19: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>::{closure#0}
20: tokio::runtime::context::runtime::enter_runtime::<<tokio::runtime::scheduler::current_thread::CurrentThread>::block_on<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>::{closure#0}, core::result::Result<(), anyhow::Error>>
21: <tokio::runtime::scheduler::current_thread::CurrentThread>::block_on::<tracing::instrument::Instrumented<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>>
22: <tokio::runtime::runtime::Runtime>::block_on_inner::<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>
23: <tokio::runtime::runtime::Runtime>::block_on::<core::pin::Pin<&mut dyn core::future::future::Future<Output = core::result::Result<(), anyhow::Error>>>>
24: mz_mysql_util::partition::tests::test_live_mysql_partition_keys_below_empty_string
25: mz_mysql_util::partition::tests::test_live_mysql_partition_keys_below_empty_string::{closure#0}
26: <mz_mysql_util::partition::tests::test_live_mysql_partition_keys_below_empty_string::{closure#0} as core::ops::function::FnOnce<()>>::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
04e3e3f to
5fee710
Compare
| let estimated_row_count = estimated_row_count.max(1); | ||
|
|
||
| // Estimates vary wildly especially near the full table size (see `KeyProber::estimate_range_rows` for more details). | ||
| // Estimates tend to get more useful as smaller chunks, so break up the table into at least 1/8ths (2 workers * 4) |
There was a problem hiding this comment.
The comment should explain why 1/8th (e.g. due to some testing, arbitrary, etc.).
There was a problem hiding this comment.
Added some explanation.
| db: &mut KeyProber<'_>, | ||
| workers: usize, | ||
| estimated_row_count: u64, | ||
| min_rows_per_worker: u64, |
There was a problem hiding this comment.
min_rows_per_worker seems to only guide the splitting, it doesn't enforce any bounds on the rows per worker. If the intention is to guide splitting, maybe a better name would be min_split_threshold with some explanation in a doc comment.
There was a problem hiding this comment.
yeah, in my mind it's "the minimum number of rows to bother trying to split down to". Will update and document.
| /// Computes up to `num_workers - 1` partition boundaries that divide the primary key space | ||
| /// into `num_workers` roughly even partitions. | ||
| /// This should be run in a repeatable read transaction against a primary key varchar/char column | ||
| /// with the `utf8mb4_bin` collation. |
There was a problem hiding this comment.
The doc needs to make it very clear that these are invariants expected of the caller. Since this function doesn't assert any of those things, a heads up on what goes wrong if the invariants aren't upheld.
KeyProber docs should call out similar invariants.
47030b9 to
d765772
Compare
### Motivation Split out some testing from #38022 to keep that PR manageable. Part of: [SS-97](https://linear.app/materializeinc/issue/SS-97) ### Description Adds testing for more diverse character types, collations, and for asserting searches use the B-tree rather than scanning the full table. ### For Context I've caught a couple of bugs in the later PR from discussion with Marty and a review with Dennis: #38093. 1. For PAD SPACE collations certain characters will sort before a shorter string. Our algorithm still works cleanly for that case at a high level, just throwing out the lower bounds which for utf8mb4_bin are all things like null, carriage return, and tab. The one tricky spot we needed to handle correctly was the upper bound, for which we're able to append NUL characters to get behavior like what we would have expected initially. I tried fixing this directly initially, but thought the fix was more complex than the perf issue was worth (see [here](#38147)). 2. Certain collations can't be supported with this approach because multiple characters in a row can in fact sort differently from it's prefix. There are tests here highlighting that and then most of the tests have been moved to utf8mb4_bin to match the first collation we intend to support.
7b3cfb1 to
f993f7b
Compare
Discovers boundaries that split a table string primary key space into per-worker ranges of roughly equal estimated row counts. Ranges the optimizer estimates too large are recursively subdivided at each distinct key prefix one character longer, probing through KeyProber, then accumulated into per-worker buckets, so discovery costs EXPLAIN index dives instead of an O(rows) index pass. Inaccurate estimates skew bucket sizes but never correctness: any ordered boundary list partitions the key space. All key ordering happens server-side under the column collation. The walk guards against non-advancing prefixes and caps children per split so a misbehaving server cannot hang it. KeyProber steps past exact keys shorter than the prefix length, so a lone short key among keys extending it cannot leave a range unsplittable. Also documents the caller contracts on like_prefix_pattern and explain_row_estimate.
Drop MAX_DEPTH, MAX_CHILDREN_PER_SPLIT, and the non-advancing prefix guard. On healthy data the walk terminates because child ranges shrink and fresh estimates track them. The pathological cases (phantom estimates, misbehaving servers) will be bounded by the per-table request budget once it lands, rather than by per-mechanism caps. Reformulate bucket sizing as a per-worker share divided by BUCKETS_PER_WORKER, dropping the double-to-8 bucket floor for small worker counts.
partition() becomes a composition of three stages: bucket_target_rows (pure sizing math), split_into_ranges (the only stage touching PartitionDb), and assign_boundaries (pure bucket accumulation). The pure stages are now directly unit-testable and the signatures document the data flow.
Rename the sizing knobs to say what they mean (TARGET_RANGES_PER_WORKER, min_rows_per_worker, target_max_rows_per_range), trim module and function docs to the essentials, and stop deduplicating repeated boundary ends. Duplicate ends only arise from non-advancing servers, and the snapshot layer validates boundary monotonicity server-side before using boundaries.
The trait is the partitioner-owned seam over the concrete KeyProber, a distinct name keeps it from shadowing the prober it wraps.
partition_table_by_pk_prefix becomes partition_table, the trait methods take the probe names they delegate to, and Range names its exclusive lower bound prefix.
Simplify children_prefixes into a clean prefix walk that accepts skipping exact keys shorter than the probe depth. Split breadth first with a coarse target of 1/max(workers, 8) of the table, restore key order via per-parent ordinal sort keys instead of client-side key comparison, and fold boundary assignment into compute_boundaries. Estimates are u64 end to end and a missing optimizer estimate is now a named MissingRowEstimate error raised inside estimate_range_rows.
…and smaller partitions for more accuracy
f993f7b to
a3c03ee
Compare
Motivation
Part of SS-97.
Working on a faster way to identify partition boundaries for string primary keys.
Description
Adds code for partitioning the primary keys of a table with a single-column string primary key.
The core idea for partitioning without needing to understand MySQL's string sort order rules is to walk through string prefixes, in order, estimating the sizes and refining as you go based on table size estimates.
We're doing a BFS here so in the future if we want to exit early (which is planned for a subsequent PR) we have a better shot of useful partitioning.