diff --git a/.github/actions/allure-report-generate/action.yml b/.github/actions/allure-report-generate/action.yml index f959833119..14e1653cb0 100644 --- a/.github/actions/allure-report-generate/action.yml +++ b/.github/actions/allure-report-generate/action.yml @@ -76,8 +76,8 @@ runs: rm -f ${ALLURE_ZIP} fi env: - ALLURE_VERSION: 2.23.1 - ALLURE_ZIP_SHA256: 11141bfe727504b3fd80c0f9801eb317407fd0ac983ebb57e671f14bac4bcd86 + ALLURE_VERSION: 2.24.0 + ALLURE_ZIP_SHA256: 60b1d6ce65d9ef24b23cf9c2c19fd736a123487c38e54759f1ed1a7a77353c90 # Potentially we could have several running build for the same key (for example, for the main branch), so we use improvised lock for this - name: Acquire lock diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 65a2101dc6..a41258c401 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -1092,8 +1092,10 @@ jobs: run: | if [[ "$GITHUB_REF_NAME" == "main" ]]; then gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f deployPreprodRegion=false - elif [[ "$GITHUB_REF_NAME" == "release" ]]; then + + # TODO: move deployPreprodRegion to release (`"$GITHUB_REF_NAME" == "release"` block), once Staging support different compute tag prefixes for different regions gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f deployPreprodRegion=true + elif [[ "$GITHUB_REF_NAME" == "release" ]]; then gh workflow --repo neondatabase/aws run deploy-prod.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f disclamerAcknowledged=true else echo "GITHUB_REF_NAME (value '$GITHUB_REF_NAME') is not set to either 'main' or 'release'" diff --git a/Cargo.lock b/Cargo.lock index 36e7069eb1..be3f179d5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,6 +798,22 @@ dependencies = [ "either", ] +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" + +[[package]] +name = "camino-tempfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ab15a83d13f75dbd86f082bdefd160b628476ef58d3b900a0ef74e001bb097" +dependencies = [ + "camino", + "tempfile", +] + [[package]] name = "cast" version = "0.3.0" @@ -1053,6 +1069,7 @@ name = "control_plane" version = "0.1.0" dependencies = [ "anyhow", + "camino", "clap", "comfy-table", "compute_api", @@ -2641,6 +2658,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes", + "camino", "clap", "git-version", "pageserver", @@ -2661,6 +2679,8 @@ dependencies = [ "async-trait", "byteorder", "bytes", + "camino", + "camino-tempfile", "chrono", "clap", "close_fds", @@ -2712,7 +2732,6 @@ dependencies = [ "strum_macros", "svg_fmt", "sync_wrapper", - "tempfile", "tenant_size_model", "thiserror", "tokio", @@ -3405,6 +3424,8 @@ dependencies = [ "aws-sdk-s3", "aws-smithy-http", "aws-types", + "camino", + "camino-tempfile", "hyper", "metrics", "once_cell", @@ -3413,7 +3434,6 @@ dependencies = [ "scopeguard", "serde", "serde_json", - "tempfile", "test-context", "tokio", "tokio-util", @@ -3765,6 +3785,8 @@ dependencies = [ "async-trait", "byteorder", "bytes", + "camino", + "camino-tempfile", "chrono", "clap", "const_format", @@ -3793,7 +3815,6 @@ dependencies = [ "serde_with", "signal-hook", "storage_broker", - "tempfile", "thiserror", "tokio", "tokio-io-timeout", @@ -5092,6 +5113,8 @@ dependencies = [ "bincode", "byteorder", "bytes", + "camino", + "camino-tempfile", "chrono", "const_format", "criterion", @@ -5117,7 +5140,6 @@ dependencies = [ "signal-hook", "strum", "strum_macros", - "tempfile", "thiserror", "tokio", "tokio-stream", @@ -5191,6 +5213,7 @@ name = "wal_craft" version = "0.1.0" dependencies = [ "anyhow", + "camino-tempfile", "clap", "env_logger", "log", @@ -5198,7 +5221,6 @@ dependencies = [ "postgres", "postgres_ffi", "regex", - "tempfile", "utils", "workspace_hack", ] diff --git a/Cargo.toml b/Cargo.toml index b0bcf69039..2b9da977e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ bindgen = "0.65" bstr = "1.0" byteorder = "1.4" bytes = "1.0" +camino = "1.1.6" cfg-if = "1.0.0" chrono = { version = "0.4", default-features = false, features = ["clock"] } clap = { version = "4.0", features = ["derive"] } @@ -187,7 +188,7 @@ workspace_hack = { version = "0.1", path = "./workspace_hack/" } criterion = "0.5.1" rcgen = "0.11" rstest = "0.18" -tempfile = "3.4" +camino-tempfile = "1.0.2" tonic-build = "0.9" [patch.crates-io] diff --git a/Dockerfile.compute-node b/Dockerfile.compute-node index 7e34b66d68..120520208c 100644 --- a/Dockerfile.compute-node +++ b/Dockerfile.compute-node @@ -368,8 +368,8 @@ RUN wget https://github.com/citusdata/postgresql-hll/archive/refs/tags/v2.18.tar FROM build-deps AS plpgsql-check-pg-build COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/ -RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.4.0.tar.gz -O plpgsql_check.tar.gz && \ - echo "9ba58387a279b35a3bfa39ee611e5684e6cddb2ba046ddb2c5190b3bd2ca254a plpgsql_check.tar.gz" | sha256sum --check && \ +RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.5.3.tar.gz -O plpgsql_check.tar.gz && \ + echo "6631ec3e7fb3769eaaf56e3dfedb829aa761abf163d13dba354b4c218508e1c0 plpgsql_check.tar.gz" | sha256sum --check && \ mkdir plpgsql_check-src && cd plpgsql_check-src && tar xvzf ../plpgsql_check.tar.gz --strip-components=1 -C . && \ make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \ make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \ diff --git a/compute_tools/src/compute.rs b/compute_tools/src/compute.rs index 5c08ebe06a..62e541ebce 100644 --- a/compute_tools/src/compute.rs +++ b/compute_tools/src/compute.rs @@ -1039,7 +1039,7 @@ LIMIT 100", let remote_extensions = spec .remote_extensions .as_ref() - .ok_or(anyhow::anyhow!("Remote extensions are not configured",))?; + .ok_or(anyhow::anyhow!("Remote extensions are not configured"))?; info!("parse shared_preload_libraries from spec.cluster.settings"); let mut libs_vec = Vec::new(); diff --git a/compute_tools/src/monitor.rs b/compute_tools/src/monitor.rs index 1085a27902..f974d6023d 100644 --- a/compute_tools/src/monitor.rs +++ b/compute_tools/src/monitor.rs @@ -1,5 +1,5 @@ use std::sync::Arc; -use std::{thread, time}; +use std::{thread, time::Duration}; use chrono::{DateTime, Utc}; use postgres::{Client, NoTls}; @@ -7,7 +7,7 @@ use tracing::{debug, info}; use crate::compute::ComputeNode; -const MONITOR_CHECK_INTERVAL: u64 = 500; // milliseconds +const MONITOR_CHECK_INTERVAL: Duration = Duration::from_millis(500); // Spin in a loop and figure out the last activity time in the Postgres. // Then update it in the shared state. This function never errors out. @@ -17,13 +17,12 @@ fn watch_compute_activity(compute: &ComputeNode) { let connstr = compute.connstr.as_str(); // Define `client` outside of the loop to reuse existing connection if it's active. let mut client = Client::connect(connstr, NoTls); - let timeout = time::Duration::from_millis(MONITOR_CHECK_INTERVAL); info!("watching Postgres activity at {}", connstr); loop { // Should be outside of the write lock to allow others to read while we sleep. - thread::sleep(timeout); + thread::sleep(MONITOR_CHECK_INTERVAL); match &mut client { Ok(cli) => { diff --git a/control_plane/Cargo.toml b/control_plane/Cargo.toml index ec685915f9..7ccddc161e 100644 --- a/control_plane/Cargo.toml +++ b/control_plane/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [dependencies] anyhow.workspace = true +camino.workspace = true clap.workspace = true comfy-table.workspace = true git-version.workspace = true diff --git a/control_plane/src/attachment_service.rs b/control_plane/src/attachment_service.rs index f0e649cfa8..d7828cdba7 100644 --- a/control_plane/src/attachment_service.rs +++ b/control_plane/src/attachment_service.rs @@ -1,5 +1,6 @@ use crate::{background_process, local_env::LocalEnv}; use anyhow::anyhow; +use camino::Utf8PathBuf; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; use std::{path::PathBuf, process::Child}; @@ -47,8 +48,9 @@ impl AttachmentService { } } - fn pid_file(&self) -> PathBuf { - self.env.base_data_dir.join("attachment_service.pid") + fn pid_file(&self) -> Utf8PathBuf { + Utf8PathBuf::from_path_buf(self.env.base_data_dir.join("attachment_service.pid")) + .expect("non-Unicode path") } pub fn start(&self) -> anyhow::Result { diff --git a/control_plane/src/background_process.rs b/control_plane/src/background_process.rs index 64664d65ff..186d49fe8b 100644 --- a/control_plane/src/background_process.rs +++ b/control_plane/src/background_process.rs @@ -16,12 +16,13 @@ use std::ffi::OsStr; use std::io::Write; use std::os::unix::prelude::AsRawFd; use std::os::unix::process::CommandExt; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::{Child, Command}; use std::time::Duration; use std::{fs, io, thread}; use anyhow::Context; +use camino::{Utf8Path, Utf8PathBuf}; use nix::errno::Errno; use nix::fcntl::{FcntlArg, FdFlag}; use nix::sys::signal::{kill, Signal}; @@ -45,9 +46,9 @@ const NOTICE_AFTER_RETRIES: u64 = 50; /// it itself. pub enum InitialPidFile<'t> { /// Create a pidfile, to allow future CLI invocations to manipulate the process. - Create(&'t Path), + Create(&'t Utf8Path), /// The process will create the pidfile itself, need to wait for that event. - Expect(&'t Path), + Expect(&'t Utf8Path), } /// Start a background child process using the parameters given. @@ -137,7 +138,11 @@ where } /// Stops the process, using the pid file given. Returns Ok also if the process is already not running. -pub fn stop_process(immediate: bool, process_name: &str, pid_file: &Path) -> anyhow::Result<()> { +pub fn stop_process( + immediate: bool, + process_name: &str, + pid_file: &Utf8Path, +) -> anyhow::Result<()> { let pid = match pid_file::read(pid_file) .with_context(|| format!("read pid_file {pid_file:?}"))? { @@ -252,9 +257,9 @@ fn fill_aws_secrets_vars(mut cmd: &mut Command) -> &mut Command { /// will remain held until the cmd exits. fn pre_exec_create_pidfile

(cmd: &mut Command, path: P) -> &mut Command where - P: Into, + P: Into, { - let path: PathBuf = path.into(); + let path: Utf8PathBuf = path.into(); // SAFETY // pre_exec is marked unsafe because it runs between fork and exec. // Why is that dangerous in various ways? @@ -311,7 +316,7 @@ where fn process_started( pid: Pid, - pid_file_to_check: Option<&Path>, + pid_file_to_check: Option<&Utf8Path>, status_check: &F, ) -> anyhow::Result where diff --git a/control_plane/src/bin/neon_local.rs b/control_plane/src/bin/neon_local.rs index 4cdb91bfd2..d43566d2df 100644 --- a/control_plane/src/bin/neon_local.rs +++ b/control_plane/src/bin/neon_local.rs @@ -116,6 +116,7 @@ fn main() -> Result<()> { "attachment_service" => handle_attachment_service(sub_args, &env), "safekeeper" => handle_safekeeper(sub_args, &env), "endpoint" => handle_endpoint(sub_args, &env), + "mappings" => handle_mappings(sub_args, &mut env), "pg" => bail!("'pg' subcommand has been renamed to 'endpoint'"), _ => bail!("unexpected subcommand {sub_name}"), }; @@ -816,6 +817,38 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<( Ok(()) } +fn handle_mappings(sub_match: &ArgMatches, env: &mut local_env::LocalEnv) -> Result<()> { + let (sub_name, sub_args) = match sub_match.subcommand() { + Some(ep_subcommand_data) => ep_subcommand_data, + None => bail!("no mappings subcommand provided"), + }; + + match sub_name { + "map" => { + let branch_name = sub_args + .get_one::("branch-name") + .expect("branch-name argument missing"); + + let tenant_id = sub_args + .get_one::("tenant-id") + .map(|x| TenantId::from_str(x)) + .expect("tenant-id argument missing") + .expect("malformed tenant-id arg"); + + let timeline_id = sub_args + .get_one::("timeline-id") + .map(|x| TimelineId::from_str(x)) + .expect("timeline-id argument missing") + .expect("malformed timeline-id arg"); + + env.register_branch_mapping(branch_name.to_owned(), tenant_id, timeline_id)?; + + Ok(()) + } + other => unimplemented!("mappings subcommand {other}"), + } +} + fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> { fn get_pageserver(env: &local_env::LocalEnv, args: &ArgMatches) -> Result { let node_id = if let Some(id_str) = args.get_one::("pageserver-id") { @@ -1084,6 +1117,7 @@ fn cli() -> Command { // --id, when using a pageserver command let pageserver_id_arg = Arg::new("pageserver-id") .long("id") + .global(true) .help("pageserver id") .required(false); // --pageserver-id when using a non-pageserver command @@ -1254,17 +1288,20 @@ fn cli() -> Command { Command::new("pageserver") .arg_required_else_help(true) .about("Manage pageserver") + .arg(pageserver_id_arg) .subcommand(Command::new("status")) - .arg(pageserver_id_arg.clone()) - .subcommand(Command::new("start").about("Start local pageserver") - .arg(pageserver_id_arg.clone()) - .arg(pageserver_config_args.clone())) - .subcommand(Command::new("stop").about("Stop local pageserver") - .arg(pageserver_id_arg.clone()) - .arg(stop_mode_arg.clone())) - .subcommand(Command::new("restart").about("Restart local pageserver") - .arg(pageserver_id_arg.clone()) - .arg(pageserver_config_args.clone())) + .subcommand(Command::new("start") + .about("Start local pageserver") + .arg(pageserver_config_args.clone()) + ) + .subcommand(Command::new("stop") + .about("Stop local pageserver") + .arg(stop_mode_arg.clone()) + ) + .subcommand(Command::new("restart") + .about("Restart local pageserver") + .arg(pageserver_config_args.clone()) + ) ) .subcommand( Command::new("attachment_service") @@ -1321,8 +1358,8 @@ fn cli() -> Command { .about("Start postgres.\n If the endpoint doesn't exist yet, it is created.") .arg(endpoint_id_arg.clone()) .arg(tenant_id_arg.clone()) - .arg(branch_name_arg) - .arg(timeline_id_arg) + .arg(branch_name_arg.clone()) + .arg(timeline_id_arg.clone()) .arg(lsn_arg) .arg(pg_port_arg) .arg(http_port_arg) @@ -1335,7 +1372,7 @@ fn cli() -> Command { .subcommand( Command::new("stop") .arg(endpoint_id_arg) - .arg(tenant_id_arg) + .arg(tenant_id_arg.clone()) .arg( Arg::new("destroy") .help("Also delete data directory (now optional, should be default in future)") @@ -1346,6 +1383,18 @@ fn cli() -> Command { ) ) + .subcommand( + Command::new("mappings") + .arg_required_else_help(true) + .about("Manage neon_local branch name mappings") + .subcommand( + Command::new("map") + .about("Create new mapping which cannot exist already") + .arg(branch_name_arg.clone()) + .arg(tenant_id_arg.clone()) + .arg(timeline_id_arg.clone()) + ) + ) // Obsolete old name for 'endpoint'. We now just print an error if it's used. .subcommand( Command::new("pg") diff --git a/control_plane/src/broker.rs b/control_plane/src/broker.rs index 8d40c7afc1..6be865cc2e 100644 --- a/control_plane/src/broker.rs +++ b/control_plane/src/broker.rs @@ -7,7 +7,7 @@ //! ``` use anyhow::Context; -use std::path::PathBuf; +use camino::Utf8PathBuf; use crate::{background_process, local_env}; @@ -30,7 +30,7 @@ pub fn start_broker_process(env: &local_env::LocalEnv) -> anyhow::Result<()> { || { let url = broker.client_url(); let status_url = url.join("status").with_context(|| { - format!("Failed to append /status path to broker endpoint {url}",) + format!("Failed to append /status path to broker endpoint {url}") })?; let request = client .get(status_url) @@ -50,6 +50,7 @@ pub fn stop_broker_process(env: &local_env::LocalEnv) -> anyhow::Result<()> { background_process::stop_process(true, "storage_broker", &storage_broker_pid_file_path(env)) } -fn storage_broker_pid_file_path(env: &local_env::LocalEnv) -> PathBuf { - env.base_data_dir.join("storage_broker.pid") +fn storage_broker_pid_file_path(env: &local_env::LocalEnv) -> Utf8PathBuf { + Utf8PathBuf::from_path_buf(env.base_data_dir.join("storage_broker.pid")) + .expect("non-Unicode path") } diff --git a/control_plane/src/pageserver.rs b/control_plane/src/pageserver.rs index a6b675fdb5..0746dde4ef 100644 --- a/control_plane/src/pageserver.rs +++ b/control_plane/src/pageserver.rs @@ -14,6 +14,7 @@ use std::process::{Child, Command}; use std::{io, result}; use anyhow::{bail, Context}; +use camino::Utf8PathBuf; use pageserver_api::models::{self, TenantInfo, TimelineInfo}; use postgres_backend::AuthType; use postgres_connection::{parse_host_port, PgConnectionConfig}; @@ -144,7 +145,7 @@ impl PageServerNode { pub fn initialize(&self, config_overrides: &[&str]) -> anyhow::Result<()> { // First, run `pageserver --init` and wait for it to write a config into FS and exit. self.pageserver_init(config_overrides) - .with_context(|| format!("Failed to run init for pageserver node {}", self.conf.id,)) + .with_context(|| format!("Failed to run init for pageserver node {}", self.conf.id)) } pub fn repo_path(&self) -> PathBuf { @@ -154,8 +155,9 @@ impl PageServerNode { /// The pid file is created by the pageserver process, with its pid stored inside. /// Other pageservers cannot lock the same file and overwrite it for as long as the current /// pageserver runs. (Unless someone removes the file manually; never do that!) - fn pid_file(&self) -> PathBuf { - self.repo_path().join("pageserver.pid") + fn pid_file(&self) -> Utf8PathBuf { + Utf8PathBuf::from_path_buf(self.repo_path().join("pageserver.pid")) + .expect("non-Unicode path") } pub fn start(&self, config_overrides: &[&str]) -> anyhow::Result { diff --git a/control_plane/src/safekeeper.rs b/control_plane/src/safekeeper.rs index eb8fe1af17..a8baa0ac53 100644 --- a/control_plane/src/safekeeper.rs +++ b/control_plane/src/safekeeper.rs @@ -11,6 +11,7 @@ use std::process::Child; use std::{io, result}; use anyhow::Context; +use camino::Utf8PathBuf; use postgres_connection::PgConnectionConfig; use reqwest::blocking::{Client, RequestBuilder, Response}; use reqwest::{IntoUrl, Method}; @@ -97,8 +98,9 @@ impl SafekeeperNode { SafekeeperNode::datadir_path_by_id(&self.env, self.id) } - pub fn pid_file(&self) -> PathBuf { - self.datadir_path().join("safekeeper.pid") + pub fn pid_file(&self) -> Utf8PathBuf { + Utf8PathBuf::from_path_buf(self.datadir_path().join("safekeeper.pid")) + .expect("non-Unicode path") } pub fn start(&self, extra_opts: Vec) -> anyhow::Result { diff --git a/libs/pageserver_api/src/models.rs b/libs/pageserver_api/src/models.rs index 68620787bb..b2064cb7fe 100644 --- a/libs/pageserver_api/src/models.rs +++ b/libs/pageserver_api/src/models.rs @@ -10,6 +10,7 @@ use serde_with::{serde_as, DisplayFromStr}; use strum_macros; use utils::{ completion, + generation::Generation, history_buffer::HistoryBufferWithDropCounter, id::{NodeId, TenantId, TimelineId}, lsn::Lsn, @@ -218,6 +219,8 @@ impl std::ops::Deref for TenantCreateRequest { } } +/// An alternative representation of `pageserver::tenant::TenantConf` with +/// simpler types. #[derive(Serialize, Deserialize, Debug, Default)] pub struct TenantConfig { pub checkpoint_distance: Option, @@ -243,6 +246,39 @@ pub struct TenantConfig { pub gc_feedback: Option, } +/// A flattened analog of a `pagesever::tenant::LocationMode`, which +/// lists out all possible states (and the virtual "Detached" state) +/// in a flat form rather than using rust-style enums. +#[derive(Serialize, Deserialize, Debug)] +pub enum LocationConfigMode { + AttachedSingle, + AttachedMulti, + AttachedStale, + Secondary, + Detached, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct LocationConfigSecondary { + pub warm: bool, +} + +/// An alternative representation of `pageserver::tenant::LocationConf`, +/// for use in external-facing APIs. +#[derive(Serialize, Deserialize, Debug)] +pub struct LocationConfig { + pub mode: LocationConfigMode, + /// If attaching, in what generation? + #[serde(default)] + pub generation: Option, + #[serde(default)] + pub secondary_conf: Option, + + // If requesting mode `Secondary`, configuration for that. + // Custom storage configuration for the tenant, if any + pub tenant_conf: TenantConfig, +} + #[serde_as] #[derive(Serialize, Deserialize)] #[serde(transparent)] @@ -253,6 +289,16 @@ pub struct StatusResponse { pub id: NodeId, } +#[serde_as] +#[derive(Serialize, Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct TenantLocationConfigRequest { + #[serde_as(as = "DisplayFromStr")] + pub tenant_id: TenantId, + #[serde(flatten)] + pub config: LocationConfig, // as we have a flattened field, we should reject all unknown fields in it +} + #[serde_as] #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] diff --git a/libs/postgres_backend/src/lib.rs b/libs/postgres_backend/src/lib.rs index 453c58431a..08c4e03d13 100644 --- a/libs/postgres_backend/src/lib.rs +++ b/libs/postgres_backend/src/lib.rs @@ -442,10 +442,20 @@ impl PostgresBackend { trace!("got message {:?}", msg); let result = self.process_message(handler, msg, &mut query_string).await; - self.flush().await?; + tokio::select!( + biased; + _ = shutdown_watcher() => { + // We were requested to shut down. + tracing::info!("shutdown request received during response flush"); + return Ok(()) + }, + flush_r = self.flush() => { + flush_r?; + } + ); + match result? { ProcessMsgResult::Continue => { - self.flush().await?; continue; } ProcessMsgResult::Break => break, diff --git a/libs/postgres_ffi/wal_craft/Cargo.toml b/libs/postgres_ffi/wal_craft/Cargo.toml index bea888b23e..0edc642402 100644 --- a/libs/postgres_ffi/wal_craft/Cargo.toml +++ b/libs/postgres_ffi/wal_craft/Cargo.toml @@ -12,7 +12,7 @@ log.workspace = true once_cell.workspace = true postgres.workspace = true postgres_ffi.workspace = true -tempfile.workspace = true +camino-tempfile.workspace = true workspace_hack.workspace = true diff --git a/libs/postgres_ffi/wal_craft/src/lib.rs b/libs/postgres_ffi/wal_craft/src/lib.rs index fb627ca258..75ffd3f055 100644 --- a/libs/postgres_ffi/wal_craft/src/lib.rs +++ b/libs/postgres_ffi/wal_craft/src/lib.rs @@ -1,4 +1,5 @@ use anyhow::{bail, ensure}; +use camino_tempfile::{tempdir, Utf8TempDir}; use log::*; use postgres::types::PgLsn; use postgres::Client; @@ -8,7 +9,6 @@ use std::cmp::Ordering; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{Duration, Instant}; -use tempfile::{tempdir, TempDir}; macro_rules! xlog_utils_test { ($version:ident) => { @@ -33,7 +33,7 @@ pub struct Conf { pub struct PostgresServer { process: std::process::Child, - _unix_socket_dir: TempDir, + _unix_socket_dir: Utf8TempDir, client_config: postgres::Config, } diff --git a/libs/remote_storage/Cargo.toml b/libs/remote_storage/Cargo.toml index 2b808779f4..d938648750 100644 --- a/libs/remote_storage/Cargo.toml +++ b/libs/remote_storage/Cargo.toml @@ -13,6 +13,7 @@ aws-types.workspace = true aws-config.workspace = true aws-sdk-s3.workspace = true aws-credential-types.workspace = true +camino.workspace = true hyper = { workspace = true, features = ["stream"] } serde.workspace = true serde_json.workspace = true @@ -27,6 +28,6 @@ pin-project-lite.workspace = true workspace_hack.workspace = true [dev-dependencies] -tempfile.workspace = true +camino-tempfile.workspace = true test-context.workspace = true rand.workspace = true diff --git a/libs/remote_storage/src/lib.rs b/libs/remote_storage/src/lib.rs index a92b87632b..3560c94c71 100644 --- a/libs/remote_storage/src/lib.rs +++ b/libs/remote_storage/src/lib.rs @@ -13,12 +13,12 @@ use std::{ collections::HashMap, fmt::Debug, num::{NonZeroU32, NonZeroUsize}, - path::{Path, PathBuf}, pin::Pin, sync::Arc, }; use anyhow::{bail, Context}; +use camino::{Utf8Path, Utf8PathBuf}; use serde::{Deserialize, Serialize}; use tokio::io; @@ -52,7 +52,7 @@ const REMOTE_STORAGE_PREFIX_SEPARATOR: char = '/'; /// The prefix is an implementation detail, that allows representing local paths /// as the remote ones, stripping the local storage prefix away. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RemotePath(PathBuf); +pub struct RemotePath(Utf8PathBuf); impl Serialize for RemotePath { fn serialize(&self, serializer: S) -> Result @@ -69,18 +69,18 @@ impl<'de> Deserialize<'de> for RemotePath { D: serde::Deserializer<'de>, { let str = String::deserialize(deserializer)?; - Ok(Self(PathBuf::from(&str))) + Ok(Self(Utf8PathBuf::from(&str))) } } impl std::fmt::Display for RemotePath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.display()) + std::fmt::Display::fmt(&self.0, f) } } impl RemotePath { - pub fn new(relative_path: &Path) -> anyhow::Result { + pub fn new(relative_path: &Utf8Path) -> anyhow::Result { anyhow::ensure!( relative_path.is_relative(), "Path {relative_path:?} is not relative" @@ -89,30 +89,30 @@ impl RemotePath { } pub fn from_string(relative_path: &str) -> anyhow::Result { - Self::new(Path::new(relative_path)) + Self::new(Utf8Path::new(relative_path)) } - pub fn with_base(&self, base_path: &Path) -> PathBuf { + pub fn with_base(&self, base_path: &Utf8Path) -> Utf8PathBuf { base_path.join(&self.0) } pub fn object_name(&self) -> Option<&str> { - self.0.file_name().and_then(|os_str| os_str.to_str()) + self.0.file_name() } - pub fn join(&self, segment: &Path) -> Self { + pub fn join(&self, segment: &Utf8Path) -> Self { Self(self.0.join(segment)) } - pub fn get_path(&self) -> &PathBuf { + pub fn get_path(&self) -> &Utf8PathBuf { &self.0 } pub fn extension(&self) -> Option<&str> { - self.0.extension()?.to_str() + self.0.extension() } - pub fn strip_prefix(&self, p: &RemotePath) -> Result<&Path, std::path::StripPrefixError> { + pub fn strip_prefix(&self, p: &RemotePath) -> Result<&Utf8Path, std::path::StripPrefixError> { self.0.strip_prefix(&p.0) } } @@ -311,7 +311,7 @@ impl GenericRemoteStorage { pub fn from_config(storage_config: &RemoteStorageConfig) -> anyhow::Result { Ok(match &storage_config.storage { RemoteStorageKind::LocalFs(root) => { - info!("Using fs root '{}' as a remote storage", root.display()); + info!("Using fs root '{root}' as a remote storage"); Self::LocalFs(LocalFs::new(root.clone())?) } RemoteStorageKind::AwsS3(s3_config) => { @@ -379,7 +379,7 @@ pub struct RemoteStorageConfig { pub enum RemoteStorageKind { /// Storage based on local file system. /// Specify a root folder to place all stored files into. - LocalFs(PathBuf), + LocalFs(Utf8PathBuf), /// AWS S3 based storage, storing all files in the S3 bucket /// specified by the config AwsS3(S3Config), @@ -474,7 +474,7 @@ impl RemoteStorageConfig { concurrency_limit, max_keys_per_list_response, }), - (Some(local_path), None, None) => RemoteStorageKind::LocalFs(PathBuf::from( + (Some(local_path), None, None) => RemoteStorageKind::LocalFs(Utf8PathBuf::from( parse_toml_string("local_path", local_path)?, )), (Some(_), Some(_), _) => bail!("local_path and bucket_name are mutually exclusive"), @@ -519,23 +519,23 @@ mod tests { #[test] fn test_object_name() { - let k = RemotePath::new(Path::new("a/b/c")).unwrap(); + let k = RemotePath::new(Utf8Path::new("a/b/c")).unwrap(); assert_eq!(k.object_name(), Some("c")); - let k = RemotePath::new(Path::new("a/b/c/")).unwrap(); + let k = RemotePath::new(Utf8Path::new("a/b/c/")).unwrap(); assert_eq!(k.object_name(), Some("c")); - let k = RemotePath::new(Path::new("a/")).unwrap(); + let k = RemotePath::new(Utf8Path::new("a/")).unwrap(); assert_eq!(k.object_name(), Some("a")); // XXX is it impossible to have an empty key? - let k = RemotePath::new(Path::new("")).unwrap(); + let k = RemotePath::new(Utf8Path::new("")).unwrap(); assert_eq!(k.object_name(), None); } #[test] fn rempte_path_cannot_be_created_from_absolute_ones() { - let err = RemotePath::new(Path::new("/")).expect_err("Should fail on absolute paths"); + let err = RemotePath::new(Utf8Path::new("/")).expect_err("Should fail on absolute paths"); assert_eq!(err.to_string(), "Path \"/\" is not relative"); } } diff --git a/libs/remote_storage/src/local_fs.rs b/libs/remote_storage/src/local_fs.rs index 5040183045..3d32b6b631 100644 --- a/libs/remote_storage/src/local_fs.rs +++ b/libs/remote_storage/src/local_fs.rs @@ -4,15 +4,10 @@ //! This storage used in tests, but can also be used in cases when a certain persistent //! volume is mounted to the local FS. -use std::{ - borrow::Cow, - future::Future, - io::ErrorKind, - path::{Path, PathBuf}, - pin::Pin, -}; +use std::{borrow::Cow, future::Future, io::ErrorKind, pin::Pin}; use anyhow::{bail, ensure, Context}; +use camino::{Utf8Path, Utf8PathBuf}; use tokio::{ fs, io::{self, AsyncReadExt, AsyncSeekExt, AsyncWriteExt}, @@ -28,20 +23,20 @@ const LOCAL_FS_TEMP_FILE_SUFFIX: &str = "___temp"; #[derive(Debug, Clone)] pub struct LocalFs { - storage_root: PathBuf, + storage_root: Utf8PathBuf, } impl LocalFs { /// Attempts to create local FS storage, along with its root directory. /// Storage root will be created (if does not exist) and transformed into an absolute path (if passed as relative). - pub fn new(mut storage_root: PathBuf) -> anyhow::Result { + pub fn new(mut storage_root: Utf8PathBuf) -> anyhow::Result { if !storage_root.exists() { std::fs::create_dir_all(&storage_root).with_context(|| { format!("Failed to create all directories in the given root path {storage_root:?}") })?; } if !storage_root.is_absolute() { - storage_root = storage_root.canonicalize().with_context(|| { + storage_root = storage_root.canonicalize_utf8().with_context(|| { format!("Failed to represent path {storage_root:?} as an absolute path") })?; } @@ -50,7 +45,7 @@ impl LocalFs { } // mirrors S3Bucket::s3_object_to_relative_path - fn local_file_to_relative_path(&self, key: PathBuf) -> RemotePath { + fn local_file_to_relative_path(&self, key: Utf8PathBuf) -> RemotePath { let relative_path = key .strip_prefix(&self.storage_root) .expect("relative path must contain storage_root as prefix"); @@ -59,22 +54,18 @@ impl LocalFs { async fn read_storage_metadata( &self, - file_path: &Path, + file_path: &Utf8Path, ) -> anyhow::Result> { let metadata_path = storage_metadata_path(file_path); if metadata_path.exists() && metadata_path.is_file() { let metadata_string = fs::read_to_string(&metadata_path).await.with_context(|| { - format!( - "Failed to read metadata from the local storage at '{}'", - metadata_path.display() - ) + format!("Failed to read metadata from the local storage at '{metadata_path}'") })?; serde_json::from_str(&metadata_string) .with_context(|| { format!( - "Failed to deserialize metadata from the local storage at '{}'", - metadata_path.display() + "Failed to deserialize metadata from the local storage at '{metadata_path}'", ) }) .map(|metadata| Some(StorageMetadata(metadata))) @@ -171,25 +162,21 @@ impl RemoteStorage for LocalFs { } } - // Note that PathBuf starts_with only considers full path segments, but + // Note that Utf8PathBuf starts_with only considers full path segments, but // object prefixes are arbitrary strings, so we need the strings for doing // starts_with later. - let prefix = full_path.to_string_lossy(); + let prefix = full_path.as_str(); let mut files = vec![]; - let mut directory_queue = vec![initial_dir.clone()]; + let mut directory_queue = vec![initial_dir]; while let Some(cur_folder) = directory_queue.pop() { - let mut entries = fs::read_dir(cur_folder.clone()).await?; - while let Some(entry) = entries.next_entry().await? { - let file_name: PathBuf = entry.file_name().into(); - let full_file_name = cur_folder.clone().join(&file_name); - if full_file_name - .to_str() - .map(|s| s.starts_with(prefix.as_ref())) - .unwrap_or(false) - { + let mut entries = cur_folder.read_dir_utf8()?; + while let Some(Ok(entry)) = entries.next() { + let file_name = entry.file_name(); + let full_file_name = cur_folder.join(file_name); + if full_file_name.as_str().starts_with(prefix) { let file_remote_path = self.local_file_to_relative_path(full_file_name.clone()); - files.push(file_remote_path.clone()); + files.push(file_remote_path); if full_file_name.is_dir() { directory_queue.push(full_file_name); } @@ -230,10 +217,7 @@ impl RemoteStorage for LocalFs { .open(&temp_file_path) .await .with_context(|| { - format!( - "Failed to open target fs destination at '{}'", - target_file_path.display() - ) + format!("Failed to open target fs destination at '{target_file_path}'") })?, ); @@ -244,8 +228,7 @@ impl RemoteStorage for LocalFs { .await .with_context(|| { format!( - "Failed to upload file (write temp) to the local storage at '{}'", - temp_file_path.display() + "Failed to upload file (write temp) to the local storage at '{temp_file_path}'", ) })?; @@ -262,8 +245,7 @@ impl RemoteStorage for LocalFs { destination.flush().await.with_context(|| { format!( - "Failed to upload (flush temp) file to the local storage at '{}'", - temp_file_path.display() + "Failed to upload (flush temp) file to the local storage at '{temp_file_path}'", ) })?; @@ -271,8 +253,7 @@ impl RemoteStorage for LocalFs { .await .with_context(|| { format!( - "Failed to upload (rename) file to the local storage at '{}'", - target_file_path.display() + "Failed to upload (rename) file to the local storage at '{target_file_path}'", ) })?; @@ -286,8 +267,7 @@ impl RemoteStorage for LocalFs { .await .with_context(|| { format!( - "Failed to write metadata to the local storage at '{}'", - storage_metadata_path.display() + "Failed to write metadata to the local storage at '{storage_metadata_path}'", ) })?; } @@ -393,16 +373,16 @@ impl RemoteStorage for LocalFs { } } -fn storage_metadata_path(original_path: &Path) -> PathBuf { +fn storage_metadata_path(original_path: &Utf8Path) -> Utf8PathBuf { path_with_suffix_extension(original_path, "metadata") } fn get_all_files<'a, P>( directory_path: P, recursive: bool, -) -> Pin>> + Send + Sync + 'a>> +) -> Pin>> + Send + Sync + 'a>> where - P: AsRef + Send + Sync + 'a, + P: AsRef + Send + Sync + 'a, { Box::pin(async move { let directory_path = directory_path.as_ref(); @@ -412,7 +392,13 @@ where let mut dir_contents = fs::read_dir(directory_path).await?; while let Some(dir_entry) = dir_contents.next_entry().await? { let file_type = dir_entry.file_type().await?; - let entry_path = dir_entry.path(); + let entry_path = + Utf8PathBuf::from_path_buf(dir_entry.path()).map_err(|pb| { + anyhow::Error::msg(format!( + "non-Unicode path: {}", + pb.to_string_lossy() + )) + })?; if file_type.is_symlink() { debug!("{entry_path:?} is a symlink, skipping") } else if file_type.is_dir() { @@ -435,13 +421,10 @@ where }) } -async fn create_target_directory(target_file_path: &Path) -> anyhow::Result<()> { +async fn create_target_directory(target_file_path: &Utf8Path) -> anyhow::Result<()> { let target_dir = match target_file_path.parent() { Some(parent_dir) => parent_dir, - None => bail!( - "File path '{}' has no parent directory", - target_file_path.display() - ), + None => bail!("File path '{target_file_path}' has no parent directory"), }; if !target_dir.exists() { fs::create_dir_all(target_dir).await?; @@ -449,13 +432,9 @@ async fn create_target_directory(target_file_path: &Path) -> anyhow::Result<()> Ok(()) } -fn file_exists(file_path: &Path) -> anyhow::Result { +fn file_exists(file_path: &Utf8Path) -> anyhow::Result { if file_path.exists() { - ensure!( - file_path.is_file(), - "file path '{}' is not a file", - file_path.display() - ); + ensure!(file_path.is_file(), "file path '{file_path}' is not a file"); Ok(true) } else { Ok(false) @@ -466,13 +445,13 @@ fn file_exists(file_path: &Path) -> anyhow::Result { mod fs_tests { use super::*; + use camino_tempfile::tempdir; use std::{collections::HashMap, io::Write}; - use tempfile::tempdir; async fn read_and_assert_remote_file_contents( storage: &LocalFs, #[allow(clippy::ptr_arg)] - // have to use &PathBuf due to `storage.local_path` parameter requirements + // have to use &Utf8PathBuf due to `storage.local_path` parameter requirements remote_storage_path: &RemotePath, expected_metadata: Option<&StorageMetadata>, ) -> anyhow::Result { @@ -519,7 +498,7 @@ mod fs_tests { async fn upload_file_negatives() -> anyhow::Result<()> { let storage = create_storage()?; - let id = RemotePath::new(Path::new("dummy"))?; + let id = RemotePath::new(Utf8Path::new("dummy"))?; let content = std::io::Cursor::new(b"12345"); // Check that you get an error if the size parameter doesn't match the actual @@ -544,7 +523,8 @@ mod fs_tests { } fn create_storage() -> anyhow::Result { - LocalFs::new(tempdir()?.path().to_owned()) + let storage_root = tempdir()?.path().to_path_buf(); + LocalFs::new(storage_root) } #[tokio::test] @@ -561,7 +541,7 @@ mod fs_tests { ); let non_existing_path = "somewhere/else"; - match storage.download(&RemotePath::new(Path::new(non_existing_path))?).await { + match storage.download(&RemotePath::new(Utf8Path::new(non_existing_path))?).await { Err(DownloadError::NotFound) => {} // Should get NotFound for non existing keys other => panic!("Should get a NotFound error when downloading non-existing storage files, but got: {other:?}"), } @@ -775,7 +755,7 @@ mod fs_tests { } async fn create_file_for_upload( - path: &Path, + path: &Utf8Path, contents: &str, ) -> anyhow::Result<(io::BufReader, usize)> { std::fs::create_dir_all(path.parent().unwrap())?; diff --git a/libs/remote_storage/src/s3_bucket.rs b/libs/remote_storage/src/s3_bucket.rs index fc6d7fa61b..db7eef91e2 100644 --- a/libs/remote_storage/src/s3_bucket.rs +++ b/libs/remote_storage/src/s3_bucket.rs @@ -180,12 +180,11 @@ impl S3Bucket { assert_eq!(std::path::MAIN_SEPARATOR, REMOTE_STORAGE_PREFIX_SEPARATOR); let path_string = path .get_path() - .to_string_lossy() - .trim_end_matches(REMOTE_STORAGE_PREFIX_SEPARATOR) - .to_string(); + .as_str() + .trim_end_matches(REMOTE_STORAGE_PREFIX_SEPARATOR); match &self.prefix_in_bucket { - Some(prefix) => prefix.clone() + "/" + &path_string, - None => path_string, + Some(prefix) => prefix.clone() + "/" + path_string, + None => path_string.to_string(), } } @@ -601,8 +600,8 @@ fn start_measuring_requests( #[cfg(test)] mod tests { + use camino::Utf8Path; use std::num::NonZeroUsize; - use std::path::Path; use crate::{RemotePath, S3Bucket, S3Config}; @@ -611,7 +610,7 @@ mod tests { let all_paths = ["", "some/path", "some/path/"]; let all_paths: Vec = all_paths .iter() - .map(|x| RemotePath::new(Path::new(x)).expect("bad path")) + .map(|x| RemotePath::new(Utf8Path::new(x)).expect("bad path")) .collect(); let prefixes = [ None, diff --git a/libs/remote_storage/tests/test_real_s3.rs b/libs/remote_storage/tests/test_real_s3.rs index b220349829..7e2aa9f6d7 100644 --- a/libs/remote_storage/tests/test_real_s3.rs +++ b/libs/remote_storage/tests/test_real_s3.rs @@ -2,11 +2,12 @@ use std::collections::HashSet; use std::env; use std::num::{NonZeroU32, NonZeroUsize}; use std::ops::ControlFlow; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::sync::Arc; use std::time::UNIX_EPOCH; use anyhow::Context; +use camino::Utf8Path; use once_cell::sync::OnceCell; use remote_storage::{ GenericRemoteStorage, RemotePath, RemoteStorageConfig, RemoteStorageKind, S3Config, @@ -55,7 +56,7 @@ async fn s3_pagination_should_work(ctx: &mut MaybeEnabledS3WithTestBlobs) -> any let test_client = Arc::clone(&ctx.enabled.client); let expected_remote_prefixes = ctx.remote_prefixes.clone(); - let base_prefix = RemotePath::new(Path::new(ctx.enabled.base_prefix)) + let base_prefix = RemotePath::new(Utf8Path::new(ctx.enabled.base_prefix)) .context("common_prefix construction")?; let root_remote_prefixes = test_client .list_prefixes(None) @@ -108,7 +109,7 @@ async fn s3_list_files_works(ctx: &mut MaybeEnabledS3WithSimpleTestBlobs) -> any }; let test_client = Arc::clone(&ctx.enabled.client); let base_prefix = - RemotePath::new(Path::new("folder1")).context("common_prefix construction")?; + RemotePath::new(Utf8Path::new("folder1")).context("common_prefix construction")?; let root_files = test_client .list_files(None) .await @@ -129,9 +130,9 @@ async fn s3_list_files_works(ctx: &mut MaybeEnabledS3WithSimpleTestBlobs) -> any let trim_remote_blobs: HashSet<_> = ctx .remote_blobs .iter() - .map(|x| x.get_path().to_str().expect("must be valid name")) + .map(|x| x.get_path()) .filter(|x| x.starts_with("folder1")) - .map(|x| RemotePath::new(Path::new(x)).expect("must be valid name")) + .map(|x| RemotePath::new(x).expect("must be valid path")) .collect(); assert_eq!( nested_remote_files, trim_remote_blobs, @@ -148,10 +149,9 @@ async fn s3_delete_non_exising_works(ctx: &mut MaybeEnabledS3) -> anyhow::Result MaybeEnabledS3::Disabled => return Ok(()), }; - let path = RemotePath::new(&PathBuf::from(format!( - "{}/for_sure_there_is_nothing_there_really", - ctx.base_prefix, - ))) + let path = RemotePath::new(Utf8Path::new( + format!("{}/for_sure_there_is_nothing_there_really", ctx.base_prefix).as_str(), + )) .with_context(|| "RemotePath conversion")?; ctx.client.delete(&path).await.expect("should succeed"); @@ -167,13 +167,13 @@ async fn s3_delete_objects_works(ctx: &mut MaybeEnabledS3) -> anyhow::Result<()> MaybeEnabledS3::Disabled => return Ok(()), }; - let path1 = RemotePath::new(&PathBuf::from(format!("{}/path1", ctx.base_prefix,))) + let path1 = RemotePath::new(Utf8Path::new(format!("{}/path1", ctx.base_prefix).as_str())) .with_context(|| "RemotePath conversion")?; - let path2 = RemotePath::new(&PathBuf::from(format!("{}/path2", ctx.base_prefix,))) + let path2 = RemotePath::new(Utf8Path::new(format!("{}/path2", ctx.base_prefix).as_str())) .with_context(|| "RemotePath conversion")?; - let path3 = RemotePath::new(&PathBuf::from(format!("{}/path3", ctx.base_prefix,))) + let path3 = RemotePath::new(Utf8Path::new(format!("{}/path3", ctx.base_prefix).as_str())) .with_context(|| "RemotePath conversion")?; let data1 = "remote blob data1".as_bytes(); @@ -427,10 +427,10 @@ async fn upload_s3_data( for i in 1..upload_tasks_count + 1 { let task_client = Arc::clone(client); upload_tasks.spawn(async move { - let prefix = PathBuf::from(format!("{base_prefix_str}/sub_prefix_{i}/")); - let blob_prefix = RemotePath::new(&prefix) + let prefix = format!("{base_prefix_str}/sub_prefix_{i}/"); + let blob_prefix = RemotePath::new(Utf8Path::new(&prefix)) .with_context(|| format!("{prefix:?} to RemotePath conversion"))?; - let blob_path = blob_prefix.join(Path::new(&format!("blob_{i}"))); + let blob_path = blob_prefix.join(Utf8Path::new(&format!("blob_{i}"))); debug!("Creating remote item {i} at path {blob_path:?}"); let data = format!("remote blob data {i}").into_bytes(); @@ -512,8 +512,10 @@ async fn upload_simple_s3_data( let task_client = Arc::clone(client); upload_tasks.spawn(async move { let blob_path = PathBuf::from(format!("folder{}/blob_{}.txt", i / 7, i)); - let blob_path = RemotePath::new(&blob_path) - .with_context(|| format!("{blob_path:?} to RemotePath conversion"))?; + let blob_path = RemotePath::new( + Utf8Path::from_path(blob_path.as_path()).expect("must be valid blob path"), + ) + .with_context(|| format!("{blob_path:?} to RemotePath conversion"))?; debug!("Creating remote item {i} at path {blob_path:?}"); let data = format!("remote blob data {i}").into_bytes(); diff --git a/libs/utils/Cargo.toml b/libs/utils/Cargo.toml index 1eb9e6ab4d..27df0265b4 100644 --- a/libs/utils/Cargo.toml +++ b/libs/utils/Cargo.toml @@ -10,6 +10,7 @@ async-trait.workspace = true anyhow.workspace = true bincode.workspace = true bytes.workspace = true +camino.workspace = true chrono.workspace = true heapless.workspace = true hex = { workspace = true, features = ["serde"] } @@ -53,7 +54,7 @@ byteorder.workspace = true bytes.workspace = true criterion.workspace = true hex-literal.workspace = true -tempfile.workspace = true +camino-tempfile.workspace = true [[bench]] name = "benchmarks" diff --git a/libs/utils/src/auth.rs b/libs/utils/src/auth.rs index 716984b64e..54b90fa070 100644 --- a/libs/utils/src/auth.rs +++ b/libs/utils/src/auth.rs @@ -2,9 +2,9 @@ use serde; use std::fs; -use std::path::Path; use anyhow::Result; +use camino::Utf8Path; use jsonwebtoken::{ decode, encode, Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, }; @@ -65,7 +65,7 @@ impl JwtAuth { } } - pub fn from_key_path(key_path: &Path) -> Result { + pub fn from_key_path(key_path: &Utf8Path) -> Result { let public_key = fs::read(key_path)?; Ok(Self::new(DecodingKey::from_ed_pem(&public_key)?)) } diff --git a/libs/utils/src/crashsafe.rs b/libs/utils/src/crashsafe.rs index fd20d2d2ed..b089af4a02 100644 --- a/libs/utils/src/crashsafe.rs +++ b/libs/utils/src/crashsafe.rs @@ -1,14 +1,14 @@ use std::{ borrow::Cow, - ffi::OsStr, fs::{self, File}, io, - path::{Path, PathBuf}, }; +use camino::{Utf8Path, Utf8PathBuf}; + /// Similar to [`std::fs::create_dir`], except we fsync the /// created directory and its parent. -pub fn create_dir(path: impl AsRef) -> io::Result<()> { +pub fn create_dir(path: impl AsRef) -> io::Result<()> { let path = path.as_ref(); fs::create_dir(path)?; @@ -18,7 +18,7 @@ pub fn create_dir(path: impl AsRef) -> io::Result<()> { /// Similar to [`std::fs::create_dir_all`], except we fsync all /// newly created directories and the pre-existing parent. -pub fn create_dir_all(path: impl AsRef) -> io::Result<()> { +pub fn create_dir_all(path: impl AsRef) -> io::Result<()> { let mut path = path.as_ref(); let mut dirs_to_create = Vec::new(); @@ -30,7 +30,7 @@ pub fn create_dir_all(path: impl AsRef) -> io::Result<()> { Ok(_) => { return Err(io::Error::new( io::ErrorKind::AlreadyExists, - format!("non-directory found in path: {}", path.display()), + format!("non-directory found in path: {path}"), )); } Err(ref e) if e.kind() == io::ErrorKind::NotFound => {} @@ -44,7 +44,7 @@ pub fn create_dir_all(path: impl AsRef) -> io::Result<()> { None => { return Err(io::Error::new( io::ErrorKind::InvalidInput, - format!("can't find parent of path '{}'", path.display()).as_str(), + format!("can't find parent of path '{path}'"), )); } } @@ -70,21 +70,18 @@ pub fn create_dir_all(path: impl AsRef) -> io::Result<()> { /// Adds a suffix to the file(directory) name, either appending the suffix to the end of its extension, /// or if there's no extension, creates one and puts a suffix there. -pub fn path_with_suffix_extension(original_path: impl AsRef, suffix: &str) -> PathBuf { - let new_extension = match original_path - .as_ref() - .extension() - .map(OsStr::to_string_lossy) - { +pub fn path_with_suffix_extension( + original_path: impl AsRef, + suffix: &str, +) -> Utf8PathBuf { + let new_extension = match original_path.as_ref().extension() { Some(extension) => Cow::Owned(format!("{extension}.{suffix}")), None => Cow::Borrowed(suffix), }; - original_path - .as_ref() - .with_extension(new_extension.as_ref()) + original_path.as_ref().with_extension(new_extension) } -pub fn fsync_file_and_parent(file_path: &Path) -> io::Result<()> { +pub fn fsync_file_and_parent(file_path: &Utf8Path) -> io::Result<()> { let parent = file_path.parent().ok_or_else(|| { io::Error::new( io::ErrorKind::Other, @@ -97,7 +94,7 @@ pub fn fsync_file_and_parent(file_path: &Path) -> io::Result<()> { Ok(()) } -pub fn fsync(path: &Path) -> io::Result<()> { +pub fn fsync(path: &Utf8Path) -> io::Result<()> { File::open(path) .map_err(|e| io::Error::new(e.kind(), format!("Failed to open the file {path:?}: {e}"))) .and_then(|file| { @@ -111,19 +108,18 @@ pub fn fsync(path: &Path) -> io::Result<()> { .map_err(|e| io::Error::new(e.kind(), format!("Failed to fsync file {path:?}: {e}"))) } -pub async fn fsync_async(path: impl AsRef) -> Result<(), std::io::Error> { - tokio::fs::File::open(path).await?.sync_all().await +pub async fn fsync_async(path: impl AsRef) -> Result<(), std::io::Error> { + tokio::fs::File::open(path.as_ref()).await?.sync_all().await } #[cfg(test)] mod tests { - use tempfile::tempdir; use super::*; #[test] fn test_create_dir_fsyncd() { - let dir = tempdir().unwrap(); + let dir = camino_tempfile::tempdir().unwrap(); let existing_dir_path = dir.path(); let err = create_dir(existing_dir_path).unwrap_err(); @@ -139,7 +135,7 @@ mod tests { #[test] fn test_create_dir_all_fsyncd() { - let dir = tempdir().unwrap(); + let dir = camino_tempfile::tempdir().unwrap(); let existing_dir_path = dir.path(); create_dir_all(existing_dir_path).unwrap(); @@ -166,29 +162,29 @@ mod tests { #[test] fn test_path_with_suffix_extension() { - let p = PathBuf::from("/foo/bar"); + let p = Utf8PathBuf::from("/foo/bar"); assert_eq!( - &path_with_suffix_extension(p, "temp").to_string_lossy(), + &path_with_suffix_extension(p, "temp").to_string(), "/foo/bar.temp" ); - let p = PathBuf::from("/foo/bar"); + let p = Utf8PathBuf::from("/foo/bar"); assert_eq!( - &path_with_suffix_extension(p, "temp.temp").to_string_lossy(), + &path_with_suffix_extension(p, "temp.temp").to_string(), "/foo/bar.temp.temp" ); - let p = PathBuf::from("/foo/bar.baz"); + let p = Utf8PathBuf::from("/foo/bar.baz"); assert_eq!( - &path_with_suffix_extension(p, "temp.temp").to_string_lossy(), + &path_with_suffix_extension(p, "temp.temp").to_string(), "/foo/bar.baz.temp.temp" ); - let p = PathBuf::from("/foo/bar.baz"); + let p = Utf8PathBuf::from("/foo/bar.baz"); assert_eq!( - &path_with_suffix_extension(p, ".temp").to_string_lossy(), + &path_with_suffix_extension(p, ".temp").to_string(), "/foo/bar.baz..temp" ); - let p = PathBuf::from("/foo/bar/dir/"); + let p = Utf8PathBuf::from("/foo/bar/dir/"); assert_eq!( - &path_with_suffix_extension(p, ".temp").to_string_lossy(), + &path_with_suffix_extension(p, ".temp").to_string(), "/foo/bar/dir..temp" ); } diff --git a/libs/utils/src/fs_ext.rs b/libs/utils/src/fs_ext.rs index dfb7d5abbf..90ba348a02 100644 --- a/libs/utils/src/fs_ext.rs +++ b/libs/utils/src/fs_ext.rs @@ -55,8 +55,6 @@ where #[cfg(test)] mod test { - use std::path::PathBuf; - use crate::fs_ext::{is_directory_empty, list_dir}; use super::ignore_absent_files; @@ -65,7 +63,7 @@ mod test { fn is_empty_dir() { use super::PathExt; - let dir = tempfile::tempdir().unwrap(); + let dir = camino_tempfile::tempdir().unwrap(); let dir_path = dir.path(); // test positive case @@ -75,7 +73,7 @@ mod test { ); // invoke on a file to ensure it returns an error - let file_path: PathBuf = dir_path.join("testfile"); + let file_path = dir_path.join("testfile"); let f = std::fs::File::create(&file_path).unwrap(); drop(f); assert!(file_path.is_empty_dir().is_err()); @@ -87,7 +85,7 @@ mod test { #[tokio::test] async fn is_empty_dir_async() { - let dir = tempfile::tempdir().unwrap(); + let dir = camino_tempfile::tempdir().unwrap(); let dir_path = dir.path(); // test positive case @@ -97,7 +95,7 @@ mod test { ); // invoke on a file to ensure it returns an error - let file_path: PathBuf = dir_path.join("testfile"); + let file_path = dir_path.join("testfile"); let f = std::fs::File::create(&file_path).unwrap(); drop(f); assert!(is_directory_empty(&file_path).await.is_err()); @@ -109,10 +107,9 @@ mod test { #[test] fn ignore_absent_files_works() { - let dir = tempfile::tempdir().unwrap(); - let dir_path = dir.path(); + let dir = camino_tempfile::tempdir().unwrap(); - let file_path: PathBuf = dir_path.join("testfile"); + let file_path = dir.path().join("testfile"); ignore_absent_files(|| std::fs::remove_file(&file_path)).expect("should execute normally"); @@ -126,17 +123,17 @@ mod test { #[tokio::test] async fn list_dir_works() { - let dir = tempfile::tempdir().unwrap(); + let dir = camino_tempfile::tempdir().unwrap(); let dir_path = dir.path(); assert!(list_dir(dir_path).await.unwrap().is_empty()); - let file_path: PathBuf = dir_path.join("testfile"); + let file_path = dir_path.join("testfile"); let _ = std::fs::File::create(&file_path).unwrap(); assert_eq!(&list_dir(dir_path).await.unwrap(), &["testfile"]); - let another_dir_path: PathBuf = dir_path.join("testdir"); + let another_dir_path = dir_path.join("testdir"); std::fs::create_dir(another_dir_path).unwrap(); let expected = &["testdir", "testfile"]; diff --git a/libs/utils/src/http/error.rs b/libs/utils/src/http/error.rs index dd54cd6ecd..7233d3a662 100644 --- a/libs/utils/src/http/error.rs +++ b/libs/utils/src/http/error.rs @@ -1,8 +1,9 @@ use hyper::{header, Body, Response, StatusCode}; use serde::{Deserialize, Serialize}; +use std::borrow::Cow; use std::error::Error as StdError; use thiserror::Error; -use tracing::error; +use tracing::{error, info}; #[derive(Debug, Error)] pub enum ApiError { @@ -24,6 +25,9 @@ pub enum ApiError { #[error("Precondition failed: {0}")] PreconditionFailed(Box), + #[error("Resource temporarily unavailable: {0}")] + ResourceUnavailable(Cow<'static, str>), + #[error("Shutting down")] ShuttingDown, @@ -59,6 +63,10 @@ impl ApiError { "Shutting down".to_string(), StatusCode::SERVICE_UNAVAILABLE, ), + ApiError::ResourceUnavailable(err) => HttpErrorBody::response_from_msg_and_status( + err.to_string(), + StatusCode::SERVICE_UNAVAILABLE, + ), ApiError::InternalServerError(err) => HttpErrorBody::response_from_msg_and_status( err.to_string(), StatusCode::INTERNAL_SERVER_ERROR, @@ -108,10 +116,12 @@ pub async fn route_error_handler(err: routerify::RouteError) -> Response { pub fn api_error_handler(api_error: ApiError) -> Response { // Print a stack trace for Internal Server errors - if let ApiError::InternalServerError(_) = api_error { - error!("Error processing HTTP request: {api_error:?}"); - } else { - error!("Error processing HTTP request: {api_error:#}"); + + match api_error { + ApiError::ResourceUnavailable(_) => info!("Error processing HTTP request: {api_error:#}"), + ApiError::NotFound(_) => info!("Error processing HTTP request: {api_error:#}"), + ApiError::InternalServerError(_) => error!("Error processing HTTP request: {api_error:?}"), + _ => error!("Error processing HTTP request: {api_error:#}"), } api_error.into_response() diff --git a/libs/utils/src/id.rs b/libs/utils/src/id.rs index 2ce92ee914..ec13c2f96f 100644 --- a/libs/utils/src/id.rs +++ b/libs/utils/src/id.rs @@ -1,4 +1,3 @@ -use std::ffi::OsStr; use std::{fmt, str::FromStr}; use anyhow::Context; @@ -215,12 +214,11 @@ pub struct TimelineId(Id); id_newtype!(TimelineId); -impl TryFrom> for TimelineId { +impl TryFrom> for TimelineId { type Error = anyhow::Error; - fn try_from(value: Option<&OsStr>) -> Result { + fn try_from(value: Option<&str>) -> Result { value - .and_then(OsStr::to_str) .unwrap_or_default() .parse::() .with_context(|| format!("Could not parse timeline id from {:?}", value)) diff --git a/libs/utils/src/lock_file.rs b/libs/utils/src/lock_file.rs index ca8295040c..987b9d9ad2 100644 --- a/libs/utils/src/lock_file.rs +++ b/libs/utils/src/lock_file.rs @@ -11,10 +11,10 @@ use std::{ io::{Read, Write}, ops::Deref, os::unix::prelude::AsRawFd, - path::{Path, PathBuf}, }; use anyhow::Context; +use camino::{Utf8Path, Utf8PathBuf}; use nix::{errno::Errno::EAGAIN, fcntl}; use crate::crashsafe; @@ -23,7 +23,7 @@ use crate::crashsafe; /// Returned by [`create_exclusive`]. #[must_use] pub struct UnwrittenLockFile { - path: PathBuf, + path: Utf8PathBuf, file: fs::File, } @@ -60,7 +60,7 @@ impl UnwrittenLockFile { /// /// It is not an error if the file already exists. /// It is an error if the file is already locked. -pub fn create_exclusive(lock_file_path: &Path) -> anyhow::Result { +pub fn create_exclusive(lock_file_path: &Utf8Path) -> anyhow::Result { let lock_file = fs::OpenOptions::new() .create(true) // O_CREAT .write(true) @@ -101,7 +101,7 @@ pub enum LockFileRead { /// Open & try to lock the lock file at the given `path`, returning a [handle][`LockFileRead`] to /// inspect its content. It is not an `Err(...)` if the file does not exist or is already locked. /// Check the [`LockFileRead`] variants for details. -pub fn read_and_hold_lock_file(path: &Path) -> anyhow::Result { +pub fn read_and_hold_lock_file(path: &Utf8Path) -> anyhow::Result { let res = fs::OpenOptions::new().read(true).open(path); let mut lock_file = match res { Ok(f) => f, diff --git a/libs/utils/src/logging.rs b/libs/utils/src/logging.rs index 7f17970c4c..502e02dc71 100644 --- a/libs/utils/src/logging.rs +++ b/libs/utils/src/logging.rs @@ -228,6 +228,12 @@ impl SecretString { } } +impl From for SecretString { + fn from(s: String) -> Self { + Self(s) + } +} + impl std::fmt::Debug for SecretString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "[SECRET]") diff --git a/libs/utils/src/lsn.rs b/libs/utils/src/lsn.rs index 0493d43088..7d9baf7d49 100644 --- a/libs/utils/src/lsn.rs +++ b/libs/utils/src/lsn.rs @@ -1,9 +1,9 @@ #![warn(missing_docs)] +use camino::Utf8Path; use serde::{Deserialize, Serialize}; use std::fmt; use std::ops::{Add, AddAssign}; -use std::path::Path; use std::str::FromStr; use std::sync::atomic::{AtomicU64, Ordering}; @@ -44,11 +44,9 @@ impl Lsn { /// Parse an LSN from a filename in the form `0000000000000000` pub fn from_filename(filename: F) -> Result where - F: AsRef, + F: AsRef, { - let filename: &Path = filename.as_ref(); - let filename = filename.to_str().ok_or(LsnParseError)?; - Lsn::from_hex(filename) + Lsn::from_hex(filename.as_ref().as_str()) } /// Parse an LSN from a string in the form `0000000000000000` diff --git a/libs/utils/src/pid_file.rs b/libs/utils/src/pid_file.rs index e634b08f2a..06f5d950d1 100644 --- a/libs/utils/src/pid_file.rs +++ b/libs/utils/src/pid_file.rs @@ -49,9 +49,10 @@ //! At this point, `B` and `C` are running, which is hazardous. //! Morale of the story: don't unlink pidfiles, ever. -use std::{ops::Deref, path::Path}; +use std::ops::Deref; use anyhow::Context; +use camino::Utf8Path; use nix::unistd::Pid; use crate::lock_file::{self, LockFileRead}; @@ -84,7 +85,7 @@ impl Deref for PidFileGuard { /// The claim ends as soon as the returned guard object is dropped. /// To maintain the claim for the remaining lifetime of the current process, /// use [`std::mem::forget`] or similar. -pub fn claim_for_current_process(path: &Path) -> anyhow::Result { +pub fn claim_for_current_process(path: &Utf8Path) -> anyhow::Result { let unwritten_lock_file = lock_file::create_exclusive(path).context("lock file")?; // if any of the next steps fail, we drop the file descriptor and thereby release the lock let guard = unwritten_lock_file @@ -132,7 +133,7 @@ pub enum PidFileRead { /// /// On success, this function returns a [`PidFileRead`]. /// Check its docs for a description of the meaning of its different variants. -pub fn read(pidfile: &Path) -> anyhow::Result { +pub fn read(pidfile: &Utf8Path) -> anyhow::Result { let res = lock_file::read_and_hold_lock_file(pidfile).context("read and hold pid file")?; let ret = match res { LockFileRead::NotExist => PidFileRead::NotExist, diff --git a/libs/utils/src/seqwait.rs b/libs/utils/src/seqwait.rs index 014887392e..5bc7ca91d6 100644 --- a/libs/utils/src/seqwait.rs +++ b/libs/utils/src/seqwait.rs @@ -58,7 +58,7 @@ where // to get that. impl PartialOrd for Waiter { fn partial_cmp(&self, other: &Self) -> Option { - other.wake_num.partial_cmp(&self.wake_num) + Some(self.cmp(other)) } } diff --git a/libs/vm_monitor/src/runner.rs b/libs/vm_monitor/src/runner.rs index 09863c8936..b0ee5f0310 100644 --- a/libs/vm_monitor/src/runner.rs +++ b/libs/vm_monitor/src/runner.rs @@ -4,9 +4,9 @@ //! This is the "Monitor" part of the monitor binary and is the main entrypoint for //! all functionality. +use std::fmt::Debug; use std::sync::Arc; use std::time::{Duration, Instant}; -use std::{fmt::Debug, mem}; use anyhow::{bail, Context}; use axum::extract::ws::{Message, WebSocket}; @@ -141,14 +141,6 @@ impl Runner { ); state.cgroup = Some(cgroup); - } else { - // *NOTE*: We need to forget the sender so that its drop impl does not get ran. - // This allows us to poll it in `Monitor::run` regardless of whether we - // are managing a cgroup or not. If we don't forget it, all receives will - // immediately return an error because the sender is droped and it will - // claim all select! statements, effectively turning `Monitor::run` into - // `loop { fail to receive }`. - mem::forget(requesting_send); } let mut file_cache_reserved_bytes = 0; @@ -417,7 +409,7 @@ impl Runner { } } // we need to propagate an upscale request - request = self.dispatcher.request_upscale_events.recv() => { + request = self.dispatcher.request_upscale_events.recv(), if self.cgroup.is_some() => { if request.is_none() { bail!("failed to listen for upscale event from cgroup") } diff --git a/pageserver/Cargo.toml b/pageserver/Cargo.toml index 9cb71dea09..3eb01003df 100644 --- a/pageserver/Cargo.toml +++ b/pageserver/Cargo.toml @@ -17,6 +17,8 @@ async-stream.workspace = true async-trait.workspace = true byteorder.workspace = true bytes.workspace = true +camino.workspace = true +camino-tempfile.workspace = true chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["string"] } close_fds.workspace = true @@ -80,7 +82,6 @@ enum-map.workspace = true enumset.workspace = true strum.workspace = true strum_macros.workspace = true -tempfile.workspace = true [dev-dependencies] criterion.workspace = true diff --git a/pageserver/benches/bench_walredo.rs b/pageserver/benches/bench_walredo.rs index 49216c68f1..6cda86fafa 100644 --- a/pageserver/benches/bench_walredo.rs +++ b/pageserver/benches/bench_walredo.rs @@ -25,7 +25,7 @@ fn redo_scenarios(c: &mut Criterion) { // input to the stderr. // utils::logging::init(utils::logging::LogFormat::Plain).unwrap(); - let repo_dir = tempfile::tempdir_in(env!("CARGO_TARGET_TMPDIR")).unwrap(); + let repo_dir = camino_tempfile::tempdir_in(env!("CARGO_TARGET_TMPDIR")).unwrap(); let conf = PageServerConf::dummy_conf(repo_dir.path().to_path_buf()); let conf = Box::leak(Box::new(conf)); diff --git a/pageserver/ctl/Cargo.toml b/pageserver/ctl/Cargo.toml index b3f12b72b3..ff0c530125 100644 --- a/pageserver/ctl/Cargo.toml +++ b/pageserver/ctl/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true [dependencies] anyhow.workspace = true bytes.workspace = true +camino.workspace = true clap = { workspace = true, features = ["string"] } git-version.workspace = true pageserver = { path = ".." } diff --git a/pageserver/ctl/src/layer_map_analyzer.rs b/pageserver/ctl/src/layer_map_analyzer.rs index de7b4861cb..15d4eb09e0 100644 --- a/pageserver/ctl/src/layer_map_analyzer.rs +++ b/pageserver/ctl/src/layer_map_analyzer.rs @@ -3,13 +3,14 @@ //! Currently it only analyzes holes, which are regions within the layer range that the layer contains no updates for. In the future it might do more analysis (maybe key quantiles?) but it should never return sensitive data. use anyhow::Result; +use camino::{Utf8Path, Utf8PathBuf}; use pageserver::context::{DownloadBehavior, RequestContext}; use pageserver::task_mgr::TaskKind; use pageserver::tenant::{TENANTS_SEGMENT_NAME, TIMELINES_SEGMENT_NAME}; use std::cmp::Ordering; use std::collections::BinaryHeap; use std::ops::Range; -use std::{fs, path::Path, str}; +use std::{fs, str}; use pageserver::page_cache::PAGE_SZ; use pageserver::repository::{Key, KEY_SIZE}; @@ -98,7 +99,7 @@ pub(crate) fn parse_filename(name: &str) -> Option { } // Finds the max_holes largest holes, ignoring any that are smaller than MIN_HOLE_LENGTH" -async fn get_holes(path: &Path, max_holes: usize, ctx: &RequestContext) -> Result> { +async fn get_holes(path: &Utf8Path, max_holes: usize, ctx: &RequestContext) -> Result> { let file = FileBlockReader::new(VirtualFile::open(path).await?); let summary_blk = file.read_blk(0, ctx).await?; let actual_summary = Summary::des_prefix(summary_blk.as_ref())?; @@ -167,7 +168,9 @@ pub(crate) async fn main(cmd: &AnalyzeLayerMapCmd) -> Result<()> { parse_filename(&layer.file_name().into_string().unwrap()) { if layer_file.is_delta { - layer_file.holes = get_holes(&layer.path(), max_holes, &ctx).await?; + let layer_path = + Utf8PathBuf::from_path_buf(layer.path()).expect("non-Unicode path"); + layer_file.holes = get_holes(&layer_path, max_holes, &ctx).await?; n_deltas += 1; } layers.push(layer_file); diff --git a/pageserver/ctl/src/layers.rs b/pageserver/ctl/src/layers.rs index e8d16d31f1..22ebe70b16 100644 --- a/pageserver/ctl/src/layers.rs +++ b/pageserver/ctl/src/layers.rs @@ -1,6 +1,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; +use camino::Utf8Path; use clap::Subcommand; use pageserver::context::{DownloadBehavior, RequestContext}; use pageserver::task_mgr::TaskKind; @@ -47,7 +48,7 @@ pub(crate) enum LayerCmd { } async fn read_delta_file(path: impl AsRef, ctx: &RequestContext) -> Result<()> { - let path = path.as_ref(); + let path = Utf8Path::from_path(path.as_ref()).expect("non-Unicode path"); virtual_file::init(10); page_cache::init(100); let file = FileBlockReader::new(VirtualFile::open(path).await?); diff --git a/pageserver/ctl/src/main.rs b/pageserver/ctl/src/main.rs index 0d154aca0c..c4d6e8d883 100644 --- a/pageserver/ctl/src/main.rs +++ b/pageserver/ctl/src/main.rs @@ -8,6 +8,7 @@ mod draw_timeline_dir; mod layer_map_analyzer; mod layers; +use camino::{Utf8Path, Utf8PathBuf}; use clap::{Parser, Subcommand}; use layers::LayerCmd; use pageserver::{ @@ -18,7 +19,6 @@ use pageserver::{ virtual_file, }; use postgres_ffi::ControlFileData; -use std::path::{Path, PathBuf}; use utils::{lsn::Lsn, project_git_version}; project_git_version!(GIT_VERSION); @@ -49,7 +49,7 @@ enum Commands { #[derive(Parser)] struct MetadataCmd { /// Input metadata file path - metadata_path: PathBuf, + metadata_path: Utf8PathBuf, /// Replace disk consistent Lsn disk_consistent_lsn: Option, /// Replace previous record Lsn @@ -61,13 +61,13 @@ struct MetadataCmd { #[derive(Parser)] struct PrintLayerFileCmd { /// Pageserver data path - path: PathBuf, + path: Utf8PathBuf, } #[derive(Parser)] struct AnalyzeLayerMapCmd { /// Pageserver data path - path: PathBuf, + path: Utf8PathBuf, /// Max holes max_holes: Option, } @@ -102,7 +102,7 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -fn read_pg_control_file(control_file_path: &Path) -> anyhow::Result<()> { +fn read_pg_control_file(control_file_path: &Utf8Path) -> anyhow::Result<()> { let control_file = ControlFileData::decode(&std::fs::read(control_file_path)?)?; println!("{control_file:?}"); let control_file_initdb = Lsn(control_file.checkPoint); @@ -114,7 +114,7 @@ fn read_pg_control_file(control_file_path: &Path) -> anyhow::Result<()> { Ok(()) } -async fn print_layerfile(path: &Path) -> anyhow::Result<()> { +async fn print_layerfile(path: &Utf8Path) -> anyhow::Result<()> { // Basic initialization of things that don't change after startup virtual_file::init(10); page_cache::init(100); diff --git a/pageserver/src/bin/pageserver.rs b/pageserver/src/bin/pageserver.rs index d8a00b677b..7e2b376212 100644 --- a/pageserver/src/bin/pageserver.rs +++ b/pageserver/src/bin/pageserver.rs @@ -2,9 +2,10 @@ use std::env::{var, VarError}; use std::sync::Arc; -use std::{env, ops::ControlFlow, path::Path, str::FromStr}; +use std::{env, ops::ControlFlow, str::FromStr}; use anyhow::{anyhow, Context}; +use camino::Utf8Path; use clap::{Arg, ArgAction, Command}; use metrics::launch_timestamp::{set_launch_timestamp_metric, LaunchTimestamp}; @@ -65,21 +66,17 @@ fn main() -> anyhow::Result<()> { let workdir = arg_matches .get_one::("workdir") - .map(Path::new) - .unwrap_or_else(|| Path::new(".neon")); + .map(Utf8Path::new) + .unwrap_or_else(|| Utf8Path::new(".neon")); let workdir = workdir - .canonicalize() - .with_context(|| format!("Error opening workdir '{}'", workdir.display()))?; + .canonicalize_utf8() + .with_context(|| format!("Error opening workdir '{workdir}'"))?; let cfg_file_path = workdir.join("pageserver.toml"); // Set CWD to workdir for non-daemon modes - env::set_current_dir(&workdir).with_context(|| { - format!( - "Failed to set application's current dir to '{}'", - workdir.display() - ) - })?; + env::set_current_dir(&workdir) + .with_context(|| format!("Failed to set application's current dir to '{workdir}'"))?; let conf = match initialize_config(&cfg_file_path, arg_matches, &workdir)? { ControlFlow::Continue(conf) => conf, @@ -115,12 +112,8 @@ fn main() -> anyhow::Result<()> { let tenants_path = conf.tenants_path(); if !tenants_path.exists() { - utils::crashsafe::create_dir_all(conf.tenants_path()).with_context(|| { - format!( - "Failed to create tenants root dir at '{}'", - tenants_path.display() - ) - })?; + utils::crashsafe::create_dir_all(conf.tenants_path()) + .with_context(|| format!("Failed to create tenants root dir at '{tenants_path}'"))?; } // Initialize up failpoints support @@ -137,9 +130,9 @@ fn main() -> anyhow::Result<()> { } fn initialize_config( - cfg_file_path: &Path, + cfg_file_path: &Utf8Path, arg_matches: clap::ArgMatches, - workdir: &Path, + workdir: &Utf8Path, ) -> anyhow::Result> { let init = arg_matches.get_flag("init"); let update_config = init || arg_matches.get_flag("update-config"); @@ -147,33 +140,22 @@ fn initialize_config( let (mut toml, config_file_exists) = if cfg_file_path.is_file() { if init { anyhow::bail!( - "Config file '{}' already exists, cannot init it, use --update-config to update it", - cfg_file_path.display() + "Config file '{cfg_file_path}' already exists, cannot init it, use --update-config to update it", ); } // Supplement the CLI arguments with the config file - let cfg_file_contents = std::fs::read_to_string(cfg_file_path).with_context(|| { - format!( - "Failed to read pageserver config at '{}'", - cfg_file_path.display() - ) - })?; + let cfg_file_contents = std::fs::read_to_string(cfg_file_path) + .with_context(|| format!("Failed to read pageserver config at '{cfg_file_path}'"))?; ( cfg_file_contents .parse::() .with_context(|| { - format!( - "Failed to parse '{}' as pageserver config", - cfg_file_path.display() - ) + format!("Failed to parse '{cfg_file_path}' as pageserver config") })?, true, ) } else if cfg_file_path.exists() { - anyhow::bail!( - "Config file '{}' exists but is not a regular file", - cfg_file_path.display() - ); + anyhow::bail!("Config file '{cfg_file_path}' exists but is not a regular file"); } else { // We're initializing the tenant, so there's no config file yet ( @@ -192,7 +174,7 @@ fn initialize_config( for (key, item) in doc.iter() { if config_file_exists && update_config && key == "id" && toml.contains_key(key) { - anyhow::bail!("Pageserver config file exists at '{}' and has node id already, it cannot be overridden", cfg_file_path.display()); + anyhow::bail!("Pageserver config file exists at '{cfg_file_path}' and has node id already, it cannot be overridden"); } toml.insert(key, item.clone()); } @@ -204,18 +186,11 @@ fn initialize_config( .context("Failed to parse pageserver configuration")?; if update_config { - info!("Writing pageserver config to '{}'", cfg_file_path.display()); + info!("Writing pageserver config to '{cfg_file_path}'"); - std::fs::write(cfg_file_path, toml.to_string()).with_context(|| { - format!( - "Failed to write pageserver config to '{}'", - cfg_file_path.display() - ) - })?; - info!( - "Config successfully written to '{}'", - cfg_file_path.display() - ) + std::fs::write(cfg_file_path, toml.to_string()) + .with_context(|| format!("Failed to write pageserver config to '{cfg_file_path}'"))?; + info!("Config successfully written to '{cfg_file_path}'") } Ok(if init { diff --git a/pageserver/src/config.rs b/pageserver/src/config.rs index c3f2f14a74..fe62a7299a 100644 --- a/pageserver/src/config.rs +++ b/pageserver/src/config.rs @@ -16,13 +16,13 @@ use utils::logging::SecretString; use once_cell::sync::OnceCell; use reqwest::Url; use std::num::NonZeroUsize; -use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use toml_edit; use toml_edit::{Document, Item}; +use camino::{Utf8Path, Utf8PathBuf}; use postgres_backend::AuthType; use utils::{ id::{NodeId, TenantId, TimelineId}, @@ -37,8 +37,8 @@ use crate::tenant::{ TIMELINES_SEGMENT_NAME, }; use crate::{ - IGNORED_TENANT_FILE_NAME, METADATA_FILE_NAME, TENANT_CONFIG_NAME, TIMELINE_DELETE_MARK_SUFFIX, - TIMELINE_UNINIT_MARK_SUFFIX, + IGNORED_TENANT_FILE_NAME, METADATA_FILE_NAME, TENANT_CONFIG_NAME, TENANT_LOCATION_CONFIG_NAME, + TIMELINE_DELETE_MARK_SUFFIX, TIMELINE_UNINIT_MARK_SUFFIX, }; pub mod defaults { @@ -153,9 +153,9 @@ pub struct PageServerConf { // that during unit testing, because the current directory is global // to the process but different unit tests work on different // repositories. - pub workdir: PathBuf, + pub workdir: Utf8PathBuf, - pub pg_distrib_dir: PathBuf, + pub pg_distrib_dir: Utf8PathBuf, // Authentication /// authentication method for the HTTP mgmt API @@ -164,7 +164,7 @@ pub struct PageServerConf { pub pg_auth_type: AuthType, /// Path to a file containing public key for verifying JWT tokens. /// Used for both mgmt and compute auth, if enabled. - pub auth_validation_public_key_path: Option, + pub auth_validation_public_key_path: Option, pub remote_storage_config: Option, @@ -211,6 +211,10 @@ pub struct PageServerConf { /// JWT token for use with the control plane API. pub control_plane_api_token: Option, + + /// If true, pageserver will make best-effort to operate without a control plane: only + /// for use in major incidents. + pub control_plane_emergency_mode: bool, } /// We do not want to store this in a PageServerConf because the latter may be logged @@ -253,15 +257,15 @@ struct PageServerConfigBuilder { page_cache_size: BuilderValue, max_file_descriptors: BuilderValue, - workdir: BuilderValue, + workdir: BuilderValue, - pg_distrib_dir: BuilderValue, + pg_distrib_dir: BuilderValue, http_auth_type: BuilderValue, pg_auth_type: BuilderValue, // - auth_validation_public_key_path: BuilderValue>, + auth_validation_public_key_path: BuilderValue>, remote_storage_config: BuilderValue>, id: BuilderValue, @@ -288,6 +292,7 @@ struct PageServerConfigBuilder { control_plane_api: BuilderValue>, control_plane_api_token: BuilderValue>, + control_plane_emergency_mode: BuilderValue, } impl Default for PageServerConfigBuilder { @@ -305,10 +310,12 @@ impl Default for PageServerConfigBuilder { superuser: Set(DEFAULT_SUPERUSER.to_string()), page_cache_size: Set(DEFAULT_PAGE_CACHE_SIZE), max_file_descriptors: Set(DEFAULT_MAX_FILE_DESCRIPTORS), - workdir: Set(PathBuf::new()), - pg_distrib_dir: Set(env::current_dir() - .expect("cannot access current directory") - .join("pg_install")), + workdir: Set(Utf8PathBuf::new()), + pg_distrib_dir: Set(Utf8PathBuf::from_path_buf( + env::current_dir().expect("cannot access current directory"), + ) + .expect("non-Unicode path") + .join("pg_install")), http_auth_type: Set(AuthType::Trust), pg_auth_type: Set(AuthType::Trust), auth_validation_public_key_path: Set(None), @@ -353,6 +360,7 @@ impl Default for PageServerConfigBuilder { control_plane_api: Set(None), control_plane_api_token: Set(None), + control_plane_emergency_mode: Set(false), } } } @@ -390,11 +398,11 @@ impl PageServerConfigBuilder { self.max_file_descriptors = BuilderValue::Set(max_file_descriptors) } - pub fn workdir(&mut self, workdir: PathBuf) { + pub fn workdir(&mut self, workdir: Utf8PathBuf) { self.workdir = BuilderValue::Set(workdir) } - pub fn pg_distrib_dir(&mut self, pg_distrib_dir: PathBuf) { + pub fn pg_distrib_dir(&mut self, pg_distrib_dir: Utf8PathBuf) { self.pg_distrib_dir = BuilderValue::Set(pg_distrib_dir) } @@ -408,7 +416,7 @@ impl PageServerConfigBuilder { pub fn auth_validation_public_key_path( &mut self, - auth_validation_public_key_path: Option, + auth_validation_public_key_path: Option, ) { self.auth_validation_public_key_path = BuilderValue::Set(auth_validation_public_key_path) } @@ -485,6 +493,14 @@ impl PageServerConfigBuilder { self.control_plane_api = BuilderValue::Set(api) } + pub fn control_plane_api_token(&mut self, token: Option) { + self.control_plane_api_token = BuilderValue::Set(token) + } + + pub fn control_plane_emergency_mode(&mut self, enabled: bool) { + self.control_plane_emergency_mode = BuilderValue::Set(enabled) + } + pub fn build(self) -> anyhow::Result { let concurrent_tenant_size_logical_size_queries = self .concurrent_tenant_size_logical_size_queries @@ -576,6 +592,9 @@ impl PageServerConfigBuilder { control_plane_api_token: self .control_plane_api_token .ok_or(anyhow!("missing control_plane_api_token"))?, + control_plane_emergency_mode: self + .control_plane_emergency_mode + .ok_or(anyhow!("missing control_plane_emergency_mode"))?, }) } } @@ -585,15 +604,15 @@ impl PageServerConf { // Repository paths, relative to workdir. // - pub fn tenants_path(&self) -> PathBuf { + pub fn tenants_path(&self) -> Utf8PathBuf { self.workdir.join(TENANTS_SEGMENT_NAME) } - pub fn deletion_prefix(&self) -> PathBuf { + pub fn deletion_prefix(&self) -> Utf8PathBuf { self.workdir.join("deletion") } - pub fn deletion_list_path(&self, sequence: u64) -> PathBuf { + pub fn deletion_list_path(&self, sequence: u64) -> Utf8PathBuf { // Encode a version in the filename, so that if we ever switch away from JSON we can // increment this. const VERSION: u8 = 1; @@ -602,7 +621,7 @@ impl PageServerConf { .join(format!("{sequence:016x}-{VERSION:02x}.list")) } - pub fn deletion_header_path(&self) -> PathBuf { + pub fn deletion_header_path(&self) -> Utf8PathBuf { // Encode a version in the filename, so that if we ever switch away from JSON we can // increment this. const VERSION: u8 = 1; @@ -610,30 +629,38 @@ impl PageServerConf { self.deletion_prefix().join(format!("header-{VERSION:02x}")) } - pub fn tenant_path(&self, tenant_id: &TenantId) -> PathBuf { + pub fn tenant_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.tenants_path().join(tenant_id.to_string()) } - pub fn tenant_attaching_mark_file_path(&self, tenant_id: &TenantId) -> PathBuf { + pub fn tenant_attaching_mark_file_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.tenant_path(tenant_id) .join(TENANT_ATTACHING_MARKER_FILENAME) } - pub fn tenant_ignore_mark_file_path(&self, tenant_id: &TenantId) -> PathBuf { + pub fn tenant_ignore_mark_file_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.tenant_path(tenant_id).join(IGNORED_TENANT_FILE_NAME) } /// Points to a place in pageserver's local directory, /// where certain tenant's tenantconf file should be located. - pub fn tenant_config_path(&self, tenant_id: &TenantId) -> PathBuf { + /// + /// Legacy: superseded by tenant_location_config_path. Eventually + /// remove this function. + pub fn tenant_config_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.tenant_path(tenant_id).join(TENANT_CONFIG_NAME) } - pub fn timelines_path(&self, tenant_id: &TenantId) -> PathBuf { + pub fn tenant_location_config_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { + self.tenant_path(tenant_id) + .join(TENANT_LOCATION_CONFIG_NAME) + } + + pub fn timelines_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.tenant_path(tenant_id).join(TIMELINES_SEGMENT_NAME) } - pub fn timeline_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> PathBuf { + pub fn timeline_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> Utf8PathBuf { self.timelines_path(tenant_id).join(timeline_id.to_string()) } @@ -641,7 +668,7 @@ impl PageServerConf { &self, tenant_id: TenantId, timeline_id: TimelineId, - ) -> PathBuf { + ) -> Utf8PathBuf { path_with_suffix_extension( self.timeline_path(&tenant_id, &timeline_id), TIMELINE_UNINIT_MARK_SUFFIX, @@ -652,19 +679,19 @@ impl PageServerConf { &self, tenant_id: TenantId, timeline_id: TimelineId, - ) -> PathBuf { + ) -> Utf8PathBuf { path_with_suffix_extension( self.timeline_path(&tenant_id, &timeline_id), TIMELINE_DELETE_MARK_SUFFIX, ) } - pub fn tenant_deleted_mark_file_path(&self, tenant_id: &TenantId) -> PathBuf { + pub fn tenant_deleted_mark_file_path(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.tenant_path(tenant_id) .join(TENANT_DELETED_MARKER_FILE_NAME) } - pub fn traces_path(&self) -> PathBuf { + pub fn traces_path(&self) -> Utf8PathBuf { self.workdir.join("traces") } @@ -673,7 +700,7 @@ impl PageServerConf { tenant_id: &TenantId, timeline_id: &TimelineId, connection_id: &ConnectionId, - ) -> PathBuf { + ) -> Utf8PathBuf { self.traces_path() .join(tenant_id.to_string()) .join(timeline_id.to_string()) @@ -682,20 +709,20 @@ impl PageServerConf { /// Points to a place in pageserver's local directory, /// where certain timeline's metadata file should be located. - pub fn metadata_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> PathBuf { + pub fn metadata_path(&self, tenant_id: &TenantId, timeline_id: &TimelineId) -> Utf8PathBuf { self.timeline_path(tenant_id, timeline_id) .join(METADATA_FILE_NAME) } /// Turns storage remote path of a file into its local path. - pub fn local_path(&self, remote_path: &RemotePath) -> PathBuf { + pub fn local_path(&self, remote_path: &RemotePath) -> Utf8PathBuf { remote_path.with_base(&self.workdir) } // // Postgres distribution paths // - pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result { + pub fn pg_distrib_dir(&self, pg_version: u32) -> anyhow::Result { let path = self.pg_distrib_dir.clone(); #[allow(clippy::manual_range_patterns)] @@ -705,10 +732,10 @@ impl PageServerConf { } } - pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result { + pub fn pg_bin_dir(&self, pg_version: u32) -> anyhow::Result { Ok(self.pg_distrib_dir(pg_version)?.join("bin")) } - pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result { + pub fn pg_lib_dir(&self, pg_version: u32) -> anyhow::Result { Ok(self.pg_distrib_dir(pg_version)?.join("lib")) } @@ -716,7 +743,7 @@ impl PageServerConf { /// validating the input and failing on errors. /// /// This leaves any options not present in the file in the built-in defaults. - pub fn parse_and_validate(toml: &Document, workdir: &Path) -> anyhow::Result { + pub fn parse_and_validate(toml: &Document, workdir: &Utf8Path) -> anyhow::Result { let mut builder = PageServerConfigBuilder::default(); builder.workdir(workdir.to_owned()); @@ -735,10 +762,10 @@ impl PageServerConf { builder.max_file_descriptors(parse_toml_u64(key, item)? as usize) } "pg_distrib_dir" => { - builder.pg_distrib_dir(PathBuf::from(parse_toml_string(key, item)?)) + builder.pg_distrib_dir(Utf8PathBuf::from(parse_toml_string(key, item)?)) } "auth_validation_public_key_path" => builder.auth_validation_public_key_path(Some( - PathBuf::from(parse_toml_string(key, item)?), + Utf8PathBuf::from(parse_toml_string(key, item)?), )), "http_auth_type" => builder.http_auth_type(parse_toml_from_str(key, item)?), "pg_auth_type" => builder.pg_auth_type(parse_toml_from_str(key, item)?), @@ -785,6 +812,18 @@ impl PageServerConf { builder.control_plane_api(Some(parsed.parse().context("failed to parse control plane URL")?)) } }, + "control_plane_api_token" => { + let parsed = parse_toml_string(key, item)?; + if parsed.is_empty() { + builder.control_plane_api_token(None) + } else { + builder.control_plane_api_token(Some(parsed.into())) + } + }, + "control_plane_emergency_mode" => { + builder.control_plane_emergency_mode(parse_toml_bool(key, item)?) + + }, _ => bail!("unrecognized pageserver option '{key}'"), } } @@ -798,8 +837,7 @@ impl PageServerConf { ensure!( auth_validation_public_key_path.exists(), format!( - "Can't find auth_validation_public_key at '{}'", - auth_validation_public_key_path.display() + "Can't find auth_validation_public_key at '{auth_validation_public_key_path}'", ) ); } @@ -915,12 +953,12 @@ impl PageServerConf { } #[cfg(test)] - pub fn test_repo_dir(test_name: &str) -> PathBuf { - PathBuf::from(format!("../tmp_check/test_{test_name}")) + pub fn test_repo_dir(test_name: &str) -> Utf8PathBuf { + Utf8PathBuf::from(format!("../tmp_check/test_{test_name}")) } - pub fn dummy_conf(repo_dir: PathBuf) -> Self { - let pg_distrib_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../pg_install"); + pub fn dummy_conf(repo_dir: Utf8PathBuf) -> Self { + let pg_distrib_dir = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../pg_install"); PageServerConf { id: NodeId(0), @@ -955,6 +993,7 @@ impl PageServerConf { background_task_maximum_delay: Duration::ZERO, control_plane_api: None, control_plane_api_token: None, + control_plane_emergency_mode: false, } } } @@ -1087,8 +1126,8 @@ mod tests { num::{NonZeroU32, NonZeroUsize}, }; + use camino_tempfile::{tempdir, Utf8TempDir}; use remote_storage::{RemoteStorageKind, S3Config}; - use tempfile::{tempdir, TempDir}; use utils::serde_percent::Percent; use super::*; @@ -1127,8 +1166,7 @@ background_task_maximum_delay = '334 s' let broker_endpoint = storage_broker::DEFAULT_ENDPOINT; // we have to create dummy values to overcome the validation errors let config_string = format!( - "pg_distrib_dir='{}'\nid=10\nbroker_endpoint = '{broker_endpoint}'", - pg_distrib_dir.display() + "pg_distrib_dir='{pg_distrib_dir}'\nid=10\nbroker_endpoint = '{broker_endpoint}'", ); let toml = config_string.parse()?; @@ -1179,7 +1217,8 @@ background_task_maximum_delay = '334 s' defaults::DEFAULT_BACKGROUND_TASK_MAXIMUM_DELAY )?, control_plane_api: None, - control_plane_api_token: None + control_plane_api_token: None, + control_plane_emergency_mode: false }, "Correct defaults should be used when no config values are provided" ); @@ -1194,8 +1233,7 @@ background_task_maximum_delay = '334 s' let broker_endpoint = storage_broker::DEFAULT_ENDPOINT; let config_string = format!( - "{ALL_BASE_VALUES_TOML}pg_distrib_dir='{}'\nbroker_endpoint = '{broker_endpoint}'", - pg_distrib_dir.display() + "{ALL_BASE_VALUES_TOML}pg_distrib_dir='{pg_distrib_dir}'\nbroker_endpoint = '{broker_endpoint}'", ); let toml = config_string.parse()?; @@ -1236,7 +1274,8 @@ background_task_maximum_delay = '334 s' ondemand_download_behavior_treat_error_as_warn: false, background_task_maximum_delay: Duration::from_secs(334), control_plane_api: None, - control_plane_api_token: None + control_plane_api_token: None, + control_plane_emergency_mode: false }, "Should be able to parse all basic config values correctly" ); @@ -1255,23 +1294,18 @@ background_task_maximum_delay = '334 s' let identical_toml_declarations = &[ format!( r#"[remote_storage] -local_path = '{}'"#, - local_storage_path.display() - ), - format!( - "remote_storage={{local_path='{}'}}", - local_storage_path.display() +local_path = '{local_storage_path}'"#, ), + format!("remote_storage={{local_path='{local_storage_path}'}}"), ]; for remote_storage_config_str in identical_toml_declarations { let config_string = format!( r#"{ALL_BASE_VALUES_TOML} -pg_distrib_dir='{}' +pg_distrib_dir='{pg_distrib_dir}' broker_endpoint = '{broker_endpoint}' {remote_storage_config_str}"#, - pg_distrib_dir.display(), ); let toml = config_string.parse()?; @@ -1334,11 +1368,10 @@ concurrency_limit = {s3_concurrency_limit}"# for remote_storage_config_str in identical_toml_declarations { let config_string = format!( r#"{ALL_BASE_VALUES_TOML} -pg_distrib_dir='{}' +pg_distrib_dir='{pg_distrib_dir}' broker_endpoint = '{broker_endpoint}' {remote_storage_config_str}"#, - pg_distrib_dir.display(), ); let toml = config_string.parse()?; @@ -1380,12 +1413,11 @@ broker_endpoint = '{broker_endpoint}' let config_string = format!( r#"{ALL_BASE_VALUES_TOML} -pg_distrib_dir='{}' +pg_distrib_dir='{pg_distrib_dir}' broker_endpoint = '{broker_endpoint}' [tenant_config] trace_read_requests = {trace_read_requests}"#, - pg_distrib_dir.display(), ); let toml = config_string.parse()?; @@ -1405,7 +1437,7 @@ trace_read_requests = {trace_read_requests}"#, let (workdir, pg_distrib_dir) = prepare_fs(&tempdir)?; let pageserver_conf_toml = format!( - r#"pg_distrib_dir = "{}" + r#"pg_distrib_dir = "{pg_distrib_dir}" metric_collection_endpoint = "http://sample.url" metric_collection_interval = "10min" id = 222 @@ -1423,7 +1455,6 @@ kind = "LayerAccessThreshold" period = "20m" threshold = "20m" "#, - pg_distrib_dir.display(), ); let toml: Document = pageserver_conf_toml.parse()?; let conf = PageServerConf::parse_and_validate(&toml, &workdir)?; @@ -1464,7 +1495,7 @@ threshold = "20m" Ok(()) } - fn prepare_fs(tempdir: &TempDir) -> anyhow::Result<(PathBuf, PathBuf)> { + fn prepare_fs(tempdir: &Utf8TempDir) -> anyhow::Result<(Utf8PathBuf, Utf8PathBuf)> { let tempdir_path = tempdir.path(); let workdir = tempdir_path.join("workdir"); diff --git a/pageserver/src/consumption_metrics.rs b/pageserver/src/consumption_metrics.rs index 5f64bb2b3b..72a2099d92 100644 --- a/pageserver/src/consumption_metrics.rs +++ b/pageserver/src/consumption_metrics.rs @@ -3,11 +3,11 @@ use crate::context::{DownloadBehavior, RequestContext}; use crate::task_mgr::{self, TaskKind, BACKGROUND_RUNTIME}; use crate::tenant::{mgr, LogicalSizeCalculationCause}; +use camino::Utf8PathBuf; use consumption_metrics::EventType; use pageserver_api::models::TenantState; use reqwest::Url; use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, SystemTime}; use tracing::*; @@ -41,7 +41,7 @@ pub async fn collect_metrics( _cached_metric_collection_interval: Duration, synthetic_size_calculation_interval: Duration, node_id: NodeId, - local_disk_storage: PathBuf, + local_disk_storage: Utf8PathBuf, ctx: RequestContext, ) -> anyhow::Result<()> { if _cached_metric_collection_interval != Duration::ZERO { @@ -68,7 +68,7 @@ pub async fn collect_metrics( }, ); - let path: Arc = Arc::new(local_disk_storage); + let path: Arc = Arc::new(local_disk_storage); let cancel = task_mgr::shutdown_token(); @@ -153,7 +153,7 @@ pub async fn collect_metrics( /// /// Cancellation safe. async fn restore_and_reschedule( - path: &Arc, + path: &Arc, metric_collection_interval: Duration, ) -> Cache { let (cached, earlier_metric_at) = match disk_cache::read_metrics_from_disk(path.clone()).await { diff --git a/pageserver/src/consumption_metrics/disk_cache.rs b/pageserver/src/consumption_metrics/disk_cache.rs index 4b1cd79c6d..387bf7a0f9 100644 --- a/pageserver/src/consumption_metrics/disk_cache.rs +++ b/pageserver/src/consumption_metrics/disk_cache.rs @@ -1,10 +1,12 @@ use anyhow::Context; -use std::path::PathBuf; +use camino::{Utf8Path, Utf8PathBuf}; use std::sync::Arc; use super::RawMetric; -pub(super) async fn read_metrics_from_disk(path: Arc) -> anyhow::Result> { +pub(super) async fn read_metrics_from_disk( + path: Arc, +) -> anyhow::Result> { // do not add context to each error, callsite will log with full path let span = tracing::Span::current(); tokio::task::spawn_blocking(move || { @@ -25,10 +27,10 @@ pub(super) async fn read_metrics_from_disk(path: Arc) -> anyhow::Result .and_then(|x| x) } -fn scan_and_delete_with_same_prefix(path: &std::path::Path) -> std::io::Result<()> { +fn scan_and_delete_with_same_prefix(path: &Utf8Path) -> std::io::Result<()> { let it = std::fs::read_dir(path.parent().expect("caller checked"))?; - let prefix = path.file_name().expect("caller checked").to_string_lossy(); + let prefix = path.file_name().expect("caller checked").to_string(); for entry in it { let entry = entry?; @@ -62,7 +64,7 @@ fn scan_and_delete_with_same_prefix(path: &std::path::Path) -> std::io::Result<( pub(super) async fn flush_metrics_to_disk( current_metrics: &Arc>, - path: &Arc, + path: &Arc, ) -> anyhow::Result<()> { use std::io::Write; @@ -81,7 +83,7 @@ pub(super) async fn flush_metrics_to_disk( let parent = path.parent().expect("existence checked"); let file_name = path.file_name().expect("existence checked"); - let mut tempfile = tempfile::Builder::new() + let mut tempfile = camino_tempfile::Builder::new() .prefix(file_name) .suffix(".tmp") .tempfile_in(parent)?; diff --git a/pageserver/src/control_plane_client.rs b/pageserver/src/control_plane_client.rs index 3375392373..d37dc694dd 100644 --- a/pageserver/src/control_plane_client.rs +++ b/pageserver/src/control_plane_client.rs @@ -133,6 +133,8 @@ impl ControlPlaneGenerationsApi for ControlPlaneClient { node_id: self.node_id, }; + fail::fail_point!("control-plane-client-re-attach"); + let response: ReAttachResponse = self.retry_http_forever(&re_attach_path, request).await?; tracing::info!( "Received re-attach response with {} tenants", @@ -168,6 +170,8 @@ impl ControlPlaneGenerationsApi for ControlPlaneClient { .collect(), }; + fail::fail_point!("control-plane-client-validate"); + let response: ValidateResponse = self.retry_http_forever(&re_attach_path, request).await?; Ok(response diff --git a/pageserver/src/deletion_queue.rs b/pageserver/src/deletion_queue.rs index 4c0d399789..cccc64685b 100644 --- a/pageserver/src/deletion_queue.rs +++ b/pageserver/src/deletion_queue.rs @@ -3,7 +3,6 @@ mod list_writer; mod validator; use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -13,6 +12,7 @@ use crate::tenant::remote_timeline_client::remote_layer_path; use crate::tenant::remote_timeline_client::remote_timeline_path; use crate::virtual_file::VirtualFile; use anyhow::Context; +use camino::Utf8PathBuf; use hex::FromHex; use remote_storage::{GenericRemoteStorage, RemotePath}; use serde::Deserialize; @@ -40,7 +40,6 @@ use validator::ValidatorQueueMessage; use crate::{config::PageServerConf, tenant::storage_layer::LayerFileName}; -// TODO: adminstrative "panic button" config property to disable all deletions // TODO: configurable for how long to wait before executing deletions /// We aggregate object deletions from many tenants in one place, for several reasons: @@ -186,7 +185,7 @@ where V: Serialize, I: AsRef<[u8]>, { - let transformed = input.iter().map(|(k, v)| (hex::encode(k), v.clone())); + let transformed = input.iter().map(|(k, v)| (hex::encode(k), v)); transformed .collect::>() @@ -213,7 +212,7 @@ where /// Files ending with this suffix will be ignored and erased /// during recovery as startup. -const TEMP_SUFFIX: &str = ".tmp"; +const TEMP_SUFFIX: &str = "tmp"; #[serde_as] #[derive(Debug, Serialize, Deserialize)] @@ -325,10 +324,7 @@ impl DeletionList { return false; } - let timeline_entry = tenant_entry - .timelines - .entry(*timeline) - .or_insert_with(Vec::new); + let timeline_entry = tenant_entry.timelines.entry(*timeline).or_default(); let timeline_remote_path = remote_timeline_path(tenant, timeline); @@ -336,7 +332,6 @@ impl DeletionList { timeline_entry.extend(objects.drain(..).map(|p| { p.strip_prefix(&timeline_remote_path) .expect("Timeline paths always start with the timeline prefix") - .to_string_lossy() .to_string() })); true @@ -350,7 +345,7 @@ impl DeletionList { result.extend( timeline_layers .into_iter() - .map(|l| timeline_remote_path.join(&PathBuf::from(l))), + .map(|l| timeline_remote_path.join(&Utf8PathBuf::from(l))), ); } } @@ -727,12 +722,9 @@ impl DeletionQueue { #[cfg(test)] mod test { + use camino::Utf8Path; use hex_literal::hex; - use std::{ - io::ErrorKind, - path::{Path, PathBuf}, - time::Duration, - }; + use std::{io::ErrorKind, time::Duration}; use tracing::info; use remote_storage::{RemoteStorageConfig, RemoteStorageKind}; @@ -764,7 +756,7 @@ mod test { struct TestSetup { harness: TenantHarness, - remote_fs_dir: PathBuf, + remote_fs_dir: Utf8PathBuf, storage: GenericRemoteStorage, mock_control_plane: MockControlPlane, deletion_queue: DeletionQueue, @@ -873,7 +865,7 @@ mod test { // Set up a GenericRemoteStorage targetting a directory let remote_fs_dir = harness.conf.workdir.join("remote_fs"); std::fs::create_dir_all(remote_fs_dir)?; - let remote_fs_dir = std::fs::canonicalize(harness.conf.workdir.join("remote_fs"))?; + let remote_fs_dir = harness.conf.workdir.join("remote_fs").canonicalize_utf8()?; let storage_config = RemoteStorageConfig { max_concurrent_syncs: std::num::NonZeroUsize::new( remote_storage::DEFAULT_REMOTE_STORAGE_MAX_CONCURRENT_SYNCS, @@ -909,7 +901,7 @@ mod test { } // TODO: put this in a common location so that we can share with remote_timeline_client's tests - fn assert_remote_files(expected: &[&str], remote_path: &Path) { + fn assert_remote_files(expected: &[&str], remote_path: &Utf8Path) { let mut expected: Vec = expected.iter().map(|x| String::from(*x)).collect(); expected.sort(); @@ -926,10 +918,7 @@ mod test { unreachable!(); } } else { - panic!( - "Unexpected error listing {}: {e}", - remote_path.to_string_lossy() - ); + panic!("Unexpected error listing {remote_path}: {e}"); } } }; @@ -944,7 +933,7 @@ mod test { assert_eq!(expected, found); } - fn assert_local_files(expected: &[&str], directory: &Path) { + fn assert_local_files(expected: &[&str], directory: &Utf8Path) { let dir = match std::fs::read_dir(directory) { Ok(d) => d, Err(_) => { diff --git a/pageserver/src/deletion_queue/list_writer.rs b/pageserver/src/deletion_queue/list_writer.rs index 618a59f8fe..e846340373 100644 --- a/pageserver/src/deletion_queue/list_writer.rs +++ b/pageserver/src/deletion_queue/list_writer.rs @@ -180,8 +180,7 @@ impl ListWriter { Ok(h) => Ok(Some(h.validated_sequence)), Err(e) => { warn!( - "Failed to deserialize deletion header, ignoring {}: {e:#}", - header_path.display() + "Failed to deserialize deletion header, ignoring {header_path}: {e:#}", ); // This should never happen unless we make a mistake with our serialization. // Ignoring a deletion header is not consequential for correctnes because all deletions @@ -193,10 +192,7 @@ impl ListWriter { } Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { - debug!( - "Deletion header {} not found, first start?", - header_path.display() - ); + debug!("Deletion header {header_path} not found, first start?"); Ok(None) } else { Err(anyhow::anyhow!(e)) @@ -223,10 +219,7 @@ impl ListWriter { let mut dir = match tokio::fs::read_dir(&deletion_directory).await { Ok(d) => d, Err(e) => { - warn!( - "Failed to open deletion list directory {}: {e:#}", - deletion_directory.display(), - ); + warn!("Failed to open deletion list directory {deletion_directory}: {e:#}"); // Give up: if we can't read the deletion list directory, we probably can't // write lists into it later, so the queue won't work. @@ -237,27 +230,26 @@ impl ListWriter { let list_name_pattern = Regex::new("(?[a-zA-Z0-9]{16})-(?[a-zA-Z0-9]{2}).list").unwrap(); + let temp_extension = format!(".{TEMP_SUFFIX}"); let header_path = self.conf.deletion_header_path(); let mut seqs: Vec = Vec::new(); while let Some(dentry) = dir.next_entry().await? { let file_name = dentry.file_name(); let dentry_str = file_name.to_string_lossy(); - if Some(file_name.as_os_str()) == header_path.file_name() { + if file_name == header_path.file_name().unwrap_or("") { // Don't try and parse the header's name like a list continue; } - if dentry_str.ends_with(TEMP_SUFFIX) { + if dentry_str.ends_with(&temp_extension) { info!("Cleaning up temporary file {dentry_str}"); - let absolute_path = deletion_directory.join(dentry.file_name()); + let absolute_path = + deletion_directory.join(dentry.file_name().to_str().expect("non-Unicode path")); if let Err(e) = tokio::fs::remove_file(&absolute_path).await { // Non-fatal error: we will just leave the file behind but not // try and load it. - warn!( - "Failed to clean up temporary file {}: {e:#}", - absolute_path.display() - ); + warn!("Failed to clean up temporary file {absolute_path}: {e:#}"); } continue; @@ -360,7 +352,7 @@ impl ListWriter { if let Err(e) = create_dir_all(&self.conf.deletion_prefix()) { tracing::error!( "Failed to create deletion list directory {}, deletions will not be executed ({e})", - self.conf.deletion_prefix().display() + self.conf.deletion_prefix(), ); metrics::DELETION_QUEUE.unexpected_errors.inc(); return; diff --git a/pageserver/src/deletion_queue/validator.rs b/pageserver/src/deletion_queue/validator.rs index 64603045d2..a2cbfb9dc7 100644 --- a/pageserver/src/deletion_queue/validator.rs +++ b/pageserver/src/deletion_queue/validator.rs @@ -15,10 +15,10 @@ //! Deletions are passed onward to the Deleter. use std::collections::HashMap; -use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; +use camino::Utf8PathBuf; use tokio_util::sync::CancellationToken; use tracing::debug; use tracing::info; @@ -220,6 +220,8 @@ where warn!("Dropping stale deletions for tenant {tenant_id} in generation {:?}, objects may be leaked", tenant.generation); metrics::DELETION_QUEUE.keys_dropped.inc_by(tenant.len() as u64); mutated = true; + } else { + metrics::DELETION_QUEUE.keys_validated.inc_by(tenant.len() as u64); } this_list_valid }); @@ -282,16 +284,16 @@ where Ok(()) } - async fn cleanup_lists(&mut self, list_paths: Vec) { + async fn cleanup_lists(&mut self, list_paths: Vec) { for list_path in list_paths { - debug!("Removing deletion list {}", list_path.display()); + debug!("Removing deletion list {list_path}"); if let Err(e) = tokio::fs::remove_file(&list_path).await { // Unexpected: we should have permissions and nothing else should // be touching these files. We will leave the file behind. Subsequent // pageservers will try and load it again: hopefully whatever storage // issue (probably permissions) has been fixed by then. - tracing::error!("Failed to delete {}: {e:#}", list_path.display()); + tracing::error!("Failed to delete {list_path}: {e:#}"); metrics::DELETION_QUEUE.unexpected_errors.inc(); break; } diff --git a/pageserver/src/disk_usage_eviction_task.rs b/pageserver/src/disk_usage_eviction_task.rs index d5bdfc84b9..b8af4c3d11 100644 --- a/pageserver/src/disk_usage_eviction_task.rs +++ b/pageserver/src/disk_usage_eviction_task.rs @@ -43,12 +43,12 @@ use std::{ collections::HashMap, - path::Path, sync::Arc, time::{Duration, SystemTime}, }; use anyhow::Context; +use camino::Utf8Path; use remote_storage::GenericRemoteStorage; use serde::{Deserialize, Serialize}; use tokio::time::Instant; @@ -122,7 +122,7 @@ async fn disk_usage_eviction_task( state: &State, task_config: &DiskUsageEvictionTaskConfig, storage: GenericRemoteStorage, - tenants_dir: &Path, + tenants_dir: &Utf8Path, cancel: CancellationToken, ) { scopeguard::defer! { @@ -184,7 +184,7 @@ async fn disk_usage_eviction_task_iteration( state: &State, task_config: &DiskUsageEvictionTaskConfig, storage: &GenericRemoteStorage, - tenants_dir: &Path, + tenants_dir: &Utf8Path, cancel: &CancellationToken, ) -> anyhow::Result<()> { let usage_pre = filesystem_level_usage::get(tenants_dir, task_config) @@ -620,9 +620,8 @@ impl std::ops::Deref for TimelineKey { } mod filesystem_level_usage { - use std::path::Path; - use anyhow::Context; + use camino::Utf8Path; use crate::statvfs::Statvfs; @@ -664,7 +663,7 @@ mod filesystem_level_usage { } pub fn get<'a>( - tenants_dir: &Path, + tenants_dir: &Utf8Path, config: &'a DiskUsageEvictionTaskConfig, ) -> anyhow::Result> { let mock_config = { diff --git a/pageserver/src/http/openapi_spec.yml b/pageserver/src/http/openapi_spec.yml index f5c1224f01..477a2d378d 100644 --- a/pageserver/src/http/openapi_spec.yml +++ b/pageserver/src/http/openapi_spec.yml @@ -93,9 +93,16 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + delete: description: | - Attempts to delete specified tenant. 500 and 409 errors should be retried until 404 is retrieved. + Attempts to delete specified tenant. 500, 503 and 409 errors should be retried until 404 is retrieved. 404 means that deletion successfully finished" responses: "400": @@ -134,6 +141,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/timeline: parameters: @@ -178,6 +192,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/timeline/{timeline_id}: parameters: @@ -226,6 +247,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + delete: description: "Attempts to delete specified timeline. 500 and 409 errors should be retried" responses: @@ -265,13 +293,19 @@ paths: application/json: schema: $ref: "#/components/schemas/PreconditionFailedError" - "500": description: Generic operation error content: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/timeline/{timeline_id}/get_lsn_by_timestamp: parameters: @@ -328,6 +362,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/timeline/{timeline_id}/do_gc: parameters: - name: tenant_id @@ -375,6 +416,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/attach: parameters: - name: tenant_id @@ -465,6 +513,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/detach: parameters: @@ -518,6 +573,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/ignore: parameters: @@ -560,6 +622,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/load: parameters: @@ -604,6 +673,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/synthetic_size: parameters: @@ -641,6 +717,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" /v1/tenant/{tenant_id}/size: parameters: @@ -704,6 +786,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/timeline/: parameters: @@ -780,6 +869,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/: get: description: Get tenants list @@ -810,6 +906,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + post: description: | Create a tenant. Returns new tenant id on success. @@ -860,6 +963,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/config: put: @@ -905,6 +1015,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + /v1/tenant/{tenant_id}/config/: parameters: - name: tenant_id @@ -954,6 +1071,13 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + "503": + description: Temporarily unavailable, please retry. + content: + application/json: + schema: + $ref: "#/components/schemas/ServiceUnavailableError" + components: securitySchemes: JWT: @@ -1220,6 +1344,13 @@ components: properties: msg: type: string + ServiceUnavailableError: + type: object + required: + - msg + properties: + msg: + type: string NotFoundError: type: object required: diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index e61a9dcf3f..e0529aeafa 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -6,11 +6,13 @@ use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use futures::TryFutureExt; +use hyper::header::CONTENT_TYPE; use hyper::StatusCode; use hyper::{Body, Request, Response, Uri}; use metrics::launch_timestamp::LaunchTimestamp; use pageserver_api::models::{ - DownloadRemoteLayersTaskSpawnRequest, TenantAttachRequest, TenantLoadRequest, + DownloadRemoteLayersTaskSpawnRequest, LocationConfigMode, TenantAttachRequest, + TenantLoadRequest, TenantLocationConfigRequest, }; use remote_storage::GenericRemoteStorage; use tenant_size_model::{SizeResult, StorageModel}; @@ -29,7 +31,7 @@ use crate::deletion_queue::DeletionQueueClient; use crate::metrics::{StorageTimeOperation, STORAGE_TIME_GLOBAL}; use crate::pgdatadir_mapping::LsnForTimestamp; use crate::task_mgr::TaskKind; -use crate::tenant::config::TenantConfOpt; +use crate::tenant::config::{LocationConf, TenantConfOpt}; use crate::tenant::mgr::{ GetTenantError, SetNewTenantConfigError, TenantMapInsertError, TenantStateError, }; @@ -132,7 +134,7 @@ impl From for ApiError { ApiError::InternalServerError(anyhow::anyhow!("request was cancelled")) } PageReconstructError::AncestorStopping(_) => { - ApiError::InternalServerError(anyhow::Error::new(pre)) + ApiError::ResourceUnavailable(format!("{pre}").into()) } PageReconstructError::WalRedo(pre) => { ApiError::InternalServerError(anyhow::Error::new(pre)) @@ -145,12 +147,15 @@ impl From for ApiError { fn from(tmie: TenantMapInsertError) -> ApiError { match tmie { TenantMapInsertError::StillInitializing | TenantMapInsertError::ShuttingDown => { - ApiError::InternalServerError(anyhow::Error::new(tmie)) + ApiError::ResourceUnavailable(format!("{tmie}").into()) } TenantMapInsertError::TenantAlreadyExists(id, state) => { ApiError::Conflict(format!("tenant {id} already exists, state: {state:?}")) } - TenantMapInsertError::Closure(e) => ApiError::InternalServerError(e), + TenantMapInsertError::TenantExistsSecondary(id) => { + ApiError::Conflict(format!("tenant {id} already exists as secondary")) + } + TenantMapInsertError::Other(e) => ApiError::InternalServerError(e), } } } @@ -159,6 +164,12 @@ impl From for ApiError { fn from(tse: TenantStateError) -> ApiError { match tse { TenantStateError::NotFound(tid) => ApiError::NotFound(anyhow!("tenant {}", tid).into()), + TenantStateError::NotActive(_) => { + ApiError::ResourceUnavailable("Tenant not yet active".into()) + } + TenantStateError::IsStopping(_) => { + ApiError::ResourceUnavailable("Tenant is stopping".into()) + } _ => ApiError::InternalServerError(anyhow::Error::new(tse)), } } @@ -168,14 +179,17 @@ impl From for ApiError { fn from(tse: GetTenantError) -> ApiError { match tse { GetTenantError::NotFound(tid) => ApiError::NotFound(anyhow!("tenant {}", tid).into()), - e @ GetTenantError::NotActive(_) => { + GetTenantError::Broken(reason) => { + ApiError::InternalServerError(anyhow!("tenant is broken: {}", reason)) + } + GetTenantError::NotActive(_) => { // Why is this not `ApiError::NotFound`? // Because we must be careful to never return 404 for a tenant if it does // in fact exist locally. If we did, the caller could draw the conclusion // that it can attach the tenant to another PS and we'd be in split-brain. // // (We can produce this variant only in `mgr::get_tenant(..., active=true)` calls). - ApiError::InternalServerError(anyhow::Error::new(e)) + ApiError::ResourceUnavailable("Tenant not yet active".into()) } } } @@ -382,6 +396,9 @@ async fn timeline_create_handler( format!("{err:#}") )) } + Err(e @ tenant::CreateTimelineError::AncestorNotActive) => { + json_response(StatusCode::SERVICE_UNAVAILABLE, HttpErrorBody::from_msg(e.to_string())) + } Err(tenant::CreateTimelineError::Other(err)) => Err(ApiError::InternalServerError(err)), } } @@ -622,8 +639,9 @@ async fn tenant_list_handler( let response_data = mgr::list_tenants() .instrument(info_span!("tenant_list")) .await - .map_err(anyhow::Error::new) - .map_err(ApiError::InternalServerError)? + .map_err(|_| { + ApiError::ResourceUnavailable("Tenant map is initializing or shutting down".into()) + })? .iter() .map(|(id, state)| TenantInfo { id: *id, @@ -1001,6 +1019,48 @@ async fn update_tenant_config_handler( json_response(StatusCode::OK, ()) } +async fn put_tenant_location_config_handler( + mut request: Request, + _cancel: CancellationToken, +) -> Result, ApiError> { + let request_data: TenantLocationConfigRequest = json_request(&mut request).await?; + let tenant_id = request_data.tenant_id; + check_permission(&request, Some(tenant_id))?; + + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn); + let state = get_state(&request); + let conf = state.conf; + + // The `Detached` state is special, it doesn't upsert a tenant, it removes + // its local disk content and drops it from memory. + if let LocationConfigMode::Detached = request_data.config.mode { + mgr::detach_tenant(conf, tenant_id, true) + .instrument(info_span!("tenant_detach", %tenant_id)) + .await?; + return json_response(StatusCode::OK, ()); + } + + let location_conf = + LocationConf::try_from(&request_data.config).map_err(ApiError::BadRequest)?; + + mgr::upsert_location( + state.conf, + tenant_id, + location_conf, + state.broker_client.clone(), + state.remote_storage.clone(), + state.deletion_queue_client.clone(), + &ctx, + ) + .await + // TODO: badrequest assumes the caller was asking for something unreasonable, but in + // principle we might have hit something like concurrent API calls to the same tenant, + // which is not a 400 but a 409. + .map_err(ApiError::BadRequest)?; + + json_response(StatusCode::OK, ()) +} + /// Testing helper to transition a tenant to [`crate::tenant::TenantState::Broken`]. async fn handle_tenant_break( r: Request, @@ -1180,6 +1240,136 @@ async fn deletion_queue_flush( } } +/// Try if `GetPage@Lsn` is successful, useful for manual debugging. +async fn getpage_at_lsn_handler( + request: Request, + _cancel: CancellationToken, +) -> Result, ApiError> { + let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?; + let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?; + check_permission(&request, Some(tenant_id))?; + + struct Key(crate::repository::Key); + + impl std::str::FromStr for Key { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + crate::repository::Key::from_hex(s).map(Key) + } + } + + let key: Key = parse_query_param(&request, "key")? + .ok_or_else(|| ApiError::BadRequest(anyhow!("missing 'key' query parameter")))?; + let lsn: Lsn = parse_query_param(&request, "lsn")? + .ok_or_else(|| ApiError::BadRequest(anyhow!("missing 'lsn' query parameter")))?; + + async { + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download); + let timeline = active_timeline_of_active_tenant(tenant_id, timeline_id).await?; + + let page = timeline.get(key.0, lsn, &ctx).await?; + + Result::<_, ApiError>::Ok( + Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/octet-stream") + .body(hyper::Body::from(page)) + .unwrap(), + ) + } + .instrument(info_span!("timeline_get", %tenant_id, %timeline_id)) + .await +} + +async fn timeline_collect_keyspace( + request: Request, + _cancel: CancellationToken, +) -> Result, ApiError> { + let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?; + let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?; + check_permission(&request, Some(tenant_id))?; + + struct Partitioning { + keys: crate::keyspace::KeySpace, + + at_lsn: Lsn, + } + + impl serde::Serialize for Partitioning { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_key("keys")?; + map.serialize_value(&KeySpace(&self.keys))?; + map.serialize_key("at_lsn")?; + map.serialize_value(&WithDisplay(&self.at_lsn))?; + map.end() + } + } + + struct WithDisplay<'a, T>(&'a T); + + impl<'a, T: std::fmt::Display> serde::Serialize for WithDisplay<'a, T> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.collect_str(&self.0) + } + } + + struct KeySpace<'a>(&'a crate::keyspace::KeySpace); + + impl<'a> serde::Serialize for KeySpace<'a> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(self.0.ranges.len()))?; + for kr in &self.0.ranges { + seq.serialize_element(&KeyRange(kr))?; + } + seq.end() + } + } + + struct KeyRange<'a>(&'a std::ops::Range); + + impl<'a> serde::Serialize for KeyRange<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeTuple; + let mut t = serializer.serialize_tuple(2)?; + t.serialize_element(&WithDisplay(&self.0.start))?; + t.serialize_element(&WithDisplay(&self.0.end))?; + t.end() + } + } + + let at_lsn: Option = parse_query_param(&request, "at_lsn")?; + + async { + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download); + let timeline = active_timeline_of_active_tenant(tenant_id, timeline_id).await?; + let at_lsn = at_lsn.unwrap_or_else(|| timeline.get_last_record_lsn()); + let keys = timeline + .collect_keyspace(at_lsn, &ctx) + .await + .map_err(ApiError::InternalServerError)?; + + json_response(StatusCode::OK, Partitioning { keys, at_lsn }) + } + .instrument(info_span!("timeline_collect_keyspace", %tenant_id, %timeline_id)) + .await +} + async fn active_timeline_of_active_tenant( tenant_id: TenantId, timeline_id: TimelineId, @@ -1454,6 +1644,9 @@ pub fn make_router( .get("/v1/tenant/:tenant_id/config", |r| { api_handler(r, get_tenant_config_handler) }) + .put("/v1/tenant/:tenant_id/location_config", |r| { + api_handler(r, put_tenant_location_config_handler) + }) .get("/v1/tenant/:tenant_id/timeline", |r| { api_handler(r, timeline_list_handler) }) @@ -1524,5 +1717,12 @@ pub fn make_router( .post("/v1/tracing/event", |r| { testing_api_handler("emit a tracing event", r, post_tracing_event_handler) }) + .get("/v1/tenant/:tenant_id/timeline/:timeline_id/getpage", |r| { + testing_api_handler("getpage@lsn", r, getpage_at_lsn_handler) + }) + .get( + "/v1/tenant/:tenant_id/timeline/:timeline_id/keyspace", + |r| testing_api_handler("read out the keyspace", r, timeline_collect_keyspace), + ) .any(handler_404)) } diff --git a/pageserver/src/import_datadir.rs b/pageserver/src/import_datadir.rs index 5a1affdb11..30975c1fc9 100644 --- a/pageserver/src/import_datadir.rs +++ b/pageserver/src/import_datadir.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use anyhow::{bail, ensure, Context, Result}; use bytes::Bytes; +use camino::Utf8Path; use futures::StreamExt; use tokio::io::{AsyncRead, AsyncReadExt}; use tokio_tar::Archive; @@ -29,7 +30,7 @@ use postgres_ffi::{BLCKSZ, WAL_SEGMENT_SIZE}; use utils::lsn::Lsn; // Returns checkpoint LSN from controlfile -pub fn get_lsn_from_controlfile(path: &Path) -> Result { +pub fn get_lsn_from_controlfile(path: &Utf8Path) -> Result { // Read control file to extract the LSN let controlfile_path = path.join("global").join("pg_control"); let controlfile = ControlFileData::decode(&std::fs::read(controlfile_path)?)?; @@ -46,7 +47,7 @@ pub fn get_lsn_from_controlfile(path: &Path) -> Result { /// cluster was not shut down cleanly. pub async fn import_timeline_from_postgres_datadir( tline: &Timeline, - pgdata_path: &Path, + pgdata_path: &Utf8Path, pgdata_lsn: Lsn, ctx: &RequestContext, ) -> Result<()> { @@ -256,7 +257,7 @@ async fn import_slru( /// Scan PostgreSQL WAL files in given directory and load all records between /// 'startpoint' and 'endpoint' into the repository. async fn import_wal( - walpath: &Path, + walpath: &Utf8Path, tline: &Timeline, startpoint: Lsn, endpoint: Lsn, diff --git a/pageserver/src/lib.rs b/pageserver/src/lib.rs index e370e063ba..8199cd38e6 100644 --- a/pageserver/src/lib.rs +++ b/pageserver/src/lib.rs @@ -25,9 +25,8 @@ pub mod walredo; pub mod failpoint_support; -use std::path::Path; - use crate::task_mgr::TaskKind; +use camino::Utf8Path; use deletion_queue::DeletionQueue; use tracing::info; @@ -113,6 +112,10 @@ pub const METADATA_FILE_NAME: &str = "metadata"; /// Full path: `tenants//config`. pub const TENANT_CONFIG_NAME: &str = "config"; +/// Per-tenant configuration file. +/// Full path: `tenants//config`. +pub const TENANT_LOCATION_CONFIG_NAME: &str = "config-v1"; + /// A suffix used for various temporary files. Any temporary files found in the /// data directory at pageserver startup can be automatically removed. pub const TEMP_FILE_SUFFIX: &str = "___temp"; @@ -132,25 +135,25 @@ pub const TIMELINE_DELETE_MARK_SUFFIX: &str = "___delete"; /// Full path: `tenants//___ignored_tenant`. pub const IGNORED_TENANT_FILE_NAME: &str = "___ignored_tenant"; -pub fn is_temporary(path: &Path) -> bool { +pub fn is_temporary(path: &Utf8Path) -> bool { match path.file_name() { - Some(name) => name.to_string_lossy().ends_with(TEMP_FILE_SUFFIX), + Some(name) => name.ends_with(TEMP_FILE_SUFFIX), None => false, } } -fn ends_with_suffix(path: &Path, suffix: &str) -> bool { +fn ends_with_suffix(path: &Utf8Path, suffix: &str) -> bool { match path.file_name() { - Some(name) => name.to_string_lossy().ends_with(suffix), + Some(name) => name.ends_with(suffix), None => false, } } -pub fn is_uninit_mark(path: &Path) -> bool { +pub fn is_uninit_mark(path: &Utf8Path) -> bool { ends_with_suffix(path, TIMELINE_UNINIT_MARK_SUFFIX) } -pub fn is_delete_mark(path: &Path) -> bool { +pub fn is_delete_mark(path: &Utf8Path) -> bool { ends_with_suffix(path, TIMELINE_DELETE_MARK_SUFFIX) } diff --git a/pageserver/src/metrics.rs b/pageserver/src/metrics.rs index f85f525630..c154b4a4ca 100644 --- a/pageserver/src/metrics.rs +++ b/pageserver/src/metrics.rs @@ -94,15 +94,35 @@ pub(crate) static READ_NUM_FS_LAYERS: Lazy = Lazy::new(|| { }); // Metrics collected on operations on the storage repository. -pub(crate) static RECONSTRUCT_TIME: Lazy = Lazy::new(|| { - register_histogram!( + +pub(crate) struct ReconstructTimeMetrics { + ok: Histogram, + err: Histogram, +} + +pub(crate) static RECONSTRUCT_TIME: Lazy = Lazy::new(|| { + let inner = register_histogram_vec!( "pageserver_getpage_reconstruct_seconds", "Time spent in reconstruct_value (reconstruct a page from deltas)", + &["result"], CRITICAL_OP_BUCKETS.into(), ) - .expect("failed to define a metric") + .expect("failed to define a metric"); + ReconstructTimeMetrics { + ok: inner.get_metric_with_label_values(&["ok"]).unwrap(), + err: inner.get_metric_with_label_values(&["err"]).unwrap(), + } }); +impl ReconstructTimeMetrics { + pub(crate) fn for_result(&self, result: &Result) -> &Histogram { + match result { + Ok(_) => &self.ok, + Err(_) => &self.err, + } + } +} + pub(crate) static MATERIALIZED_PAGE_CACHE_HIT_DIRECT: Lazy = Lazy::new(|| { register_int_counter!( "pageserver_materialized_cache_hits_direct_total", @@ -671,10 +691,9 @@ impl StorageIoTime { .expect("failed to define a metric"); let metrics = std::array::from_fn(|i| { let op = StorageIoOperation::from_repr(i).unwrap(); - let metric = storage_io_histogram_vec + storage_io_histogram_vec .get_metric_with_label_values(&[op.as_str()]) - .unwrap(); - metric + .unwrap() }); Self { metrics } } @@ -947,6 +966,7 @@ pub(crate) struct DeletionQueueMetrics { pub(crate) keys_submitted: IntCounter, pub(crate) keys_dropped: IntCounter, pub(crate) keys_executed: IntCounter, + pub(crate) keys_validated: IntCounter, pub(crate) dropped_lsn_updates: IntCounter, pub(crate) unexpected_errors: IntCounter, pub(crate) remote_errors: IntCounterVec, @@ -968,7 +988,13 @@ pub(crate) static DELETION_QUEUE: Lazy = Lazy::new(|| { keys_executed: register_int_counter!( "pageserver_deletion_queue_executed_total", - "Number of objects deleted. Only includes objects that we actually deleted, sum with pageserver_deletion_queue_dropped_total for the total number of keys processed." + "Number of objects deleted. Only includes objects that we actually deleted, sum with pageserver_deletion_queue_dropped_total for the total number of keys processed to completion" + ) + .expect("failed to define a metric"), + + keys_validated: register_int_counter!( + "pageserver_deletion_queue_validated_total", + "Number of keys validated for deletion. Sum with pageserver_deletion_queue_dropped_total for the total number of keys that have passed through the validation stage." ) .expect("failed to define a metric"), @@ -1856,7 +1882,6 @@ pub fn preinitialize_metrics() { // histograms [ &READ_NUM_FS_LAYERS, - &RECONSTRUCT_TIME, &WAIT_LSN_TIME, &WAL_REDO_TIME, &WAL_REDO_WAIT_TIME, @@ -1867,4 +1892,7 @@ pub fn preinitialize_metrics() { .for_each(|h| { Lazy::force(h); }); + + // Custom + Lazy::force(&RECONSTRUCT_TIME); } diff --git a/pageserver/src/page_cache.rs b/pageserver/src/page_cache.rs index 97ca2bfea7..f6acf64f22 100644 --- a/pageserver/src/page_cache.rs +++ b/pageserver/src/page_cache.rs @@ -66,8 +66,7 @@ //! inserted to the mapping, but you must hold the write-lock on the slot until //! the contents are valid. If you need to release the lock without initializing //! the contents, you must remove the mapping first. We make that easy for the -//! callers with PageWriteGuard: when lock_for_write() returns an uninitialized -//! page, the caller must explicitly call guard.mark_valid() after it has +//! callers with PageWriteGuard: the caller must explicitly call guard.mark_valid() after it has //! initialized it. If the guard is dropped without calling mark_valid(), the //! mapping is automatically removed and the slot is marked free. //! @@ -286,23 +285,25 @@ impl AsRef<[u8; PAGE_SZ]> for PageReadGuard<'_> { /// /// Counterintuitively, this is used even for a read, if the requested page is not /// currently found in the page cache. In that case, the caller of lock_for_read() -/// is expected to fill in the page contents and call mark_valid(). Similarly -/// lock_for_write() can return an invalid buffer that the caller is expected to -/// to initialize. -/// +/// is expected to fill in the page contents and call mark_valid(). pub struct PageWriteGuard<'i> { - inner: tokio::sync::RwLockWriteGuard<'i, SlotInner>, + state: PageWriteGuardState<'i>, +} - _permit: PinnedSlotsPermit, - - // Are the page contents currently valid? - // Used to mark pages as invalid that are assigned but not yet filled with data. - valid: bool, +enum PageWriteGuardState<'i> { + Invalid { + inner: tokio::sync::RwLockWriteGuard<'i, SlotInner>, + _permit: PinnedSlotsPermit, + }, + Downgraded, } impl std::ops::DerefMut for PageWriteGuard<'_> { fn deref_mut(&mut self) -> &mut Self::Target { - self.inner.buf + match &mut self.state { + PageWriteGuardState::Invalid { inner, _permit } => inner.buf, + PageWriteGuardState::Downgraded => unreachable!(), + } } } @@ -310,25 +311,37 @@ impl std::ops::Deref for PageWriteGuard<'_> { type Target = [u8; PAGE_SZ]; fn deref(&self) -> &Self::Target { - self.inner.buf + match &self.state { + PageWriteGuardState::Invalid { inner, _permit } => inner.buf, + PageWriteGuardState::Downgraded => unreachable!(), + } } } impl AsMut<[u8; PAGE_SZ]> for PageWriteGuard<'_> { fn as_mut(&mut self) -> &mut [u8; PAGE_SZ] { - self.inner.buf + match &mut self.state { + PageWriteGuardState::Invalid { inner, _permit } => inner.buf, + PageWriteGuardState::Downgraded => unreachable!(), + } } } -impl PageWriteGuard<'_> { +impl<'a> PageWriteGuard<'a> { /// Mark that the buffer contents are now valid. - pub fn mark_valid(&mut self) { - assert!(self.inner.key.is_some()); - assert!( - !self.valid, - "mark_valid called on a buffer that was already valid" - ); - self.valid = true; + #[must_use] + pub fn mark_valid(mut self) -> PageReadGuard<'a> { + let prev = std::mem::replace(&mut self.state, PageWriteGuardState::Downgraded); + match prev { + PageWriteGuardState::Invalid { inner, _permit } => { + assert!(inner.key.is_some()); + PageReadGuard { + _permit: Arc::new(_permit), + slot_guard: inner.downgrade(), + } + } + PageWriteGuardState::Downgraded => unreachable!(), + } } } @@ -339,11 +352,14 @@ impl Drop for PageWriteGuard<'_> { /// initializing it, remove the mapping from the page cache. /// fn drop(&mut self) { - assert!(self.inner.key.is_some()); - if !self.valid { - let self_key = self.inner.key.as_ref().unwrap(); - PAGE_CACHE.get().unwrap().remove_mapping(self_key); - self.inner.key = None; + match &mut self.state { + PageWriteGuardState::Invalid { inner, _permit } => { + assert!(inner.key.is_some()); + let self_key = inner.key.as_ref().unwrap(); + PAGE_CACHE.get().unwrap().remove_mapping(self_key); + inner.key = None; + } + PageWriteGuardState::Downgraded => {} } } } @@ -354,12 +370,6 @@ pub enum ReadBufResult<'a> { NotFound(PageWriteGuard<'a>), } -/// lock_for_write() return value -pub enum WriteBufResult<'a> { - Found(PageWriteGuard<'a>), - NotFound(PageWriteGuard<'a>), -} - impl PageCache { // // Section 1.1: Public interface functions for looking up and memorizing materialized page @@ -446,20 +456,77 @@ impl PageCache { lsn, }; - match self.lock_for_write(&cache_key).await? { - WriteBufResult::Found(write_guard) => { - // We already had it in cache. Another thread must've put it there - // concurrently. Check that it had the same contents that we - // replayed. - assert!(*write_guard == img); + let mut permit = Some(self.try_get_pinned_slot_permit().await?); + loop { + // First check if the key already exists in the cache. + if let Some(slot_idx) = self.search_mapping_exact(&cache_key) { + // The page was found in the mapping. Lock the slot, and re-check + // that it's still what we expected (because we don't released the mapping + // lock already, another thread could have evicted the page) + let slot = &self.slots[slot_idx]; + let inner = slot.inner.write().await; + if inner.key.as_ref() == Some(&cache_key) { + slot.inc_usage_count(); + debug_assert!( + { + let guard = inner.permit.lock().unwrap(); + guard.upgrade().is_none() + }, + "we hold a write lock, so, no one else should have a permit" + ); + debug_assert_eq!(inner.buf.len(), img.len()); + // We already had it in cache. Another thread must've put it there + // concurrently. Check that it had the same contents that we + // replayed. + assert!(inner.buf == img); + return Ok(()); + } } - WriteBufResult::NotFound(mut write_guard) => { - write_guard.copy_from_slice(img); - write_guard.mark_valid(); - } - } + debug_assert!(permit.is_some()); - Ok(()) + // Not found. Find a victim buffer + let (slot_idx, mut inner) = self + .find_victim(permit.as_ref().unwrap()) + .await + .context("Failed to find evict victim")?; + + // Insert mapping for this. At this point, we may find that another + // thread did the same thing concurrently. In that case, we evicted + // our victim buffer unnecessarily. Put it into the free list and + // continue with the slot that the other thread chose. + if let Some(_existing_slot_idx) = self.try_insert_mapping(&cache_key, slot_idx) { + // TODO: put to free list + + // We now just loop back to start from beginning. This is not + // optimal, we'll perform the lookup in the mapping again, which + // is not really necessary because we already got + // 'existing_slot_idx'. But this shouldn't happen often enough + // to matter much. + continue; + } + + // Make the slot ready + let slot = &self.slots[slot_idx]; + inner.key = Some(cache_key.clone()); + slot.set_usage_count(1); + // Create a write guard for the slot so we go through the expected motions. + debug_assert!( + { + let guard = inner.permit.lock().unwrap(); + guard.upgrade().is_none() + }, + "we hold a write lock, so, no one else should have a permit" + ); + let mut write_guard = PageWriteGuard { + state: PageWriteGuardState::Invalid { + _permit: permit.take().unwrap(), + inner, + }, + }; + write_guard.copy_from_slice(img); + let _ = write_guard.mark_valid(); + return Ok(()); + } } // Section 1.2: Public interface functions for working with immutable file pages. @@ -638,99 +705,10 @@ impl PageCache { ); return Ok(ReadBufResult::NotFound(PageWriteGuard { - _permit: permit.take().unwrap(), - inner, - valid: false, - })); - } - } - - /// Look up a page in the cache and lock it in write mode. If it's not - /// found, returns None. - /// - /// When locking a page for writing, the search criteria is always "exact". - async fn try_lock_for_write( - &self, - cache_key: &CacheKey, - permit: &mut Option, - ) -> Option { - if let Some(slot_idx) = self.search_mapping_for_write(cache_key) { - // The page was found in the mapping. Lock the slot, and re-check - // that it's still what we expected (because we don't released the mapping - // lock already, another thread could have evicted the page) - let slot = &self.slots[slot_idx]; - let inner = slot.inner.write().await; - if inner.key.as_ref() == Some(cache_key) { - slot.inc_usage_count(); - debug_assert!( - { - let guard = inner.permit.lock().unwrap(); - guard.upgrade().is_none() - }, - "we hold a write lock, so, no one else should have a permit" - ); - return Some(PageWriteGuard { + state: PageWriteGuardState::Invalid { _permit: permit.take().unwrap(), inner, - valid: true, - }); - } - } - None - } - - /// Return a write-locked buffer for given block. - /// - /// Similar to lock_for_read(), but the returned buffer is write-locked and - /// may be modified by the caller even if it's already found in the cache. - async fn lock_for_write(&self, cache_key: &CacheKey) -> anyhow::Result { - let mut permit = Some(self.try_get_pinned_slot_permit().await?); - loop { - // First check if the key already exists in the cache. - if let Some(write_guard) = self.try_lock_for_write(cache_key, &mut permit).await { - debug_assert!(permit.is_none()); - return Ok(WriteBufResult::Found(write_guard)); - } - debug_assert!(permit.is_some()); - - // Not found. Find a victim buffer - let (slot_idx, mut inner) = self - .find_victim(permit.as_ref().unwrap()) - .await - .context("Failed to find evict victim")?; - - // Insert mapping for this. At this point, we may find that another - // thread did the same thing concurrently. In that case, we evicted - // our victim buffer unnecessarily. Put it into the free list and - // continue with the slot that the other thread chose. - if let Some(_existing_slot_idx) = self.try_insert_mapping(cache_key, slot_idx) { - // TODO: put to free list - - // We now just loop back to start from beginning. This is not - // optimal, we'll perform the lookup in the mapping again, which - // is not really necessary because we already got - // 'existing_slot_idx'. But this shouldn't happen often enough - // to matter much. - continue; - } - - // Make the slot ready - let slot = &self.slots[slot_idx]; - inner.key = Some(cache_key.clone()); - slot.set_usage_count(1); - - debug_assert!( - { - let guard = inner.permit.lock().unwrap(); - guard.upgrade().is_none() }, - "we hold a write lock, so, no one else should have a permit" - ); - - return Ok(WriteBufResult::NotFound(PageWriteGuard { - _permit: permit.take().unwrap(), - inner, - valid: false, })); } } @@ -775,7 +753,7 @@ impl PageCache { /// /// Like 'search_mapping, but performs an "exact" search. Used for /// allocating a new buffer. - fn search_mapping_for_write(&self, key: &CacheKey) -> Option { + fn search_mapping_exact(&self, key: &CacheKey) -> Option { match key { CacheKey::MaterializedPage { hash_key, lsn } => { let map = self.materialized_page_map.read().unwrap(); diff --git a/pageserver/src/page_service.rs b/pageserver/src/page_service.rs index 2a87ee0381..5ab4fbbd4c 100644 --- a/pageserver/src/page_service.rs +++ b/pageserver/src/page_service.rs @@ -35,6 +35,7 @@ use std::time::Duration; use tokio::io::AsyncWriteExt; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_util::io::StreamReader; +use tokio_util::sync::CancellationToken; use tracing::field; use tracing::*; use utils::id::ConnectionId; @@ -64,69 +65,6 @@ use crate::trace::Tracer; use postgres_ffi::pg_constants::DEFAULTTABLESPACE_OID; use postgres_ffi::BLCKSZ; -fn copyin_stream(pgb: &mut PostgresBackend) -> impl Stream> + '_ -where - IO: AsyncRead + AsyncWrite + Unpin, -{ - async_stream::try_stream! { - loop { - let msg = tokio::select! { - biased; - - _ = task_mgr::shutdown_watcher() => { - // We were requested to shut down. - let msg = "pageserver is shutting down"; - let _ = pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, None)); - Err(QueryError::Other(anyhow::anyhow!(msg))) - } - - msg = pgb.read_message() => { msg.map_err(QueryError::from)} - }; - - match msg { - Ok(Some(message)) => { - let copy_data_bytes = match message { - FeMessage::CopyData(bytes) => bytes, - FeMessage::CopyDone => { break }, - FeMessage::Sync => continue, - FeMessage::Terminate => { - let msg = "client terminated connection with Terminate message during COPY"; - let query_error = QueryError::Disconnected(ConnectionError::Io(io::Error::new(io::ErrorKind::ConnectionReset, msg))); - // error can't happen here, ErrorResponse serialization should be always ok - pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, Some(query_error.pg_error_code()))).map_err(|e| e.into_io_error())?; - Err(io::Error::new(io::ErrorKind::ConnectionReset, msg))?; - break; - } - m => { - let msg = format!("unexpected message {m:?}"); - // error can't happen here, ErrorResponse serialization should be always ok - pgb.write_message_noflush(&BeMessage::ErrorResponse(&msg, None)).map_err(|e| e.into_io_error())?; - Err(io::Error::new(io::ErrorKind::Other, msg))?; - break; - } - }; - - yield copy_data_bytes; - } - Ok(None) => { - let msg = "client closed connection during COPY"; - let query_error = QueryError::Disconnected(ConnectionError::Io(io::Error::new(io::ErrorKind::ConnectionReset, msg))); - // error can't happen here, ErrorResponse serialization should be always ok - pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, Some(query_error.pg_error_code()))).map_err(|e| e.into_io_error())?; - pgb.flush().await?; - Err(io::Error::new(io::ErrorKind::ConnectionReset, msg))?; - } - Err(QueryError::Disconnected(ConnectionError::Io(io_error))) => { - Err(io_error)?; - } - Err(other) => { - Err(io::Error::new(io::ErrorKind::Other, other.to_string()))?; - } - }; - } - } -} - /// Read the end of a tar archive. /// /// A tar archive normally ends with two consecutive blocks of zeros, 512 bytes each. @@ -284,7 +222,13 @@ async fn page_service_conn_main( // and create a child per-query context when it invokes process_query. // But it's in a shared crate, so, we store connection_ctx inside PageServerHandler // and create the per-query context in process_query ourselves. - let mut conn_handler = PageServerHandler::new(conf, broker_client, auth, connection_ctx); + let mut conn_handler = PageServerHandler::new( + conf, + broker_client, + auth, + connection_ctx, + task_mgr::shutdown_token(), + ); let pgbackend = PostgresBackend::new_from_io(socket, peer_addr, auth_type, None)?; match pgbackend @@ -318,6 +262,10 @@ struct PageServerHandler { /// For each query received over the connection, /// `process_query` creates a child context from this one. connection_ctx: RequestContext, + + /// A token that should fire when the tenant transitions from + /// attached state, or when the pageserver is shutting down. + cancel: CancellationToken, } impl PageServerHandler { @@ -326,6 +274,7 @@ impl PageServerHandler { broker_client: storage_broker::BrokerClientChannel, auth: Option>, connection_ctx: RequestContext, + cancel: CancellationToken, ) -> Self { PageServerHandler { _conf: conf, @@ -333,6 +282,91 @@ impl PageServerHandler { auth, claims: None, connection_ctx, + cancel, + } + } + + /// Wrap PostgresBackend::flush to respect our CancellationToken: it is important to use + /// this rather than naked flush() in order to shut down promptly. Without this, we would + /// block shutdown of a tenant if a postgres client was failing to consume bytes we send + /// in the flush. + async fn flush_cancellable(&self, pgb: &mut PostgresBackend) -> Result<(), QueryError> + where + IO: AsyncRead + AsyncWrite + Send + Sync + Unpin, + { + tokio::select!( + flush_r = pgb.flush() => { + Ok(flush_r?) + }, + _ = self.cancel.cancelled() => { + Err(QueryError::Other(anyhow::anyhow!("Shutting down"))) + } + ) + } + + fn copyin_stream<'a, IO>( + &'a self, + pgb: &'a mut PostgresBackend, + ) -> impl Stream> + 'a + where + IO: AsyncRead + AsyncWrite + Send + Sync + Unpin, + { + async_stream::try_stream! { + loop { + let msg = tokio::select! { + biased; + + _ = task_mgr::shutdown_watcher() => { + // We were requested to shut down. + let msg = "pageserver is shutting down"; + let _ = pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, None)); + Err(QueryError::Other(anyhow::anyhow!(msg))) + } + + msg = pgb.read_message() => { msg.map_err(QueryError::from)} + }; + + match msg { + Ok(Some(message)) => { + let copy_data_bytes = match message { + FeMessage::CopyData(bytes) => bytes, + FeMessage::CopyDone => { break }, + FeMessage::Sync => continue, + FeMessage::Terminate => { + let msg = "client terminated connection with Terminate message during COPY"; + let query_error = QueryError::Disconnected(ConnectionError::Io(io::Error::new(io::ErrorKind::ConnectionReset, msg))); + // error can't happen here, ErrorResponse serialization should be always ok + pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, Some(query_error.pg_error_code()))).map_err(|e| e.into_io_error())?; + Err(io::Error::new(io::ErrorKind::ConnectionReset, msg))?; + break; + } + m => { + let msg = format!("unexpected message {m:?}"); + // error can't happen here, ErrorResponse serialization should be always ok + pgb.write_message_noflush(&BeMessage::ErrorResponse(&msg, None)).map_err(|e| e.into_io_error())?; + Err(io::Error::new(io::ErrorKind::Other, msg))?; + break; + } + }; + + yield copy_data_bytes; + } + Ok(None) => { + let msg = "client closed connection during COPY"; + let query_error = QueryError::Disconnected(ConnectionError::Io(io::Error::new(io::ErrorKind::ConnectionReset, msg))); + // error can't happen here, ErrorResponse serialization should be always ok + pgb.write_message_noflush(&BeMessage::ErrorResponse(msg, Some(query_error.pg_error_code()))).map_err(|e| e.into_io_error())?; + self.flush_cancellable(pgb).await.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + Err(io::Error::new(io::ErrorKind::ConnectionReset, msg))?; + } + Err(QueryError::Disconnected(ConnectionError::Io(io_error))) => { + Err(io_error)?; + } + Err(other) => { + Err(io::Error::new(io::ErrorKind::Other, other.to_string()))?; + } + }; + } } } @@ -372,7 +406,7 @@ impl PageServerHandler { // switch client to COPYBOTH pgb.write_message_noflush(&BeMessage::CopyBothResponse)?; - pgb.flush().await?; + self.flush_cancellable(pgb).await?; let metrics = metrics::SmgrQueryTimePerTimeline::new(&tenant_id, &timeline_id); @@ -412,38 +446,60 @@ impl PageServerHandler { // TODO: We could create a new per-request context here, with unique ID. // Currently we use the same per-timeline context for all requests - let response = match neon_fe_msg { + let (response, span) = match neon_fe_msg { PagestreamFeMessage::Exists(req) => { let _timer = metrics.start_timer(metrics::SmgrQueryType::GetRelExists); - self.handle_get_rel_exists_request(&timeline, &req, &ctx) - .await + let span = tracing::info_span!("handle_get_rel_exists_request", rel = %req.rel, req_lsn = %req.lsn); + ( + self.handle_get_rel_exists_request(&timeline, &req, &ctx) + .instrument(span.clone()) + .await, + span, + ) } PagestreamFeMessage::Nblocks(req) => { let _timer = metrics.start_timer(metrics::SmgrQueryType::GetRelSize); - self.handle_get_nblocks_request(&timeline, &req, &ctx).await + let span = tracing::info_span!("handle_get_nblocks_request", rel = %req.rel, req_lsn = %req.lsn); + ( + self.handle_get_nblocks_request(&timeline, &req, &ctx) + .instrument(span.clone()) + .await, + span, + ) } PagestreamFeMessage::GetPage(req) => { let _timer = metrics.start_timer(metrics::SmgrQueryType::GetPageAtLsn); - self.handle_get_page_at_lsn_request(&timeline, &req, &ctx) - .await + let span = tracing::info_span!("handle_get_page_at_lsn_request", rel = %req.rel, blkno = %req.blkno, req_lsn = %req.lsn); + ( + self.handle_get_page_at_lsn_request(&timeline, &req, &ctx) + .instrument(span.clone()) + .await, + span, + ) } PagestreamFeMessage::DbSize(req) => { let _timer = metrics.start_timer(metrics::SmgrQueryType::GetDbSize); - self.handle_db_size_request(&timeline, &req, &ctx).await + let span = tracing::info_span!("handle_db_size_request", dbnode = %req.dbnode, req_lsn = %req.lsn); + ( + self.handle_db_size_request(&timeline, &req, &ctx) + .instrument(span.clone()) + .await, + span, + ) } }; let response = response.unwrap_or_else(|e| { // print the all details to the log with {:#}, but for the client the // error message is enough - error!("error reading relation or page version: {:?}", e); + span.in_scope(|| error!("error reading relation or page version: {:#}", e)); PagestreamBeMessage::Error(PagestreamErrorResponse { message: e.to_string(), }) }); pgb.write_message_noflush(&BeMessage::CopyData(&response.serialize()))?; - pgb.flush().await?; + self.flush_cancellable(pgb).await?; } Ok(()) } @@ -486,9 +542,9 @@ impl PageServerHandler { // Import basebackup provided via CopyData info!("importing basebackup"); pgb.write_message_noflush(&BeMessage::CopyInResponse)?; - pgb.flush().await?; + self.flush_cancellable(pgb).await?; - let mut copyin_reader = pin!(StreamReader::new(copyin_stream(pgb))); + let mut copyin_reader = pin!(StreamReader::new(self.copyin_stream(pgb))); timeline .import_basebackup_from_tar( &mut copyin_reader, @@ -541,8 +597,8 @@ impl PageServerHandler { // Import wal provided via CopyData info!("importing wal"); pgb.write_message_noflush(&BeMessage::CopyInResponse)?; - pgb.flush().await?; - let mut copyin_reader = pin!(StreamReader::new(copyin_stream(pgb))); + self.flush_cancellable(pgb).await?; + let mut copyin_reader = pin!(StreamReader::new(self.copyin_stream(pgb))); import_wal_from_tar(&timeline, &mut copyin_reader, start_lsn, end_lsn, &ctx).await?; info!("wal import complete"); @@ -627,7 +683,6 @@ impl PageServerHandler { Ok(lsn) } - #[instrument(skip(self, timeline, req, ctx), fields(rel = %req.rel, req_lsn = %req.lsn))] async fn handle_get_rel_exists_request( &self, timeline: &Timeline, @@ -648,7 +703,6 @@ impl PageServerHandler { })) } - #[instrument(skip(self, timeline, req, ctx), fields(rel = %req.rel, req_lsn = %req.lsn))] async fn handle_get_nblocks_request( &self, timeline: &Timeline, @@ -667,7 +721,6 @@ impl PageServerHandler { })) } - #[instrument(skip(self, timeline, req, ctx), fields(dbnode = %req.dbnode, req_lsn = %req.lsn))] async fn handle_db_size_request( &self, timeline: &Timeline, @@ -689,7 +742,6 @@ impl PageServerHandler { })) } - #[instrument(skip(self, timeline, req, ctx), fields(rel = %req.rel, blkno = %req.blkno, req_lsn = %req.lsn))] async fn handle_get_page_at_lsn_request( &self, timeline: &Timeline, @@ -754,7 +806,7 @@ impl PageServerHandler { // switch client to COPYOUT pgb.write_message_noflush(&BeMessage::CopyOutResponse)?; - pgb.flush().await?; + self.flush_cancellable(pgb).await?; // Send a tarball of the latest layer on the timeline. Compress if not // fullbackup. TODO Compress in that case too (tests need to be updated) @@ -806,7 +858,7 @@ impl PageServerHandler { } pgb.write_message_noflush(&BeMessage::CopyDone)?; - pgb.flush().await?; + self.flush_cancellable(pgb).await?; let basebackup_after = started .elapsed() @@ -1265,7 +1317,10 @@ async fn get_active_tenant_with_timeout( Ok(tenant) => tenant, Err(e @ GetTenantError::NotFound(_)) => return Err(GetActiveTenantError::NotFound(e)), Err(GetTenantError::NotActive(_)) => { - unreachable!("we're calling get_tenant with active=false") + unreachable!("we're calling get_tenant with active_only=false") + } + Err(GetTenantError::Broken(_)) => { + unreachable!("we're calling get_tenant with active_only=false") } }; let wait_time = Duration::from_secs(30); diff --git a/pageserver/src/statvfs.rs b/pageserver/src/statvfs.rs index 28d950b5e6..08b5264290 100644 --- a/pageserver/src/statvfs.rs +++ b/pageserver/src/statvfs.rs @@ -1,6 +1,6 @@ //! Wrapper around nix::sys::statvfs::Statvfs that allows for mocking. -use std::path::Path; +use camino::Utf8Path; pub enum Statvfs { Real(nix::sys::statvfs::Statvfs), @@ -12,11 +12,13 @@ pub enum Statvfs { // Sincce it should only be a problem on > 2TiB disks, let's ignore // the problem for now and upcast to u64. impl Statvfs { - pub fn get(tenants_dir: &Path, mocked: Option<&mock::Behavior>) -> nix::Result { + pub fn get(tenants_dir: &Utf8Path, mocked: Option<&mock::Behavior>) -> nix::Result { if let Some(mocked) = mocked { Ok(Statvfs::Mock(mock::get(tenants_dir, mocked)?)) } else { - Ok(Statvfs::Real(nix::sys::statvfs::statvfs(tenants_dir)?)) + Ok(Statvfs::Real(nix::sys::statvfs::statvfs( + tenants_dir.as_std_path(), + )?)) } } @@ -55,8 +57,8 @@ impl Statvfs { pub mod mock { use anyhow::Context; + use camino::Utf8Path; use regex::Regex; - use std::path::Path; use tracing::log::info; #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -86,7 +88,7 @@ pub mod mock { } } - pub fn get(tenants_dir: &Path, behavior: &Behavior) -> nix::Result { + pub fn get(tenants_dir: &Utf8Path, behavior: &Behavior) -> nix::Result { info!("running mocked statvfs"); match behavior { @@ -119,7 +121,7 @@ pub mod mock { } } - fn walk_dir_disk_usage(path: &Path, name_filter: Option<&Regex>) -> anyhow::Result { + fn walk_dir_disk_usage(path: &Utf8Path, name_filter: Option<&Regex>) -> anyhow::Result { let mut total = 0; for entry in walkdir::WalkDir::new(path) { let entry = entry?; diff --git a/pageserver/src/tenant.rs b/pageserver/src/tenant.rs index 47bfd4a8ef..57c1b5f070 100644 --- a/pageserver/src/tenant.rs +++ b/pageserver/src/tenant.rs @@ -12,6 +12,7 @@ //! use anyhow::{bail, Context}; +use camino::{Utf8Path, Utf8PathBuf}; use futures::FutureExt; use pageserver_api::models::TimelineState; use remote_storage::DownloadError; @@ -34,8 +35,6 @@ use std::fs; use std::fs::File; use std::io; use std::ops::Bound::Included; -use std::path::Path; -use std::path::PathBuf; use std::process::Command; use std::process::Stdio; use std::sync::atomic::AtomicU64; @@ -45,6 +44,8 @@ use std::sync::MutexGuard; use std::sync::{Mutex, RwLock}; use std::time::{Duration, Instant}; +use self::config::AttachedLocationConfig; +use self::config::LocationConf; use self::config::TenantConf; use self::delete::DeleteTenantFlow; use self::metadata::LoadMetadataError; @@ -65,6 +66,7 @@ use crate::metrics::{remove_tenant_metrics, TENANT_STATE_METRIC, TENANT_SYNTHETI use crate::repository::GcResult; use crate::task_mgr; use crate::task_mgr::TaskKind; +use crate::tenant::config::LocationMode; use crate::tenant::config::TenantConfOpt; use crate::tenant::metadata::load_metadata; pub use crate::tenant::remote_timeline_client::index::IndexPart; @@ -161,6 +163,28 @@ pub struct TenantSharedResources { pub deletion_queue_client: DeletionQueueClient, } +/// A [`Tenant`] is really an _attached_ tenant. The configuration +/// for an attached tenant is a subset of the [`LocationConf`], represented +/// in this struct. +pub(super) struct AttachedTenantConf { + tenant_conf: TenantConfOpt, + location: AttachedLocationConfig, +} + +impl AttachedTenantConf { + fn try_from(location_conf: LocationConf) -> anyhow::Result { + match &location_conf.mode { + LocationMode::Attached(attach_conf) => Ok(Self { + tenant_conf: location_conf.tenant_conf, + location: attach_conf.clone(), + }), + LocationMode::Secondary(_) => { + anyhow::bail!("Attempted to construct AttachedTenantConf from a LocationConf in secondary mode") + } + } + } +} + /// /// Tenant consists of multiple timelines. Keep them in a hash table. /// @@ -178,12 +202,15 @@ pub struct Tenant { // We keep TenantConfOpt sturct here to preserve the information // about parameters that are not set. // This is necessary to allow global config updates. - tenant_conf: Arc>, + tenant_conf: Arc>, tenant_id: TenantId, /// The remote storage generation, used to protect S3 objects from split-brain. /// Does not change over the lifetime of the [`Tenant`] object. + /// + /// This duplicates the generation stored in LocationConf, but that structure is mutable: + /// this copy enforces the invariant that generatio doesn't change during a Tenant's lifetime. generation: Generation, timelines: Mutex>>, @@ -379,6 +406,8 @@ pub enum CreateTimelineError { AlreadyExists, #[error(transparent)] AncestorLsn(anyhow::Error), + #[error("ancestor timeline is not active")] + AncestorNotActive, #[error(transparent)] Other(#[from] anyhow::Error), } @@ -527,14 +556,13 @@ impl Tenant { pub(crate) fn spawn_attach( conf: &'static PageServerConf, tenant_id: TenantId, - generation: Generation, resources: TenantSharedResources, + attached_conf: AttachedTenantConf, tenants: &'static tokio::sync::RwLock, ctx: &RequestContext, ) -> anyhow::Result> { // TODO dedup with spawn_load - let tenant_conf = - Self::load_tenant_config(conf, &tenant_id).context("load tenant config")?; + let wal_redo_manager = Arc::new(PostgresRedoManager::new(conf, tenant_id)); let TenantSharedResources { broker_client, @@ -542,14 +570,12 @@ impl Tenant { deletion_queue_client, } = resources; - let wal_redo_manager = Arc::new(PostgresRedoManager::new(conf, tenant_id)); let tenant = Arc::new(Tenant::new( TenantState::Attaching, conf, - tenant_conf, + attached_conf, wal_redo_manager, tenant_id, - generation, remote_storage.clone(), deletion_queue_client, )); @@ -772,7 +798,7 @@ impl Tenant { } std::fs::remove_file(&marker_file) - .with_context(|| format!("unlink attach marker file {}", marker_file.display()))?; + .with_context(|| format!("unlink attach marker file {marker_file}"))?; crashsafe::fsync(marker_file.parent().expect("marker file has parent dir")) .context("fsync tenant directory after unlinking attach marker file")?; @@ -860,10 +886,9 @@ impl Tenant { backtrace: String::new(), }, conf, - TenantConfOpt::default(), + AttachedTenantConf::try_from(LocationConf::default()).unwrap(), wal_redo_manager, tenant_id, - Generation::broken(), None, DeletionQueueClient::broken(), )) @@ -882,7 +907,7 @@ impl Tenant { pub(crate) fn spawn_load( conf: &'static PageServerConf, tenant_id: TenantId, - generation: Generation, + attached_conf: AttachedTenantConf, resources: TenantSharedResources, init_order: Option, tenants: &'static tokio::sync::RwLock, @@ -890,14 +915,6 @@ impl Tenant { ) -> Arc { span::debug_assert_current_span_has_tenant_id(); - let tenant_conf = match Self::load_tenant_config(conf, &tenant_id) { - Ok(conf) => conf, - Err(e) => { - error!("load tenant config failed: {:?}", e); - return Tenant::create_broken_tenant(conf, tenant_id, format!("{e:#}")); - } - }; - let broker_client = resources.broker_client; let remote_storage = resources.remote_storage; @@ -905,10 +922,9 @@ impl Tenant { let tenant = Tenant::new( TenantState::Loading, conf, - tenant_conf, + attached_conf, wal_redo_manager, tenant_id, - generation, remote_storage.clone(), resources.deletion_queue_client.clone(), ); @@ -1024,58 +1040,47 @@ impl Tenant { let timelines_dir = self.conf.timelines_path(&self.tenant_id); - for entry in - std::fs::read_dir(&timelines_dir).context("list timelines directory for tenant")? + for entry in timelines_dir + .read_dir_utf8() + .context("list timelines directory for tenant")? { let entry = entry.context("read timeline dir entry")?; let timeline_dir = entry.path(); - if crate::is_temporary(&timeline_dir) { - info!( - "Found temporary timeline directory, removing: {}", - timeline_dir.display() - ); - if let Err(e) = std::fs::remove_dir_all(&timeline_dir) { - error!( - "Failed to remove temporary directory '{}': {:?}", - timeline_dir.display(), - e - ); + if crate::is_temporary(timeline_dir) { + info!("Found temporary timeline directory, removing: {timeline_dir}"); + if let Err(e) = std::fs::remove_dir_all(timeline_dir) { + error!("Failed to remove temporary directory '{timeline_dir}': {e:?}"); } - } else if is_uninit_mark(&timeline_dir) { + } else if is_uninit_mark(timeline_dir) { if !timeline_dir.exists() { - warn!( - "Timeline dir entry become invalid: {}", - timeline_dir.display() - ); + warn!("Timeline dir entry become invalid: {timeline_dir}"); continue; } let timeline_uninit_mark_file = &timeline_dir; info!( - "Found an uninit mark file {}, removing the timeline and its uninit mark", - timeline_uninit_mark_file.display() + "Found an uninit mark file {timeline_uninit_mark_file}, removing the timeline and its uninit mark", ); - let timeline_id = TimelineId::try_from(timeline_uninit_mark_file.file_stem()) - .with_context(|| { - format!( - "Could not parse timeline id out of the timeline uninit mark name {}", - timeline_uninit_mark_file.display() + let timeline_id = + TimelineId::try_from(timeline_uninit_mark_file.file_stem()) + .with_context(|| { + format!( + "Could not parse timeline id out of the timeline uninit mark name {timeline_uninit_mark_file}", ) - })?; + })?; let timeline_dir = self.conf.timeline_path(&self.tenant_id, &timeline_id); if let Err(e) = remove_timeline_and_uninit_mark(&timeline_dir, timeline_uninit_mark_file) { error!("Failed to clean up uninit marked timeline: {e:?}"); } - } else if crate::is_delete_mark(&timeline_dir) { + } else if crate::is_delete_mark(timeline_dir) { // If metadata exists, load as usual, continue deletion - let timeline_id = - TimelineId::try_from(timeline_dir.file_stem()).with_context(|| { + let timeline_id = TimelineId::try_from(timeline_dir.file_stem()) + .with_context(|| { format!( - "Could not parse timeline id out of the timeline uninit mark name {}", - timeline_dir.display() + "Could not parse timeline id out of the timeline uninit mark name {timeline_dir}", ) })?; @@ -1114,17 +1119,13 @@ impl Tenant { } } else { if !timeline_dir.exists() { - warn!( - "Timeline dir entry become invalid: {}", - timeline_dir.display() - ); + warn!("Timeline dir entry become invalid: {timeline_dir}"); continue; } - let timeline_id = - TimelineId::try_from(timeline_dir.file_name()).with_context(|| { + let timeline_id = TimelineId::try_from(timeline_dir.file_name()) + .with_context(|| { format!( - "Could not parse timeline id out of the timeline dir name {}", - timeline_dir.display() + "Could not parse timeline id out of the timeline dir name {timeline_dir}", ) })?; let timeline_uninit_mark_file = self @@ -1136,7 +1137,7 @@ impl Tenant { "Found an uninit mark file, removing the timeline and its uninit mark", ); if let Err(e) = - remove_timeline_and_uninit_mark(&timeline_dir, &timeline_uninit_mark_file) + remove_timeline_and_uninit_mark(timeline_dir, &timeline_uninit_mark_file) { error!("Failed to clean up uninit marked timeline: {e:?}"); } @@ -1152,18 +1153,13 @@ impl Tenant { } let file_name = entry.file_name(); - if let Ok(timeline_id) = - file_name.to_str().unwrap_or_default().parse::() - { + if let Ok(timeline_id) = file_name.parse::() { let metadata = load_metadata(self.conf, &self.tenant_id, &timeline_id) .context("failed to load metadata")?; timelines_to_load.insert(timeline_id, metadata); } else { // A file or directory that doesn't look like a timeline ID - warn!( - "unexpected file or directory in timelines directory: {}", - file_name.to_string_lossy() - ); + warn!("unexpected file or directory in timelines directory: {file_name}"); } } } @@ -1593,6 +1589,12 @@ impl Tenant { .get_timeline(ancestor_timeline_id, false) .context("Cannot branch off the timeline that's not present in pageserver")?; + // instead of waiting around, just deny the request because ancestor is not yet + // ready for other purposes either. + if !ancestor_timeline.is_active() { + return Err(CreateTimelineError::AncestorNotActive); + } + if let Some(lsn) = ancestor_start_lsn.as_mut() { *lsn = lsn.align(); @@ -1625,8 +1627,6 @@ impl Tenant { } }; - loaded_timeline.activate(broker_client, None, ctx); - if let Some(remote_client) = loaded_timeline.remote_client.as_ref() { // Wait for the upload of the 'index_part.json` file to finish, so that when we return // Ok, the timeline is durable in remote storage. @@ -1638,6 +1638,8 @@ impl Tenant { })?; } + loaded_timeline.activate(broker_client, None, ctx); + Ok(loaded_timeline) } @@ -1667,6 +1669,15 @@ impl Tenant { "Cannot run GC iteration on inactive tenant" ); + { + let conf = self.tenant_conf.read().unwrap(); + + if !conf.location.may_delete_layers_hint() { + info!("Skipping GC in location state {:?}", conf.location); + return Ok(GcResult::default()); + } + } + self.gc_iteration_internal(target_timeline_id, horizon, pitr, ctx) .await } @@ -1685,6 +1696,14 @@ impl Tenant { "Cannot run compaction iteration on inactive tenant" ); + { + let conf = self.tenant_conf.read().unwrap(); + if !conf.location.may_delete_layers_hint() || !conf.location.may_upload_layers_hint() { + info!("Skipping compaction in location state {:?}", conf.location); + return Ok(()); + } + } + // Scan through the hashmap and collect a list of all the timelines, // while holding the lock. Then drop the lock and actually perform the // compactions. We don't want to block everything else while the @@ -2110,7 +2129,7 @@ where impl Tenant { pub fn tenant_specific_overrides(&self) -> TenantConfOpt { - *self.tenant_conf.read().unwrap() + self.tenant_conf.read().unwrap().tenant_conf } pub fn effective_config(&self) -> TenantConf { @@ -2119,84 +2138,95 @@ impl Tenant { } pub fn get_checkpoint_distance(&self) -> u64 { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .checkpoint_distance .unwrap_or(self.conf.default_tenant_conf.checkpoint_distance) } pub fn get_checkpoint_timeout(&self) -> Duration { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .checkpoint_timeout .unwrap_or(self.conf.default_tenant_conf.checkpoint_timeout) } pub fn get_compaction_target_size(&self) -> u64 { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .compaction_target_size .unwrap_or(self.conf.default_tenant_conf.compaction_target_size) } pub fn get_compaction_period(&self) -> Duration { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .compaction_period .unwrap_or(self.conf.default_tenant_conf.compaction_period) } pub fn get_compaction_threshold(&self) -> usize { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .compaction_threshold .unwrap_or(self.conf.default_tenant_conf.compaction_threshold) } pub fn get_gc_horizon(&self) -> u64 { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .gc_horizon .unwrap_or(self.conf.default_tenant_conf.gc_horizon) } pub fn get_gc_period(&self) -> Duration { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .gc_period .unwrap_or(self.conf.default_tenant_conf.gc_period) } pub fn get_image_creation_threshold(&self) -> usize { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .image_creation_threshold .unwrap_or(self.conf.default_tenant_conf.image_creation_threshold) } pub fn get_pitr_interval(&self) -> Duration { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .pitr_interval .unwrap_or(self.conf.default_tenant_conf.pitr_interval) } pub fn get_trace_read_requests(&self) -> bool { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .trace_read_requests .unwrap_or(self.conf.default_tenant_conf.trace_read_requests) } pub fn get_min_resident_size_override(&self) -> Option { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .min_resident_size_override .or(self.conf.default_tenant_conf.min_resident_size_override) } pub fn set_new_tenant_config(&self, new_tenant_conf: TenantConfOpt) { - *self.tenant_conf.write().unwrap() = new_tenant_conf; + self.tenant_conf.write().unwrap().tenant_conf = new_tenant_conf; + // Don't hold self.timelines.lock() during the notifies. + // There's no risk of deadlock right now, but there could be if we consolidate + // mutexes in struct Timeline in the future. + let timelines = self.list_timelines(); + for timeline in timelines { + timeline.tenant_conf_updated(); + } + } + + pub(crate) fn set_new_location_config(&self, new_conf: AttachedTenantConf) { + *self.tenant_conf.write().unwrap() = new_conf; // Don't hold self.timelines.lock() during the notifies. // There's no risk of deadlock right now, but there could be if we consolidate // mutexes in struct Timeline in the future. @@ -2266,10 +2296,9 @@ impl Tenant { fn new( state: TenantState, conf: &'static PageServerConf, - tenant_conf: TenantConfOpt, + attached_conf: AttachedTenantConf, walredo_mgr: Arc, tenant_id: TenantId, - generation: Generation, remote_storage: Option, deletion_queue_client: DeletionQueueClient, ) -> Tenant { @@ -2329,12 +2358,12 @@ impl Tenant { Tenant { tenant_id, - generation, + generation: attached_conf.location.generation, conf, // using now here is good enough approximation to catch tenants with really long // activation times. loading_started_at: Instant::now(), - tenant_conf: Arc::new(RwLock::new(tenant_conf)), + tenant_conf: Arc::new(RwLock::new(attached_conf)), timelines: Mutex::new(HashMap::new()), gc_cs: tokio::sync::Mutex::new(()), walredo_mgr, @@ -2352,54 +2381,124 @@ impl Tenant { pub(super) fn load_tenant_config( conf: &'static PageServerConf, tenant_id: &TenantId, - ) -> anyhow::Result { - let target_config_path = conf.tenant_config_path(tenant_id); - let target_config_display = target_config_path.display(); + ) -> anyhow::Result { + let legacy_config_path = conf.tenant_config_path(tenant_id); + let config_path = conf.tenant_location_config_path(tenant_id); - info!("loading tenantconf from {target_config_display}"); + if config_path.exists() { + // New-style config takes precedence + let deserialized = Self::read_config(&config_path)?; + Ok(toml_edit::de::from_document::(deserialized)?) + } else if legacy_config_path.exists() { + // Upgrade path: found an old-style configuration only + let deserialized = Self::read_config(&legacy_config_path)?; - // FIXME If the config file is not found, assume that we're attaching - // a detached tenant and config is passed via attach command. - // https://github.com/neondatabase/neon/issues/1555 - // OR: we're loading after incomplete deletion that managed to remove config. - if !target_config_path.exists() { - info!("tenant config not found in {target_config_display}"); - return Ok(TenantConfOpt::default()); + let mut tenant_conf = TenantConfOpt::default(); + for (key, item) in deserialized.iter() { + match key { + "tenant_config" => { + tenant_conf = PageServerConf::parse_toml_tenant_conf(item).with_context(|| { + format!("Failed to parse config from file '{legacy_config_path}' as pageserver config") + })?; + } + _ => bail!( + "config file {legacy_config_path} has unrecognized pageserver option '{key}'" + ), + } + } + + // Legacy configs are implicitly in attached state + Ok(LocationConf::attached_single( + tenant_conf, + Generation::none(), + )) + } else { + // FIXME If the config file is not found, assume that we're attaching + // a detached tenant and config is passed via attach command. + // https://github.com/neondatabase/neon/issues/1555 + // OR: we're loading after incomplete deletion that managed to remove config. + info!( + "tenant config not found in {} or {}", + config_path, legacy_config_path + ); + Ok(LocationConf::default()) } + } + + fn read_config(path: &Utf8Path) -> anyhow::Result { + info!("loading tenant configuration from {path}"); // load and parse file - let config = fs::read_to_string(&target_config_path).with_context(|| { - format!("Failed to load config from path '{target_config_display}'") - })?; + let config = fs::read_to_string(path) + .with_context(|| format!("Failed to load config from path '{path}'"))?; - let toml = config.parse::().with_context(|| { - format!("Failed to parse config from file '{target_config_display}' as toml file") - })?; - - let mut tenant_conf = TenantConfOpt::default(); - for (key, item) in toml.iter() { - match key { - "tenant_config" => { - tenant_conf = PageServerConf::parse_toml_tenant_conf(item).with_context(|| { - format!("Failed to parse config from file '{target_config_display}' as pageserver config") - })?; - } - _ => bail!("config file {target_config_display} has unrecognized pageserver option '{key}'"), - - } - } - - Ok(tenant_conf) + config + .parse::() + .with_context(|| format!("Failed to parse config from file '{path}' as toml file")) } #[tracing::instrument(skip_all, fields(%tenant_id))] pub(super) async fn persist_tenant_config( + conf: &'static PageServerConf, tenant_id: &TenantId, - target_config_path: &Path, - tenant_conf: TenantConfOpt, + location_conf: &LocationConf, ) -> anyhow::Result<()> { - // imitate a try-block with a closure - info!("persisting tenantconf to {}", target_config_path.display()); + let legacy_config_path = conf.tenant_config_path(tenant_id); + let config_path = conf.tenant_location_config_path(tenant_id); + Self::persist_tenant_config_at(tenant_id, &config_path, &legacy_config_path, location_conf) + .await + } + + #[tracing::instrument(skip_all, fields(%tenant_id))] + pub(super) async fn persist_tenant_config_at( + tenant_id: &TenantId, + config_path: &Utf8Path, + legacy_config_path: &Utf8Path, + location_conf: &LocationConf, + ) -> anyhow::Result<()> { + // Forward compat: write out an old-style configuration that old versions can read, in case we roll back + Self::persist_tenant_config_legacy( + tenant_id, + legacy_config_path, + &location_conf.tenant_conf, + ) + .await?; + + if let LocationMode::Attached(attach_conf) = &location_conf.mode { + // Once we use LocationMode, generations are mandatory. If we aren't using generations, + // then drop out after writing legacy-style config. + if attach_conf.generation.is_none() { + tracing::debug!("Running without generations, not writing new-style LocationConf"); + return Ok(()); + } + } + + info!("persisting tenantconf to {config_path}"); + + let mut conf_content = r#"# This file contains a specific per-tenant's config. +# It is read in case of pageserver restart. +"# + .to_string(); + + // Convert the config to a toml file. + conf_content += &toml_edit::ser::to_string_pretty(&location_conf)?; + + let conf_content = conf_content.as_bytes(); + + let temp_path = path_with_suffix_extension(config_path, TEMP_FILE_SUFFIX); + VirtualFile::crashsafe_overwrite(config_path, &temp_path, conf_content) + .await + .with_context(|| format!("write tenant {tenant_id} config to {config_path}"))?; + Ok(()) + } + + #[tracing::instrument(skip_all, fields(%tenant_id))] + async fn persist_tenant_config_legacy( + tenant_id: &TenantId, + target_config_path: &Utf8Path, + tenant_conf: &TenantConfOpt, + ) -> anyhow::Result<()> { + info!("persisting tenantconf to {target_config_path}"); let mut conf_content = r#"# This file contains a specific per-tenant's config. # It is read in case of pageserver restart. @@ -2416,12 +2515,7 @@ impl Tenant { let temp_path = path_with_suffix_extension(target_config_path, TEMP_FILE_SUFFIX); VirtualFile::crashsafe_overwrite(target_config_path, &temp_path, conf_content) .await - .with_context(|| { - format!( - "write tenant {tenant_id} config to {}", - target_config_path.display() - ) - })?; + .with_context(|| format!("write tenant {tenant_id} config to {target_config_path}"))?; Ok(()) } @@ -2788,10 +2882,7 @@ impl Tenant { // current initdb was not run yet, so remove whatever was left from the previous runs if initdb_path.exists() { fs::remove_dir_all(&initdb_path).with_context(|| { - format!( - "Failed to remove already existing initdb directory: {}", - initdb_path.display() - ) + format!("Failed to remove already existing initdb directory: {initdb_path}") })?; } // Init temporarily repo to get bootstrap data, this creates a directory in the `initdb_path` path @@ -2800,7 +2891,7 @@ impl Tenant { scopeguard::defer! { if let Err(e) = fs::remove_dir_all(&initdb_path) { // this is unlikely, but we will remove the directory on pageserver restart or another bootstrap call - error!("Failed to remove temporary initdb directory '{}': {}", initdb_path.display(), e); + error!("Failed to remove temporary initdb directory '{initdb_path}': {e}"); } } let pgdata_path = &initdb_path; @@ -2950,7 +3041,7 @@ impl Tenant { async fn create_timeline_files( &self, - timeline_path: &Path, + timeline_path: &Utf8Path, new_timeline_id: &TimelineId, new_metadata: &TimelineMetadata, ) -> anyhow::Result<()> { @@ -2984,8 +3075,7 @@ impl Tenant { let timeline_path = self.conf.timeline_path(&tenant_id, &timeline_id); anyhow::ensure!( !timeline_path.exists(), - "Timeline {} already exists, cannot create its uninit mark file", - timeline_path.display() + "Timeline {timeline_path} already exists, cannot create its uninit mark file", ); let uninit_mark_path = self @@ -3077,7 +3167,10 @@ impl Tenant { } } -fn remove_timeline_and_uninit_mark(timeline_dir: &Path, uninit_mark: &Path) -> anyhow::Result<()> { +fn remove_timeline_and_uninit_mark( + timeline_dir: &Utf8Path, + uninit_mark: &Utf8Path, +) -> anyhow::Result<()> { fs::remove_dir_all(timeline_dir) .or_else(|e| { if e.kind() == std::io::ErrorKind::NotFound { @@ -3089,17 +3182,10 @@ fn remove_timeline_and_uninit_mark(timeline_dir: &Path, uninit_mark: &Path) -> a } }) .with_context(|| { - format!( - "Failed to remove unit marked timeline directory {}", - timeline_dir.display() - ) + format!("Failed to remove unit marked timeline directory {timeline_dir}") })?; - fs::remove_file(uninit_mark).with_context(|| { - format!( - "Failed to remove timeline uninit mark file {}", - uninit_mark.display() - ) - })?; + fs::remove_file(uninit_mark) + .with_context(|| format!("Failed to remove timeline uninit mark file {uninit_mark}"))?; Ok(()) } @@ -3111,10 +3197,10 @@ pub(crate) enum CreateTenantFilesMode { pub(crate) async fn create_tenant_files( conf: &'static PageServerConf, - tenant_conf: TenantConfOpt, + location_conf: &LocationConf, tenant_id: &TenantId, mode: CreateTenantFilesMode, -) -> anyhow::Result { +) -> anyhow::Result { let target_tenant_directory = conf.tenant_path(tenant_id); anyhow::ensure!( !target_tenant_directory @@ -3125,22 +3211,16 @@ pub(crate) async fn create_tenant_files( let temporary_tenant_dir = path_with_suffix_extension(&target_tenant_directory, TEMP_FILE_SUFFIX); - debug!( - "Creating temporary directory structure in {}", - temporary_tenant_dir.display() - ); + debug!("Creating temporary directory structure in {temporary_tenant_dir}"); // top-level dir may exist if we are creating it through CLI crashsafe::create_dir_all(&temporary_tenant_dir).with_context(|| { - format!( - "could not create temporary tenant directory {}", - temporary_tenant_dir.display() - ) + format!("could not create temporary tenant directory {temporary_tenant_dir}") })?; let creation_result = try_create_target_tenant_dir( conf, - tenant_conf, + location_conf, tenant_id, mode, &temporary_tenant_dir, @@ -3166,11 +3246,11 @@ pub(crate) async fn create_tenant_files( async fn try_create_target_tenant_dir( conf: &'static PageServerConf, - tenant_conf: TenantConfOpt, + location_conf: &LocationConf, tenant_id: &TenantId, mode: CreateTenantFilesMode, - temporary_tenant_dir: &Path, - target_tenant_directory: &Path, + temporary_tenant_dir: &Utf8Path, + target_tenant_directory: &Utf8Path, ) -> Result<(), anyhow::Error> { match mode { CreateTenantFilesMode::Create => {} // needs no attach marker, writing tenant conf + atomic rename of dir is good enough @@ -3196,20 +3276,31 @@ async fn try_create_target_tenant_dir( temporary_tenant_dir, ) .with_context(|| format!("resolve tenant {tenant_id} temporary timelines dir"))?; - let temporary_tenant_config_path = rebase_directory( + let temporary_legacy_tenant_config_path = rebase_directory( &conf.tenant_config_path(tenant_id), target_tenant_directory, temporary_tenant_dir, ) .with_context(|| format!("resolve tenant {tenant_id} temporary config path"))?; + let temporary_tenant_config_path = rebase_directory( + &conf.tenant_location_config_path(tenant_id), + target_tenant_directory, + temporary_tenant_dir, + ) + .with_context(|| format!("resolve tenant {tenant_id} temporary config path"))?; - Tenant::persist_tenant_config(tenant_id, &temporary_tenant_config_path, tenant_conf).await?; + Tenant::persist_tenant_config_at( + tenant_id, + &temporary_tenant_config_path, + &temporary_legacy_tenant_config_path, + location_conf, + ) + .await?; crashsafe::create_dir(&temporary_tenant_timelines_dir).with_context(|| { format!( "create tenant {} temporary timelines directory {}", - tenant_id, - temporary_tenant_timelines_dir.display() + tenant_id, temporary_tenant_timelines_dir, ) })?; fail::fail_point!("tenant-creation-before-tmp-rename", |_| { @@ -3224,35 +3315,34 @@ async fn try_create_target_tenant_dir( fs::rename(temporary_tenant_dir, target_tenant_directory).with_context(|| { format!( "move tenant {} temporary directory {} into the permanent one {}", - tenant_id, - temporary_tenant_dir.display(), - target_tenant_directory.display() + tenant_id, temporary_tenant_dir, target_tenant_directory ) })?; let target_dir_parent = target_tenant_directory.parent().with_context(|| { format!( "get tenant {} dir parent for {}", - tenant_id, - target_tenant_directory.display() + tenant_id, target_tenant_directory, ) })?; crashsafe::fsync(target_dir_parent).with_context(|| { format!( "fsync renamed directory's parent {} for tenant {}", - target_dir_parent.display(), - tenant_id, + target_dir_parent, tenant_id, ) })?; Ok(()) } -fn rebase_directory(original_path: &Path, base: &Path, new_base: &Path) -> anyhow::Result { +fn rebase_directory( + original_path: &Utf8Path, + base: &Utf8Path, + new_base: &Utf8Path, +) -> anyhow::Result { let relative_path = original_path.strip_prefix(base).with_context(|| { format!( "Failed to strip base prefix '{}' off path '{}'", - base.display(), - original_path.display() + base, original_path ) })?; Ok(new_base.join(relative_path)) @@ -3262,20 +3352,18 @@ fn rebase_directory(original_path: &Path, base: &Path, new_base: &Path) -> anyho /// to get bootstrap data for timeline initialization. fn run_initdb( conf: &'static PageServerConf, - initdb_target_dir: &Path, + initdb_target_dir: &Utf8Path, pg_version: u32, ) -> anyhow::Result<()> { let initdb_bin_path = conf.pg_bin_dir(pg_version)?.join("initdb"); let initdb_lib_dir = conf.pg_lib_dir(pg_version)?; info!( "running {} in {}, libdir: {}", - initdb_bin_path.display(), - initdb_target_dir.display(), - initdb_lib_dir.display(), + initdb_bin_path, initdb_target_dir, initdb_lib_dir, ); let initdb_output = Command::new(&initdb_bin_path) - .args(["-D", &initdb_target_dir.to_string_lossy()]) + .args(["-D", initdb_target_dir.as_ref()]) .args(["-U", &conf.superuser]) .args(["-E", "utf8"]) .arg("--no-instructions") @@ -3290,8 +3378,7 @@ fn run_initdb( .with_context(|| { format!( "failed to execute {} at target dir {}", - initdb_bin_path.display(), - initdb_target_dir.display() + initdb_bin_path, initdb_target_dir, ) })?; if !initdb_output.status.success() { @@ -3311,7 +3398,7 @@ impl Drop for Tenant { } /// Dump contents of a layer file to stdout. pub async fn dump_layerfile_from_path( - path: &Path, + path: &Utf8Path, verbose: bool, ctx: &RequestContext, ) -> anyhow::Result<()> { @@ -3344,8 +3431,8 @@ pub async fn dump_layerfile_from_path( pub mod harness { use bytes::{Bytes, BytesMut}; use once_cell::sync::OnceCell; + use std::fs; use std::sync::Arc; - use std::{fs, path::PathBuf}; use utils::logging; use utils::lsn::Lsn; @@ -3410,7 +3497,7 @@ pub mod harness { pub tenant_id: TenantId, pub generation: Generation, pub remote_storage: GenericRemoteStorage, - pub remote_fs_dir: PathBuf, + pub remote_fs_dir: Utf8PathBuf, pub deletion_queue: MockDeletionQueue, } @@ -3489,10 +3576,13 @@ pub mod harness { let tenant = Arc::new(Tenant::new( TenantState::Loading, self.conf, - TenantConfOpt::from(self.tenant_conf), + AttachedTenantConf::try_from(LocationConf::attached_single( + TenantConfOpt::from(self.tenant_conf), + self.generation, + )) + .unwrap(), walredo_mgr, self.tenant_id, - self.generation, Some(self.remote_storage.clone()), self.deletion_queue.new_client(), )); @@ -3509,7 +3599,7 @@ pub mod harness { Ok(tenant) } - pub fn timeline_path(&self, timeline_id: &TimelineId) -> PathBuf { + pub fn timeline_path(&self, timeline_id: &TimelineId) -> Utf8PathBuf { self.conf.timeline_path(&self.tenant_id, timeline_id) } } diff --git a/pageserver/src/tenant/blob_io.rs b/pageserver/src/tenant/blob_io.rs index 21327deb70..bedf09a40c 100644 --- a/pageserver/src/tenant/blob_io.rs +++ b/pageserver/src/tenant/blob_io.rs @@ -238,14 +238,14 @@ mod tests { use rand::{Rng, SeedableRng}; async fn round_trip_test(blobs: &[Vec]) -> Result<(), Error> { - let temp_dir = tempfile::tempdir()?; - let path = temp_dir.path().join("file"); + let temp_dir = camino_tempfile::tempdir()?; + let pathbuf = temp_dir.path().join("file"); let ctx = RequestContext::new(TaskKind::UnitTest, DownloadBehavior::Error); // Write part (in block to drop the file) let mut offsets = Vec::new(); { - let file = VirtualFile::create(&path).await?; + let file = VirtualFile::create(pathbuf.as_path()).await?; let mut wtr = BlobWriter::::new(file, 0); for blob in blobs.iter() { let offs = wtr.write_blob(blob).await?; @@ -258,7 +258,7 @@ mod tests { wtr.flush_buffer().await?; } - let file = VirtualFile::open(&path).await?; + let file = VirtualFile::open(pathbuf.as_path()).await?; let rdr = BlockReaderRef::VirtualFile(&file); let rdr = BlockCursor::new(rdr); for (idx, (blob, offset)) in blobs.iter().zip(offsets.iter()).enumerate() { diff --git a/pageserver/src/tenant/block_io.rs b/pageserver/src/tenant/block_io.rs index d81cf1b8a0..0617017528 100644 --- a/pageserver/src/tenant/block_io.rs +++ b/pageserver/src/tenant/block_io.rs @@ -186,26 +186,21 @@ impl FileBlockReader { ctx: &RequestContext, ) -> Result { let cache = page_cache::get(); - loop { - match cache - .read_immutable_buf(self.file_id, blknum, ctx) - .await - .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - format!("Failed to read immutable buf: {e:#}"), - ) - })? { - ReadBufResult::Found(guard) => break Ok(guard.into()), - ReadBufResult::NotFound(mut write_guard) => { - // Read the page from disk into the buffer - self.fill_buffer(write_guard.deref_mut(), blknum).await?; - write_guard.mark_valid(); - - // Swap for read lock - continue; - } - }; + match cache + .read_immutable_buf(self.file_id, blknum, ctx) + .await + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Failed to read immutable buf: {e:#}"), + ) + })? { + ReadBufResult::Found(guard) => Ok(guard.into()), + ReadBufResult::NotFound(mut write_guard) => { + // Read the page from disk into the buffer + self.fill_buffer(write_guard.deref_mut(), blknum).await?; + Ok(write_guard.mark_valid().into()) + } } } } diff --git a/pageserver/src/tenant/config.rs b/pageserver/src/tenant/config.rs index ffe2c5eab6..5f8c7f6c59 100644 --- a/pageserver/src/tenant/config.rs +++ b/pageserver/src/tenant/config.rs @@ -13,6 +13,7 @@ use pageserver_api::models; use serde::{Deserialize, Serialize}; use std::num::NonZeroU64; use std::time::Duration; +use utils::generation::Generation; pub mod defaults { // FIXME: This current value is very low. I would imagine something like 1 GB or 10 GB @@ -44,7 +45,211 @@ pub mod defaults { pub const DEFAULT_EVICTIONS_LOW_RESIDENCE_DURATION_METRIC_THRESHOLD: &str = "24 hour"; } -/// Per-tenant configuration options +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub(crate) enum AttachmentMode { + /// Our generation is current as far as we know, and as far as we know we are the only attached + /// pageserver. This is the "normal" attachment mode. + Single, + /// Our generation number is current as far as we know, but we are advised that another + /// pageserver is still attached, and therefore to avoid executing deletions. This is + /// the attachment mode of a pagesever that is the destination of a migration. + Multi, + /// Our generation number is superseded, or about to be superseded. We are advised + /// to avoid remote storage writes if possible, and to avoid sending billing data. This + /// is the attachment mode of a pageserver that is the origin of a migration. + Stale, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub(crate) struct AttachedLocationConfig { + pub(crate) generation: Generation, + pub(crate) attach_mode: AttachmentMode, + // TODO: add a flag to override AttachmentMode's policies under + // disk pressure (i.e. unblock uploads under disk pressure in Stale + // state, unblock deletions after timeout in Multi state) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub(crate) struct SecondaryLocationConfig { + /// If true, keep the local cache warm by polling remote storage + pub(crate) warm: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub(crate) enum LocationMode { + Attached(AttachedLocationConfig), + Secondary(SecondaryLocationConfig), +} + +/// Per-tenant, per-pageserver configuration. All pageservers use the same TenantConf, +/// but have distinct LocationConf. +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct LocationConf { + /// The location-specific part of the configuration, describes the operating + /// mode of this pageserver for this tenant. + pub(crate) mode: LocationMode, + /// The pan-cluster tenant configuration, the same on all locations + pub(crate) tenant_conf: TenantConfOpt, +} + +impl std::fmt::Debug for LocationConf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.mode { + LocationMode::Attached(conf) => { + write!( + f, + "Attached {:?}, gen={:?}", + conf.attach_mode, conf.generation + ) + } + LocationMode::Secondary(conf) => { + write!(f, "Secondary, warm={}", conf.warm) + } + } + } +} + +impl AttachedLocationConfig { + /// Consult attachment mode to determine whether we are currently permitted + /// to delete layers. This is only advisory, not required for data safety. + /// See [`AttachmentMode`] for more context. + pub(crate) fn may_delete_layers_hint(&self) -> bool { + // TODO: add an override for disk pressure in AttachedLocationConfig, + // and respect it here. + match &self.attach_mode { + AttachmentMode::Single => true, + AttachmentMode::Multi | AttachmentMode::Stale => { + // In Multi mode we avoid doing deletions because some other + // attached pageserver might get 404 while trying to read + // a layer we delete which is still referenced in their metadata. + // + // In Stale mode, we avoid doing deletions because we expect + // that they would ultimately fail validation in the deletion + // queue due to our stale generation. + false + } + } + } + + /// Whether we are currently hinted that it is worthwhile to upload layers. + /// This is only advisory, not required for data safety. + /// See [`AttachmentMode`] for more context. + pub(crate) fn may_upload_layers_hint(&self) -> bool { + // TODO: add an override for disk pressure in AttachedLocationConfig, + // and respect it here. + match &self.attach_mode { + AttachmentMode::Single | AttachmentMode::Multi => true, + AttachmentMode::Stale => { + // In Stale mode, we avoid doing uploads because we expect that + // our replacement pageserver will already have started its own + // IndexPart that will never reference layers we upload: it is + // wasteful. + false + } + } + } +} + +impl LocationConf { + /// For use when loading from a legacy configuration: presence of a tenant + /// implies it is in AttachmentMode::Single, which used to be the only + /// possible state. This function should eventually be removed. + pub(crate) fn attached_single(tenant_conf: TenantConfOpt, generation: Generation) -> Self { + Self { + mode: LocationMode::Attached(AttachedLocationConfig { + generation, + attach_mode: AttachmentMode::Single, + }), + tenant_conf, + } + } + + /// For use when attaching/re-attaching: update the generation stored in this + /// structure. If we were in a secondary state, promote to attached (posession + /// of a fresh generation implies this). + pub(crate) fn attach_in_generation(&mut self, generation: Generation) { + match &mut self.mode { + LocationMode::Attached(attach_conf) => { + attach_conf.generation = generation; + } + LocationMode::Secondary(_) => { + // We are promoted to attached by the control plane's re-attach response + self.mode = LocationMode::Attached(AttachedLocationConfig { + generation, + attach_mode: AttachmentMode::Single, + }) + } + } + } + + pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result { + let tenant_conf = TenantConfOpt::try_from(&conf.tenant_conf)?; + + fn get_generation(conf: &'_ models::LocationConfig) -> Result { + conf.generation + .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching")) + } + + let mode = match &conf.mode { + models::LocationConfigMode::AttachedMulti => { + LocationMode::Attached(AttachedLocationConfig { + generation: get_generation(conf)?, + attach_mode: AttachmentMode::Multi, + }) + } + models::LocationConfigMode::AttachedSingle => { + LocationMode::Attached(AttachedLocationConfig { + generation: get_generation(conf)?, + attach_mode: AttachmentMode::Single, + }) + } + models::LocationConfigMode::AttachedStale => { + LocationMode::Attached(AttachedLocationConfig { + generation: get_generation(conf)?, + attach_mode: AttachmentMode::Stale, + }) + } + models::LocationConfigMode::Secondary => { + anyhow::ensure!(conf.generation.is_none()); + + let warm = conf + .secondary_conf + .as_ref() + .map(|c| c.warm) + .unwrap_or(false); + LocationMode::Secondary(SecondaryLocationConfig { warm }) + } + models::LocationConfigMode::Detached => { + // Should not have been called: API code should translate this mode + // into a detach rather than trying to decode it as a LocationConf + return Err(anyhow::anyhow!("Cannot decode a Detached configuration")); + } + }; + + Ok(Self { mode, tenant_conf }) + } +} + +impl Default for LocationConf { + // TODO: this should be removed once tenant loading can guarantee that we are never + // loading from a directory without a configuration. + // => tech debt since https://github.com/neondatabase/neon/issues/1555 + fn default() -> Self { + Self { + mode: LocationMode::Attached(AttachedLocationConfig { + generation: Generation::none(), + attach_mode: AttachmentMode::Single, + }), + tenant_conf: TenantConfOpt::default(), + } + } +} + +/// A tenant's calcuated configuration, which is the result of merging a +/// tenant's TenantConfOpt with the global TenantConf from PageServerConf. +/// +/// For storing and transmitting individual tenant's configuration, see +/// TenantConfOpt. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct TenantConf { // Flush out an inmemory layer, if it's holding WAL older than this diff --git a/pageserver/src/tenant/delete.rs b/pageserver/src/tenant/delete.rs index 5c23af8036..d8763b0a64 100644 --- a/pageserver/src/tenant/delete.rs +++ b/pageserver/src/tenant/delete.rs @@ -1,9 +1,7 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; +use std::sync::Arc; use anyhow::Context; +use camino::{Utf8Path, Utf8PathBuf}; use pageserver_api::models::TenantState; use remote_storage::{DownloadError, GenericRemoteStorage, RemotePath}; use tokio::sync::OwnedMutexGuard; @@ -62,7 +60,7 @@ fn remote_tenant_delete_mark_path( .context("Failed to strip workdir prefix") .and_then(RemotePath::new) .context("tenant path")?; - Ok(tenant_remote_path.join(Path::new("deleted"))) + Ok(tenant_remote_path.join(Utf8Path::new("deleted"))) } async fn create_remote_delete_mark( @@ -148,7 +146,7 @@ async fn schedule_ordered_timeline_deletions( Ok(already_running_deletions) } -async fn ensure_timelines_dir_empty(timelines_path: &Path) -> Result<(), DeleteTenantError> { +async fn ensure_timelines_dir_empty(timelines_path: &Utf8Path) -> Result<(), DeleteTenantError> { // Assert timelines dir is empty. if !fs_ext::is_directory_empty(timelines_path).await? { // Display first 10 items in directory @@ -188,20 +186,18 @@ async fn cleanup_remaining_fs_traces( conf: &PageServerConf, tenant_id: &TenantId, ) -> Result<(), DeleteTenantError> { - let rm = |p: PathBuf, is_dir: bool| async move { + let rm = |p: Utf8PathBuf, is_dir: bool| async move { if is_dir { tokio::fs::remove_dir(&p).await } else { tokio::fs::remove_file(&p).await } .or_else(fs_ext::ignore_not_found) - .with_context(|| { - let to_display = p.display(); - format!("failed to delete {to_display}") - }) + .with_context(|| format!("failed to delete {p}")) }; rm(conf.tenant_config_path(tenant_id), false).await?; + rm(conf.tenant_location_config_path(tenant_id), false).await?; fail::fail_point!("tenant-delete-before-remove-timelines-dir", |_| { Err(anyhow::anyhow!( diff --git a/pageserver/src/tenant/ephemeral_file.rs b/pageserver/src/tenant/ephemeral_file.rs index 8785f51c06..5b99a1dd03 100644 --- a/pageserver/src/tenant/ephemeral_file.rs +++ b/pageserver/src/tenant/ephemeral_file.rs @@ -6,11 +6,11 @@ use crate::context::RequestContext; use crate::page_cache::{self, PAGE_SZ}; use crate::tenant::block_io::{BlockCursor, BlockLease, BlockReader}; use crate::virtual_file::VirtualFile; +use camino::Utf8PathBuf; use std::cmp::min; use std::fs::OpenOptions; use std::io::{self, ErrorKind}; use std::ops::DerefMut; -use std::path::PathBuf; use std::sync::atomic::AtomicU64; use tracing::*; use utils::id::{TenantId, TimelineId}; @@ -40,7 +40,9 @@ impl EphemeralFile { let filename = conf .timeline_path(&tenant_id, &timeline_id) - .join(PathBuf::from(format!("ephemeral-{filename_disambiguator}"))); + .join(Utf8PathBuf::from(format!( + "ephemeral-{filename_disambiguator}" + ))); let file = VirtualFile::open_with_options( &filename, @@ -70,38 +72,32 @@ impl EphemeralFile { let flushed_blknums = 0..self.len / PAGE_SZ as u64; if flushed_blknums.contains(&(blknum as u64)) { let cache = page_cache::get(); - loop { - match cache - .read_immutable_buf(self.page_cache_file_id, blknum, ctx) - .await - .map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::Other, - // order path before error because error is anyhow::Error => might have many contexts - format!( - "ephemeral file: read immutable page #{}: {}: {:#}", - blknum, - self.file.path.display(), - e, - ), - ) - })? { - page_cache::ReadBufResult::Found(guard) => { - return Ok(BlockLease::PageReadGuard(guard)) - } - page_cache::ReadBufResult::NotFound(mut write_guard) => { - let buf: &mut [u8] = write_guard.deref_mut(); - debug_assert_eq!(buf.len(), PAGE_SZ); - self.file - .read_exact_at(&mut buf[..], blknum as u64 * PAGE_SZ as u64) - .await?; - write_guard.mark_valid(); - - // Swap for read lock - continue; - } - }; - } + match cache + .read_immutable_buf(self.page_cache_file_id, blknum, ctx) + .await + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + // order path before error because error is anyhow::Error => might have many contexts + format!( + "ephemeral file: read immutable page #{}: {}: {:#}", + blknum, self.file.path, e, + ), + ) + })? { + page_cache::ReadBufResult::Found(guard) => { + return Ok(BlockLease::PageReadGuard(guard)) + } + page_cache::ReadBufResult::NotFound(mut write_guard) => { + let buf: &mut [u8] = write_guard.deref_mut(); + debug_assert_eq!(buf.len(), PAGE_SZ); + self.file + .read_exact_at(&mut buf[..], blknum as u64 * PAGE_SZ as u64) + .await?; + let read_guard = write_guard.mark_valid(); + return Ok(BlockLease::PageReadGuard(read_guard)); + } + }; } else { debug_assert_eq!(blknum as u64, self.len / PAGE_SZ as u64); Ok(BlockLease::EphemeralFileMutableTail(&self.mutable_tail)) @@ -171,7 +167,7 @@ impl EphemeralFile { let buf: &mut [u8] = write_guard.deref_mut(); debug_assert_eq!(buf.len(), PAGE_SZ); buf.copy_from_slice(&self.ephemeral_file.mutable_tail); - write_guard.mark_valid(); + let _ = write_guard.mark_valid(); // pre-warm successful } Err(e) => { @@ -195,7 +191,7 @@ impl EphemeralFile { "ephemeral_file: write_blob: write-back full tail blk #{}: {:#}: {}", self.blknum, e, - self.ephemeral_file.file.path.display(), + self.ephemeral_file.file.path, ), )); } @@ -258,8 +254,7 @@ impl Drop for EphemeralFile { // not found files might also be related to https://github.com/neondatabase/neon/issues/2442 error!( "could not remove ephemeral file '{}': {}", - self.file.path.display(), - e + self.file.path, e ); } } diff --git a/pageserver/src/tenant/mgr.rs b/pageserver/src/tenant/mgr.rs index 17bcc9eb5f..a92fbccdea 100644 --- a/pageserver/src/tenant/mgr.rs +++ b/pageserver/src/tenant/mgr.rs @@ -1,10 +1,9 @@ //! This module acts as a switchboard to access different repositories managed by this //! page server. +use camino::{Utf8Path, Utf8PathBuf}; use rand::{distributions::Alphanumeric, Rng}; use std::collections::{hash_map, HashMap}; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::fs; @@ -25,9 +24,11 @@ use crate::control_plane_client::{ }; use crate::deletion_queue::DeletionQueueClient; use crate::task_mgr::{self, TaskKind}; -use crate::tenant::config::TenantConfOpt; +use crate::tenant::config::{LocationConf, LocationMode, TenantConfOpt}; use crate::tenant::delete::DeleteTenantFlow; -use crate::tenant::{create_tenant_files, CreateTenantFilesMode, Tenant, TenantState}; +use crate::tenant::{ + create_tenant_files, AttachedTenantConf, CreateTenantFilesMode, Tenant, TenantState, +}; use crate::{InitializationOrder, IGNORED_TENANT_FILE_NAME, TEMP_FILE_SUFFIX}; use utils::crashsafe::path_with_suffix_extension; @@ -39,6 +40,39 @@ use super::delete::DeleteTenantError; use super::timeline::delete::DeleteTimelineFlow; use super::TenantSharedResources; +/// For a tenant that appears in TenantsMap, it may either be +/// - `Attached`: has a full Tenant object, is elegible to service +/// reads and ingest WAL. +/// - `Secondary`: is only keeping a local cache warm. +/// +/// Secondary is a totally distinct state rather than being a mode of a `Tenant`, because +/// that way we avoid having to carefully switch a tenant's ingestion etc on and off during +/// its lifetime, and we can preserve some important safety invariants like `Tenant` always +/// having a properly acquired generation (Secondary doesn't need a generation) +#[derive(Clone)] +pub enum TenantSlot { + Attached(Arc), + Secondary, +} + +impl TenantSlot { + /// Return the `Tenant` in this slot if attached, else None + fn get_attached(&self) -> Option<&Arc> { + match self { + Self::Attached(t) => Some(t), + Self::Secondary => None, + } + } + + /// Consume self and return the `Tenant` that was in this slot if attached, else None + fn into_attached(self) -> Option> { + match self { + Self::Attached(t) => Some(t), + Self::Secondary => None, + } + } +} + /// The tenants known to the pageserver. /// The enum variants are used to distinguish the different states that the pageserver can be in. pub(crate) enum TenantsMap { @@ -46,14 +80,27 @@ pub(crate) enum TenantsMap { Initializing, /// [`init_tenant_mgr`] is done, all on-disk tenants have been loaded. /// New tenants can be added using [`tenant_map_insert`]. - Open(HashMap>), + Open(HashMap), /// The pageserver has entered shutdown mode via [`shutdown_all_tenants`]. /// Existing tenants are still accessible, but no new tenants can be created. - ShuttingDown(HashMap>), + ShuttingDown(HashMap), } impl TenantsMap { + /// Convenience function for typical usage, where we want to get a `Tenant` object, for + /// working with attached tenants. If the TenantId is in the map but in Secondary state, + /// None is returned. pub(crate) fn get(&self, tenant_id: &TenantId) -> Option<&Arc> { + match self { + TenantsMap::Initializing => None, + TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => { + m.get(tenant_id).and_then(TenantSlot::get_attached) + } + } + } + + /// Get the contents of the map at this tenant ID, even if it is in secondary state. + pub(crate) fn get_slot(&self, tenant_id: &TenantId) -> Option<&TenantSlot> { match self { TenantsMap::Initializing => None, TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => m.get(tenant_id), @@ -62,7 +109,9 @@ impl TenantsMap { pub(crate) fn remove(&mut self, tenant_id: &TenantId) -> Option> { match self { TenantsMap::Initializing => None, - TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => m.remove(tenant_id), + TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => { + m.remove(tenant_id).and_then(TenantSlot::into_attached) + } } } } @@ -73,12 +122,12 @@ impl TenantsMap { /// /// This is pageserver-specific, as it relies on future processes after a crash to check /// for TEMP_FILE_SUFFIX when loading things. -async fn safe_remove_tenant_dir_all(path: impl AsRef) -> std::io::Result<()> { +async fn safe_remove_tenant_dir_all(path: impl AsRef) -> std::io::Result<()> { let tmp_path = safe_rename_tenant_dir(path).await?; fs::remove_dir_all(tmp_path).await } -async fn safe_rename_tenant_dir(path: impl AsRef) -> std::io::Result { +async fn safe_rename_tenant_dir(path: impl AsRef) -> std::io::Result { let parent = path .as_ref() .parent() @@ -95,13 +144,155 @@ async fn safe_rename_tenant_dir(path: impl AsRef) -> std::io::Result() + TEMP_FILE_SUFFIX; let tmp_path = path_with_suffix_extension(&path, &rand_suffix); - fs::rename(&path, &tmp_path).await?; + fs::rename(path.as_ref(), &tmp_path).await?; fs::File::open(parent).await?.sync_all().await?; Ok(tmp_path) } static TENANTS: Lazy> = Lazy::new(|| RwLock::new(TenantsMap::Initializing)); +fn emergency_generations( + tenant_confs: &HashMap>, +) -> HashMap { + tenant_confs + .iter() + .filter_map(|(tid, lc)| { + let lc = match lc { + Ok(lc) => lc, + Err(_) => return None, + }; + let gen = match &lc.mode { + LocationMode::Attached(alc) => Some(alc.generation), + LocationMode::Secondary(_) => None, + }; + + gen.map(|g| (*tid, g)) + }) + .collect() +} + +async fn init_load_generations( + conf: &'static PageServerConf, + tenant_confs: &HashMap>, + resources: &TenantSharedResources, + cancel: &CancellationToken, +) -> anyhow::Result>> { + let generations = if conf.control_plane_emergency_mode { + error!( + "Emergency mode! Tenants will be attached unsafely using their last known generation" + ); + emergency_generations(tenant_confs) + } else if let Some(client) = ControlPlaneClient::new(conf, cancel) { + info!("Calling control plane API to re-attach tenants"); + // If we are configured to use the control plane API, then it is the source of truth for what tenants to load. + match client.re_attach().await { + Ok(tenants) => tenants, + Err(RetryForeverError::ShuttingDown) => { + anyhow::bail!("Shut down while waiting for control plane re-attach response") + } + } + } else { + info!("Control plane API not configured, tenant generations are disabled"); + return Ok(None); + }; + + // The deletion queue needs to know about the startup attachment state to decide which (if any) stored + // deletion list entries may still be valid. We provide that by pushing a recovery operation into + // the queue. Sequential processing of te queue ensures that recovery is done before any new tenant deletions + // are processed, even though we don't block on recovery completing here. + // + // Must only do this if remote storage is enabled, otherwise deletion queue + // is not running and channel push will fail. + if resources.remote_storage.is_some() { + resources + .deletion_queue_client + .recover(generations.clone()) + .await?; + } + + Ok(Some(generations)) +} + +/// Initial stage of load: walk the local tenants directory, clean up any temp files, +/// and load configurations for the tenants we found. +async fn init_load_tenant_configs( + conf: &'static PageServerConf, +) -> anyhow::Result>> { + let tenants_dir = conf.tenants_path(); + + let mut dir_entries = tenants_dir + .read_dir_utf8() + .with_context(|| format!("Failed to list tenants dir {tenants_dir:?}"))?; + + let mut configs = HashMap::new(); + + loop { + match dir_entries.next() { + None => break, + Some(Ok(dentry)) => { + let tenant_dir_path = dentry.path().to_path_buf(); + if crate::is_temporary(&tenant_dir_path) { + info!("Found temporary tenant directory, removing: {tenant_dir_path}"); + // No need to use safe_remove_tenant_dir_all because this is already + // a temporary path + if let Err(e) = fs::remove_dir_all(&tenant_dir_path).await { + error!( + "Failed to remove temporary directory '{}': {:?}", + tenant_dir_path, e + ); + } + continue; + } + + // This case happens if we: + // * crash during attach before creating the attach marker file + // * crash during tenant delete before removing tenant directory + let is_empty = tenant_dir_path.is_empty_dir().with_context(|| { + format!("Failed to check whether {tenant_dir_path:?} is an empty dir") + })?; + if is_empty { + info!("removing empty tenant directory {tenant_dir_path:?}"); + if let Err(e) = fs::remove_dir(&tenant_dir_path).await { + error!( + "Failed to remove empty tenant directory '{}': {e:#}", + tenant_dir_path + ) + } + continue; + } + + let tenant_ignore_mark_file = tenant_dir_path.join(IGNORED_TENANT_FILE_NAME); + if tenant_ignore_mark_file.exists() { + info!("Found an ignore mark file {tenant_ignore_mark_file:?}, skipping the tenant"); + continue; + } + + let tenant_id = match tenant_dir_path + .file_name() + .unwrap_or_default() + .parse::() + { + Ok(id) => id, + Err(_) => { + warn!( + "Invalid tenant path (garbage in our repo directory?): {tenant_dir_path}", + ); + continue; + } + }; + + configs.insert(tenant_id, Tenant::load_tenant_config(conf, &tenant_id)); + } + Some(Err(e)) => { + // An error listing the top level directory indicates serious problem + // with local filesystem: we will fail to load, and fail to start. + anyhow::bail!(e); + } + } + } + Ok(configs) +} + /// Initialize repositories with locally available timelines. /// Timelines that are only partially available locally (remote storage has more data than this pageserver) /// are scheduled for download and added to the tenant once download is completed. @@ -112,157 +303,96 @@ pub async fn init_tenant_mgr( init_order: InitializationOrder, cancel: CancellationToken, ) -> anyhow::Result<()> { - // Scan local filesystem for attached tenants - let tenants_dir = conf.tenants_path(); - let mut tenants = HashMap::new(); - // If we are configured to use the control plane API, then it is the source of truth for what tenants to load. - let tenant_generations = if let Some(client) = ControlPlaneClient::new(conf, &cancel) { - let result = match client.re_attach().await { - Ok(tenants) => tenants, - Err(RetryForeverError::ShuttingDown) => { - anyhow::bail!("Shut down while waiting for control plane re-attach response") - } - }; - - // The deletion queue needs to know about the startup attachment state to decide which (if any) stored - // deletion list entries may still be valid. We provide that by pushing a recovery operation into - // the queue. Sequential processing of te queue ensures that recovery is done before any new tenant deletions - // are processed, even though we don't block on recovery completing here. - // - // Must only do this if remote storage is enabled, otherwise deletion queue - // is not running and channel push will fail. - if resources.remote_storage.is_some() { - resources - .deletion_queue_client - .recover(result.clone()) - .await?; - } - - Some(result) - } else { - info!("Control plane API not configured, tenant generations are disabled"); - None - }; - - let mut dir_entries = fs::read_dir(&tenants_dir) - .await - .with_context(|| format!("Failed to list tenants dir {tenants_dir:?}"))?; - let ctx = RequestContext::todo_child(TaskKind::Startup, DownloadBehavior::Warn); - loop { - match dir_entries.next_entry().await { - Ok(None) => break, - Ok(Some(dir_entry)) => { - let tenant_dir_path = dir_entry.path(); - if crate::is_temporary(&tenant_dir_path) { - info!( - "Found temporary tenant directory, removing: {}", - tenant_dir_path.display() - ); - // No need to use safe_remove_tenant_dir_all because this is already - // a temporary path - if let Err(e) = fs::remove_dir_all(&tenant_dir_path).await { - error!( - "Failed to remove temporary directory '{}': {:?}", - tenant_dir_path.display(), - e - ); - } - } else { - // This case happens if we: - // * crash during attach before creating the attach marker file - // * crash during tenant delete before removing tenant directory - let is_empty = tenant_dir_path.is_empty_dir().with_context(|| { - format!("Failed to check whether {tenant_dir_path:?} is an empty dir") - })?; - if is_empty { - info!("removing empty tenant directory {tenant_dir_path:?}"); - if let Err(e) = fs::remove_dir(&tenant_dir_path).await { - error!( - "Failed to remove empty tenant directory '{}': {e:#}", - tenant_dir_path.display() - ) - } - continue; - } + // Scan local filesystem for attached tenants + let tenant_configs = init_load_tenant_configs(conf).await?; - let tenant_ignore_mark_file = tenant_dir_path.join(IGNORED_TENANT_FILE_NAME); - if tenant_ignore_mark_file.exists() { - info!("Found an ignore mark file {tenant_ignore_mark_file:?}, skipping the tenant"); - continue; - } + // Determine which tenants are to be attached + let tenant_generations = + init_load_generations(conf, &tenant_configs, &resources, &cancel).await?; - let tenant_id = match tenant_dir_path - .file_name() - .and_then(OsStr::to_str) - .unwrap_or_default() - .parse::() - { - Ok(id) => id, - Err(_) => { - warn!( - "Invalid tenant path (garbage in our repo directory?): {}", - tenant_dir_path.display() - ); - continue; - } - }; + // Construct `Tenant` objects and start them running + for (tenant_id, location_conf) in tenant_configs { + let tenant_dir_path = conf.tenant_path(&tenant_id); - let generation = if let Some(generations) = &tenant_generations { - // We have a generation map: treat it as the authority for whether - // this tenant is really attached. - if let Some(gen) = generations.get(&tenant_id) { - *gen - } else { - info!("Detaching tenant {tenant_id}, control plane omitted it in re-attach response"); - if let Err(e) = safe_remove_tenant_dir_all(&tenant_dir_path).await { - error!( - "Failed to remove detached tenant directory '{}': {:?}", - tenant_dir_path.display(), - e - ); - } - continue; - } - } else { - // Legacy mode: no generation information, any tenant present - // on local disk may activate - info!( - "Starting tenant {} in legacy mode, no generation", - tenant_dir_path.display() - ); - Generation::none() - }; + let mut location_conf = match location_conf { + Ok(l) => l, + Err(e) => { + warn!(%tenant_id, "Marking tenant broken, failed to {e:#}"); - match schedule_local_tenant_processing( + tenants.insert( + tenant_id, + TenantSlot::Attached(Tenant::create_broken_tenant( conf, tenant_id, - &tenant_dir_path, - generation, - resources.clone(), - Some(init_order.clone()), - &TENANTS, - &ctx, - ) { - Ok(tenant) => { - tenants.insert(tenant.tenant_id(), tenant); - } - Err(e) => { - error!("Failed to collect tenant files from dir {tenants_dir:?} for entry {dir_entry:?}, reason: {e:#}"); + format!("{}", e), + )), + ); + continue; + } + }; + + let generation = if let Some(generations) = &tenant_generations { + // We have a generation map: treat it as the authority for whether + // this tenant is really attached. + if let Some(gen) = generations.get(&tenant_id) { + *gen + } else { + match &location_conf.mode { + LocationMode::Secondary(_) => { + // We do not require the control plane's permission for secondary mode + // tenants, because they do no remote writes and hence require no + // generation number + info!(%tenant_id, "Loaded tenant in secondary mode"); + tenants.insert(tenant_id, TenantSlot::Secondary); + } + LocationMode::Attached(_) => { + // TODO: augment re-attach API to enable the control plane to + // instruct us about secondary attachments. That way, instead of throwing + // away local state, we can gracefully fall back to secondary here, if the control + // plane tells us so. + // (https://github.com/neondatabase/neon/issues/5377) + info!(%tenant_id, "Detaching tenant, control plane omitted it in re-attach response"); + if let Err(e) = safe_remove_tenant_dir_all(&tenant_dir_path).await { + error!(%tenant_id, + "Failed to remove detached tenant directory '{tenant_dir_path}': {e:?}", + ); } } - } + }; + + continue; + } + } else { + // Legacy mode: no generation information, any tenant present + // on local disk may activate + info!(%tenant_id, "Starting tenant in legacy mode, no generation",); + Generation::none() + }; + + // Presence of a generation number implies attachment: attach the tenant + // if it wasn't already, and apply the generation number. + location_conf.attach_in_generation(generation); + Tenant::persist_tenant_config(conf, &tenant_id, &location_conf).await?; + + match schedule_local_tenant_processing( + conf, + tenant_id, + &tenant_dir_path, + AttachedTenantConf::try_from(location_conf)?, + resources.clone(), + Some(init_order.clone()), + &TENANTS, + &ctx, + ) { + Ok(tenant) => { + tenants.insert(tenant.tenant_id(), TenantSlot::Attached(tenant)); } Err(e) => { - // On error, print it, but continue with the other tenants. If we error out - // here, the pageserver startup fails altogether, causing outage for *all* - // tenants. That seems worse. - error!( - "Failed to list tenants dir entry in directory {tenants_dir:?}, reason: {e:?}" - ); + error!(%tenant_id, "Failed to start tenant: {e:#}"); } } } @@ -279,8 +409,8 @@ pub async fn init_tenant_mgr( pub(crate) fn schedule_local_tenant_processing( conf: &'static PageServerConf, tenant_id: TenantId, - tenant_path: &Path, - generation: Generation, + tenant_path: &Utf8Path, + location_conf: AttachedTenantConf, resources: TenantSharedResources, init_order: Option, tenants: &'static tokio::sync::RwLock, @@ -317,7 +447,7 @@ pub(crate) fn schedule_local_tenant_processing( "attaching mark file present but no remote storage configured".to_string(), ) } else { - match Tenant::spawn_attach(conf, tenant_id, generation, resources, tenants, ctx) { + match Tenant::spawn_attach(conf, tenant_id, resources, location_conf, tenants, ctx) { Ok(tenant) => tenant, Err(e) => { error!("Failed to spawn_attach tenant {tenant_id}, reason: {e:#}"); @@ -329,7 +459,13 @@ pub(crate) fn schedule_local_tenant_processing( info!("tenant {tenant_id} is assumed to be loadable, starting load operation"); // Start loading the tenant into memory. It will initially be in Loading state. Tenant::spawn_load( - conf, tenant_id, generation, resources, init_order, tenants, ctx, + conf, + tenant_id, + location_conf, + resources, + init_order, + tenants, + ctx, ) }; Ok(tenant) @@ -385,7 +521,16 @@ async fn shutdown_all_tenants0(tenants: &tokio::sync::RwLock) { let res = { let (_guard, shutdown_progress) = completion::channel(); - tenant.shutdown(shutdown_progress, freeze_and_flush).await + match tenant { + TenantSlot::Attached(t) => { + t.shutdown(shutdown_progress, freeze_and_flush).await + } + TenantSlot::Secondary => { + // TODO: once secondary mode downloads are implemented, + // ensure they have all stopped before we reach this point. + Ok(()) + } + } }; if let Err(other_progress) = res { @@ -458,16 +603,19 @@ pub async fn create_tenant( ctx: &RequestContext, ) -> Result, TenantMapInsertError> { tenant_map_insert(tenant_id, || async { + + let location_conf = LocationConf::attached_single(tenant_conf, generation); + // We're holding the tenants lock in write mode while doing local IO. // If this section ever becomes contentious, introduce a new `TenantState::Creating` // and do the work in that state. - let tenant_directory = super::create_tenant_files(conf, tenant_conf, &tenant_id, CreateTenantFilesMode::Create).await?; + let tenant_directory = super::create_tenant_files(conf, &location_conf, &tenant_id, CreateTenantFilesMode::Create).await?; // TODO: tenant directory remains on disk if we bail out from here on. // See https://github.com/neondatabase/neon/issues/4233 let created_tenant = schedule_local_tenant_processing(conf, tenant_id, &tenant_directory, - generation, resources, None, &TENANTS, ctx)?; + AttachedTenantConf::try_from(location_conf)?, resources, None, &TENANTS, ctx)?; // TODO: tenant object & its background loops remain, untracked in tenant map, if we fail here. // See https://github.com/neondatabase/neon/issues/4233 @@ -496,20 +644,137 @@ pub async fn set_new_tenant_config( info!("configuring tenant {tenant_id}"); let tenant = get_tenant(tenant_id, true).await?; - let tenant_config_path = conf.tenant_config_path(&tenant_id); - Tenant::persist_tenant_config(&tenant_id, &tenant_config_path, new_tenant_conf) + // This is a legacy API that only operates on attached tenants: the preferred + // API to use is the location_config/ endpoint, which lets the caller provide + // the full LocationConf. + let location_conf = LocationConf::attached_single(new_tenant_conf, tenant.generation); + + Tenant::persist_tenant_config(conf, &tenant_id, &location_conf) .await .map_err(SetNewTenantConfigError::Persist)?; tenant.set_new_tenant_config(new_tenant_conf); Ok(()) } +#[instrument(skip_all, fields(tenant_id, new_location_config))] +pub(crate) async fn upsert_location( + conf: &'static PageServerConf, + tenant_id: TenantId, + new_location_config: LocationConf, + broker_client: storage_broker::BrokerClientChannel, + remote_storage: Option, + deletion_queue_client: DeletionQueueClient, + ctx: &RequestContext, +) -> Result<(), anyhow::Error> { + info!("configuring tenant location {tenant_id} to state {new_location_config:?}"); + + let mut existing_tenant = match get_tenant(tenant_id, false).await { + Ok(t) => Some(t), + Err(GetTenantError::NotFound(_)) => None, + Err(e) => anyhow::bail!(e), + }; + + // If we need to shut down a Tenant, do that first + let shutdown_tenant = match (&new_location_config.mode, &existing_tenant) { + (LocationMode::Secondary(_), Some(t)) => Some(t), + (LocationMode::Attached(attach_conf), Some(t)) => { + if attach_conf.generation != t.generation { + Some(t) + } else { + None + } + } + _ => None, + }; + + // TODO: currently we risk concurrent operations interfering with the tenant + // while we await shutdown, but we also should not hold the TenantsMap lock + // across the whole operation. Before we start using this function in production, + // a follow-on change will revise how concurrency is handled in TenantsMap. + // (https://github.com/neondatabase/neon/issues/5378) + + if let Some(tenant) = shutdown_tenant { + let (_guard, progress) = utils::completion::channel(); + info!("Shutting down attached tenant"); + match tenant.shutdown(progress, false).await { + Ok(()) => {} + Err(barrier) => { + info!("Shutdown already in progress, waiting for it to complete"); + barrier.wait().await; + } + } + existing_tenant = None; + } + + if let Some(tenant) = existing_tenant { + // Update the existing tenant + Tenant::persist_tenant_config(conf, &tenant_id, &new_location_config) + .await + .map_err(SetNewTenantConfigError::Persist)?; + tenant.set_new_location_config(AttachedTenantConf::try_from(new_location_config)?); + } else { + // Upsert a fresh TenantSlot into TenantsMap. Do it within the map write lock, + // and re-check that the state of anything we are replacing is as expected. + tenant_map_upsert_slot(tenant_id, |old_value| async move { + if let Some(TenantSlot::Attached(t)) = old_value { + if !matches!(t.current_state(), TenantState::Stopping { .. }) { + anyhow::bail!("Tenant state changed during location configuration update"); + } + } + + let new_slot = match &new_location_config.mode { + LocationMode::Secondary(_) => TenantSlot::Secondary, + LocationMode::Attached(_attach_config) => { + // Do a schedule_local_tenant_processing + // FIXME: should avoid doing this disk I/O inside the TenantsMap lock, + // we have the same problem in load_tenant/attach_tenant. Probably + // need a lock in TenantSlot to fix this. + Tenant::persist_tenant_config(conf, &tenant_id, &new_location_config) + .await + .map_err(SetNewTenantConfigError::Persist)?; + let tenant_path = conf.tenant_path(&tenant_id); + let resources = TenantSharedResources { + broker_client, + remote_storage, + deletion_queue_client, + }; + let new_tenant = schedule_local_tenant_processing( + conf, + tenant_id, + &tenant_path, + AttachedTenantConf::try_from(new_location_config)?, + resources, + None, + &TENANTS, + ctx, + ) + .with_context(|| { + format!("Failed to schedule tenant processing in path {tenant_path:?}") + })?; + + TenantSlot::Attached(new_tenant) + } + }; + + Ok(new_slot) + }) + .await?; + } + + Ok(()) +} + #[derive(Debug, thiserror::Error)] pub enum GetTenantError { #[error("Tenant {0} not found")] NotFound(TenantId), #[error("Tenant {0} is not active")] NotActive(TenantId), + /// Broken is logically a subset of NotActive, but a distinct error is useful as + /// NotActive is usually a retryable state for API purposes, whereas Broken + /// is a stuck error state + #[error("Tenant is broken: {0}")] + Broken(String), } /// Gets the tenant from the in-memory data, erroring if it's absent or is not fitting to the query. @@ -524,10 +789,20 @@ pub async fn get_tenant( let tenant = m .get(&tenant_id) .ok_or(GetTenantError::NotFound(tenant_id))?; - if active_only && !tenant.is_active() { - Err(GetTenantError::NotActive(tenant_id)) - } else { - Ok(Arc::clone(tenant)) + + match tenant.current_state() { + TenantState::Broken { + reason, + backtrace: _, + } if active_only => Err(GetTenantError::Broken(reason)), + TenantState::Active => Ok(Arc::clone(tenant)), + _ => { + if active_only { + Err(GetTenantError::NotActive(tenant_id)) + } else { + Ok(Arc::clone(tenant)) + } + } } } @@ -600,7 +875,7 @@ async fn detach_tenant0( tenants: &tokio::sync::RwLock, tenant_id: TenantId, detach_ignored: bool, -) -> Result { +) -> Result { let tenant_dir_rename_operation = |tenant_id_to_clean| async move { let local_tenant_directory = conf.tenant_path(&tenant_id_to_clean); safe_rename_tenant_dir(&local_tenant_directory) @@ -649,7 +924,12 @@ pub async fn load_tenant( remote_storage, deletion_queue_client }; - let new_tenant = schedule_local_tenant_processing(conf, tenant_id, &tenant_path, generation, resources, None, &TENANTS, ctx) + + let mut location_conf = Tenant::load_tenant_config(conf, &tenant_id).map_err( TenantMapInsertError::Other)?; + location_conf.attach_in_generation(generation); + Tenant::persist_tenant_config(conf, &tenant_id, &location_conf).await?; + + let new_tenant = schedule_local_tenant_processing(conf, tenant_id, &tenant_path, AttachedTenantConf::try_from(location_conf)?, resources, None, &TENANTS, ctx) .with_context(|| { format!("Failed to schedule tenant processing in path {tenant_path:?}") })?; @@ -702,7 +982,10 @@ pub async fn list_tenants() -> Result, TenantMapLis TenantsMap::Open(m) | TenantsMap::ShuttingDown(m) => m, }; Ok(m.iter() - .map(|(id, tenant)| (*id, tenant.current_state())) + .filter_map(|(id, tenant)| match tenant { + TenantSlot::Attached(tenant) => Some((*id, tenant.current_state())), + TenantSlot::Secondary => None, + }) .collect()) } @@ -719,7 +1002,8 @@ pub async fn attach_tenant( ctx: &RequestContext, ) -> Result<(), TenantMapInsertError> { tenant_map_insert(tenant_id, || async { - let tenant_dir = create_tenant_files(conf, tenant_conf, &tenant_id, CreateTenantFilesMode::Attach).await?; + let location_conf = LocationConf::attached_single(tenant_conf, generation); + let tenant_dir = create_tenant_files(conf, &location_conf, &tenant_id, CreateTenantFilesMode::Attach).await?; // TODO: tenant directory remains on disk if we bail out from here on. // See https://github.com/neondatabase/neon/issues/4233 @@ -730,8 +1014,7 @@ pub async fn attach_tenant( .context("check for attach marker file existence")?; anyhow::ensure!(marker_file_exists, "create_tenant_files should have created the attach marker file"); - - let attached_tenant = schedule_local_tenant_processing(conf, tenant_id, &tenant_dir, generation, resources, None, &TENANTS, ctx)?; + let attached_tenant = schedule_local_tenant_processing(conf, tenant_id, &tenant_dir, AttachedTenantConf::try_from(location_conf)?, resources, None, &TENANTS, ctx)?; // TODO: tenant object & its background loops remain, untracked in tenant map, if we fail here. // See https://github.com/neondatabase/neon/issues/4233 @@ -754,8 +1037,10 @@ pub enum TenantMapInsertError { ShuttingDown, #[error("tenant {0} already exists, state: {1:?}")] TenantAlreadyExists(TenantId, TenantState), + #[error("tenant {0} already exists in secondary state")] + TenantExistsSecondary(TenantId), #[error(transparent)] - Closure(#[from] anyhow::Error), + Other(#[from] anyhow::Error), } /// Give the given closure access to the tenants map entry for the given `tenant_id`, iff that @@ -779,20 +1064,47 @@ where TenantsMap::Open(m) => m, }; match m.entry(tenant_id) { - hash_map::Entry::Occupied(e) => Err(TenantMapInsertError::TenantAlreadyExists( - tenant_id, - e.get().current_state(), - )), + hash_map::Entry::Occupied(e) => match e.get() { + TenantSlot::Attached(t) => Err(TenantMapInsertError::TenantAlreadyExists( + tenant_id, + t.current_state(), + )), + TenantSlot::Secondary => Err(TenantMapInsertError::TenantExistsSecondary(tenant_id)), + }, hash_map::Entry::Vacant(v) => match insert_fn().await { Ok(tenant) => { - v.insert(tenant.clone()); + v.insert(TenantSlot::Attached(tenant.clone())); Ok(tenant) } - Err(e) => Err(TenantMapInsertError::Closure(e)), + Err(e) => Err(TenantMapInsertError::Other(e)), }, } } +async fn tenant_map_upsert_slot<'a, F, R>( + tenant_id: TenantId, + upsert_fn: F, +) -> Result<(), TenantMapInsertError> +where + F: FnOnce(Option) -> R, + R: std::future::Future>, +{ + let mut guard = TENANTS.write().await; + let m = match &mut *guard { + TenantsMap::Initializing => return Err(TenantMapInsertError::StillInitializing), + TenantsMap::ShuttingDown(_) => return Err(TenantMapInsertError::ShuttingDown), + TenantsMap::Open(m) => m, + }; + + match upsert_fn(m.remove(&tenant_id)).await { + Ok(upsert_val) => { + m.insert(tenant_id, upsert_val); + Ok(()) + } + Err(e) => Err(TenantMapInsertError::Other(e)), + } +} + /// Stops and removes the tenant from memory, if it's not [`TenantState::Stopping`] already, bails otherwise. /// Allows to remove other tenant resources manually, via `tenant_cleanup`. /// If the cleanup fails, tenant will stay in memory in [`TenantState::Broken`] state, and another removal @@ -812,28 +1124,40 @@ where // tenant-wde cleanup operations may take some time (removing the entire tenant directory), we want to // avoid holding the lock for the entire process. let tenant = { - tenants + match tenants .write() .await - .get(&tenant_id) - .cloned() + .get_slot(&tenant_id) .ok_or(TenantStateError::NotFound(tenant_id))? + { + TenantSlot::Attached(t) => Some(t.clone()), + TenantSlot::Secondary => None, + } }; // allow pageserver shutdown to await for our completion let (_guard, progress) = completion::channel(); - // whenever we remove a tenant from memory, we don't want to flush and wait for upload - let freeze_and_flush = false; + // If the tenant was attached, shut it down gracefully. For secondary + // locations this part is not necessary + match tenant { + Some(attached_tenant) => { + // whenever we remove a tenant from memory, we don't want to flush and wait for upload + let freeze_and_flush = false; - // shutdown is sure to transition tenant to stopping, and wait for all tasks to complete, so - // that we can continue safely to cleanup. - match tenant.shutdown(progress, freeze_and_flush).await { - Ok(()) => {} - Err(_other) => { - // if pageserver shutdown or other detach/ignore is already ongoing, we don't want to - // wait for it but return an error right away because these are distinct requests. - return Err(TenantStateError::IsStopping(tenant_id)); + // shutdown is sure to transition tenant to stopping, and wait for all tasks to complete, so + // that we can continue safely to cleanup. + match attached_tenant.shutdown(progress, freeze_and_flush).await { + Ok(()) => {} + Err(_other) => { + // if pageserver shutdown or other detach/ignore is already ongoing, we don't want to + // wait for it but return an error right away because these are distinct requests. + return Err(TenantStateError::IsStopping(tenant_id)); + } + } + } + None => { + // Nothing to wait on when not attached, proceed. } } @@ -924,6 +1248,8 @@ mod tests { use std::sync::Arc; use tracing::{info_span, Instrument}; + use crate::tenant::mgr::TenantSlot; + use super::{super::harness::TenantHarness, TenantsMap}; #[tokio::test(start_paused = true)] @@ -945,7 +1271,7 @@ mod tests { // tenant harness configures the logging and we cannot escape it let _e = info_span!("testing", tenant_id = %id).entered(); - let tenants = HashMap::from([(id, t.clone())]); + let tenants = HashMap::from([(id, TenantSlot::Attached(t.clone()))]); let tenants = Arc::new(tokio::sync::RwLock::new(TenantsMap::Open(tenants))); let (until_cleanup_completed, can_complete_cleanup) = utils::completion::channel(); diff --git a/pageserver/src/tenant/par_fsync.rs b/pageserver/src/tenant/par_fsync.rs index 705b42aff7..3b1526e910 100644 --- a/pageserver/src/tenant/par_fsync.rs +++ b/pageserver/src/tenant/par_fsync.rs @@ -1,16 +1,17 @@ use std::{ io, - path::{Path, PathBuf}, sync::atomic::{AtomicUsize, Ordering}, }; -fn fsync_path(path: &Path) -> io::Result<()> { +use camino::{Utf8Path, Utf8PathBuf}; + +fn fsync_path(path: &Utf8Path) -> io::Result<()> { // TODO use VirtualFile::fsync_all once we fully go async. let file = std::fs::File::open(path)?; file.sync_all() } -fn parallel_worker(paths: &[PathBuf], next_path_idx: &AtomicUsize) -> io::Result<()> { +fn parallel_worker(paths: &[Utf8PathBuf], next_path_idx: &AtomicUsize) -> io::Result<()> { while let Some(path) = paths.get(next_path_idx.fetch_add(1, Ordering::Relaxed)) { fsync_path(path)?; } @@ -18,7 +19,7 @@ fn parallel_worker(paths: &[PathBuf], next_path_idx: &AtomicUsize) -> io::Result Ok(()) } -fn fsync_in_thread_pool(paths: &[PathBuf]) -> io::Result<()> { +fn fsync_in_thread_pool(paths: &[Utf8PathBuf]) -> io::Result<()> { // TODO: remove this function in favor of `par_fsync_async` once we asyncify everything. /// Use at most this number of threads. @@ -47,7 +48,7 @@ fn fsync_in_thread_pool(paths: &[PathBuf]) -> io::Result<()> { } /// Parallel fsync all files. Can be used in non-async context as it is using rayon thread pool. -pub fn par_fsync(paths: &[PathBuf]) -> io::Result<()> { +pub fn par_fsync(paths: &[Utf8PathBuf]) -> io::Result<()> { if paths.len() == 1 { fsync_path(&paths[0])?; return Ok(()); @@ -58,7 +59,7 @@ pub fn par_fsync(paths: &[PathBuf]) -> io::Result<()> { /// Parallel fsync asynchronously. If number of files are less than PARALLEL_PATH_THRESHOLD, fsync is done in the current /// execution thread. Otherwise, we will spawn_blocking and run it in tokio. -pub async fn par_fsync_async(paths: &[PathBuf]) -> io::Result<()> { +pub async fn par_fsync_async(paths: &[Utf8PathBuf]) -> io::Result<()> { const MAX_CONCURRENT_FSYNC: usize = 64; let mut next = paths.iter().peekable(); let mut js = tokio::task::JoinSet::new(); diff --git a/pageserver/src/tenant/remote_timeline_client.rs b/pageserver/src/tenant/remote_timeline_client.rs index ee99151ef2..622f9b1b9e 100644 --- a/pageserver/src/tenant/remote_timeline_client.rs +++ b/pageserver/src/tenant/remote_timeline_client.rs @@ -209,6 +209,7 @@ pub mod index; mod upload; use anyhow::Context; +use camino::Utf8Path; use chrono::{NaiveDateTime, Utc}; // re-export these pub use download::{is_temp_download_file, list_remote_timelines}; @@ -219,7 +220,6 @@ use utils::backoff::{ }; use std::collections::{HashMap, VecDeque}; -use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex}; @@ -901,9 +901,27 @@ impl RemoteTimelineClient { .await .context("list prefixes")?; - let remaining: Vec = remaining + // We will delete the current index_part object last, since it acts as a deletion + // marker via its deleted_at attribute + let latest_index = remaining + .iter() + .filter(|p| { + p.object_name() + .map(|n| n.starts_with(IndexPart::FILE_NAME)) + .unwrap_or(false) + }) + .filter_map(|path| parse_remote_index_path(path.clone()).map(|gen| (path, gen))) + .max_by_key(|i| i.1) + .map(|i| i.0.clone()) + .unwrap_or( + // No generation-suffixed indices, assume we are dealing with + // a legacy index. + remote_index_path(&self.tenant_id, &self.timeline_id, Generation::none()), + ); + + let remaining_layers: Vec = remaining .into_iter() - .filter(|p| p.object_name() != Some(IndexPart::FILE_NAME)) + .filter(|p| p!= &latest_index) .inspect(|path| { if let Some(name) = path.object_name() { info!(%name, "deleting a file not referenced from index_part.json"); @@ -913,9 +931,11 @@ impl RemoteTimelineClient { }) .collect(); - let not_referenced_count = remaining.len(); - if !remaining.is_empty() { - self.deletion_queue_client.push_immediate(remaining).await?; + let not_referenced_count = remaining_layers.len(); + if !remaining_layers.is_empty() { + self.deletion_queue_client + .push_immediate(remaining_layers) + .await?; } fail::fail_point!("timeline-delete-before-index-delete", |_| { @@ -924,11 +944,9 @@ impl RemoteTimelineClient { ))? }); - let index_file_path = timeline_storage_path.join(Path::new(IndexPart::FILE_NAME)); - debug!("enqueuing index part deletion"); self.deletion_queue_client - .push_immediate([index_file_path].to_vec()) + .push_immediate([latest_index].to_vec()) .await?; // Timeline deletion is rare and we have probably emitted a reasonably number of objects: wait @@ -1409,7 +1427,7 @@ pub fn remote_timelines_path(tenant_id: &TenantId) -> RemotePath { } pub fn remote_timeline_path(tenant_id: &TenantId, timeline_id: &TimelineId) -> RemotePath { - remote_timelines_path(tenant_id).join(&PathBuf::from(timeline_id.to_string())) + remote_timelines_path(tenant_id).join(Utf8Path::new(&timeline_id.to_string())) } pub fn remote_layer_path( @@ -1452,14 +1470,7 @@ pub(crate) fn parse_remote_index_path(path: RemotePath) -> Option { } }; - let file_name_str = match file_name.to_str() { - Some(s) => s, - None => { - tracing::warn!("Malformed index key {:?}", path); - return None; - } - }; - match file_name_str.split_once('-') { + match file_name.split_once('-') { Some((_, gen_suffix)) => Generation::parse_suffix(gen_suffix), None => None, } @@ -1471,20 +1482,16 @@ pub(crate) fn parse_remote_index_path(path: RemotePath) -> Option { /// Errors if the path provided does not start from pageserver's workdir. pub fn remote_path( conf: &PageServerConf, - local_path: &Path, + local_path: &Utf8Path, generation: Generation, ) -> anyhow::Result { let stripped = local_path .strip_prefix(&conf.workdir) .context("Failed to strip workdir prefix")?; - let suffixed = format!( - "{0}{1}", - stripped.to_string_lossy(), - generation.get_suffix() - ); + let suffixed = format!("{0}{1}", stripped, generation.get_suffix()); - RemotePath::new(&PathBuf::from(suffixed)).with_context(|| { + RemotePath::new(Utf8Path::new(&suffixed)).with_context(|| { format!( "to resolve remote part of path {:?} for base {:?}", local_path, conf.workdir @@ -1504,7 +1511,7 @@ mod tests { DEFAULT_PG_VERSION, }; - use std::{collections::HashSet, path::Path}; + use std::collections::HashSet; use utils::lsn::Lsn; pub(super) fn dummy_contents(name: &str) -> Vec { @@ -1538,7 +1545,7 @@ mod tests { assert_eq!(avec, bvec); } - fn assert_remote_files(expected: &[&str], remote_path: &Path, generation: Generation) { + fn assert_remote_files(expected: &[&str], remote_path: &Utf8Path, generation: Generation) { let mut expected: Vec = expected .iter() .map(|x| format!("{}{}", x, generation.get_suffix())) @@ -1657,12 +1664,12 @@ mod tests { let timeline_path = harness.timeline_path(&TIMELINE_ID); - println!("workdir: {}", harness.conf.workdir.display()); + println!("workdir: {}", harness.conf.workdir); let remote_timeline_dir = harness .remote_fs_dir .join(timeline_path.strip_prefix(&harness.conf.workdir).unwrap()); - println!("remote_timeline_dir: {}", remote_timeline_dir.display()); + println!("remote_timeline_dir: {remote_timeline_dir}"); let generation = harness.generation; @@ -1909,7 +1916,7 @@ mod tests { let index_path = test_state.harness.remote_fs_dir.join( remote_index_path(&test_state.harness.tenant_id, &TIMELINE_ID, generation).get_path(), ); - eprintln!("Writing {}", index_path.display()); + eprintln!("Writing {index_path}"); std::fs::write(&index_path, index_part_bytes).unwrap(); example_index_part } diff --git a/pageserver/src/tenant/remote_timeline_client/download.rs b/pageserver/src/tenant/remote_timeline_client/download.rs index 5c173c613f..ef8d217be4 100644 --- a/pageserver/src/tenant/remote_timeline_client/download.rs +++ b/pageserver/src/tenant/remote_timeline_client/download.rs @@ -5,10 +5,10 @@ use std::collections::HashSet; use std::future::Future; -use std::path::Path; use std::time::Duration; use anyhow::{anyhow, Context}; +use camino::Utf8Path; use tokio::fs; use tokio::io::AsyncWriteExt; use tokio_util::sync::CancellationToken; @@ -74,12 +74,7 @@ pub async fn download_layer_file<'a>( // TODO: this doesn't use the cached fd for some reason? let mut destination_file = fs::File::create(&temp_file_path) .await - .with_context(|| { - format!( - "create a destination file for layer '{}'", - temp_file_path.display() - ) - }) + .with_context(|| format!("create a destination file for layer '{temp_file_path}'")) .map_err(DownloadError::Other)?; let mut download = storage .download(&remote_path) @@ -121,7 +116,7 @@ pub async fn download_layer_file<'a>( destination_file .flush() .await - .with_context(|| format!("flush source file at {}", temp_file_path.display())) + .with_context(|| format!("flush source file at {temp_file_path}")) .map_err(DownloadError::Other)?; let expected = layer_metadata.file_size(); @@ -135,12 +130,7 @@ pub async fn download_layer_file<'a>( destination_file .sync_all() .await - .with_context(|| { - format!( - "failed to fsync source file at {}", - temp_file_path.display() - ) - }) + .with_context(|| format!("failed to fsync source file at {temp_file_path}")) .map_err(DownloadError::Other)?; drop(destination_file); @@ -152,27 +142,23 @@ pub async fn download_layer_file<'a>( fs::rename(&temp_file_path, &local_path) .await - .with_context(|| format!("rename download layer file to {}", local_path.display(),)) + .with_context(|| format!("rename download layer file to {local_path}")) .map_err(DownloadError::Other)?; crashsafe::fsync_async(&local_path) .await - .with_context(|| format!("fsync layer file {}", local_path.display(),)) + .with_context(|| format!("fsync layer file {local_path}")) .map_err(DownloadError::Other)?; - tracing::debug!("download complete: {}", local_path.display()); + tracing::debug!("download complete: {local_path}"); Ok(bytes_amount) } const TEMP_DOWNLOAD_EXTENSION: &str = "temp_download"; -pub fn is_temp_download_file(path: &Path) -> bool { - let extension = path.extension().map(|pname| { - pname - .to_str() - .expect("paths passed to this function must be valid Rust strings") - }); +pub fn is_temp_download_file(path: &Utf8Path) -> bool { + let extension = path.extension(); match extension { Some(TEMP_DOWNLOAD_EXTENSION) => true, Some(_) => false, diff --git a/pageserver/src/tenant/remote_timeline_client/upload.rs b/pageserver/src/tenant/remote_timeline_client/upload.rs index c442c4f445..90e603deb0 100644 --- a/pageserver/src/tenant/remote_timeline_client/upload.rs +++ b/pageserver/src/tenant/remote_timeline_client/upload.rs @@ -1,8 +1,9 @@ //! Helper functions to upload files to remote storage with a RemoteStorage use anyhow::{bail, Context}; +use camino::Utf8Path; use fail::fail_point; -use std::{io::ErrorKind, path::Path}; +use std::io::ErrorKind; use tokio::fs; use super::Generation; @@ -30,6 +31,7 @@ pub(super) async fn upload_index_part<'a>( fail_point!("before-upload-index", |_| { bail!("failpoint before-upload-index") }); + pausable_failpoint!("before-upload-index-pausable"); let index_part_bytes = serde_json::to_vec(&index_part).context("serialize index part file into bytes")?; @@ -50,7 +52,7 @@ pub(super) async fn upload_index_part<'a>( pub(super) async fn upload_timeline_layer<'a>( conf: &'static PageServerConf, storage: &'a GenericRemoteStorage, - source_path: &'a Path, + source_path: &'a Utf8Path, known_metadata: &'a LayerFileMetadata, generation: Generation, ) -> anyhow::Result<()> { @@ -68,7 +70,7 @@ pub(super) async fn upload_timeline_layer<'a>( // upload. However, a nonexistent file can also be indicative of // something worse, like when a file is scheduled for upload before // it has been written to disk yet. - info!(path = %source_path.display(), "File to upload doesn't exist. Likely the file has been deleted and an upload is not required any more."); + info!(path = %source_path, "File to upload doesn't exist. Likely the file has been deleted and an upload is not required any more."); return Ok(()); } Err(e) => { @@ -93,7 +95,7 @@ pub(super) async fn upload_timeline_layer<'a>( storage .upload(source_file, fs_size, &storage_path, None) .await - .with_context(|| format!("upload layer from local path '{}'", source_path.display()))?; + .with_context(|| format!("upload layer from local path '{source_path}'"))?; Ok(()) } diff --git a/pageserver/src/tenant/storage_layer.rs b/pageserver/src/tenant/storage_layer.rs index a39e041eaf..b3aacb20d2 100644 --- a/pageserver/src/tenant/storage_layer.rs +++ b/pageserver/src/tenant/storage_layer.rs @@ -14,6 +14,7 @@ use crate::task_mgr::TaskKind; use crate::walrecord::NeonWalRecord; use anyhow::Result; use bytes::Bytes; +use camino::Utf8PathBuf; use enum_map::EnumMap; use enumset::EnumSet; use once_cell::sync::Lazy; @@ -22,7 +23,6 @@ use pageserver_api::models::{ HistoricLayerInfo, LayerResidenceEvent, LayerResidenceEventReason, LayerResidenceStatus, }; use std::ops::Range; -use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tracing::warn; @@ -378,7 +378,7 @@ pub trait PersistentLayer: Layer + AsLayerDesc { // Path to the layer file in the local filesystem. // `None` for `RemoteLayer`. - fn local_path(&self) -> Option; + fn local_path(&self) -> Option; /// Permanently remove this layer from disk. fn delete_resident_layer_file(&self) -> Result<()>; @@ -456,7 +456,7 @@ pub mod tests { /// config. In that case, we use the Path variant to hold the full path to the file on /// disk. enum PathOrConf { - Path(PathBuf), + Path(Utf8PathBuf), Conf(&'static PageServerConf), } diff --git a/pageserver/src/tenant/storage_layer/delta_layer.rs b/pageserver/src/tenant/storage_layer/delta_layer.rs index fbc5ecc9c0..55fb491b65 100644 --- a/pageserver/src/tenant/storage_layer/delta_layer.rs +++ b/pageserver/src/tenant/storage_layer/delta_layer.rs @@ -41,6 +41,7 @@ use crate::virtual_file::VirtualFile; use crate::{walrecord, TEMP_FILE_SUFFIX}; use crate::{DELTA_FILE_MAGIC, STORAGE_FORMAT_VERSION}; use anyhow::{bail, ensure, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; use pageserver_api::models::{HistoricLayerInfo, LayerAccessKind}; use rand::{distributions::Alphanumeric, Rng}; use serde::{Deserialize, Serialize}; @@ -48,7 +49,6 @@ use std::fs::{self, File}; use std::io::SeekFrom; use std::ops::Range; use std::os::unix::fs::FileExt; -use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::sync::OnceCell; use tracing::*; @@ -267,7 +267,7 @@ impl PersistentLayer for DeltaLayer { Some(self) } - fn local_path(&self) -> Option { + fn local_path(&self) -> Option { self.local_path() } @@ -374,7 +374,7 @@ impl DeltaLayer { .await } - pub(crate) fn local_path(&self) -> Option { + pub(crate) fn local_path(&self) -> Option { Some(self.path()) } @@ -409,7 +409,7 @@ impl DeltaLayer { tenant_id: &TenantId, timeline_id: &TimelineId, fname: &DeltaFileName, - ) -> PathBuf { + ) -> Utf8PathBuf { match path_or_conf { PathOrConf::Path(path) => path.clone(), PathOrConf::Conf(conf) => conf @@ -424,7 +424,7 @@ impl DeltaLayer { timeline_id: &TimelineId, key_start: Key, lsn_range: &Range, - ) -> PathBuf { + ) -> Utf8PathBuf { let rand_string: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(8) @@ -455,7 +455,7 @@ impl DeltaLayer { self.inner .get_or_try_init(|| self.load_inner(ctx)) .await - .with_context(|| format!("Failed to load delta layer {}", self.path().display())) + .with_context(|| format!("Failed to load delta layer {}", self.path())) } async fn load_inner(&self, ctx: &RequestContext) -> Result> { @@ -471,7 +471,7 @@ impl DeltaLayer { if let PathOrConf::Path(ref path) = self.path_or_conf { // not production code - let actual_filename = path.file_name().unwrap().to_str().unwrap().to_owned(); + let actual_filename = path.file_name().unwrap().to_owned(); let expected_filename = self.filename().file_name(); if actual_filename != expected_filename { @@ -510,9 +510,8 @@ impl DeltaLayer { /// Create a DeltaLayer struct representing an existing file on disk. /// /// This variant is only used for debugging purposes, by the 'pagectl' binary. - pub fn new_for_path(path: &Path, file: File) -> Result { - let mut summary_buf = Vec::new(); - summary_buf.resize(PAGE_SZ, 0); + pub fn new_for_path(path: &Utf8Path, file: File) -> Result { + let mut summary_buf = vec![0; PAGE_SZ]; file.read_exact_at(&mut summary_buf, 0)?; let summary = Summary::des_prefix(&summary_buf)?; @@ -538,7 +537,7 @@ impl DeltaLayer { self.desc.delta_file_name() } /// Path to the layer file in pageserver workdir. - pub fn path(&self) -> PathBuf { + pub fn path(&self) -> Utf8PathBuf { Self::path_for( &self.path_or_conf, &self.desc.tenant_id, @@ -573,7 +572,7 @@ impl DeltaLayer { /// struct DeltaLayerWriterInner { conf: &'static PageServerConf, - pub path: PathBuf, + pub path: Utf8PathBuf, timeline_id: TimelineId, tenant_id: TenantId, @@ -711,7 +710,7 @@ impl DeltaLayerWriterInner { ensure!( metadata.len() <= S3_UPLOAD_LIMIT, "Created delta layer file at {} of size {} above limit {S3_UPLOAD_LIMIT}!", - file.path.display(), + file.path, metadata.len() ); @@ -748,7 +747,7 @@ impl DeltaLayerWriterInner { ); std::fs::rename(self.path, &final_path)?; - trace!("created delta layer {}", final_path.display()); + trace!("created delta layer {final_path}"); Ok(layer) } @@ -847,13 +846,13 @@ impl Drop for DeltaLayerWriter { impl DeltaLayerInner { pub(super) async fn load( - path: &std::path::Path, + path: &Utf8Path, summary: Option

, ctx: &RequestContext, ) -> anyhow::Result { let file = VirtualFile::open(path) .await - .with_context(|| format!("Failed to open file '{}'", path.display()))?; + .with_context(|| format!("Failed to open file '{path}'"))?; let file = FileBlockReader::new(file); let summary_blk = file.read_blk(0, ctx).await?; @@ -933,15 +932,12 @@ impl DeltaLayerInner { .read_blob_into_buf(pos, &mut buf, ctx) .await .with_context(|| { - format!( - "Failed to read blob from virtual file {}", - file.file.path.display() - ) + format!("Failed to read blob from virtual file {}", file.file.path) })?; let val = Value::des(&buf).with_context(|| { format!( "Failed to deserialize file blob from virtual file {}", - file.file.path.display() + file.file.path ) })?; match val { diff --git a/pageserver/src/tenant/storage_layer/image_layer.rs b/pageserver/src/tenant/storage_layer/image_layer.rs index a5470a9f9d..94138a0786 100644 --- a/pageserver/src/tenant/storage_layer/image_layer.rs +++ b/pageserver/src/tenant/storage_layer/image_layer.rs @@ -37,6 +37,7 @@ use crate::virtual_file::VirtualFile; use crate::{IMAGE_FILE_MAGIC, STORAGE_FORMAT_VERSION, TEMP_FILE_SUFFIX}; use anyhow::{bail, ensure, Context, Result}; use bytes::Bytes; +use camino::{Utf8Path, Utf8PathBuf}; use hex; use pageserver_api::models::{HistoricLayerInfo, LayerAccessKind}; use rand::{distributions::Alphanumeric, Rng}; @@ -45,7 +46,6 @@ use std::fs::{self, File}; use std::io::SeekFrom; use std::ops::Range; use std::os::unix::prelude::FileExt; -use std::path::{Path, PathBuf}; use tokio::sync::OnceCell; use tracing::*; @@ -195,7 +195,7 @@ impl AsLayerDesc for ImageLayer { } impl PersistentLayer for ImageLayer { - fn local_path(&self) -> Option { + fn local_path(&self) -> Option { self.local_path() } @@ -269,10 +269,10 @@ impl ImageLayer { .get_value_reconstruct_data(key, reconstruct_state, ctx) .await // FIXME: makes no sense to dump paths - .with_context(|| format!("read {}", self.path().display())) + .with_context(|| format!("read {}", self.path())) } - pub(crate) fn local_path(&self) -> Option { + pub(crate) fn local_path(&self) -> Option { Some(self.path()) } @@ -304,7 +304,7 @@ impl ImageLayer { timeline_id: TimelineId, tenant_id: TenantId, fname: &ImageFileName, - ) -> PathBuf { + ) -> Utf8PathBuf { match path_or_conf { PathOrConf::Path(path) => path.to_path_buf(), PathOrConf::Conf(conf) => conf @@ -318,7 +318,7 @@ impl ImageLayer { timeline_id: TimelineId, tenant_id: TenantId, fname: &ImageFileName, - ) -> PathBuf { + ) -> Utf8PathBuf { let rand_string: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(8) @@ -342,7 +342,7 @@ impl ImageLayer { self.inner .get_or_try_init(|| self.load_inner(ctx)) .await - .with_context(|| format!("Failed to load image layer {}", self.path().display())) + .with_context(|| format!("Failed to load image layer {}", self.path())) } async fn load_inner(&self, ctx: &RequestContext) -> Result { @@ -359,7 +359,7 @@ impl ImageLayer { if let PathOrConf::Path(ref path) = self.path_or_conf { // not production code - let actual_filename = path.file_name().unwrap().to_str().unwrap().to_owned(); + let actual_filename = path.file_name().unwrap().to_owned(); let expected_filename = self.filename().file_name(); if actual_filename != expected_filename { @@ -399,9 +399,8 @@ impl ImageLayer { /// Create an ImageLayer struct representing an existing file on disk. /// /// This variant is only used for debugging purposes, by the 'pagectl' binary. - pub fn new_for_path(path: &Path, file: File) -> Result { - let mut summary_buf = Vec::new(); - summary_buf.resize(PAGE_SZ, 0); + pub fn new_for_path(path: &Utf8Path, file: File) -> Result { + let mut summary_buf = vec![0; PAGE_SZ]; file.read_exact_at(&mut summary_buf, 0)?; let summary = Summary::des_prefix(&summary_buf)?; let metadata = file @@ -427,7 +426,7 @@ impl ImageLayer { } /// Path to the layer file in pageserver workdir. - pub fn path(&self) -> PathBuf { + pub fn path(&self) -> Utf8PathBuf { Self::path_for( &self.path_or_conf, self.desc.timeline_id, @@ -439,14 +438,14 @@ impl ImageLayer { impl ImageLayerInner { pub(super) async fn load( - path: &std::path::Path, + path: &Utf8Path, lsn: Lsn, summary: Option, ctx: &RequestContext, ) -> anyhow::Result { let file = VirtualFile::open(path) .await - .with_context(|| format!("Failed to open file '{}'", path.display()))?; + .with_context(|| format!("Failed to open file '{}'", path))?; let file = FileBlockReader::new(file); let summary_blk = file.read_blk(0, ctx).await?; let actual_summary = Summary::des_prefix(summary_blk.as_ref())?; @@ -526,7 +525,7 @@ impl ImageLayerInner { /// struct ImageLayerWriterInner { conf: &'static PageServerConf, - path: PathBuf, + path: Utf8PathBuf, timeline_id: TimelineId, tenant_id: TenantId, key_range: Range, @@ -558,7 +557,7 @@ impl ImageLayerWriterInner { lsn, }, ); - info!("new image layer {}", path.display()); + info!("new image layer {path}"); let mut file = VirtualFile::open_with_options( &path, std::fs::OpenOptions::new().write(true).create_new(true), @@ -685,7 +684,7 @@ impl ImageLayerWriterInner { ); std::fs::rename(self.path, final_path)?; - trace!("created image layer {}", layer.path().display()); + trace!("created image layer {}", layer.path()); Ok(layer) } diff --git a/pageserver/src/tenant/storage_layer/remote_layer.rs b/pageserver/src/tenant/storage_layer/remote_layer.rs index 3968c16c31..cafe5f6bb6 100644 --- a/pageserver/src/tenant/storage_layer/remote_layer.rs +++ b/pageserver/src/tenant/storage_layer/remote_layer.rs @@ -8,9 +8,9 @@ use crate::tenant::remote_timeline_client::index::LayerFileMetadata; use crate::tenant::storage_layer::{Layer, ValueReconstructResult, ValueReconstructState}; use crate::tenant::timeline::layer_manager::LayerManager; use anyhow::{bail, Result}; +use camino::Utf8PathBuf; use pageserver_api::models::HistoricLayerInfo; use std::ops::Range; -use std::path::PathBuf; use std::sync::Arc; use utils::{ @@ -92,7 +92,7 @@ impl AsLayerDesc for RemoteLayer { } impl PersistentLayer for RemoteLayer { - fn local_path(&self) -> Option { + fn local_path(&self) -> Option { None } diff --git a/pageserver/src/tenant/timeline.rs b/pageserver/src/tenant/timeline.rs index 9b62ba1c50..4dddb7b2fd 100644 --- a/pageserver/src/tenant/timeline.rs +++ b/pageserver/src/tenant/timeline.rs @@ -9,6 +9,7 @@ mod walreceiver; use anyhow::{anyhow, bail, ensure, Context, Result}; use bytes::Bytes; +use camino::{Utf8Path, Utf8PathBuf}; use fail::fail_point; use futures::StreamExt; use itertools::Itertools; @@ -29,7 +30,6 @@ use utils::id::TenantTimelineId; use std::cmp::{max, min, Ordering}; use std::collections::{BinaryHeap, HashMap, HashSet}; use std::ops::{Deref, Range}; -use std::path::{Path, PathBuf}; use std::pin::pin; use std::sync::atomic::Ordering as AtomicOrdering; use std::sync::{Arc, Mutex, RwLock, Weak}; @@ -56,7 +56,7 @@ use crate::config::PageServerConf; use crate::keyspace::{KeyPartitioning, KeySpace, KeySpaceRandomAccum}; use crate::metrics::{ TimelineMetrics, MATERIALIZED_PAGE_CACHE_HIT, MATERIALIZED_PAGE_CACHE_HIT_DIRECT, - RECONSTRUCT_TIME, UNEXPECTED_ONDEMAND_DOWNLOADS, + UNEXPECTED_ONDEMAND_DOWNLOADS, }; use crate::pgdatadir_mapping::LsnForTimestamp; use crate::pgdatadir_mapping::{is_rel_fsm_block_key, is_rel_vm_block_key}; @@ -91,12 +91,12 @@ use self::logical_size::LogicalSize; use self::walreceiver::{WalReceiver, WalReceiverConf}; use super::config::TenantConf; -use super::debug_assert_current_span_has_tenant_and_timeline_id; use super::remote_timeline_client::index::IndexPart; use super::remote_timeline_client::RemoteTimelineClient; use super::storage_layer::{ AsLayerDesc, DeltaLayer, ImageLayer, LayerAccessStatsReset, PersistentLayerDesc, }; +use super::{debug_assert_current_span_has_tenant_and_timeline_id, AttachedTenantConf}; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(super) enum FlushLoopState { @@ -149,7 +149,7 @@ pub struct TimelineResources { pub struct Timeline { conf: &'static PageServerConf, - tenant_conf: Arc>, + tenant_conf: Arc>, myself: Weak, @@ -158,6 +158,9 @@ pub struct Timeline { /// The generation of the tenant that instantiated us: this is used for safety when writing remote objects. /// Never changes for the lifetime of this [`Timeline`] object. + /// + /// This duplicates the generation stored in LocationConf, but that structure is mutable: + /// this copy enforces the invariant that generatio doesn't change during a Tenant's lifetime. generation: Generation, pub pg_version: u32, @@ -496,13 +499,39 @@ impl Timeline { }; let timer = crate::metrics::GET_RECONSTRUCT_DATA_TIME.start_timer(); - self.get_reconstruct_data(key, lsn, &mut reconstruct_state, ctx) + let path = self + .get_reconstruct_data(key, lsn, &mut reconstruct_state, ctx) .await?; timer.stop_and_record(); - RECONSTRUCT_TIME - .observe_closure_duration(|| self.reconstruct_value(key, lsn, reconstruct_state)) - .await + let start = Instant::now(); + let res = self.reconstruct_value(key, lsn, reconstruct_state).await; + let elapsed = start.elapsed(); + crate::metrics::RECONSTRUCT_TIME + .for_result(&res) + .observe(elapsed.as_secs_f64()); + + if cfg!(feature = "testing") && res.is_err() { + // it can only be walredo issue + use std::fmt::Write; + + let mut msg = String::new(); + + path.into_iter().for_each(|(res, cont_lsn, layer)| { + writeln!( + msg, + "- layer traversal: result {res:?}, cont_lsn {cont_lsn}, layer: {}", + layer(), + ) + .expect("string grows") + }); + + // this is to rule out or provide evidence that we could in some cases read a duplicate + // walrecord + tracing::info!("walredo failed, path:\n{msg}"); + } + + res } /// Get last or prev record separately. Same as get_last_record_rlsn().last/prev. @@ -1352,42 +1381,42 @@ const REPARTITION_FREQ_IN_CHECKPOINT_DISTANCE: u64 = 10; // Private functions impl Timeline { fn get_checkpoint_distance(&self) -> u64 { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .checkpoint_distance .unwrap_or(self.conf.default_tenant_conf.checkpoint_distance) } fn get_checkpoint_timeout(&self) -> Duration { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .checkpoint_timeout .unwrap_or(self.conf.default_tenant_conf.checkpoint_timeout) } fn get_compaction_target_size(&self) -> u64 { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .compaction_target_size .unwrap_or(self.conf.default_tenant_conf.compaction_target_size) } fn get_compaction_threshold(&self) -> usize { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .compaction_threshold .unwrap_or(self.conf.default_tenant_conf.compaction_threshold) } fn get_image_creation_threshold(&self) -> usize { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .image_creation_threshold .unwrap_or(self.conf.default_tenant_conf.image_creation_threshold) } fn get_eviction_policy(&self) -> EvictionPolicy { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .eviction_policy .unwrap_or(self.conf.default_tenant_conf.eviction_policy) @@ -1403,7 +1432,7 @@ impl Timeline { } fn get_gc_feedback(&self) -> bool { - let tenant_conf = self.tenant_conf.read().unwrap(); + let tenant_conf = &self.tenant_conf.read().unwrap().tenant_conf; tenant_conf .gc_feedback .unwrap_or(self.conf.default_tenant_conf.gc_feedback) @@ -1416,7 +1445,7 @@ impl Timeline { // The threshold is embedded in the metric. So, we need to update it. { let new_threshold = Self::get_evictions_low_residence_duration_metric_threshold( - &self.tenant_conf.read().unwrap(), + &self.tenant_conf.read().unwrap().tenant_conf, &self.conf.default_tenant_conf, ); let tenant_id_str = self.tenant_id.to_string(); @@ -1435,7 +1464,7 @@ impl Timeline { #[allow(clippy::too_many_arguments)] pub(super) fn new( conf: &'static PageServerConf, - tenant_conf: Arc>, + tenant_conf: Arc>, metadata: &TimelineMetadata, ancestor: Option>, timeline_id: TimelineId, @@ -1458,7 +1487,7 @@ impl Timeline { let evictions_low_residence_duration_metric_threshold = Self::get_evictions_low_residence_duration_metric_threshold( - &tenant_conf_guard, + &tenant_conf_guard.tenant_conf, &conf.default_tenant_conf, ); drop(tenant_conf_guard); @@ -1623,12 +1652,15 @@ impl Timeline { let tenant_conf_guard = self.tenant_conf.read().unwrap(); let wal_connect_timeout = tenant_conf_guard + .tenant_conf .walreceiver_connect_timeout .unwrap_or(self.conf.default_tenant_conf.walreceiver_connect_timeout); let lagging_wal_timeout = tenant_conf_guard + .tenant_conf .lagging_wal_timeout .unwrap_or(self.conf.default_tenant_conf.lagging_wal_timeout); let max_lsn_wal_lag = tenant_conf_guard + .tenant_conf .max_lsn_wal_lag .unwrap_or(self.conf.default_tenant_conf.max_lsn_wal_lag); drop(tenant_conf_guard); @@ -1710,7 +1742,7 @@ impl Timeline { Discovered::Temporary(name) => (name, "temporary timeline file"), Discovered::TemporaryDownload(name) => (name, "temporary download"), }; - path.push(name); + path.push(Utf8Path::new(&name)); init::cleanup(&path, kind)?; path.pop(); } @@ -2191,10 +2223,10 @@ impl TraversalLayerExt for Arc { let timeline_id = self.layer_desc().timeline_id; match self.local_path() { Some(local_path) => { - debug_assert!(local_path.to_str().unwrap().contains(&format!("{}", timeline_id)), + debug_assert!(local_path.to_string().contains(&format!("{}", timeline_id)), "need timeline ID to uniquely identify the layer when traversal crosses ancestor boundary", ); - format!("{}", local_path.display()) + format!("{local_path}") } None => { format!("remote {}/{self}", timeline_id) @@ -2224,7 +2256,7 @@ impl Timeline { request_lsn: Lsn, reconstruct_state: &mut ValueReconstructState, ctx: &RequestContext, - ) -> Result<(), PageReconstructError> { + ) -> Result, PageReconstructError> { // Start from the current timeline. let mut timeline_owned; let mut timeline = self; @@ -2255,12 +2287,12 @@ impl Timeline { // The function should have updated 'state' //info!("CALLED for {} at {}: {:?} with {} records, cached {}", key, cont_lsn, result, reconstruct_state.records.len(), cached_lsn); match result { - ValueReconstructResult::Complete => return Ok(()), + ValueReconstructResult::Complete => return Ok(traversal_path), ValueReconstructResult::Continue => { // If we reached an earlier cached page image, we're done. if cont_lsn == cached_lsn + 1 { MATERIALIZED_PAGE_CACHE_HIT.inc_by(1); - return Ok(()); + return Ok(traversal_path); } if prev_lsn <= cont_lsn { // Didn't make any progress in last iteration. Error out to avoid @@ -2331,7 +2363,7 @@ impl Timeline { // during branch creation. match ancestor.wait_to_become_active(ctx).await { Ok(()) => {} - Err(state) if state == TimelineState::Stopping => { + Err(TimelineState::Stopping) => { return Err(PageReconstructError::AncestorStopping(ancestor.timeline_id)); } Err(state) => { @@ -3696,6 +3728,11 @@ impl Timeline { }); writer.as_mut().unwrap().put_value(key, lsn, value).await?; + + if !new_layers.is_empty() { + fail_point!("after-timeline-compacted-first-L1"); + } + prev_key = Some(key); } if let Some(writer) = writer { @@ -3717,7 +3754,7 @@ impl Timeline { ); } } - let mut layer_paths: Vec = new_layers.iter().map(|l| l.path()).collect(); + let mut layer_paths: Vec = new_layers.iter().map(|l| l.path()).collect(); // Fsync all the layer files and directory using multiple threads to // minimize latency. @@ -3827,10 +3864,7 @@ impl Timeline { let new_delta_path = l.path(); let metadata = new_delta_path.metadata().with_context(|| { - format!( - "read file metadata for new created layer {}", - new_delta_path.display() - ) + format!("read file metadata for new created layer {new_delta_path}") })?; if let Some(remote_client) = &self.remote_client { @@ -3853,6 +3887,7 @@ impl Timeline { ); let l = l as Arc; if guard.contains(&l) { + tracing::error!(layer=%l, "duplicated L1 layer"); duplicated_layers.insert(l.layer_desc().key()); } else { if LayerMap::is_l0(l.layer_desc()) { @@ -4764,11 +4799,10 @@ fn is_send() { /// Add a suffix to a layer file's name: .{num}.old /// Uses the first available num (starts at 0) -fn rename_to_backup(path: &Path) -> anyhow::Result<()> { +fn rename_to_backup(path: &Utf8Path) -> anyhow::Result<()> { let filename = path .file_name() - .ok_or_else(|| anyhow!("Path {} don't have a file name", path.display()))? - .to_string_lossy(); + .ok_or_else(|| anyhow!("Path {path} don't have a file name"))?; let mut new_path = path.to_owned(); for i in 0u32.. { diff --git a/pageserver/src/tenant/timeline/init.rs b/pageserver/src/tenant/timeline/init.rs index 22976a514d..3902afe89a 100644 --- a/pageserver/src/tenant/timeline/init.rs +++ b/pageserver/src/tenant/timeline/init.rs @@ -12,7 +12,8 @@ use crate::{ METADATA_FILE_NAME, }; use anyhow::Context; -use std::{collections::HashMap, ffi::OsString, path::Path, str::FromStr}; +use camino::Utf8Path; +use std::{collections::HashMap, str::FromStr}; use utils::lsn::Lsn; /// Identified files in the timeline directory. @@ -20,46 +21,43 @@ pub(super) enum Discovered { /// The only one we care about Layer(LayerFileName, u64), /// Old ephmeral files from previous launches, should be removed - Ephemeral(OsString), + Ephemeral(String), /// Old temporary timeline files, unsure what these really are, should be removed - Temporary(OsString), + Temporary(String), /// Temporary on-demand download files, should be removed - TemporaryDownload(OsString), + TemporaryDownload(String), /// "metadata" file we persist locally and include in `index_part.json` Metadata, /// Backup file from previously future layers IgnoredBackup, /// Unrecognized, warn about these - Unknown(OsString), + Unknown(String), } /// Scans the timeline directory for interesting files. -pub(super) fn scan_timeline_dir(path: &Path) -> anyhow::Result> { +pub(super) fn scan_timeline_dir(path: &Utf8Path) -> anyhow::Result> { let mut ret = Vec::new(); - for direntry in std::fs::read_dir(path)? { + for direntry in path.read_dir_utf8()? { let direntry = direntry?; - let direntry_path = direntry.path(); - let file_name = direntry.file_name(); + let file_name = direntry.file_name().to_string(); - let fname = file_name.to_string_lossy(); - - let discovered = match LayerFileName::from_str(&fname) { + let discovered = match LayerFileName::from_str(&file_name) { Ok(file_name) => { let file_size = direntry.metadata()?.len(); Discovered::Layer(file_name, file_size) } Err(_) => { - if fname == METADATA_FILE_NAME { + if file_name == METADATA_FILE_NAME { Discovered::Metadata - } else if fname.ends_with(".old") { + } else if file_name.ends_with(".old") { // ignore these Discovered::IgnoredBackup - } else if remote_timeline_client::is_temp_download_file(&direntry_path) { + } else if remote_timeline_client::is_temp_download_file(direntry.path()) { Discovered::TemporaryDownload(file_name) - } else if is_ephemeral_file(&fname) { + } else if is_ephemeral_file(&file_name) { Discovered::Ephemeral(file_name) - } else if is_temporary(&direntry_path) { + } else if is_temporary(direntry.path()) { Discovered::Temporary(file_name) } else { Discovered::Unknown(file_name) @@ -162,15 +160,14 @@ pub(super) fn reconcile( .collect::>() } -pub(super) fn cleanup(path: &Path, kind: &str) -> anyhow::Result<()> { +pub(super) fn cleanup(path: &Utf8Path, kind: &str) -> anyhow::Result<()> { let file_name = path.file_name().expect("must be file path"); tracing::debug!(kind, ?file_name, "cleaning up"); - std::fs::remove_file(path) - .with_context(|| format!("failed to remove {kind} at {}", path.display())) + std::fs::remove_file(path).with_context(|| format!("failed to remove {kind} at {path}")) } pub(super) fn cleanup_local_file_for_remote( - path: &Path, + path: &Utf8Path, local: &LayerFileMetadata, remote: &LayerFileMetadata, ) -> anyhow::Result<()> { @@ -182,8 +179,7 @@ pub(super) fn cleanup_local_file_for_remote( if let Err(err) = crate::tenant::timeline::rename_to_backup(path) { assert!( path.exists(), - "we would leave the local_layer without a file if this does not hold: {}", - path.display() + "we would leave the local_layer without a file if this does not hold: {path}", ); Err(err) } else { @@ -192,7 +188,7 @@ pub(super) fn cleanup_local_file_for_remote( } pub(super) fn cleanup_future_layer( - path: &Path, + path: &Utf8Path, name: &LayerFileName, disk_consistent_lsn: Lsn, ) -> anyhow::Result<()> { diff --git a/pageserver/src/tenant/timeline/uninit.rs b/pageserver/src/tenant/timeline/uninit.rs index 5a15e86458..6b68fdeb84 100644 --- a/pageserver/src/tenant/timeline/uninit.rs +++ b/pageserver/src/tenant/timeline/uninit.rs @@ -1,6 +1,7 @@ -use std::{collections::hash_map::Entry, fs, path::PathBuf, sync::Arc}; +use std::{collections::hash_map::Entry, fs, sync::Arc}; use anyhow::Context; +use camino::Utf8PathBuf; use tracing::{error, info, info_span, warn}; use utils::{crashsafe, fs_ext, id::TimelineId, lsn::Lsn}; @@ -155,12 +156,12 @@ pub(crate) fn cleanup_timeline_directory(uninit_mark: TimelineUninitMark) { #[must_use] pub(crate) struct TimelineUninitMark { uninit_mark_deleted: bool, - uninit_mark_path: PathBuf, - pub(crate) timeline_path: PathBuf, + uninit_mark_path: Utf8PathBuf, + pub(crate) timeline_path: Utf8PathBuf, } impl TimelineUninitMark { - pub(crate) fn new(uninit_mark_path: PathBuf, timeline_path: PathBuf) -> Self { + pub(crate) fn new(uninit_mark_path: Utf8PathBuf, timeline_path: Utf8PathBuf) -> Self { Self { uninit_mark_deleted: false, uninit_mark_path, @@ -197,14 +198,13 @@ impl Drop for TimelineUninitMark { if self.timeline_path.exists() { error!( "Uninit mark {} is not removed, timeline {} stays uninitialized", - self.uninit_mark_path.display(), - self.timeline_path.display() + self.uninit_mark_path, self.timeline_path ) } else { // unblock later timeline creation attempts warn!( "Removing intermediate uninit mark file {}", - self.uninit_mark_path.display() + self.uninit_mark_path ); if let Err(e) = self.delete_mark_file_if_present() { error!("Failed to remove the uninit mark file: {e}") diff --git a/pageserver/src/tenant/upload_queue.rs b/pageserver/src/tenant/upload_queue.rs index 08b1cb8866..8150e71c95 100644 --- a/pageserver/src/tenant/upload_queue.rs +++ b/pageserver/src/tenant/upload_queue.rs @@ -253,7 +253,7 @@ impl std::fmt::Display for UploadOp { write!(f, "UploadMetadata(lsn: {})", lsn) } UploadOp::Delete(delete) => { - write!(f, "Delete({} layers)", delete.layers.len(),) + write!(f, "Delete({} layers)", delete.layers.len()) } UploadOp::Barrier(_) => write!(f, "Barrier"), } diff --git a/pageserver/src/trace.rs b/pageserver/src/trace.rs index 9e466dd9b0..18ec269198 100644 --- a/pageserver/src/trace.rs +++ b/pageserver/src/trace.rs @@ -1,8 +1,8 @@ use bytes::Bytes; +use camino::Utf8PathBuf; use std::{ fs::{create_dir_all, File}, io::{BufWriter, Write}, - path::PathBuf, }; pub struct Tracer { @@ -16,7 +16,7 @@ impl Drop for Tracer { } impl Tracer { - pub fn new(path: PathBuf) -> Self { + pub fn new(path: Utf8PathBuf) -> Self { let parent = path.parent().expect("failed to parse parent path"); create_dir_all(parent).expect("failed to create trace dir"); diff --git a/pageserver/src/virtual_file.rs b/pageserver/src/virtual_file.rs index dfb8d397b4..a2e8f30e15 100644 --- a/pageserver/src/virtual_file.rs +++ b/pageserver/src/virtual_file.rs @@ -12,11 +12,11 @@ //! use crate::metrics::{StorageIoOperation, STORAGE_IO_SIZE, STORAGE_IO_TIME_METRIC}; use crate::tenant::TENANTS_SEGMENT_NAME; +use camino::{Utf8Path, Utf8PathBuf}; use once_cell::sync::OnceCell; use std::fs::{self, File, OpenOptions}; use std::io::{Error, ErrorKind, Seek, SeekFrom}; use std::os::unix::fs::FileExt; -use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{RwLock, RwLockWriteGuard}; @@ -51,7 +51,7 @@ pub struct VirtualFile { /// if a new file is created, we only pass the create flag when it's initially /// opened, in the VirtualFile::create() function, and strip the flag before /// storing it here. - pub path: PathBuf, + pub path: Utf8PathBuf, open_options: OpenOptions, // These are strings becase we only use them for metrics, and those expect strings. @@ -177,19 +177,19 @@ impl OpenFiles { pub enum CrashsafeOverwriteError { #[error("final path has no parent dir")] FinalPathHasNoParentDir, - #[error("remove tempfile: {0}")] + #[error("remove tempfile")] RemovePreviousTempfile(#[source] std::io::Error), - #[error("create tempfile: {0}")] + #[error("create tempfile")] CreateTempfile(#[source] std::io::Error), - #[error("write tempfile: {0}")] + #[error("write tempfile")] WriteContents(#[source] std::io::Error), - #[error("sync tempfile: {0}")] + #[error("sync tempfile")] SyncTempfile(#[source] std::io::Error), - #[error("rename tempfile to final path: {0}")] + #[error("rename tempfile to final path")] RenameTempfileToFinalPath(#[source] std::io::Error), - #[error("open final path parent dir: {0}")] + #[error("open final path parent dir")] OpenFinalPathParentDir(#[source] std::io::Error), - #[error("sync final path parent dir: {0}")] + #[error("sync final path parent dir")] SyncFinalPathParentDir(#[source] std::io::Error), } impl CrashsafeOverwriteError { @@ -210,13 +210,13 @@ impl CrashsafeOverwriteError { impl VirtualFile { /// Open a file in read-only mode. Like File::open. - pub async fn open(path: &Path) -> Result { + pub async fn open(path: &Utf8Path) -> Result { Self::open_with_options(path, OpenOptions::new().read(true)).await } /// Create a new file for writing. If the file exists, it will be truncated. /// Like File::create. - pub async fn create(path: &Path) -> Result { + pub async fn create(path: &Utf8Path) -> Result { Self::open_with_options( path, OpenOptions::new().write(true).create(true).truncate(true), @@ -230,10 +230,10 @@ impl VirtualFile { /// they will be applied also when the file is subsequently re-opened, not only /// on the first time. Make sure that's sane! pub async fn open_with_options( - path: &Path, + path: &Utf8Path, open_options: &OpenOptions, ) -> Result { - let path_str = path.to_string_lossy(); + let path_str = path.to_string(); let parts = path_str.split('/').collect::>(); let tenant_id; let timeline_id; @@ -281,8 +281,8 @@ impl VirtualFile { /// atomic, a crash during the write operation will never leave behind a /// partially written file. pub async fn crashsafe_overwrite( - final_path: &Path, - tmp_path: &Path, + final_path: &Utf8Path, + tmp_path: &Utf8Path, content: &[u8], ) -> Result<(), CrashsafeOverwriteError> { let Some(final_path_parent) = final_path.parent() else { @@ -734,7 +734,7 @@ mod tests { async fn test_files(testname: &str, openfunc: OF) -> Result<(), Error> where - OF: Fn(PathBuf, OpenOptions) -> FT, + OF: Fn(Utf8PathBuf, OpenOptions) -> FT, FT: Future>, { let testdir = crate::config::PageServerConf::test_repo_dir(testname); diff --git a/pageserver/src/walredo.rs b/pageserver/src/walredo.rs index bc250166ce..23433fa694 100644 --- a/pageserver/src/walredo.rs +++ b/pageserver/src/walredo.rs @@ -38,6 +38,9 @@ use tracing::*; use utils::crashsafe::path_with_suffix_extension; use utils::{bin_ser::BeSer, id::TenantId, lsn::Lsn, nonblock::set_nonblock}; +#[cfg(feature = "testing")] +use std::sync::atomic::{AtomicUsize, Ordering}; + use crate::metrics::{ WAL_REDO_BYTES_HISTOGRAM, WAL_REDO_RECORDS_HISTOGRAM, WAL_REDO_RECORD_COUNTER, WAL_REDO_TIME, WAL_REDO_WAIT_TIME, @@ -113,6 +116,9 @@ struct ProcessOutput { pub struct PostgresRedoManager { tenant_id: TenantId, conf: &'static PageServerConf, + /// Counter to separate same sized walredo inputs failing at the same millisecond. + #[cfg(feature = "testing")] + dump_sequence: AtomicUsize, stdout: Mutex>, stdin: Mutex>, @@ -224,6 +230,8 @@ impl PostgresRedoManager { PostgresRedoManager { tenant_id, conf, + #[cfg(feature = "testing")] + dump_sequence: AtomicUsize::default(), stdin: Mutex::new(None), stdout: Mutex::new(None), stderr: Mutex::new(None), @@ -290,25 +298,27 @@ impl PostgresRedoManager { WAL_REDO_BYTES_HISTOGRAM.observe(nbytes as f64); debug!( - "postgres applied {} WAL records ({} bytes) in {} us to reconstruct page image at LSN {}", - len, - nbytes, - duration.as_micros(), - lsn - ); + "postgres applied {} WAL records ({} bytes) in {} us to reconstruct page image at LSN {}", + len, + nbytes, + duration.as_micros(), + lsn + ); // If something went wrong, don't try to reuse the process. Kill it, and // next request will launch a new one. - if result.is_err() { + if let Err(e) = result.as_ref() { error!( - "error applying {} WAL records {}..{} ({} bytes) to base image with LSN {} to reconstruct page image at LSN {}", - records.len(), - records.first().map(|p| p.0).unwrap_or(Lsn(0)), - records.last().map(|p| p.0).unwrap_or(Lsn(0)), - nbytes, - base_img_lsn, - lsn - ); + n_attempts, + "error applying {} WAL records {}..{} ({} bytes) to base image with LSN {} to reconstruct page image at LSN {}: {}", + records.len(), + records.first().map(|p| p.0).unwrap_or(Lsn(0)), + records.last().map(|p| p.0).unwrap_or(Lsn(0)), + nbytes, + base_img_lsn, + lsn, + utils::error::report_compact_sources(e), + ); // self.stdin only holds stdin & stderr as_raw_fd(). // Dropping it as part of take() doesn't close them. // The owning objects (ChildStdout and ChildStderr) are stored in @@ -325,6 +335,8 @@ impl PostgresRedoManager { if let Some(proc) = self.stdin.lock().unwrap().take() { proc.child.kill_and_wait(); } + } else if n_attempts != 0 { + info!(n_attempts, "retried walredo succeeded"); } n_attempts += 1; if n_attempts > MAX_RETRY_ATTEMPTS || result.is_ok() { @@ -742,7 +754,7 @@ impl PostgresRedoManager { #[instrument(skip_all, fields(tenant_id=%self.tenant_id, pid=%input.as_ref().unwrap().child.id()))] fn apply_wal_records( &self, - mut input: MutexGuard>, + input: MutexGuard>, tag: BufferTag, base_img: &Option, records: &[(Lsn, NeonWalRecord)], @@ -779,6 +791,23 @@ impl PostgresRedoManager { build_get_page_msg(tag, &mut writebuf); WAL_REDO_RECORD_COUNTER.inc_by(records.len() as u64); + let res = self.apply_wal_records0(&writebuf, input, wal_redo_timeout); + + if res.is_err() { + // not all of these can be caused by this particular input, however these are so rare + // in tests so capture all. + self.record_and_log(&writebuf); + } + + res + } + + fn apply_wal_records0( + &self, + writebuf: &[u8], + mut input: MutexGuard>, + wal_redo_timeout: Duration, + ) -> Result { let proc = input.as_mut().unwrap(); let mut nwrite = 0usize; let stdout_fd = proc.stdout_fd; @@ -796,7 +825,7 @@ impl PostgresRedoManager { while nwrite < writebuf.len() { let n = loop { match nix::poll::poll(&mut pollfds[0..2], wal_redo_timeout.as_millis() as i32) { - Err(e) if e == nix::errno::Errno::EINTR => continue, + Err(nix::errno::Errno::EINTR) => continue, res => break res, } }?; @@ -888,7 +917,7 @@ impl PostgresRedoManager { // and forward any logging information that the child writes to its stderr to the page server's log. let n = loop { match nix::poll::poll(&mut pollfds[1..3], wal_redo_timeout.as_millis() as i32) { - Err(e) if e == nix::errno::Errno::EINTR => continue, + Err(nix::errno::Errno::EINTR) => continue, res => break res, } }?; @@ -984,6 +1013,38 @@ impl PostgresRedoManager { } Ok(res) } + + #[cfg(feature = "testing")] + fn record_and_log(&self, writebuf: &[u8]) { + let millis = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis(); + + let seq = self.dump_sequence.fetch_add(1, Ordering::Relaxed); + + // these files will be collected to an allure report + let filename = format!("walredo-{millis}-{}-{seq}.walredo", writebuf.len()); + + let path = self.conf.tenant_path(&self.tenant_id).join(&filename); + + let res = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .read(true) + .open(path) + .and_then(|mut f| f.write_all(writebuf)); + + // trip up allowed_errors + if let Err(e) = res { + tracing::error!(target=%filename, length=writebuf.len(), "failed to write out the walredo errored input: {e}"); + } else { + tracing::error!(filename, "erroring walredo input saved"); + } + } + + #[cfg(not(feature = "testing"))] + fn record_and_log(&self, _: &[u8]) {} } /// Wrapper type around `std::process::Child` which guarantees that the child @@ -1217,13 +1278,13 @@ mod tests { struct RedoHarness { // underscored because unused, except for removal at drop - _repo_dir: tempfile::TempDir, + _repo_dir: camino_tempfile::Utf8TempDir, manager: PostgresRedoManager, } impl RedoHarness { fn new() -> anyhow::Result { - let repo_dir = tempfile::tempdir()?; + let repo_dir = camino_tempfile::tempdir()?; let conf = PageServerConf::dummy_conf(repo_dir.path().to_path_buf()); let conf = Box::leak(Box::new(conf)); let tenant_id = TenantId::generate(); diff --git a/pgxn/neon/Makefile b/pgxn/neon/Makefile index 53917d8bc4..e88901ed78 100644 --- a/pgxn/neon/Makefile +++ b/pgxn/neon/Makefile @@ -7,12 +7,12 @@ OBJS = \ extension_server.o \ file_cache.o \ libpagestore.o \ - libpqwalproposer.o \ neon.o \ + neon_utils.o \ pagestore_smgr.o \ relsize_cache.o \ walproposer.o \ - walproposer_utils.o \ + walproposer_pg.o \ control_plane_connector.o PG_CPPFLAGS = -I$(libpq_srcdir) diff --git a/pgxn/neon/libpagestore.c b/pgxn/neon/libpagestore.c index c89de11594..ca24ec7586 100644 --- a/pgxn/neon/libpagestore.c +++ b/pgxn/neon/libpagestore.c @@ -30,7 +30,7 @@ #include "neon.h" #include "walproposer.h" -#include "walproposer_utils.h" +#include "neon_utils.h" #define PageStoreTrace DEBUG5 diff --git a/pgxn/neon/libpqwalproposer.c b/pgxn/neon/libpqwalproposer.c deleted file mode 100644 index ce9a1475d3..0000000000 --- a/pgxn/neon/libpqwalproposer.c +++ /dev/null @@ -1,424 +0,0 @@ -#include "postgres.h" - -#include "libpq-fe.h" -#include "neon.h" -#include "walproposer.h" - -/* Header in walproposer.h -- Wrapper struct to abstract away the libpq connection */ -struct WalProposerConn -{ - PGconn *pg_conn; - bool is_nonblocking; /* whether the connection is non-blocking */ - char *recvbuf; /* last received data from - * walprop_async_read */ -}; - -/* Helper function */ -static bool -ensure_nonblocking_status(WalProposerConn *conn, bool is_nonblocking) -{ - /* If we're already correctly blocking or nonblocking, all good */ - if (is_nonblocking == conn->is_nonblocking) - return true; - - /* Otherwise, set it appropriately */ - if (PQsetnonblocking(conn->pg_conn, is_nonblocking) == -1) - return false; - - conn->is_nonblocking = is_nonblocking; - return true; -} - -/* Exported function definitions */ -char * -walprop_error_message(WalProposerConn *conn) -{ - return PQerrorMessage(conn->pg_conn); -} - -WalProposerConnStatusType -walprop_status(WalProposerConn *conn) -{ - switch (PQstatus(conn->pg_conn)) - { - case CONNECTION_OK: - return WP_CONNECTION_OK; - case CONNECTION_BAD: - return WP_CONNECTION_BAD; - default: - return WP_CONNECTION_IN_PROGRESS; - } -} - -WalProposerConn * -walprop_connect_start(char *conninfo, char *password) -{ - WalProposerConn *conn; - PGconn *pg_conn; - const char *keywords[3]; - const char *values[3]; - int n; - - /* - * Connect using the given connection string. If the - * NEON_AUTH_TOKEN environment variable was set, use that as - * the password. - * - * The connection options are parsed in the order they're given, so - * when we set the password before the connection string, the - * connection string can override the password from the env variable. - * Seems useful, although we don't currently use that capability - * anywhere. - */ - n = 0; - if (password) - { - keywords[n] = "password"; - values[n] = password; - n++; - } - keywords[n] = "dbname"; - values[n] = conninfo; - n++; - keywords[n] = NULL; - values[n] = NULL; - n++; - pg_conn = PQconnectStartParams(keywords, values, 1); - - /* - * Allocation of a PQconn can fail, and will return NULL. We want to fully - * replicate the behavior of PQconnectStart here. - */ - if (!pg_conn) - return NULL; - - /* - * And in theory this allocation can fail as well, but it's incredibly - * unlikely if we just successfully allocated a PGconn. - * - * palloc will exit on failure though, so there's not much we could do if - * it *did* fail. - */ - conn = palloc(sizeof(WalProposerConn)); - conn->pg_conn = pg_conn; - conn->is_nonblocking = false; /* connections always start in blocking - * mode */ - conn->recvbuf = NULL; - return conn; -} - -WalProposerConnectPollStatusType -walprop_connect_poll(WalProposerConn *conn) -{ - WalProposerConnectPollStatusType return_val; - - switch (PQconnectPoll(conn->pg_conn)) - { - case PGRES_POLLING_FAILED: - return_val = WP_CONN_POLLING_FAILED; - break; - case PGRES_POLLING_READING: - return_val = WP_CONN_POLLING_READING; - break; - case PGRES_POLLING_WRITING: - return_val = WP_CONN_POLLING_WRITING; - break; - case PGRES_POLLING_OK: - return_val = WP_CONN_POLLING_OK; - break; - - /* - * There's a comment at its source about this constant being - * unused. We'll expect it's never returned. - */ - case PGRES_POLLING_ACTIVE: - elog(FATAL, "Unexpected PGRES_POLLING_ACTIVE returned from PQconnectPoll"); - - /* - * This return is never actually reached, but it's here to make - * the compiler happy - */ - return WP_CONN_POLLING_FAILED; - - default: - Assert(false); - return_val = WP_CONN_POLLING_FAILED; /* keep the compiler quiet */ - } - - return return_val; -} - -bool -walprop_send_query(WalProposerConn *conn, char *query) -{ - /* - * We need to be in blocking mode for sending the query to run without - * requiring a call to PQflush - */ - if (!ensure_nonblocking_status(conn, false)) - return false; - - /* PQsendQuery returns 1 on success, 0 on failure */ - if (!PQsendQuery(conn->pg_conn, query)) - return false; - - return true; -} - -WalProposerExecStatusType -walprop_get_query_result(WalProposerConn *conn) -{ - PGresult *result; - WalProposerExecStatusType return_val; - - /* Marker variable if we need to log an unexpected success result */ - char *unexpected_success = NULL; - - /* Consume any input that we might be missing */ - if (!PQconsumeInput(conn->pg_conn)) - return WP_EXEC_FAILED; - - if (PQisBusy(conn->pg_conn)) - return WP_EXEC_NEEDS_INPUT; - - - result = PQgetResult(conn->pg_conn); - - /* - * PQgetResult returns NULL only if getting the result was successful & - * there's no more of the result to get. - */ - if (!result) - { - elog(WARNING, "[libpqwalproposer] Unexpected successful end of command results"); - return WP_EXEC_UNEXPECTED_SUCCESS; - } - - /* Helper macro to reduce boilerplate */ -#define UNEXPECTED_SUCCESS(msg) \ - return_val = WP_EXEC_UNEXPECTED_SUCCESS; \ - unexpected_success = msg; \ - break; - - - switch (PQresultStatus(result)) - { - /* "true" success case */ - case PGRES_COPY_BOTH: - return_val = WP_EXEC_SUCCESS_COPYBOTH; - break; - - /* Unexpected success case */ - case PGRES_EMPTY_QUERY: - UNEXPECTED_SUCCESS("empty query return"); - case PGRES_COMMAND_OK: - UNEXPECTED_SUCCESS("data-less command end"); - case PGRES_TUPLES_OK: - UNEXPECTED_SUCCESS("tuples return"); - case PGRES_COPY_OUT: - UNEXPECTED_SUCCESS("'Copy Out' response"); - case PGRES_COPY_IN: - UNEXPECTED_SUCCESS("'Copy In' response"); - case PGRES_SINGLE_TUPLE: - UNEXPECTED_SUCCESS("single tuple return"); - case PGRES_PIPELINE_SYNC: - UNEXPECTED_SUCCESS("pipeline sync point"); - - /* Failure cases */ - case PGRES_BAD_RESPONSE: - case PGRES_NONFATAL_ERROR: - case PGRES_FATAL_ERROR: - case PGRES_PIPELINE_ABORTED: - return_val = WP_EXEC_FAILED; - break; - - default: - Assert(false); - return_val = WP_EXEC_FAILED; /* keep the compiler quiet */ - } - - if (unexpected_success) - elog(WARNING, "[libpqwalproposer] Unexpected successful %s", unexpected_success); - - return return_val; -} - -pgsocket -walprop_socket(WalProposerConn *conn) -{ - return PQsocket(conn->pg_conn); -} - -int -walprop_flush(WalProposerConn *conn) -{ - return (PQflush(conn->pg_conn)); -} - -void -walprop_finish(WalProposerConn *conn) -{ - if (conn->recvbuf != NULL) - PQfreemem(conn->recvbuf); - PQfinish(conn->pg_conn); - pfree(conn); -} - -/* - * Receive a message from the safekeeper. - * - * On success, the data is placed in *buf. It is valid until the next call - * to this function. - */ -PGAsyncReadResult -walprop_async_read(WalProposerConn *conn, char **buf, int *amount) -{ - int result; - - if (conn->recvbuf != NULL) - { - PQfreemem(conn->recvbuf); - conn->recvbuf = NULL; - } - - /* Call PQconsumeInput so that we have the data we need */ - if (!PQconsumeInput(conn->pg_conn)) - { - *amount = 0; - *buf = NULL; - return PG_ASYNC_READ_FAIL; - } - - /* - * The docs for PQgetCopyData list the return values as: 0 if the copy is - * still in progress, but no "complete row" is available -1 if the copy is - * done -2 if an error occurred (> 0) if it was successful; that value is - * the amount transferred. - * - * The protocol we use between walproposer and safekeeper means that we - * *usually* wouldn't expect to see that the copy is done, but this can - * sometimes be triggered by the server returning an ErrorResponse (which - * also happens to have the effect that the copy is done). - */ - switch (result = PQgetCopyData(conn->pg_conn, &conn->recvbuf, true)) - { - case 0: - *amount = 0; - *buf = NULL; - return PG_ASYNC_READ_TRY_AGAIN; - case -1: - { - /* - * If we get -1, it's probably because of a server error; the - * safekeeper won't normally send a CopyDone message. - * - * We can check PQgetResult to make sure that the server - * failed; it'll always result in PGRES_FATAL_ERROR - */ - ExecStatusType status = PQresultStatus(PQgetResult(conn->pg_conn)); - - if (status != PGRES_FATAL_ERROR) - elog(FATAL, "unexpected result status %d after failed PQgetCopyData", status); - - /* - * If there was actually an error, it'll be properly reported - * by calls to PQerrorMessage -- we don't have to do anything - * else - */ - *amount = 0; - *buf = NULL; - return PG_ASYNC_READ_FAIL; - } - case -2: - *amount = 0; - *buf = NULL; - return PG_ASYNC_READ_FAIL; - default: - /* Positive values indicate the size of the returned result */ - *amount = result; - *buf = conn->recvbuf; - return PG_ASYNC_READ_SUCCESS; - } -} - -PGAsyncWriteResult -walprop_async_write(WalProposerConn *conn, void const *buf, size_t size) -{ - int result; - - /* If we aren't in non-blocking mode, switch to it. */ - if (!ensure_nonblocking_status(conn, true)) - return PG_ASYNC_WRITE_FAIL; - - /* - * The docs for PQputcopyData list the return values as: 1 if the data was - * queued, 0 if it was not queued because of full buffers, or -1 if an - * error occurred - */ - result = PQputCopyData(conn->pg_conn, buf, size); - - /* - * We won't get a result of zero because walproposer always empties the - * connection's buffers before sending more - */ - Assert(result != 0); - - switch (result) - { - case 1: - /* good -- continue */ - break; - case -1: - return PG_ASYNC_WRITE_FAIL; - default: - elog(FATAL, "invalid return %d from PQputCopyData", result); - } - - /* - * After queueing the data, we still need to flush to get it to send. This - * might take multiple tries, but we don't want to wait around until it's - * done. - * - * PQflush has the following returns (directly quoting the docs): 0 if - * sucessful, 1 if it was unable to send all the data in the send queue - * yet -1 if it failed for some reason - */ - switch (result = PQflush(conn->pg_conn)) - { - case 0: - return PG_ASYNC_WRITE_SUCCESS; - case 1: - return PG_ASYNC_WRITE_TRY_FLUSH; - case -1: - return PG_ASYNC_WRITE_FAIL; - default: - elog(FATAL, "invalid return %d from PQflush", result); - } -} - -/* - * This function is very similar to walprop_async_write. For more - * information, refer to the comments there. - */ -bool -walprop_blocking_write(WalProposerConn *conn, void const *buf, size_t size) -{ - int result; - - /* If we are in non-blocking mode, switch out of it. */ - if (!ensure_nonblocking_status(conn, false)) - return false; - - if ((result = PQputCopyData(conn->pg_conn, buf, size)) == -1) - return false; - - Assert(result == 1); - - /* Because the connection is non-blocking, flushing returns 0 or -1 */ - - if ((result = PQflush(conn->pg_conn)) == -1) - return false; - - Assert(result == 0); - return true; -} diff --git a/pgxn/neon/neon.h b/pgxn/neon/neon.h index 2610da4311..3300c67456 100644 --- a/pgxn/neon/neon.h +++ b/pgxn/neon/neon.h @@ -18,6 +18,10 @@ extern char *neon_auth_token; extern char *neon_timeline; extern char *neon_tenant; +extern char *wal_acceptors_list; +extern int wal_acceptor_reconnect_timeout; +extern int wal_acceptor_connection_timeout; + extern void pg_init_libpagestore(void); extern void pg_init_walproposer(void); @@ -30,4 +34,10 @@ extern void pg_init_extension_server(void); extern bool neon_redo_read_buffer_filter(XLogReaderState *record, uint8 block_id); extern bool (*old_redo_read_buffer_filter) (XLogReaderState *record, uint8 block_id); +extern uint64 BackpressureThrottlingTime(void); +extern void replication_feedback_get_lsns(XLogRecPtr *writeLsn, XLogRecPtr *flushLsn, XLogRecPtr *applyLsn); + +extern void PGDLLEXPORT WalProposerSync(int argc, char *argv[]); +extern void PGDLLEXPORT WalProposerMain(Datum main_arg); + #endif /* NEON_H */ diff --git a/pgxn/neon/neon_utils.c b/pgxn/neon/neon_utils.c new file mode 100644 index 0000000000..06faea7490 --- /dev/null +++ b/pgxn/neon/neon_utils.c @@ -0,0 +1,116 @@ +#include "postgres.h" + +#include "access/timeline.h" +#include "access/xlogutils.h" +#include "common/logging.h" +#include "common/ip.h" +#include "funcapi.h" +#include "libpq/libpq.h" +#include "libpq/pqformat.h" +#include "miscadmin.h" +#include "postmaster/interrupt.h" +#include "replication/slot.h" +#include "replication/walsender_private.h" + +#include "storage/ipc.h" +#include "utils/builtins.h" +#include "utils/ps_status.h" + +#include "libpq-fe.h" +#include +#include + +#if PG_VERSION_NUM >= 150000 +#include "access/xlogutils.h" +#include "access/xlogrecovery.h" +#endif +#if PG_MAJORVERSION_NUM >= 16 +#include "utils/guc.h" +#endif + +/* + * Convert a character which represents a hexadecimal digit to an integer. + * + * Returns -1 if the character is not a hexadecimal digit. + */ +int +HexDecodeChar(char c) +{ + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + + return -1; +} + +/* + * Decode a hex string into a byte string, 2 hex chars per byte. + * + * Returns false if invalid characters are encountered; otherwise true. + */ +bool +HexDecodeString(uint8 *result, char *input, int nbytes) +{ + int i; + + for (i = 0; i < nbytes; ++i) + { + int n1 = HexDecodeChar(input[i * 2]); + int n2 = HexDecodeChar(input[i * 2 + 1]); + + if (n1 < 0 || n2 < 0) + return false; + result[i] = n1 * 16 + n2; + } + + return true; +} + +/* -------------------------------- + * pq_getmsgint32_le - get a binary 4-byte int from a message buffer in native (LE) order + * -------------------------------- + */ +uint32 +pq_getmsgint32_le(StringInfo msg) +{ + uint32 n32; + + pq_copymsgbytes(msg, (char *) &n32, sizeof(n32)); + + return n32; +} + +/* -------------------------------- + * pq_getmsgint64 - get a binary 8-byte int from a message buffer in native (LE) order + * -------------------------------- + */ +uint64 +pq_getmsgint64_le(StringInfo msg) +{ + uint64 n64; + + pq_copymsgbytes(msg, (char *) &n64, sizeof(n64)); + + return n64; +} + +/* append a binary [u]int32 to a StringInfo buffer in native (LE) order */ +void +pq_sendint32_le(StringInfo buf, uint32 i) +{ + enlargeStringInfo(buf, sizeof(uint32)); + memcpy(buf->data + buf->len, &i, sizeof(uint32)); + buf->len += sizeof(uint32); +} + +/* append a binary [u]int64 to a StringInfo buffer in native (LE) order */ +void +pq_sendint64_le(StringInfo buf, uint64 i) +{ + enlargeStringInfo(buf, sizeof(uint64)); + memcpy(buf->data + buf->len, &i, sizeof(uint64)); + buf->len += sizeof(uint64); +} diff --git a/pgxn/neon/neon_utils.h b/pgxn/neon/neon_utils.h new file mode 100644 index 0000000000..e3fafc8d0f --- /dev/null +++ b/pgxn/neon/neon_utils.h @@ -0,0 +1,12 @@ +#ifndef __NEON_UTILS_H__ +#define __NEON_UTILS_H__ + +#include "postgres.h" + +bool HexDecodeString(uint8 *result, char *input, int nbytes); +uint32 pq_getmsgint32_le(StringInfo msg); +uint64 pq_getmsgint64_le(StringInfo msg); +void pq_sendint32_le(StringInfo buf, uint32 i); +void pq_sendint64_le(StringInfo buf, uint64 i); + +#endif /* __NEON_UTILS_H__ */ diff --git a/pgxn/neon/pagestore_smgr.c b/pgxn/neon/pagestore_smgr.c index 2e4364cbfa..5e172a0be4 100644 --- a/pgxn/neon/pagestore_smgr.c +++ b/pgxn/neon/pagestore_smgr.c @@ -721,7 +721,7 @@ prefetch_register_buffer(BufferTag tag, bool *force_latest, XLogRecPtr *force_ls /* use an intermediate PrefetchRequest struct to ensure correct alignment */ req.buftag = tag; - + Retry: entry = prfh_lookup(MyPState->prf_hash, (PrefetchRequest *) &req); if (entry != NULL) @@ -858,7 +858,11 @@ prefetch_register_buffer(BufferTag tag, bool *force_latest, XLogRecPtr *force_ls if (flush_every_n_requests > 0 && MyPState->ring_unused - MyPState->ring_flush >= flush_every_n_requests) { - page_server->flush(); + if (!page_server->flush()) + { + /* Prefetch set is reset in case of error, so we should try to register our request once again */ + goto Retry; + } MyPState->ring_flush = MyPState->ring_unused; } diff --git a/pgxn/neon/walproposer.c b/pgxn/neon/walproposer.c index a9342bd984..c1fd5e3ef3 100644 --- a/pgxn/neon/walproposer.c +++ b/pgxn/neon/walproposer.c @@ -7,9 +7,9 @@ * * We have two ways of launching WalProposer: * - * 1. As a background worker which will run physical WalSender with - * am_wal_proposer flag set to true. WalSender in turn would handle WAL - * reading part and call WalProposer when ready to scatter WAL. + * 1. As a background worker which will pretend to be physical WalSender. + * WalProposer will receive notifications about new available WAL and + * will immediately broadcast it to alive safekeepers. * * 2. As a standalone utility by running `postgres --sync-safekeepers`. That * is needed to create LSN from which it is safe to start postgres. More @@ -29,107 +29,25 @@ * safekeepers, learn start LSN of future epoch and run basebackup' * won't work. * + * Both ways are implemented in walproposer_pg.c file. This file contains + * generic part of walproposer which can be used in both cases, but can also + * be used as an independent library. + * *------------------------------------------------------------------------- */ #include "postgres.h" - -#include -#include -#include -#include "access/xact.h" -#include "access/xlogdefs.h" -#include "access/xlogutils.h" -#include "access/xloginsert.h" -#if PG_VERSION_NUM >= 150000 -#include "access/xlogrecovery.h" -#endif -#include "storage/fd.h" -#include "storage/latch.h" -#include "miscadmin.h" -#include "pgstat.h" -#include "access/xlog.h" #include "libpq/pqformat.h" -#include "replication/slot.h" -#include "replication/walreceiver.h" -#if PG_VERSION_NUM >= 160000 -#include "replication/walsender_private.h" -#endif -#include "postmaster/bgworker.h" -#include "postmaster/interrupt.h" -#include "postmaster/postmaster.h" -#include "storage/pmsignal.h" -#include "storage/proc.h" -#include "storage/ipc.h" -#include "storage/lwlock.h" -#include "storage/shmem.h" -#include "storage/spin.h" -#include "tcop/tcopprot.h" -#include "utils/builtins.h" -#include "utils/guc.h" -#include "utils/memutils.h" -#include "utils/ps_status.h" -#include "utils/timestamp.h" - #include "neon.h" #include "walproposer.h" -#include "walproposer_utils.h" - -static bool syncSafekeepers = false; - -char *wal_acceptors_list = ""; -int wal_acceptor_reconnect_timeout = 1000; -int wal_acceptor_connection_timeout = 10000; -bool am_wal_proposer = false; - -#define WAL_PROPOSER_SLOT_NAME "wal_proposer_slot" - -static int n_safekeepers = 0; -static int quorum = 0; -static Safekeeper safekeeper[MAX_SAFEKEEPERS]; -static XLogRecPtr availableLsn; /* WAL has been generated up to this point */ -static XLogRecPtr lastSentCommitLsn; /* last commitLsn broadcast to* - * safekeepers */ -static ProposerGreeting greetRequest; -static VoteRequest voteRequest; /* Vote request for safekeeper */ -static WaitEventSet *waitEvents; -static AppendResponse quorumFeedback; -/* - * Minimal LSN which may be needed for recovery of some safekeeper, - * record-aligned (first record which might not yet received by someone). - */ -static XLogRecPtr truncateLsn; - -/* - * Term of the proposer. We want our term to be highest and unique, - * so we collect terms from safekeepers quorum, choose max and +1. - * After that our term is fixed and must not change. If we observe - * that some safekeeper has higher term, it means that we have another - * running compute, so we must stop immediately. - */ -static term_t propTerm; -static TermHistory propTermHistory; /* term history of the proposer */ -static XLogRecPtr propEpochStartLsn; /* epoch start lsn of the proposer */ -static term_t donorEpoch; /* Most advanced acceptor epoch */ -static int donor; /* Most advanced acceptor */ -static XLogRecPtr timelineStartLsn; /* timeline globally starts at this LSN */ -static int n_votes = 0; -static int n_connected = 0; -static TimestampTz last_reconnect_attempt; - -static WalproposerShmemState * walprop_shared; +#include "neon_utils.h" /* Prototypes for private functions */ -static void WalProposerRegister(void); -static void WalProposerInit(XLogRecPtr flushRecPtr, uint64 systemId); -static void WalProposerStart(void); -static void WalProposerLoop(void); -static void InitEventSet(void); -static void UpdateEventSet(Safekeeper *sk, uint32 events); +static void WalProposerLoop(WalProposer *wp); static void HackyRemoveWalProposerEvent(Safekeeper *to_remove); static void ShutdownConnection(Safekeeper *sk); static void ResetConnection(Safekeeper *sk); -static long TimeToReconnect(TimestampTz now); -static void ReconnectSafekeepers(void); +static long TimeToReconnect(WalProposer *wp, TimestampTz now); +static void ReconnectSafekeepers(WalProposer *wp); static void AdvancePollState(Safekeeper *sk, uint32 events); static void HandleConnectionEvent(Safekeeper *sk); static void SendStartWALPush(Safekeeper *sk); @@ -138,403 +56,44 @@ static void SendProposerGreeting(Safekeeper *sk); static void RecvAcceptorGreeting(Safekeeper *sk); static void SendVoteRequest(Safekeeper *sk); static void RecvVoteResponse(Safekeeper *sk); -static void HandleElectedProposer(void); -static term_t GetHighestTerm(TermHistory * th); +static void HandleElectedProposer(WalProposer *wp); +static term_t GetHighestTerm(TermHistory *th); static term_t GetEpoch(Safekeeper *sk); -static void DetermineEpochStartLsn(void); -static bool WalProposerRecovery(int donor, TimeLineID timeline, XLogRecPtr startpos, XLogRecPtr endpos); +static void DetermineEpochStartLsn(WalProposer *wp); static void SendProposerElected(Safekeeper *sk); -static void WalProposerStartStreaming(XLogRecPtr startpos); static void StartStreaming(Safekeeper *sk); static void SendMessageToNode(Safekeeper *sk); -static void BroadcastAppendRequest(void); +static void BroadcastAppendRequest(WalProposer *wp); static void HandleActiveState(Safekeeper *sk, uint32 events); static bool SendAppendRequests(Safekeeper *sk); static bool RecvAppendResponses(Safekeeper *sk); -static void CombineHotStanbyFeedbacks(HotStandbyFeedback * hs); -static XLogRecPtr CalculateMinFlushLsn(void); -static XLogRecPtr GetAcknowledgedByQuorumWALPosition(void); -static void HandleSafekeeperResponse(void); +static XLogRecPtr CalculateMinFlushLsn(WalProposer *wp); +static XLogRecPtr GetAcknowledgedByQuorumWALPosition(WalProposer *wp); +static void HandleSafekeeperResponse(WalProposer *wp); static bool AsyncRead(Safekeeper *sk, char **buf, int *buf_size); -static bool AsyncReadMessage(Safekeeper *sk, AcceptorProposerMessage * anymsg); +static bool AsyncReadMessage(Safekeeper *sk, AcceptorProposerMessage *anymsg); static bool BlockingWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState success_state); static bool AsyncWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState flush_state); static bool AsyncFlush(Safekeeper *sk); +static int CompareLsn(const void *a, const void *b); +static char *FormatSafekeeperState(SafekeeperState state); +static void AssertEventsOkForState(uint32 events, Safekeeper *sk); +static uint32 SafekeeperStateDesiredEvents(SafekeeperState state); +static char *FormatEvents(uint32 events); -static void nwp_shmem_startup_hook(void); -static void nwp_register_gucs(void); -static void nwp_prepare_shmem(void); -static uint64 backpressure_lag_impl(void); -static bool backpressure_throttling_impl(void); - -static process_interrupts_callback_t PrevProcessInterruptsCallback; -static shmem_startup_hook_type prev_shmem_startup_hook_type; -#if PG_VERSION_NUM >= 150000 -static shmem_request_hook_type prev_shmem_request_hook = NULL; -static void walproposer_shmem_request(void); -#endif - -void -pg_init_walproposer(void) -{ - if (!process_shared_preload_libraries_in_progress) - return; - - nwp_register_gucs(); - - nwp_prepare_shmem(); - - delay_backend_us = &backpressure_lag_impl; - PrevProcessInterruptsCallback = ProcessInterruptsCallback; - ProcessInterruptsCallback = backpressure_throttling_impl; - - WalProposerRegister(); -} - -/* - * Entry point for `postgres --sync-safekeepers`. - */ -PGDLLEXPORT void -WalProposerSync(int argc, char *argv[]) -{ - struct stat stat_buf; - - syncSafekeepers = true; -#if PG_VERSION_NUM < 150000 - ThisTimeLineID = 1; -#endif - - /* - * Initialize postmaster_alive_fds as WaitEventSet checks them. - * - * Copied from InitPostmasterDeathWatchHandle() - */ - if (pipe(postmaster_alive_fds) < 0) - ereport(FATAL, - (errcode_for_file_access(), - errmsg_internal("could not create pipe to monitor postmaster death: %m"))); - if (fcntl(postmaster_alive_fds[POSTMASTER_FD_WATCH], F_SETFL, O_NONBLOCK) == -1) - ereport(FATAL, - (errcode_for_socket_access(), - errmsg_internal("could not set postmaster death monitoring pipe to nonblocking mode: %m"))); - - ChangeToDataDir(); - - /* Create pg_wal directory, if it doesn't exist */ - if (stat(XLOGDIR, &stat_buf) != 0) - { - ereport(LOG, (errmsg("creating missing WAL directory \"%s\"", XLOGDIR))); - if (MakePGDirectory(XLOGDIR) < 0) - { - ereport(ERROR, - (errcode_for_file_access(), - errmsg("could not create directory \"%s\": %m", - XLOGDIR))); - exit(1); - } - } - - WalProposerInit(0, 0); - - BackgroundWorkerUnblockSignals(); - - WalProposerStart(); -} - -static void -nwp_register_gucs(void) -{ - DefineCustomStringVariable( - "neon.safekeepers", - "List of Neon WAL acceptors (host:port)", - NULL, /* long_desc */ - &wal_acceptors_list, /* valueAddr */ - "", /* bootValue */ - PGC_POSTMASTER, - GUC_LIST_INPUT, /* extensions can't use* - * GUC_LIST_QUOTE */ - NULL, NULL, NULL); - - DefineCustomIntVariable( - "neon.safekeeper_reconnect_timeout", - "Walproposer reconnects to offline safekeepers once in this interval.", - NULL, - &wal_acceptor_reconnect_timeout, - 1000, 0, INT_MAX, /* default, min, max */ - PGC_SIGHUP, /* context */ - GUC_UNIT_MS, /* flags */ - NULL, NULL, NULL); - - DefineCustomIntVariable( - "neon.safekeeper_connect_timeout", - "Connection or connection attempt to safekeeper is terminated if no message is received (or connection attempt doesn't finish) within this period.", - NULL, - &wal_acceptor_connection_timeout, - 10000, 0, INT_MAX, - PGC_SIGHUP, - GUC_UNIT_MS, - NULL, NULL, NULL); -} - -/* shmem handling */ - -static void -nwp_prepare_shmem(void) -{ -#if PG_VERSION_NUM >= 150000 - prev_shmem_request_hook = shmem_request_hook; - shmem_request_hook = walproposer_shmem_request; -#else - RequestAddinShmemSpace(WalproposerShmemSize()); -#endif - prev_shmem_startup_hook_type = shmem_startup_hook; - shmem_startup_hook = nwp_shmem_startup_hook; -} - -#if PG_VERSION_NUM >= 150000 -/* - * shmem_request hook: request additional shared resources. We'll allocate or - * attach to the shared resources in nwp_shmem_startup_hook(). - */ -static void -walproposer_shmem_request(void) -{ - if (prev_shmem_request_hook) - prev_shmem_request_hook(); - - RequestAddinShmemSpace(WalproposerShmemSize()); -} -#endif - -static void -nwp_shmem_startup_hook(void) -{ - if (prev_shmem_startup_hook_type) - prev_shmem_startup_hook_type(); - - WalproposerShmemInit(); -} - -/* - * WAL proposer bgworker entry point. - */ -PGDLLEXPORT void -WalProposerMain(Datum main_arg) -{ -#if PG_VERSION_NUM >= 150000 - TimeLineID tli; -#endif - - /* Establish signal handlers. */ - pqsignal(SIGUSR1, procsignal_sigusr1_handler); - pqsignal(SIGHUP, SignalHandlerForConfigReload); - pqsignal(SIGTERM, die); - - BackgroundWorkerUnblockSignals(); - -#if PG_VERSION_NUM >= 150000 - /* FIXME pass proper tli to WalProposerInit ? */ - GetXLogReplayRecPtr(&tli); - WalProposerInit(GetFlushRecPtr(NULL), GetSystemIdentifier()); -#else - GetXLogReplayRecPtr(&ThisTimeLineID); - WalProposerInit(GetFlushRecPtr(), GetSystemIdentifier()); -#endif - - last_reconnect_attempt = GetCurrentTimestamp(); - - application_name = (char *) "walproposer"; /* for - * synchronous_standby_names */ - am_wal_proposer = true; - am_walsender = true; - InitWalSender(); - InitProcessPhase2(); - - /* Create replication slot for WAL proposer if not exists */ - if (SearchNamedReplicationSlot(WAL_PROPOSER_SLOT_NAME, false) == NULL) - { - ReplicationSlotCreate(WAL_PROPOSER_SLOT_NAME, false, RS_PERSISTENT, false); - ReplicationSlotReserveWal(); - /* Write this slot to disk */ - ReplicationSlotMarkDirty(); - ReplicationSlotSave(); - ReplicationSlotRelease(); - } - - WalProposerStart(); -} - -/* - * Create new AppendRequest message and start sending it. This function is - * called from walsender every time the new WAL is available. - */ -void -WalProposerBroadcast(XLogRecPtr startpos, XLogRecPtr endpos) -{ - Assert(startpos == availableLsn && endpos >= availableLsn); - availableLsn = endpos; - BroadcastAppendRequest(); -} - -/* - * Advance the WAL proposer state machine, waiting each time for events to occur. - * Will exit only when latch is set, i.e. new WAL should be pushed from walsender - * to walproposer. - */ -void -WalProposerPoll(void) -{ - while (true) - { - Safekeeper *sk = NULL; - bool wait_timeout = false; - bool late_cv_trigger = false; - WaitEvent event = {0}; - int rc = 0; - TimestampTz now = GetCurrentTimestamp(); - long timeout = TimeToReconnect(now); - -#if PG_MAJORVERSION_NUM >= 16 - if (WalSndCtl != NULL) - ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv); -#endif - - /* - * Wait for a wait event to happen, or timeout: - * - Safekeeper socket can become available for READ or WRITE - * - Our latch got set, because - * * PG15-: We got woken up by a process triggering the WalSender - * * PG16+: WalSndCtl->wal_flush_cv was triggered - */ - rc = WaitEventSetWait(waitEvents, timeout, - &event, 1, WAIT_EVENT_WAL_SENDER_MAIN); -#if PG_MAJORVERSION_NUM >= 16 - if (WalSndCtl != NULL) - late_cv_trigger = ConditionVariableCancelSleep(); -#endif - - /* - * If wait is terminated by latch set (walsenders' latch is set on - * each wal flush), then exit loop. (no need for pm death check due to - * WL_EXIT_ON_PM_DEATH) - */ - if ((rc == 1 && event.events & WL_LATCH_SET) || late_cv_trigger) - { - /* Reset our latch */ - ResetLatch(MyLatch); - - break; - } - - /* - * If the event contains something that one of our safekeeper states - * was waiting for, we'll advance its state. - */ - if (rc == 1 && (event.events & (WL_SOCKET_MASK))) - { - sk = (Safekeeper *) event.user_data; - AdvancePollState(sk, event.events); - } - - /* - * If the timeout expired, attempt to reconnect to any safekeepers - * that we dropped - */ - ReconnectSafekeepers(); - - if (rc == 0) /* timeout expired */ - { - wait_timeout = true; - - /* - * Ensure flushrecptr is set to a recent value. This fixes a case - * where we've not been notified of new WAL records when we were - * planning on consuming them. - */ - if (!syncSafekeepers) { - XLogRecPtr flushed; - -#if PG_MAJORVERSION_NUM < 15 - flushed = GetFlushRecPtr(); -#else - flushed = GetFlushRecPtr(NULL); -#endif - if (flushed > availableLsn) - break; - } - } - - now = GetCurrentTimestamp(); - if (rc == 0 || TimeToReconnect(now) <= 0) /* timeout expired: poll state */ - { - TimestampTz now; - - /* - * If no WAL was generated during timeout (and we have already - * collected the quorum), then send pool message - */ - if (availableLsn != InvalidXLogRecPtr) - { - BroadcastAppendRequest(); - } - - /* - * Abandon connection attempts which take too long. - */ - now = GetCurrentTimestamp(); - for (int i = 0; i < n_safekeepers; i++) - { - Safekeeper *sk = &safekeeper[i]; - - if (TimestampDifferenceExceeds(sk->latestMsgReceivedAt, now, - wal_acceptor_connection_timeout)) - { - elog(WARNING, "terminating connection to safekeeper '%s:%s' in '%s' state: no messages received during the last %dms or connection attempt took longer than that", - sk->host, sk->port, FormatSafekeeperState(sk->state), wal_acceptor_connection_timeout); - ShutdownConnection(sk); - } - } - } - } -} - -/* - * Register a background worker proposing WAL to wal acceptors. - */ -static void -WalProposerRegister(void) -{ - BackgroundWorker bgw; - - if (*wal_acceptors_list == '\0') - return; - - memset(&bgw, 0, sizeof(bgw)); - bgw.bgw_flags = BGWORKER_SHMEM_ACCESS; - bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; - snprintf(bgw.bgw_library_name, BGW_MAXLEN, "neon"); - snprintf(bgw.bgw_function_name, BGW_MAXLEN, "WalProposerMain"); - snprintf(bgw.bgw_name, BGW_MAXLEN, "WAL proposer"); - snprintf(bgw.bgw_type, BGW_MAXLEN, "WAL proposer"); - bgw.bgw_restart_time = 5; - bgw.bgw_notify_pid = 0; - bgw.bgw_main_arg = (Datum) 0; - - RegisterBackgroundWorker(&bgw); -} - -static void -WalProposerInit(XLogRecPtr flushRecPtr, uint64 systemId) +WalProposer * +WalProposerCreate(WalProposerConfig *config, walproposer_api api) { char *host; char *sep; char *port; + WalProposer *wp; - load_file("libpqwalreceiver", false); - if (WalReceiverFunctions == NULL) - elog(ERROR, "libpqwalreceiver didn't initialize correctly"); + wp = palloc0(sizeof(WalProposer)); + wp->config = config; + wp->api = api; - for (host = wal_acceptors_list; host != NULL && *host != '\0'; host = sep) + for (host = wp->config->safekeepers_list; host != NULL && *host != '\0'; host = sep) { port = strchr(host, ':'); if (port == NULL) @@ -545,118 +104,186 @@ WalProposerInit(XLogRecPtr flushRecPtr, uint64 systemId) sep = strchr(port, ','); if (sep != NULL) *sep++ = '\0'; - if (n_safekeepers + 1 >= MAX_SAFEKEEPERS) + if (wp->n_safekeepers + 1 >= MAX_SAFEKEEPERS) { elog(FATAL, "Too many safekeepers"); } - safekeeper[n_safekeepers].host = host; - safekeeper[n_safekeepers].port = port; - safekeeper[n_safekeepers].state = SS_OFFLINE; - safekeeper[n_safekeepers].conn = NULL; + wp->safekeeper[wp->n_safekeepers].host = host; + wp->safekeeper[wp->n_safekeepers].port = port; + wp->safekeeper[wp->n_safekeepers].state = SS_OFFLINE; + wp->safekeeper[wp->n_safekeepers].conn = NULL; + wp->safekeeper[wp->n_safekeepers].wp = wp; { - Safekeeper *sk = &safekeeper[n_safekeepers]; - int written = 0; + Safekeeper *sk = &wp->safekeeper[wp->n_safekeepers]; + int written = 0; written = snprintf((char *) &sk->conninfo, MAXCONNINFO, "host=%s port=%s dbname=replication options='-c timeline_id=%s tenant_id=%s'", - sk->host, sk->port, neon_timeline, neon_tenant); + sk->host, sk->port, wp->config->neon_timeline, wp->config->neon_tenant); if (written > MAXCONNINFO || written < 0) elog(FATAL, "could not create connection string for safekeeper %s:%s", sk->host, sk->port); } - initStringInfo(&safekeeper[n_safekeepers].outbuf); - safekeeper[n_safekeepers].xlogreader = XLogReaderAllocate(wal_segment_size, NULL, XL_ROUTINE(.segment_open = wal_segment_open,.segment_close = wal_segment_close), NULL); - if (safekeeper[n_safekeepers].xlogreader == NULL) + initStringInfo(&wp->safekeeper[wp->n_safekeepers].outbuf); + wp->safekeeper[wp->n_safekeepers].xlogreader = wp->api.wal_reader_allocate(); + if (wp->safekeeper[wp->n_safekeepers].xlogreader == NULL) elog(FATAL, "Failed to allocate xlog reader"); - safekeeper[n_safekeepers].flushWrite = false; - safekeeper[n_safekeepers].startStreamingAt = InvalidXLogRecPtr; - safekeeper[n_safekeepers].streamingAt = InvalidXLogRecPtr; - n_safekeepers += 1; + wp->safekeeper[wp->n_safekeepers].flushWrite = false; + wp->safekeeper[wp->n_safekeepers].startStreamingAt = InvalidXLogRecPtr; + wp->safekeeper[wp->n_safekeepers].streamingAt = InvalidXLogRecPtr; + wp->n_safekeepers += 1; } - if (n_safekeepers < 1) + if (wp->n_safekeepers < 1) { elog(FATAL, "Safekeepers addresses are not specified"); } - quorum = n_safekeepers / 2 + 1; + wp->quorum = wp->n_safekeepers / 2 + 1; /* Fill the greeting package */ - greetRequest.tag = 'g'; - greetRequest.protocolVersion = SK_PROTOCOL_VERSION; - greetRequest.pgVersion = PG_VERSION_NUM; - pg_strong_random(&greetRequest.proposerId, sizeof(greetRequest.proposerId)); - greetRequest.systemId = systemId; - if (!neon_timeline) + wp->greetRequest.tag = 'g'; + wp->greetRequest.protocolVersion = SK_PROTOCOL_VERSION; + wp->greetRequest.pgVersion = PG_VERSION_NUM; + wp->api.strong_random(&wp->greetRequest.proposerId, sizeof(wp->greetRequest.proposerId)); + wp->greetRequest.systemId = wp->config->systemId; + if (!wp->config->neon_timeline) elog(FATAL, "neon.timeline_id is not provided"); - if (*neon_timeline != '\0' && - !HexDecodeString(greetRequest.timeline_id, neon_timeline, 16)) - elog(FATAL, "Could not parse neon.timeline_id, %s", neon_timeline); - if (!neon_tenant) + if (*wp->config->neon_timeline != '\0' && + !HexDecodeString(wp->greetRequest.timeline_id, wp->config->neon_timeline, 16)) + elog(FATAL, "Could not parse neon.timeline_id, %s", wp->config->neon_timeline); + if (!wp->config->neon_tenant) elog(FATAL, "neon.tenant_id is not provided"); - if (*neon_tenant != '\0' && - !HexDecodeString(greetRequest.tenant_id, neon_tenant, 16)) - elog(FATAL, "Could not parse neon.tenant_id, %s", neon_tenant); + if (*wp->config->neon_tenant != '\0' && + !HexDecodeString(wp->greetRequest.tenant_id, wp->config->neon_tenant, 16)) + elog(FATAL, "Could not parse neon.tenant_id, %s", wp->config->neon_tenant); -#if PG_VERSION_NUM >= 150000 - /* FIXME don't use hardcoded timeline id */ - greetRequest.timeline = 1; -#else - greetRequest.timeline = ThisTimeLineID; -#endif - greetRequest.walSegSize = wal_segment_size; + wp->greetRequest.timeline = wp->api.get_timeline_id(); + wp->greetRequest.walSegSize = wp->config->wal_segment_size; - InitEventSet(); -} + wp->api.init_event_set(wp->n_safekeepers); -static void -WalProposerStart(void) -{ - - /* Initiate connections to all safekeeper nodes */ - for (int i = 0; i < n_safekeepers; i++) - { - ResetConnection(&safekeeper[i]); - } - - WalProposerLoop(); -} - -static void -WalProposerLoop(void) -{ - while (true) - WalProposerPoll(); -} - -/* Initializes the internal event set, provided that it is currently null */ -static void -InitEventSet(void) -{ - if (waitEvents) - elog(FATAL, "double-initialization of event set"); - - waitEvents = CreateWaitEventSet(TopMemoryContext, 2 + n_safekeepers); - AddWaitEventToSet(waitEvents, WL_LATCH_SET, PGINVALID_SOCKET, - MyLatch, NULL); - AddWaitEventToSet(waitEvents, WL_EXIT_ON_PM_DEATH, PGINVALID_SOCKET, - NULL, NULL); + return wp; } /* - * Updates the events we're already waiting on for the safekeeper, setting it to - * the provided `events` - * - * This function is called any time the safekeeper's state switches to one where - * it has to wait to continue. This includes the full body of AdvancePollState - * and calls to IO helper functions. + * Create new AppendRequest message and start sending it. This function is + * called from walsender every time the new WAL is available. */ -static void -UpdateEventSet(Safekeeper *sk, uint32 events) +void +WalProposerBroadcast(WalProposer *wp, XLogRecPtr startpos, XLogRecPtr endpos) { - /* eventPos = -1 when we don't have an event */ - Assert(sk->eventPos != -1); + Assert(startpos == wp->availableLsn && endpos >= wp->availableLsn); + wp->availableLsn = endpos; + BroadcastAppendRequest(wp); +} - ModifyWaitEvent(waitEvents, sk->eventPos, events, NULL); +/* + * Advance the WAL proposer state machine, waiting each time for events to occur. + * Will exit only when latch is set, i.e. new WAL should be pushed from walsender + * to walproposer. + */ +void +WalProposerPoll(WalProposer *wp) +{ + while (true) + { + Safekeeper *sk = NULL; + int rc = 0; + uint32 events = 0; + TimestampTz now = wp->api.get_current_timestamp(); + long timeout = TimeToReconnect(wp, now); + + rc = wp->api.wait_event_set(timeout, &sk, &events); + + /* Exit loop if latch is set (we got new WAL) */ + if ((rc == 1 && events & WL_LATCH_SET)) + break; + + /* + * If the event contains something that one of our safekeeper states + * was waiting for, we'll advance its state. + */ + if (rc == 1 && (events & WL_SOCKET_MASK)) + { + Assert(sk != NULL); + AdvancePollState(sk, events); + } + + /* + * If the timeout expired, attempt to reconnect to any safekeepers + * that we dropped + */ + ReconnectSafekeepers(wp); + + if (rc == 0) /* timeout expired */ + { + /* + * Ensure flushrecptr is set to a recent value. This fixes a case + * where we've not been notified of new WAL records when we were + * planning on consuming them. + */ + if (!wp->config->syncSafekeepers) + { + XLogRecPtr flushed = wp->api.get_flush_rec_ptr(); + + if (flushed > wp->availableLsn) + break; + } + } + + now = wp->api.get_current_timestamp(); + /* timeout expired: poll state */ + if (rc == 0 || TimeToReconnect(wp, now) <= 0) + { + TimestampTz now; + + /* + * If no WAL was generated during timeout (and we have already + * collected the quorum), then send empty keepalive message + */ + if (wp->availableLsn != InvalidXLogRecPtr) + { + BroadcastAppendRequest(wp); + } + + /* + * Abandon connection attempts which take too long. + */ + now = wp->api.get_current_timestamp(); + for (int i = 0; i < wp->n_safekeepers; i++) + { + Safekeeper *sk = &wp->safekeeper[i]; + + if (TimestampDifferenceExceeds(sk->latestMsgReceivedAt, now, + wp->config->safekeeper_connection_timeout)) + { + elog(WARNING, "terminating connection to safekeeper '%s:%s' in '%s' state: no messages received during the last %dms or connection attempt took longer than that", + sk->host, sk->port, FormatSafekeeperState(sk->state), wp->config->safekeeper_connection_timeout); + ShutdownConnection(sk); + } + } + } + } +} + +void +WalProposerStart(WalProposer *wp) +{ + + /* Initiate connections to all safekeeper nodes */ + for (int i = 0; i < wp->n_safekeepers; i++) + { + ResetConnection(&wp->safekeeper[i]); + } + + WalProposerLoop(wp); +} + +static void +WalProposerLoop(WalProposer *wp) +{ + while (true) + WalProposerPoll(wp); } /* @@ -667,24 +294,22 @@ UpdateEventSet(Safekeeper *sk, uint32 events) static void HackyRemoveWalProposerEvent(Safekeeper *to_remove) { + WalProposer *wp = to_remove->wp; + /* Remove the existing event set */ - if (waitEvents) - { - FreeWaitEventSet(waitEvents); - waitEvents = NULL; - } + wp->api.free_event_set(); /* Re-initialize it without adding any safekeeper events */ - InitEventSet(); + wp->api.init_event_set(wp->n_safekeepers); /* * loop through the existing safekeepers. If they aren't the one we're * removing, and if they have a socket we can use, re-add the applicable * events. */ - for (int i = 0; i < n_safekeepers; i++) + for (int i = 0; i < wp->n_safekeepers; i++) { uint32 desired_events = WL_NO_EVENTS; - Safekeeper *sk = &safekeeper[i]; + Safekeeper *sk = &wp->safekeeper[i]; sk->eventPos = -1; @@ -695,7 +320,8 @@ HackyRemoveWalProposerEvent(Safekeeper *to_remove) if (sk->conn != NULL) { desired_events = SafekeeperStateDesiredEvents(sk->state); - sk->eventPos = AddWaitEventToSet(waitEvents, desired_events, walprop_socket(sk->conn), NULL, sk); + /* will set sk->eventPos */ + wp->api.add_safekeeper_event_set(sk, desired_events); } } } @@ -705,7 +331,7 @@ static void ShutdownConnection(Safekeeper *sk) { if (sk->conn) - walprop_finish(sk->conn); + sk->wp->api.conn_finish(sk->conn); sk->conn = NULL; sk->state = SS_OFFLINE; sk->flushWrite = false; @@ -727,7 +353,7 @@ ShutdownConnection(Safekeeper *sk) static void ResetConnection(Safekeeper *sk) { - pgsocket sock; /* socket of the new connection */ + WalProposer *wp = sk->wp; if (sk->state != SS_OFFLINE) { @@ -737,7 +363,7 @@ ResetConnection(Safekeeper *sk) /* * Try to establish new connection */ - sk->conn = walprop_connect_start((char *) &sk->conninfo, neon_auth_token); + sk->conn = wp->api.conn_connect_start((char *) &sk->conninfo); /* * "If the result is null, then libpq has been unable to allocate a new @@ -751,7 +377,7 @@ ResetConnection(Safekeeper *sk) * PQconnectPoll. Before we do that though, we need to check that it * didn't immediately fail. */ - if (walprop_status(sk->conn) == WP_CONNECTION_BAD) + if (wp->api.conn_status(sk->conn) == WP_CONNECTION_BAD) { /*--- * According to libpq docs: @@ -763,13 +389,13 @@ ResetConnection(Safekeeper *sk) * https://www.postgresql.org/docs/devel/libpq-connect.html#LIBPQ-PQCONNECTSTARTPARAMS */ elog(WARNING, "Immediate failure to connect with node '%s:%s':\n\terror: %s", - sk->host, sk->port, walprop_error_message(sk->conn)); + sk->host, sk->port, wp->api.conn_error_message(sk->conn)); /* * Even though the connection failed, we still need to clean up the * object */ - walprop_finish(sk->conn); + wp->api.conn_finish(sk->conn); sk->conn = NULL; return; } @@ -790,10 +416,9 @@ ResetConnection(Safekeeper *sk) elog(LOG, "connecting with node %s:%s", sk->host, sk->port); sk->state = SS_CONNECTING_WRITE; - sk->latestMsgReceivedAt = GetCurrentTimestamp(); + sk->latestMsgReceivedAt = wp->api.get_current_timestamp(); - sock = walprop_socket(sk->conn); - sk->eventPos = AddWaitEventToSet(waitEvents, WL_SOCKET_WRITEABLE, sock, NULL, sk); + wp->api.add_safekeeper_event_set(sk, WL_SOCKET_WRITEABLE); return; } @@ -803,16 +428,16 @@ ResetConnection(Safekeeper *sk) * (do we actually need this?). */ static long -TimeToReconnect(TimestampTz now) +TimeToReconnect(WalProposer *wp, TimestampTz now) { TimestampTz passed; TimestampTz till_reconnect; - if (wal_acceptor_reconnect_timeout <= 0) + if (wp->config->safekeeper_reconnect_timeout <= 0) return -1; - passed = now - last_reconnect_attempt; - till_reconnect = wal_acceptor_reconnect_timeout * 1000 - passed; + passed = now - wp->last_reconnect_attempt; + till_reconnect = wp->config->safekeeper_reconnect_timeout * 1000 - passed; if (till_reconnect <= 0) return 0; return (long) (till_reconnect / 1000); @@ -820,17 +445,17 @@ TimeToReconnect(TimestampTz now) /* If the timeout has expired, attempt to reconnect to all offline safekeepers */ static void -ReconnectSafekeepers(void) +ReconnectSafekeepers(WalProposer *wp) { - TimestampTz now = GetCurrentTimestamp(); + TimestampTz now = wp->api.get_current_timestamp(); - if (TimeToReconnect(now) == 0) + if (TimeToReconnect(wp, now) == 0) { - last_reconnect_attempt = now; - for (int i = 0; i < n_safekeepers; i++) + wp->last_reconnect_attempt = now; + for (int i = 0; i < wp->n_safekeepers; i++) { - if (safekeeper[i].state == SS_OFFLINE) - ResetConnection(&safekeeper[i]); + if (wp->safekeeper[i].state == SS_OFFLINE) + ResetConnection(&wp->safekeeper[i]); } } } @@ -938,7 +563,8 @@ AdvancePollState(Safekeeper *sk, uint32 events) static void HandleConnectionEvent(Safekeeper *sk) { - WalProposerConnectPollStatusType result = walprop_connect_poll(sk->conn); + WalProposer *wp = sk->wp; + WalProposerConnectPollStatusType result = wp->api.conn_connect_poll(sk->conn); /* The new set of events we'll wait on, after updating */ uint32 new_events = WL_NO_EVENTS; @@ -948,7 +574,8 @@ HandleConnectionEvent(Safekeeper *sk) case WP_CONN_POLLING_OK: elog(LOG, "connected with node %s:%s", sk->host, sk->port); - sk->latestMsgReceivedAt = GetCurrentTimestamp(); + sk->latestMsgReceivedAt = wp->api.get_current_timestamp(); + /* * We have to pick some event to update event set. We'll * eventually need the socket to be readable, so we go with that. @@ -970,7 +597,7 @@ HandleConnectionEvent(Safekeeper *sk) case WP_CONN_POLLING_FAILED: elog(WARNING, "failed to connect to node '%s:%s': %s", - sk->host, sk->port, walprop_error_message(sk->conn)); + sk->host, sk->port, wp->api.conn_error_message(sk->conn)); /* * If connecting failed, we don't want to restart the connection @@ -987,7 +614,7 @@ HandleConnectionEvent(Safekeeper *sk) * old event and re-register an event on the new socket. */ HackyRemoveWalProposerEvent(sk); - sk->eventPos = AddWaitEventToSet(waitEvents, new_events, walprop_socket(sk->conn), NULL, sk); + wp->api.add_safekeeper_event_set(sk, new_events); /* If we successfully connected, send START_WAL_PUSH query */ if (result == WP_CONN_POLLING_OK) @@ -1002,21 +629,25 @@ HandleConnectionEvent(Safekeeper *sk) static void SendStartWALPush(Safekeeper *sk) { - if (!walprop_send_query(sk->conn, "START_WAL_PUSH")) + WalProposer *wp = sk->wp; + + if (!wp->api.conn_send_query(sk->conn, "START_WAL_PUSH")) { elog(WARNING, "Failed to send 'START_WAL_PUSH' query to safekeeper %s:%s: %s", - sk->host, sk->port, walprop_error_message(sk->conn)); + sk->host, sk->port, wp->api.conn_error_message(sk->conn)); ShutdownConnection(sk); return; } sk->state = SS_WAIT_EXEC_RESULT; - UpdateEventSet(sk, WL_SOCKET_READABLE); + wp->api.update_event_set(sk, WL_SOCKET_READABLE); } static void RecvStartWALPushResult(Safekeeper *sk) { - switch (walprop_get_query_result(sk->conn)) + WalProposer *wp = sk->wp; + + switch (wp->api.conn_get_query_result(sk->conn)) { /* * Successful result, move on to starting the handshake @@ -1040,7 +671,7 @@ RecvStartWALPushResult(Safekeeper *sk) case WP_EXEC_FAILED: elog(WARNING, "Failed to send query to safekeeper %s:%s: %s", - sk->host, sk->port, walprop_error_message(sk->conn)); + sk->host, sk->port, wp->api.conn_error_message(sk->conn)); ShutdownConnection(sk); return; @@ -1069,19 +700,21 @@ SendProposerGreeting(Safekeeper *sk) * On failure, logging & resetting the connection is handled. We just need * to handle the control flow. */ - BlockingWrite(sk, &greetRequest, sizeof(greetRequest), SS_HANDSHAKE_RECV); + BlockingWrite(sk, &sk->wp->greetRequest, sizeof(sk->wp->greetRequest), SS_HANDSHAKE_RECV); } static void RecvAcceptorGreeting(Safekeeper *sk) { + WalProposer *wp = sk->wp; + /* * If our reading doesn't immediately succeed, any necessary error * handling or state setting is taken care of. We can leave any other work * until later. */ sk->greetResponse.apm.tag = 'g'; - if (!AsyncReadMessage(sk, (AcceptorProposerMessage *) & sk->greetResponse)) + if (!AsyncReadMessage(sk, (AcceptorProposerMessage *) &sk->greetResponse)) return; elog(LOG, "received AcceptorGreeting from safekeeper %s:%s", sk->host, sk->port); @@ -1089,37 +722,37 @@ RecvAcceptorGreeting(Safekeeper *sk) /* Protocol is all good, move to voting. */ sk->state = SS_VOTING; - /* + /* * Note: it would be better to track the counter on per safekeeper basis, - * but at worst walproposer would restart with 'term rejected', so leave as - * is for now. + * but at worst walproposer would restart with 'term rejected', so leave + * as is for now. */ - ++n_connected; - if (n_connected <= quorum) + ++wp->n_connected; + if (wp->n_connected <= wp->quorum) { /* We're still collecting terms from the majority. */ - propTerm = Max(sk->greetResponse.term, propTerm); + wp->propTerm = Max(sk->greetResponse.term, wp->propTerm); /* Quorum is acquried, prepare the vote request. */ - if (n_connected == quorum) + if (wp->n_connected == wp->quorum) { - propTerm++; - elog(LOG, "proposer connected to quorum (%d) safekeepers, propTerm=" INT64_FORMAT, quorum, propTerm); + wp->propTerm++; + elog(LOG, "proposer connected to quorum (%d) safekeepers, propTerm=" INT64_FORMAT, wp->quorum, wp->propTerm); - voteRequest = (VoteRequest) + wp->voteRequest = (VoteRequest) { .tag = 'v', - .term = propTerm + .term = wp->propTerm }; - memcpy(voteRequest.proposerId.data, greetRequest.proposerId.data, UUID_LEN); + memcpy(wp->voteRequest.proposerId.data, wp->greetRequest.proposerId.data, UUID_LEN); } } - else if (sk->greetResponse.term > propTerm) + else if (sk->greetResponse.term > wp->propTerm) { /* Another compute with higher term is running. */ elog(FATAL, "WAL acceptor %s:%s with term " INT64_FORMAT " rejects our connection request with term " INT64_FORMAT "", sk->host, sk->port, - sk->greetResponse.term, propTerm); + sk->greetResponse.term, wp->propTerm); } /* @@ -1128,27 +761,27 @@ RecvAcceptorGreeting(Safekeeper *sk) * * If we do have quorum, we can start an election. */ - if (n_connected < quorum) + if (wp->n_connected < wp->quorum) { /* * SS_VOTING is an idle state; read-ready indicates the connection * closed. */ - UpdateEventSet(sk, WL_SOCKET_READABLE); + wp->api.update_event_set(sk, WL_SOCKET_READABLE); } else { /* * Now send voting request to the cohort and wait responses */ - for (int j = 0; j < n_safekeepers; j++) + for (int j = 0; j < wp->n_safekeepers; j++) { /* * Remember: SS_VOTING indicates that the safekeeper is * participating in voting, but hasn't sent anything yet. */ - if (safekeeper[j].state == SS_VOTING) - SendVoteRequest(&safekeeper[j]); + if (wp->safekeeper[j].state == SS_VOTING) + SendVoteRequest(&wp->safekeeper[j]); } } } @@ -1156,10 +789,12 @@ RecvAcceptorGreeting(Safekeeper *sk) static void SendVoteRequest(Safekeeper *sk) { + WalProposer *wp = sk->wp; + /* We have quorum for voting, send our vote request */ - elog(LOG, "requesting vote from %s:%s for term " UINT64_FORMAT, sk->host, sk->port, voteRequest.term); + elog(LOG, "requesting vote from %s:%s for term " UINT64_FORMAT, sk->host, sk->port, wp->voteRequest.term); /* On failure, logging & resetting is handled */ - if (!BlockingWrite(sk, &voteRequest, sizeof(voteRequest), SS_WAIT_VERDICT)) + if (!BlockingWrite(sk, &wp->voteRequest, sizeof(wp->voteRequest), SS_WAIT_VERDICT)) return; /* If successful, wait for read-ready with SS_WAIT_VERDICT */ @@ -1168,8 +803,10 @@ SendVoteRequest(Safekeeper *sk) static void RecvVoteResponse(Safekeeper *sk) { + WalProposer *wp = sk->wp; + sk->voteResponse.apm.tag = 'v'; - if (!AsyncReadMessage(sk, (AcceptorProposerMessage *) & sk->voteResponse)) + if (!AsyncReadMessage(sk, (AcceptorProposerMessage *) &sk->voteResponse)) return; elog(LOG, @@ -1185,21 +822,21 @@ RecvVoteResponse(Safekeeper *sk) * we are not elected yet and thus need the vote. */ if ((!sk->voteResponse.voteGiven) && - (sk->voteResponse.term > propTerm || n_votes < quorum)) + (sk->voteResponse.term > wp->propTerm || wp->n_votes < wp->quorum)) { elog(FATAL, "WAL acceptor %s:%s with term " INT64_FORMAT " rejects our connection request with term " INT64_FORMAT "", sk->host, sk->port, - sk->voteResponse.term, propTerm); + sk->voteResponse.term, wp->propTerm); } - Assert(sk->voteResponse.term == propTerm); + Assert(sk->voteResponse.term == wp->propTerm); /* Handshake completed, do we have quorum? */ - n_votes++; - if (n_votes < quorum) + wp->n_votes++; + if (wp->n_votes < wp->quorum) { sk->state = SS_IDLE; /* can't do much yet, no quorum */ } - else if (n_votes > quorum) + else if (wp->n_votes > wp->quorum) { /* recovery already performed, just start streaming */ SendProposerElected(sk); @@ -1207,10 +844,10 @@ RecvVoteResponse(Safekeeper *sk) else { sk->state = SS_IDLE; - UpdateEventSet(sk, WL_SOCKET_READABLE); /* Idle states wait for - * read-ready */ + /* Idle state waits for read-ready events */ + wp->api.update_event_set(sk, WL_SOCKET_READABLE); - HandleElectedProposer(); + HandleElectedProposer(sk->wp); } } @@ -1222,36 +859,36 @@ RecvVoteResponse(Safekeeper *sk) * replication from walsender. */ static void -HandleElectedProposer(void) +HandleElectedProposer(WalProposer *wp) { - DetermineEpochStartLsn(); + DetermineEpochStartLsn(wp); /* * Check if not all safekeepers are up-to-date, we need to download WAL * needed to synchronize them */ - if (truncateLsn < propEpochStartLsn) + if (wp->truncateLsn < wp->propEpochStartLsn) { elog(LOG, "start recovery because truncateLsn=%X/%X is not " "equal to epochStartLsn=%X/%X", - LSN_FORMAT_ARGS(truncateLsn), - LSN_FORMAT_ARGS(propEpochStartLsn)); + LSN_FORMAT_ARGS(wp->truncateLsn), + LSN_FORMAT_ARGS(wp->propEpochStartLsn)); /* Perform recovery */ - if (!WalProposerRecovery(donor, greetRequest.timeline, truncateLsn, propEpochStartLsn)) + if (!wp->api.recovery_download(&wp->safekeeper[wp->donor], wp->greetRequest.timeline, wp->truncateLsn, wp->propEpochStartLsn)) elog(FATAL, "Failed to recover state"); } - else if (syncSafekeepers) + else if (wp->config->syncSafekeepers) { /* Sync is not needed: just exit */ - fprintf(stdout, "%X/%X\n", LSN_FORMAT_ARGS(propEpochStartLsn)); - exit(0); + wp->api.finish_sync_safekeepers(wp->propEpochStartLsn); + /* unreachable */ } - for (int i = 0; i < n_safekeepers; i++) + for (int i = 0; i < wp->n_safekeepers; i++) { - if (safekeeper[i].state == SS_IDLE) - SendProposerElected(&safekeeper[i]); + if (wp->safekeeper[i].state == SS_IDLE) + SendProposerElected(&wp->safekeeper[i]); } /* @@ -1260,7 +897,7 @@ HandleElectedProposer(void) * because that state is used only for quorum waiting. */ - if (syncSafekeepers) + if (wp->config->syncSafekeepers) { /* * Send empty message to enforce receiving feedback even from nodes @@ -1268,19 +905,19 @@ HandleElectedProposer(void) * epoch which finishes sync-safeekepers who doesn't generate any real * new records. Will go away once we switch to async acks. */ - BroadcastAppendRequest(); + BroadcastAppendRequest(wp); /* keep polling until all safekeepers are synced */ return; } - WalProposerStartStreaming(propEpochStartLsn); + wp->api.start_streaming(wp, wp->propEpochStartLsn); /* Should not return here */ } /* latest term in TermHistory, or 0 is there is no entries */ static term_t -GetHighestTerm(TermHistory * th) +GetHighestTerm(TermHistory *th) { return th->n_entries > 0 ? th->entries[th->n_entries - 1].term : 0; } @@ -1294,9 +931,9 @@ GetEpoch(Safekeeper *sk) /* If LSN points to the page header, skip it */ static XLogRecPtr -SkipXLogPageHeader(XLogRecPtr lsn) +SkipXLogPageHeader(WalProposer *wp, XLogRecPtr lsn) { - if (XLogSegmentOffset(lsn, wal_segment_size) == 0) + if (XLogSegmentOffset(lsn, wp->config->wal_segment_size) == 0) { lsn += SizeOfXLogLongPHD; } @@ -1316,41 +953,41 @@ SkipXLogPageHeader(XLogRecPtr lsn) * only for skipping recovery). */ static void -DetermineEpochStartLsn(void) +DetermineEpochStartLsn(WalProposer *wp) { TermHistory *dth; - propEpochStartLsn = InvalidXLogRecPtr; - donorEpoch = 0; - truncateLsn = InvalidXLogRecPtr; - timelineStartLsn = InvalidXLogRecPtr; + wp->propEpochStartLsn = InvalidXLogRecPtr; + wp->donorEpoch = 0; + wp->truncateLsn = InvalidXLogRecPtr; + wp->timelineStartLsn = InvalidXLogRecPtr; - for (int i = 0; i < n_safekeepers; i++) + for (int i = 0; i < wp->n_safekeepers; i++) { - if (safekeeper[i].state == SS_IDLE) + if (wp->safekeeper[i].state == SS_IDLE) { - if (GetEpoch(&safekeeper[i]) > donorEpoch || - (GetEpoch(&safekeeper[i]) == donorEpoch && - safekeeper[i].voteResponse.flushLsn > propEpochStartLsn)) + if (GetEpoch(&wp->safekeeper[i]) > wp->donorEpoch || + (GetEpoch(&wp->safekeeper[i]) == wp->donorEpoch && + wp->safekeeper[i].voteResponse.flushLsn > wp->propEpochStartLsn)) { - donorEpoch = GetEpoch(&safekeeper[i]); - propEpochStartLsn = safekeeper[i].voteResponse.flushLsn; - donor = i; + wp->donorEpoch = GetEpoch(&wp->safekeeper[i]); + wp->propEpochStartLsn = wp->safekeeper[i].voteResponse.flushLsn; + wp->donor = i; } - truncateLsn = Max(safekeeper[i].voteResponse.truncateLsn, truncateLsn); + wp->truncateLsn = Max(wp->safekeeper[i].voteResponse.truncateLsn, wp->truncateLsn); - if (safekeeper[i].voteResponse.timelineStartLsn != InvalidXLogRecPtr) + if (wp->safekeeper[i].voteResponse.timelineStartLsn != InvalidXLogRecPtr) { /* timelineStartLsn should be the same everywhere or unknown */ - if (timelineStartLsn != InvalidXLogRecPtr && - timelineStartLsn != safekeeper[i].voteResponse.timelineStartLsn) + if (wp->timelineStartLsn != InvalidXLogRecPtr && + wp->timelineStartLsn != wp->safekeeper[i].voteResponse.timelineStartLsn) { elog(WARNING, "inconsistent timelineStartLsn: current %X/%X, received %X/%X", - LSN_FORMAT_ARGS(timelineStartLsn), - LSN_FORMAT_ARGS(safekeeper[i].voteResponse.timelineStartLsn)); + LSN_FORMAT_ARGS(wp->timelineStartLsn), + LSN_FORMAT_ARGS(wp->safekeeper[i].voteResponse.timelineStartLsn)); } - timelineStartLsn = safekeeper[i].voteResponse.timelineStartLsn; + wp->timelineStartLsn = wp->safekeeper[i].voteResponse.timelineStartLsn; } } } @@ -1359,14 +996,14 @@ DetermineEpochStartLsn(void) * If propEpochStartLsn is 0 everywhere, we are bootstrapping -- nothing * was committed yet. Start streaming then from the basebackup LSN. */ - if (propEpochStartLsn == InvalidXLogRecPtr && !syncSafekeepers) + if (wp->propEpochStartLsn == InvalidXLogRecPtr && !wp->config->syncSafekeepers) { - propEpochStartLsn = truncateLsn = GetRedoStartLsn(); - if (timelineStartLsn == InvalidXLogRecPtr) + wp->propEpochStartLsn = wp->truncateLsn = wp->api.get_redo_start_lsn(); + if (wp->timelineStartLsn == InvalidXLogRecPtr) { - timelineStartLsn = GetRedoStartLsn(); + wp->timelineStartLsn = wp->api.get_redo_start_lsn(); } - elog(LOG, "bumped epochStartLsn to the first record %X/%X", LSN_FORMAT_ARGS(propEpochStartLsn)); + elog(LOG, "bumped epochStartLsn to the first record %X/%X", LSN_FORMAT_ARGS(wp->propEpochStartLsn)); } /* @@ -1374,46 +1011,48 @@ DetermineEpochStartLsn(void) * some connected safekeeper; it must have carried truncateLsn pointing to * the first record. */ - Assert((truncateLsn != InvalidXLogRecPtr) || - (syncSafekeepers && truncateLsn == propEpochStartLsn)); + Assert((wp->truncateLsn != InvalidXLogRecPtr) || + (wp->config->syncSafekeepers && wp->truncateLsn == wp->propEpochStartLsn)); /* * We will be generating WAL since propEpochStartLsn, so we should set * availableLsn to mark this LSN as the latest available position. */ - availableLsn = propEpochStartLsn; + wp->availableLsn = wp->propEpochStartLsn; /* * Proposer's term history is the donor's + its own entry. */ - dth = &safekeeper[donor].voteResponse.termHistory; - propTermHistory.n_entries = dth->n_entries + 1; - propTermHistory.entries = palloc(sizeof(TermSwitchEntry) * propTermHistory.n_entries); - memcpy(propTermHistory.entries, dth->entries, sizeof(TermSwitchEntry) * dth->n_entries); - propTermHistory.entries[propTermHistory.n_entries - 1].term = propTerm; - propTermHistory.entries[propTermHistory.n_entries - 1].lsn = propEpochStartLsn; + dth = &wp->safekeeper[wp->donor].voteResponse.termHistory; + wp->propTermHistory.n_entries = dth->n_entries + 1; + wp->propTermHistory.entries = palloc(sizeof(TermSwitchEntry) * wp->propTermHistory.n_entries); + memcpy(wp->propTermHistory.entries, dth->entries, sizeof(TermSwitchEntry) * dth->n_entries); + wp->propTermHistory.entries[wp->propTermHistory.n_entries - 1].term = wp->propTerm; + wp->propTermHistory.entries[wp->propTermHistory.n_entries - 1].lsn = wp->propEpochStartLsn; elog(LOG, "got votes from majority (%d) of nodes, term " UINT64_FORMAT ", epochStartLsn %X/%X, donor %s:%s, truncate_lsn %X/%X", - quorum, - propTerm, - LSN_FORMAT_ARGS(propEpochStartLsn), - safekeeper[donor].host, safekeeper[donor].port, - LSN_FORMAT_ARGS(truncateLsn)); + wp->quorum, + wp->propTerm, + LSN_FORMAT_ARGS(wp->propEpochStartLsn), + wp->safekeeper[wp->donor].host, wp->safekeeper[wp->donor].port, + LSN_FORMAT_ARGS(wp->truncateLsn)); /* * Ensure the basebackup we are running (at RedoStartLsn) matches LSN * since which we are going to write according to the consensus. If not, * we must bail out, as clog and other non rel data is inconsistent. */ - if (!syncSafekeepers) + if (!wp->config->syncSafekeepers) { + WalproposerShmemState *walprop_shared = wp->api.get_shmem_state(); + /* * Basebackup LSN always points to the beginning of the record (not * the page), as StartupXLOG most probably wants it this way. * Safekeepers don't skip header as they need continious stream of * data, so correct LSN for comparison. */ - if (SkipXLogPageHeader(propEpochStartLsn) != GetRedoStartLsn()) + if (SkipXLogPageHeader(wp, wp->propEpochStartLsn) != wp->api.get_redo_start_lsn()) { /* * However, allow to proceed if previously elected leader was me; @@ -1425,119 +1064,14 @@ DetermineEpochStartLsn(void) { elog(PANIC, "collected propEpochStartLsn %X/%X, but basebackup LSN %X/%X", - LSN_FORMAT_ARGS(propEpochStartLsn), - LSN_FORMAT_ARGS(GetRedoStartLsn())); + LSN_FORMAT_ARGS(wp->propEpochStartLsn), + LSN_FORMAT_ARGS(wp->api.get_redo_start_lsn())); } } - walprop_shared->mineLastElectedTerm = propTerm; + walprop_shared->mineLastElectedTerm = wp->propTerm; } } -/* - * Receive WAL from most advanced safekeeper - */ -static bool -WalProposerRecovery(int donor, TimeLineID timeline, XLogRecPtr startpos, XLogRecPtr endpos) -{ - char *err; - WalReceiverConn *wrconn; - WalRcvStreamOptions options; - char conninfo[MAXCONNINFO]; - - if (!neon_auth_token) - { - memcpy(conninfo, safekeeper[donor].conninfo, MAXCONNINFO); - } - else - { - int written = 0; - - written = snprintf((char *) conninfo, MAXCONNINFO, "password=%s %s", neon_auth_token, safekeeper[donor].conninfo); - if (written > MAXCONNINFO || written < 0) - elog(FATAL, "could not append password to the safekeeper connection string"); - } - -#if PG_MAJORVERSION_NUM < 16 - wrconn = walrcv_connect(conninfo, false, "wal_proposer_recovery", &err); -#else - wrconn = walrcv_connect(conninfo, false, false, "wal_proposer_recovery", &err); -#endif - - if (!wrconn) - { - ereport(WARNING, - (errmsg("could not connect to WAL acceptor %s:%s: %s", - safekeeper[donor].host, safekeeper[donor].port, - err))); - return false; - } - elog(LOG, - "start recovery from %s:%s starting from %X/%08X till %X/%08X timeline " - "%d", - safekeeper[donor].host, safekeeper[donor].port, (uint32) (startpos >> 32), - (uint32) startpos, (uint32) (endpos >> 32), (uint32) endpos, timeline); - - options.logical = false; - options.startpoint = startpos; - options.slotname = NULL; - options.proto.physical.startpointTLI = timeline; - - if (walrcv_startstreaming(wrconn, &options)) - { - XLogRecPtr rec_start_lsn; - XLogRecPtr rec_end_lsn = 0; - int len; - char *buf; - pgsocket wait_fd = PGINVALID_SOCKET; - - while ((len = walrcv_receive(wrconn, &buf, &wait_fd)) >= 0) - { - if (len == 0) - { - (void) WaitLatchOrSocket( - MyLatch, WL_EXIT_ON_PM_DEATH | WL_SOCKET_READABLE, wait_fd, - -1, WAIT_EVENT_WAL_RECEIVER_MAIN); - } - else - { - Assert(buf[0] == 'w' || buf[0] == 'k'); - if (buf[0] == 'k') - continue; /* keepalive */ - memcpy(&rec_start_lsn, &buf[XLOG_HDR_START_POS], - sizeof rec_start_lsn); - rec_start_lsn = pg_ntoh64(rec_start_lsn); - rec_end_lsn = rec_start_lsn + len - XLOG_HDR_SIZE; - - /* write WAL to disk */ - XLogWalPropWrite(&buf[XLOG_HDR_SIZE], len - XLOG_HDR_SIZE, rec_start_lsn); - - ereport(DEBUG1, - (errmsg("Recover message %X/%X length %d", - LSN_FORMAT_ARGS(rec_start_lsn), len))); - if (rec_end_lsn >= endpos) - break; - } - } - ereport(LOG, - (errmsg("end of replication stream at %X/%X: %m", - LSN_FORMAT_ARGS(rec_end_lsn)))); - walrcv_disconnect(wrconn); - - /* failed to receive all WAL till endpos */ - if (rec_end_lsn < endpos) - return false; - } - else - { - ereport(LOG, - (errmsg("primary server contains no more WAL on requested timeline %u LSN %X/%08X", - timeline, (uint32) (startpos >> 32), (uint32) startpos))); - return false; - } - - return true; -} - /* * Determine for sk the starting streaming point and send it message * 1) Announcing we are elected proposer (which immediately advances epoch if @@ -1550,6 +1084,7 @@ WalProposerRecovery(int donor, TimeLineID timeline, XLogRecPtr startpos, XLogRec static void SendProposerElected(Safekeeper *sk) { + WalProposer *wp = sk->wp; ProposerElected msg; TermHistory *th; term_t lastCommonTerm; @@ -1567,22 +1102,22 @@ SendProposerElected(Safekeeper *sk) th = &sk->voteResponse.termHistory; /* We must start somewhere. */ - Assert(propTermHistory.n_entries >= 1); + Assert(wp->propTermHistory.n_entries >= 1); - for (i = 0; i < Min(propTermHistory.n_entries, th->n_entries); i++) + for (i = 0; i < Min(wp->propTermHistory.n_entries, th->n_entries); i++) { - if (propTermHistory.entries[i].term != th->entries[i].term) + if (wp->propTermHistory.entries[i].term != th->entries[i].term) break; /* term must begin everywhere at the same point */ - Assert(propTermHistory.entries[i].lsn == th->entries[i].lsn); + Assert(wp->propTermHistory.entries[i].lsn == th->entries[i].lsn); } i--; /* step back to the last common term */ if (i < 0) { /* safekeeper is empty or no common point, start from the beginning */ - sk->startStreamingAt = propTermHistory.entries[0].lsn; + sk->startStreamingAt = wp->propTermHistory.entries[0].lsn; - if (sk->startStreamingAt < truncateLsn) + if (sk->startStreamingAt < wp->truncateLsn) { /* * There's a gap between the WAL starting point and a truncateLsn, @@ -1603,10 +1138,10 @@ SendProposerElected(Safekeeper *sk) * safekeeper, and it's aligned to the WAL record, so we can * safely start streaming from this point. */ - sk->startStreamingAt = truncateLsn; + sk->startStreamingAt = wp->truncateLsn; elog(WARNING, "empty safekeeper joined cluster as %s:%s, historyStart=%X/%X, sk->startStreamingAt=%X/%X", - sk->host, sk->port, LSN_FORMAT_ARGS(propTermHistory.entries[0].lsn), + sk->host, sk->port, LSN_FORMAT_ARGS(wp->propTermHistory.entries[0].lsn), LSN_FORMAT_ARGS(sk->startStreamingAt)); } } @@ -1618,28 +1153,28 @@ SendProposerElected(Safekeeper *sk) * proposer, LSN it is currently writing, but then we just pick * safekeeper pos as it obviously can't be higher. */ - if (propTermHistory.entries[i].term == propTerm) + if (wp->propTermHistory.entries[i].term == wp->propTerm) { sk->startStreamingAt = sk->voteResponse.flushLsn; } else { - XLogRecPtr propEndLsn = propTermHistory.entries[i + 1].lsn; + XLogRecPtr propEndLsn = wp->propTermHistory.entries[i + 1].lsn; XLogRecPtr skEndLsn = (i + 1 < th->n_entries ? th->entries[i + 1].lsn : sk->voteResponse.flushLsn); sk->startStreamingAt = Min(propEndLsn, skEndLsn); } } - Assert(sk->startStreamingAt >= truncateLsn && sk->startStreamingAt <= availableLsn); + Assert(sk->startStreamingAt >= wp->truncateLsn && sk->startStreamingAt <= wp->availableLsn); msg.tag = 'e'; - msg.term = propTerm; + msg.term = wp->propTerm; msg.startStreamingAt = sk->startStreamingAt; - msg.termHistory = &propTermHistory; - msg.timelineStartLsn = timelineStartLsn; + msg.termHistory = &wp->propTermHistory; + msg.timelineStartLsn = wp->timelineStartLsn; - lastCommonTerm = i >= 0 ? propTermHistory.entries[i].term : 0; + lastCommonTerm = i >= 0 ? wp->propTermHistory.entries[i].term : 0; elog(LOG, "sending elected msg to node " UINT64_FORMAT " term=" UINT64_FORMAT ", startStreamingAt=%X/%X (lastCommonTerm=" UINT64_FORMAT "), termHistory.n_entries=%u to %s:%s, timelineStartLsn=%X/%X", sk->greetResponse.nodeId, msg.term, LSN_FORMAT_ARGS(msg.startStreamingAt), lastCommonTerm, msg.termHistory->n_entries, sk->host, sk->port, LSN_FORMAT_ARGS(msg.timelineStartLsn)); @@ -1662,22 +1197,6 @@ SendProposerElected(Safekeeper *sk) StartStreaming(sk); } -/* - * Start walsender streaming replication - */ -static void -WalProposerStartStreaming(XLogRecPtr startpos) -{ - StartReplicationCmd cmd; - - elog(LOG, "WAL proposer starts streaming at %X/%X", - LSN_FORMAT_ARGS(startpos)); - cmd.slotname = WAL_PROPOSER_SLOT_NAME; - cmd.timeline = greetRequest.timeline; - cmd.startpoint = startpos; - StartProposerReplication(&cmd); -} - /* * Start streaming to safekeeper sk, always updates state to SS_ACTIVE and sets * correct event set. @@ -1719,25 +1238,25 @@ SendMessageToNode(Safekeeper *sk) * Broadcast new message to all caught-up safekeepers */ static void -BroadcastAppendRequest() +BroadcastAppendRequest(WalProposer *wp) { - for (int i = 0; i < n_safekeepers; i++) - if (safekeeper[i].state == SS_ACTIVE) - SendMessageToNode(&safekeeper[i]); + for (int i = 0; i < wp->n_safekeepers; i++) + if (wp->safekeeper[i].state == SS_ACTIVE) + SendMessageToNode(&wp->safekeeper[i]); } static void -PrepareAppendRequest(AppendRequestHeader * req, XLogRecPtr beginLsn, XLogRecPtr endLsn) +PrepareAppendRequest(WalProposer *wp, AppendRequestHeader *req, XLogRecPtr beginLsn, XLogRecPtr endLsn) { Assert(endLsn >= beginLsn); req->tag = 'a'; - req->term = propTerm; - req->epochStartLsn = propEpochStartLsn; + req->term = wp->propTerm; + req->epochStartLsn = wp->propEpochStartLsn; req->beginLsn = beginLsn; req->endLsn = endLsn; - req->commitLsn = GetAcknowledgedByQuorumWALPosition(); - req->truncateLsn = truncateLsn; - req->proposerId = greetRequest.proposerId; + req->commitLsn = GetAcknowledgedByQuorumWALPosition(wp); + req->truncateLsn = wp->truncateLsn; + req->proposerId = wp->greetRequest.proposerId; } /* @@ -1746,6 +1265,8 @@ PrepareAppendRequest(AppendRequestHeader * req, XLogRecPtr beginLsn, XLogRecPtr static void HandleActiveState(Safekeeper *sk, uint32 events) { + WalProposer *wp = sk->wp; + uint32 newEvents = WL_SOCKET_READABLE; if (events & WL_SOCKET_WRITEABLE) @@ -1765,10 +1286,10 @@ HandleActiveState(Safekeeper *sk, uint32 events) * after arrival. But it's good to have it here in case we change this * behavior in the future. */ - if (sk->streamingAt != availableLsn || sk->flushWrite) + if (sk->streamingAt != wp->availableLsn || sk->flushWrite) newEvents |= WL_SOCKET_WRITEABLE; - UpdateEventSet(sk, newEvents); + wp->api.update_event_set(sk, newEvents); } /* @@ -1783,10 +1304,10 @@ HandleActiveState(Safekeeper *sk, uint32 events) static bool SendAppendRequests(Safekeeper *sk) { + WalProposer *wp = sk->wp; XLogRecPtr endLsn; AppendRequestHeader *req; PGAsyncWriteResult writeResult; - WALReadError errinfo; bool sentAnything = false; if (sk->flushWrite) @@ -1803,7 +1324,7 @@ SendAppendRequests(Safekeeper *sk) sk->flushWrite = false; } - while (sk->streamingAt != availableLsn || !sentAnything) + while (sk->streamingAt != wp->availableLsn || !sentAnything) { sentAnything = true; @@ -1811,13 +1332,13 @@ SendAppendRequests(Safekeeper *sk) endLsn += MAX_SEND_SIZE; /* if we went beyond available WAL, back off */ - if (endLsn > availableLsn) + if (endLsn > wp->availableLsn) { - endLsn = availableLsn; + endLsn = wp->availableLsn; } req = &sk->appendRequest; - PrepareAppendRequest(&sk->appendRequest, sk->streamingAt, endLsn); + PrepareAppendRequest(sk->wp, &sk->appendRequest, sk->streamingAt, endLsn); ereport(DEBUG2, (errmsg("sending message len %ld beginLsn=%X/%X endLsn=%X/%X commitLsn=%X/%X truncateLsn=%X/%X to %s:%s", @@ -1825,7 +1346,7 @@ SendAppendRequests(Safekeeper *sk) LSN_FORMAT_ARGS(req->beginLsn), LSN_FORMAT_ARGS(req->endLsn), LSN_FORMAT_ARGS(req->commitLsn), - LSN_FORMAT_ARGS(truncateLsn), sk->host, sk->port))); + LSN_FORMAT_ARGS(wp->truncateLsn), sk->host, sk->port))); resetStringInfo(&sk->outbuf); @@ -1834,23 +1355,14 @@ SendAppendRequests(Safekeeper *sk) /* write the WAL itself */ enlargeStringInfo(&sk->outbuf, req->endLsn - req->beginLsn); - if (!WALRead(sk->xlogreader, - &sk->outbuf.data[sk->outbuf.len], - req->beginLsn, - req->endLsn - req->beginLsn, -#if PG_VERSION_NUM >= 150000 - /* FIXME don't use hardcoded timeline_id here */ - 1, -#else - ThisTimeLineID, -#endif - &errinfo)) - { - WALReadRaiseError(&errinfo); - } + /* wal_read will raise error on failure */ + wp->api.wal_read(sk->xlogreader, + &sk->outbuf.data[sk->outbuf.len], + req->beginLsn, + req->endLsn - req->beginLsn); sk->outbuf.len += req->endLsn - req->beginLsn; - writeResult = walprop_async_write(sk->conn, sk->outbuf.data, sk->outbuf.len); + writeResult = wp->api.conn_async_write(sk->conn, sk->outbuf.data, sk->outbuf.len); /* Mark current message as sent, whatever the result is */ sk->streamingAt = endLsn; @@ -1874,7 +1386,7 @@ SendAppendRequests(Safekeeper *sk) case PG_ASYNC_WRITE_FAIL: elog(WARNING, "Failed to send to node %s:%s in %s state: %s", sk->host, sk->port, FormatSafekeeperState(sk->state), - walprop_error_message(sk->conn)); + wp->api.conn_error_message(sk->conn)); ShutdownConnection(sk); return false; default: @@ -1897,6 +1409,7 @@ SendAppendRequests(Safekeeper *sk) static bool RecvAppendResponses(Safekeeper *sk) { + WalProposer *wp = sk->wp; XLogRecPtr minQuorumLsn; bool readAnything = false; @@ -1908,7 +1421,7 @@ RecvAppendResponses(Safekeeper *sk) * work until later. */ sk->appendResponse.apm.tag = 'a'; - if (!AsyncReadMessage(sk, (AcceptorProposerMessage *) & sk->appendResponse)) + if (!AsyncReadMessage(sk, (AcceptorProposerMessage *) &sk->appendResponse)) break; ereport(DEBUG2, @@ -1918,12 +1431,12 @@ RecvAppendResponses(Safekeeper *sk) LSN_FORMAT_ARGS(sk->appendResponse.commitLsn), sk->host, sk->port))); - if (sk->appendResponse.term > propTerm) + if (sk->appendResponse.term > wp->propTerm) { /* Another compute with higher term is running. */ elog(PANIC, "WAL acceptor %s:%s with term " INT64_FORMAT " rejected our request, our term " INT64_FORMAT "", sk->host, sk->port, - sk->appendResponse.term, propTerm); + sk->appendResponse.term, wp->propTerm); } readAnything = true; @@ -1932,16 +1445,16 @@ RecvAppendResponses(Safekeeper *sk) if (!readAnything) return sk->state == SS_ACTIVE; - HandleSafekeeperResponse(); + HandleSafekeeperResponse(wp); /* * Also send the new commit lsn to all the safekeepers. */ - minQuorumLsn = GetAcknowledgedByQuorumWALPosition(); - if (minQuorumLsn > lastSentCommitLsn) + minQuorumLsn = GetAcknowledgedByQuorumWALPosition(wp); + if (minQuorumLsn > wp->lastSentCommitLsn) { - BroadcastAppendRequest(); - lastSentCommitLsn = minQuorumLsn; + BroadcastAppendRequest(wp); + wp->lastSentCommitLsn = minQuorumLsn; } return sk->state == SS_ACTIVE; @@ -1949,7 +1462,7 @@ RecvAppendResponses(Safekeeper *sk) /* Parse a PageserverFeedback message, or the PageserverFeedback part of an AppendResponse */ void -ParsePageserverFeedbackMessage(StringInfo reply_message, PageserverFeedback * rf) +ParsePageserverFeedbackMessage(StringInfo reply_message, PageserverFeedback *rf) { uint8 nkeys; int i; @@ -2025,56 +1538,20 @@ ParsePageserverFeedbackMessage(StringInfo reply_message, PageserverFeedback * rf } } -/* - * Combine hot standby feedbacks from all safekeepers. - */ -static void -CombineHotStanbyFeedbacks(HotStandbyFeedback * hs) -{ - hs->ts = 0; - hs->xmin.value = ~0; /* largest unsigned value */ - hs->catalog_xmin.value = ~0; /* largest unsigned value */ - - for (int i = 0; i < n_safekeepers; i++) - { - if (safekeeper[i].appendResponse.hs.ts != 0) - { - HotStandbyFeedback *skhs = &safekeeper[i].appendResponse.hs; - if (FullTransactionIdIsNormal(skhs->xmin) - && FullTransactionIdPrecedes(skhs->xmin, hs->xmin)) - { - hs->xmin = skhs->xmin; - hs->ts = skhs->ts; - } - if (FullTransactionIdIsNormal(skhs->catalog_xmin) - && FullTransactionIdPrecedes(skhs->catalog_xmin, hs->xmin)) - { - hs->catalog_xmin = skhs->catalog_xmin; - hs->ts = skhs->ts; - } - } - } - - if (hs->xmin.value == ~0) - hs->xmin = InvalidFullTransactionId; - if (hs->catalog_xmin.value == ~0) - hs->catalog_xmin = InvalidFullTransactionId; -} - /* * Get minimum of flushed LSNs of all safekeepers, which is the LSN of the * last WAL record that can be safely discarded. */ static XLogRecPtr -CalculateMinFlushLsn(void) +CalculateMinFlushLsn(WalProposer *wp) { - XLogRecPtr lsn = n_safekeepers > 0 - ? safekeeper[0].appendResponse.flushLsn - : InvalidXLogRecPtr; + XLogRecPtr lsn = wp->n_safekeepers > 0 + ? wp->safekeeper[0].appendResponse.flushLsn + : InvalidXLogRecPtr; - for (int i = 1; i < n_safekeepers; i++) + for (int i = 1; i < wp->n_safekeepers; i++) { - lsn = Min(lsn, safekeeper[i].appendResponse.flushLsn); + lsn = Min(lsn, wp->safekeeper[i].appendResponse.flushLsn); } return lsn; } @@ -2083,163 +1560,37 @@ CalculateMinFlushLsn(void) * Calculate WAL position acknowledged by quorum */ static XLogRecPtr -GetAcknowledgedByQuorumWALPosition(void) +GetAcknowledgedByQuorumWALPosition(WalProposer *wp) { XLogRecPtr responses[MAX_SAFEKEEPERS]; /* * Sort acknowledged LSNs */ - for (int i = 0; i < n_safekeepers; i++) + for (int i = 0; i < wp->n_safekeepers; i++) { /* * Like in Raft, we aren't allowed to commit entries from previous * terms, so ignore reported LSN until it gets to epochStartLsn. */ - responses[i] = safekeeper[i].appendResponse.flushLsn >= propEpochStartLsn ? safekeeper[i].appendResponse.flushLsn : 0; + responses[i] = wp->safekeeper[i].appendResponse.flushLsn >= wp->propEpochStartLsn ? wp->safekeeper[i].appendResponse.flushLsn : 0; } - qsort(responses, n_safekeepers, sizeof(XLogRecPtr), CompareLsn); + qsort(responses, wp->n_safekeepers, sizeof(XLogRecPtr), CompareLsn); /* * Get the smallest LSN committed by quorum */ - return responses[n_safekeepers - quorum]; -} - -/* - * WalproposerShmemSize --- report amount of shared memory space needed - */ -Size -WalproposerShmemSize(void) -{ - return sizeof(WalproposerShmemState); -} - -bool -WalproposerShmemInit(void) -{ - bool found; - - LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); - walprop_shared = ShmemInitStruct("Walproposer shared state", - sizeof(WalproposerShmemState), - &found); - - if (!found) - { - memset(walprop_shared, 0, WalproposerShmemSize()); - SpinLockInit(&walprop_shared->mutex); - pg_atomic_init_u64(&walprop_shared->backpressureThrottlingTime, 0); - } - LWLockRelease(AddinShmemInitLock); - - return found; -} - -void -replication_feedback_set(PageserverFeedback * rf) -{ - SpinLockAcquire(&walprop_shared->mutex); - memcpy(&walprop_shared->feedback, rf, sizeof(PageserverFeedback)); - SpinLockRelease(&walprop_shared->mutex); -} - -void -replication_feedback_get_lsns(XLogRecPtr *writeLsn, XLogRecPtr *flushLsn, XLogRecPtr *applyLsn) -{ - SpinLockAcquire(&walprop_shared->mutex); - *writeLsn = walprop_shared->feedback.last_received_lsn; - *flushLsn = walprop_shared->feedback.disk_consistent_lsn; - *applyLsn = walprop_shared->feedback.remote_consistent_lsn; - SpinLockRelease(&walprop_shared->mutex); -} - -/* - * Get PageserverFeedback fields from the most advanced safekeeper - */ -static void -GetLatestNeonFeedback(PageserverFeedback * rf) -{ - int latest_safekeeper = 0; - XLogRecPtr last_received_lsn = InvalidXLogRecPtr; - - for (int i = 0; i < n_safekeepers; i++) - { - if (safekeeper[i].appendResponse.rf.last_received_lsn > last_received_lsn) - { - latest_safekeeper = i; - last_received_lsn = safekeeper[i].appendResponse.rf.last_received_lsn; - } - } - - rf->currentClusterSize = safekeeper[latest_safekeeper].appendResponse.rf.currentClusterSize; - rf->last_received_lsn = safekeeper[latest_safekeeper].appendResponse.rf.last_received_lsn; - rf->disk_consistent_lsn = safekeeper[latest_safekeeper].appendResponse.rf.disk_consistent_lsn; - rf->remote_consistent_lsn = safekeeper[latest_safekeeper].appendResponse.rf.remote_consistent_lsn; - rf->replytime = safekeeper[latest_safekeeper].appendResponse.rf.replytime; - - elog(DEBUG2, "GetLatestNeonFeedback: currentClusterSize %lu," - " last_received_lsn %X/%X, disk_consistent_lsn %X/%X, remote_consistent_lsn %X/%X, replytime %lu", - rf->currentClusterSize, - LSN_FORMAT_ARGS(rf->last_received_lsn), - LSN_FORMAT_ARGS(rf->disk_consistent_lsn), - LSN_FORMAT_ARGS(rf->remote_consistent_lsn), - rf->replytime); - - replication_feedback_set(rf); + return responses[wp->n_safekeepers - wp->quorum]; } static void -HandleSafekeeperResponse(void) +HandleSafekeeperResponse(WalProposer *wp) { - HotStandbyFeedback hsFeedback; XLogRecPtr minQuorumLsn; - XLogRecPtr diskConsistentLsn; XLogRecPtr minFlushLsn; - minQuorumLsn = GetAcknowledgedByQuorumWALPosition(); - diskConsistentLsn = quorumFeedback.rf.disk_consistent_lsn; - - if (!syncSafekeepers) - { - /* Get PageserverFeedback fields from the most advanced safekeeper */ - GetLatestNeonFeedback(&quorumFeedback.rf); - SetZenithCurrentClusterSize(quorumFeedback.rf.currentClusterSize); - } - - if (minQuorumLsn > quorumFeedback.flushLsn || diskConsistentLsn != quorumFeedback.rf.disk_consistent_lsn) - { - - if (minQuorumLsn > quorumFeedback.flushLsn) - quorumFeedback.flushLsn = minQuorumLsn; - - /* advance the replication slot */ - if (!syncSafekeepers) - ProcessStandbyReply( - /* write_lsn - This is what durably stored in WAL service. */ - quorumFeedback.flushLsn, - /* flush_lsn - This is what durably stored in WAL service. */ - quorumFeedback.flushLsn, - - /* - * apply_lsn - This is what processed and durably saved at* - * pageserver. - */ - quorumFeedback.rf.disk_consistent_lsn, - GetCurrentTimestamp(), false); - } - - CombineHotStanbyFeedbacks(&hsFeedback); - if (hsFeedback.ts != 0 && memcmp(&hsFeedback, &quorumFeedback.hs, sizeof hsFeedback) != 0) - { - quorumFeedback.hs = hsFeedback; - if (!syncSafekeepers) - ProcessStandbyHSFeedback(hsFeedback.ts, - XidFromFullTransactionId(hsFeedback.xmin), - EpochFromFullTransactionId(hsFeedback.xmin), - XidFromFullTransactionId(hsFeedback.catalog_xmin), - EpochFromFullTransactionId(hsFeedback.catalog_xmin)); - } + minQuorumLsn = GetAcknowledgedByQuorumWALPosition(wp); + wp->api.process_safekeeper_feedback(wp, minQuorumLsn); /* * Try to advance truncateLsn to minFlushLsn, which is the last record @@ -2255,17 +1606,16 @@ HandleSafekeeperResponse(void) * term' in Raft); 2) chunks we read from WAL and send are plain sheets of * bytes, but safekeepers ack only on record boundaries. */ - minFlushLsn = CalculateMinFlushLsn(); - if (minFlushLsn > truncateLsn) + minFlushLsn = CalculateMinFlushLsn(wp); + if (minFlushLsn > wp->truncateLsn) { - truncateLsn = minFlushLsn; + wp->truncateLsn = minFlushLsn; /* * Advance the replication slot to free up old WAL files. Note that * slot doesn't exist if we are in syncSafekeepers mode. */ - if (MyReplicationSlot) - PhysicalConfirmReceivedLocation(truncateLsn); + wp->api.confirm_wal_streamed(wp->truncateLsn); } /* @@ -2280,15 +1630,15 @@ HandleSafekeeperResponse(void) * (due to pageserver connecting to not-synced-safekeeper) we currently * wait for all seemingly alive safekeepers to get synced. */ - if (syncSafekeepers) + if (wp->config->syncSafekeepers) { int n_synced; n_synced = 0; - for (int i = 0; i < n_safekeepers; i++) + for (int i = 0; i < wp->n_safekeepers; i++) { - Safekeeper *sk = &safekeeper[i]; - bool synced = sk->appendResponse.commitLsn >= propEpochStartLsn; + Safekeeper *sk = &wp->safekeeper[i]; + bool synced = sk->appendResponse.commitLsn >= wp->propEpochStartLsn; /* alive safekeeper which is not synced yet; wait for it */ if (sk->state != SS_OFFLINE && !synced) @@ -2297,23 +1647,23 @@ HandleSafekeeperResponse(void) n_synced++; } - if (n_synced >= quorum) + if (n_synced >= wp->quorum) { /* A quorum of safekeepers has been synced! */ - - /* - * Send empty message to broadcast latest truncateLsn to all safekeepers. - * This helps to finish next sync-safekeepers eailier, by skipping recovery - * step. - * - * We don't need to wait for response because it doesn't affect correctness, - * and TCP should be able to deliver the message to safekeepers in case of - * network working properly. - */ - BroadcastAppendRequest(); - fprintf(stdout, "%X/%X\n", LSN_FORMAT_ARGS(propEpochStartLsn)); - exit(0); + /* + * Send empty message to broadcast latest truncateLsn to all + * safekeepers. This helps to finish next sync-safekeepers + * eailier, by skipping recovery step. + * + * We don't need to wait for response because it doesn't affect + * correctness, and TCP should be able to deliver the message to + * safekeepers in case of network working properly. + */ + BroadcastAppendRequest(wp); + + wp->api.finish_sync_safekeepers(wp->propEpochStartLsn); + /* unreachable */ } } } @@ -2325,7 +1675,9 @@ HandleSafekeeperResponse(void) static bool AsyncRead(Safekeeper *sk, char **buf, int *buf_size) { - switch (walprop_async_read(sk->conn, buf, buf_size)) + WalProposer *wp = sk->wp; + + switch (wp->api.conn_async_read(sk->conn, buf, buf_size)) { case PG_ASYNC_READ_SUCCESS: return true; @@ -2337,7 +1689,7 @@ AsyncRead(Safekeeper *sk, char **buf, int *buf_size) case PG_ASYNC_READ_FAIL: elog(WARNING, "Failed to read from node %s:%s in %s state: %s", sk->host, sk->port, FormatSafekeeperState(sk->state), - walprop_error_message(sk->conn)); + wp->api.conn_error_message(sk->conn)); ShutdownConnection(sk); return false; } @@ -2355,8 +1707,10 @@ AsyncRead(Safekeeper *sk, char **buf, int *buf_size) * failed, a warning is emitted and the connection is reset. */ static bool -AsyncReadMessage(Safekeeper *sk, AcceptorProposerMessage * anymsg) +AsyncReadMessage(Safekeeper *sk, AcceptorProposerMessage *anymsg) { + WalProposer *wp = sk->wp; + char *buf; int buf_size; uint64 tag; @@ -2378,7 +1732,7 @@ AsyncReadMessage(Safekeeper *sk, AcceptorProposerMessage * anymsg) ResetConnection(sk); return false; } - sk->latestMsgReceivedAt = GetCurrentTimestamp(); + sk->latestMsgReceivedAt = wp->api.get_current_timestamp(); switch (tag) { case 'g': @@ -2444,13 +1798,14 @@ AsyncReadMessage(Safekeeper *sk, AcceptorProposerMessage * anymsg) static bool BlockingWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState success_state) { + WalProposer *wp = sk->wp; uint32 events; - if (!walprop_blocking_write(sk->conn, msg, msg_size)) + if (!wp->api.conn_blocking_write(sk->conn, msg, msg_size)) { elog(WARNING, "Failed to send to node %s:%s in %s state: %s", sk->host, sk->port, FormatSafekeeperState(sk->state), - walprop_error_message(sk->conn)); + wp->api.conn_error_message(sk->conn)); ShutdownConnection(sk); return false; } @@ -2463,7 +1818,7 @@ BlockingWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState succes */ events = SafekeeperStateDesiredEvents(success_state); if (events) - UpdateEventSet(sk, events); + wp->api.update_event_set(sk, events); return true; } @@ -2478,7 +1833,9 @@ BlockingWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState succes static bool AsyncWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState flush_state) { - switch (walprop_async_write(sk->conn, msg, msg_size)) + WalProposer *wp = sk->wp; + + switch (wp->api.conn_async_write(sk->conn, msg, msg_size)) { case PG_ASYNC_WRITE_SUCCESS: return true; @@ -2490,12 +1847,12 @@ AsyncWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState flush_sta * this function */ sk->state = flush_state; - UpdateEventSet(sk, WL_SOCKET_READABLE | WL_SOCKET_WRITEABLE); + wp->api.update_event_set(sk, WL_SOCKET_READABLE | WL_SOCKET_WRITEABLE); return false; case PG_ASYNC_WRITE_FAIL: elog(WARNING, "Failed to send to node %s:%s in %s state: %s", sk->host, sk->port, FormatSafekeeperState(sk->state), - walprop_error_message(sk->conn)); + wp->api.conn_error_message(sk->conn)); ShutdownConnection(sk); return false; default: @@ -2515,13 +1872,15 @@ AsyncWrite(Safekeeper *sk, void *msg, size_t msg_size, SafekeeperState flush_sta static bool AsyncFlush(Safekeeper *sk) { + WalProposer *wp = sk->wp; + /*--- * PQflush returns: * 0 if successful [we're good to move on] * 1 if unable to send everything yet [call PQflush again] * -1 if it failed [emit an error] */ - switch (walprop_flush(sk->conn)) + switch (wp->api.conn_flush(sk->conn)) { case 0: /* flush is done */ @@ -2532,7 +1891,7 @@ AsyncFlush(Safekeeper *sk) case -1: elog(WARNING, "Failed to flush write to node %s:%s in %s state: %s", sk->host, sk->port, FormatSafekeeperState(sk->state), - walprop_error_message(sk->conn)); + wp->api.conn_error_message(sk->conn)); ResetConnection(sk); return false; default: @@ -2541,88 +1900,210 @@ AsyncFlush(Safekeeper *sk) } } -/* Check if we need to suspend inserts because of lagging replication. */ -static uint64 -backpressure_lag_impl(void) +static int +CompareLsn(const void *a, const void *b) { - if (max_replication_apply_lag > 0 || max_replication_flush_lag > 0 || max_replication_write_lag > 0) - { - XLogRecPtr writePtr; - XLogRecPtr flushPtr; - XLogRecPtr applyPtr; -#if PG_VERSION_NUM >= 150000 - XLogRecPtr myFlushLsn = GetFlushRecPtr(NULL); -#else - XLogRecPtr myFlushLsn = GetFlushRecPtr(); -#endif - replication_feedback_get_lsns(&writePtr, &flushPtr, &applyPtr); -#define MB ((XLogRecPtr)1024 * 1024) + XLogRecPtr lsn1 = *((const XLogRecPtr *) a); + XLogRecPtr lsn2 = *((const XLogRecPtr *) b); - elog(DEBUG2, "current flushLsn %X/%X PageserverFeedback: write %X/%X flush %X/%X apply %X/%X", - LSN_FORMAT_ARGS(myFlushLsn), - LSN_FORMAT_ARGS(writePtr), - LSN_FORMAT_ARGS(flushPtr), - LSN_FORMAT_ARGS(applyPtr)); - - if ((writePtr != InvalidXLogRecPtr && max_replication_write_lag > 0 && myFlushLsn > writePtr + max_replication_write_lag * MB)) - { - return (myFlushLsn - writePtr - max_replication_write_lag * MB); - } - - if ((flushPtr != InvalidXLogRecPtr && max_replication_flush_lag > 0 && myFlushLsn > flushPtr + max_replication_flush_lag * MB)) - { - return (myFlushLsn - flushPtr - max_replication_flush_lag * MB); - } - - if ((applyPtr != InvalidXLogRecPtr && max_replication_apply_lag > 0 && myFlushLsn > applyPtr + max_replication_apply_lag * MB)) - { - return (myFlushLsn - applyPtr - max_replication_apply_lag * MB); - } - } - return 0; + if (lsn1 < lsn2) + return -1; + else if (lsn1 == lsn2) + return 0; + else + return 1; } -#define BACK_PRESSURE_DELAY 10000L // 0.01 sec - -static bool -backpressure_throttling_impl(void) +/* Returns a human-readable string corresonding to the SafekeeperState + * + * The string should not be freed. + * + * The strings are intended to be used as a prefix to "state", e.g.: + * + * elog(LOG, "currently in %s state", FormatSafekeeperState(sk->state)); + * + * If this sort of phrasing doesn't fit the message, instead use something like: + * + * elog(LOG, "currently in state [%s]", FormatSafekeeperState(sk->state)); + */ +static char * +FormatSafekeeperState(SafekeeperState state) { - int64 lag; - TimestampTz start, - stop; - bool retry = PrevProcessInterruptsCallback - ? PrevProcessInterruptsCallback() - : false; + char *return_val = NULL; + + switch (state) + { + case SS_OFFLINE: + return_val = "offline"; + break; + case SS_CONNECTING_READ: + case SS_CONNECTING_WRITE: + return_val = "connecting"; + break; + case SS_WAIT_EXEC_RESULT: + return_val = "receiving query result"; + break; + case SS_HANDSHAKE_RECV: + return_val = "handshake (receiving)"; + break; + case SS_VOTING: + return_val = "voting"; + break; + case SS_WAIT_VERDICT: + return_val = "wait-for-verdict"; + break; + case SS_SEND_ELECTED_FLUSH: + return_val = "send-announcement-flush"; + break; + case SS_IDLE: + return_val = "idle"; + break; + case SS_ACTIVE: + return_val = "active"; + break; + } + + Assert(return_val != NULL); + + return return_val; +} + +/* Asserts that the provided events are expected for given safekeeper's state */ +static void +AssertEventsOkForState(uint32 events, Safekeeper *sk) +{ + uint32 expected = SafekeeperStateDesiredEvents(sk->state); /* - * Don't throttle read only transactions or wal sender. - * Do throttle CREATE INDEX CONCURRENTLY, however. It performs some - * stages outside a transaction, even though it writes a lot of WAL. - * Check PROC_IN_SAFE_IC flag to cover that case. + * The events are in-line with what we're expecting, under two conditions: + * (a) if we aren't expecting anything, `events` has no read- or + * write-ready component. (b) if we are expecting something, there's + * overlap (i.e. `events & expected != 0`) */ - if (am_walsender - || (!(MyProc->statusFlags & PROC_IN_SAFE_IC) - && !TransactionIdIsValid(GetCurrentTransactionIdIfAny()))) - return retry; + bool events_ok_for_state; /* long name so the `Assert` is more + * clear later */ - /* Calculate replicas lag */ - lag = backpressure_lag_impl(); - if (lag == 0) - return retry; + if (expected == WL_NO_EVENTS) + events_ok_for_state = ((events & (WL_SOCKET_READABLE | WL_SOCKET_WRITEABLE)) == 0); + else + events_ok_for_state = ((events & expected) != 0); - /* Suspend writers until replicas catch up */ - set_ps_display("backpressure throttling"); - - elog(DEBUG2, "backpressure throttling: lag %lu", lag); - start = GetCurrentTimestamp(); - pg_usleep(BACK_PRESSURE_DELAY); - stop = GetCurrentTimestamp(); - pg_atomic_add_fetch_u64(&walprop_shared->backpressureThrottlingTime, stop - start); - return true; + if (!events_ok_for_state) + { + /* + * To give a descriptive message in the case of failure, we use elog + * and then an assertion that's guaranteed to fail. + */ + elog(WARNING, "events %s mismatched for safekeeper %s:%s in state [%s]", + FormatEvents(events), sk->host, sk->port, FormatSafekeeperState(sk->state)); + Assert(events_ok_for_state); + } } -uint64 -BackpressureThrottlingTime(void) +/* Returns the set of events a safekeeper in this state should be waiting on + * + * This will return WL_NO_EVENTS (= 0) for some events. */ +static uint32 +SafekeeperStateDesiredEvents(SafekeeperState state) { - return pg_atomic_read_u64(&walprop_shared->backpressureThrottlingTime); + uint32 result = WL_NO_EVENTS; + + /* If the state doesn't have a modifier, we can check the base state */ + switch (state) + { + /* Connecting states say what they want in the name */ + case SS_CONNECTING_READ: + result = WL_SOCKET_READABLE; + break; + case SS_CONNECTING_WRITE: + result = WL_SOCKET_WRITEABLE; + break; + + /* Reading states need the socket to be read-ready to continue */ + case SS_WAIT_EXEC_RESULT: + case SS_HANDSHAKE_RECV: + case SS_WAIT_VERDICT: + result = WL_SOCKET_READABLE; + break; + + /* + * Idle states use read-readiness as a sign that the connection + * has been disconnected. + */ + case SS_VOTING: + case SS_IDLE: + result = WL_SOCKET_READABLE; + break; + + /* + * Flush states require write-ready for flushing. Active state + * does both reading and writing. + * + * TODO: SS_ACTIVE sometimes doesn't need to be write-ready. We + * should check sk->flushWrite here to set WL_SOCKET_WRITEABLE. + */ + case SS_SEND_ELECTED_FLUSH: + case SS_ACTIVE: + result = WL_SOCKET_READABLE | WL_SOCKET_WRITEABLE; + break; + + /* The offline state expects no events. */ + case SS_OFFLINE: + result = WL_NO_EVENTS; + break; + + default: + Assert(false); + break; + } + + return result; +} + +/* Returns a human-readable string corresponding to the event set + * + * If the events do not correspond to something set as the `events` field of a `WaitEvent`, the + * returned string may be meaingless. + * + * The string should not be freed. It should also not be expected to remain the same between + * function calls. */ +static char * +FormatEvents(uint32 events) +{ + static char return_str[8]; + + /* Helper variable to check if there's extra bits */ + uint32 all_flags = WL_LATCH_SET + | WL_SOCKET_READABLE + | WL_SOCKET_WRITEABLE + | WL_TIMEOUT + | WL_POSTMASTER_DEATH + | WL_EXIT_ON_PM_DEATH + | WL_SOCKET_CONNECTED; + + /* + * The formatting here isn't supposed to be *particularly* useful -- it's + * just to give an sense of what events have been triggered without + * needing to remember your powers of two. + */ + + return_str[0] = (events & WL_LATCH_SET) ? 'L' : '_'; + return_str[1] = (events & WL_SOCKET_READABLE) ? 'R' : '_'; + return_str[2] = (events & WL_SOCKET_WRITEABLE) ? 'W' : '_'; + return_str[3] = (events & WL_TIMEOUT) ? 'T' : '_'; + return_str[4] = (events & WL_POSTMASTER_DEATH) ? 'D' : '_'; + return_str[5] = (events & WL_EXIT_ON_PM_DEATH) ? 'E' : '_'; + return_str[5] = (events & WL_SOCKET_CONNECTED) ? 'C' : '_'; + + if (events & (~all_flags)) + { + elog(WARNING, "Event formatting found unexpected component %d", + events & (~all_flags)); + return_str[6] = '*'; + return_str[7] = '\0'; + } + else + return_str[6] = '\0'; + + return (char *) &return_str; } diff --git a/pgxn/neon/walproposer.h b/pgxn/neon/walproposer.h index fa1ba30a8f..a1a9ccdfdd 100644 --- a/pgxn/neon/walproposer.h +++ b/pgxn/neon/walproposer.h @@ -1,8 +1,8 @@ #ifndef __NEON_WALPROPOSER_H__ #define __NEON_WALPROPOSER_H__ -#include "access/xlogdefs.h" #include "postgres.h" +#include "access/xlogdefs.h" #include "port.h" #include "access/xlog_internal.h" #include "access/transam.h" @@ -16,29 +16,15 @@ #define MAX_SAFEKEEPERS 32 #define MAX_SEND_SIZE (XLOG_BLCKSZ * 16) /* max size of a single* WAL * message */ -#define XLOG_HDR_SIZE (1 + 8 * 3) /* 'w' + startPos + walEnd + timestamp */ -#define XLOG_HDR_START_POS 1 /* offset of start position in wal sender* - * message header */ -#define XLOG_HDR_END_POS (1 + 8) /* offset of end position in wal sender* - * message header */ - /* * In the spirit of WL_SOCKET_READABLE and others, this corresponds to no events having occurred, * because all WL_* events are given flags equal to some (1 << i), starting from i = 0 */ #define WL_NO_EVENTS 0 -extern char *wal_acceptors_list; -extern int wal_acceptor_reconnect_timeout; -extern int wal_acceptor_connection_timeout; -extern bool am_wal_proposer; - -struct WalProposerConn; /* Defined in libpqwalproposer */ +struct WalProposerConn; /* Defined in implementation (walprop_pg.c) */ typedef struct WalProposerConn WalProposerConn; -struct WalMessage; -typedef struct WalMessage WalMessage; - /* Possible return values from ReadPGAsync */ typedef enum { @@ -52,7 +38,7 @@ typedef enum PG_ASYNC_READ_TRY_AGAIN, /* Reading failed. Check PQerrorMessage(conn) */ PG_ASYNC_READ_FAIL, -} PGAsyncReadResult; +} PGAsyncReadResult; /* Possible return values from WritePGAsync */ typedef enum @@ -71,7 +57,7 @@ typedef enum PG_ASYNC_WRITE_TRY_FLUSH, /* Writing failed. Check PQerrorMessage(conn) */ PG_ASYNC_WRITE_FAIL, -} PGAsyncWriteResult; +} PGAsyncWriteResult; /* * WAL safekeeper state, which is used to wait for some event. @@ -147,7 +133,7 @@ typedef enum * to read. */ SS_ACTIVE, -} SafekeeperState; +} SafekeeperState; /* Consensus logical timestamp. */ typedef uint64 term_t; @@ -171,12 +157,12 @@ typedef struct ProposerGreeting uint8 tenant_id[16]; TimeLineID timeline; uint32 walSegSize; -} ProposerGreeting; +} ProposerGreeting; typedef struct AcceptorProposerMessage { uint64 tag; -} AcceptorProposerMessage; +} AcceptorProposerMessage; /* * Acceptor -> Proposer initial response: the highest term acceptor voted for. @@ -186,7 +172,7 @@ typedef struct AcceptorGreeting AcceptorProposerMessage apm; term_t term; NNodeId nodeId; -} AcceptorGreeting; +} AcceptorGreeting; /* * Proposer -> Acceptor vote request. @@ -196,20 +182,20 @@ typedef struct VoteRequest uint64 tag; term_t term; pg_uuid_t proposerId; /* for monitoring/debugging */ -} VoteRequest; +} VoteRequest; /* Element of term switching chain. */ typedef struct TermSwitchEntry { term_t term; XLogRecPtr lsn; -} TermSwitchEntry; +} TermSwitchEntry; typedef struct TermHistory { uint32 n_entries; TermSwitchEntry *entries; -} TermHistory; +} TermHistory; /* Vote itself, sent from safekeeper to proposer */ typedef struct VoteResponse @@ -227,7 +213,7 @@ typedef struct VoteResponse * recovery of some safekeeper */ TermHistory termHistory; XLogRecPtr timelineStartLsn; /* timeline globally starts at this LSN */ -} VoteResponse; +} VoteResponse; /* * Proposer -> Acceptor message announcing proposer is elected and communicating @@ -243,7 +229,7 @@ typedef struct ProposerElected TermHistory *termHistory; /* timeline globally starts at this LSN */ XLogRecPtr timelineStartLsn; -} ProposerElected; +} ProposerElected; /* * Header of request with WAL message sent from proposer to safekeeper. @@ -268,7 +254,7 @@ typedef struct AppendRequestHeader */ XLogRecPtr truncateLsn; pg_uuid_t proposerId; /* for monitoring/debugging */ -} AppendRequestHeader; +} AppendRequestHeader; /* * Hot standby feedback received from replica @@ -278,7 +264,7 @@ typedef struct HotStandbyFeedback TimestampTz ts; FullTransactionId xmin; FullTransactionId catalog_xmin; -} HotStandbyFeedback; +} HotStandbyFeedback; typedef struct PageserverFeedback { @@ -289,7 +275,7 @@ typedef struct PageserverFeedback XLogRecPtr disk_consistent_lsn; XLogRecPtr remote_consistent_lsn; TimestampTz replytime; -} PageserverFeedback; +} PageserverFeedback; typedef struct WalproposerShmemState { @@ -297,7 +283,7 @@ typedef struct WalproposerShmemState PageserverFeedback feedback; term_t mineLastElectedTerm; pg_atomic_uint64 backpressureThrottlingTime; -} WalproposerShmemState; +} WalproposerShmemState; /* * Report safekeeper state to proposer @@ -321,17 +307,22 @@ typedef struct AppendResponse /* and custom neon feedback. */ /* This part of the message is extensible. */ PageserverFeedback rf; -} AppendResponse; +} AppendResponse; /* PageserverFeedback is extensible part of the message that is parsed separately */ /* Other fields are fixed part */ #define APPENDRESPONSE_FIXEDPART_SIZE offsetof(AppendResponse, rf) +struct WalProposer; +typedef struct WalProposer WalProposer; + /* * Descriptor of safekeeper */ typedef struct Safekeeper { + WalProposer *wp; + char const *host; char const *port; @@ -340,7 +331,7 @@ typedef struct Safekeeper * * May contain private information like password and should not be logged. */ - char conninfo[MAXCONNINFO]; + char conninfo[MAXCONNINFO]; /* * postgres protocol connection to the WAL acceptor @@ -373,27 +364,12 @@ typedef struct Safekeeper int eventPos; /* position in wait event set. Equal to -1 if* * no event */ SafekeeperState state; /* safekeeper state machine state */ - TimestampTz latestMsgReceivedAt; /* when latest msg is received */ + TimestampTz latestMsgReceivedAt; /* when latest msg is received */ AcceptorGreeting greetResponse; /* acceptor greeting */ VoteResponse voteResponse; /* the vote */ AppendResponse appendResponse; /* feedback for master */ } Safekeeper; -extern void PGDLLEXPORT WalProposerSync(int argc, char *argv[]); -extern void PGDLLEXPORT WalProposerMain(Datum main_arg); -extern void WalProposerBroadcast(XLogRecPtr startpos, XLogRecPtr endpos); -extern void WalProposerPoll(void); -extern void ParsePageserverFeedbackMessage(StringInfo reply_message, - PageserverFeedback *rf); -extern void StartProposerReplication(StartReplicationCmd *cmd); - -extern Size WalproposerShmemSize(void); -extern bool WalproposerShmemInit(void); -extern void replication_feedback_set(PageserverFeedback *rf); -extern void replication_feedback_get_lsns(XLogRecPtr *writeLsn, XLogRecPtr *flushLsn, XLogRecPtr *applyLsn); - -/* libpqwalproposer hooks & helper type */ - /* Re-exported PostgresPollingStatusType */ typedef enum { @@ -406,7 +382,7 @@ typedef enum * 'libpq-fe.h' still has PGRES_POLLING_ACTIVE, but says it's unused. * We've removed it here to avoid clutter. */ -} WalProposerConnectPollStatusType; +} WalProposerConnectPollStatusType; /* Re-exported and modified ExecStatusType */ typedef enum @@ -431,7 +407,7 @@ typedef enum WP_EXEC_NEEDS_INPUT, /* Catch-all failure. Check PQerrorMessage. */ WP_EXEC_FAILED, -} WalProposerExecStatusType; +} WalProposerExecStatusType; /* Re-exported ConnStatusType */ typedef enum @@ -445,67 +421,252 @@ typedef enum * that extra functionality, so we collect them into a single tag here. */ WP_CONNECTION_IN_PROGRESS, -} WalProposerConnStatusType; - -/* Re-exported PQerrorMessage */ -extern char *walprop_error_message(WalProposerConn *conn); - -/* Re-exported PQstatus */ -extern WalProposerConnStatusType walprop_status(WalProposerConn *conn); - -/* Re-exported PQconnectStart */ -extern WalProposerConn * walprop_connect_start(char *conninfo, char *password); - -/* Re-exported PQconectPoll */ -extern WalProposerConnectPollStatusType walprop_connect_poll(WalProposerConn *conn); - -/* Blocking wrapper around PQsendQuery */ -extern bool walprop_send_query(WalProposerConn *conn, char *query); - -/* Wrapper around PQconsumeInput + PQisBusy + PQgetResult */ -extern WalProposerExecStatusType walprop_get_query_result(WalProposerConn *conn); - -/* Re-exported PQsocket */ -extern pgsocket walprop_socket(WalProposerConn *conn); - -/* Wrapper around PQconsumeInput (if socket's read-ready) + PQflush */ -extern int walprop_flush(WalProposerConn *conn); - -/* Re-exported PQfinish */ -extern void walprop_finish(WalProposerConn *conn); +} WalProposerConnStatusType; /* - * Ergonomic wrapper around PGgetCopyData - * - * Reads a CopyData block from a safekeeper, setting *amount to the number - * of bytes returned. - * - * This function is allowed to assume certain properties specific to the - * protocol with the safekeepers, so it should not be used as-is for any - * other purpose. - * - * Note: If possible, using is generally preferred, because it - * performs a bit of extra checking work that's always required and is normally - * somewhat verbose. + * Collection of hooks for walproposer, to call postgres functions, + * read WAL and send it over the network. */ -extern PGAsyncReadResult walprop_async_read(WalProposerConn *conn, char **buf, int *amount); +typedef struct walproposer_api +{ + /* + * Get WalproposerShmemState. This is used to store information about last + * elected term. + */ + WalproposerShmemState *(*get_shmem_state) (void); + + /* + * Start receiving notifications about new WAL. This is an infinite loop + * which calls WalProposerBroadcast() and WalProposerPoll() to send the + * WAL. + */ + void (*start_streaming) (WalProposer *wp, XLogRecPtr startpos); + + /* Get pointer to the latest available WAL. */ + XLogRecPtr (*get_flush_rec_ptr) (void); + + /* Get current time. */ + TimestampTz (*get_current_timestamp) (void); + + /* Get postgres timeline. */ + TimeLineID (*get_timeline_id) (void); + + /* Current error message, aka PQerrorMessage. */ + char *(*conn_error_message) (WalProposerConn *conn); + + /* Connection status, aka PQstatus. */ + WalProposerConnStatusType (*conn_status) (WalProposerConn *conn); + + /* Start the connection, aka PQconnectStart. */ + WalProposerConn *(*conn_connect_start) (char *conninfo); + + /* Poll an asynchronous connection, aka PQconnectPoll. */ + WalProposerConnectPollStatusType (*conn_connect_poll) (WalProposerConn *conn); + + /* Send a blocking SQL query, aka PQsendQuery. */ + bool (*conn_send_query) (WalProposerConn *conn, char *query); + + /* Read the query result, aka PQgetResult. */ + WalProposerExecStatusType (*conn_get_query_result) (WalProposerConn *conn); + + /* Flush buffer to the network, aka PQflush. */ + int (*conn_flush) (WalProposerConn *conn); + + /* Close the connection, aka PQfinish. */ + void (*conn_finish) (WalProposerConn *conn); + + /* Try to read CopyData message, aka PQgetCopyData. */ + PGAsyncReadResult (*conn_async_read) (WalProposerConn *conn, char **buf, int *amount); + + /* Try to write CopyData message, aka PQputCopyData. */ + PGAsyncWriteResult (*conn_async_write) (WalProposerConn *conn, void const *buf, size_t size); + + /* Blocking CopyData write, aka PQputCopyData + PQflush. */ + bool (*conn_blocking_write) (WalProposerConn *conn, void const *buf, size_t size); + + /* Download WAL from startpos to endpos and make it available locally. */ + bool (*recovery_download) (Safekeeper *sk, TimeLineID timeline, XLogRecPtr startpos, XLogRecPtr endpos); + + /* Read WAL from disk to buf. */ + void (*wal_read) (XLogReaderState *state, char *buf, XLogRecPtr startptr, Size count); + + /* Allocate WAL reader. */ + XLogReaderState *(*wal_reader_allocate) (void); + + /* Deallocate event set. */ + void (*free_event_set) (void); + + /* Initialize event set. */ + void (*init_event_set) (int n_safekeepers); + + /* Update events for an existing safekeeper connection. */ + void (*update_event_set) (Safekeeper *sk, uint32 events); + + /* Add a new safekeeper connection to the event set. */ + void (*add_safekeeper_event_set) (Safekeeper *sk, uint32 events); + + /* + * Wait until some event happens: - timeout is reached - socket event for + * safekeeper connection - new WAL is available + * + * Returns 0 if timeout is reached, 1 if some event happened. Updates + * events mask to indicate events and sets sk to the safekeeper which has + * an event. + */ + int (*wait_event_set) (long timeout, Safekeeper **sk, uint32 *events); + + /* Read random bytes. */ + bool (*strong_random) (void *buf, size_t len); + + /* + * Get a basebackup LSN. Used to cross-validate with the latest available + * LSN on the safekeepers. + */ + XLogRecPtr (*get_redo_start_lsn) (void); + + /* + * Finish sync safekeepers with the given LSN. This function should not + * return and should exit the program. + */ + void (*finish_sync_safekeepers) (XLogRecPtr lsn); + + /* + * Called after every new message from the safekeeper. Used to propagate + * backpressure feedback and to confirm WAL persistence (has been commited + * on the quorum of safekeepers). + */ + void (*process_safekeeper_feedback) (WalProposer *wp, XLogRecPtr commitLsn); + + /* + * Called on peer_horizon_lsn updates. Used to advance replication slot + * and to free up disk space by deleting unnecessary WAL. + */ + void (*confirm_wal_streamed) (XLogRecPtr lsn); +} walproposer_api; /* - * Ergonomic wrapper around PQputCopyData + PQflush - * - * Starts to write a CopyData block to a safekeeper. - * - * For information on the meaning of return codes, refer to PGAsyncWriteResult. + * Configuration of the WAL proposer. */ -extern PGAsyncWriteResult walprop_async_write(WalProposerConn *conn, void const *buf, size_t size); +typedef struct WalProposerConfig +{ + /* hex-encoded TenantId cstr */ + char *neon_tenant; + + /* hex-encoded TimelineId cstr */ + char *neon_timeline; + + /* + * Comma-separated list of safekeepers, in the following format: + * host1:port1,host2:port2,host3:port3 + * + * This cstr should be editable. + */ + char *safekeepers_list; + + /* + * WalProposer reconnects to offline safekeepers once in this interval. + * Time is in milliseconds. + */ + int safekeeper_reconnect_timeout; + + /* + * WalProposer terminates the connection if it doesn't receive any message + * from the safekeeper in this interval. Time is in milliseconds. + */ + int safekeeper_connection_timeout; + + /* + * WAL segment size. Will be passed to safekeepers in greet request. Also + * used to detect page headers. + */ + int wal_segment_size; + + /* + * If safekeeper was started in sync mode, walproposer will not subscribe + * for new WAL and will exit when quorum of safekeepers will be synced to + * the latest available LSN. + */ + bool syncSafekeepers; + + /* Will be passed to safekeepers in greet request. */ + uint64 systemId; +} WalProposerConfig; + /* - * Blocking equivalent to walprop_async_write_fn - * - * Returns 'true' if successful, 'false' on failure. + * WAL proposer state. */ -extern bool walprop_blocking_write(WalProposerConn *conn, void const *buf, size_t size); +typedef struct WalProposer +{ + WalProposerConfig *config; + int n_safekeepers; -extern uint64 BackpressureThrottlingTime(void); + /* (n_safekeepers / 2) + 1 */ + int quorum; + + Safekeeper safekeeper[MAX_SAFEKEEPERS]; + + /* WAL has been generated up to this point */ + XLogRecPtr availableLsn; + + /* last commitLsn broadcasted to safekeepers */ + XLogRecPtr lastSentCommitLsn; + + ProposerGreeting greetRequest; + + /* Vote request for safekeeper */ + VoteRequest voteRequest; + + /* + * Minimal LSN which may be needed for recovery of some safekeeper, + * record-aligned (first record which might not yet received by someone). + */ + XLogRecPtr truncateLsn; + + /* + * Term of the proposer. We want our term to be highest and unique, so we + * collect terms from safekeepers quorum, choose max and +1. After that + * our term is fixed and must not change. If we observe that some + * safekeeper has higher term, it means that we have another running + * compute, so we must stop immediately. + */ + term_t propTerm; + + /* term history of the proposer */ + TermHistory propTermHistory; + + /* epoch start lsn of the proposer */ + XLogRecPtr propEpochStartLsn; + + /* Most advanced acceptor epoch */ + term_t donorEpoch; + + /* Most advanced acceptor */ + int donor; + + /* timeline globally starts at this LSN */ + XLogRecPtr timelineStartLsn; + + /* number of votes collected from safekeepers */ + int n_votes; + + /* number of successful connections over the lifetime of walproposer */ + int n_connected; + + /* + * Timestamp of the last reconnection attempt. Related to + * config->safekeeper_reconnect_timeout + */ + TimestampTz last_reconnect_attempt; + + walproposer_api api; +} WalProposer; + +extern WalProposer *WalProposerCreate(WalProposerConfig *config, walproposer_api api); +extern void WalProposerStart(WalProposer *wp); +extern void WalProposerBroadcast(WalProposer *wp, XLogRecPtr startpos, XLogRecPtr endpos); +extern void WalProposerPoll(WalProposer *wp); +extern void ParsePageserverFeedbackMessage(StringInfo reply_message, + PageserverFeedback *rf); #endif /* __NEON_WALPROPOSER_H__ */ diff --git a/pgxn/neon/walproposer_pg.c b/pgxn/neon/walproposer_pg.c new file mode 100644 index 0000000000..654b411e94 --- /dev/null +++ b/pgxn/neon/walproposer_pg.c @@ -0,0 +1,1667 @@ +/* + * Implementation of postgres based walproposer disk and IO routines, i.e. the + * real ones. The reason this is separate from walproposer.c is ability to + * replace them with mocks, allowing to do simulation testing. + * + * Also contains initialization of postgres based walproposer. + */ + +#include "postgres.h" + +#include +#include +#include +#include "access/xact.h" +#include "access/xlogdefs.h" +#include "access/xlogutils.h" +#include "access/xloginsert.h" +#if PG_VERSION_NUM >= 150000 +#include "access/xlogrecovery.h" +#endif +#include "storage/fd.h" +#include "storage/latch.h" +#include "miscadmin.h" +#include "pgstat.h" +#include "access/xlog.h" +#include "libpq/pqformat.h" +#include "replication/slot.h" +#include "replication/walreceiver.h" +#include "replication/walsender_private.h" +#include "postmaster/bgworker.h" +#include "postmaster/interrupt.h" +#include "postmaster/postmaster.h" +#include "storage/pmsignal.h" +#include "storage/proc.h" +#include "storage/ipc.h" +#include "storage/lwlock.h" +#include "storage/shmem.h" +#include "storage/spin.h" +#include "tcop/tcopprot.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/memutils.h" +#include "utils/ps_status.h" +#include "utils/timestamp.h" + +#include "neon.h" +#include "walproposer.h" +#include "libpq-fe.h" + +#define XLOG_HDR_SIZE (1 + 8 * 3) /* 'w' + startPos + walEnd + timestamp */ +#define XLOG_HDR_START_POS 1 /* offset of start position in wal sender* + * message header */ + +#define WAL_PROPOSER_SLOT_NAME "wal_proposer_slot" + +char *wal_acceptors_list = ""; +int wal_acceptor_reconnect_timeout = 1000; +int wal_acceptor_connection_timeout = 10000; + +static AppendResponse quorumFeedback; +static WalproposerShmemState *walprop_shared; +static WalProposerConfig walprop_config; +static XLogRecPtr sentPtr = InvalidXLogRecPtr; +static const walproposer_api walprop_pg; + +static void nwp_shmem_startup_hook(void); +static void nwp_register_gucs(void); +static void nwp_prepare_shmem(void); +static uint64 backpressure_lag_impl(void); +static bool backpressure_throttling_impl(void); +static void walprop_register_bgworker(void); + +static void walprop_pg_init_standalone_sync_safekeepers(void); +static void walprop_pg_init_walsender(void); +static void walprop_pg_init_bgworker(void); +static TimestampTz walprop_pg_get_current_timestamp(void); +static void walprop_pg_load_libpqwalreceiver(void); + +static process_interrupts_callback_t PrevProcessInterruptsCallback; +static shmem_startup_hook_type prev_shmem_startup_hook_type; +#if PG_VERSION_NUM >= 150000 +static shmem_request_hook_type prev_shmem_request_hook = NULL; +static void walproposer_shmem_request(void); +#endif + +static void StartProposerReplication(WalProposer *wp, StartReplicationCmd *cmd); +static void WalSndLoop(WalProposer *wp); +static void XLogBroadcastWalProposer(WalProposer *wp); + +static void XLogWalPropWrite(char *buf, Size nbytes, XLogRecPtr recptr); +static void XLogWalPropClose(XLogRecPtr recptr); + +static void +init_walprop_config(bool syncSafekeepers) +{ + walprop_config.neon_tenant = neon_tenant; + walprop_config.neon_timeline = neon_timeline; + walprop_config.safekeepers_list = wal_acceptors_list; + walprop_config.safekeeper_reconnect_timeout = wal_acceptor_reconnect_timeout; + walprop_config.safekeeper_connection_timeout = wal_acceptor_connection_timeout; + walprop_config.wal_segment_size = wal_segment_size; + walprop_config.syncSafekeepers = syncSafekeepers; + if (!syncSafekeepers) + walprop_config.systemId = GetSystemIdentifier(); + else + walprop_config.systemId = 0; +} + +/* + * Entry point for `postgres --sync-safekeepers`. + */ +PGDLLEXPORT void +WalProposerSync(int argc, char *argv[]) +{ + WalProposer *wp; + + init_walprop_config(true); + walprop_pg_init_standalone_sync_safekeepers(); + walprop_pg_load_libpqwalreceiver(); + + wp = WalProposerCreate(&walprop_config, walprop_pg); + + WalProposerStart(wp); +} + +/* + * WAL proposer bgworker entry point. + */ +PGDLLEXPORT void +WalProposerMain(Datum main_arg) +{ + WalProposer *wp; + + init_walprop_config(false); + walprop_pg_init_bgworker(); + walprop_pg_load_libpqwalreceiver(); + + wp = WalProposerCreate(&walprop_config, walprop_pg); + wp->last_reconnect_attempt = walprop_pg_get_current_timestamp(); + + walprop_pg_init_walsender(); + WalProposerStart(wp); +} + +/* + * Initialize GUCs, bgworker, shmem and backpressure. + */ +void +pg_init_walproposer(void) +{ + if (!process_shared_preload_libraries_in_progress) + return; + + nwp_register_gucs(); + + nwp_prepare_shmem(); + + delay_backend_us = &backpressure_lag_impl; + PrevProcessInterruptsCallback = ProcessInterruptsCallback; + ProcessInterruptsCallback = backpressure_throttling_impl; + + walprop_register_bgworker(); +} + +static void +nwp_register_gucs(void) +{ + DefineCustomStringVariable( + "neon.safekeepers", + "List of Neon WAL acceptors (host:port)", + NULL, /* long_desc */ + &wal_acceptors_list, /* valueAddr */ + "", /* bootValue */ + PGC_POSTMASTER, + GUC_LIST_INPUT, /* extensions can't use* + * GUC_LIST_QUOTE */ + NULL, NULL, NULL); + + DefineCustomIntVariable( + "neon.safekeeper_reconnect_timeout", + "Walproposer reconnects to offline safekeepers once in this interval.", + NULL, + &wal_acceptor_reconnect_timeout, + 1000, 0, INT_MAX, /* default, min, max */ + PGC_SIGHUP, /* context */ + GUC_UNIT_MS, /* flags */ + NULL, NULL, NULL); + + DefineCustomIntVariable( + "neon.safekeeper_connect_timeout", + "Connection or connection attempt to safekeeper is terminated if no message is received (or connection attempt doesn't finish) within this period.", + NULL, + &wal_acceptor_connection_timeout, + 10000, 0, INT_MAX, + PGC_SIGHUP, + GUC_UNIT_MS, + NULL, NULL, NULL); +} + +/* Check if we need to suspend inserts because of lagging replication. */ +static uint64 +backpressure_lag_impl(void) +{ + if (max_replication_apply_lag > 0 || max_replication_flush_lag > 0 || max_replication_write_lag > 0) + { + XLogRecPtr writePtr; + XLogRecPtr flushPtr; + XLogRecPtr applyPtr; +#if PG_VERSION_NUM >= 150000 + XLogRecPtr myFlushLsn = GetFlushRecPtr(NULL); +#else + XLogRecPtr myFlushLsn = GetFlushRecPtr(); +#endif + replication_feedback_get_lsns(&writePtr, &flushPtr, &applyPtr); +#define MB ((XLogRecPtr)1024 * 1024) + + elog(DEBUG2, "current flushLsn %X/%X PageserverFeedback: write %X/%X flush %X/%X apply %X/%X", + LSN_FORMAT_ARGS(myFlushLsn), + LSN_FORMAT_ARGS(writePtr), + LSN_FORMAT_ARGS(flushPtr), + LSN_FORMAT_ARGS(applyPtr)); + + if ((writePtr != InvalidXLogRecPtr && max_replication_write_lag > 0 && myFlushLsn > writePtr + max_replication_write_lag * MB)) + { + return (myFlushLsn - writePtr - max_replication_write_lag * MB); + } + + if ((flushPtr != InvalidXLogRecPtr && max_replication_flush_lag > 0 && myFlushLsn > flushPtr + max_replication_flush_lag * MB)) + { + return (myFlushLsn - flushPtr - max_replication_flush_lag * MB); + } + + if ((applyPtr != InvalidXLogRecPtr && max_replication_apply_lag > 0 && myFlushLsn > applyPtr + max_replication_apply_lag * MB)) + { + return (myFlushLsn - applyPtr - max_replication_apply_lag * MB); + } + } + return 0; +} + +/* + * WalproposerShmemSize --- report amount of shared memory space needed + */ +static Size +WalproposerShmemSize(void) +{ + return sizeof(WalproposerShmemState); +} + +static bool +WalproposerShmemInit(void) +{ + bool found; + + LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE); + walprop_shared = ShmemInitStruct("Walproposer shared state", + sizeof(WalproposerShmemState), + &found); + + if (!found) + { + memset(walprop_shared, 0, WalproposerShmemSize()); + SpinLockInit(&walprop_shared->mutex); + pg_atomic_init_u64(&walprop_shared->backpressureThrottlingTime, 0); + } + LWLockRelease(AddinShmemInitLock); + + return found; +} + +#define BACK_PRESSURE_DELAY 10000L // 0.01 sec + +static bool +backpressure_throttling_impl(void) +{ + int64 lag; + TimestampTz start, + stop; + bool retry = PrevProcessInterruptsCallback + ? PrevProcessInterruptsCallback() + : false; + + /* + * Don't throttle read only transactions or wal sender. Do throttle CREATE + * INDEX CONCURRENTLY, however. It performs some stages outside a + * transaction, even though it writes a lot of WAL. Check PROC_IN_SAFE_IC + * flag to cover that case. + */ + if (am_walsender + || (!(MyProc->statusFlags & PROC_IN_SAFE_IC) + && !TransactionIdIsValid(GetCurrentTransactionIdIfAny()))) + return retry; + + /* Calculate replicas lag */ + lag = backpressure_lag_impl(); + if (lag == 0) + return retry; + + /* Suspend writers until replicas catch up */ + set_ps_display("backpressure throttling"); + + elog(DEBUG2, "backpressure throttling: lag %lu", lag); + start = GetCurrentTimestamp(); + pg_usleep(BACK_PRESSURE_DELAY); + stop = GetCurrentTimestamp(); + pg_atomic_add_fetch_u64(&walprop_shared->backpressureThrottlingTime, stop - start); + return true; +} + +uint64 +BackpressureThrottlingTime(void) +{ + return pg_atomic_read_u64(&walprop_shared->backpressureThrottlingTime); +} + +/* + * Register a background worker proposing WAL to wal acceptors. + */ +static void +walprop_register_bgworker(void) +{ + BackgroundWorker bgw; + + /* If no wal acceptors are specified, don't start the background worker. */ + if (*wal_acceptors_list == '\0') + return; + + memset(&bgw, 0, sizeof(bgw)); + bgw.bgw_flags = BGWORKER_SHMEM_ACCESS; + bgw.bgw_start_time = BgWorkerStart_RecoveryFinished; + snprintf(bgw.bgw_library_name, BGW_MAXLEN, "neon"); + snprintf(bgw.bgw_function_name, BGW_MAXLEN, "WalProposerMain"); + snprintf(bgw.bgw_name, BGW_MAXLEN, "WAL proposer"); + snprintf(bgw.bgw_type, BGW_MAXLEN, "WAL proposer"); + bgw.bgw_restart_time = 5; + bgw.bgw_notify_pid = 0; + bgw.bgw_main_arg = (Datum) 0; + + RegisterBackgroundWorker(&bgw); +} + +/* shmem handling */ + +static void +nwp_prepare_shmem(void) +{ +#if PG_VERSION_NUM >= 150000 + prev_shmem_request_hook = shmem_request_hook; + shmem_request_hook = walproposer_shmem_request; +#else + RequestAddinShmemSpace(WalproposerShmemSize()); +#endif + prev_shmem_startup_hook_type = shmem_startup_hook; + shmem_startup_hook = nwp_shmem_startup_hook; +} + +#if PG_VERSION_NUM >= 150000 +/* + * shmem_request hook: request additional shared resources. We'll allocate or + * attach to the shared resources in nwp_shmem_startup_hook(). + */ +static void +walproposer_shmem_request(void) +{ + if (prev_shmem_request_hook) + prev_shmem_request_hook(); + + RequestAddinShmemSpace(WalproposerShmemSize()); +} +#endif + +static void +nwp_shmem_startup_hook(void) +{ + if (prev_shmem_startup_hook_type) + prev_shmem_startup_hook_type(); + + WalproposerShmemInit(); +} + +static WalproposerShmemState * +walprop_pg_get_shmem_state(void) +{ + Assert(walprop_shared != NULL); + return walprop_shared; +} + +void +replication_feedback_set(PageserverFeedback *rf) +{ + SpinLockAcquire(&walprop_shared->mutex); + memcpy(&walprop_shared->feedback, rf, sizeof(PageserverFeedback)); + SpinLockRelease(&walprop_shared->mutex); +} + +void +replication_feedback_get_lsns(XLogRecPtr *writeLsn, XLogRecPtr *flushLsn, XLogRecPtr *applyLsn) +{ + SpinLockAcquire(&walprop_shared->mutex); + *writeLsn = walprop_shared->feedback.last_received_lsn; + *flushLsn = walprop_shared->feedback.disk_consistent_lsn; + *applyLsn = walprop_shared->feedback.remote_consistent_lsn; + SpinLockRelease(&walprop_shared->mutex); +} + +/* + * Start walsender streaming replication + */ +static void +walprop_pg_start_streaming(WalProposer *wp, XLogRecPtr startpos) +{ + StartReplicationCmd cmd; + + elog(LOG, "WAL proposer starts streaming at %X/%X", + LSN_FORMAT_ARGS(startpos)); + cmd.slotname = WAL_PROPOSER_SLOT_NAME; + cmd.timeline = wp->greetRequest.timeline; + cmd.startpoint = startpos; + StartProposerReplication(wp, &cmd); +} + +static void +walprop_pg_init_walsender(void) +{ + am_walsender = true; + InitWalSender(); + InitProcessPhase2(); + + /* Create replication slot for WAL proposer if not exists */ + if (SearchNamedReplicationSlot(WAL_PROPOSER_SLOT_NAME, false) == NULL) + { + ReplicationSlotCreate(WAL_PROPOSER_SLOT_NAME, false, RS_PERSISTENT, false); + ReplicationSlotReserveWal(); + /* Write this slot to disk */ + ReplicationSlotMarkDirty(); + ReplicationSlotSave(); + ReplicationSlotRelease(); + } +} + +static void +walprop_pg_init_standalone_sync_safekeepers(void) +{ + struct stat stat_buf; + +#if PG_VERSION_NUM < 150000 + ThisTimeLineID = 1; +#endif + + /* + * Initialize postmaster_alive_fds as WaitEventSet checks them. + * + * Copied from InitPostmasterDeathWatchHandle() + */ + if (pipe(postmaster_alive_fds) < 0) + ereport(FATAL, + (errcode_for_file_access(), + errmsg_internal("could not create pipe to monitor postmaster death: %m"))); + if (fcntl(postmaster_alive_fds[POSTMASTER_FD_WATCH], F_SETFL, O_NONBLOCK) == -1) + ereport(FATAL, + (errcode_for_socket_access(), + errmsg_internal("could not set postmaster death monitoring pipe to nonblocking mode: %m"))); + + ChangeToDataDir(); + + /* Create pg_wal directory, if it doesn't exist */ + if (stat(XLOGDIR, &stat_buf) != 0) + { + ereport(LOG, (errmsg("creating missing WAL directory \"%s\"", XLOGDIR))); + if (MakePGDirectory(XLOGDIR) < 0) + { + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not create directory \"%s\": %m", + XLOGDIR))); + exit(1); + } + } + BackgroundWorkerUnblockSignals(); +} + +static void +walprop_pg_init_bgworker(void) +{ +#if PG_VERSION_NUM >= 150000 + TimeLineID tli; +#endif + + /* Establish signal handlers. */ + pqsignal(SIGUSR1, procsignal_sigusr1_handler); + pqsignal(SIGHUP, SignalHandlerForConfigReload); + pqsignal(SIGTERM, die); + + BackgroundWorkerUnblockSignals(); + + application_name = (char *) "walproposer"; /* for + * synchronous_standby_names */ + +#if PG_VERSION_NUM >= 150000 + /* FIXME pass proper tli to WalProposerInit ? */ + GetXLogReplayRecPtr(&tli); +#else + GetXLogReplayRecPtr(&ThisTimeLineID); +#endif +} + +static XLogRecPtr +walprop_pg_get_flush_rec_ptr(void) +{ +#if PG_MAJORVERSION_NUM < 15 + return GetFlushRecPtr(); +#else + return GetFlushRecPtr(NULL); +#endif +} + +static TimestampTz +walprop_pg_get_current_timestamp(void) +{ + return GetCurrentTimestamp(); +} + +static TimeLineID +walprop_pg_get_timeline_id(void) +{ +#if PG_VERSION_NUM >= 150000 + /* FIXME don't use hardcoded timeline id */ + return 1; +#else + return ThisTimeLineID; +#endif +} + +static void +walprop_pg_load_libpqwalreceiver(void) +{ + load_file("libpqwalreceiver", false); + if (WalReceiverFunctions == NULL) + elog(ERROR, "libpqwalreceiver didn't initialize correctly"); +} + +/* Header in walproposer.h -- Wrapper struct to abstract away the libpq connection */ +struct WalProposerConn +{ + PGconn *pg_conn; + bool is_nonblocking; /* whether the connection is non-blocking */ + char *recvbuf; /* last received data from walprop_async_read */ +}; + +/* Helper function */ +static bool +ensure_nonblocking_status(WalProposerConn *conn, bool is_nonblocking) +{ + /* If we're already correctly blocking or nonblocking, all good */ + if (is_nonblocking == conn->is_nonblocking) + return true; + + /* Otherwise, set it appropriately */ + if (PQsetnonblocking(conn->pg_conn, is_nonblocking) == -1) + return false; + + conn->is_nonblocking = is_nonblocking; + return true; +} + +/* Exported function definitions */ +static char * +walprop_error_message(WalProposerConn *conn) +{ + return PQerrorMessage(conn->pg_conn); +} + +static WalProposerConnStatusType +walprop_status(WalProposerConn *conn) +{ + switch (PQstatus(conn->pg_conn)) + { + case CONNECTION_OK: + return WP_CONNECTION_OK; + case CONNECTION_BAD: + return WP_CONNECTION_BAD; + default: + return WP_CONNECTION_IN_PROGRESS; + } +} + +static WalProposerConn * +walprop_connect_start(char *conninfo) +{ + WalProposerConn *conn; + PGconn *pg_conn; + const char *keywords[3]; + const char *values[3]; + int n; + char *password = neon_auth_token; + + /* + * Connect using the given connection string. If the NEON_AUTH_TOKEN + * environment variable was set, use that as the password. + * + * The connection options are parsed in the order they're given, so when + * we set the password before the connection string, the connection string + * can override the password from the env variable. Seems useful, although + * we don't currently use that capability anywhere. + */ + n = 0; + if (password) + { + keywords[n] = "password"; + values[n] = password; + n++; + } + keywords[n] = "dbname"; + values[n] = conninfo; + n++; + keywords[n] = NULL; + values[n] = NULL; + n++; + pg_conn = PQconnectStartParams(keywords, values, 1); + + /* + * Allocation of a PQconn can fail, and will return NULL. We want to fully + * replicate the behavior of PQconnectStart here. + */ + if (!pg_conn) + return NULL; + + /* + * And in theory this allocation can fail as well, but it's incredibly + * unlikely if we just successfully allocated a PGconn. + * + * palloc will exit on failure though, so there's not much we could do if + * it *did* fail. + */ + conn = palloc(sizeof(WalProposerConn)); + conn->pg_conn = pg_conn; + conn->is_nonblocking = false; /* connections always start in blocking + * mode */ + conn->recvbuf = NULL; + return conn; +} + +static WalProposerConnectPollStatusType +walprop_connect_poll(WalProposerConn *conn) +{ + WalProposerConnectPollStatusType return_val; + + switch (PQconnectPoll(conn->pg_conn)) + { + case PGRES_POLLING_FAILED: + return_val = WP_CONN_POLLING_FAILED; + break; + case PGRES_POLLING_READING: + return_val = WP_CONN_POLLING_READING; + break; + case PGRES_POLLING_WRITING: + return_val = WP_CONN_POLLING_WRITING; + break; + case PGRES_POLLING_OK: + return_val = WP_CONN_POLLING_OK; + break; + + /* + * There's a comment at its source about this constant being + * unused. We'll expect it's never returned. + */ + case PGRES_POLLING_ACTIVE: + elog(FATAL, "Unexpected PGRES_POLLING_ACTIVE returned from PQconnectPoll"); + + /* + * This return is never actually reached, but it's here to make + * the compiler happy + */ + return WP_CONN_POLLING_FAILED; + + default: + Assert(false); + return_val = WP_CONN_POLLING_FAILED; /* keep the compiler quiet */ + } + + return return_val; +} + +static bool +walprop_send_query(WalProposerConn *conn, char *query) +{ + /* + * We need to be in blocking mode for sending the query to run without + * requiring a call to PQflush + */ + if (!ensure_nonblocking_status(conn, false)) + return false; + + /* PQsendQuery returns 1 on success, 0 on failure */ + if (!PQsendQuery(conn->pg_conn, query)) + return false; + + return true; +} + +static WalProposerExecStatusType +walprop_get_query_result(WalProposerConn *conn) +{ + PGresult *result; + WalProposerExecStatusType return_val; + + /* Marker variable if we need to log an unexpected success result */ + char *unexpected_success = NULL; + + /* Consume any input that we might be missing */ + if (!PQconsumeInput(conn->pg_conn)) + return WP_EXEC_FAILED; + + if (PQisBusy(conn->pg_conn)) + return WP_EXEC_NEEDS_INPUT; + + + result = PQgetResult(conn->pg_conn); + + /* + * PQgetResult returns NULL only if getting the result was successful & + * there's no more of the result to get. + */ + if (!result) + { + elog(WARNING, "[libpqwalproposer] Unexpected successful end of command results"); + return WP_EXEC_UNEXPECTED_SUCCESS; + } + + /* Helper macro to reduce boilerplate */ +#define UNEXPECTED_SUCCESS(msg) \ + return_val = WP_EXEC_UNEXPECTED_SUCCESS; \ + unexpected_success = msg; \ + break; + + + switch (PQresultStatus(result)) + { + /* "true" success case */ + case PGRES_COPY_BOTH: + return_val = WP_EXEC_SUCCESS_COPYBOTH; + break; + + /* Unexpected success case */ + case PGRES_EMPTY_QUERY: + UNEXPECTED_SUCCESS("empty query return"); + case PGRES_COMMAND_OK: + UNEXPECTED_SUCCESS("data-less command end"); + case PGRES_TUPLES_OK: + UNEXPECTED_SUCCESS("tuples return"); + case PGRES_COPY_OUT: + UNEXPECTED_SUCCESS("'Copy Out' response"); + case PGRES_COPY_IN: + UNEXPECTED_SUCCESS("'Copy In' response"); + case PGRES_SINGLE_TUPLE: + UNEXPECTED_SUCCESS("single tuple return"); + case PGRES_PIPELINE_SYNC: + UNEXPECTED_SUCCESS("pipeline sync point"); + + /* Failure cases */ + case PGRES_BAD_RESPONSE: + case PGRES_NONFATAL_ERROR: + case PGRES_FATAL_ERROR: + case PGRES_PIPELINE_ABORTED: + return_val = WP_EXEC_FAILED; + break; + + default: + Assert(false); + return_val = WP_EXEC_FAILED; /* keep the compiler quiet */ + } + + if (unexpected_success) + elog(WARNING, "[libpqwalproposer] Unexpected successful %s", unexpected_success); + + return return_val; +} + +static pgsocket +walprop_socket(WalProposerConn *conn) +{ + return PQsocket(conn->pg_conn); +} + +static int +walprop_flush(WalProposerConn *conn) +{ + return (PQflush(conn->pg_conn)); +} + +static void +walprop_finish(WalProposerConn *conn) +{ + if (conn->recvbuf != NULL) + PQfreemem(conn->recvbuf); + PQfinish(conn->pg_conn); + pfree(conn); +} + +/* + * Receive a message from the safekeeper. + * + * On success, the data is placed in *buf. It is valid until the next call + * to this function. + */ +static PGAsyncReadResult +walprop_async_read(WalProposerConn *conn, char **buf, int *amount) +{ + int result; + + if (conn->recvbuf != NULL) + { + PQfreemem(conn->recvbuf); + conn->recvbuf = NULL; + } + + /* Call PQconsumeInput so that we have the data we need */ + if (!PQconsumeInput(conn->pg_conn)) + { + *amount = 0; + *buf = NULL; + return PG_ASYNC_READ_FAIL; + } + + /* + * The docs for PQgetCopyData list the return values as: 0 if the copy is + * still in progress, but no "complete row" is available -1 if the copy is + * done -2 if an error occurred (> 0) if it was successful; that value is + * the amount transferred. + * + * The protocol we use between walproposer and safekeeper means that we + * *usually* wouldn't expect to see that the copy is done, but this can + * sometimes be triggered by the server returning an ErrorResponse (which + * also happens to have the effect that the copy is done). + */ + switch (result = PQgetCopyData(conn->pg_conn, &conn->recvbuf, true)) + { + case 0: + *amount = 0; + *buf = NULL; + return PG_ASYNC_READ_TRY_AGAIN; + case -1: + { + /* + * If we get -1, it's probably because of a server error; the + * safekeeper won't normally send a CopyDone message. + * + * We can check PQgetResult to make sure that the server + * failed; it'll always result in PGRES_FATAL_ERROR + */ + ExecStatusType status = PQresultStatus(PQgetResult(conn->pg_conn)); + + if (status != PGRES_FATAL_ERROR) + elog(FATAL, "unexpected result status %d after failed PQgetCopyData", status); + + /* + * If there was actually an error, it'll be properly reported + * by calls to PQerrorMessage -- we don't have to do anything + * else + */ + *amount = 0; + *buf = NULL; + return PG_ASYNC_READ_FAIL; + } + case -2: + *amount = 0; + *buf = NULL; + return PG_ASYNC_READ_FAIL; + default: + /* Positive values indicate the size of the returned result */ + *amount = result; + *buf = conn->recvbuf; + return PG_ASYNC_READ_SUCCESS; + } +} + +static PGAsyncWriteResult +walprop_async_write(WalProposerConn *conn, void const *buf, size_t size) +{ + int result; + + /* If we aren't in non-blocking mode, switch to it. */ + if (!ensure_nonblocking_status(conn, true)) + return PG_ASYNC_WRITE_FAIL; + + /* + * The docs for PQputcopyData list the return values as: 1 if the data was + * queued, 0 if it was not queued because of full buffers, or -1 if an + * error occurred + */ + result = PQputCopyData(conn->pg_conn, buf, size); + + /* + * We won't get a result of zero because walproposer always empties the + * connection's buffers before sending more + */ + Assert(result != 0); + + switch (result) + { + case 1: + /* good -- continue */ + break; + case -1: + return PG_ASYNC_WRITE_FAIL; + default: + elog(FATAL, "invalid return %d from PQputCopyData", result); + } + + /* + * After queueing the data, we still need to flush to get it to send. This + * might take multiple tries, but we don't want to wait around until it's + * done. + * + * PQflush has the following returns (directly quoting the docs): 0 if + * sucessful, 1 if it was unable to send all the data in the send queue + * yet -1 if it failed for some reason + */ + switch (result = PQflush(conn->pg_conn)) + { + case 0: + return PG_ASYNC_WRITE_SUCCESS; + case 1: + return PG_ASYNC_WRITE_TRY_FLUSH; + case -1: + return PG_ASYNC_WRITE_FAIL; + default: + elog(FATAL, "invalid return %d from PQflush", result); + } +} + +/* + * This function is very similar to walprop_async_write. For more + * information, refer to the comments there. + */ +static bool +walprop_blocking_write(WalProposerConn *conn, void const *buf, size_t size) +{ + int result; + + /* If we are in non-blocking mode, switch out of it. */ + if (!ensure_nonblocking_status(conn, false)) + return false; + + if ((result = PQputCopyData(conn->pg_conn, buf, size)) == -1) + return false; + + Assert(result == 1); + + /* Because the connection is non-blocking, flushing returns 0 or -1 */ + + if ((result = PQflush(conn->pg_conn)) == -1) + return false; + + Assert(result == 0); + return true; +} + +/* + * Subscribe for new WAL and stream it in the loop to safekeepers. + * + * At the moment, this never returns, but an ereport(ERROR) will take us back + * to the main loop. + */ +static void +StartProposerReplication(WalProposer *wp, StartReplicationCmd *cmd) +{ + XLogRecPtr FlushPtr; + TimeLineID currTLI; + +#if PG_VERSION_NUM < 150000 + if (ThisTimeLineID == 0) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("IDENTIFY_SYSTEM has not been run before START_REPLICATION"))); +#endif + + /* + * We assume here that we're logging enough information in the WAL for + * log-shipping, since this is checked in PostmasterMain(). + * + * NOTE: wal_level can only change at shutdown, so in most cases it is + * difficult for there to be WAL data that we can still see that was + * written at wal_level='minimal'. + */ + + if (cmd->slotname) + { + ReplicationSlotAcquire(cmd->slotname, true); + if (SlotIsLogical(MyReplicationSlot)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot use a logical replication slot for physical replication"))); + + /* + * We don't need to verify the slot's restart_lsn here; instead we + * rely on the caller requesting the starting point to use. If the + * WAL segment doesn't exist, we'll fail later. + */ + } + + /* + * Select the timeline. If it was given explicitly by the client, use + * that. Otherwise use the timeline of the last replayed record, which is + * kept in ThisTimeLineID. + * + * Neon doesn't currently use PG Timelines, but it may in the future, so + * we keep this code around to lighten the load for when we need it. + */ +#if PG_VERSION_NUM >= 150000 + FlushPtr = GetFlushRecPtr(&currTLI); +#else + FlushPtr = GetFlushRecPtr(); + currTLI = ThisTimeLineID; +#endif + + /* + * When we first start replication the standby will be behind the primary. + * For some applications, for example synchronous replication, it is + * important to have a clear state for this initial catchup mode, so we + * can trigger actions when we change streaming state later. We may stay + * in this state for a long time, which is exactly why we want to be able + * to monitor whether or not we are still here. + */ + WalSndSetState(WALSNDSTATE_CATCHUP); + + /* + * Don't allow a request to stream from a future point in WAL that hasn't + * been flushed to disk in this server yet. + */ + if (FlushPtr < cmd->startpoint) + { + ereport(ERROR, + (errmsg("requested starting point %X/%X is ahead of the WAL flush position of this server %X/%X", + LSN_FORMAT_ARGS(cmd->startpoint), + LSN_FORMAT_ARGS(FlushPtr)))); + } + + /* Start streaming from the requested point */ + sentPtr = cmd->startpoint; + + /* Initialize shared memory status, too */ + SpinLockAcquire(&MyWalSnd->mutex); + MyWalSnd->sentPtr = sentPtr; + SpinLockRelease(&MyWalSnd->mutex); + + SyncRepInitConfig(); + + /* Infinite send loop, never returns */ + WalSndLoop(wp); + + WalSndSetState(WALSNDSTATE_STARTUP); + + if (cmd->slotname) + ReplicationSlotRelease(); +} + +/* + * Main loop that waits for LSN updates and calls the walproposer. + * Synchronous replication sets latch in WalSndWakeup at walsender.c + */ +static void +WalSndLoop(WalProposer *wp) +{ + /* Clear any already-pending wakeups */ + ResetLatch(MyLatch); + + for (;;) + { + CHECK_FOR_INTERRUPTS(); + + XLogBroadcastWalProposer(wp); + + if (MyWalSnd->state == WALSNDSTATE_CATCHUP) + WalSndSetState(WALSNDSTATE_STREAMING); + WalProposerPoll(wp); + } +} + +/* + * Notify walproposer about the new WAL position. + */ +static void +XLogBroadcastWalProposer(WalProposer *wp) +{ + XLogRecPtr startptr; + XLogRecPtr endptr; + + /* Start from the last sent position */ + startptr = sentPtr; + + /* + * Streaming the current timeline on a primary. + * + * Attempt to send all data that's already been written out and fsync'd to + * disk. We cannot go further than what's been written out given the + * current implementation of WALRead(). And in any case it's unsafe to + * send WAL that is not securely down to disk on the primary: if the + * primary subsequently crashes and restarts, standbys must not have + * applied any WAL that got lost on the primary. + */ +#if PG_VERSION_NUM >= 150000 + endptr = GetFlushRecPtr(NULL); +#else + endptr = GetFlushRecPtr(); +#endif + + /* + * Record the current system time as an approximation of the time at which + * this WAL location was written for the purposes of lag tracking. + * + * In theory we could make XLogFlush() record a time in shmem whenever WAL + * is flushed and we could get that time as well as the LSN when we call + * GetFlushRecPtr() above (and likewise for the cascading standby + * equivalent), but rather than putting any new code into the hot WAL path + * it seems good enough to capture the time here. We should reach this + * after XLogFlush() runs WalSndWakeupProcessRequests(), and although that + * may take some time, we read the WAL flush pointer and take the time + * very close to together here so that we'll get a later position if it is + * still moving. + * + * Because LagTrackerWrite ignores samples when the LSN hasn't advanced, + * this gives us a cheap approximation for the WAL flush time for this + * LSN. + * + * Note that the LSN is not necessarily the LSN for the data contained in + * the present message; it's the end of the WAL, which might be further + * ahead. All the lag tracking machinery cares about is finding out when + * that arbitrary LSN is eventually reported as written, flushed and + * applied, so that it can measure the elapsed time. + */ + LagTrackerWrite(endptr, GetCurrentTimestamp()); + + /* Do we have any work to do? */ + Assert(startptr <= endptr); + if (endptr <= startptr) + return; + + WalProposerBroadcast(wp, startptr, endptr); + sentPtr = endptr; + + /* Update shared memory status */ + { + WalSnd *walsnd = MyWalSnd; + + SpinLockAcquire(&walsnd->mutex); + walsnd->sentPtr = sentPtr; + SpinLockRelease(&walsnd->mutex); + } + + /* Report progress of XLOG streaming in PS display */ + if (update_process_title) + { + char activitymsg[50]; + + snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%X", + LSN_FORMAT_ARGS(sentPtr)); + set_ps_display(activitymsg); + } +} + +/* + * Receive WAL from most advanced safekeeper + */ +static bool +WalProposerRecovery(Safekeeper *sk, TimeLineID timeline, XLogRecPtr startpos, XLogRecPtr endpos) +{ + char *err; + WalReceiverConn *wrconn; + WalRcvStreamOptions options; + char conninfo[MAXCONNINFO]; + + if (!neon_auth_token) + { + memcpy(conninfo, sk->conninfo, MAXCONNINFO); + } + else + { + int written = 0; + + written = snprintf((char *) conninfo, MAXCONNINFO, "password=%s %s", neon_auth_token, sk->conninfo); + if (written > MAXCONNINFO || written < 0) + elog(FATAL, "could not append password to the safekeeper connection string"); + } + +#if PG_MAJORVERSION_NUM < 16 + wrconn = walrcv_connect(conninfo, false, "wal_proposer_recovery", &err); +#else + wrconn = walrcv_connect(conninfo, false, false, "wal_proposer_recovery", &err); +#endif + + if (!wrconn) + { + ereport(WARNING, + (errmsg("could not connect to WAL acceptor %s:%s: %s", + sk->host, sk->port, + err))); + return false; + } + elog(LOG, + "start recovery from %s:%s starting from %X/%08X till %X/%08X timeline " + "%d", + sk->host, sk->port, (uint32) (startpos >> 32), + (uint32) startpos, (uint32) (endpos >> 32), (uint32) endpos, timeline); + + options.logical = false; + options.startpoint = startpos; + options.slotname = NULL; + options.proto.physical.startpointTLI = timeline; + + if (walrcv_startstreaming(wrconn, &options)) + { + XLogRecPtr rec_start_lsn; + XLogRecPtr rec_end_lsn = 0; + int len; + char *buf; + pgsocket wait_fd = PGINVALID_SOCKET; + + while ((len = walrcv_receive(wrconn, &buf, &wait_fd)) >= 0) + { + if (len == 0) + { + (void) WaitLatchOrSocket( + MyLatch, WL_EXIT_ON_PM_DEATH | WL_SOCKET_READABLE, wait_fd, + -1, WAIT_EVENT_WAL_RECEIVER_MAIN); + } + else + { + Assert(buf[0] == 'w' || buf[0] == 'k'); + if (buf[0] == 'k') + continue; /* keepalive */ + memcpy(&rec_start_lsn, &buf[XLOG_HDR_START_POS], + sizeof rec_start_lsn); + rec_start_lsn = pg_ntoh64(rec_start_lsn); + rec_end_lsn = rec_start_lsn + len - XLOG_HDR_SIZE; + + /* write WAL to disk */ + XLogWalPropWrite(&buf[XLOG_HDR_SIZE], len - XLOG_HDR_SIZE, rec_start_lsn); + + ereport(DEBUG1, + (errmsg("Recover message %X/%X length %d", + LSN_FORMAT_ARGS(rec_start_lsn), len))); + if (rec_end_lsn >= endpos) + break; + } + } + ereport(LOG, + (errmsg("end of replication stream at %X/%X: %m", + LSN_FORMAT_ARGS(rec_end_lsn)))); + walrcv_disconnect(wrconn); + + /* failed to receive all WAL till endpos */ + if (rec_end_lsn < endpos) + return false; + } + else + { + ereport(LOG, + (errmsg("primary server contains no more WAL on requested timeline %u LSN %X/%08X", + timeline, (uint32) (startpos >> 32), (uint32) startpos))); + return false; + } + + return true; +} + +/* + * These variables are used similarly to openLogFile/SegNo, + * but for walproposer to write the XLOG during recovery. walpropFileTLI is the TimeLineID + * corresponding the filename of walpropFile. + */ +static int walpropFile = -1; +static TimeLineID walpropFileTLI = 0; +static XLogSegNo walpropSegNo = 0; + +/* + * Write XLOG data to disk. + */ +static void +XLogWalPropWrite(char *buf, Size nbytes, XLogRecPtr recptr) +{ + int startoff; + int byteswritten; + + while (nbytes > 0) + { + int segbytes; + + /* Close the current segment if it's completed */ + if (walpropFile >= 0 && !XLByteInSeg(recptr, walpropSegNo, wal_segment_size)) + XLogWalPropClose(recptr); + + if (walpropFile < 0) + { +#if PG_VERSION_NUM >= 150000 + /* FIXME Is it ok to use hardcoded value here? */ + TimeLineID tli = 1; +#else + bool use_existent = true; +#endif + /* Create/use new log file */ + XLByteToSeg(recptr, walpropSegNo, wal_segment_size); +#if PG_VERSION_NUM >= 150000 + walpropFile = XLogFileInit(walpropSegNo, tli); + walpropFileTLI = tli; +#else + walpropFile = XLogFileInit(walpropSegNo, &use_existent, false); + walpropFileTLI = ThisTimeLineID; +#endif + } + + /* Calculate the start offset of the received logs */ + startoff = XLogSegmentOffset(recptr, wal_segment_size); + + if (startoff + nbytes > wal_segment_size) + segbytes = wal_segment_size - startoff; + else + segbytes = nbytes; + + /* OK to write the logs */ + errno = 0; + + byteswritten = pg_pwrite(walpropFile, buf, segbytes, (off_t) startoff); + if (byteswritten <= 0) + { + char xlogfname[MAXFNAMELEN]; + int save_errno; + + /* if write didn't set errno, assume no disk space */ + if (errno == 0) + errno = ENOSPC; + + save_errno = errno; + XLogFileName(xlogfname, walpropFileTLI, walpropSegNo, wal_segment_size); + errno = save_errno; + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not write to log segment %s " + "at offset %u, length %lu: %m", + xlogfname, startoff, (unsigned long) segbytes))); + } + + /* Update state for write */ + recptr += byteswritten; + + nbytes -= byteswritten; + buf += byteswritten; + } + + /* + * Close the current segment if it's fully written up in the last cycle of + * the loop. + */ + if (walpropFile >= 0 && !XLByteInSeg(recptr, walpropSegNo, wal_segment_size)) + { + XLogWalPropClose(recptr); + } +} + +/* + * Close the current segment. + */ +static void +XLogWalPropClose(XLogRecPtr recptr) +{ + Assert(walpropFile >= 0 && !XLByteInSeg(recptr, walpropSegNo, wal_segment_size)); + + if (close(walpropFile) != 0) + { + char xlogfname[MAXFNAMELEN]; + + XLogFileName(xlogfname, walpropFileTLI, walpropSegNo, wal_segment_size); + + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not close log segment %s: %m", + xlogfname))); + } + + walpropFile = -1; +} + +static void +walprop_pg_wal_read(XLogReaderState *state, char *buf, XLogRecPtr startptr, Size count) +{ + WALReadError errinfo; + + if (!WALRead(state, + buf, + startptr, + count, + walprop_pg_get_timeline_id(), + &errinfo)) + { + WALReadRaiseError(&errinfo); + } +} + +static XLogReaderState * +walprop_pg_wal_reader_allocate(void) +{ + return XLogReaderAllocate(wal_segment_size, NULL, XL_ROUTINE(.segment_open = wal_segment_open,.segment_close = wal_segment_close), NULL); +} + +static WaitEventSet *waitEvents; + +static void +walprop_pg_free_event_set(void) +{ + if (waitEvents) + { + FreeWaitEventSet(waitEvents); + waitEvents = NULL; + } +} + +static void +walprop_pg_init_event_set(int n_safekeepers) +{ + if (waitEvents) + elog(FATAL, "double-initialization of event set"); + + waitEvents = CreateWaitEventSet(TopMemoryContext, 2 + n_safekeepers); + AddWaitEventToSet(waitEvents, WL_LATCH_SET, PGINVALID_SOCKET, + MyLatch, NULL); + AddWaitEventToSet(waitEvents, WL_EXIT_ON_PM_DEATH, PGINVALID_SOCKET, + NULL, NULL); +} + +static void +walprop_pg_update_event_set(Safekeeper *sk, uint32 events) +{ + /* eventPos = -1 when we don't have an event */ + Assert(sk->eventPos != -1); + + ModifyWaitEvent(waitEvents, sk->eventPos, events, NULL); +} + +static void +walprop_pg_add_safekeeper_event_set(Safekeeper *sk, uint32 events) +{ + sk->eventPos = AddWaitEventToSet(waitEvents, events, walprop_socket(sk->conn), NULL, sk); +} + +static int +walprop_pg_wait_event_set(long timeout, Safekeeper **sk, uint32 *events) +{ + WaitEvent event = {0}; + int rc = 0; + bool late_cv_trigger = false; + + *sk = NULL; + *events = 0; + +#if PG_MAJORVERSION_NUM >= 16 + if (WalSndCtl != NULL) + ConditionVariablePrepareToSleep(&WalSndCtl->wal_flush_cv); +#endif + + /* + * Wait for a wait event to happen, or timeout: - Safekeeper socket can + * become available for READ or WRITE - Our latch got set, because * + * PG15-: We got woken up by a process triggering the WalSender * PG16+: + * WalSndCtl->wal_flush_cv was triggered + */ + rc = WaitEventSetWait(waitEvents, timeout, + &event, 1, WAIT_EVENT_WAL_SENDER_MAIN); +#if PG_MAJORVERSION_NUM >= 16 + if (WalSndCtl != NULL) + late_cv_trigger = ConditionVariableCancelSleep(); +#endif + + /* + * If wait is terminated by latch set (walsenders' latch is set on each + * wal flush). (no need for pm death check due to WL_EXIT_ON_PM_DEATH) + */ + if ((rc == 1 && event.events & WL_LATCH_SET) || late_cv_trigger) + { + /* Reset our latch */ + ResetLatch(MyLatch); + *events = WL_LATCH_SET; + return 1; + } + + /* + * If the event contains something about the socket, it means we got an + * event from a safekeeper socket. + */ + if (rc == 1 && (event.events & (WL_SOCKET_MASK))) + { + *sk = (Safekeeper *) event.user_data; + *events = event.events; + return 1; + } + + /* XXX: Can we have non-timeout event here? */ + *events = event.events; + return rc; +} + +static void +walprop_pg_finish_sync_safekeepers(XLogRecPtr lsn) +{ + fprintf(stdout, "%X/%X\n", LSN_FORMAT_ARGS(lsn)); + exit(0); +} + +/* + * Get PageserverFeedback fields from the most advanced safekeeper + */ +static void +GetLatestNeonFeedback(PageserverFeedback *rf, WalProposer *wp) +{ + int latest_safekeeper = 0; + XLogRecPtr last_received_lsn = InvalidXLogRecPtr; + + for (int i = 0; i < wp->n_safekeepers; i++) + { + if (wp->safekeeper[i].appendResponse.rf.last_received_lsn > last_received_lsn) + { + latest_safekeeper = i; + last_received_lsn = wp->safekeeper[i].appendResponse.rf.last_received_lsn; + } + } + + rf->currentClusterSize = wp->safekeeper[latest_safekeeper].appendResponse.rf.currentClusterSize; + rf->last_received_lsn = wp->safekeeper[latest_safekeeper].appendResponse.rf.last_received_lsn; + rf->disk_consistent_lsn = wp->safekeeper[latest_safekeeper].appendResponse.rf.disk_consistent_lsn; + rf->remote_consistent_lsn = wp->safekeeper[latest_safekeeper].appendResponse.rf.remote_consistent_lsn; + rf->replytime = wp->safekeeper[latest_safekeeper].appendResponse.rf.replytime; + + elog(DEBUG2, "GetLatestNeonFeedback: currentClusterSize %lu," + " last_received_lsn %X/%X, disk_consistent_lsn %X/%X, remote_consistent_lsn %X/%X, replytime %lu", + rf->currentClusterSize, + LSN_FORMAT_ARGS(rf->last_received_lsn), + LSN_FORMAT_ARGS(rf->disk_consistent_lsn), + LSN_FORMAT_ARGS(rf->remote_consistent_lsn), + rf->replytime); + + replication_feedback_set(rf); +} + +/* + * Combine hot standby feedbacks from all safekeepers. + */ +static void +CombineHotStanbyFeedbacks(HotStandbyFeedback *hs, WalProposer *wp) +{ + hs->ts = 0; + hs->xmin.value = ~0; /* largest unsigned value */ + hs->catalog_xmin.value = ~0; /* largest unsigned value */ + + for (int i = 0; i < wp->n_safekeepers; i++) + { + if (wp->safekeeper[i].appendResponse.hs.ts != 0) + { + HotStandbyFeedback *skhs = &wp->safekeeper[i].appendResponse.hs; + + if (FullTransactionIdIsNormal(skhs->xmin) + && FullTransactionIdPrecedes(skhs->xmin, hs->xmin)) + { + hs->xmin = skhs->xmin; + hs->ts = skhs->ts; + } + if (FullTransactionIdIsNormal(skhs->catalog_xmin) + && FullTransactionIdPrecedes(skhs->catalog_xmin, hs->xmin)) + { + hs->catalog_xmin = skhs->catalog_xmin; + hs->ts = skhs->ts; + } + } + } + + if (hs->xmin.value == ~0) + hs->xmin = InvalidFullTransactionId; + if (hs->catalog_xmin.value == ~0) + hs->catalog_xmin = InvalidFullTransactionId; +} + +static void +walprop_pg_process_safekeeper_feedback(WalProposer *wp, XLogRecPtr commitLsn) +{ + HotStandbyFeedback hsFeedback; + XLogRecPtr diskConsistentLsn; + + diskConsistentLsn = quorumFeedback.rf.disk_consistent_lsn; + + if (!wp->config->syncSafekeepers) + { + /* Get PageserverFeedback fields from the most advanced safekeeper */ + GetLatestNeonFeedback(&quorumFeedback.rf, wp); + SetZenithCurrentClusterSize(quorumFeedback.rf.currentClusterSize); + } + + if (commitLsn > quorumFeedback.flushLsn || diskConsistentLsn != quorumFeedback.rf.disk_consistent_lsn) + { + + if (commitLsn > quorumFeedback.flushLsn) + quorumFeedback.flushLsn = commitLsn; + + /* advance the replication slot */ + if (!wp->config->syncSafekeepers) + ProcessStandbyReply( + /* write_lsn - This is what durably stored in WAL service. */ + quorumFeedback.flushLsn, + /* flush_lsn - This is what durably stored in WAL service. */ + quorumFeedback.flushLsn, + + /* + * apply_lsn - This is what processed and durably saved at* + * pageserver. + */ + quorumFeedback.rf.disk_consistent_lsn, + walprop_pg_get_current_timestamp(), false); + } + + CombineHotStanbyFeedbacks(&hsFeedback, wp); + if (hsFeedback.ts != 0 && memcmp(&hsFeedback, &quorumFeedback.hs, sizeof hsFeedback) != 0) + { + quorumFeedback.hs = hsFeedback; + if (!wp->config->syncSafekeepers) + ProcessStandbyHSFeedback(hsFeedback.ts, + XidFromFullTransactionId(hsFeedback.xmin), + EpochFromFullTransactionId(hsFeedback.xmin), + XidFromFullTransactionId(hsFeedback.catalog_xmin), + EpochFromFullTransactionId(hsFeedback.catalog_xmin)); + } +} + +static void +walprop_pg_confirm_wal_streamed(XLogRecPtr lsn) +{ + if (MyReplicationSlot) + PhysicalConfirmReceivedLocation(lsn); +} + +static const walproposer_api walprop_pg = { + .get_shmem_state = walprop_pg_get_shmem_state, + .start_streaming = walprop_pg_start_streaming, + .get_flush_rec_ptr = walprop_pg_get_flush_rec_ptr, + .get_current_timestamp = walprop_pg_get_current_timestamp, + .get_timeline_id = walprop_pg_get_timeline_id, + .conn_error_message = walprop_error_message, + .conn_status = walprop_status, + .conn_connect_start = walprop_connect_start, + .conn_connect_poll = walprop_connect_poll, + .conn_send_query = walprop_send_query, + .conn_get_query_result = walprop_get_query_result, + .conn_flush = walprop_flush, + .conn_finish = walprop_finish, + .conn_async_read = walprop_async_read, + .conn_async_write = walprop_async_write, + .conn_blocking_write = walprop_blocking_write, + .recovery_download = WalProposerRecovery, + .wal_read = walprop_pg_wal_read, + .wal_reader_allocate = walprop_pg_wal_reader_allocate, + .free_event_set = walprop_pg_free_event_set, + .init_event_set = walprop_pg_init_event_set, + .update_event_set = walprop_pg_update_event_set, + .add_safekeeper_event_set = walprop_pg_add_safekeeper_event_set, + .wait_event_set = walprop_pg_wait_event_set, + .strong_random = pg_strong_random, + .get_redo_start_lsn = GetRedoStartLsn, + .finish_sync_safekeepers = walprop_pg_finish_sync_safekeepers, + .process_safekeeper_feedback = walprop_pg_process_safekeeper_feedback, + .confirm_wal_streamed = walprop_pg_confirm_wal_streamed, +}; diff --git a/pgxn/neon/walproposer_utils.c b/pgxn/neon/walproposer_utils.c deleted file mode 100644 index 05030360f6..0000000000 --- a/pgxn/neon/walproposer_utils.c +++ /dev/null @@ -1,659 +0,0 @@ -#include "postgres.h" - -#include "access/timeline.h" -#include "access/xlogutils.h" -#include "common/logging.h" -#include "common/ip.h" -#include "funcapi.h" -#include "libpq/libpq.h" -#include "libpq/pqformat.h" -#include "miscadmin.h" -#include "postmaster/interrupt.h" -#include "replication/slot.h" -#include "walproposer_utils.h" -#include "replication/walsender_private.h" - -#include "storage/ipc.h" -#include "utils/builtins.h" -#include "utils/ps_status.h" - -#include "libpq-fe.h" -#include -#include - -#if PG_VERSION_NUM >= 150000 -#include "access/xlogutils.h" -#include "access/xlogrecovery.h" -#endif -#if PG_MAJORVERSION_NUM >= 16 -#include "utils/guc.h" -#endif - -/* - * These variables are used similarly to openLogFile/SegNo, - * but for walproposer to write the XLOG during recovery. walpropFileTLI is the TimeLineID - * corresponding the filename of walpropFile. - */ -static int walpropFile = -1; -static TimeLineID walpropFileTLI = 0; -static XLogSegNo walpropSegNo = 0; - -/* START cloned file-local variables and functions from walsender.c */ - -/* - * How far have we sent WAL already? This is also advertised in - * MyWalSnd->sentPtr. (Actually, this is the next WAL location to send.) - */ -static XLogRecPtr sentPtr = InvalidXLogRecPtr; - -static void WalSndLoop(void); -static void XLogBroadcastWalProposer(void); -/* END cloned file-level variables and functions from walsender.c */ - -int -CompareLsn(const void *a, const void *b) -{ - XLogRecPtr lsn1 = *((const XLogRecPtr *) a); - XLogRecPtr lsn2 = *((const XLogRecPtr *) b); - - if (lsn1 < lsn2) - return -1; - else if (lsn1 == lsn2) - return 0; - else - return 1; -} - -/* Returns a human-readable string corresonding to the SafekeeperState - * - * The string should not be freed. - * - * The strings are intended to be used as a prefix to "state", e.g.: - * - * elog(LOG, "currently in %s state", FormatSafekeeperState(sk->state)); - * - * If this sort of phrasing doesn't fit the message, instead use something like: - * - * elog(LOG, "currently in state [%s]", FormatSafekeeperState(sk->state)); - */ -char * -FormatSafekeeperState(SafekeeperState state) -{ - char *return_val = NULL; - - switch (state) - { - case SS_OFFLINE: - return_val = "offline"; - break; - case SS_CONNECTING_READ: - case SS_CONNECTING_WRITE: - return_val = "connecting"; - break; - case SS_WAIT_EXEC_RESULT: - return_val = "receiving query result"; - break; - case SS_HANDSHAKE_RECV: - return_val = "handshake (receiving)"; - break; - case SS_VOTING: - return_val = "voting"; - break; - case SS_WAIT_VERDICT: - return_val = "wait-for-verdict"; - break; - case SS_SEND_ELECTED_FLUSH: - return_val = "send-announcement-flush"; - break; - case SS_IDLE: - return_val = "idle"; - break; - case SS_ACTIVE: - return_val = "active"; - break; - } - - Assert(return_val != NULL); - - return return_val; -} - -/* Asserts that the provided events are expected for given safekeeper's state */ -void -AssertEventsOkForState(uint32 events, Safekeeper *sk) -{ - uint32 expected = SafekeeperStateDesiredEvents(sk->state); - - /* - * The events are in-line with what we're expecting, under two conditions: - * (a) if we aren't expecting anything, `events` has no read- or - * write-ready component. (b) if we are expecting something, there's - * overlap (i.e. `events & expected != 0`) - */ - bool events_ok_for_state; /* long name so the `Assert` is more - * clear later */ - - if (expected == WL_NO_EVENTS) - events_ok_for_state = ((events & (WL_SOCKET_READABLE | WL_SOCKET_WRITEABLE)) == 0); - else - events_ok_for_state = ((events & expected) != 0); - - if (!events_ok_for_state) - { - /* - * To give a descriptive message in the case of failure, we use elog - * and then an assertion that's guaranteed to fail. - */ - elog(WARNING, "events %s mismatched for safekeeper %s:%s in state [%s]", - FormatEvents(events), sk->host, sk->port, FormatSafekeeperState(sk->state)); - Assert(events_ok_for_state); - } -} - -/* Returns the set of events a safekeeper in this state should be waiting on - * - * This will return WL_NO_EVENTS (= 0) for some events. */ -uint32 -SafekeeperStateDesiredEvents(SafekeeperState state) -{ - uint32 result = WL_NO_EVENTS; - - /* If the state doesn't have a modifier, we can check the base state */ - switch (state) - { - /* Connecting states say what they want in the name */ - case SS_CONNECTING_READ: - result = WL_SOCKET_READABLE; - break; - case SS_CONNECTING_WRITE: - result = WL_SOCKET_WRITEABLE; - break; - - /* Reading states need the socket to be read-ready to continue */ - case SS_WAIT_EXEC_RESULT: - case SS_HANDSHAKE_RECV: - case SS_WAIT_VERDICT: - result = WL_SOCKET_READABLE; - break; - - /* - * Idle states use read-readiness as a sign that the connection - * has been disconnected. - */ - case SS_VOTING: - case SS_IDLE: - result = WL_SOCKET_READABLE; - break; - - /* - * Flush states require write-ready for flushing. Active state - * does both reading and writing. - * - * TODO: SS_ACTIVE sometimes doesn't need to be write-ready. We - * should check sk->flushWrite here to set WL_SOCKET_WRITEABLE. - */ - case SS_SEND_ELECTED_FLUSH: - case SS_ACTIVE: - result = WL_SOCKET_READABLE | WL_SOCKET_WRITEABLE; - break; - - /* The offline state expects no events. */ - case SS_OFFLINE: - result = WL_NO_EVENTS; - break; - - default: - Assert(false); - break; - } - - return result; -} - -/* Returns a human-readable string corresponding to the event set - * - * If the events do not correspond to something set as the `events` field of a `WaitEvent`, the - * returned string may be meaingless. - * - * The string should not be freed. It should also not be expected to remain the same between - * function calls. */ -char * -FormatEvents(uint32 events) -{ - static char return_str[8]; - - /* Helper variable to check if there's extra bits */ - uint32 all_flags = WL_LATCH_SET - | WL_SOCKET_READABLE - | WL_SOCKET_WRITEABLE - | WL_TIMEOUT - | WL_POSTMASTER_DEATH - | WL_EXIT_ON_PM_DEATH - | WL_SOCKET_CONNECTED; - - /* - * The formatting here isn't supposed to be *particularly* useful -- it's - * just to give an sense of what events have been triggered without - * needing to remember your powers of two. - */ - - return_str[0] = (events & WL_LATCH_SET) ? 'L' : '_'; - return_str[1] = (events & WL_SOCKET_READABLE) ? 'R' : '_'; - return_str[2] = (events & WL_SOCKET_WRITEABLE) ? 'W' : '_'; - return_str[3] = (events & WL_TIMEOUT) ? 'T' : '_'; - return_str[4] = (events & WL_POSTMASTER_DEATH) ? 'D' : '_'; - return_str[5] = (events & WL_EXIT_ON_PM_DEATH) ? 'E' : '_'; - return_str[5] = (events & WL_SOCKET_CONNECTED) ? 'C' : '_'; - - if (events & (~all_flags)) - { - elog(WARNING, "Event formatting found unexpected component %d", - events & (~all_flags)); - return_str[6] = '*'; - return_str[7] = '\0'; - } - else - return_str[6] = '\0'; - - return (char *) &return_str; -} - -/* - * Convert a character which represents a hexadecimal digit to an integer. - * - * Returns -1 if the character is not a hexadecimal digit. - */ -static int -HexDecodeChar(char c) -{ - if (c >= '0' && c <= '9') - return c - '0'; - if (c >= 'a' && c <= 'f') - return c - 'a' + 10; - if (c >= 'A' && c <= 'F') - return c - 'A' + 10; - - return -1; -} - -/* - * Decode a hex string into a byte string, 2 hex chars per byte. - * - * Returns false if invalid characters are encountered; otherwise true. - */ -bool -HexDecodeString(uint8 *result, char *input, int nbytes) -{ - int i; - - for (i = 0; i < nbytes; ++i) - { - int n1 = HexDecodeChar(input[i * 2]); - int n2 = HexDecodeChar(input[i * 2 + 1]); - - if (n1 < 0 || n2 < 0) - return false; - result[i] = n1 * 16 + n2; - } - - return true; -} - -/* -------------------------------- - * pq_getmsgint32_le - get a binary 4-byte int from a message buffer in native (LE) order - * -------------------------------- - */ -uint32 -pq_getmsgint32_le(StringInfo msg) -{ - uint32 n32; - - pq_copymsgbytes(msg, (char *) &n32, sizeof(n32)); - - return n32; -} - -/* -------------------------------- - * pq_getmsgint64 - get a binary 8-byte int from a message buffer in native (LE) order - * -------------------------------- - */ -uint64 -pq_getmsgint64_le(StringInfo msg) -{ - uint64 n64; - - pq_copymsgbytes(msg, (char *) &n64, sizeof(n64)); - - return n64; -} - -/* append a binary [u]int32 to a StringInfo buffer in native (LE) order */ -void -pq_sendint32_le(StringInfo buf, uint32 i) -{ - enlargeStringInfo(buf, sizeof(uint32)); - memcpy(buf->data + buf->len, &i, sizeof(uint32)); - buf->len += sizeof(uint32); -} - -/* append a binary [u]int64 to a StringInfo buffer in native (LE) order */ -void -pq_sendint64_le(StringInfo buf, uint64 i) -{ - enlargeStringInfo(buf, sizeof(uint64)); - memcpy(buf->data + buf->len, &i, sizeof(uint64)); - buf->len += sizeof(uint64); -} - -/* - * Write XLOG data to disk. - */ -void -XLogWalPropWrite(char *buf, Size nbytes, XLogRecPtr recptr) -{ - int startoff; - int byteswritten; - - while (nbytes > 0) - { - int segbytes; - - /* Close the current segment if it's completed */ - if (walpropFile >= 0 && !XLByteInSeg(recptr, walpropSegNo, wal_segment_size)) - XLogWalPropClose(recptr); - - if (walpropFile < 0) - { -#if PG_VERSION_NUM >= 150000 - /* FIXME Is it ok to use hardcoded value here? */ - TimeLineID tli = 1; -#else - bool use_existent = true; -#endif - /* Create/use new log file */ - XLByteToSeg(recptr, walpropSegNo, wal_segment_size); -#if PG_VERSION_NUM >= 150000 - walpropFile = XLogFileInit(walpropSegNo, tli); - walpropFileTLI = tli; -#else - walpropFile = XLogFileInit(walpropSegNo, &use_existent, false); - walpropFileTLI = ThisTimeLineID; -#endif - } - - /* Calculate the start offset of the received logs */ - startoff = XLogSegmentOffset(recptr, wal_segment_size); - - if (startoff + nbytes > wal_segment_size) - segbytes = wal_segment_size - startoff; - else - segbytes = nbytes; - - /* OK to write the logs */ - errno = 0; - - byteswritten = pg_pwrite(walpropFile, buf, segbytes, (off_t) startoff); - if (byteswritten <= 0) - { - char xlogfname[MAXFNAMELEN]; - int save_errno; - - /* if write didn't set errno, assume no disk space */ - if (errno == 0) - errno = ENOSPC; - - save_errno = errno; - XLogFileName(xlogfname, walpropFileTLI, walpropSegNo, wal_segment_size); - errno = save_errno; - ereport(PANIC, - (errcode_for_file_access(), - errmsg("could not write to log segment %s " - "at offset %u, length %lu: %m", - xlogfname, startoff, (unsigned long) segbytes))); - } - - /* Update state for write */ - recptr += byteswritten; - - nbytes -= byteswritten; - buf += byteswritten; - } - - /* - * Close the current segment if it's fully written up in the last cycle of - * the loop. - */ - if (walpropFile >= 0 && !XLByteInSeg(recptr, walpropSegNo, wal_segment_size)) - { - XLogWalPropClose(recptr); - } -} - -/* - * Close the current segment. - */ -void -XLogWalPropClose(XLogRecPtr recptr) -{ - Assert(walpropFile >= 0 && !XLByteInSeg(recptr, walpropSegNo, wal_segment_size)); - - if (close(walpropFile) != 0) - { - char xlogfname[MAXFNAMELEN]; - - XLogFileName(xlogfname, walpropFileTLI, walpropSegNo, wal_segment_size); - - ereport(PANIC, - (errcode_for_file_access(), - errmsg("could not close log segment %s: %m", - xlogfname))); - } - - walpropFile = -1; -} - -/* START of cloned functions from walsender.c */ - -/* - * Subscribe for new WAL and stream it in the loop to safekeepers. - * - * At the moment, this never returns, but an ereport(ERROR) will take us back - * to the main loop. - */ -void -StartProposerReplication(StartReplicationCmd *cmd) -{ - XLogRecPtr FlushPtr; - TimeLineID currTLI; - -#if PG_VERSION_NUM < 150000 - if (ThisTimeLineID == 0) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("IDENTIFY_SYSTEM has not been run before START_REPLICATION"))); -#endif - - /* - * We assume here that we're logging enough information in the WAL for - * log-shipping, since this is checked in PostmasterMain(). - * - * NOTE: wal_level can only change at shutdown, so in most cases it is - * difficult for there to be WAL data that we can still see that was - * written at wal_level='minimal'. - */ - - if (cmd->slotname) - { - ReplicationSlotAcquire(cmd->slotname, true); - if (SlotIsLogical(MyReplicationSlot)) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot use a logical replication slot for physical replication"))); - - /* - * We don't need to verify the slot's restart_lsn here; instead we - * rely on the caller requesting the starting point to use. If the - * WAL segment doesn't exist, we'll fail later. - */ - } - - /* - * Select the timeline. If it was given explicitly by the client, use - * that. Otherwise use the timeline of the last replayed record, which is - * kept in ThisTimeLineID. - * - * Neon doesn't currently use PG Timelines, but it may in the future, so - * we keep this code around to lighten the load for when we need it. - */ -#if PG_VERSION_NUM >= 150000 - FlushPtr = GetFlushRecPtr(&currTLI); -#else - FlushPtr = GetFlushRecPtr(); - currTLI = ThisTimeLineID; -#endif - - /* - * When we first start replication the standby will be behind the - * primary. For some applications, for example synchronous - * replication, it is important to have a clear state for this initial - * catchup mode, so we can trigger actions when we change streaming - * state later. We may stay in this state for a long time, which is - * exactly why we want to be able to monitor whether or not we are - * still here. - */ - WalSndSetState(WALSNDSTATE_CATCHUP); - - /* - * Don't allow a request to stream from a future point in WAL that - * hasn't been flushed to disk in this server yet. - */ - if (FlushPtr < cmd->startpoint) - { - ereport(ERROR, - (errmsg("requested starting point %X/%X is ahead of the WAL flush position of this server %X/%X", - LSN_FORMAT_ARGS(cmd->startpoint), - LSN_FORMAT_ARGS(FlushPtr)))); - } - - /* Start streaming from the requested point */ - sentPtr = cmd->startpoint; - - /* Initialize shared memory status, too */ - SpinLockAcquire(&MyWalSnd->mutex); - MyWalSnd->sentPtr = sentPtr; - SpinLockRelease(&MyWalSnd->mutex); - - SyncRepInitConfig(); - - /* Infinite send loop, never returns */ - WalSndLoop(); - - WalSndSetState(WALSNDSTATE_STARTUP); - - if (cmd->slotname) - ReplicationSlotRelease(); -} - -/* - * Main loop that waits for LSN updates and calls the walproposer. - * Synchronous replication sets latch in WalSndWakeup at walsender.c - */ -static void -WalSndLoop(void) -{ - /* Clear any already-pending wakeups */ - ResetLatch(MyLatch); - - for (;;) - { - CHECK_FOR_INTERRUPTS(); - - XLogBroadcastWalProposer(); - - if (MyWalSnd->state == WALSNDSTATE_CATCHUP) - WalSndSetState(WALSNDSTATE_STREAMING); - WalProposerPoll(); - } -} - -/* - * Notify walproposer about the new WAL position. - */ -static void -XLogBroadcastWalProposer(void) -{ - XLogRecPtr startptr; - XLogRecPtr endptr; - - /* Start from the last sent position */ - startptr = sentPtr; - - /* - * Streaming the current timeline on a primary. - * - * Attempt to send all data that's already been written out and - * fsync'd to disk. We cannot go further than what's been written out - * given the current implementation of WALRead(). And in any case - * it's unsafe to send WAL that is not securely down to disk on the - * primary: if the primary subsequently crashes and restarts, standbys - * must not have applied any WAL that got lost on the primary. - */ -#if PG_VERSION_NUM >= 150000 - endptr = GetFlushRecPtr(NULL); -#else - endptr = GetFlushRecPtr(); -#endif - - /* - * Record the current system time as an approximation of the time at which - * this WAL location was written for the purposes of lag tracking. - * - * In theory we could make XLogFlush() record a time in shmem whenever WAL - * is flushed and we could get that time as well as the LSN when we call - * GetFlushRecPtr() above (and likewise for the cascading standby - * equivalent), but rather than putting any new code into the hot WAL path - * it seems good enough to capture the time here. We should reach this - * after XLogFlush() runs WalSndWakeupProcessRequests(), and although that - * may take some time, we read the WAL flush pointer and take the time - * very close to together here so that we'll get a later position if it is - * still moving. - * - * Because LagTrackerWrite ignores samples when the LSN hasn't advanced, - * this gives us a cheap approximation for the WAL flush time for this - * LSN. - * - * Note that the LSN is not necessarily the LSN for the data contained in - * the present message; it's the end of the WAL, which might be further - * ahead. All the lag tracking machinery cares about is finding out when - * that arbitrary LSN is eventually reported as written, flushed and - * applied, so that it can measure the elapsed time. - */ - LagTrackerWrite(endptr, GetCurrentTimestamp()); - - /* Do we have any work to do? */ - Assert(startptr <= endptr); - if (endptr <= startptr) - return; - - WalProposerBroadcast(startptr, endptr); - sentPtr = endptr; - - /* Update shared memory status */ - { - WalSnd *walsnd = MyWalSnd; - - SpinLockAcquire(&walsnd->mutex); - walsnd->sentPtr = sentPtr; - SpinLockRelease(&walsnd->mutex); - } - - /* Report progress of XLOG streaming in PS display */ - if (update_process_title) - { - char activitymsg[50]; - - snprintf(activitymsg, sizeof(activitymsg), "streaming %X/%X", - LSN_FORMAT_ARGS(sentPtr)); - set_ps_display(activitymsg); - } -} diff --git a/pgxn/neon/walproposer_utils.h b/pgxn/neon/walproposer_utils.h deleted file mode 100644 index aa5df5fa43..0000000000 --- a/pgxn/neon/walproposer_utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef __NEON_WALPROPOSER_UTILS_H__ -#define __NEON_WALPROPOSER_UTILS_H__ - -#include "walproposer.h" - -int CompareLsn(const void *a, const void *b); -char *FormatSafekeeperState(SafekeeperState state); -void AssertEventsOkForState(uint32 events, Safekeeper *sk); -uint32 SafekeeperStateDesiredEvents(SafekeeperState state); -char *FormatEvents(uint32 events); -bool HexDecodeString(uint8 *result, char *input, int nbytes); -uint32 pq_getmsgint32_le(StringInfo msg); -uint64 pq_getmsgint64_le(StringInfo msg); -void pq_sendint32_le(StringInfo buf, uint32 i); -void pq_sendint64_le(StringInfo buf, uint64 i); -void XLogWalPropWrite(char *buf, Size nbytes, XLogRecPtr recptr); -void XLogWalPropClose(XLogRecPtr recptr); - -#endif /* __NEON_WALPROPOSER_UTILS_H__ */ diff --git a/poetry.lock b/poetry.lock index 70961dc797..efc13f7c87 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2415,18 +2415,18 @@ files = [ [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.17" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, + {file = "urllib3-1.26.17-py2.py3-none-any.whl", hash = "sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b"}, + {file = "urllib3-1.26.17.tar.gz", hash = "sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] +brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -2648,7 +2648,65 @@ files = [ docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +[[package]] +name = "zstandard" +version = "0.21.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zstandard-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:649a67643257e3b2cff1c0a73130609679a5673bf389564bc6d4b164d822a7ce"}, + {file = "zstandard-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:144a4fe4be2e747bf9c646deab212666e39048faa4372abb6a250dab0f347a29"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b72060402524ab91e075881f6b6b3f37ab715663313030d0ce983da44960a86f"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8257752b97134477fb4e413529edaa04fc0457361d304c1319573de00ba796b1"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c053b7c4cbf71cc26808ed67ae955836232f7638444d709bfc302d3e499364fa"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2769730c13638e08b7a983b32cb67775650024632cd0476bf1ba0e6360f5ac7d"}, + {file = "zstandard-0.21.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d3bc4de588b987f3934ca79140e226785d7b5e47e31756761e48644a45a6766"}, + {file = "zstandard-0.21.0-cp310-cp310-win32.whl", hash = "sha256:67829fdb82e7393ca68e543894cd0581a79243cc4ec74a836c305c70a5943f07"}, + {file = "zstandard-0.21.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6048a287f8d2d6e8bc67f6b42a766c61923641dd4022b7fd3f7439e17ba5a4d"}, + {file = "zstandard-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7f2afab2c727b6a3d466faee6974a7dad0d9991241c498e7317e5ccf53dbc766"}, + {file = "zstandard-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff0852da2abe86326b20abae912d0367878dd0854b8931897d44cfeb18985472"}, + {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12fa383e315b62630bd407477d750ec96a0f438447d0e6e496ab67b8b451d39"}, + {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1b9703fe2e6b6811886c44052647df7c37478af1b4a1a9078585806f42e5b15"}, + {file = "zstandard-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df28aa5c241f59a7ab524f8ad8bb75d9a23f7ed9d501b0fed6d40ec3064784e8"}, + {file = "zstandard-0.21.0-cp311-cp311-win32.whl", hash = "sha256:0aad6090ac164a9d237d096c8af241b8dcd015524ac6dbec1330092dba151657"}, + {file = "zstandard-0.21.0-cp311-cp311-win_amd64.whl", hash = "sha256:48b6233b5c4cacb7afb0ee6b4f91820afbb6c0e3ae0fa10abbc20000acdf4f11"}, + {file = "zstandard-0.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e7d560ce14fd209db6adacce8908244503a009c6c39eee0c10f138996cd66d3e"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e6e131a4df2eb6f64961cea6f979cdff22d6e0d5516feb0d09492c8fd36f3bc"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1e0c62a67ff425927898cf43da2cf6b852289ebcc2054514ea9bf121bec10a5"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1545fb9cb93e043351d0cb2ee73fa0ab32e61298968667bb924aac166278c3fc"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6c821eb6870f81d73bf10e5deed80edcac1e63fbc40610e61f340723fd5f7c"}, + {file = "zstandard-0.21.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ddb086ea3b915e50f6604be93f4f64f168d3fc3cef3585bb9a375d5834392d4f"}, + {file = "zstandard-0.21.0-cp37-cp37m-win32.whl", hash = "sha256:57ac078ad7333c9db7a74804684099c4c77f98971c151cee18d17a12649bc25c"}, + {file = "zstandard-0.21.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1243b01fb7926a5a0417120c57d4c28b25a0200284af0525fddba812d575f605"}, + {file = "zstandard-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ea68b1ba4f9678ac3d3e370d96442a6332d431e5050223626bdce748692226ea"}, + {file = "zstandard-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8070c1cdb4587a8aa038638acda3bd97c43c59e1e31705f2766d5576b329e97c"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af612c96599b17e4930fe58bffd6514e6c25509d120f4eae6031b7595912f85"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cff891e37b167bc477f35562cda1248acc115dbafbea4f3af54ec70821090965"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9fec02ce2b38e8b2e86079ff0b912445495e8ab0b137f9c0505f88ad0d61296"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdbe350691dec3078b187b8304e6a9c4d9db3eb2d50ab5b1d748533e746d099"}, + {file = "zstandard-0.21.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b69cccd06a4a0a1d9fb3ec9a97600055cf03030ed7048d4bcb88c574f7895773"}, + {file = "zstandard-0.21.0-cp38-cp38-win32.whl", hash = "sha256:9980489f066a391c5572bc7dc471e903fb134e0b0001ea9b1d3eff85af0a6f1b"}, + {file = "zstandard-0.21.0-cp38-cp38-win_amd64.whl", hash = "sha256:0e1e94a9d9e35dc04bf90055e914077c80b1e0c15454cc5419e82529d3e70728"}, + {file = "zstandard-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2d61675b2a73edcef5e327e38eb62bdfc89009960f0e3991eae5cc3d54718de"}, + {file = "zstandard-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:25fbfef672ad798afab12e8fd204d122fca3bc8e2dcb0a2ba73bf0a0ac0f5f07"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62957069a7c2626ae80023998757e27bd28d933b165c487ab6f83ad3337f773d"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e10ed461e4807471075d4b7a2af51f5234c8f1e2a0c1d37d5ca49aaaad49e8"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cff89a036c639a6a9299bf19e16bfb9ac7def9a7634c52c257166db09d950e7"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52b2b5e3e7670bd25835e0e0730a236f2b0df87672d99d3bf4bf87248aa659fb"}, + {file = "zstandard-0.21.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1367da0dde8ae5040ef0413fb57b5baeac39d8931c70536d5f013b11d3fc3a5"}, + {file = "zstandard-0.21.0-cp39-cp39-win32.whl", hash = "sha256:db62cbe7a965e68ad2217a056107cc43d41764c66c895be05cf9c8b19578ce9c"}, + {file = "zstandard-0.21.0-cp39-cp39-win_amd64.whl", hash = "sha256:a8d200617d5c876221304b0e3fe43307adde291b4a897e7b0617a61611dfff6a"}, + {file = "zstandard-0.21.0.tar.gz", hash = "sha256:f08e3a10d01a247877e4cb61a82a319ea746c356a3786558bed2481e6c405546"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "c40f62277e788011920f4edb6f7392046ee440f792a104c903097415def9a916" +content-hash = "c5981d8d7c2deadd47c823bc35f86f830c8e320b653d2d3718bade1f4d2dabca" diff --git a/proxy/src/bin/pg_sni_router.rs b/proxy/src/bin/pg_sni_router.rs index 849af47cfc..6574500c97 100644 --- a/proxy/src/bin/pg_sni_router.rs +++ b/proxy/src/bin/pg_sni_router.rs @@ -168,6 +168,11 @@ async fn task_main( .instrument(tracing::info_span!("handle_client", ?session_id)) ); } + Some(Err(e)) = connections.join_next(), if !connections.is_empty() => { + if !e.is_panic() && !e.is_cancelled() { + warn!("unexpected error from joined connection task: {e:?}"); + } + } _ = cancellation_token.cancelled() => { drop(listener); break; diff --git a/proxy/src/http/sql_over_http.rs b/proxy/src/http/sql_over_http.rs index b74b3e9646..380e36d530 100644 --- a/proxy/src/http/sql_over_http.rs +++ b/proxy/src/http/sql_over_http.rs @@ -45,7 +45,8 @@ enum Payload { Batch(BatchQueryData), } -const MAX_REQUEST_SIZE: u64 = 1024 * 1024; // 1 MB +const MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024; // 10 MiB +const MAX_REQUEST_SIZE: u64 = 10 * 1024 * 1024; // 10 MiB static RAW_TEXT_OUTPUT: HeaderName = HeaderName::from_static("neon-raw-text-output"); static ARRAY_MODE: HeaderName = HeaderName::from_static("neon-array-mode"); @@ -262,6 +263,8 @@ async fn handle_inner( None => MAX_REQUEST_SIZE + 1, }; + // we don't have a streaming request support yet so this is to prevent OOM + // from a malicious user sending an extremely large request body if request_content_length > MAX_REQUEST_SIZE { return Err(anyhow::anyhow!( "request is too large (max is {MAX_REQUEST_SIZE} bytes)" @@ -384,6 +387,13 @@ async fn query_to_json( let row = row?; *current_size += row.body_len(); rows.push(row); + // we don't have a streaming response support yet so this is to prevent OOM + // from a malicious query (eg a cross join) + if *current_size > MAX_RESPONSE_SIZE { + return Err(anyhow::anyhow!( + "response is too large (max is {MAX_RESPONSE_SIZE} bytes)" + )); + } } // grab the command tag and number of rows affected diff --git a/proxy/src/proxy.rs b/proxy/src/proxy.rs index 71e00ed58f..cef3cea514 100644 --- a/proxy/src/proxy.rs +++ b/proxy/src/proxy.rs @@ -130,6 +130,11 @@ pub async fn task_main( }), ); } + Some(Err(e)) = connections.join_next(), if !connections.is_empty() => { + if !e.is_panic() && !e.is_cancelled() { + warn!("unexpected error from joined connection task: {e:?}"); + } + } _ = cancellation_token.cancelled() => { drop(listener); break; diff --git a/pyproject.toml b/pyproject.toml index 2ff8f6982d..946a64288a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ aiohttp = "3.8.5" pytest-rerunfailures = "^11.1.2" types-pytest-lazy-fixture = "^0.6.3.3" pytest-split = "^0.8.1" +zstandard = "^0.21.0" [tool.poetry.group.dev.dependencies] black = "^23.3.0" diff --git a/safekeeper/Cargo.toml b/safekeeper/Cargo.toml index 393570df6a..64ef9f6997 100644 --- a/safekeeper/Cargo.toml +++ b/safekeeper/Cargo.toml @@ -10,6 +10,8 @@ anyhow.workspace = true async-trait.workspace = true byteorder.workspace = true bytes.workspace = true +camino.workspace = true +camino-tempfile.workspace = true chrono.workspace = true clap = { workspace = true, features = ["derive"] } const_format.workspace = true @@ -36,7 +38,6 @@ tokio = { workspace = true, features = ["fs"] } tokio-io-timeout.workspace = true tokio-postgres.workspace = true toml_edit.workspace = true -tempfile.workspace = true tracing.workspace = true url.workspace = true metrics.workspace = true diff --git a/safekeeper/src/bin/safekeeper.rs b/safekeeper/src/bin/safekeeper.rs index 848b1d7644..763dcd9eb8 100644 --- a/safekeeper/src/bin/safekeeper.rs +++ b/safekeeper/src/bin/safekeeper.rs @@ -2,6 +2,7 @@ // Main entry point for the safekeeper executable // use anyhow::{bail, Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; use clap::Parser; use futures::future::BoxFuture; use futures::stream::FuturesUnordered; @@ -14,7 +15,6 @@ use toml_edit::Document; use std::fs::{self, File}; use std::io::{ErrorKind, Write}; -use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -63,7 +63,7 @@ split), and serving the hardened part further downstream to pageserver(s). struct Args { /// Path to the safekeeper data directory. #[arg(short = 'D', long, default_value = "./")] - datadir: PathBuf, + datadir: Utf8PathBuf, /// Safekeeper node id. #[arg(long)] id: Option, @@ -92,7 +92,7 @@ struct Args { no_sync: bool, /// Dump control file at path specified by this argument and exit. #[arg(long)] - dump_control_file: Option, + dump_control_file: Option, /// Broker endpoint for storage nodes coordination in the form /// http[s]://host:port. In case of https schema TLS is connection is /// established; plaintext otherwise. @@ -128,19 +128,19 @@ struct Args { /// validations of JWT tokens. Empty string is allowed and means disabling /// auth. #[arg(long, verbatim_doc_comment, value_parser = opt_pathbuf_parser)] - pg_auth_public_key_path: Option, + pg_auth_public_key_path: Option, /// If given, enables auth on incoming connections to tenant only WAL /// service endpoint (--listen-pg-tenant-only). Value specifies path to a /// .pem public key used for validations of JWT tokens. Empty string is /// allowed and means disabling auth. #[arg(long, verbatim_doc_comment, value_parser = opt_pathbuf_parser)] - pg_tenant_only_auth_public_key_path: Option, + pg_tenant_only_auth_public_key_path: Option, /// If given, enables auth on incoming connections to http management /// service endpoint (--listen-http). Value specifies path to a .pem public /// key used for validations of JWT tokens. Empty string is allowed and /// means disabling auth. #[arg(long, verbatim_doc_comment, value_parser = opt_pathbuf_parser)] - http_auth_public_key_path: Option, + http_auth_public_key_path: Option, /// Format for logging, either 'plain' or 'json'. #[arg(long, default_value = "plain")] log_format: String, @@ -151,8 +151,8 @@ struct Args { } // Like PathBufValueParser, but allows empty string. -fn opt_pathbuf_parser(s: &str) -> Result { - Ok(PathBuf::from_str(s).unwrap()) +fn opt_pathbuf_parser(s: &str) -> Result { + Ok(Utf8PathBuf::from_str(s).unwrap()) } #[tokio::main(flavor = "current_thread")] @@ -203,7 +203,7 @@ async fn main() -> anyhow::Result<()> { info!("version: {GIT_VERSION}"); let args_workdir = &args.datadir; - let workdir = args_workdir.canonicalize().with_context(|| { + let workdir = args_workdir.canonicalize_utf8().with_context(|| { format!("Failed to get the absolute path for input workdir {args_workdir:?}") })?; @@ -222,7 +222,7 @@ async fn main() -> anyhow::Result<()> { None } Some(path) => { - info!("loading pg auth JWT key from {}", path.display()); + info!("loading pg auth JWT key from {path}"); Some(Arc::new( JwtAuth::from_key_path(path).context("failed to load the auth key")?, )) @@ -234,10 +234,7 @@ async fn main() -> anyhow::Result<()> { None } Some(path) => { - info!( - "loading pg tenant only auth JWT key from {}", - path.display() - ); + info!("loading pg tenant only auth JWT key from {path}"); Some(Arc::new( JwtAuth::from_key_path(path).context("failed to load the auth key")?, )) @@ -249,7 +246,7 @@ async fn main() -> anyhow::Result<()> { None } Some(path) => { - info!("loading http auth JWT key from {}", path.display()); + info!("loading http auth JWT key from {path}"); Some(Arc::new( JwtAuth::from_key_path(path).context("failed to load the auth key")?, )) @@ -447,7 +444,7 @@ async fn start_safekeeper(conf: SafeKeeperConf) -> Result<()> { } /// Determine safekeeper id. -fn set_id(workdir: &Path, given_id: Option) -> Result { +fn set_id(workdir: &Utf8Path, given_id: Option) -> Result { let id_file_path = workdir.join(ID_FILE_NAME); let my_id: NodeId; diff --git a/safekeeper/src/control_file.rs b/safekeeper/src/control_file.rs index 504c2d355d..7aadd67ac6 100644 --- a/safekeeper/src/control_file.rs +++ b/safekeeper/src/control_file.rs @@ -2,12 +2,13 @@ use anyhow::{bail, ensure, Context, Result}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use camino::Utf8PathBuf; use tokio::fs::{self, File}; use tokio::io::AsyncWriteExt; use std::io::Read; use std::ops::Deref; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::time::Instant; use crate::control_file_upgrade::upgrade_control_file; @@ -39,7 +40,7 @@ pub trait Storage: Deref { #[derive(Debug)] pub struct FileStorage { // save timeline dir to avoid reconstructing it every time - timeline_dir: PathBuf, + timeline_dir: Utf8PathBuf, conf: SafeKeeperConf, /// Last state persisted to disk. @@ -174,7 +175,7 @@ impl Storage for FileStorage { let mut control_partial = File::create(&control_partial_path).await.with_context(|| { format!( "failed to create partial control file at: {}", - &control_partial_path.display() + &control_partial_path ) })?; let mut buf: Vec = Vec::new(); @@ -189,13 +190,13 @@ impl Storage for FileStorage { control_partial.write_all(&buf).await.with_context(|| { format!( "failed to write safekeeper state into control file at: {}", - control_partial_path.display() + control_partial_path ) })?; control_partial.flush().await.with_context(|| { format!( "failed to flush safekeeper state into control file at: {}", - control_partial_path.display() + control_partial_path ) })?; @@ -204,7 +205,7 @@ impl Storage for FileStorage { control_partial.sync_all().await.with_context(|| { format!( "failed to sync partial control file at {}", - control_partial_path.display() + control_partial_path ) })?; } @@ -216,12 +217,10 @@ impl Storage for FileStorage { // this sync is not required by any standard but postgres does this (see durable_rename) if !self.conf.no_sync { let new_f = File::open(&control_path).await?; - new_f.sync_all().await.with_context(|| { - format!( - "failed to sync control file at: {}", - &control_path.display() - ) - })?; + new_f + .sync_all() + .await + .with_context(|| format!("failed to sync control file at: {}", &control_path))?; // fsync the directory (linux specific) let tli_dir = File::open(&self.timeline_dir).await?; @@ -250,7 +249,7 @@ mod test { use utils::{id::TenantTimelineId, lsn::Lsn}; fn stub_conf() -> SafeKeeperConf { - let workdir = tempfile::tempdir().unwrap().into_path(); + let workdir = camino_tempfile::tempdir().unwrap().into_path(); SafeKeeperConf { workdir, ..SafeKeeperConf::dummy() diff --git a/safekeeper/src/debug_dump.rs b/safekeeper/src/debug_dump.rs index 387b577a13..ee9d7118c6 100644 --- a/safekeeper/src/debug_dump.rs +++ b/safekeeper/src/debug_dump.rs @@ -7,6 +7,7 @@ use std::io::Read; use std::path::PathBuf; use anyhow::Result; +use camino::Utf8Path; use chrono::{DateTime, Utc}; use postgres_ffi::XLogSegNo; use serde::Deserialize; @@ -201,7 +202,7 @@ pub async fn build(args: Args) -> Result { /// Builds DiskContent from a directory path. It can fail if the directory /// is deleted between the time we get the path and the time we try to open it. -fn build_disk_content(path: &std::path::Path) -> Result { +fn build_disk_content(path: &Utf8Path) -> Result { let mut files = Vec::new(); for entry in fs::read_dir(path)? { if entry.is_err() { @@ -256,7 +257,7 @@ fn build_file_info(entry: DirEntry) -> Result { fn build_config(config: SafeKeeperConf) -> Config { Config { id: config.my_id, - workdir: config.workdir, + workdir: config.workdir.into(), listen_pg_addr: config.listen_pg_addr, listen_http_addr: config.listen_http_addr, no_sync: config.no_sync, diff --git a/safekeeper/src/lib.rs b/safekeeper/src/lib.rs index d785d0e53a..00aa405047 100644 --- a/safekeeper/src/lib.rs +++ b/safekeeper/src/lib.rs @@ -1,8 +1,8 @@ +use camino::Utf8PathBuf; use once_cell::sync::Lazy; use remote_storage::RemoteStorageConfig; use tokio::runtime::Runtime; -use std::path::PathBuf; use std::time::Duration; use storage_broker::Uri; @@ -51,7 +51,7 @@ pub struct SafeKeeperConf { // that during unit testing, because the current directory is global // to the process but different unit tests work on different // data directories to avoid clashing with each other. - pub workdir: PathBuf, + pub workdir: Utf8PathBuf, pub my_id: NodeId, pub listen_pg_addr: String, pub listen_pg_addr_tenant_only: Option, @@ -73,11 +73,11 @@ pub struct SafeKeeperConf { } impl SafeKeeperConf { - pub fn tenant_dir(&self, tenant_id: &TenantId) -> PathBuf { + pub fn tenant_dir(&self, tenant_id: &TenantId) -> Utf8PathBuf { self.workdir.join(tenant_id.to_string()) } - pub fn timeline_dir(&self, ttid: &TenantTimelineId) -> PathBuf { + pub fn timeline_dir(&self, ttid: &TenantTimelineId) -> Utf8PathBuf { self.tenant_dir(&ttid.tenant_id) .join(ttid.timeline_id.to_string()) } @@ -87,7 +87,7 @@ impl SafeKeeperConf { #[cfg(test)] fn dummy() -> Self { SafeKeeperConf { - workdir: PathBuf::from("./"), + workdir: Utf8PathBuf::from("./"), no_sync: false, listen_pg_addr: defaults::DEFAULT_PG_LISTEN_ADDR.to_string(), listen_pg_addr_tenant_only: None, diff --git a/safekeeper/src/pull_timeline.rs b/safekeeper/src/pull_timeline.rs index a2ed4c0cf4..1343bba5cc 100644 --- a/safekeeper/src/pull_timeline.rs +++ b/safekeeper/src/pull_timeline.rs @@ -167,11 +167,11 @@ async fn pull_timeline(status: TimelineStatus, host: String) -> Result tokio::fs::create_dir_all(&temp_base).await?; - let tli_dir = tempfile::Builder::new() + let tli_dir = camino_tempfile::Builder::new() .suffix("_temptli") .prefix(&format!("{}_{}_", ttid.tenant_id, ttid.timeline_id)) .tempdir_in(temp_base)?; - let tli_dir_path = tli_dir.path().to_owned(); + let tli_dir_path = tli_dir.path().to_path_buf(); // Note: some time happens between fetching list of files and fetching files themselves. // It's possible that some files will be removed from safekeeper and we will fail to fetch them. @@ -220,9 +220,7 @@ async fn pull_timeline(status: TimelineStatus, host: String) -> Result info!( "Moving timeline {} from {} to {}", - ttid, - tli_dir_path.display(), - timeline_path.display() + ttid, tli_dir_path, timeline_path ); tokio::fs::create_dir_all(conf.tenant_dir(&ttid.tenant_id)).await?; tokio::fs::rename(tli_dir_path, &timeline_path).await?; diff --git a/safekeeper/src/safekeeper.rs b/safekeeper/src/safekeeper.rs index b9fcd2c0b2..85e556aff2 100644 --- a/safekeeper/src/safekeeper.rs +++ b/safekeeper/src/safekeeper.rs @@ -456,7 +456,7 @@ impl ProposerAcceptorMessage { Ok(ProposerAcceptorMessage::AppendRequest(msg)) } - _ => bail!("unknown proposer-acceptor message tag: {}", tag,), + _ => bail!("unknown proposer-acceptor message tag: {}", tag), } } } diff --git a/safekeeper/src/timeline.rs b/safekeeper/src/timeline.rs index 3e066de34f..37821eae23 100644 --- a/safekeeper/src/timeline.rs +++ b/safekeeper/src/timeline.rs @@ -2,6 +2,7 @@ //! to glue together SafeKeeper and all other background services. use anyhow::{anyhow, bail, Result}; +use camino::Utf8PathBuf; use postgres_ffi::XLogSegNo; use serde::{Deserialize, Serialize}; use serde_with::serde_as; @@ -9,7 +10,6 @@ use tokio::fs; use serde_with::DisplayFromStr; use std::cmp::max; -use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{Mutex, MutexGuard}; use tokio::{ @@ -331,7 +331,7 @@ pub struct Timeline { cancellation_rx: watch::Receiver, /// Directory where timeline state is stored. - pub timeline_dir: PathBuf, + pub timeline_dir: Utf8PathBuf, } impl Timeline { @@ -723,9 +723,9 @@ impl Timeline { if horizon_segno <= 1 || horizon_segno <= shared_state.last_removed_segno { return Ok(()); // nothing to do } - let remover = shared_state.sk.wal_store.remove_up_to(horizon_segno - 1); + // release the lock before removing - remover + shared_state.sk.wal_store.remove_up_to(horizon_segno - 1) }; // delete old WAL files @@ -805,7 +805,7 @@ impl Timeline { } /// Deletes directory and it's contents. Returns false if directory does not exist. -async fn delete_dir(path: &PathBuf) -> Result { +async fn delete_dir(path: &Utf8PathBuf) -> Result { match fs::remove_dir_all(path).await { Ok(_) => Ok(true), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), diff --git a/safekeeper/src/timelines_global_map.rs b/safekeeper/src/timelines_global_map.rs index 2f591655a9..1434644123 100644 --- a/safekeeper/src/timelines_global_map.rs +++ b/safekeeper/src/timelines_global_map.rs @@ -6,10 +6,10 @@ use crate::safekeeper::ServerInfo; use crate::timeline::{Timeline, TimelineError}; use crate::SafeKeeperConf; use anyhow::{bail, Context, Result}; +use camino::Utf8PathBuf; use once_cell::sync::Lazy; use serde::Serialize; use std::collections::HashMap; -use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc::Sender; @@ -89,7 +89,7 @@ impl GlobalTimelines { }; let mut tenant_count = 0; for tenants_dir_entry in std::fs::read_dir(&tenants_dir) - .with_context(|| format!("failed to list tenants dir {}", tenants_dir.display()))? + .with_context(|| format!("failed to list tenants dir {}", tenants_dir))? { match &tenants_dir_entry { Ok(tenants_dir_entry) => { @@ -102,9 +102,7 @@ impl GlobalTimelines { } Err(e) => error!( "failed to list tenants dir entry {:?} in directory {}, reason: {:?}", - tenants_dir_entry, - tenants_dir.display(), - e + tenants_dir_entry, tenants_dir, e ), } } @@ -136,7 +134,7 @@ impl GlobalTimelines { let timelines_dir = conf.tenant_dir(&tenant_id); for timelines_dir_entry in std::fs::read_dir(&timelines_dir) - .with_context(|| format!("failed to list timelines dir {}", timelines_dir.display()))? + .with_context(|| format!("failed to list timelines dir {}", timelines_dir))? { match &timelines_dir_entry { Ok(timeline_dir_entry) => { @@ -168,9 +166,7 @@ impl GlobalTimelines { } Err(e) => error!( "failed to list timelines dir entry {:?} in directory {}, reason: {:?}", - timelines_dir_entry, - timelines_dir.display(), - e + timelines_dir_entry, timelines_dir, e ), } } @@ -421,7 +417,7 @@ pub struct TimelineDeleteForceResult { } /// Deletes directory and it's contents. Returns false if directory does not exist. -fn delete_dir(path: PathBuf) -> Result { +fn delete_dir(path: Utf8PathBuf) -> Result { match std::fs::remove_dir_all(path) { Ok(_) => Ok(true), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), diff --git a/safekeeper/src/wal_backup.rs b/safekeeper/src/wal_backup.rs index eae3f3fe86..da8c197411 100644 --- a/safekeeper/src/wal_backup.rs +++ b/safekeeper/src/wal_backup.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; use futures::stream::FuturesOrdered; use futures::StreamExt; use tokio::task::JoinHandle; @@ -7,7 +8,6 @@ use utils::id::NodeId; use std::cmp::min; use std::collections::HashMap; -use std::path::{Path, PathBuf}; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; @@ -230,8 +230,8 @@ pub async fn wal_backup_launcher_task_main( struct WalBackupTask { timeline: Arc, - timeline_dir: PathBuf, - workspace_dir: PathBuf, + timeline_dir: Utf8PathBuf, + workspace_dir: Utf8PathBuf, wal_seg_size: usize, parallel_jobs: usize, commit_lsn_watch_rx: watch::Receiver, @@ -240,8 +240,8 @@ struct WalBackupTask { /// Offload single timeline. async fn backup_task_main( ttid: TenantTimelineId, - timeline_dir: PathBuf, - workspace_dir: PathBuf, + timeline_dir: Utf8PathBuf, + workspace_dir: Utf8PathBuf, parallel_jobs: usize, mut shutdown_rx: Receiver<()>, ) { @@ -351,8 +351,8 @@ pub async fn backup_lsn_range( backup_lsn: &mut Lsn, end_lsn: Lsn, wal_seg_size: usize, - timeline_dir: &Path, - workspace_dir: &Path, + timeline_dir: &Utf8Path, + workspace_dir: &Utf8Path, parallel_jobs: usize, ) -> Result<()> { if parallel_jobs < 1 { @@ -408,8 +408,8 @@ pub async fn backup_lsn_range( async fn backup_single_segment( seg: &Segment, - timeline_dir: &Path, - workspace_dir: &Path, + timeline_dir: &Utf8Path, + workspace_dir: &Utf8Path, ) -> Result { let segment_file_path = seg.file_path(timeline_dir)?; let remote_segment_path = segment_file_path @@ -429,7 +429,7 @@ async fn backup_single_segment( BACKUP_ERRORS.inc(); } res?; - debug!("Backup of {} done", segment_file_path.display()); + debug!("Backup of {} done", segment_file_path); Ok(*seg) } @@ -454,7 +454,7 @@ impl Segment { XLogFileName(PG_TLI, self.seg_no, self.size()) } - pub fn file_path(self, timeline_dir: &Path) -> Result { + pub fn file_path(self, timeline_dir: &Utf8Path) -> Result { Ok(timeline_dir.join(self.object_name())) } @@ -479,19 +479,22 @@ fn get_segments(start: Lsn, end: Lsn, seg_size: usize) -> Vec { static REMOTE_STORAGE: OnceCell> = OnceCell::new(); -async fn backup_object(source_file: &Path, target_file: &RemotePath, size: usize) -> Result<()> { +async fn backup_object( + source_file: &Utf8Path, + target_file: &RemotePath, + size: usize, +) -> Result<()> { let storage = REMOTE_STORAGE .get() .expect("failed to get remote storage") .as_ref() .unwrap(); - let file = tokio::io::BufReader::new(File::open(&source_file).await.with_context(|| { - format!( - "Failed to open file {} for wal backup", - source_file.display() - ) - })?); + let file = tokio::io::BufReader::new( + File::open(&source_file) + .await + .with_context(|| format!("Failed to open file {} for wal backup", source_file))?, + ); storage .upload_storage_object(Box::new(file), size, target_file) diff --git a/safekeeper/src/wal_storage.rs b/safekeeper/src/wal_storage.rs index 4ee66ddc8e..2070122e8e 100644 --- a/safekeeper/src/wal_storage.rs +++ b/safekeeper/src/wal_storage.rs @@ -9,13 +9,13 @@ use anyhow::{bail, Context, Result}; use bytes::Bytes; +use camino::{Utf8Path, Utf8PathBuf}; use futures::future::BoxFuture; use postgres_ffi::v14::xlog_utils::{IsPartialXLogFileName, IsXLogFileName, XLogFromFileName}; use postgres_ffi::{dispatch_pgversion, XLogSegNo, PG_TLI}; use remote_storage::RemotePath; use std::cmp::{max, min}; use std::io::{self, SeekFrom}; -use std::path::{Path, PathBuf}; use std::pin::Pin; use tokio::fs::{self, remove_file, File, OpenOptions}; use tokio::io::{AsyncRead, AsyncWriteExt}; @@ -72,7 +72,7 @@ pub trait Storage { /// When storage is created first time, all LSNs are zeroes and there are no segments on disk. pub struct PhysicalStorage { metrics: WalStorageMetrics, - timeline_dir: PathBuf, + timeline_dir: Utf8PathBuf, conf: SafeKeeperConf, /// Size of WAL segment in bytes. @@ -123,7 +123,7 @@ impl PhysicalStorage { /// the disk. Otherwise, all LSNs are set to zero. pub fn new( ttid: &TenantTimelineId, - timeline_dir: PathBuf, + timeline_dir: Utf8PathBuf, conf: &SafeKeeperConf, state: &SafeKeeperState, ) -> Result { @@ -142,7 +142,11 @@ impl PhysicalStorage { dispatch_pgversion!( version, - pgv::xlog_utils::find_end_of_wal(&timeline_dir, wal_seg_size, state.commit_lsn,)?, + pgv::xlog_utils::find_end_of_wal( + timeline_dir.as_std_path(), + wal_seg_size, + state.commit_lsn, + )?, bail!("unsupported postgres version: {}", version) ) }; @@ -458,7 +462,7 @@ impl Storage for PhysicalStorage { /// Remove all WAL segments in timeline_dir that match the given predicate. async fn remove_segments_from_disk( - timeline_dir: &Path, + timeline_dir: &Utf8Path, wal_seg_size: usize, remove_predicate: impl Fn(XLogSegNo) -> bool, ) -> Result<()> { @@ -497,8 +501,8 @@ async fn remove_segments_from_disk( } pub struct WalReader { - workdir: PathBuf, - timeline_dir: PathBuf, + workdir: Utf8PathBuf, + timeline_dir: Utf8PathBuf, wal_seg_size: usize, pos: Lsn, wal_segment: Option>>, @@ -519,8 +523,8 @@ pub struct WalReader { impl WalReader { pub fn new( - workdir: PathBuf, - timeline_dir: PathBuf, + workdir: Utf8PathBuf, + timeline_dir: Utf8PathBuf, state: &SafeKeeperState, start_pos: Lsn, enable_remote_read: bool, @@ -687,7 +691,7 @@ impl WalReader { } /// Helper function for opening a wal file. - async fn open_wal_file(wal_file_path: &Path) -> Result { + async fn open_wal_file(wal_file_path: &Utf8Path) -> Result { // First try to open the .partial file. let mut partial_path = wal_file_path.to_owned(); partial_path.set_extension("partial"); @@ -722,10 +726,10 @@ async fn write_zeroes(file: &mut File, mut count: usize) -> Result<()> { /// Helper returning full path to WAL segment file and its .partial brother. fn wal_file_paths( - timeline_dir: &Path, + timeline_dir: &Utf8Path, segno: XLogSegNo, wal_seg_size: usize, -) -> Result<(PathBuf, PathBuf)> { +) -> Result<(Utf8PathBuf, Utf8PathBuf)> { let wal_file_name = XLogFileName(PG_TLI, segno, wal_seg_size); let wal_file_path = timeline_dir.join(wal_file_name.clone()); let wal_file_partial_path = timeline_dir.join(wal_file_name + ".partial"); diff --git a/test_runner/fixtures/neon_fixtures.py b/test_runner/fixtures/neon_fixtures.py index 92e7cd06cd..d9a75637b8 100644 --- a/test_runner/fixtures/neon_fixtures.py +++ b/test_runner/fixtures/neon_fixtures.py @@ -37,6 +37,7 @@ from psycopg2.extensions import connection as PgConnection from psycopg2.extensions import cursor as PgCursor from psycopg2.extensions import make_dsn, parse_dsn from typing_extensions import Literal +from urllib3.util.retry import Retry from fixtures.broker import NeonBroker from fixtures.log_helper import log @@ -1084,15 +1085,32 @@ class AbstractNeonCli(abc.ABC): stderr=subprocess.PIPE, timeout=timeout, ) + + indent = " " if not res.returncode: - log.info(f"Run {res.args} success: {res.stdout}") + stripped = res.stdout.strip() + lines = stripped.splitlines() + if len(lines) < 2: + log.debug(f"Run {res.args} success: {stripped}") + else: + log.debug("Run %s success:\n%s" % (res.args, textwrap.indent(stripped, indent))) elif check_return_code: # this way command output will be in recorded and shown in CI in failure message - msg = f"""\ - Run {res.args} failed: - stdout: {res.stdout} - stderr: {res.stderr} + indent = indent * 2 + msg = textwrap.dedent( + """\ + Run %s failed: + stdout: + %s + stderr: + %s """ + ) + msg = msg % ( + res.args, + textwrap.indent(res.stdout.strip(), indent), + textwrap.indent(res.stderr.strip(), indent), + ) log.info(msg) raise RuntimeError(msg) from subprocess.CalledProcessError( res.returncode, res.args, res.stdout, res.stderr @@ -1446,6 +1464,29 @@ class NeonCli(AbstractNeonCli): return self.raw_cli(args, check_return_code=check_return_code) + def map_branch( + self, name: str, tenant_id: TenantId, timeline_id: TimelineId + ) -> "subprocess.CompletedProcess[str]": + """ + Map tenant id and timeline id to a neon_local branch name. They do not have to exist. + Usually needed when creating branches via PageserverHttpClient and not neon_local. + + After creating a name mapping, you can use EndpointFactory.create_start + with this registered branch name. + """ + args = [ + "mappings", + "map", + "--branch-name", + name, + "--tenant-id", + str(tenant_id), + "--timeline-id", + str(timeline_id), + ] + + return self.raw_cli(args, check_return_code=True) + def start(self, check_return_code=True) -> "subprocess.CompletedProcess[str]": return self.raw_cli(["start"], check_return_code=check_return_code) @@ -1651,11 +1692,14 @@ class NeonPageserver(PgProtocol): if '"testing"' not in self.version: pytest.skip("pageserver was built without 'testing' feature") - def http_client(self, auth_token: Optional[str] = None) -> PageserverHttpClient: + def http_client( + self, auth_token: Optional[str] = None, retries: Optional[Retry] = None + ) -> PageserverHttpClient: return PageserverHttpClient( port=self.service_port.http, auth_token=auth_token, is_testing_enabled_or_skip=self.is_testing_enabled_or_skip, + retries=retries, ) @property diff --git a/test_runner/fixtures/pageserver/http.py b/test_runner/fixtures/pageserver/http.py index 9fdcd22bc2..460a30ad56 100644 --- a/test_runner/fixtures/pageserver/http.py +++ b/test_runner/fixtures/pageserver/http.py @@ -7,6 +7,8 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Tuple import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry from fixtures.log_helper import log from fixtures.metrics import Metrics, parse_metrics @@ -113,12 +115,40 @@ class TenantConfig: class PageserverHttpClient(requests.Session): - def __init__(self, port: int, is_testing_enabled_or_skip: Fn, auth_token: Optional[str] = None): + def __init__( + self, + port: int, + is_testing_enabled_or_skip: Fn, + auth_token: Optional[str] = None, + retries: Optional[Retry] = None, + ): super().__init__() self.port = port self.auth_token = auth_token self.is_testing_enabled_or_skip = is_testing_enabled_or_skip + if retries is None: + # We apply a retry policy that is different to the default `requests` behavior, + # because the pageserver has various transiently unavailable states that benefit + # from a client retrying on 503 + + retries = Retry( + # Status retries are for retrying on 503 while e.g. waiting for tenants to activate + status=5, + # Connection retries are for waiting for the pageserver to come up and listen + connect=5, + # No read retries: if a request hangs that is not expected behavior + # (this may change in future if we do fault injection of a kind that causes + # requests TCP flows to stick) + read=False, + backoff_factor=0, + status_forcelist=[503], + allowed_methods=None, + remove_headers_on_redirect=[], + ) + + self.mount("http://", HTTPAdapter(max_retries=retries)) + if auth_token is not None: self.headers["Authorization"] = f"Bearer {auth_token}" diff --git a/test_runner/fixtures/pageserver/utils.py b/test_runner/fixtures/pageserver/utils.py index 70c2a06a07..e54b5408b4 100644 --- a/test_runner/fixtures/pageserver/utils.py +++ b/test_runner/fixtures/pageserver/utils.py @@ -74,11 +74,14 @@ def wait_until_tenant_state( for _ in range(iterations): try: tenant = pageserver_http.tenant_status(tenant_id=tenant_id) + except Exception as e: + log.debug(f"Tenant {tenant_id} state retrieval failure: {e}") + else: log.debug(f"Tenant {tenant_id} data: {tenant}") if tenant["state"]["slug"] == expected_state: return tenant - except Exception as e: - log.debug(f"Tenant {tenant_id} state retrieval failure: {e}") + if tenant["state"]["slug"] == "Broken": + raise RuntimeError(f"tenant became Broken, not {expected_state}") time.sleep(period) diff --git a/test_runner/fixtures/utils.py b/test_runner/fixtures/utils.py index 46ab446f99..e54b82dfb4 100644 --- a/test_runner/fixtures/utils.py +++ b/test_runner/fixtures/utils.py @@ -3,7 +3,6 @@ import json import os import re import subprocess -import tarfile import threading import time from pathlib import Path @@ -11,6 +10,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Ty from urllib.parse import urlencode import allure +import zstandard from psycopg2.extensions import cursor from fixtures.log_helper import log @@ -222,7 +222,7 @@ def get_scale_for_db(size_mb: int) -> int: ATTACHMENT_NAME_REGEX: re.Pattern = re.compile( # type: ignore[type-arg] - r"regression\.diffs|.+\.(?:log|stderr|stdout|filediff|metrics|html)" + r"regression\.diffs|.+\.(?:log|stderr|stdout|filediff|metrics|html|walredo)" ) @@ -231,25 +231,35 @@ def allure_attach_from_dir(dir: Path): for attachment in Path(dir).glob("**/*"): if ATTACHMENT_NAME_REGEX.fullmatch(attachment.name) and attachment.stat().st_size > 0: - source = str(attachment) name = str(attachment.relative_to(dir)) - # compress files larger than 1Mb, they're hardly readable in a browser - if attachment.stat().st_size > 1024 * 1024: - source = f"{attachment}.tar.gz" - with tarfile.open(source, "w:gz") as tar: - tar.add(attachment, arcname=attachment.name) - name = f"{name}.tar.gz" + # compress files that are larger than 1Mb, they're hardly readable in a browser + if attachment.stat().st_size > 1024**2: + compressed = attachment.with_suffix(".zst") - if source.endswith(".tar.gz"): + cctx = zstandard.ZstdCompressor() + with attachment.open("rb") as fin, compressed.open("wb") as fout: + cctx.copy_stream(fin, fout) + + name = f"{name}.zst" + attachment = compressed + + source = str(attachment) + if source.endswith(".gz"): attachment_type = "application/gzip" - extension = "tar.gz" + extension = "gz" + elif source.endswith(".zst"): + attachment_type = "application/zstd" + extension = "zst" elif source.endswith(".svg"): attachment_type = "image/svg+xml" extension = "svg" elif source.endswith(".html"): attachment_type = "text/html" extension = "html" + elif source.endswith(".walredo"): + attachment_type = "application/octet-stream" + extension = "walredo" else: attachment_type = "text/plain" extension = attachment.suffix.removeprefix(".") diff --git a/test_runner/regress/test_branching.py b/test_runner/regress/test_branching.py index 31f9df6ebe..2541d5d475 100644 --- a/test_runner/regress/test_branching.py +++ b/test_runner/regress/test_branching.py @@ -1,14 +1,24 @@ import random import threading import time -from typing import List +from queue import SimpleQueue +from typing import Any, Dict, List, Union import pytest from fixtures.log_helper import log -from fixtures.neon_fixtures import Endpoint, NeonEnv, PgBin -from fixtures.types import Lsn +from fixtures.neon_fixtures import ( + Endpoint, + NeonEnv, + NeonEnvBuilder, + PgBin, +) +from fixtures.pageserver.http import PageserverApiException +from fixtures.pageserver.utils import wait_until_tenant_active +from fixtures.types import Lsn, TimelineId from fixtures.utils import query_scalar from performance.test_perf_pgbench import get_scales_matrix +from requests import RequestException +from requests.exceptions import RetryError # Test branch creation @@ -128,3 +138,245 @@ def test_branching_unnormalized_start_lsn(neon_simple_env: NeonEnv, pg_bin: PgBi endpoint1 = env.endpoints.create_start("b1") pg_bin.run_capture(["pgbench", "-i", endpoint1.connstr()]) + + +def test_cannot_create_endpoint_on_non_uploaded_timeline(neon_env_builder: NeonEnvBuilder): + """ + Endpoint should not be possible to create because branch has not been uploaded. + """ + + env = neon_env_builder.init_configs() + env.start() + + env.pageserver.allowed_errors.append( + ".*request{method=POST path=/v1/tenant/.*/timeline request_id=.*}: request was dropped before completing.*" + ) + env.pageserver.allowed_errors.append( + ".*page_service_conn_main.*: query handler for 'basebackup .* is not active, state: Loading" + ) + ps_http = env.pageserver.http_client() + + # pause all uploads + ps_http.configure_failpoints(("before-upload-index-pausable", "pause")) + ps_http.tenant_create(env.initial_tenant) + + initial_branch = "initial_branch" + + def start_creating_timeline(): + with pytest.raises(RequestException): + ps_http.timeline_create( + env.pg_version, env.initial_tenant, env.initial_timeline, timeout=60 + ) + + t = threading.Thread(target=start_creating_timeline) + try: + t.start() + + wait_until_paused(env, "before-upload-index-pausable") + + env.neon_cli.map_branch(initial_branch, env.initial_tenant, env.initial_timeline) + + with pytest.raises(RuntimeError, match="is not active, state: Loading"): + env.endpoints.create_start(initial_branch, tenant_id=env.initial_tenant) + finally: + # FIXME: paused uploads bother shutdown + env.pageserver.stop(immediate=True) + + t.join() + + +def test_cannot_branch_from_non_uploaded_branch(neon_env_builder: NeonEnvBuilder): + """ + Branch should not be possible to create because ancestor has not been uploaded. + """ + + env = neon_env_builder.init_configs() + env.start() + + env.pageserver.allowed_errors.append( + ".*request{method=POST path=/v1/tenant/.*/timeline request_id=.*}: request was dropped before completing.*" + ) + ps_http = env.pageserver.http_client() + + # pause all uploads + ps_http.configure_failpoints(("before-upload-index-pausable", "pause")) + ps_http.tenant_create(env.initial_tenant) + + def start_creating_timeline(): + with pytest.raises(RequestException): + ps_http.timeline_create( + env.pg_version, env.initial_tenant, env.initial_timeline, timeout=60 + ) + + t = threading.Thread(target=start_creating_timeline) + try: + t.start() + + wait_until_paused(env, "before-upload-index-pausable") + + branch_id = TimelineId.generate() + + with pytest.raises(RetryError, match="too many 503 error responses"): + ps_http.timeline_create( + env.pg_version, + env.initial_tenant, + branch_id, + ancestor_timeline_id=env.initial_timeline, + ) + + with pytest.raises( + PageserverApiException, + match=f"NotFound: Timeline {env.initial_tenant}/{branch_id} was not found", + ): + ps_http.timeline_detail(env.initial_tenant, branch_id) + # important to note that a task might still be in progress to complete + # the work, but will never get to that because we have the pause + # failpoint + finally: + # FIXME: paused uploads bother shutdown + env.pageserver.stop(immediate=True) + + t.join() + + +def test_competing_branchings_from_loading_race_to_ok_or_err(neon_env_builder: NeonEnvBuilder): + """ + If the activate only after upload is used, then retries could become competing. + """ + + env = neon_env_builder.init_configs() + env.start() + + env.pageserver.allowed_errors.append( + ".*request{method=POST path=/v1/tenant/.*/timeline request_id=.*}: request was dropped before completing.*" + ) + env.pageserver.allowed_errors.append( + ".*Error processing HTTP request: InternalServerError\\(Timeline .*/.* already exists in pageserver's memory" + ) + ps_http = env.pageserver.http_client() + + # pause all uploads + ps_http.configure_failpoints(("before-upload-index-pausable", "pause")) + ps_http.tenant_create(env.initial_tenant) + + def start_creating_timeline(): + ps_http.timeline_create( + env.pg_version, env.initial_tenant, env.initial_timeline, timeout=60 + ) + + create_root = threading.Thread(target=start_creating_timeline) + + branch_id = TimelineId.generate() + + queue: SimpleQueue[Union[Dict[Any, Any], Exception]] = SimpleQueue() + barrier = threading.Barrier(3) + + def try_branch(): + barrier.wait() + barrier.wait() + try: + ret = ps_http.timeline_create( + env.pg_version, + env.initial_tenant, + branch_id, + ancestor_timeline_id=env.initial_timeline, + timeout=5, + ) + queue.put(ret) + except Exception as e: + queue.put(e) + + threads = [threading.Thread(target=try_branch) for _ in range(2)] + + try: + create_root.start() + + for t in threads: + t.start() + + wait_until_paused(env, "before-upload-index-pausable") + + barrier.wait() + ps_http.configure_failpoints(("before-upload-index-pausable", "off")) + barrier.wait() + + # now both requests race to branch, only one can win because they take gc_cs, Tenant::timelines or marker files + first = queue.get() + second = queue.get() + + log.info(first) + log.info(second) + + (succeeded, failed) = (first, second) if isinstance(second, Exception) else (second, first) + assert isinstance(failed, Exception) + assert isinstance(succeeded, Dict) + + # FIXME: there's probably multiple valid status codes: + # - Timeline 62505b9a9f6b1d29117b1b74eaf07b12/56cd19d3b2dbcc65e9d53ec6ca304f24 already exists + # - whatever 409 response says, but that is a subclass of PageserverApiException + assert isinstance(failed, PageserverApiException) + assert succeeded["state"] == "Active" + finally: + # we might still have the failpoint active + env.pageserver.stop(immediate=True) + + # pytest should nag if we leave threads unjoined + for t in threads: + t.join() + create_root.join() + + +def test_non_uploaded_branch_availability_after_restart(neon_env_builder: NeonEnvBuilder): + """ + Currently before RFC#27 we keep and continue uploading branches which were not successfully uploaded before shutdown. + + This test likely duplicates some other test, but it's easier to write one than to make sure there will be a failing test when the rfc is implemented. + """ + + env = neon_env_builder.init_configs() + env.start() + + env.pageserver.allowed_errors.append( + ".*request{method=POST path=/v1/tenant/.*/timeline request_id=.*}: request was dropped before completing.*" + ) + ps_http = env.pageserver.http_client() + + # pause all uploads + ps_http.configure_failpoints(("before-upload-index-pausable", "pause")) + ps_http.tenant_create(env.initial_tenant) + + def start_creating_timeline(): + with pytest.raises(RequestException): + ps_http.timeline_create( + env.pg_version, env.initial_tenant, env.initial_timeline, timeout=60 + ) + + t = threading.Thread(target=start_creating_timeline) + try: + t.start() + + wait_until_paused(env, "before-upload-index-pausable") + finally: + # FIXME: paused uploads bother shutdown + env.pageserver.stop(immediate=True) + t.join() + + # now without a failpoint + env.pageserver.start() + + wait_until_tenant_active(ps_http, env.initial_tenant) + + # currently it lives on and will get eventually uploaded, but this will change + detail = ps_http.timeline_detail(env.initial_tenant, env.initial_timeline) + assert detail["state"] == "Active" + + +def wait_until_paused(env: NeonEnv, failpoint: str): + found = False + msg = f"at failpoint {failpoint}" + for _ in range(20): + time.sleep(1) + found = env.pageserver.log_contains(msg) is not None + if found: + break + assert found diff --git a/test_runner/regress/test_duplicate_layers.py b/test_runner/regress/test_duplicate_layers.py index 7f76a8e042..ec5a2f7473 100644 --- a/test_runner/regress/test_duplicate_layers.py +++ b/test_runner/regress/test_duplicate_layers.py @@ -1,19 +1,24 @@ import time import pytest -from fixtures.neon_fixtures import NeonEnvBuilder, PgBin +from fixtures.log_helper import log +from fixtures.neon_fixtures import NeonEnvBuilder, PgBin, wait_for_last_flush_lsn +from fixtures.pageserver.utils import ( + wait_for_upload_queue_empty, + wait_until_tenant_active, +) +from fixtures.remote_storage import LocalFsStorage, RemoteStorageKind +from requests.exceptions import ConnectionError -# Test duplicate layer detection -# -# This test sets fail point at the end of first compaction phase: -# after flushing new L1 layers but before deletion of L0 layers -# it should cause generation of duplicate L1 layer by compaction after restart. -@pytest.mark.timeout(600) def test_duplicate_layers(neon_env_builder: NeonEnvBuilder, pg_bin: PgBin): env = neon_env_builder.init_start() pageserver_http = env.pageserver.http_client() + # use a failpoint to return all L0s as L1s + message = ".*duplicated L1 layer layer=.*" + env.pageserver.allowed_errors.append(message) + # Use aggressive compaction and checkpoint settings tenant_id, _ = env.neon_cli.create_tenant( conf={ @@ -33,4 +38,102 @@ def test_duplicate_layers(neon_env_builder: NeonEnvBuilder, pg_bin: PgBin): time.sleep(10) # let compaction to be performed assert env.pageserver.log_contains("compact-level0-phase1-return-same") - pg_bin.run_capture(["pgbench", "-P1", "-N", "-c5", "-T200", "-Mprepared", connstr]) + +def test_actually_duplicated_l1(neon_env_builder: NeonEnvBuilder, pg_bin: PgBin): + """ + This test sets fail point at the end of first compaction phase: + after flushing new L1 layers but before deletion of L0 layers + it should cause generation of duplicate L1 layer by compaction after restart. + """ + neon_env_builder.enable_pageserver_remote_storage(RemoteStorageKind.LOCAL_FS) + + env = neon_env_builder.init_start( + initial_tenant_conf={ + "checkpoint_distance": f"{1024 ** 2}", + "compaction_target_size": f"{1024 ** 2}", + "compaction_period": "0 s", + "compaction_threshold": "3", + } + ) + pageserver_http = env.pageserver.http_client() + + tenant_id, timeline_id = env.initial_tenant, env.initial_timeline + + pageserver_http.configure_failpoints(("after-timeline-compacted-first-L1", "exit")) + + endpoint = env.endpoints.create_start("main", tenant_id=tenant_id) + connstr = endpoint.connstr(options="-csynchronous_commit=off") + pg_bin.run_capture(["pgbench", "-i", "-s1", connstr]) + + wait_for_last_flush_lsn(env, endpoint, tenant_id, timeline_id) + + # make sure we receive no new wal after this, so that we'll write over the same L1 file. + endpoint.stop() + for sk in env.safekeepers: + sk.stop() + + # hit the exit failpoint + with pytest.raises(ConnectionError, match="Remote end closed connection without response"): + pageserver_http.timeline_compact(tenant_id, timeline_id) + env.pageserver.stop() + + # now the duplicate L1 has been created, but is not yet uploaded + assert isinstance(env.pageserver_remote_storage, LocalFsStorage) + + # path = env.remote_storage.timeline_path(tenant_id, timeline_id) + l1_found = None + for path in env.pageserver.timeline_dir(tenant_id, timeline_id).iterdir(): + if path.name == "metadata" or path.name.startswith("ephemeral-"): + continue + + if len(path.suffixes) > 0: + # temp files + continue + + [key_range, lsn_range] = path.name.split("__", maxsplit=1) + + if "-" not in lsn_range: + # image layer + continue + + [key_start, key_end] = key_range.split("-", maxsplit=1) + + if key_start == "0" * 36 and key_end == "F" * 36: + # L0 + continue + + if l1_found is not None: + raise RuntimeError(f"found multiple L1: {l1_found.name} and {path.name}") + l1_found = path + + assert l1_found is not None, "failed to find L1 locally" + original_created_at = l1_found.stat()[8] + + uploaded = env.pageserver_remote_storage.timeline_path(tenant_id, timeline_id) / l1_found.name + assert not uploaded.exists(), "to-be-overwritten should not yet be uploaded" + + # give room for fs timestamps + time.sleep(1) + + env.pageserver.start() + wait_until_tenant_active(pageserver_http, tenant_id) + + message = f".*duplicated L1 layer layer={l1_found.name}" + env.pageserver.allowed_errors.append(message) + + pageserver_http.timeline_compact(tenant_id, timeline_id) + # give time for log flush + time.sleep(1) + + found_msg = env.pageserver.log_contains(message) + assert found_msg is not None, "no layer was duplicated, has this been fixed already?" + + log.info(f"found log line: {found_msg}") + + overwritten_at = l1_found.stat()[8] + assert original_created_at < overwritten_at, "expected the L1 to be overwritten" + + wait_for_upload_queue_empty(pageserver_http, tenant_id, timeline_id) + + uploaded_at = uploaded.stat()[8] + assert overwritten_at <= uploaded_at, "expected the L1 to finally be uploaded" diff --git a/test_runner/regress/test_pageserver_generations.py b/test_runner/regress/test_pageserver_generations.py index 81d38ac934..1b9f48706f 100644 --- a/test_runner/regress/test_pageserver_generations.py +++ b/test_runner/regress/test_pageserver_generations.py @@ -10,6 +10,7 @@ of the pageserver are: """ +import enum import re import time from typing import Optional @@ -81,7 +82,7 @@ def generate_uploads_and_deletions( f""" INSERT INTO foo (id, val) SELECT g, '{data}' - FROM generate_series(1, 20000) g + FROM generate_series(1, 200) g ON CONFLICT (id) DO UPDATE SET val = EXCLUDED.val """, @@ -116,6 +117,10 @@ def get_deletion_queue_submitted(ps_http) -> int: return get_metric_or_0(ps_http, "pageserver_deletion_queue_submitted_total") +def get_deletion_queue_validated(ps_http) -> int: + return get_metric_or_0(ps_http, "pageserver_deletion_queue_validated_total") + + def get_deletion_queue_dropped(ps_http) -> int: return get_metric_or_0(ps_http, "pageserver_deletion_queue_dropped_total") @@ -272,13 +277,29 @@ def test_deferred_deletion(neon_env_builder: NeonEnvBuilder): assert get_deletion_queue_unexpected_errors(ps_http) == 0 -@pytest.mark.parametrize("keep_attachment", [True, False]) +class KeepAttachment(str, enum.Enum): + KEEP = "keep" + LOSE = "lose" + + +class ValidateBefore(str, enum.Enum): + VALIDATE = "validate" + NO_VALIDATE = "no-validate" + + +@pytest.mark.parametrize("keep_attachment", [KeepAttachment.KEEP, KeepAttachment.LOSE]) +@pytest.mark.parametrize("validate_before", [ValidateBefore.VALIDATE, ValidateBefore.NO_VALIDATE]) def test_deletion_queue_recovery( - neon_env_builder: NeonEnvBuilder, pg_bin: PgBin, keep_attachment: bool + neon_env_builder: NeonEnvBuilder, + pg_bin: PgBin, + keep_attachment: KeepAttachment, + validate_before: ValidateBefore, ): """ - :param keep_attachment: If true, we re-attach after restart. Else, we act as if some other + :param keep_attachment: whether to re-attach after restart. Else, we act as if some other node took the attachment while we were restarting. + :param validate_before: whether to wait for deletions to be validated before restart. This + makes them elegible to be executed after restart, if the same node keeps the attachment. """ neon_env_builder.enable_generations = True neon_env_builder.enable_pageserver_remote_storage( @@ -288,12 +309,20 @@ def test_deletion_queue_recovery( ps_http = env.pageserver.http_client() - # Prevent deletion lists from being executed, to build up some backlog of deletions - ps_http.configure_failpoints( - [ - ("deletion-queue-before-execute", "return"), - ] - ) + failpoints = [ + # Prevent deletion lists from being executed, to build up some backlog of deletions + ("deletion-queue-before-execute", "return"), + ] + + if validate_before == ValidateBefore.NO_VALIDATE: + failpoints.append( + # Prevent deletion lists from being validated, we will test that they are + # dropped properly during recovery. 'pause' is okay here because we kill + # the pageserver with immediate=true + ("control-plane-client-validate", "pause") + ) + + ps_http.configure_failpoints(failpoints) generate_uploads_and_deletions(env) @@ -305,10 +334,25 @@ def test_deletion_queue_recovery( assert get_deletion_queue_unexpected_errors(ps_http) == 0 assert get_deletion_queue_dropped_lsn_updates(ps_http) == 0 + if validate_before == ValidateBefore.VALIDATE: + + def assert_validation_complete(): + assert get_deletion_queue_submitted(ps_http) == get_deletion_queue_validated(ps_http) + + wait_until(20, 1, assert_validation_complete) + + # The validatated keys statistic advances before the header is written, so we + # also wait to see the header hit the disk: this seems paranoid but the race + # can really happen on a heavily overloaded test machine. + def assert_header_written(): + assert (env.pageserver.workdir / "deletion" / "header-01").exists() + + wait_until(20, 1, assert_header_written) + log.info(f"Restarting pageserver with {before_restart_depth} deletions enqueued") env.pageserver.stop(immediate=True) - if not keep_attachment: + if keep_attachment == KeepAttachment.LOSE: some_other_pageserver = 101010 assert env.attachment_service is not None env.attachment_service.attach_hook(env.initial_tenant, some_other_pageserver) @@ -327,14 +371,17 @@ def test_deletion_queue_recovery( ps_http.deletion_queue_flush(execute=True) wait_until(10, 1, lambda: assert_deletion_queue(ps_http, lambda n: n == 0)) - if keep_attachment: - # If we kept the attachment, then our pre-restart deletions should have executed - # successfully + if keep_attachment == KeepAttachment.KEEP or validate_before == ValidateBefore.VALIDATE: + # - If we kept the attachment, then our pre-restart deletions should execute + # because on re-attach they were from the immediately preceding generation + # - If we validated before restart, then the deletions should execute because the + # deletion queue header records a validated deletion list sequence number. assert get_deletion_queue_executed(ps_http) == before_restart_depth else: + env.pageserver.allowed_errors.extend([".*Dropping stale deletions.*"]) + # If we lost the attachment, we should have dropped our pre-restart deletions. assert get_deletion_queue_dropped(ps_http) == before_restart_depth - env.pageserver.allowed_errors.extend([".*Dropping stale deletions.*"]) assert get_deletion_queue_unexpected_errors(ps_http) == 0 assert get_deletion_queue_dropped_lsn_updates(ps_http) == 0 @@ -350,3 +397,73 @@ def test_deletion_queue_recovery( assert get_deletion_queue_unexpected_errors(ps_http) == 0 assert get_deletion_queue_dropped_lsn_updates(ps_http) == 0 + + +def test_emergency_mode(neon_env_builder: NeonEnvBuilder, pg_bin: PgBin): + neon_env_builder.enable_generations = True + neon_env_builder.enable_pageserver_remote_storage( + RemoteStorageKind.MOCK_S3, + ) + env = neon_env_builder.init_start(initial_tenant_conf=TENANT_CONF) + + ps_http = env.pageserver.http_client() + + generate_uploads_and_deletions(env) + + env.pageserver.allowed_errors.extend( + [ + # When the pageserver can't reach the control plane, it will complain + ".*calling control plane generation validation API failed.*", + # Emergency mode is a big deal, we log errors whenever it is used. + ".*Emergency mode!.*", + ] + ) + + # Simulate a major incident: the control plane goes offline + assert env.attachment_service is not None + env.attachment_service.stop() + + # Remember how many validations had happened before the control plane went offline + validated = get_deletion_queue_validated(ps_http) + + generate_uploads_and_deletions(env, init=False) + + # The running pageserver should stop progressing deletions + time.sleep(10) + assert get_deletion_queue_validated(ps_http) == validated + + # Restart the pageserver: ordinarily we would _avoid_ doing this during such an + # incident, but it might be unavoidable: if so, we want to be able to start up + # and serve clients. + env.pageserver.stop() # Non-immediate: implicitly checking that shutdown doesn't hang waiting for CP + env.pageserver.start( + overrides=("--pageserver-config-override=control_plane_emergency_mode=true",) + ) + + # The pageserver should provide service to clients + generate_uploads_and_deletions(env, init=False) + + # The pageserver should neither validate nor execute any deletions, it should have + # loaded the DeletionLists from before though + time.sleep(10) + assert get_deletion_queue_depth(ps_http) > 0 + assert get_deletion_queue_validated(ps_http) == 0 + assert get_deletion_queue_executed(ps_http) == 0 + + # When the control plane comes back up, normal service should resume + env.attachment_service.start() + + ps_http.deletion_queue_flush(execute=True) + assert get_deletion_queue_depth(ps_http) == 0 + assert get_deletion_queue_validated(ps_http) > 0 + assert get_deletion_queue_executed(ps_http) > 0 + + # The pageserver should work fine when subsequently restarted in non-emergency mode + env.pageserver.stop() # Non-immediate: implicitly checking that shutdown doesn't hang waiting for CP + env.pageserver.start() + + generate_uploads_and_deletions(env, init=False) + ps_http.deletion_queue_flush(execute=True) + assert get_deletion_queue_depth(ps_http) == 0 + assert get_deletion_queue_validated(ps_http) > 0 + assert get_deletion_queue_executed(ps_http) > 0 diff --git a/test_runner/regress/test_pageserver_restart.py b/test_runner/regress/test_pageserver_restart.py index 2965a354bd..f7dc80a6d8 100644 --- a/test_runner/regress/test_pageserver_restart.py +++ b/test_runner/regress/test_pageserver_restart.py @@ -120,6 +120,10 @@ def test_pageserver_chaos(neon_env_builder: NeonEnvBuilder): env = neon_env_builder.init_start() + # these can happen, if we shutdown at a good time. to be fixed as part of #5172. + message = ".*duplicated L1 layer layer=.*" + env.pageserver.allowed_errors.append(message) + # Use a tiny checkpoint distance, to create a lot of layers quickly. # That allows us to stress the compaction and layer flushing logic more. tenant, _ = env.neon_cli.create_tenant( diff --git a/test_runner/regress/test_pageserver_restarts_under_workload.py b/test_runner/regress/test_pageserver_restarts_under_workload.py index 65569f3bac..71058268a6 100644 --- a/test_runner/regress/test_pageserver_restarts_under_workload.py +++ b/test_runner/regress/test_pageserver_restarts_under_workload.py @@ -17,6 +17,8 @@ def test_pageserver_restarts_under_worload(neon_simple_env: NeonEnv, pg_bin: PgB n_restarts = 10 scale = 10 + env.pageserver.allowed_errors.append(".*query handler.*failed.*Shutting down") + def run_pgbench(connstr: str): log.info(f"Start a pgbench workload on pg {connstr}") pg_bin.run_capture(["pgbench", "-i", f"-s{scale}", connstr]) diff --git a/test_runner/regress/test_tenant_delete.py b/test_runner/regress/test_tenant_delete.py index 3a56ca51a6..7a0b2694b8 100644 --- a/test_runner/regress/test_tenant_delete.py +++ b/test_runner/regress/test_tenant_delete.py @@ -45,14 +45,11 @@ def test_tenant_delete_smoke( [ # The deletion queue will complain when it encounters simulated S3 errors ".*deletion executor: DeleteObjects request failed.*", + # lucky race with stopping from flushing a layer we fail to schedule any uploads + ".*layer flush task.+: could not flush frozen layer: update_metadata_file", ] ) - # lucky race with stopping from flushing a layer we fail to schedule any uploads - env.pageserver.allowed_errors.append( - ".*layer flush task.+: could not flush frozen layer: update_metadata_file" - ) - ps_http = env.pageserver.http_client() # first try to delete non existing tenant @@ -194,11 +191,9 @@ def test_delete_tenant_exercise_crash_safety_failpoints( ) if simulate_failures: - env.pageserver.allowed_errors.extend( - [ - # The deletion queue will complain when it encounters simulated S3 errors - ".*deletion executor: DeleteObjects request failed.*", - ] + env.pageserver.allowed_errors.append( + # The deletion queue will complain when it encounters simulated S3 errors + ".*deletion executor: DeleteObjects request failed.*", ) ps_http = env.pageserver.http_client() @@ -293,6 +288,10 @@ def test_tenant_delete_is_resumed_on_attach( neon_env_builder.enable_pageserver_remote_storage(remote_storage_kind) env = neon_env_builder.init_start(initial_tenant_conf=MANY_SMALL_LAYERS_TENANT_CONFIG) + env.pageserver.allowed_errors.append( + # lucky race with stopping from flushing a layer we fail to schedule any uploads + ".*layer flush task.+: could not flush frozen layer: update_metadata_file" + ) tenant_id = env.initial_tenant diff --git a/test_runner/regress/test_tenant_detach.py b/test_runner/regress/test_tenant_detach.py index a20523b1f3..519af1cbde 100644 --- a/test_runner/regress/test_tenant_detach.py +++ b/test_runner/regress/test_tenant_detach.py @@ -752,6 +752,9 @@ def test_ignore_while_attaching( env.pageserver.allowed_errors.append( f".*Tenant {tenant_id} will not become active\\. Current state: Stopping.*" ) + # An endpoint is starting up concurrently with our detach, it can + # experience RPC failure due to shutdown. + env.pageserver.allowed_errors.append(".*query handler.*failed.*Shutting down") data_id = 1 data_secret = "very secret secret" diff --git a/test_runner/regress/test_timeline_delete.py b/test_runner/regress/test_timeline_delete.py index 3af144c31c..c412809a3a 100644 --- a/test_runner/regress/test_timeline_delete.py +++ b/test_runner/regress/test_timeline_delete.py @@ -34,6 +34,7 @@ from fixtures.remote_storage import ( ) from fixtures.types import Lsn, TenantId, TimelineId from fixtures.utils import query_scalar, run_pg_bench_small, wait_until +from urllib3.util.retry import Retry def test_timeline_delete(neon_simple_env: NeonEnv): @@ -614,7 +615,7 @@ def test_delete_timeline_client_hangup(neon_env_builder: NeonEnvBuilder): child_timeline_id = env.neon_cli.create_branch("child", "main") - ps_http = env.pageserver.http_client() + ps_http = env.pageserver.http_client(retries=Retry(0, read=False)) failpoint_name = "persist_deleted_index_part" ps_http.configure_failpoints((failpoint_name, "pause")) @@ -854,7 +855,7 @@ def test_timeline_delete_resumed_on_attach( # error from http response is also logged ".*InternalServerError\\(Tenant is marked as deleted on remote storage.*", # Polling after attach may fail with this - f".*InternalServerError\\(Tenant {tenant_id} is not active.*", + ".*Resource temporarily unavailable.*Tenant not yet active", '.*shutdown_pageserver{exit_code=0}: stopping left-over name="remote upload".*', ) ) diff --git a/test_runner/regress/test_wal_acceptor.py b/test_runner/regress/test_wal_acceptor.py index 8199f5777b..631798d643 100644 --- a/test_runner/regress/test_wal_acceptor.py +++ b/test_runner/regress/test_wal_acceptor.py @@ -801,6 +801,9 @@ def test_timeline_status(neon_env_builder: NeonEnvBuilder, auth_enabled: bool): wa_http_cli_debug = wa.http_client(auth_token=env.auth_keys.generate_safekeeper_token()) wa_http_cli_debug.check_status() + # create a dummy table to wait for timeline initialization in safekeeper + endpoint.safe_psql("create table wait_for_sk()") + # fetch something sensible from status tli_status = wa_http_cli.timeline_status(tenant_id, timeline_id) epoch = tli_status.acceptor_epoch