diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..bcec289 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,32 @@ +# cargo-nextest runs one process per test and schedules across test +# binaries, so the suite is no longer serialized behind the slowest +# binary. Ports come from `tests/common/ports.rs`, which reserves them +# across processes. + +[profile.default] +# Unit tests are cheap and numerous; the cap that matters is on the +# cluster group below. +test-threads = "num-cpus" +# Warn while a test drags, kill only what is clearly wedged. The +# longest drill (kill_restart, 5 spawn/kill/restart cycles) runs ~100s +# unloaded. +slow-timeout = { period = "45s", terminate-after = 8 } +failure-output = "immediate-final" +final-status-level = "slow" + +# Every integration test stages one or two PostgreSQL clusters plus a +# ClickHouse server, so their concurrency is bounded by RAM and disk, +# not by cores. +[test-groups.clusters] +max-threads = 4 + +[[profile.default.overrides]] +filter = 'kind(test)' +test-group = 'clusters' + +# Longest pole by far (~90s: 5 seeded spawn/kill/restart cycles each). +# Started last it sets the wall clock on its own; started first it hides +# under everything else. +[[profile.default.overrides]] +filter = 'binary(kill_restart)' +priority = 50 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 027c505..dfc93fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,18 +30,6 @@ jobs: pg-major: [16, 17, 18] steps: - # ubuntu-latest ships ~14GB free on /; `cargo --all-targets` links - # every integration test as its own static binary and can exhaust it - # mid-link. ENOSPC surfaces as an lld SIGBUS + a spurious "report to - # LLVM" banner. Fast `rm -rf` of the big preinstalled dirs - # (android/dotnet/haskell, ~21GB) buys enough room; skip large-packages - # (slow apt remove, minutes) and swap-storage (frees /mnt, not /). - - name: Free disk space - uses: jlumbroso/free-disk-space@v1.3.1 - with: - large-packages: false - swap-storage: false - - name: Checkout walshadow uses: actions/checkout@v7 with: @@ -53,6 +41,13 @@ jobs: with: components: clippy, rustfmt + # nextest runs one process per test and schedules across test + # binaries; `cargo test` runs binaries one at a time. + - name: Install cargo-nextest + uses: taiki-e/install-action@v2 + with: + tool: cargo-nextest + - name: Cargo cache uses: Swatinem/rust-cache@v2 with: @@ -164,17 +159,18 @@ jobs: # + wal_stream_chunk_boundary (use the freshly regenerated fixtures), # clickhouse-c-rs clickhouse_local + readme_quickstarts (need # clickhouse binary + server on :9000). - - name: Test (workspace, single-threaded — each test owns a PG cluster) + # + # Concurrency comes from `.config/nextest.toml`: unit tests run + # `num-cpus` wide, cluster drills are capped by the `clusters` + # test group. No `--no-capture`, it would force one thread. + - name: Test (workspace) working-directory: walshadow env: - # Tests pick non-overlapping ports but PG cluster startup is heavy; - # avoid thrashing the runner by serializing. - RUST_TEST_THREADS: "1" # Opts tests with `fx::dump_artifacts` into copying their tempdir # here on failure. Empty on success; uploaded as a job artifact # by the next step when present. WALSHADOW_ARTIFACT_DIR: ${{ github.workspace }}/test-artifacts - run: cargo test --workspace --all-targets --locked -- --nocapture + run: cargo nextest run --workspace --all-targets --locked --no-fail-fast - name: Upload test artifacts on failure if: failure() @@ -201,6 +197,12 @@ jobs: timeout-minutes: 60 steps: + # Coverage-only: instrumented binaries carry a counter table per + # test target and every test process drops its own `.profraw`, so + # this job's disk draw dwarfs the matrix jobs' (which run without + # any cleanup). Frees android/dotnet/haskell (~20GB); skip + # large-packages (slow apt remove, minutes) and swap-storage + # (frees /mnt, not /). - name: Free disk space uses: jlumbroso/free-disk-space@v1.3.1 with: @@ -220,10 +222,10 @@ jobs: # this component. components: llvm-tools-preview - - name: Install cargo-llvm-cov + - name: Install cargo-llvm-cov + cargo-nextest uses: taiki-e/install-action@v2 with: - tool: cargo-llvm-cov + tool: cargo-llvm-cov,cargo-nextest - name: Cargo cache uses: Swatinem/rust-cache@v2 @@ -311,12 +313,11 @@ jobs: - name: Run instrumented test suite (cargo llvm-cov) working-directory: walshadow env: - RUST_TEST_THREADS: "1" WALSHADOW_ARTIFACT_DIR: ${{ github.workspace }}/test-artifacts run: | cargo llvm-cov clean --workspace - cargo llvm-cov --workspace --all-targets --locked --no-report \ - -- --nocapture + cargo llvm-cov nextest --workspace --all-targets --locked \ + --no-report --no-fail-fast - name: Upload test artifacts on failure if: failure() diff --git a/README.md b/README.md index 294fbab..579d9bc 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,7 @@ params stay boot-only ``` make -C pgext -cargo test +cargo nextest run --workspace --all-targets cargo clippy --all-targets -- -D warnings ``` @@ -143,6 +143,13 @@ without it. Walshadow-side timeouts are seconds-scale by design — long timeouts mask stalls rather than surface them +CI runs the suite through [cargo-nextest](https://nexte.st), which +schedules across test binaries; `cargo test` still works but runs +binaries one at a time. Concurrency limits live in +`.config/nextest.toml`. Tests reserve TCP ports through +`tests/common/ports.rs`, so parallel runs — including several at once on +one machine — do not collide + ## Repository layout ``` diff --git a/plans/ops.md b/plans/ops.md index be134a7..0deb872 100644 --- a/plans/ops.md +++ b/plans/ops.md @@ -15,8 +15,8 @@ is to make long-running daemon survivable, observable, resumable debug window, drop older ones to bound disk - Persist resume state (six LSNs + resolved floor + source identity) across `kill -9` so daemon restart hands source's slot byte-identical - write/flush/apply triple, and `cargo test --test kill_restart` proves - end-state parity over 15 seeded kill/restart cycles + write/flush/apply triple, and `cargo nextest run -E 'test(kill_restart)'` + proves end-state parity over 15 seeded kill/restart cycles ## Preflight validators @@ -390,9 +390,10 @@ persist between kill and restart ## Kill-restart drill [`tests/kill_restart.rs`](../tests/kill_restart.rs). Three cutoff -strategies × five seeded windows = 15 daemon spawn/kill/restart cycles -per CI invocation. Source PG + CH server + basebackup-cloned shadow -stand up once, daemon cycles inside +strategies, one test each, five seeded windows per test = 15 daemon +spawn/kill/restart cycles per CI invocation. Each test stands up its own +source PG + CH server + basebackup-cloned shadow, daemon cycles inside, +so the three run concurrently Strategies: @@ -418,15 +419,16 @@ assert CH `count + sum(id) + md5(string_agg(name, ',' ORDER BY id))` matches source `WALSHADOW_KILL_SEED` env (default `0xC11AC11A`) seeds inline -splitmix-style LCG so CI is reproducible. Per-(strategy, run) seed -derivative shifts 250-750 ms kill window within each strategy. Nightly -rotation across seeds surfaces rare-window bugs +splitmix-style LCG so CI is reproducible. Strategy is folded into the +base seed and the per-run derivative shifts the 250-750 ms kill window, +so the three strategies walk different windows. Nightly rotation across +seeds surfaces rare-window bugs Test is NOT `#[ignore]`. Uses runtime skip-gates checking `fx::pg_available()` / `fx::pg_basebackup_available()` / `fx::clickhouse_available()` — silently `return` when binaries are absent, panics on actual failure when present (switched away from -`#[ignore]` so default `cargo test` exercises drill on any dev box with +`#[ignore]` so a default test run exercises drill on any dev box with PG + CH on PATH) Source pins `wal_keep_size = '128MB'` so 250-750 ms of WAL stays diff --git a/tests/add_column_default.rs b/tests/add_column_default.rs index dd53061..0f9d59b 100644 --- a/tests/add_column_default.rs +++ b/tests/add_column_default.rs @@ -28,12 +28,6 @@ use walshadow::mapping::ColumnMapping; use walshadow::mapping::TableTarget; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17401; -const SHADOW_PORT: u16 = 17402; -const CH_TCP_PORT: u16 = 17409; -const CH_HTTP_PORT: u16 = 17410; -const WALSENDER_PORT: u16 = 17450; - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn add_column_default_replicates_pre_alter_default() { if !fx::pg_available() { @@ -49,6 +43,7 @@ async fn add_column_default_replicates_pre_alter_default() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -62,16 +57,16 @@ async fn add_column_default_replicates_pre_alter_default() { "CREATE SCHEMA s14;\n\ CREATE TABLE s14.t (id bigint PRIMARY KEY, payload text);\n\ ALTER TABLE s14.t REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -115,7 +110,7 @@ async fn add_column_default_replicates_pre_alter_default() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-add-col-default", ddl: None, diff --git a/tests/bin_stream_e2e.rs b/tests/bin_stream_e2e.rs index d905570..b61987d 100644 --- a/tests/bin_stream_e2e.rs +++ b/tests/bin_stream_e2e.rs @@ -12,6 +12,9 @@ //! //! Skipped silently when `initdb` or `pg_basebackup` aren't on `$PATH`. +#[path = "common/ports.rs"] +mod ports; + use std::fs; use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; @@ -24,14 +27,6 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result, bail}; use walshadow::shadow::{BridgeConf, Shadow, ShadowConfig}; -// Reserved port slot for this test binary — 56170-range, distinct -// from pipeline_e2e (56100) / bootstrap_*_e2e (56140) so concurrent `cargo test` -// invocations don't trip over each other. -const SOURCE_PORT: u16 = 26171; -const SHADOW_PORT: u16 = 26172; -const METRICS_PORT: u16 = 26173; -const WALSENDER_PORT: u16 = 26174; - fn pg_available() -> bool { Command::new("initdb") .arg("--version") @@ -206,6 +201,34 @@ fn metric_u64(body: &str, name: &str) -> Result { .with_context(|| format!("{name} not in metrics body")) } +/// Run statements against the source, one autocommit `-c` each. +fn psql_exec(socket_dir: &Path, stmts: &[&str]) -> Result<()> { + let mut cmd = Command::new("psql"); + cmd.args([ + "-h", + socket_dir.to_str().context("source sock not utf8")?, + "-p", + &ports::PG_SOURCE_PORT.to_string(), + "-U", + "postgres", + "-d", + "postgres", + "-v", + "ON_ERROR_STOP=1", + ]); + for stmt in stmts { + cmd.args(["-c", stmt]); + } + let out = cmd.output().context("spawn workload psql")?; + if !out.status.success() { + bail!( + "workload psql failed: {}", + String::from_utf8_lossy(&out.stderr), + ); + } + Ok(()) +} + /// Wait for a child to exit, polling every 100 ms up to `deadline`. /// Returns the exit status on success; kills + reaps the child on /// timeout so a stuck daemon doesn't outlive the test. @@ -234,11 +257,13 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { } let bridge_lib_dir = pgext_dir(); + let metrics_port = ports::reserve_port(); + let walsender_port = ports::reserve_port(); let tmp = tempfile::tempdir().unwrap(); // 1. Source PG, schema before basebackup so shadow inherits the // same oids/filenodes the daemon's tracker seeds against. - let source = make_pg(&tmp, "source", SOURCE_PORT); + let source = make_pg(&tmp, "source", ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("source base conf"); append_source_conf(&source); @@ -264,13 +289,14 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { fs::create_dir_all(&shadow_filter_dir).unwrap(); let shadow_sock = tmp.path().join("shadow-sock"); fs::create_dir_all(&shadow_sock).unwrap(); - rewrite_for_shadow(&shadow_data, SHADOW_PORT, &shadow_sock).expect("retarget shadow conf"); - enable_recovery(&shadow_data, &shadow_filter_dir, WALSENDER_PORT) + rewrite_for_shadow(&shadow_data, ports::PG_SHADOW_PORT, &shadow_sock) + .expect("retarget shadow conf"); + enable_recovery(&shadow_data, &shadow_filter_dir, walsender_port) .expect("enable shadow recovery"); append_bridge_conf(&shadow_data, &shadow_sock, bridge_lib_dir).expect("preload bridge worker"); let mut shadow_cfg = ShadowConfig::new(shadow_data.clone(), shadow_filter_dir.clone()); - shadow_cfg.port = SHADOW_PORT; + shadow_cfg.port = ports::PG_SHADOW_PORT; shadow_cfg.socket_dir = shadow_sock.clone(); shadow_cfg.ctl_timeout = Duration::from_secs(60); let shadow = Shadow::new(shadow_cfg); @@ -293,13 +319,13 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { let bin = env!("CARGO_BIN_EXE_walshadow-stream"); let stderr_path = tmp.path().join("daemon.stderr.log"); let stderr_file = fs::File::create(&stderr_path).expect("open daemon stderr log"); - let metrics_addr: SocketAddr = format!("127.0.0.1:{METRICS_PORT}").parse().unwrap(); + let metrics_addr: SocketAddr = format!("127.0.0.1:{metrics_port}").parse().unwrap(); let mut child = Command::new(bin) .args([ "--host", source.config().socket_dir.to_str().unwrap(), "--port", - &SOURCE_PORT.to_string(), + &ports::PG_SOURCE_PORT.to_string(), "--user", "postgres", "--dbname", @@ -311,7 +337,7 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { "--shadow-socket-dir", shadow_sock.to_str().unwrap(), "--shadow-port", - &SHADOW_PORT.to_string(), + &ports::PG_SHADOW_PORT.to_string(), "--shadow-user", "postgres", "--shadow-dbname", @@ -325,7 +351,7 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { "--metrics-bind", &metrics_addr.to_string(), "--walsender-bind", - &format!("127.0.0.1:{WALSENDER_PORT}"), + &format!("127.0.0.1:{walsender_port}"), // Retention disabled — no shadow_replay sweeper churn // racing the test's max-segments exit. Default would // poll shadow on a 60s cadence; we'd never observe it. @@ -377,35 +403,15 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { // replay, while bs.t's INSERTs stay invisible there (their // destination is the CH emitter, exercised by pipeline_e2e). // Autocommit per `-c` keeps each commit in the same segment - // as its records; pg_switch_wal seals the work. + // as its records. let driver_sock = source.config().socket_dir.clone(); - let out = Command::new("psql") - .args([ - "-h", - driver_sock.to_str().unwrap(), - "-p", - &SOURCE_PORT.to_string(), - "-U", - "postgres", - "-d", - "postgres", - "-v", - "ON_ERROR_STOP=1", - "-c", + psql_exec( + &driver_sock, + &[ "CREATE TABLE bs.t2 (id int PRIMARY KEY, payload text)", - "-c", "INSERT INTO bs.t SELECT g, repeat('x', g)::text FROM generate_series(1, 5) g", - "-c", - "SELECT pg_switch_wal()", - ]) - .output() - .context("spawn workload psql")?; - if !out.status.success() { - bail!( - "workload psql failed: {}", - String::from_utf8_lossy(&out.stderr), - ); - } + ], + )?; // 6b. Null-tail watermark: the metrics-only pipeline's contiguous // ack must advance past the workload's commits — routed-nothing @@ -425,6 +431,12 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { std::thread::sleep(Duration::from_millis(200)); } + // 6c. Seal the workload's segment only once the scrapes are done: + // pg_switch_wal trips `--max-segments=1` and the metrics + // endpoint dies with the daemon, so a scrape racing that exit + // reads a broken pipe. + psql_exec(&driver_sock, &["SELECT pg_switch_wal()"])?; + // 7. Wait for the daemon to hit `--max-segments=1` and exit // cleanly. 60s budget covers basebackup retry + status-tick // cadence on slow CI. @@ -541,12 +553,6 @@ async fn bin_stream_replicates_segments_and_serves_metrics() { } } -// Distinct port slot for the wire-drop test so it can run concurrently. -const WD_SOURCE_PORT: u16 = 26181; -const WD_SHADOW_PORT: u16 = 26182; -const WD_METRICS_PORT: u16 = 26183; -const WD_WALSENDER_PORT: u16 = 26184; - /// `kill - -` on the shadow cluster's process group, read from /// `postmaster.pid` + `/proc//stat` (field 5 = pgrp). Pausing the *group* /// (not just the postmaster) stops the walreceiver child too, so it stops @@ -585,7 +591,7 @@ fn spawn_writer(socket_dir: &Path) -> Result { -c \"INSERT INTO bs.load SELECT repeat('x',100) FROM generate_series(1,200)\" \ >/dev/null 2>&1; sleep 0.2; done", socket_dir.display(), - WD_SOURCE_PORT, + ports::PG_SOURCE_PORT, ); Command::new("bash") .arg("-c") @@ -625,8 +631,10 @@ async fn wire_drop_midsegment_shadow_resumes_streaming() { } let bridge_lib_dir = pgext_dir(); + let metrics_port = ports::reserve_port(); + let walsender_port = ports::reserve_port(); let tmp = tempfile::tempdir().unwrap(); - let source = make_pg(&tmp, "wd-source", WD_SOURCE_PORT); + let source = make_pg(&tmp, "wd-source", ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("source base conf"); append_source_conf(&source); @@ -648,8 +656,8 @@ async fn wire_drop_midsegment_shadow_resumes_streaming() { fs::create_dir_all(&filter_dir).unwrap(); let shadow_sock = tmp.path().join("wd-shadow-sock"); fs::create_dir_all(&shadow_sock).unwrap(); - rewrite_for_shadow(&shadow_data, WD_SHADOW_PORT, &shadow_sock).expect("retarget shadow"); - enable_recovery(&shadow_data, &filter_dir, WD_WALSENDER_PORT).expect("enable recovery"); + rewrite_for_shadow(&shadow_data, ports::PG_SHADOW_PORT, &shadow_sock).expect("retarget shadow"); + enable_recovery(&shadow_data, &filter_dir, walsender_port).expect("enable recovery"); append_bridge_conf(&shadow_data, &shadow_sock, bridge_lib_dir).expect("preload bridge worker"); // Slow the walreceiver restart so killing it leaves a multi-second window in // which the writer advances the head — guaranteeing the reconnect lands @@ -661,7 +669,7 @@ async fn wire_drop_midsegment_shadow_resumes_streaming() { } let mut shadow_cfg = ShadowConfig::new(shadow_data.clone(), filter_dir.clone()); - shadow_cfg.port = WD_SHADOW_PORT; + shadow_cfg.port = ports::PG_SHADOW_PORT; shadow_cfg.socket_dir = shadow_sock.clone(); shadow_cfg.ctl_timeout = Duration::from_secs(60); let shadow = Shadow::new(shadow_cfg); @@ -673,13 +681,13 @@ async fn wire_drop_midsegment_shadow_resumes_streaming() { let bin = env!("CARGO_BIN_EXE_walshadow-stream"); let stderr_path = tmp.path().join("wd-daemon.stderr.log"); let stderr_file = fs::File::create(&stderr_path).unwrap(); - let metrics_addr: SocketAddr = format!("127.0.0.1:{WD_METRICS_PORT}").parse().unwrap(); + let metrics_addr: SocketAddr = format!("127.0.0.1:{metrics_port}").parse().unwrap(); let mut child = Command::new(bin) .args([ "--host", source.config().socket_dir.to_str().unwrap(), "--port", - &WD_SOURCE_PORT.to_string(), + &ports::PG_SOURCE_PORT.to_string(), "--user", "postgres", "--dbname", @@ -691,7 +699,7 @@ async fn wire_drop_midsegment_shadow_resumes_streaming() { "--shadow-socket-dir", shadow_sock.to_str().unwrap(), "--shadow-port", - &WD_SHADOW_PORT.to_string(), + &ports::PG_SHADOW_PORT.to_string(), "--shadow-user", "postgres", "--shadow-dbname", @@ -703,7 +711,7 @@ async fn wire_drop_midsegment_shadow_resumes_streaming() { "--metrics-bind", &metrics_addr.to_string(), "--walsender-bind", - &format!("127.0.0.1:{WD_WALSENDER_PORT}"), + &format!("127.0.0.1:{walsender_port}"), // Default (large) threshold: we force the disconnect by killing the // walreceiver, not by overflowing the queue — so baseline streaming // never trips a spurious drop. diff --git a/tests/bootstrap_direct_ch.rs b/tests/bootstrap_direct_ch.rs index 192b803..a4e89f8 100644 --- a/tests/bootstrap_direct_ch.rs +++ b/tests/bootstrap_direct_ch.rs @@ -43,18 +43,6 @@ use walshadow::mapping::TableTarget; use walshadow::schema::RelName; use walshadow::shadow::{Shadow, ShadowConfig}; -// Reserved port slot — 17300-range. Kept below the Linux ephemeral -// port range (32768-60999) so an outbound TCP connect from the daemon -// (to CH / shadow PG) can't land on a port we're about to bind for the -// metrics / walsender listener. CH's `interserver_http_port` defaults -// to `http_port + 1`, so METRICS / WALSENDER must dodge that slot too. -const SOURCE_PORT: u16 = 17301; -const SHADOW_PORT: u16 = 17302; -const CH_TCP_PORT: u16 = 17309; -const CH_HTTP_PORT: u16 = 17310; -const METRICS_PORT: u16 = 17315; -const WALSENDER_PORT: u16 = 17316; - const N_ROWS: i32 = 64; fn make_source(tmp: &tempfile::TempDir) -> Shadow { @@ -62,7 +50,7 @@ fn make_source(tmp: &tempfile::TempDir) -> Shadow { tmp.path().join("source-data"), tmp.path().join("source-filtered"), ); - cfg.port = SOURCE_PORT; + cfg.port = fx::PG_SOURCE_PORT; cfg.socket_dir = tmp.path().join("source-sock"); cfg.ctl_timeout = Duration::from_secs(60); fs::create_dir_all(&cfg.filter_out_dir).unwrap(); @@ -85,6 +73,7 @@ async fn direct_bootstrap_ch_end_to_end() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); // 1. Source PG. @@ -100,7 +89,7 @@ async fn direct_bootstrap_ch_end_to_end() { // 3. CH server + dest table. let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); fx::create_ch_dest_table(&ch, "default", "t").expect("create ch table"); // 4. CH-config TOML. @@ -108,7 +97,7 @@ async fn direct_bootstrap_ch_end_to_end() { fx::write_ch_config_toml( &ch_config_path, "127.0.0.1", - CH_TCP_PORT, + slot.ch_tcp, "default", &RelName::new("s14", "t"), &TableTarget::new("default", "t"), @@ -136,13 +125,13 @@ async fn direct_bootstrap_ch_end_to_end() { let bin = env!("CARGO_BIN_EXE_walshadow-stream"); let stderr_path = tmp.path().join("daemon.stderr.log"); let stderr_file = fs::File::create(&stderr_path).expect("open daemon stderr log"); - let metrics_addr: SocketAddr = format!("127.0.0.1:{METRICS_PORT}").parse().unwrap(); + let metrics_addr: SocketAddr = format!("127.0.0.1:{}", slot.metrics).parse().unwrap(); let child = Command::new(bin) .args([ "--host", source.config().socket_dir.to_str().unwrap(), "--port", - &SOURCE_PORT.to_string(), + &fx::PG_SOURCE_PORT.to_string(), "--user", "postgres", "--dbname", @@ -154,7 +143,7 @@ async fn direct_bootstrap_ch_end_to_end() { "--shadow-socket-dir", shadow_sock.to_str().unwrap(), "--shadow-port", - &SHADOW_PORT.to_string(), + &fx::PG_SHADOW_PORT.to_string(), "--shadow-user", "postgres", "--shadow-dbname", @@ -166,7 +155,7 @@ async fn direct_bootstrap_ch_end_to_end() { "--metrics-bind", &metrics_addr.to_string(), "--walsender-bind", - &format!("127.0.0.1:{WALSENDER_PORT}"), + &format!("127.0.0.1:{}", slot.walsender), "--retention-bytes", "0", "--ch-config", @@ -230,7 +219,7 @@ async fn direct_bootstrap_ch_end_to_end() { if bootstrap_shadow_data_dir.join("postmaster.pid").exists() { let mut shadow_cfg = ShadowConfig::new(bootstrap_shadow_data_dir.clone(), shadow_filter_dir.clone()); - shadow_cfg.port = SHADOW_PORT; + shadow_cfg.port = fx::PG_SHADOW_PORT; shadow_cfg.socket_dir = shadow_sock.clone(); shadow_cfg.ctl_timeout = Duration::from_secs(60); let shadow = Shadow::new(shadow_cfg); diff --git a/tests/bootstrap_direct_e2e.rs b/tests/bootstrap_direct_e2e.rs index 3e60c80..e60aeff 100644 --- a/tests/bootstrap_direct_e2e.rs +++ b/tests/bootstrap_direct_e2e.rs @@ -20,6 +20,9 @@ //! //! Skipped silently when `initdb` is not on `$PATH`. +#[path = "common/ports.rs"] +mod ports; + use std::collections::HashSet; use std::fs; use std::io::Write; @@ -36,10 +39,6 @@ use walshadow::backup_source_direct::DirectSource; use walshadow::heap_decoder::{ColumnValue, HeapOp}; use walshadow::shadow::{Shadow, ShadowConfig}; -/// Reserved port slot — 56145 sits between `bootstrap_object_store_e2e` -/// (56141) and other test binaries' 56170/56300 slots. -const SOURCE_PORT: u16 = 56145; - /// Same row budget as the object-store sibling: one heap page worth so /// the page walker has guaranteed bytes without a multi-page sweep. const N_ROWS: i32 = 64; @@ -57,7 +56,7 @@ fn make_source(tmp: &tempfile::TempDir) -> Shadow { tmp.path().join("source-data"), tmp.path().join("source-filtered"), ); - cfg.port = SOURCE_PORT; + cfg.port = ports::PG_SOURCE_PORT; cfg.socket_dir = tmp.path().join("source-sock"); cfg.ctl_timeout = Duration::from_secs(60); fs::create_dir_all(&cfg.filter_out_dir).unwrap(); diff --git a/tests/bootstrap_object_store_ch.rs b/tests/bootstrap_object_store_ch.rs index 7d40e14..ab3af59 100644 --- a/tests/bootstrap_object_store_ch.rs +++ b/tests/bootstrap_object_store_ch.rs @@ -53,17 +53,6 @@ use walshadow::mapping::TableTarget; use walshadow::schema::RelName; use walshadow::shadow::{Shadow, ShadowConfig}; -// Reserved port slot — 17320-range. Below the Linux ephemeral port -// range (32768-60999) so outbound TCP connects can't grab a port the -// daemon is about to bind. CH's `interserver_http_port = http_port + 1` -// must dodge METRICS/WALSENDER, so the two clusters are spaced apart. -const SOURCE_PORT: u16 = 17321; -const SHADOW_PORT: u16 = 17322; -const CH_TCP_PORT: u16 = 17329; -const CH_HTTP_PORT: u16 = 17330; -const METRICS_PORT: u16 = 17335; -const WALSENDER_PORT: u16 = 17336; - const N_ROWS: i32 = 64; /// Walk source's `pg_wal/` and push every completed 24-hex-digit WAL @@ -99,7 +88,7 @@ fn make_source(tmp: &tempfile::TempDir) -> Shadow { tmp.path().join("source-data"), tmp.path().join("source-filtered"), ); - cfg.port = SOURCE_PORT; + cfg.port = fx::PG_SOURCE_PORT; cfg.socket_dir = tmp.path().join("source-sock"); cfg.ctl_timeout = Duration::from_secs(60); fs::create_dir_all(&cfg.filter_out_dir).unwrap(); @@ -131,6 +120,7 @@ async fn object_store_bootstrap_ch_end_to_end() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); // 1. Source PG. @@ -194,7 +184,7 @@ async fn object_store_bootstrap_ch_end_to_end() { // 4. CH server + dest table. let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); fx::create_ch_dest_table(&ch, "default", "t").expect("create ch table"); // 5. CH-config TOML. @@ -202,7 +192,7 @@ async fn object_store_bootstrap_ch_end_to_end() { fx::write_ch_config_toml( &ch_config_path, "127.0.0.1", - CH_TCP_PORT, + slot.ch_tcp, "default", &RelName::new("s14", "t"), &TableTarget::new("default", "t"), @@ -231,13 +221,13 @@ async fn object_store_bootstrap_ch_end_to_end() { let bin = env!("CARGO_BIN_EXE_walshadow-stream"); let stderr_path = tmp.path().join("daemon.stderr.log"); let stderr_file = fs::File::create(&stderr_path).expect("open daemon stderr log"); - let metrics_addr: SocketAddr = format!("127.0.0.1:{METRICS_PORT}").parse().unwrap(); + let metrics_addr: SocketAddr = format!("127.0.0.1:{}", slot.metrics).parse().unwrap(); let child = Command::new(bin) .args([ "--host", source.config().socket_dir.to_str().unwrap(), "--port", - &SOURCE_PORT.to_string(), + &fx::PG_SOURCE_PORT.to_string(), "--user", "postgres", "--dbname", @@ -249,7 +239,7 @@ async fn object_store_bootstrap_ch_end_to_end() { "--shadow-socket-dir", shadow_sock.to_str().unwrap(), "--shadow-port", - &SHADOW_PORT.to_string(), + &fx::PG_SHADOW_PORT.to_string(), "--shadow-user", "postgres", "--shadow-dbname", @@ -261,7 +251,7 @@ async fn object_store_bootstrap_ch_end_to_end() { "--metrics-bind", &metrics_addr.to_string(), "--walsender-bind", - &format!("127.0.0.1:{WALSENDER_PORT}"), + &format!("127.0.0.1:{}", slot.walsender), "--retention-bytes", "0", "--ch-config", @@ -276,7 +266,7 @@ async fn object_store_bootstrap_ch_end_to_end() { "120", ]) .env("PGHOST", source.config().socket_dir.to_str().unwrap()) - .env("PGPORT", SOURCE_PORT.to_string()) + .env("PGPORT", fx::PG_SOURCE_PORT.to_string()) .env("PGUSER", "postgres") .env("PGDATABASE", "postgres") .env("RUST_LOG", "warn,walshadow=info") @@ -332,7 +322,7 @@ async fn object_store_bootstrap_ch_end_to_end() { if bootstrap_shadow_data_dir.join("postmaster.pid").exists() { let mut shadow_cfg = ShadowConfig::new(bootstrap_shadow_data_dir.clone(), shadow_filter_dir.clone()); - shadow_cfg.port = SHADOW_PORT; + shadow_cfg.port = fx::PG_SHADOW_PORT; shadow_cfg.socket_dir = shadow_sock.clone(); shadow_cfg.ctl_timeout = Duration::from_secs(60); let shadow = Shadow::new(shadow_cfg); diff --git a/tests/bootstrap_object_store_e2e.rs b/tests/bootstrap_object_store_e2e.rs index 4742640..ff77098 100644 --- a/tests/bootstrap_object_store_e2e.rs +++ b/tests/bootstrap_object_store_e2e.rs @@ -43,6 +43,9 @@ //! oracle if ever wanted; bootstrap doesn't depend on it. This test runs //! purely off `walshadow` + `walrus` crates. +#[path = "common/ports.rs"] +mod ports; + use std::collections::HashSet; use std::fs; use std::io::Write; @@ -65,10 +68,6 @@ use walshadow::backup_source_object_store::ObjectStoreSource; use walshadow::heap_decoder::{ColumnValue, HeapOp}; use walshadow::shadow::{Shadow, ShadowConfig}; -/// Port slot reserved for the bootstrap object-store drill (56140-range). Single source-PG per -/// test binary so the env-var rendezvous below stays single-writer. -const SOURCE_PORT: u16 = 56141; - /// Row count loaded into the user table before push runs. Must be >0 /// and small enough that one heap page fits the lot — at one /// ~32-byte tuple per row, 8 KiB holds ~250 rows, so 64 is safely on @@ -88,7 +87,7 @@ fn make_source(tmp: &tempfile::TempDir) -> Shadow { tmp.path().join("source-data"), tmp.path().join("source-filtered"), ); - cfg.port = SOURCE_PORT; + cfg.port = ports::PG_SOURCE_PORT; cfg.socket_dir = tmp.path().join("source-sock"); cfg.ctl_timeout = Duration::from_secs(60); fs::create_dir_all(&cfg.filter_out_dir).unwrap(); diff --git a/tests/bootstrap_pipeline_ch.rs b/tests/bootstrap_pipeline_ch.rs index 31451c9..4cc3405 100644 --- a/tests/bootstrap_pipeline_ch.rs +++ b/tests/bootstrap_pipeline_ch.rs @@ -34,9 +34,6 @@ use walshadow::pipeline::{Fatal, bootstrap, tail}; use walshadow::schema::{RelAttr, RelDescriptor, RelName, ReplIdent}; use walshadow::toast::ToastResolver; -const CH_TCP_PORT: u16 = 17571; -const CH_HTTP_PORT: u16 = 17572; - const START_LSN: u64 = 0x5000_0000; const ROWS_PER_TABLE: i32 = 30; const INSERTERS: usize = 2; @@ -108,8 +105,9 @@ async fn bootstrap_tail_fans_out_n2() { return; } + let slot = fx::Ports::alloc(); let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); for t in ["foo", "baz"] { @@ -128,7 +126,7 @@ async fn bootstrap_tail_fans_out_n2() { // fan across the two inserter connections and ack out of order. let mut cfg = EmitterConfig { host: "127.0.0.1".into(), - port: CH_TCP_PORT, + port: slot.ch_tcp, database: "walshadow_test".into(), compression: CompressionChoice::None, row_budget: 4, diff --git a/tests/bridge.rs b/tests/bridge.rs index 26a105e..1db477d 100644 --- a/tests/bridge.rs +++ b/tests/bridge.rs @@ -40,6 +40,9 @@ //! xids, not recovery, so an open transaction exercises the same code a //! standby's replaying transaction does. +#[path = "common/ports.rs"] +mod ports; + use std::fs; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; @@ -58,8 +61,6 @@ use walshadow::schema::ReplIdent; use walshadow::shadow::{BridgeConf, Shadow, ShadowConfig}; use walshadow::shadow_catalog::{CatalogError, ShadowCatalog, ShadowCatalogConfig}; -const BASE_PORT: u16 = 56401; - fn pg_available() -> bool { Command::new("initdb") .arg("--version") @@ -220,7 +221,7 @@ async fn bridge_hello_and_decode_batch() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = dial(&guard.sh).await; let info = bridge.info().expect("hello"); @@ -273,7 +274,7 @@ async fn bridge_decode_matches_typoutput() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 5); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = dial(&guard.sh).await; let sql = connect_sql(&guard.sh).await; @@ -371,7 +372,7 @@ async fn bridge_scans_uncommitted_ddl() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 1); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = dial(&guard.sh).await; let setup = connect_sql(&guard.sh).await; @@ -569,7 +570,7 @@ async fn bridge_reconnects_after_worker_exit() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 2); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = dial(&guard.sh).await; assert_eq!(bridge.replay_lsn().await.expect("before"), 0); @@ -649,7 +650,7 @@ async fn bridge_drops_bad_frames_per_connection() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 3); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = dial(&guard.sh).await; let path = guard.sh.bridge_socket().unwrap().to_path_buf(); @@ -690,7 +691,7 @@ async fn bridge_overlay_descriptors_track_open_ddl() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 6); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = Arc::new(dial(&guard.sh).await); let mut cat = open_catalog(&guard.sh, bridge).await; let (_stand_in, mut mirror) = @@ -849,7 +850,7 @@ async fn bridge_error_frames_stay_parseable() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 4); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let bridge = dial(&guard.sh).await; let path = guard.sh.bridge_socket().unwrap().to_path_buf(); let mut raw = UnixStream::connect(&path).expect("raw connect"); @@ -943,7 +944,7 @@ async fn bridge_committed_read_falls_back_when_replay_moves() { return; } let tmp = tempfile::tempdir().unwrap(); - let guard = start_pg(&tmp, BASE_PORT + 7); + let guard = start_pg(&tmp, ports::PG_SHADOW_PORT); let setup = connect_sql(&guard.sh).await; setup .batch_execute("CREATE TABLE t (id int PRIMARY KEY, a text)") diff --git a/tests/catalog_seed.rs b/tests/catalog_seed.rs index 4daa94f..62cab77 100644 --- a/tests/catalog_seed.rs +++ b/tests/catalog_seed.rs @@ -11,6 +11,9 @@ //! — this stands in for the upstream "source PG", not shadow PG. The //! same binary serves both roles. +#[path = "common/ports.rs"] +mod ports; + use std::process::Command; use std::time::Duration; @@ -92,7 +95,7 @@ async fn seed_picks_up_initial_mapped_catalog_filenodes() { return; } let tmp = tempfile::tempdir().unwrap(); - let sh = make_cluster(&tmp, 55701); + let sh = make_cluster(&tmp, ports::PG_SOURCE_PORT); sh.initdb().expect("initdb"); sh.write_base_conf().expect("conf"); sh.start().expect("start"); @@ -147,7 +150,7 @@ async fn seed_closes_pre_attach_pg_class_rotation_hole() { return; } let tmp = tempfile::tempdir().unwrap(); - let sh = make_cluster(&tmp, 55702); + let sh = make_cluster(&tmp, ports::PG_SOURCE_PORT); sh.initdb().expect("initdb"); sh.write_base_conf().expect("conf"); sh.start().expect("start"); @@ -216,7 +219,7 @@ async fn seed_skips_user_tables() { return; } let tmp = tempfile::tempdir().unwrap(); - let sh = make_cluster(&tmp, 55703); + let sh = make_cluster(&tmp, ports::PG_SOURCE_PORT); sh.initdb().expect("initdb"); sh.write_base_conf().expect("conf"); sh.start().expect("start"); diff --git a/tests/common/bootstrap_ch_fixture.rs b/tests/common/bootstrap_ch_fixture.rs index f95b957..62bba4d 100644 --- a/tests/common/bootstrap_ch_fixture.rs +++ b/tests/common/bootstrap_ch_fixture.rs @@ -15,6 +15,11 @@ #![allow(dead_code)] +#[path = "ports.rs"] +mod ports; +#[allow(unused_imports)] +pub use ports::{PG_SHADOW_PORT, PG_SOURCE_PORT, Ports, reserve_port, reserve_span}; + use std::fs; use std::io::Write; use std::net::TcpStream; diff --git a/tests/common/inproc_harness.rs b/tests/common/inproc_harness.rs index 1a10013..f28ac32 100644 --- a/tests/common/inproc_harness.rs +++ b/tests/common/inproc_harness.rs @@ -17,6 +17,11 @@ #![allow(dead_code)] +#[path = "ports.rs"] +mod ports; +#[allow(unused_imports)] +pub use ports::{PG_SHADOW_PORT, PG_SOURCE_PORT, Ports, reserve_port, reserve_span}; + use std::fs; use std::io::Write as _; use std::net::TcpStream; diff --git a/tests/common/ports.rs b/tests/common/ports.rs new file mode 100644 index 0000000..b745daf --- /dev/null +++ b/tests/common/ports.rs @@ -0,0 +1,132 @@ +//! Port handout for tests that spawn real servers. +//! +//! `cargo nextest` runs one process per test with many tests in flight, so +//! ports must be disjoint across processes, not just inside a binary. Two +//! rules follow: +//! +//! - Stay below Linux's ephemeral floor (`net.ipv4.ip_local_port_range` starts +//! at 32768), so an outbound connect cannot be holding a port a server is +//! about to bind. +//! - Reserve through a lock file held for the life of the process. A bind probe +//! alone leaves a window: `clickhouse server` takes seconds to bind, and a +//! sibling test probing that port meanwhile would find it free. Lock files +//! sit under `TMPDIR`, so concurrent test processes must share one. +//! +//! Postgres clusters here are socket-only (`listen_addresses = ''`) and PG keys +//! its SysV segment off the data dir inode (PG `src/backend/port/sysv_shmem.c`), +//! so a cluster's `port` only names a socket file inside a per-test temp dir. +//! One fixed number per role is therefore safe, no reservation needed. + +#![allow(dead_code)] + +use std::collections::HashSet; +use std::fs::{File, OpenOptions}; +use std::net::TcpListener; +use std::path::PathBuf; +use std::sync::Mutex; + +/// Socket-only cluster ports. Distinct per role so a test that puts source and +/// shadow in one socket dir still gets distinct socket files. +pub const PG_SOURCE_PORT: u16 = 5432; +pub const PG_SHADOW_PORT: u16 = 5433; + +const FLOOR: u16 = 17000; +const CEIL: u16 = 32000; + +/// Locks are never released mid-process: a freed port could be re-picked while +/// the server that owns it is still starting up. +static HELD: Mutex> = Mutex::new(Vec::new()); +/// Same rule inside one process, and it also covers the degraded no-lock-file +/// path where flock cannot speak for us. +static TAKEN: Mutex>> = Mutex::new(None); + +fn take(port: u16) -> bool { + TAKEN + .lock() + .unwrap() + .get_or_insert_with(HashSet::new) + .insert(port) +} + +fn lock_dir() -> PathBuf { + std::env::temp_dir().join("walshadow-test-ports") +} + +/// Lock file + bind probe. `None` when another live test process holds the +/// port, or anything else on the host has it bound. Inner `None` means the +/// lock dir is unusable (another user owns it), leaving the bind probe as the +/// only guard. +fn claim(port: u16) -> Option> { + if !take(port) { + return None; + } + let lock = OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(lock_dir().join(port.to_string())) + .ok(); + if let Some(f) = &lock { + f.try_lock().ok()?; + } + TcpListener::bind(("127.0.0.1", port)).ok()?; + Some(lock) +} + +/// Spread concurrent processes over the range so they don't all rescan the +/// same prefix. +fn scan_start() -> u16 { + let span = CEIL - FLOOR; + FLOOR + ((std::process::id() as u16).wrapping_mul(64) % span) +} + +/// First port of `len` consecutive free ports. ClickHouse derives its +/// interserver port as `http_port + 1`, so its pair must be adjacent. +pub fn reserve_span(len: u16) -> u16 { + assert!(len > 0); + let _ = std::fs::create_dir_all(lock_dir()); + let span = CEIL - FLOOR; + let start = scan_start(); + for offset in 0..span { + let base = FLOOR + (start - FLOOR + offset) % span; + if base + len > CEIL { + continue; + } + let claimed: Vec> = (base..base + len).map_while(claim).collect(); + if claimed.len() == len as usize { + HELD.lock().unwrap().extend(claimed.into_iter().flatten()); + return base; + } + } + panic!("no free port span of {len} in {FLOOR}..{CEIL}"); +} + +pub fn reserve_port() -> u16 { + reserve_span(1) +} + +/// Every real listener one cluster-plus-daemon drill needs. Field names match +/// what test bodies already read; `source` / `shadow` are socket-only. +#[derive(Clone, Copy, Debug)] +pub struct Ports { + pub source: u16, + pub shadow: u16, + pub ch_tcp: u16, + /// `ch_http + 1` is ClickHouse's interserver port, reserved alongside it. + pub ch_http: u16, + pub metrics: u16, + pub walsender: u16, +} + +impl Ports { + pub fn alloc() -> Self { + Self { + source: PG_SOURCE_PORT, + shadow: PG_SHADOW_PORT, + ch_tcp: reserve_port(), + ch_http: reserve_span(2), + metrics: reserve_port(), + walsender: reserve_port(), + } + } +} diff --git a/tests/composite_pkey.rs b/tests/composite_pkey.rs index 4a56ed2..e60084c 100644 --- a/tests/composite_pkey.rs +++ b/tests/composite_pkey.rs @@ -14,28 +14,6 @@ use walshadow::schema::RelName; use walshadow::shadow::Shadow; // walsender must clear ch_http by >1 (CH binds interserver = ch_http + 1). -const SLOT_BASIC: PortSlot = PortSlot { - source: 17700, - shadow: 17701, - ch_tcp: 17702, - ch_http: 17703, - walsender: 17707, -}; -const SLOT_TOAST: PortSlot = PortSlot { - source: 17710, - shadow: 17711, - ch_tcp: 17712, - ch_http: 17713, - walsender: 17717, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} const SCHEMA_SQL: &str = "CREATE SCHEMA ck;\n\ CREATE TABLE ck.t (a int, b int, val text, PRIMARY KEY (a, b));\n\ @@ -82,7 +60,7 @@ fn create_ch_dest(ch: &fx::ChServer) { } async fn run_drill( - slot: PortSlot, + slot: fx::Ports, app_name: &str, workload: &str, ) -> (Shadow, fx::ChServer, tempfile::TempDir) { @@ -163,7 +141,8 @@ async fn composite_pk_dedup_keys_on_full_tuple() { DELETE FROM ck.t WHERE a = 2 AND b = 1;\n\ COMMIT;\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_BASIC, "walshadow-composite-pk-basic", workload).await; + let (source, ch, _tmp) = + run_drill(fx::Ports::alloc(), "walshadow-composite-pk-basic", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!( @@ -197,7 +176,8 @@ async fn composite_pk_with_toast_value() { UPDATE ck.t SET val = repeat('z', 11000) WHERE a = 1 AND b = 2;\n\ DELETE FROM ck.t WHERE a = 1 AND b = 1;\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_TOAST, "walshadow-composite-pk-toast", workload).await; + let (source, ch, _tmp) = + run_drill(fx::Ports::alloc(), "walshadow-composite-pk-toast", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!( diff --git a/tests/control_plane_e2e.rs b/tests/control_plane_e2e.rs index f9141e4..f97c3e8 100644 --- a/tests/control_plane_e2e.rs +++ b/tests/control_plane_e2e.rs @@ -21,51 +21,6 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result, bail}; use walshadow::shadow::{Shadow, ShadowConfig}; -struct Ports { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - metrics: u16, - walsender: u16, -} - -// 17400-range: below the ephemeral range, clear of bootstrap_direct_ch -// (17300) and runtime_config_e2e (17700). CH's interserver port is -// ch_http + 1, so metrics/walsender dodge that slot. -const P1: Ports = Ports { - source: 17401, - shadow: 17402, - ch_tcp: 17409, - ch_http: 17410, - metrics: 17415, - walsender: 17416, -}; -const P2: Ports = Ports { - source: 17421, - shadow: 17422, - ch_tcp: 17429, - ch_http: 17430, - metrics: 17435, - walsender: 17436, -}; -const P3: Ports = Ports { - source: 17441, - shadow: 17442, - ch_tcp: 17449, - ch_http: 17450, - metrics: 17455, - walsender: 17456, -}; -const P4: Ports = Ports { - source: 17461, - shadow: 17462, - ch_tcp: 17469, - ch_http: 17470, - metrics: 17475, - walsender: 17476, -}; - /// Running daemon + its source PG + CH, with the paths the tests poke. struct Harness { _tmp: tempfile::TempDir, @@ -87,7 +42,7 @@ impl Harness { /// Bootstrap source + CH + daemon and block until the daemon's /// metrics port is up (bootstrap done, shadow serving, WAL pump in /// its main loop) and the seed row has drained to CH. - async fn up(ports: &Ports) -> Result { + async fn up(ports: &fx::Ports) -> Result { let tmp = tempfile::tempdir().unwrap(); // Source PG + schema. demo.users is pinned by the base config, @@ -418,7 +373,9 @@ async fn pause_resume_via_ctl_and_sighup_no_restart() { if !gated() { return; } - let mut h = Harness::up(&P1).await.expect("bring up harness"); + let mut h = Harness::up(&fx::Ports::alloc()) + .await + .expect("bring up harness"); let result = async { // Baseline: a WAL update flows to CH. @@ -492,7 +449,9 @@ async fn live_table_opt_in_auto_creates_on_reload() { if !gated() { return; } - let mut h = Harness::up(&P2).await.expect("bring up harness"); + let mut h = Harness::up(&fx::Ports::alloc()) + .await + .expect("bring up harness"); let result = async { // An existing table with a pre-opt-in row, absent from CH. @@ -562,7 +521,9 @@ async fn apply_preserves_previously_pinned_table() { if !gated() { return; } - let mut h = Harness::up(&P3).await.expect("bring up harness"); + let mut h = Harness::up(&fx::Ports::alloc()) + .await + .expect("bring up harness"); let result = async { h.psql("UPDATE demo.users SET email = 'before-select@x' WHERE id = 1")?; @@ -603,7 +564,9 @@ async fn pause_apply_resume_reroutes_backlog_whole() { if !gated() { return; } - let mut h = Harness::up(&P4).await.expect("bring up harness"); + let mut h = Harness::up(&fx::Ports::alloc()) + .await + .expect("bring up harness"); let result = async { // Unmapped table; id=1 plans (discards) before the pause. The users diff --git a/tests/copy_into.rs b/tests/copy_into.rs index 8a7693f..b095c64 100644 --- a/tests/copy_into.rs +++ b/tests/copy_into.rs @@ -24,11 +24,6 @@ use walshadow::mapping::ColumnMapping; use walshadow::mapping::TableTarget; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17421; -const SHADOW_PORT: u16 = 17422; -const CH_TCP_PORT: u16 = 17423; -const CH_HTTP_PORT: u16 = 17424; -const WALSENDER_PORT: u16 = 17452; const N_ROWS: u32 = 500; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] @@ -46,6 +41,7 @@ async fn copy_into_multi_insert_replicates() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -59,16 +55,16 @@ async fn copy_into_multi_insert_replicates() { "CREATE SCHEMA s14;\n\ CREATE TABLE s14.copy_t (id bigint PRIMARY KEY, name text NOT NULL);\n\ ALTER TABLE s14.copy_t REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -106,7 +102,7 @@ async fn copy_into_multi_insert_replicates() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-copy-into", ddl: None, diff --git a/tests/ddl_replicates.rs b/tests/ddl_replicates.rs index 362c9ca..afaa9f9 100644 --- a/tests/ddl_replicates.rs +++ b/tests/ddl_replicates.rs @@ -35,12 +35,7 @@ use walshadow::schema::RelName; // Each test shifts these by +0 / +10 / +20. The CH server's // `interserver_http_port = http_port + 1` so leave a 5-port gap -// between CH_HTTP_PORT and WALSENDER_PORT to avoid collision. -const SOURCE_PORT: u16 = 17461; -const SHADOW_PORT: u16 = 17462; -const CH_TCP_PORT: u16 = 17463; -const CH_HTTP_PORT: u16 = 17464; -const WALSENDER_PORT: u16 = 17468; +// between slot.ch_http and slot.walsender to avoid collision. #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn alter_add_column_replicates_without_toml_edit() { @@ -57,6 +52,7 @@ async fn alter_add_column_replicates_without_toml_edit() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -70,16 +66,16 @@ async fn alter_add_column_replicates_without_toml_edit() { "CREATE SCHEMA s15;\n\ CREATE TABLE s15.orders (id bigint PRIMARY KEY, payload text);\n\ ALTER TABLE s15.orders REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Pre-create CH dest with only the original two columns. Note: the @@ -121,7 +117,7 @@ async fn alter_add_column_replicates_without_toml_edit() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-ddl-alter-add", ddl: Some(fx::DdlPipelineArgs::default()), @@ -195,12 +191,8 @@ async fn create_table_auto_replicates_in_namespace() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 10; - let shadow_port = SHADOW_PORT + 10; - let ch_tcp_port = CH_TCP_PORT + 10; - let ch_http_port = CH_HTTP_PORT + 10; - let walsender_port = WALSENDER_PORT + 10; let ( fx::BootstrappedClusters { source, @@ -211,16 +203,16 @@ async fn create_table_auto_replicates_in_namespace() { ) = fx::bootstrap_clusters( &tmp, "CREATE SCHEMA s15ns;\n", - source_port, - shadow_port, - walsender_port, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -242,7 +234,7 @@ async fn create_table_auto_replicates_in_namespace() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-ddl-create-auto", ddl: Some(ddl_args), @@ -298,12 +290,8 @@ async fn drop_table_strategy_drop_removes_dest() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 20; - let shadow_port = SHADOW_PORT + 20; - let ch_tcp_port = CH_TCP_PORT + 20; - let ch_http_port = CH_HTTP_PORT + 20; - let walsender_port = WALSENDER_PORT + 20; let ( fx::BootstrappedClusters { source, @@ -314,16 +302,16 @@ async fn drop_table_strategy_drop_removes_dest() { ) = fx::bootstrap_clusters( &tmp, "CREATE SCHEMA s15drop;\n", - source_port, - shadow_port, - walsender_port, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -345,7 +333,7 @@ async fn drop_table_strategy_drop_removes_dest() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-ddl-drop", ddl: Some(ddl_args), @@ -400,13 +388,9 @@ async fn pinned_mapping_create_drop_create_recreates_dest() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); // +50, not +40: 1750x belongs to pipeline_parallel_e2e - let source_port = SOURCE_PORT + 50; - let shadow_port = SHADOW_PORT + 50; - let ch_tcp_port = CH_TCP_PORT + 50; - let ch_http_port = CH_HTTP_PORT + 50; - let walsender_port = WALSENDER_PORT + 50; let ( fx::BootstrappedClusters { source, @@ -418,16 +402,16 @@ async fn pinned_mapping_create_drop_create_recreates_dest() { &tmp, "CREATE SCHEMA s15pin;\n\ CREATE TABLE s15pin.t (id bigint PRIMARY KEY, body text);\n", - source_port, - shadow_port, - walsender_port, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Operator-managed dest for the pinned mapping (custom table name so @@ -472,7 +456,7 @@ async fn pinned_mapping_create_drop_create_recreates_dest() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-ddl-pinned-recreate", ddl: Some(ddl_args), @@ -540,12 +524,8 @@ async fn auto_create_honors_per_namespace_target_database() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 30; - let shadow_port = SHADOW_PORT + 30; - let ch_tcp_port = CH_TCP_PORT + 30; - let ch_http_port = CH_HTTP_PORT + 30; - let walsender_port = WALSENDER_PORT + 30; let ( fx::BootstrappedClusters { source, @@ -556,16 +536,16 @@ async fn auto_create_honors_per_namespace_target_database() { ) = fx::bootstrap_clusters( &tmp, "CREATE SCHEMA s15warehouse;\n", - source_port, - shadow_port, - walsender_port, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); // Global DB and the namespace's override DB both exist; the table // must land in the override, "warehouse". ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") @@ -590,7 +570,7 @@ async fn auto_create_honors_per_namespace_target_database() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", // global differs from the override - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-ddl-ns-target", ddl: Some(ddl_args), @@ -647,12 +627,8 @@ async fn create_table_auto_replicates_from_toml_namespace() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 60; - let shadow_port = SHADOW_PORT + 60; - let ch_tcp_port = CH_TCP_PORT + 60; - let ch_http_port = CH_HTTP_PORT + 60; - let walsender_port = WALSENDER_PORT + 60; let ( fx::BootstrappedClusters { source, @@ -663,16 +639,16 @@ async fn create_table_auto_replicates_from_toml_namespace() { ) = fx::bootstrap_clusters( &tmp, "CREATE SCHEMA s15toml;\n", - source_port, - shadow_port, - walsender_port, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -686,7 +662,7 @@ async fn create_table_auto_replicates_from_toml_namespace() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-ddl-create-auto-toml", ddl: Some(fx::DdlPipelineArgs::default()), diff --git a/tests/desc_log_e2e.rs b/tests/desc_log_e2e.rs index b4a3028..cd98d9f 100644 --- a/tests/desc_log_e2e.rs +++ b/tests/desc_log_e2e.rs @@ -13,36 +13,6 @@ use std::time::Duration; use walshadow::mapping::{ColumnMapping, TableTarget}; use walshadow::schema::RelName; -const SLOT_PREPARED: PortSlot = PortSlot { - source: 17960, - shadow: 17961, - ch_tcp: 17962, - ch_http: 17963, - walsender: 17967, -}; -const SLOT_RENAME: PortSlot = PortSlot { - source: 17970, - shadow: 17971, - ch_tcp: 17972, - ch_http: 17973, - walsender: 17977, -}; -const SLOT_INTERVAL: PortSlot = PortSlot { - source: 18020, - shadow: 18021, - ch_tcp: 18022, - ch_http: 18023, - walsender: 18027, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - /// Live 2PC: DDL inside a prepared xact reaches CH at COMMIT PREPARED. /// The commit record's header xid is the finishing backend's; the /// capture-keyed events live under the prepared xid (B2) — pre-fix the @@ -57,7 +27,7 @@ async fn prepared_ddl_drains_at_commit_prepared() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); return; } - let slot = SLOT_PREPARED; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -164,7 +134,7 @@ async fn schema_rename_reroutes_under_new_namespace() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); return; } - let slot = SLOT_RENAME; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -276,7 +246,7 @@ async fn in_place_intervals_compatible_and_ambiguous() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); return; } - let slot = SLOT_INTERVAL; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { diff --git a/tests/desc_log_restart_e2e.rs b/tests/desc_log_restart_e2e.rs index ffd8d47..7d41d2c 100644 --- a/tests/desc_log_restart_e2e.rs +++ b/tests/desc_log_restart_e2e.rs @@ -20,29 +20,6 @@ use walshadow::mapping::{ColumnMapping, TableTarget}; use walshadow::schema::RelName; use walshadow::shadow::Shadow; -const SLOT_COMMIT: PortSlot = PortSlot { - source: 17980, - shadow: 17981, - ch_tcp: 17982, - ch_http: 17983, - walsender: 17987, -}; -const SLOT_PREPARED: PortSlot = PortSlot { - source: 17990, - shadow: 17991, - ch_tcp: 17992, - ch_http: 17993, - walsender: 17997, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - /// psql session holding a transaction open across a pipeline restart; /// statements execute as lines arrive on stdin struct TxnSession { @@ -153,7 +130,7 @@ async fn rename_commit_after_restart_reroutes() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); return; } - let slot = SLOT_COMMIT; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -251,7 +228,7 @@ async fn prepared_rename_commit_after_restart_reroutes() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); return; } - let slot = SLOT_PREPARED; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { diff --git a/tests/dirty_admission_e2e.rs b/tests/dirty_admission_e2e.rs index aceb952..fdd6028 100644 --- a/tests/dirty_admission_e2e.rs +++ b/tests/dirty_admission_e2e.rs @@ -20,64 +20,6 @@ use std::time::Duration; use walshadow::mapping::NamespaceMapping; use walshadow::shadow::Shadow; -const SLOT_INTERLEAVE: PortSlot = PortSlot { - source: 18030, - shadow: 18031, - ch_tcp: 18032, - ch_http: 18033, - walsender: 18037, -}; -const SLOT_SUBXACT: PortSlot = PortSlot { - source: 18040, - shadow: 18041, - ch_tcp: 18042, - ch_http: 18043, - walsender: 18047, -}; -const SLOT_TOP_ABORT: PortSlot = PortSlot { - source: 18050, - shadow: 18051, - ch_tcp: 18052, - ch_http: 18053, - walsender: 18057, -}; -const SLOT_GATE: PortSlot = PortSlot { - source: 18060, - shadow: 18061, - ch_tcp: 18062, - ch_http: 18063, - walsender: 18067, -}; -const SLOT_CREATE_COPY: PortSlot = PortSlot { - source: 18070, - shadow: 18071, - ch_tcp: 18072, - ch_http: 18073, - walsender: 18077, -}; -const SLOT_BENIGN: PortSlot = PortSlot { - source: 18080, - shadow: 18081, - ch_tcp: 18082, - ch_http: 18083, - walsender: 18087, -}; -const SLOT_FENCE: PortSlot = PortSlot { - source: 18090, - shadow: 18091, - ch_tcp: 18092, - ch_http: 18093, - walsender: 18097, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - fn skip_gate() -> bool { if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); @@ -95,12 +37,12 @@ struct Drill { } /// Bootstrap clusters + CH + auto-create pipeline for one namespace. -async fn build_drill(slot: PortSlot, schema_sql: &str, namespace: &str, app_name: &str) -> Drill { +async fn build_drill(slot: fx::Ports, schema_sql: &str, namespace: &str, app_name: &str) -> Drill { build_drill_with(slot, schema_sql, namespace, app_name, |_| {}).await } async fn build_drill_with( - slot: PortSlot, + slot: fx::Ports, schema_sql: &str, namespace: &str, app_name: &str, @@ -179,7 +121,7 @@ async fn interleaved_clean_xact_unaffected_by_dirty_tree() { return; } let mut drill = build_drill( - SLOT_INTERLEAVE, + fx::Ports::alloc(), "CREATE SCHEMA dai;\n\ CREATE TABLE dai.dirty_t (id bigint PRIMARY KEY, v text);\n\ CREATE TABLE dai.clean_t (id bigint PRIMARY KEY, v text);\n", @@ -264,7 +206,7 @@ async fn subxact_catalog_touch_defers_top_and_child_rows() { return; } let mut drill = build_drill( - SLOT_SUBXACT, + fx::Ports::alloc(), "CREATE SCHEMA das;\n\ CREATE TABLE das.t (id bigint PRIMARY KEY, v text);\n", "das", @@ -334,7 +276,7 @@ async fn phase_gate_every_post_touch_record_defers_and_fences() { return; } let mut drill = build_drill( - SLOT_GATE, + fx::Ports::alloc(), "CREATE SCHEMA dag;\n\ CREATE TABLE dag.t (id bigint PRIMARY KEY, v text);\n\ ALTER TABLE dag.t REPLICA IDENTITY FULL;\n", @@ -425,7 +367,7 @@ async fn create_table_and_copy_same_xact_delivers() { return; } let mut drill = build_drill( - SLOT_CREATE_COPY, + fx::Ports::alloc(), "CREATE SCHEMA dac;\n", "dac", "walshadow-dirty-create-copy", @@ -484,7 +426,7 @@ async fn benign_in_place_alter_then_dml_delivers() { return; } let mut drill = build_drill( - SLOT_BENIGN, + fx::Ports::alloc(), "CREATE SCHEMA dab;\n\ CREATE TABLE dab.t (id bigint PRIMARY KEY, v varchar(10));\n", "dab", @@ -558,7 +500,7 @@ async fn physical_in_place_alter_fences_deferred_rows() { return; } let mut drill = build_drill_with( - SLOT_FENCE, + fx::Ports::alloc(), "CREATE SCHEMA daf;\n\ CREATE TABLE daf.t (id bigint PRIMARY KEY, v varchar(10));\n", "daf", @@ -599,7 +541,7 @@ async fn top_abort_with_ddl_appends_no_metadata_and_emits_no_rows() { return; } let mut drill = build_drill( - SLOT_TOP_ABORT, + fx::Ports::alloc(), "CREATE SCHEMA daa;\n\ CREATE TABLE daa.t (id bigint PRIMARY KEY, v text);\n", "daa", diff --git a/tests/emitter_budget_flush.rs b/tests/emitter_budget_flush.rs index e77084c..91f3af9 100644 --- a/tests/emitter_budget_flush.rs +++ b/tests/emitter_budget_flush.rs @@ -30,9 +30,6 @@ use walshadow::pipeline::batcher::{BatcherMsg, RoutedRow}; use walshadow::pipeline::{Fatal, tail}; use walshadow::schema::{RelAttr, RelDescriptor, RelName, ReplIdent}; -const CH_TCP_PORT: u16 = 17619; -const CH_HTTP_PORT: u16 = 17620; - const RFN: RelFileNode = RelFileNode { spc_node: 1663, db_node: 5, @@ -128,8 +125,9 @@ async fn budget_trips_seal_complete_inserts() { return; } + let slot = fx::Ports::alloc(); let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -147,7 +145,7 @@ async fn budget_trips_seal_complete_inserts() { // (2 budget trips + the final FlushAll). let cfg = EmitterConfig { host: "127.0.0.1".into(), - port: CH_TCP_PORT, + port: slot.ch_tcp, database: "walshadow_test".into(), compression: CompressionChoice::Lz4, row_budget: 2, diff --git a/tests/emitter_native_types.rs b/tests/emitter_native_types.rs index 8402d4f..2094dc3 100644 --- a/tests/emitter_native_types.rs +++ b/tests/emitter_native_types.rs @@ -28,9 +28,6 @@ use walshadow::pipeline::batcher::{BatcherMsg, RoutedRow}; use walshadow::pipeline::{Fatal, tail}; use walshadow::schema::{RelDescriptor, RelName, ReplIdent}; -const CH_TCP_PORT: u16 = 17629; -const CH_HTTP_PORT: u16 = 17630; - const RFN: RelFileNode = RelFileNode { spc_node: 1663, db_node: 5, @@ -63,8 +60,9 @@ async fn native_numeric_time_timetz_round_trip() { return; } + let slot = fx::Ports::alloc(); let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -83,7 +81,7 @@ async fn native_numeric_time_timetz_round_trip() { let cfg = EmitterConfig { host: "127.0.0.1".into(), - port: CH_TCP_PORT, + port: slot.ch_tcp, database: "walshadow_test".into(), compression: CompressionChoice::Lz4, ..Default::default() diff --git a/tests/emitter_tls.rs b/tests/emitter_tls.rs index 150b467..10b95fa 100644 --- a/tests/emitter_tls.rs +++ b/tests/emitter_tls.rs @@ -18,8 +18,11 @@ #![cfg(target_os = "linux")] +#[path = "common/ports.rs"] +mod ports; + use std::io; -use std::net::{TcpListener, TcpStream}; +use std::net::TcpStream; use std::os::unix::process::CommandExt as _; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; @@ -159,23 +162,6 @@ fn make_cert(dir: &Path) -> (PathBuf, PathBuf, Vec) { (p("cert.pem"), p("key.pem"), der) } -fn pick_ports() -> (u16, u16, u16, u16) { - let socks = [ - TcpListener::bind(("127.0.0.1", 0)).unwrap(), - TcpListener::bind(("127.0.0.1", 0)).unwrap(), - TcpListener::bind(("127.0.0.1", 0)).unwrap(), - TcpListener::bind(("127.0.0.1", 0)).unwrap(), - ]; - let ports = ( - socks[0].local_addr().unwrap().port(), - socks[1].local_addr().unwrap().port(), - socks[2].local_addr().unwrap().port(), - socks[3].local_addr().unwrap().port(), - ); - drop(socks); - ports -} - /// Spawned `clickhouse server` with a plaintext native port (readiness + /// verifying queries) and a TLS native port (the emitter's path). struct TlsChServer { @@ -195,7 +181,10 @@ impl TlsChServer { std::fs::create_dir_all(&log_dir).unwrap(); let (cert_pem, key_pem, ca_der) = make_cert(tmp.path()); - let (plain_port, secure_port, http_port, interserver_port) = pick_ports(); + let plain_port = ports::reserve_port(); + let secure_port = ports::reserve_port(); + let http_port = ports::reserve_port(); + let interserver_port = ports::reserve_port(); let child = Command::new("clickhouse") .args([ diff --git a/tests/foreign_database_e2e.rs b/tests/foreign_database_e2e.rs index e8f6f6b..8b7debb 100644 --- a/tests/foreign_database_e2e.rs +++ b/tests/foreign_database_e2e.rs @@ -14,12 +14,6 @@ use std::time::Duration; use walshadow::mapping::NamespaceMapping; -const SOURCE_PORT: u16 = 17561; -const SHADOW_PORT: u16 = 17562; -const CH_TCP_PORT: u16 = 17563; -const CH_HTTP_PORT: u16 = 17564; -const WALSENDER_PORT: u16 = 17568; - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn foreign_database_ddl_and_dml_never_reach_the_followed_output() { if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { @@ -27,6 +21,7 @@ async fn foreign_database_ddl_and_dml_never_reach_the_followed_output() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -39,16 +34,16 @@ async fn foreign_database_ddl_and_dml_never_reach_the_followed_output() { &tmp, "CREATE SCHEMA fdb;\n\ CREATE TABLE fdb.t (id bigint PRIMARY KEY, v text);\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -68,7 +63,7 @@ async fn foreign_database_ddl_and_dml_never_reach_the_followed_output() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-foreign-db", ddl: Some(ddl_args), diff --git a/tests/kill_restart.rs b/tests/kill_restart.rs index bd53c8d..ecb5cb0 100644 --- a/tests/kill_restart.rs +++ b/tests/kill_restart.rs @@ -1,8 +1,8 @@ //! `kill -9` mid-stream + restart drill (v1.0 acceptance §5). //! -//! Spawns a source PG + ClickHouse server once, then loops three cutoff -//! strategies × five seeded kill windows = 15 daemon spawn/kill/restart -//! cycles. Each cycle: +//! One test per cutoff strategy, each spawning its own source PG + +//! ClickHouse server once and looping five seeded kill windows = 15 +//! daemon spawn/kill/restart cycles across the three. Each cycle: //! //! 1. spawn `walshadow-stream` (basebackup-cloned shadow PG already //! wired in by `bootstrap_clusters_for_kill`) @@ -20,8 +20,8 @@ //! id)) matches source's //! //! `WALSHADOW_KILL_SEED` env seeds the LCG; unset → fixed 0xC11AC11A so -//! CI is reproducible. Per-(strategy, run) seed derivative shifts the -//! 250-750 ms kill window inside each strategy. +//! CI is reproducible. Strategy folds into the base seed and the per-run +//! derivative shifts the 250-750 ms kill window. //! //! Skipped silently when `initdb`, `pg_basebackup`, or the `clickhouse` //! multitool is absent. Linux-only — `Shadow` fixture is POSIX-style. @@ -48,21 +48,11 @@ use walshadow::pg::parse_pg_lsn; use walshadow::schema::RelName; use walshadow::shadow::{Shadow, ShadowConfig}; -// 17360-range — below the Linux ephemeral port range so outbound -// connects can't grab a port we're about to bind. CH's -// `interserver_http_port = http_port + 1` must dodge METRICS / WALSENDER. -const SOURCE_PORT: u16 = 17361; -const SHADOW_PORT: u16 = 17362; -const CH_TCP_PORT: u16 = 17369; -const CH_HTTP_PORT: u16 = 17370; -const METRICS_PORT: u16 = 17375; -const WALSENDER_PORT: u16 = 17376; - /// Fixed seed for CI reproducibility. Operators rotate locally via /// `WALSHADOW_KILL_SEED=...` to widen coverage. const DEFAULT_SEED: u64 = 0xC11AC11A; -/// Cutoff strategies — 5 seeded runs each. +/// Cutoff strategies — one test each, 5 seeded runs per test. #[derive(Clone, Copy, Debug)] enum Strategy { MidSegment, @@ -70,12 +60,6 @@ enum Strategy { PostCommit, } -const STRATEGIES: &[Strategy] = &[ - Strategy::MidSegment, - Strategy::MidXact, - Strategy::PostCommit, -]; - const RUNS_PER_STRATEGY: u32 = 5; /// Splitmix-style LCG; deterministic, no dep cost. @@ -192,7 +176,7 @@ impl DaemonFlags { "--host".into(), self.source_sock.to_string_lossy().into_owned(), "--port".into(), - SOURCE_PORT.to_string(), + fx::PG_SOURCE_PORT.to_string(), "--user".into(), "postgres".into(), "--dbname".into(), @@ -204,7 +188,7 @@ impl DaemonFlags { "--shadow-socket-dir".into(), self.shadow_sock.to_string_lossy().into_owned(), "--shadow-port".into(), - SHADOW_PORT.to_string(), + fx::PG_SHADOW_PORT.to_string(), "--shadow-user".into(), "postgres".into(), "--shadow-dbname".into(), @@ -268,7 +252,7 @@ async fn small_insert_loop( "-h", sock.to_str().unwrap(), "-p", - &SOURCE_PORT.to_string(), + &fx::PG_SOURCE_PORT.to_string(), "-U", "postgres", "-d", @@ -304,7 +288,7 @@ async fn large_xact(source_sock: PathBuf, start_id: i64) { "-h", source_sock.to_str().unwrap(), "-p", - &SOURCE_PORT.to_string(), + &fx::PG_SOURCE_PORT.to_string(), "-U", "postgres", "-d", @@ -378,11 +362,11 @@ async fn wait_for_ack_catchup( bail!("emitter_ack_lsn never reached {target:X} in {deadline:?}"); } -fn write_ch_config(ch_config_path: &Path) -> Result<()> { +fn write_ch_config(ch_config_path: &Path, ch_tcp_port: u16) -> Result<()> { fx::write_ch_config_toml( ch_config_path, "127.0.0.1", - CH_TCP_PORT, + ch_tcp_port, "default", &RelName::new("kr", "t"), &TableTarget::new("default", "kr_t"), @@ -390,7 +374,21 @@ fn write_ch_config(ch_config_path: &Path) -> Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn kill_restart_preserves_end_state() { +async fn kill_restart_mid_segment_preserves_end_state() { + drill(Strategy::MidSegment).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn kill_restart_mid_xact_preserves_end_state() { + drill(Strategy::MidXact).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn kill_restart_post_commit_preserves_end_state() { + drill(Strategy::PostCommit).await; +} + +async fn drill(strategy: Strategy) { if !fx::pg_available() { eprintln!("skip: no initdb on PATH"); return; @@ -404,21 +402,25 @@ async fn kill_restart_preserves_end_state() { return; } - if let Err(e) = run_drill().await { + if let Err(e) = run_drill(strategy).await { panic!("kill-restart drill failed: {e:#}"); } } -async fn run_drill() -> Result<()> { +async fn run_drill(strategy: Strategy) -> Result<()> { + // Strategy folded into the seed so the three tests walk different kill + // windows off one base seed. let seed_env = std::env::var("WALSHADOW_KILL_SEED") .ok() .and_then(|s| s.parse::().ok()) - .unwrap_or(DEFAULT_SEED); + .unwrap_or(DEFAULT_SEED) + ^ (strategy as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15); + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir()?; // 1. Source PG. - let source = make_pg(&tmp, "source", SOURCE_PORT); + let source = make_pg(&tmp, "source", fx::PG_SOURCE_PORT); source.initdb().context("initdb source")?; source.write_base_conf().context("source base conf")?; append_source_conf(&source).context("append source conf")?; @@ -448,24 +450,25 @@ async fn run_drill() -> Result<()> { fs::create_dir_all(&shadow_filter_dir)?; let shadow_sock = tmp.path().join("shadow-sock"); fs::create_dir_all(&shadow_sock)?; - rewrite_for_shadow(&shadow_data, SHADOW_PORT, &shadow_sock).context("retarget shadow")?; - enable_recovery(&shadow_data, &shadow_filter_dir, WALSENDER_PORT).context("recovery conf")?; + rewrite_for_shadow(&shadow_data, fx::PG_SHADOW_PORT, &shadow_sock) + .context("retarget shadow")?; + enable_recovery(&shadow_data, &shadow_filter_dir, slot.walsender).context("recovery conf")?; // Bridge worker outlives every daemon cycle: the daemon dials it at boot, // so each restart reconnects to the same preloaded worker fx::append_bridge_conf(&shadow_data, &shadow_sock, "postgres", fx::pgext_dir()) .context("preload bridge worker")?; let mut shadow_cfg = ShadowConfig::new(shadow_data.clone(), shadow_filter_dir.clone()); - shadow_cfg.port = SHADOW_PORT; + shadow_cfg.port = fx::PG_SHADOW_PORT; shadow_cfg.socket_dir = shadow_sock.clone(); shadow_cfg.ctl_timeout = Duration::from_secs(60); let shadow = Shadow::new(shadow_cfg); shadow.start().context("start shadow standby")?; let _shd_stop = fx::StopOnDrop { sh: &shadow }; - // 3. CH server + dest table (alive across all 15 daemon cycles). + // 3. CH server + dest table (alive across all 5 daemon cycles). let ch_tmp = tempfile::tempdir()?; - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).context("spawn ch")?; + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).context("spawn ch")?; fx::create_ch_dest_table(&ch, "default", "kr_t").context("create ch dest table")?; // 4. Daemon flags — identical across every spawn so kill / restart @@ -473,14 +476,14 @@ async fn run_drill() -> Result<()> { let spill_dir = tmp.path().join("spill"); fs::create_dir_all(&spill_dir)?; let ch_config_path = tmp.path().join("ch-config.toml"); - write_ch_config(&ch_config_path).context("write ch-config")?; + write_ch_config(&ch_config_path, slot.ch_tcp).context("write ch-config")?; let flags = DaemonFlags { source_sock: source.config().socket_dir.clone(), shadow_sock: shadow_sock.clone(), filter_dir: shadow_filter_dir.clone(), spill_dir: spill_dir.clone(), - metrics_addr: format!("127.0.0.1:{METRICS_PORT}").parse().unwrap(), - walsender_bind: format!("127.0.0.1:{WALSENDER_PORT}").parse().unwrap(), + metrics_addr: format!("127.0.0.1:{}", slot.metrics).parse().unwrap(), + walsender_bind: format!("127.0.0.1:{}", slot.walsender).parse().unwrap(), ch_config: ch_config_path.clone(), }; @@ -488,35 +491,33 @@ async fn run_drill() -> Result<()> { // window so concurrent inserts don't collide on the PK. let next_id = Arc::new(std::sync::atomic::AtomicI64::new(1)); - // 5. Drill loop: each (strategy, run) is one kill/restart cycle. + // 5. Drill loop: each run is one kill/restart cycle. let mut seed = seed_env; - for strategy in STRATEGIES { - for run in 0..RUNS_PER_STRATEGY { - let cycle_seed = next_seeded(&mut seed); - let kill_delay_ms = 250 + (cycle_seed % 500); - let stderr_path = tmp - .path() - .join(format!("daemon.{strategy:?}.{run}.stderr.log")); - - let outcome = run_cycle( - *strategy, - run, - kill_delay_ms, - &source, - &ch, - &flags, - &stderr_path, - next_id.clone(), - ) - .await; - - if let Err(e) = outcome { - let stderr_blob = fs::read_to_string(&stderr_path).unwrap_or_default(); - bail!( - "cycle {strategy:?}#{run} (seed={cycle_seed:#x}, kill_delay={kill_delay_ms}ms): {e:#}\n\ - --- daemon stderr ---\n{stderr_blob}", - ); - } + for run in 0..RUNS_PER_STRATEGY { + let cycle_seed = next_seeded(&mut seed); + let kill_delay_ms = 250 + (cycle_seed % 500); + let stderr_path = tmp + .path() + .join(format!("daemon.{strategy:?}.{run}.stderr.log")); + + let outcome = run_cycle( + strategy, + run, + kill_delay_ms, + &source, + &ch, + &flags, + &stderr_path, + next_id.clone(), + ) + .await; + + if let Err(e) = outcome { + let stderr_blob = fs::read_to_string(&stderr_path).unwrap_or_default(); + bail!( + "cycle {strategy:?}#{run} (seed={cycle_seed:#x}, kill_delay={kill_delay_ms}ms): {e:#}\n\ + --- daemon stderr ---\n{stderr_blob}", + ); } } diff --git a/tests/multixact_visibility.rs b/tests/multixact_visibility.rs index 36e3cab..20272c4 100644 --- a/tests/multixact_visibility.rs +++ b/tests/multixact_visibility.rs @@ -9,6 +9,9 @@ //! //! Skipped silently when `initdb` is not on `$PATH`. +#[path = "common/ports.rs"] +mod ports; + use std::process::Command; use std::time::Duration; @@ -53,7 +56,7 @@ fn multixact_updater_matches_live_pg() { return; } let tmp = tempfile::tempdir().unwrap(); - let sh = make_cluster(&tmp, 55731); + let sh = make_cluster(&tmp, ports::PG_SOURCE_PORT); sh.initdb().expect("initdb"); sh.write_base_conf().expect("conf"); sh.start().expect("start"); diff --git a/tests/ops.rs b/tests/ops.rs index 5fd1643..8aadb0f 100644 --- a/tests/ops.rs +++ b/tests/ops.rs @@ -15,6 +15,9 @@ //! `retention::tests::*`); their HTTP/file-system surfaces don't need a //! live PG to validate. +#[path = "common/ports.rs"] +mod ports; + use std::fs; use std::io::Write; use std::process::Command; @@ -27,15 +30,6 @@ use walshadow::preflight::{Inputs, PreflightError}; use walshadow::schema::RelName; use walshadow::shadow::{Shadow, ShadowConfig}; -// Non-overlapping ports so a leftover from an earlier failed run doesn't -// shadow the next start. -const SOURCE_PORT_A: u16 = 56301; -const SHADOW_PORT_A: u16 = 56302; -const SOURCE_PORT_B: u16 = 56303; -const SHADOW_PORT_B: u16 = 56304; -const SOURCE_PORT_C: u16 = 56305; -const SHADOW_PORT_C: u16 = 56306; - fn pg_available() -> bool { Command::new("initdb") .arg("--version") @@ -113,7 +107,7 @@ async fn preflight_rejects_wal_level_and_missing_replica_identity() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_pg(&tmp, "src-bad", SOURCE_PORT_A); + let source = make_pg(&tmp, "src-bad", ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("source base conf"); // wal_level=replica + keyless relations so both validators trip on @@ -131,16 +125,16 @@ async fn preflight_rejects_wal_level_and_missing_replica_identity() { ) .expect("schema"); - let shadow = make_pg(&tmp, "shd-bad", SHADOW_PORT_A); + let shadow = make_pg(&tmp, "shd-bad", ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb shadow"); shadow.write_base_conf().expect("shadow base conf"); shadow.start().expect("start shadow"); let _shd_stop = StopOnDrop { sh: &shadow }; - let src_sql = connect_sql(&source.config().socket_dir, SOURCE_PORT_A) + let src_sql = connect_sql(&source.config().socket_dir, ports::PG_SOURCE_PORT) .await .expect("source sql"); - let shd_sql = connect_sql(&shadow.config().socket_dir, SHADOW_PORT_A) + let shd_sql = connect_sql(&shadow.config().socket_dir, ports::PG_SHADOW_PORT) .await .expect("shadow sql"); @@ -198,23 +192,23 @@ async fn preflight_rejects_old_version_missing_slot_and_unknown_rel() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_pg(&tmp, "src-old", SOURCE_PORT_C); + let source = make_pg(&tmp, "src-old", ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("source base conf"); append_conf(&source, "logical"); source.start().expect("start source"); let _src_stop = StopOnDrop { sh: &source }; - let shadow = make_pg(&tmp, "shd-old", SHADOW_PORT_C); + let shadow = make_pg(&tmp, "shd-old", ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb shadow"); shadow.write_base_conf().expect("shadow base conf"); shadow.start().expect("start shadow"); let _shd_stop = StopOnDrop { sh: &shadow }; - let src_sql = connect_sql(&source.config().socket_dir, SOURCE_PORT_C) + let src_sql = connect_sql(&source.config().socket_dir, ports::PG_SOURCE_PORT) .await .expect("source sql"); - let shd_sql = connect_sql(&shadow.config().socket_dir, SHADOW_PORT_C) + let shd_sql = connect_sql(&shadow.config().socket_dir, ports::PG_SHADOW_PORT) .await .expect("shadow sql"); @@ -264,7 +258,7 @@ async fn preflight_passes_once_source_is_logical_and_relations_keyed() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_pg(&tmp, "src-ok", SOURCE_PORT_B); + let source = make_pg(&tmp, "src-ok", ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("source base conf"); append_conf(&source, "logical"); @@ -284,16 +278,16 @@ async fn preflight_passes_once_source_is_logical_and_relations_keyed() { ) .expect("schema"); - let shadow = make_pg(&tmp, "shd-ok", SHADOW_PORT_B); + let shadow = make_pg(&tmp, "shd-ok", ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb shadow"); shadow.write_base_conf().expect("shadow base conf"); shadow.start().expect("start shadow"); let _shd_stop = StopOnDrop { sh: &shadow }; - let src_sql = connect_sql(&source.config().socket_dir, SOURCE_PORT_B) + let src_sql = connect_sql(&source.config().socket_dir, ports::PG_SOURCE_PORT) .await .expect("source sql"); - let shd_sql = connect_sql(&shadow.config().socket_dir, SHADOW_PORT_B) + let shd_sql = connect_sql(&shadow.config().socket_dir, ports::PG_SHADOW_PORT) .await .expect("shadow sql"); diff --git a/tests/oracle.rs b/tests/oracle.rs index 46cd645..198c5b2 100644 --- a/tests/oracle.rs +++ b/tests/oracle.rs @@ -17,6 +17,9 @@ //! 4. `oracle_recovers_after_cluster_restart` — resolution fails while the //! cluster is down (counted `errors`), recovers once it is back. +#[path = "common/ports.rs"] +mod ports; + use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; @@ -31,7 +34,6 @@ use walshadow::oracle::{Oracle, resolve_pending_tuple}; use walshadow::schema::{INETOID, INTERVALOID, NUMERICOID}; use walshadow::shadow::{BridgeConf, Shadow, ShadowConfig}; -const SHADOW_PORT: u16 = 56301; /// int4 array, ie `INT4ARRAYOID` const INT4ARRAYOID: u32 = 1007; @@ -144,7 +146,7 @@ fn array_int4_1_2_3_bytes() -> Vec { #[tokio::test(flavor = "current_thread")] async fn oracle_resolves_tier3_disk_bytes() { let tmp = tempfile::tempdir().unwrap(); - let Some(guard) = start_pg(&tmp, SHADOW_PORT) else { + let Some(guard) = start_pg(&tmp, ports::PG_SHADOW_PORT) else { return; }; let oracle = oracle_on(&guard.sh).await; @@ -173,7 +175,7 @@ async fn oracle_resolves_tier3_disk_bytes() { #[tokio::test(flavor = "current_thread")] async fn oracle_falls_back_on_undecodable_bytes() { let tmp = tempfile::tempdir().unwrap(); - let Some(guard) = start_pg(&tmp, SHADOW_PORT + 1) else { + let Some(guard) = start_pg(&tmp, ports::PG_SHADOW_PORT) else { return; }; let oracle = oracle_on(&guard.sh).await; @@ -199,7 +201,7 @@ async fn oracle_falls_back_on_undecodable_bytes() { #[tokio::test(flavor = "current_thread")] async fn oracle_resolves_pg_pending_to_text() { let tmp = tempfile::tempdir().unwrap(); - let Some(guard) = start_pg(&tmp, SHADOW_PORT + 2) else { + let Some(guard) = start_pg(&tmp, ports::PG_SHADOW_PORT) else { return; }; let oracle = oracle_on(&guard.sh).await; @@ -257,7 +259,7 @@ async fn oracle_resolves_pg_pending_to_text() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn oracle_recovers_after_cluster_restart() { let tmp = tempfile::tempdir().unwrap(); - let Some(guard) = start_pg(&tmp, SHADOW_PORT + 3) else { + let Some(guard) = start_pg(&tmp, ports::PG_SHADOW_PORT) else { return; }; let oracle = oracle_on(&guard.sh).await; diff --git a/tests/oracle_types_e2e.rs b/tests/oracle_types_e2e.rs index da781d2..1f09b21 100644 --- a/tests/oracle_types_e2e.rs +++ b/tests/oracle_types_e2e.rs @@ -21,50 +21,6 @@ use walshadow::oracle::Oracle; use walshadow::schema::RelName; use walshadow::shadow::Shadow; -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - -const SLOT_ARR: PortSlot = PortSlot { - source: 17910, - shadow: 17911, - ch_tcp: 17912, - ch_http: 17913, - walsender: 17917, -}; -const SLOT_ENUM: PortSlot = PortSlot { - source: 17920, - shadow: 17921, - ch_tcp: 17922, - ch_http: 17923, - walsender: 17927, -}; -const SLOT_GEO: PortSlot = PortSlot { - source: 17930, - shadow: 17931, - ch_tcp: 17932, - ch_http: 17933, - walsender: 17937, -}; -const SLOT_VEC: PortSlot = PortSlot { - source: 17940, - shadow: 17941, - ch_tcp: 17942, - ch_http: 17943, - walsender: 17947, -}; -const SLOT_RIF: PortSlot = PortSlot { - source: 17950, - shadow: 17951, - ch_tcp: 17952, - ch_http: 17953, - walsender: 17957, -}; - fn skip_gate() -> bool { if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse"); @@ -95,7 +51,7 @@ fn col(attnum: i16, name: &str, ty: &str) -> ColumnMapping { } async fn run_oracle( - slot: PortSlot, + slot: fx::Ports, app_name: &str, schema_sql: &str, ch_create_sql: &str, @@ -175,7 +131,7 @@ async fn arrays_resolve_via_oracle() { return; } let (source, ch, _tmp) = run_oracle( - SLOT_ARR, + fx::Ports::alloc(), "walshadow-oracle-arrays", "CREATE TABLE public.arr (id int PRIMARY KEY, ints int[], texts text[], nums numeric[]);\n", "CREATE OR REPLACE TABLE walshadow_test.arr (\ @@ -226,7 +182,7 @@ async fn enums_resolve_via_oracle() { return; } let (source, ch, _tmp) = run_oracle( - SLOT_ENUM, + fx::Ports::alloc(), "walshadow-oracle-enums", "CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');\n\ CREATE TABLE public.en (id int PRIMARY KEY, m mood, ms mood[]);\n", @@ -271,7 +227,7 @@ async fn geometric_types_resolve_via_oracle() { return; } let (source, ch, _tmp) = run_oracle( - SLOT_GEO, + fx::Ports::alloc(), "walshadow-oracle-geo", "CREATE TABLE public.geo (\ id int PRIMARY KEY, p point, ln line, ls lseg, bx box, \ @@ -322,7 +278,7 @@ async fn pgvector_resolves_via_oracle() { return; } let (source, ch, _tmp) = run_oracle( - SLOT_VEC, + fx::Ports::alloc(), "walshadow-oracle-vector", "CREATE EXTENSION vector;\n\ CREATE TABLE public.vec (\ @@ -360,7 +316,7 @@ async fn array_update_under_rif_resolves_old_tuple() { return; } let (source, ch, _tmp) = run_oracle( - SLOT_RIF, + fx::Ports::alloc(), "walshadow-oracle-rif", "CREATE TABLE public.arr (id int PRIMARY KEY, ints int[]);\n\ ALTER TABLE public.arr REPLICA IDENTITY FULL;\n", diff --git a/tests/pending_capture_e2e.rs b/tests/pending_capture_e2e.rs index 1e9c55b..abd9fdc 100644 --- a/tests/pending_capture_e2e.rs +++ b/tests/pending_capture_e2e.rs @@ -33,36 +33,6 @@ use std::time::Duration; use walshadow::mapping::NamespaceMapping; use walshadow::shadow::Shadow; -const SLOT_COVERED: PortSlot = PortSlot { - source: 18100, - shadow: 18101, - ch_tcp: 18102, - ch_http: 18103, - walsender: 18107, -}; -const SLOT_SAVEPOINT: PortSlot = PortSlot { - source: 18110, - shadow: 18111, - ch_tcp: 18112, - ch_http: 18113, - walsender: 18117, -}; -const SLOT_BORN: PortSlot = PortSlot { - source: 18120, - shadow: 18121, - ch_tcp: 18122, - ch_http: 18123, - walsender: 18127, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - fn skip_gate() -> bool { if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse on PATH"); @@ -79,7 +49,7 @@ struct Drill { _tmp: tempfile::TempDir, } -async fn build_drill(slot: PortSlot, schema_sql: &str, app_name: &str) -> Drill { +async fn build_drill(slot: fx::Ports, schema_sql: &str, app_name: &str) -> Drill { let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -169,7 +139,7 @@ async fn pending_timeline_decodes_the_post_ddl_row() { return; } let mut drill = build_drill( - SLOT_COVERED, + fx::Ports::alloc(), "CREATE SCHEMA pc;\n\ CREATE TABLE pc.t (id bigint PRIMARY KEY, v varchar(10));\n", "walshadow-pending-covered", @@ -235,7 +205,12 @@ async fn relation_born_in_xact_keeps_a_slot_per_boundary() { if skip_gate() { return; } - let mut drill = build_drill(SLOT_BORN, "CREATE SCHEMA pc;\n", "walshadow-pending-born").await; + let mut drill = build_drill( + fx::Ports::alloc(), + "CREATE SCHEMA pc;\n", + "walshadow-pending-born", + ) + .await; let capture = capture_stats(&drill); let driver = spawn_txn( @@ -287,7 +262,7 @@ async fn savepoint_rollback_drops_its_pending_slots() { return; } let mut drill = build_drill( - SLOT_SAVEPOINT, + fx::Ports::alloc(), "CREATE SCHEMA pc;\n\ CREATE TABLE pc.t (id bigint PRIMARY KEY, v text);\n", "walshadow-pending-savepoint", diff --git a/tests/pgbench_acceptance.rs b/tests/pgbench_acceptance.rs index 5063d2f..e4c34ed 100644 --- a/tests/pgbench_acceptance.rs +++ b/tests/pgbench_acceptance.rs @@ -36,38 +36,6 @@ use std::time::Duration; use anyhow::{Context, Result}; use walshadow::shadow::{Shadow, ShadowConfig}; -// Port slots in the 17340 / 17360 ranges. Below the Linux ephemeral range -// so outbound connects can't grab a port we're about to bind. CH's -// `interserver_http_port = http_port + 1` must dodge metrics / walsender. -// Two disjoint sets so the 1/1 and 2/2 pool variants run concurrently. -#[derive(Clone, Copy)] -struct Ports { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - metrics: u16, - walsender: u16, -} - -const SERIAL_PORTS: Ports = Ports { - source: 17341, - shadow: 17342, - ch_tcp: 17349, - ch_http: 17350, - metrics: 17355, - walsender: 17356, -}; - -const POOLED_PORTS: Ports = Ports { - source: 17361, - shadow: 17362, - ch_tcp: 17369, - ch_http: 17370, - metrics: 17375, - walsender: 17376, -}; - fn pgbench_available() -> bool { Command::new("pgbench") .arg("--version") @@ -270,7 +238,12 @@ fn pgbench_init(source: &Shadow, scale: u32) -> Result<()> { /// concurrent `AsyncClient`s for both bootstrap and WAL under the DDL /// barrier, asserting out-of-order INSERTs across connections stay /// `_lsn`-correct (the parity oracle at the end). -async fn run_ddl_intermix(ports: Ports, decoder_pool: usize, inserter_pool: usize, label: &str) { +async fn run_ddl_intermix( + ports: fx::Ports, + decoder_pool: usize, + inserter_pool: usize, + label: &str, +) { if !fx::pg_available() { tracing::warn!("skip: no initdb on PATH"); return; @@ -645,7 +618,7 @@ async fn run_ddl_intermix(ports: Ports, decoder_pool: usize, inserter_pool: usiz #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn pgbench_acceptance_ddl_intermix() { - run_ddl_intermix(SERIAL_PORTS, 1, 1, "pgbench_acceptance_ddl_intermix").await; + run_ddl_intermix(fx::Ports::alloc(), 1, 1, "pgbench_acceptance_ddl_intermix").await; } /// Same drill at decoder/inserter pool 2/2 — the live daemon coverage for @@ -653,7 +626,13 @@ async fn pgbench_acceptance_ddl_intermix() { /// `pipeline_parallel_{e2e,ddl_e2e}` tests can't provide. #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn pgbench_acceptance_ddl_intermix_pooled() { - run_ddl_intermix(POOLED_PORTS, 2, 2, "pgbench_acceptance_ddl_intermix_pooled").await; + run_ddl_intermix( + fx::Ports::alloc(), + 2, + 2, + "pgbench_acceptance_ddl_intermix_pooled", + ) + .await; } /// Poll a `std::process::Child` until exit or timeout. Mirrors diff --git a/tests/pipeline_parallel_ddl_e2e.rs b/tests/pipeline_parallel_ddl_e2e.rs index b65a761..651cf28 100644 --- a/tests/pipeline_parallel_ddl_e2e.rs +++ b/tests/pipeline_parallel_ddl_e2e.rs @@ -35,11 +35,7 @@ use walshadow::schema::RelName; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn parallel_pipeline_schema_evolution_orders_after_data() { - const SOURCE_PORT: u16 = 17561; - const SHADOW_PORT: u16 = 17562; - const CH_TCP_PORT: u16 = 17563; - const CH_HTTP_PORT: u16 = 17564; - const WALSENDER_PORT: u16 = 17568; + let slot = fx::Ports::alloc(); if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse"); @@ -59,16 +55,16 @@ async fn parallel_pipeline_schema_evolution_orders_after_data() { "CREATE SCHEMA s19;\n\ CREATE TABLE s19.orders (id bigint PRIMARY KEY, payload text);\n\ ALTER TABLE s19.orders REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Pre-create the CH dest with only the original two columns — the @@ -110,7 +106,7 @@ async fn parallel_pipeline_schema_evolution_orders_after_data() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-parallel-ddl-alter", ddl: Some(fx::DdlPipelineArgs::default()), @@ -205,11 +201,7 @@ async fn parallel_pipeline_schema_evolution_orders_after_data() { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn parallel_pipeline_truncate_orders_after_data() { - const SOURCE_PORT: u16 = 17571; - const SHADOW_PORT: u16 = 17572; - const CH_TCP_PORT: u16 = 17573; - const CH_HTTP_PORT: u16 = 17574; - const WALSENDER_PORT: u16 = 17578; + let slot = fx::Ports::alloc(); if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse"); @@ -229,16 +221,16 @@ async fn parallel_pipeline_truncate_orders_after_data() { "CREATE SCHEMA s19t;\n\ CREATE TABLE s19t.t (id bigint PRIMARY KEY, payload text);\n\ ALTER TABLE s19t.t REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -278,7 +270,7 @@ async fn parallel_pipeline_truncate_orders_after_data() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-parallel-truncate", ddl: None, diff --git a/tests/pipeline_parallel_e2e.rs b/tests/pipeline_parallel_e2e.rs index 7a0f660..b8ff300 100644 --- a/tests/pipeline_parallel_e2e.rs +++ b/tests/pipeline_parallel_e2e.rs @@ -24,18 +24,6 @@ use walshadow::mapping::ColumnMapping; use walshadow::mapping::TableTarget; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17501; -const SHADOW_PORT: u16 = 17502; -const CH_TCP_PORT: u16 = 17503; -const CH_HTTP_PORT: u16 = 17504; -const WALSENDER_PORT: u16 = 17552; - -const SLICE_SOURCE_PORT: u16 = 17505; -const SLICE_SHADOW_PORT: u16 = 17506; -const SLICE_CH_TCP_PORT: u16 = 17507; -const SLICE_CH_HTTP_PORT: u16 = 17508; -const SLICE_WALSENDER_PORT: u16 = 17553; - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn parallel_pipeline_replicates_dml() { if !fx::pg_available() { @@ -51,6 +39,7 @@ async fn parallel_pipeline_replicates_dml() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -67,16 +56,16 @@ async fn parallel_pipeline_replicates_dml() { // exercise the decode pool's unmapped-relation skip counter. "CREATE TABLE public.foo (id int PRIMARY KEY, val text);\n\ CREATE TABLE public.bar (id int);\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -115,7 +104,7 @@ async fn parallel_pipeline_replicates_dml() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-pipeline-parallel", ddl: None, @@ -292,6 +281,7 @@ async fn parallel_pipeline_slices_multi_batch_commit() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -304,16 +294,16 @@ async fn parallel_pipeline_slices_multi_batch_commit() { &tmp, "CREATE TABLE public.slices (id int PRIMARY KEY, val text, meta text);\n\ ALTER TABLE public.slices ALTER COLUMN val SET STORAGE EXTERNAL;\n", - SLICE_SOURCE_PORT, - SLICE_SHADOW_PORT, - SLICE_WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, SLICE_CH_TCP_PORT, SLICE_CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -358,7 +348,7 @@ async fn parallel_pipeline_slices_multi_batch_commit() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLICE_CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-pipeline-slices", ddl: None, diff --git a/tests/pipeline_tail.rs b/tests/pipeline_tail.rs index bb7e2d2..d8de15f 100644 --- a/tests/pipeline_tail.rs +++ b/tests/pipeline_tail.rs @@ -29,14 +29,15 @@ async fn tail_finish_flushes_and_drains_clean() { return; } let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, 17960, 17961).expect("spawn ch"); + let ch_tcp = fx::reserve_port(); + let ch = fx::ChServer::spawn(ch_tmp, ch_tcp, fx::reserve_span(2)).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); let fatal = Fatal::new(); let stats = Arc::new(EmitterStats::default()); let emitter_ack = Arc::new(AtomicU64::new(0)); - let (msg_tx, ack, parts) = tail::spawn(&emitter(17960), 2, stats, emitter_ack, fatal.clone()) + let (msg_tx, ack, parts) = tail::spawn(&emitter(ch_tcp), 2, stats, emitter_ack, fatal.clone()) .await .expect("spawn tail"); @@ -53,14 +54,15 @@ async fn tail_finish_returns_fatal_message() { return; } let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, 17970, 17971).expect("spawn ch"); + let ch_tcp = fx::reserve_port(); + let ch = fx::ChServer::spawn(ch_tmp, ch_tcp, fx::reserve_span(2)).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); let fatal = Fatal::new(); let stats = Arc::new(EmitterStats::default()); let emitter_ack = Arc::new(AtomicU64::new(0)); - let (msg_tx, ack, parts) = tail::spawn(&emitter(17970), 2, stats, emitter_ack, fatal.clone()) + let (msg_tx, ack, parts) = tail::spawn(&emitter(ch_tcp), 2, stats, emitter_ack, fatal.clone()) .await .expect("spawn tail"); @@ -79,14 +81,15 @@ async fn tail_finish_fatal_during_drain() { return; } let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, 17980, 17981).expect("spawn ch"); + let ch_tcp = fx::reserve_port(); + let ch = fx::ChServer::spawn(ch_tmp, ch_tcp, fx::reserve_span(2)).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); let fatal = Fatal::new(); let stats = Arc::new(EmitterStats::default()); let emitter_ack = Arc::new(AtomicU64::new(0)); - let (msg_tx, ack, parts) = tail::spawn(&emitter(17980), 2, stats, emitter_ack, fatal.clone()) + let (msg_tx, ack, parts) = tail::spawn(&emitter(ch_tcp), 2, stats, emitter_ack, fatal.clone()) .await .expect("spawn tail"); diff --git a/tests/runtime_config_e2e.rs b/tests/runtime_config_e2e.rs index 3e34fc8..d1648c2 100644 --- a/tests/runtime_config_e2e.rs +++ b/tests/runtime_config_e2e.rs @@ -78,14 +78,6 @@ use walshadow::schema::RelName; const INSTALL_SQL: &str = include_str!("../sql/runtime_config_install.sql"); -// Each test shifts these by +0 / +10 / +20. CH `interserver_http_port = -// http_port + 1`, keep a gap before WALSENDER_PORT. -const SOURCE_PORT: u16 = 17701; -const SHADOW_PORT: u16 = 17702; -const CH_TCP_PORT: u16 = 17703; -const CH_HTTP_PORT: u16 = 17704; -const WALSENDER_PORT: u16 = 17708; - fn overlay_ddl_args() -> fx::DdlPipelineArgs { fx::DdlPipelineArgs { config_schema: Some("walshadow".into()), @@ -100,6 +92,7 @@ async fn opt_in_via_config_table_replicates_new_table() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let schema_sql = format!( "{INSTALL_SQL}\n\ @@ -113,12 +106,12 @@ async fn opt_in_via_config_table_replicates_new_table() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, SOURCE_PORT, SHADOW_PORT, WALSENDER_PORT).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -130,7 +123,7 @@ async fn opt_in_via_config_table_replicates_new_table() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-config-opt-in", ddl: Some(overlay_ddl_args()), @@ -187,12 +180,8 @@ async fn opt_out_mid_stream_drains_and_halts() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 10; - let shadow_port = SHADOW_PORT + 10; - let ch_tcp_port = CH_TCP_PORT + 10; - let ch_http_port = CH_HTTP_PORT + 10; - let walsender_port = WALSENDER_PORT + 10; let schema_sql = format!( "{INSTALL_SQL}\n\ CREATE SCHEMA app;\n\ @@ -205,12 +194,12 @@ async fn opt_out_mid_stream_drains_and_halts() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -248,7 +237,7 @@ async fn opt_out_mid_stream_drains_and_halts() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-config-opt-out", ddl: Some(overlay_ddl_args()), @@ -316,12 +305,8 @@ async fn forward_decl_materializes_on_create_table() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 20; - let shadow_port = SHADOW_PORT + 20; - let ch_tcp_port = CH_TCP_PORT + 20; - let ch_http_port = CH_HTTP_PORT + 20; - let walsender_port = WALSENDER_PORT + 20; let schema_sql = format!("{INSTALL_SQL}\nCREATE SCHEMA app;\n"); let ( fx::BootstrappedClusters { @@ -330,12 +315,12 @@ async fn forward_decl_materializes_on_create_table() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -346,7 +331,7 @@ async fn forward_decl_materializes_on_create_table() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-config-fwd-decl", ddl: Some(overlay_ddl_args()), @@ -423,12 +408,8 @@ async fn opt_in_non_empty_backfills_pre_opt_in_rows() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 30; - let shadow_port = SHADOW_PORT + 30; - let ch_tcp_port = CH_TCP_PORT + 30; - let ch_http_port = CH_HTTP_PORT + 30; - let walsender_port = WALSENDER_PORT + 30; // Rows land before the WAL stream ever starts, so COPY is the only path // that can carry them to CH. Column mix drives all three wire-decode // paths: int8/text/timestamptz native, numeric via ::text, jsonb cast. @@ -453,12 +434,12 @@ async fn opt_in_non_empty_backfills_pre_opt_in_rows() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -469,7 +450,7 @@ async fn opt_in_non_empty_backfills_pre_opt_in_rows() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-config-backfill", ddl: Some(overlay_ddl_args()), @@ -565,12 +546,8 @@ async fn opt_in_then_alter_add_column_reaches_ch() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 40; - let shadow_port = SHADOW_PORT + 40; - let ch_tcp_port = CH_TCP_PORT + 40; - let ch_http_port = CH_HTTP_PORT + 40; - let walsender_port = WALSENDER_PORT + 40; let schema_sql = format!( "{INSTALL_SQL}\n\ CREATE SCHEMA app;\n\ @@ -583,12 +560,12 @@ async fn opt_in_then_alter_add_column_reaches_ch() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -600,7 +577,7 @@ async fn opt_in_then_alter_add_column_reaches_ch() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-config-opt-in-alter", ddl: Some(overlay_ddl_args()), @@ -675,12 +652,8 @@ async fn auto_create_namespace_via_config_namespace() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 60; - let shadow_port = SHADOW_PORT + 60; - let ch_tcp_port = CH_TCP_PORT + 60; - let ch_http_port = CH_HTTP_PORT + 60; - let walsender_port = WALSENDER_PORT + 60; let schema_sql = format!("{INSTALL_SQL}\nCREATE SCHEMA app;\n"); let ( fx::BootstrappedClusters { @@ -689,12 +662,12 @@ async fn auto_create_namespace_via_config_namespace() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -706,7 +679,7 @@ async fn auto_create_namespace_via_config_namespace() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-config-ns-auto-create", ddl: Some(overlay_ddl_args()), @@ -770,12 +743,8 @@ async fn column_target_type_override_reaches_projection() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 50; - let shadow_port = SHADOW_PORT + 50; - let ch_tcp_port = CH_TCP_PORT + 50; - let ch_http_port = CH_HTTP_PORT + 50; - let walsender_port = WALSENDER_PORT + 50; let schema_sql = format!( "{INSTALL_SQL}\n\ CREATE SCHEMA app;\n\ @@ -788,12 +757,12 @@ async fn column_target_type_override_reaches_projection() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Operator-migrated dest type; the override is what makes the @@ -833,7 +802,7 @@ async fn column_target_type_override_reaches_projection() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-config-column-override", ddl: Some(overlay_ddl_args()), @@ -888,12 +857,8 @@ async fn pre_opt_in_xact_discards_post_opt_in_routes() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 70; - let shadow_port = SHADOW_PORT + 70; - let ch_tcp_port = CH_TCP_PORT + 70; - let ch_http_port = CH_HTTP_PORT + 70; - let walsender_port = WALSENDER_PORT + 70; let schema_sql = format!( "{INSTALL_SQL}\n\ CREATE SCHEMA app;\n\ @@ -906,12 +871,12 @@ async fn pre_opt_in_xact_discards_post_opt_in_routes() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, &schema_sql, source_port, shadow_port, walsender_port).await; + ) = fx::bootstrap_clusters(&tmp, &schema_sql, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -922,7 +887,7 @@ async fn pre_opt_in_xact_discards_post_opt_in_routes() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-config-pre-opt-in-discard", ddl: Some(overlay_ddl_args()), diff --git a/tests/schema_evolution_cdc.rs b/tests/schema_evolution_cdc.rs index 5ed82d1..5341871 100644 --- a/tests/schema_evolution_cdc.rs +++ b/tests/schema_evolution_cdc.rs @@ -11,43 +11,6 @@ use std::time::Duration; use walshadow::mapping::NamespaceMapping; use walshadow::shadow::Shadow; -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - -const SLOT_LOST: PortSlot = PortSlot { - source: 17820, - shadow: 17821, - ch_tcp: 17822, - ch_http: 17823, - walsender: 17827, -}; -const SLOT_CUTOFF: PortSlot = PortSlot { - source: 17840, - shadow: 17841, - ch_tcp: 17842, - ch_http: 17843, - walsender: 17847, -}; -const SLOT_NULLABLE: PortSlot = PortSlot { - source: 17850, - shadow: 17851, - ch_tcp: 17852, - ch_http: 17853, - walsender: 17857, -}; -const SLOT_NULLABLE_IDX: PortSlot = PortSlot { - source: 17860, - shadow: 17861, - ch_tcp: 17862, - ch_http: 17863, - walsender: 17867, -}; - fn skip_gate() -> bool { if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse"); @@ -57,7 +20,7 @@ fn skip_gate() -> bool { } async fn run( - slot: PortSlot, + slot: fx::Ports, ns: &str, app_name: &str, schema_sql: &str, @@ -136,7 +99,7 @@ async fn added_column_without_dml_visible_next_batch() { return; } let (source, shadow, ch, _tmp) = run( - SLOT_LOST, + fx::Ports::alloc(), "se_lost", "walshadow-se-lost", "CREATE SCHEMA se_lost;\n", @@ -169,7 +132,7 @@ async fn schema_change_on_one_table_spares_sibling() { return; } let (source, shadow, ch, _tmp) = run( - SLOT_CUTOFF, + fx::Ports::alloc(), "se_cut", "walshadow-se-cutoff", "CREATE SCHEMA se_cut;\n", @@ -210,7 +173,7 @@ async fn nullable_add_column_under_replident_full() { return; } let (source, shadow, ch, _tmp) = run( - SLOT_NULLABLE, + fx::Ports::alloc(), "se_null", "walshadow-se-nullable", "CREATE SCHEMA se_null;\n", @@ -262,7 +225,7 @@ async fn nullable_add_column_under_replident_index() { return; } let (source, shadow, ch, _tmp) = run( - SLOT_NULLABLE_IDX, + fx::Ports::alloc(), "se_idx", "walshadow-se-nullable-idx", "CREATE SCHEMA se_idx;\n", diff --git a/tests/schema_evolution_pinned.rs b/tests/schema_evolution_pinned.rs index 1f58e0c..3c058d9 100644 --- a/tests/schema_evolution_pinned.rs +++ b/tests/schema_evolution_pinned.rs @@ -35,14 +35,6 @@ use walshadow::mapping::ColumnMapping; use walshadow::mapping::TableTarget; use walshadow::schema::RelName; -// +0 / +10 shift per test. CH interserver_http_port = http_port + 1, so -// leave a 5-port gap between CH_HTTP_PORT and WALSENDER_PORT. -const SOURCE_PORT: u16 = 17541; -const SHADOW_PORT: u16 = 17542; -const CH_TCP_PORT: u16 = 17543; -const CH_HTTP_PORT: u16 = 17544; -const WALSENDER_PORT: u16 = 17548; - fn skip_if_missing() -> bool { if !fx::pg_available() || !fx::pg_basebackup_available() || !fx::clickhouse_available() { eprintln!("skip: missing initdb / pg_basebackup / clickhouse"); @@ -83,6 +75,7 @@ async fn pinned_alter_add_column_replicates_without_priming_dml() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -100,16 +93,16 @@ async fn pinned_alter_add_column_replicates_without_priming_dml() { email text\n\ );\n\ ALTER TABLE demo.users REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Pinned dest holds only id/name/email — signup_ts does NOT exist @@ -137,7 +130,7 @@ async fn pinned_alter_add_column_replicates_without_priming_dml() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-pinned-ddl", ddl: Some(fx::DdlPipelineArgs::default()), @@ -201,12 +194,8 @@ async fn pinned_subset_alter_adds_only_new_column() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); - let source_port = SOURCE_PORT + 10; - let shadow_port = SHADOW_PORT + 10; - let ch_tcp_port = CH_TCP_PORT + 10; - let ch_http_port = CH_HTTP_PORT + 10; - let walsender_port = WALSENDER_PORT + 10; let ( fx::BootstrappedClusters { source, @@ -225,16 +214,16 @@ async fn pinned_subset_alter_adds_only_new_column() { internal_notes text\n\ );\n\ ALTER TABLE demo.users REPLICA IDENTITY FULL;\n", - source_port, - shadow_port, - walsender_port, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, ch_tcp_port, ch_http_port).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Dest deliberately omits `internal_notes`. The diff against the @@ -264,7 +253,7 @@ async fn pinned_subset_alter_adds_only_new_column() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-pinned-subset", ddl: Some(fx::DdlPipelineArgs::default()), diff --git a/tests/shadow_catalog.rs b/tests/shadow_catalog.rs index aa1f594..37bade0 100644 --- a/tests/shadow_catalog.rs +++ b/tests/shadow_catalog.rs @@ -3,8 +3,10 @@ //! that feeds descriptor-log capture. //! //! Skipped silently if `initdb` is not on `$PATH`. Each test spins up a -//! fresh data directory under a tempdir; tests pick non-overlapping -//! ports so cargo's parallel runner doesn't collide them. +//! fresh data directory under a tempdir. + +#[path = "common/ports.rs"] +mod ports; use std::process::Command; use std::sync::Arc; @@ -118,7 +120,7 @@ async fn user_relation_lookup_by_name() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55602); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); @@ -170,7 +172,7 @@ async fn replay_lsn_gate_times_out_when_not_in_recovery() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55603); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); @@ -199,7 +201,7 @@ async fn catalog_reconnects_after_pg_restart() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55605); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); @@ -239,7 +241,7 @@ async fn with_transient_retry_outlasts_a_pg_restart() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55606); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); @@ -315,7 +317,7 @@ async fn dropped_column_keeps_physical_slot() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55613); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); @@ -385,7 +387,7 @@ async fn replident_matrix_default_nothing_full_index() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55609); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); @@ -489,7 +491,7 @@ async fn fetch_all_descriptors_covers_eligible_kinds() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55601); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow.start().expect("start"); diff --git a/tests/shadow_lifecycle.rs b/tests/shadow_lifecycle.rs index 8f6149c..b0b3320 100644 --- a/tests/shadow_lifecycle.rs +++ b/tests/shadow_lifecycle.rs @@ -11,6 +11,9 @@ //! enable standby recovery → start → wait for replay LSN to exist //! → probe in-recovery true → stop. +#[path = "common/ports.rs"] +mod ports; + use std::process::Command; use std::time::Duration; @@ -41,7 +44,7 @@ fn normal_mode_lifecycle() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55501); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("write conf"); @@ -74,7 +77,7 @@ fn standby_mode_lifecycle() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55502); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("write conf"); @@ -140,7 +143,7 @@ fn restore_command_filename_is_segment_relative() { return; } let tmp = tempfile::tempdir().unwrap(); - let shadow = make_shadow(&tmp, 55503); + let shadow = make_shadow(&tmp, ports::PG_SHADOW_PORT); shadow.initdb().expect("initdb"); shadow.write_base_conf().expect("conf"); shadow diff --git a/tests/soft_delete_cdc.rs b/tests/soft_delete_cdc.rs index efb6681..23a8f93 100644 --- a/tests/soft_delete_cdc.rs +++ b/tests/soft_delete_cdc.rs @@ -14,56 +14,6 @@ use walshadow::schema::RelName; use walshadow::shadow::Shadow; // walsender must clear ch_http by >1 (CH binds interserver = ch_http + 1). -const SLOT_IUD: PortSlot = PortSlot { - source: 17720, - shadow: 17721, - ch_tcp: 17722, - ch_http: 17723, - walsender: 17727, -}; -const SLOT_UD: PortSlot = PortSlot { - source: 17730, - shadow: 17731, - ch_tcp: 17732, - ch_http: 17733, - walsender: 17737, -}; -const SLOT_RESURRECT: PortSlot = PortSlot { - source: 17740, - shadow: 17741, - ch_tcp: 17742, - ch_http: 17743, - walsender: 17747, -}; -const SLOT_BIGXACT: PortSlot = PortSlot { - source: 18000, - shadow: 18001, - ch_tcp: 18002, - ch_http: 18003, - walsender: 18007, -}; -const SLOT_BASIC: PortSlot = PortSlot { - source: 17800, - shadow: 17801, - ch_tcp: 17802, - ch_http: 17803, - walsender: 17807, -}; -const SLOT_META: PortSlot = PortSlot { - source: 17810, - shadow: 17811, - ch_tcp: 17812, - ch_http: 17813, - walsender: 17817, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} const SCHEMA_SQL: &str = "CREATE SCHEMA sd;\n\ CREATE TABLE sd.t (id int PRIMARY KEY, val text);\n\ @@ -104,7 +54,7 @@ fn create_ch_dest(ch: &fx::ChServer) { } async fn run_drill( - slot: PortSlot, + slot: fx::Ports, app_name: &str, workload: &str, ) -> (Shadow, fx::ChServer, tempfile::TempDir) { @@ -112,7 +62,7 @@ async fn run_drill( } async fn run_drill_with( - slot: PortSlot, + slot: fx::Ports, app_name: &str, workload: &str, tune: impl FnOnce(&mut walshadow::ch_emitter::EmitterConfig), @@ -195,7 +145,7 @@ async fn iud_same_batch_collapses_to_tombstone() { DELETE FROM sd.t WHERE id = 1;\n\ COMMIT;\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_IUD, "walshadow-sd-iud", workload).await; + let (source, ch, _tmp) = run_drill(fx::Ports::alloc(), "walshadow-sd-iud", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!(source.psql_one("SELECT count(*) FROM sd.t").unwrap(), "0"); @@ -215,7 +165,7 @@ async fn ud_same_batch_collapses_to_tombstone() { DELETE FROM sd.t WHERE id = 1;\n\ COMMIT;\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_UD, "walshadow-sd-ud", workload).await; + let (source, ch, _tmp) = run_drill(fx::Ports::alloc(), "walshadow-sd-ud", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!(source.psql_one("SELECT count(*) FROM sd.t").unwrap(), "0"); @@ -232,7 +182,8 @@ async fn insert_after_delete_resurrects_key() { DELETE FROM sd.t WHERE id = 1;\n\ INSERT INTO sd.t VALUES (1, 'z');\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_RESURRECT, "walshadow-sd-resurrect", workload).await; + let (source, ch, _tmp) = + run_drill(fx::Ports::alloc(), "walshadow-sd-resurrect", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!(source.psql_one("SELECT count(*) FROM sd.t").unwrap(), "1"); @@ -258,7 +209,7 @@ async fn basic_insert_update_delete_ends_as_tombstone() { UPDATE sd.t SET val = 'b' WHERE id = 1;\n\ DELETE FROM sd.t WHERE id = 1;\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_BASIC, "walshadow-sd-basic", workload).await; + let (source, ch, _tmp) = run_drill(fx::Ports::alloc(), "walshadow-sd-basic", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!(source.psql_one("SELECT count(*) FROM sd.t").unwrap(), "0"); @@ -281,11 +232,15 @@ async fn large_single_xact_triggers_mid_loop_chunk_flush() { INSERT INTO sd.t (id, val) SELECT g, 'v' FROM generate_series(1, 40) AS g;\n\ COMMIT;\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = - run_drill_with(SLOT_BIGXACT, "walshadow-sd-bigxact", workload, |cfg| { + let (source, ch, _tmp) = run_drill_with( + fx::Ports::alloc(), + "walshadow-sd-bigxact", + workload, + |cfg| { cfg.decode_chunk_rows = 16; - }) - .await; + }, + ) + .await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!(source.psql_one("SELECT count(*) FROM sd.t").unwrap(), "40"); @@ -305,7 +260,7 @@ async fn metadata_columns_are_populated() { DELETE FROM sd.t WHERE id = 1;\n\ INSERT INTO sd.t VALUES (2, 'b');\n\ SELECT pg_switch_wal();\n"; - let (source, ch, _tmp) = run_drill(SLOT_META, "walshadow-sd-meta", workload).await; + let (source, ch, _tmp) = run_drill(fx::Ports::alloc(), "walshadow-sd-meta", workload).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!(winning_flag(&ch, 1), "true", "deleted key is a tombstone"); diff --git a/tests/source_reconnect.rs b/tests/source_reconnect.rs index 28e7e46..2773f89 100644 --- a/tests/source_reconnect.rs +++ b/tests/source_reconnect.rs @@ -4,6 +4,9 @@ //! //! Skipped silently when `initdb`/`psql` are absent. +#[path = "common/ports.rs"] +mod ports; + use std::fs; use std::io::Write as _; use std::process::Command; @@ -96,7 +99,7 @@ async fn reconnect_resumes_after_walsender_terminated() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55731); + let source = make_source(&tmp, ports::PG_SOURCE_PORT); source.initdb().unwrap(); source.write_base_conf().unwrap(); append_conf(&source, &[]); @@ -152,7 +155,7 @@ async fn recycled_segment_surfaces_58p01() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55732); + let source = make_source(&tmp, ports::PG_SOURCE_PORT); source.initdb().unwrap(); source.write_base_conf().unwrap(); append_conf( @@ -214,7 +217,7 @@ async fn slot_prevents_segment_recycle() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55733); + let source = make_source(&tmp, ports::PG_SOURCE_PORT); source.initdb().unwrap(); source.write_base_conf().unwrap(); append_conf( diff --git a/tests/subxact.rs b/tests/subxact.rs index 6de7e8c..dc6a6e4 100644 --- a/tests/subxact.rs +++ b/tests/subxact.rs @@ -28,74 +28,6 @@ use walshadow::mapping::{ColumnMapping, NamespaceMapping}; use walshadow::schema::RelName; use walshadow::shadow::Shadow; -// Each test owns a disjoint port slot. Cargo's default test runner -// parallelises tests within a binary, so reusing slots would collide -// on the source PG / shadow PG / CH listener. -const SLOT_ROLLBACK: PortSlot = PortSlot { - source: 17430, - shadow: 17431, - ch_tcp: 17432, - ch_http: 17433, - walsender: 17460, -}; -const SLOT_RELEASE: PortSlot = PortSlot { - source: 17434, - shadow: 17435, - ch_tcp: 17436, - ch_http: 17437, - walsender: 17461, -}; -const SLOT_NESTED: PortSlot = PortSlot { - source: 17438, - shadow: 17439, - ch_tcp: 17440, - ch_http: 17441, - walsender: 17462, -}; -const SLOT_TOP_ABORT: PortSlot = PortSlot { - source: 17442, - shadow: 17443, - ch_tcp: 17444, - ch_http: 17445, - walsender: 17463, -}; -const SLOT_TOAST_ROLLBACK: PortSlot = PortSlot { - source: 17780, - shadow: 17781, - ch_tcp: 17782, - ch_http: 17783, - walsender: 17787, -}; -const SLOT_IUD_ABORT: PortSlot = PortSlot { - source: 17790, - shadow: 17791, - ch_tcp: 17792, - ch_http: 17793, - walsender: 17797, -}; -const SLOT_SP_DDL: PortSlot = PortSlot { - source: 17870, - shadow: 17871, - ch_tcp: 17872, - ch_http: 17873, - walsender: 17877, -}; -const SLOT_ASSIGN: PortSlot = PortSlot { - source: 17880, - shadow: 17881, - ch_tcp: 17882, - ch_http: 17883, - walsender: 17887, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - /// Single mapping shape every subxact test reuses. fn mapping() -> Vec { vec![fx::TableMappingSpec { @@ -138,7 +70,7 @@ fn create_ch_dest(ch: &fx::ChServer) { /// Shared drill — bootstrap clusters, pump the workload, then return /// the (source, ch) handles to the caller for assertions. async fn run_drill std::thread::JoinHandle<()>>( - slot: PortSlot, + slot: fx::Ports, app_name: &str, spawn_driver: F, ) -> (Shadow, fx::ChServer, tempfile::TempDir, tempfile::TempDir) { @@ -244,7 +176,7 @@ async fn savepoint_rollback_discards_subxact_writes() { ) }; let (source, ch, _tmp1, _tmp2) = - run_drill(SLOT_ROLLBACK, "walshadow-subxact-rollback", driver).await; + run_drill(fx::Ports::alloc(), "walshadow-subxact-rollback", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; let src_count = source @@ -272,7 +204,7 @@ async fn savepoint_release_commits_subxact_writes() { ) }; let (source, ch, _tmp1, _tmp2) = - run_drill(SLOT_RELEASE, "walshadow-subxact-release", driver).await; + run_drill(fx::Ports::alloc(), "walshadow-subxact-release", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; let src_count = source @@ -304,7 +236,7 @@ async fn orm_nested_savepoints_only_post_savepoint_writes_survive() { ) }; let (source, ch, _tmp1, _tmp2) = - run_drill(SLOT_NESTED, "walshadow-subxact-nested", driver).await; + run_drill(fx::Ports::alloc(), "walshadow-subxact-nested", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; let src_count = source @@ -339,7 +271,7 @@ async fn top_abort_with_subxacts_discards_everything() { ) }; let (source, ch, _tmp1, _tmp2) = - run_drill(SLOT_TOP_ABORT, "walshadow-subxact-top-abort", driver).await; + run_drill(fx::Ports::alloc(), "walshadow-subxact-top-abort", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; let src_count = source @@ -370,7 +302,7 @@ async fn toast_value_in_rolled_back_subxact_is_discarded() { ) }; let (source, ch, _tmp1, _tmp2) = - run_drill(SLOT_TOAST_ROLLBACK, "walshadow-subxact-toast", driver).await; + run_drill(fx::Ports::alloc(), "walshadow-subxact-toast", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!( @@ -421,7 +353,7 @@ async fn iud_in_aborted_top_xact_leaves_rows_untouched() { ) }; let (source, ch, _tmp1, _tmp2) = - run_drill(SLOT_IUD_ABORT, "walshadow-subxact-iud-abort", driver).await; + run_drill(fx::Ports::alloc(), "walshadow-subxact-iud-abort", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!( @@ -442,7 +374,7 @@ async fn savepoint_after_ddl_rollback_discards_column_and_rows() { if skip_gate() { return; } - let slot = SLOT_SP_DDL; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -554,7 +486,8 @@ async fn many_subxacts_emit_assignment_record() { sql.push_str("COMMIT;\nSELECT pg_switch_wal();\n"); let driver = |source: &Shadow| spawn_txn(source, &sql); - let (source, ch, _t1, _t2) = run_drill(SLOT_ASSIGN, "walshadow-subxact-assign", driver).await; + let (source, ch, _t1, _t2) = + run_drill(fx::Ports::alloc(), "walshadow-subxact-assign", driver).await; let _src_stop = fx::StopOnDrop { sh: &source }; assert_eq!( diff --git a/tests/toast_e2e.rs b/tests/toast_e2e.rs index bab8753..112cdd9 100644 --- a/tests/toast_e2e.rs +++ b/tests/toast_e2e.rs @@ -26,13 +26,6 @@ use walshadow::mapping::TableTarget; use walshadow::mapping::ToastMode; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17591; -const SHADOW_PORT: u16 = 17592; -const CH_TCP_PORT: u16 = 17593; -const CH_HTTP_PORT: u16 = 17594; -// 17595 reserved: ChServer interserver port = http + 1 -const WALSENDER_PORT: u16 = 17596; - /// 16 bytes * 512 = 8192, comfortably past the ~2KB toast threshold and /// spanning multiple ~2KB toast chunks. const BODY_SQL: &str = "repeat('walshadow-toast-', 512)"; @@ -48,6 +41,7 @@ async fn replident_full_unchanged_toast_update() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -63,16 +57,16 @@ async fn replident_full_unchanged_toast_update() { "CREATE TABLE public.doc (id int PRIMARY KEY, meta text, body text);\n\ ALTER TABLE public.doc ALTER COLUMN body SET STORAGE EXTERNAL;\n\ ALTER TABLE public.doc REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -117,7 +111,7 @@ async fn replident_full_unchanged_toast_update() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-toast-rif", ddl: None, diff --git a/tests/toast_resolvers.rs b/tests/toast_resolvers.rs index f32babc..cec8216 100644 --- a/tests/toast_resolvers.rs +++ b/tests/toast_resolvers.rs @@ -35,8 +35,6 @@ fn assembled(body: &[u8]) -> FetchedValue { FetchedValue::Assembled(body.to_vec()) } -const CH_TCP_PORT: u16 = 17639; -const CH_HTTP_PORT: u16 = 17640; const DB: &str = "walshadow_toast_test"; fn row(relid: u32, value_id: u32, seq: u32, tid: (u32, u16), lsn: u64, body: &[u8]) -> ToastRow { @@ -107,8 +105,9 @@ async fn ch_chunk_store_put_fetch_roundtrip() { eprintln!("skip: no clickhouse binary on PATH"); return; } + let slot = fx::Ports::alloc(); let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query(&format!("CREATE DATABASE IF NOT EXISTS {DB}")) .expect("create db"); @@ -370,9 +369,9 @@ async fn ch_chunk_store_rewrite_barrier_residuals() { eprintln!("skip: no clickhouse binary on PATH"); return; } + let slot = fx::Ports::alloc(); let ch_tmp = tempfile::tempdir().unwrap(); - // +8: sibling tests' interserver ports are their http_port + 1 - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT + 8, CH_HTTP_PORT + 8).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query(&format!("CREATE DATABASE IF NOT EXISTS {DB}")) .expect("create db"); @@ -450,9 +449,9 @@ async fn ch_resolver_put_rows_then_fetch_into() { eprintln!("skip: no clickhouse binary on PATH"); return; } + let slot = fx::Ports::alloc(); let ch_tmp = tempfile::tempdir().unwrap(); - // +4: sibling test's interserver port is its http_port + 1, so +2 collides - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT + 4, CH_HTTP_PORT + 4).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query(&format!("CREATE DATABASE IF NOT EXISTS {DB}")) .expect("create db"); diff --git a/tests/toast_rewrite_e2e.rs b/tests/toast_rewrite_e2e.rs index 2f20bb0..9065c4c 100644 --- a/tests/toast_rewrite_e2e.rs +++ b/tests/toast_rewrite_e2e.rs @@ -39,14 +39,6 @@ use walshadow::mapping::ToastMode; use walshadow::mapping::{ColumnMapping, TableTarget}; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17731; -const SHADOW_PORT: u16 = 17732; -const CH_TCP_PORT: u16 = 17733; -const CH_HTTP_PORT: u16 = 17734; -// 17735 reserved: ChServer interserver port = http + 1 -const WALSENDER_PORT: u16 = 17736; -// alter_rewrite_link_swap_retires_old_mirror shifts every port +10 - /// Distinct byte sums identify values in the mirror (EXTERNAL storage keeps /// them uncompressed, so mirror bytes == raw length). const BODY_A_SQL: &str = "repeat('a-value-dies-first!!', 512)"; // 10240 @@ -88,6 +80,7 @@ async fn vacuum_full_rewrite_and_same_xact_stash() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -100,16 +93,16 @@ async fn vacuum_full_rewrite_and_same_xact_stash() { &tmp, "CREATE TABLE public.doc (id int PRIMARY KEY, meta text, body text);\n\ ALTER TABLE public.doc ALTER COLUMN body SET STORAGE EXTERNAL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); let toast_relid = source @@ -171,7 +164,7 @@ async fn vacuum_full_rewrite_and_same_xact_stash() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-toast-rewrite", ddl: Some(fx::DdlPipelineArgs::default()), @@ -479,6 +472,7 @@ async fn alter_rewrite_link_swap_retires_old_mirror() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -491,16 +485,16 @@ async fn alter_rewrite_link_swap_retires_old_mirror() { &tmp, "CREATE TABLE public.doc (id int PRIMARY KEY, body text);\n\ ALTER TABLE public.doc ALTER COLUMN body SET STORAGE EXTERNAL;\n", - SOURCE_PORT + 10, - SHADOW_PORT + 10, - WALSENDER_PORT + 10, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT + 10, CH_HTTP_PORT + 10).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); @@ -520,7 +514,7 @@ async fn alter_rewrite_link_swap_retires_old_mirror() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT + 10, + ch_tcp_port: slot.ch_tcp, mappings: vec![], app_name: "walshadow-toast-alter-rewrite", ddl: Some(fx::DdlPipelineArgs::default()), diff --git a/tests/toast_tombstone_e2e.rs b/tests/toast_tombstone_e2e.rs index c3fc9be..6590845 100644 --- a/tests/toast_tombstone_e2e.rs +++ b/tests/toast_tombstone_e2e.rs @@ -46,13 +46,6 @@ use walshadow::mapping::ToastMode; use walshadow::mapping::{ColumnMapping, TableTarget}; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17661; -const SHADOW_PORT: u16 = 17662; -const CH_TCP_PORT: u16 = 17663; -const CH_HTTP_PORT: u16 = 17664; -// 17665 reserved: ChServer interserver port = http + 1 -const WALSENDER_PORT: u16 = 17666; - /// Distinct byte sums identify values in the mirror. const BODY_A_SQL: &str = "repeat('walshadow-toast-', 512)"; // 8192 const BODY_A_LEN: u64 = 8192; @@ -90,6 +83,7 @@ async fn tombstones_supersede_then_truncate_wipes_then_drop_retires() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -102,16 +96,16 @@ async fn tombstones_supersede_then_truncate_wipes_then_drop_retires() { &tmp, "CREATE TABLE public.doc (id int PRIMARY KEY, meta text, body text);\n\ ALTER TABLE public.doc ALTER COLUMN body SET STORAGE EXTERNAL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // Pre-create the mirror (the store's CREATE IF NOT EXISTS no-ops on it) @@ -178,7 +172,7 @@ async fn tombstones_supersede_then_truncate_wipes_then_drop_retires() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-toast-tombstone", ddl: Some(fx::DdlPipelineArgs::default()), diff --git a/tests/toast_truncate_drop_e2e.rs b/tests/toast_truncate_drop_e2e.rs index a2d8d8b..b025b68 100644 --- a/tests/toast_truncate_drop_e2e.rs +++ b/tests/toast_truncate_drop_e2e.rs @@ -28,15 +28,6 @@ use walshadow::mapping::ToastMode; use walshadow::mapping::{ColumnMapping, TableTarget}; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17671; -const SHADOW_PORT: u16 = 17672; -const CH_TCP_PORT: u16 = 17673; -const CH_HTTP_PORT: u16 = 17674; -// 17675 reserved: ChServer interserver port = http + 1 -const WALSENDER_PORT: u16 = 17676; -// drop_crash_replay_keeps_referrer_bytes shifts every port +10 -// drop_retire_survives_restart_from_ledger shifts every port +20 - const BODY_SQL: &str = "repeat('dies-with-owner-drop', 512)"; // 10240 const DOC_SCHEMA_SQL: &str = "CREATE TABLE public.doc (id int PRIMARY KEY, meta text, body text);\n\ @@ -94,6 +85,7 @@ async fn cold_restart_drop_retires_mirror() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -105,16 +97,16 @@ async fn cold_restart_drop_retires_mirror() { ) = fx::bootstrap_clusters( &tmp, DOC_SCHEMA_SQL, - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); create_doc_dest(&ch); let toast_relid = source @@ -133,7 +125,7 @@ async fn cold_restart_drop_retires_mirror() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state: shadow_stream_state.clone(), ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings: doc_mappings(), app_name: "walshadow-toast-cold-restart-1", ddl: Some(fx::DdlPipelineArgs::default()), @@ -176,7 +168,7 @@ async fn cold_restart_drop_retires_mirror() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings: doc_mappings(), app_name: "walshadow-toast-cold-restart-2", ddl: Some(fx::DdlPipelineArgs::default()), @@ -255,6 +247,7 @@ async fn drop_crash_replay_keeps_referrer_bytes() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -266,16 +259,16 @@ async fn drop_crash_replay_keeps_referrer_bytes() { ) = fx::bootstrap_clusters( &tmp, DOC_SCHEMA_SQL, - SOURCE_PORT + 10, - SHADOW_PORT + 10, - WALSENDER_PORT + 10, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT + 10, CH_HTTP_PORT + 10).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); create_doc_dest(&ch); let toast_relid = source @@ -298,7 +291,7 @@ async fn drop_crash_replay_keeps_referrer_bytes() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state: state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT + 10, + ch_tcp_port: slot.ch_tcp, mappings: doc_mappings(), app_name: "walshadow-toast-crash-replay", ddl: Some(fx::DdlPipelineArgs::default()), @@ -421,6 +414,7 @@ async fn drop_retire_survives_restart_from_ledger() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -432,16 +426,16 @@ async fn drop_retire_survives_restart_from_ledger() { ) = fx::bootstrap_clusters( &tmp, DOC_SCHEMA_SQL, - SOURCE_PORT + 20, - SHADOW_PORT + 20, - WALSENDER_PORT + 20, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT + 20, CH_HTTP_PORT + 20).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); create_doc_dest(&ch); let toast_relid = source @@ -459,7 +453,7 @@ async fn drop_retire_survives_restart_from_ledger() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state: state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT + 20, + ch_tcp_port: slot.ch_tcp, mappings: doc_mappings(), app_name, ddl: Some(fx::DdlPipelineArgs::default()), diff --git a/tests/truncate.rs b/tests/truncate.rs index 2ddc9fb..4a54e27 100644 --- a/tests/truncate.rs +++ b/tests/truncate.rs @@ -25,12 +25,6 @@ use walshadow::mapping::ColumnMapping; use walshadow::mapping::TableTarget; use walshadow::schema::RelName; -const SOURCE_PORT: u16 = 17411; -const SHADOW_PORT: u16 = 17412; -const CH_TCP_PORT: u16 = 17413; -const CH_HTTP_PORT: u16 = 17414; -const WALSENDER_PORT: u16 = 17451; - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn truncate_removes_ch_rows() { if !fx::pg_available() { @@ -46,6 +40,7 @@ async fn truncate_removes_ch_rows() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -59,16 +54,16 @@ async fn truncate_removes_ch_rows() { "CREATE SCHEMA s14;\n\ CREATE TABLE s14.truncate_t (id bigint PRIMARY KEY, payload text);\n\ ALTER TABLE s14.truncate_t REPLICA IDENTITY FULL;\n", - SOURCE_PORT, - SHADOW_PORT, - WALSENDER_PORT, + slot.source, + slot.shadow, + slot.walsender, ) .await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, CH_TCP_PORT, CH_HTTP_PORT).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -106,7 +101,7 @@ async fn truncate_removes_ch_rows() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: CH_TCP_PORT, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-truncate", ddl: None, diff --git a/tests/types_sweep.rs b/tests/types_sweep.rs index f7a6721..62baf12 100644 --- a/tests/types_sweep.rs +++ b/tests/types_sweep.rs @@ -13,51 +13,6 @@ use walshadow::mapping::ColumnMapping; use walshadow::mapping::TableTarget; use walshadow::schema::RelName; -// walsender must clear ch_http by >1 (CH binds interserver = ch_http + 1). -const SLOT_BROAD: PortSlot = PortSlot { - source: 17760, - shadow: 17761, - ch_tcp: 17762, - ch_http: 17763, - walsender: 17767, -}; -const SLOT_NANINF: PortSlot = PortSlot { - source: 17770, - shadow: 17771, - ch_tcp: 17772, - ch_http: 17773, - walsender: 17777, -}; -const SLOT_TIME: PortSlot = PortSlot { - source: 17780, - shadow: 17781, - ch_tcp: 17782, - ch_http: 17783, - walsender: 17787, -}; -const SLOT_NUM: PortSlot = PortSlot { - source: 17790, - shadow: 17791, - ch_tcp: 17792, - ch_http: 17793, - walsender: 17797, -}; -const SLOT_JSON: PortSlot = PortSlot { - source: 17900, - shadow: 17901, - ch_tcp: 17902, - ch_http: 17903, - walsender: 17907, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} - fn col(attnum: i16, name: &str, ty: &str) -> ColumnMapping { ColumnMapping { src_attnum: attnum, @@ -91,6 +46,7 @@ async fn broad_type_fidelity() { ts timestamp, tstz timestamptz,\ u uuid, ip inet, j json, iv interval);\n"; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -99,19 +55,12 @@ async fn broad_type_fidelity() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters( - &tmp, - schema, - SLOT_BROAD.source, - SLOT_BROAD.shadow, - SLOT_BROAD.walsender, - ) - .await; + ) = fx::bootstrap_clusters(&tmp, schema, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, SLOT_BROAD.ch_tcp, SLOT_BROAD.ch_http).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -168,7 +117,7 @@ async fn broad_type_fidelity() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLOT_BROAD.ch_tcp, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-types-sweep", ddl: None, @@ -263,6 +212,7 @@ async fn nan_and_infinity() { let schema = "CREATE TABLE public.ni (\ id int PRIMARY KEY, f4 real, f8 double precision, ntext numeric);\n"; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -271,20 +221,12 @@ async fn nan_and_infinity() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters( - &tmp, - schema, - SLOT_NANINF.source, - SLOT_NANINF.shadow, - SLOT_NANINF.walsender, - ) - .await; + ) = fx::bootstrap_clusters(&tmp, schema, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = - fx::ChServer::spawn(ch_tmp, SLOT_NANINF.ch_tcp, SLOT_NANINF.ch_http).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); // numeric NaN can't live in Decimal, so ntext maps to String. @@ -315,7 +257,7 @@ async fn nan_and_infinity() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLOT_NANINF.ch_tcp, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-nan-inf", ddl: None, @@ -363,6 +305,7 @@ async fn time_precision() { } let schema = "CREATE TABLE public.tp (id int PRIMARY KEY, t time, tz timetz);\n"; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -371,19 +314,12 @@ async fn time_precision() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters( - &tmp, - schema, - SLOT_TIME.source, - SLOT_TIME.shadow, - SLOT_TIME.walsender, - ) - .await; + ) = fx::bootstrap_clusters(&tmp, schema, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, SLOT_TIME.ch_tcp, SLOT_TIME.ch_http).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -412,7 +348,7 @@ async fn time_precision() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLOT_TIME.ch_tcp, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-time-precision", ddl: None, @@ -468,6 +404,7 @@ async fn large_numeric() { } let schema = "CREATE TABLE public.ln (id int PRIMARY KEY, d numeric(38,4), huge numeric, neg numeric);\n"; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -476,19 +413,12 @@ async fn large_numeric() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters( - &tmp, - schema, - SLOT_NUM.source, - SLOT_NUM.shadow, - SLOT_NUM.walsender, - ) - .await; + ) = fx::bootstrap_clusters(&tmp, schema, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, SLOT_NUM.ch_tcp, SLOT_NUM.ch_http).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -518,7 +448,7 @@ async fn large_numeric() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLOT_NUM.ch_tcp, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-large-numeric", ddl: None, @@ -576,6 +506,7 @@ async fn json_null_vs_sql_null() { } let schema = "CREATE TABLE public.jn (id int PRIMARY KEY, j json);\n"; + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -584,19 +515,12 @@ async fn json_null_vs_sql_null() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters( - &tmp, - schema, - SLOT_JSON.source, - SLOT_JSON.shadow, - SLOT_JSON.walsender, - ) - .await; + ) = fx::bootstrap_clusters(&tmp, schema, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, SLOT_JSON.ch_tcp, SLOT_JSON.ch_http).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); ch.query("CREATE DATABASE IF NOT EXISTS walshadow_test") .expect("create db"); ch.query( @@ -621,7 +545,7 @@ async fn json_null_vs_sql_null() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLOT_JSON.ch_tcp, + ch_tcp_port: slot.ch_tcp, mappings, app_name: "walshadow-json-null", ddl: None, diff --git a/tests/wal_stream_e2e.rs b/tests/wal_stream_e2e.rs index d130c9a..29b8559 100644 --- a/tests/wal_stream_e2e.rs +++ b/tests/wal_stream_e2e.rs @@ -14,6 +14,9 @@ //! `WalParser` and asserts the manifest agrees with the parser's //! record count. +#[path = "common/ports.rs"] +mod ports; + use std::fs::OpenOptions; use std::io::Write; use std::os::unix::fs::PermissionsExt; @@ -77,7 +80,7 @@ async fn full_pipeline_source_to_filtered_segments_on_disk() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55801); + let source = make_source(&tmp, ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("base conf"); append_replication_conf(&source); @@ -321,7 +324,7 @@ async fn pre_rotated_pg_class_seed_keeps_catalog_writes() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55802); + let source = make_source(&tmp, ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("base conf"); append_replication_conf(&source); @@ -578,7 +581,9 @@ async fn sidecar_sql_client_negotiates_tls_over_tcp() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55803); + // Only cluster here that listens on TCP, so it needs a reserved port + // rather than the shared socket-only one. + let source = make_source(&tmp, ports::reserve_port()); source.initdb().expect("initdb source"); source.write_base_conf().expect("base conf"); append_replication_conf(&source); @@ -648,7 +653,7 @@ async fn shutdown_writes_partial_segment_and_resume_from_start_lsn_continues() { return; } let tmp = tempfile::tempdir().unwrap(); - let source = make_source(&tmp, 55804); + let source = make_source(&tmp, ports::PG_SOURCE_PORT); source.initdb().expect("initdb source"); source.write_base_conf().expect("base conf"); append_replication_conf(&source); diff --git a/tests/weird_identifiers.rs b/tests/weird_identifiers.rs index 0e58748..509ab76 100644 --- a/tests/weird_identifiers.rs +++ b/tests/weird_identifiers.rs @@ -13,21 +13,6 @@ use walshadow::mapping::TableTarget; use walshadow::schema::RelName; // walsender must clear ch_http by >1 (CH binds interserver = ch_http + 1). -const SLOT: PortSlot = PortSlot { - source: 17750, - shadow: 17751, - ch_tcp: 17752, - ch_http: 17753, - walsender: 17757, -}; - -struct PortSlot { - source: u16, - shadow: u16, - ch_tcp: u16, - ch_http: u16, - walsender: u16, -} const SCHEMA_SQL: &str = "CREATE SCHEMA w;\n\ CREATE TABLE w.\"table\" (id int PRIMARY KEY, val text);\n\ @@ -119,6 +104,7 @@ async fn weird_table_and_column_names_replicate() { if skip_gate() { return; } + let slot = fx::Ports::alloc(); let tmp = tempfile::tempdir().unwrap(); let ( fx::BootstrappedClusters { @@ -127,12 +113,12 @@ async fn weird_table_and_column_names_replicate() { shadow_filter_dir, }, shadow_stream_state, - ) = fx::bootstrap_clusters(&tmp, SCHEMA_SQL, SLOT.source, SLOT.shadow, SLOT.walsender).await; + ) = fx::bootstrap_clusters(&tmp, SCHEMA_SQL, slot.source, slot.shadow, slot.walsender).await; let _src_stop = fx::StopOnDrop { sh: &source }; let _shd_stop = fx::StopOnDrop { sh: &shadow }; let ch_tmp = tempfile::tempdir().unwrap(); - let ch = fx::ChServer::spawn(ch_tmp, SLOT.ch_tcp, SLOT.ch_http).expect("spawn ch"); + let ch = fx::ChServer::spawn(ch_tmp, slot.ch_tcp, slot.ch_http).expect("spawn ch"); create_ch_dests(&ch); let mut pipeline = fx::build_pipeline(fx::BuildPipelineArgs { @@ -142,7 +128,7 @@ async fn weird_table_and_column_names_replicate() { shadow_filter_dir: &shadow_filter_dir, shadow_stream_state, ch_database: "walshadow_test", - ch_tcp_port: SLOT.ch_tcp, + ch_tcp_port: slot.ch_tcp, mappings: mappings(), app_name: "walshadow-weird-idents", ddl: None, diff --git a/tests/xact_buffer.rs b/tests/xact_buffer.rs index b640a00..853e47e 100644 --- a/tests/xact_buffer.rs +++ b/tests/xact_buffer.rs @@ -13,7 +13,10 @@ //! filenodes via psql, and drive the buffer directly with //! synthetic `DecodedHeap` records keyed on those filenodes. //! -//! Tests parallel-safe via non-overlapping ports (`55700+`). +//! Clusters are socket-only, so tests are parallel-safe. + +#[path = "common/ports.rs"] +mod ports; use std::process::Command; use std::sync::Arc; @@ -291,7 +294,8 @@ async fn fixture_shadow_with_things( #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn commit_drains_in_arrival_order_and_clears_state() { - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55701).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -351,7 +355,8 @@ async fn defer_catalog_decode_stashes_raw_and_commit_fences() { use walshadow::record::{Record, RecordSink, Route}; use walshadow::xact_buffer::{BufferingDecoderSink, resolve_stash}; - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55709).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -431,7 +436,8 @@ async fn defer_catalog_decode_stashes_raw_and_commit_fences() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn commit_unknown_xid_no_ops() { - let Some((tmp, shadow, _cat, _rfn)) = fixture_shadow_with_things(55702).await else { + let Some((tmp, shadow, _cat, _rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -456,7 +462,8 @@ async fn commit_unknown_xid_no_ops() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn commit_drains_spilled_then_in_memory_entries() { - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55703).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -514,7 +521,8 @@ async fn commit_drains_spilled_then_in_memory_entries() { /// CDC consumer's "row materialised before its predecessor" race. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn commit_merges_top_and_subxact_in_source_lsn_order() { - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55708).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -548,7 +556,8 @@ async fn commit_merges_top_and_subxact_in_source_lsn_order() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn detoast_concatenates_uncompressed_chunks_into_text() { - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55704).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -609,7 +618,8 @@ async fn detoast_concatenates_uncompressed_chunks_into_text() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn detoast_missing_chunk_seq_errors_clearly() { - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55705).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow); @@ -677,7 +687,8 @@ async fn abort_drops_xact_and_unlinks_spill_against_real_shadow() { // Same shape as the unit test, but reachable via the production // catalog handle so the integration suite covers the bin's // dispatch chain in one place. - let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(55707).await else { + let Some((tmp, shadow, cat, rfn)) = fixture_shadow_with_things(ports::PG_SHADOW_PORT).await + else { return; }; let _stop = stop_on_drop(&shadow);