neon_local: basic sharding support

This commit is contained in:
John Spray
2023-11-05 14:17:42 +00:00
parent 1518675bac
commit 2d0930f622
5 changed files with 204 additions and 119 deletions

View File

@@ -15,7 +15,8 @@ use control_plane::pageserver::{PageServerNode, PAGESERVER_REMOTE_STORAGE_DIR};
use control_plane::safekeeper::SafekeeperNode;
use control_plane::tenant_migration::migrate_tenant;
use control_plane::{broker, local_env};
use pageserver_api::models::TimelineInfo;
use pageserver_api::models::{LocationConfig, LocationConfigMode, TimelineInfo};
use pageserver_api::shard::{ShardCount, ShardNumber, TenantShardId};
use pageserver_api::{
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_PAGESERVER_HTTP_PORT,
DEFAULT_PG_LISTEN_PORT as DEFAULT_PAGESERVER_PG_PORT,
@@ -26,6 +27,7 @@ use safekeeper_api::{
DEFAULT_PG_LISTEN_PORT as DEFAULT_SAFEKEEPER_PG_PORT,
};
use std::collections::{BTreeSet, HashMap};
use std::num::ParseIntError;
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
@@ -97,6 +99,22 @@ struct TimelineTreeEl {
pub children: BTreeSet<TimelineId>,
}
/// Helper for CLI args that contain a comma-separate list of NodeId
fn parse_ids_arg(
matches: &ArgMatches,
arg: &str,
) -> Result<Option<Vec<NodeId>>, std::num::ParseIntError> {
if let Some(id_str) = matches.get_one::<String>(arg) {
let r: Result<Vec<_>, ParseIntError> = id_str
.split(',')
.map(|ps_id| u64::from_str(str::trim(ps_id)).map(NodeId))
.collect();
r.map(Some)
} else {
Ok(Some(vec![DEFAULT_PAGESERVER_ID]))
}
}
// Main entry point for the 'neon_local' CLI utility
//
// This utility helps to manage neon installation. That includes following:
@@ -374,9 +392,10 @@ fn pageserver_config_overrides(init_match: &ArgMatches) -> Vec<&str> {
}
fn handle_tenant(tenant_match: &ArgMatches, env: &mut local_env::LocalEnv) -> anyhow::Result<()> {
let pageserver = get_default_pageserver(env);
match tenant_match.subcommand() {
Some(("list", _)) => {
// TODO: make command aware of multiple pageservers
let pageserver = get_default_pageserver(env);
for t in pageserver.tenant_list()? {
println!("{} {:?}", t.id, t.state);
}
@@ -387,38 +406,94 @@ fn handle_tenant(tenant_match: &ArgMatches, env: &mut local_env::LocalEnv) -> an
.map(|vals| vals.flat_map(|c| c.split_once(':')).collect())
.unwrap_or_default();
let shard_count: u8 = create_match
.get_one::<u8>("shard-count")
.cloned()
.unwrap_or(1);
// If tenant ID was not specified, generate one
let tenant_id = parse_tenant_id(create_match)?.unwrap_or_else(TenantId::generate);
let generation = if env.control_plane_api.is_some() {
// We must register the tenant with the attachment service, so
// that when the pageserver restarts, it will be re-attached.
let attachment_service = AttachmentService::from_env(env);
attachment_service.attach_hook(tenant_id, pageserver.conf.id)?
} else {
None
};
pageserver.tenant_create(tenant_id, generation, tenant_conf)?;
println!("tenant {tenant_id} successfully created on the pageserver");
// Create an initial timeline for the new tenant
let new_timeline_id = parse_timeline_id(create_match)?;
// We will create an initial timeline for the new tenant
let new_timeline_id =
parse_timeline_id(create_match)?.unwrap_or(TimelineId::generate());
let pg_version = create_match
.get_one::<u32>("pg-version")
.copied()
.context("Failed to parse postgres version from the argument string")?;
let timeline_info = pageserver.timeline_create(
tenant_id,
new_timeline_id,
None,
None,
Some(pg_version),
None,
)?;
let new_timeline_id = timeline_info.timeline_id;
let last_record_lsn = timeline_info.last_record_lsn;
// TODO: implement ability for one pageserver to hold multiple
// shards for the same tenant. Until then, we must place each
// shard on a different pageserver.
assert!(env.pageservers.len() >= shard_count as usize);
let cfg_shard_count = if shard_count > 1 {
shard_count
} else {
// For single-sharded mode, use the legacy unsharded configuration. This avoids
// breaking any existing tests that assume legacy unsharded storage paths
0
};
for shard_number in 0..shard_count {
let ps_conf = env.pageservers.get(shard_number as usize).unwrap();
let pageserver = PageServerNode::from_env(env, ps_conf);
// TODO: per-shard generations
let generation = if env.control_plane_api.is_some() {
// We must register the tenant with the attachment service, so
// that when the pageserver restarts, it will be re-attached.
let attachment_service = AttachmentService::from_env(env);
attachment_service.attach_hook(tenant_id, pageserver.conf.id)?
} else {
None
};
// TODO: shard-aware POST /v1/tenant. Currently tenant creation on the
// pageserver is a no-op, but we shouldn't skip the command entirely.
let tenant_conf = PageServerNode::build_config(tenant_conf.clone())?;
let tenant_shard_id = TenantShardId {
shard_number: ShardNumber(shard_number),
shard_count: ShardCount(cfg_shard_count),
tenant_id,
};
let location_conf = LocationConfig {
shard_count: cfg_shard_count,
shard_number,
shard_stripe_size: 32768,
mode: LocationConfigMode::AttachedSingle,
generation,
secondary_conf: None,
tenant_conf,
};
pageserver.location_config(tenant_shard_id, location_conf, None)?;
println!(
"tenant {tenant_id} successfully created on pageserver {}",
pageserver.conf.id
);
}
for shard_number in 0..shard_count {
let ps_conf = env.pageservers.get(shard_number as usize).unwrap();
let pageserver = PageServerNode::from_env(env, ps_conf);
let tenant_shard_id = TenantShardId {
shard_number: ShardNumber(shard_number),
shard_count: ShardCount(cfg_shard_count),
tenant_id,
};
pageserver.timeline_create(
tenant_shard_id,
Some(new_timeline_id),
None,
None,
Some(pg_version),
None,
)?;
}
env.register_branch_mapping(
DEFAULT_BRANCH_NAME.to_string(),
@@ -426,9 +501,7 @@ fn handle_tenant(tenant_match: &ArgMatches, env: &mut local_env::LocalEnv) -> an
new_timeline_id,
)?;
println!(
"Created an initial timeline '{new_timeline_id}' at Lsn {last_record_lsn} for tenant: {tenant_id}",
);
println!("Created an initial timeline '{new_timeline_id}' for tenant: {tenant_id}",);
if create_match.get_flag("set-default") {
println!("Setting tenant {tenant_id} as a default one");
@@ -448,6 +521,8 @@ fn handle_tenant(tenant_match: &ArgMatches, env: &mut local_env::LocalEnv) -> an
.map(|vals| vals.flat_map(|c| c.split_once(':')).collect())
.unwrap_or_default();
// TODO: make command aware of multiple pageservers
let pageserver = get_default_pageserver(env);
pageserver
.tenant_config(tenant_id, tenant_conf)
.with_context(|| format!("Tenant config failed for tenant with id {tenant_id}"))?;
@@ -491,7 +566,7 @@ fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) -
let new_timeline_id_opt = parse_timeline_id(create_match)?;
let timeline_info = pageserver.timeline_create(
tenant_id,
TenantShardId::unsharded(tenant_id),
new_timeline_id_opt,
None,
None,
@@ -554,7 +629,7 @@ fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) -
None,
pg_version,
ComputeMode::Primary,
DEFAULT_PAGESERVER_ID,
vec![DEFAULT_PAGESERVER_ID],
)?;
println!("Done");
}
@@ -579,7 +654,7 @@ fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::LocalEnv) -
.transpose()
.context("Failed to parse ancestor start Lsn from the request")?;
let timeline_info = pageserver.timeline_create(
tenant_id,
TenantShardId::unsharded(tenant_id),
None,
start_lsn,
Some(ancestor_timeline_id),
@@ -704,13 +779,8 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
.copied()
.unwrap_or(false);
let pageserver_id =
if let Some(id_str) = sub_args.get_one::<String>("endpoint-pageserver-id") {
NodeId(id_str.parse().context("while parsing pageserver id")?)
} else {
DEFAULT_PAGESERVER_ID
};
let pageserver_ids = parse_ids_arg(sub_args, "endpoint-pageserver-id")?
.unwrap_or(vec![DEFAULT_PAGESERVER_ID]);
let mode = match (lsn, hot_standby) {
(Some(lsn), false) => ComputeMode::Static(lsn),
(None, true) => ComputeMode::Replica,
@@ -738,7 +808,7 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
http_port,
pg_version,
mode,
pageserver_id,
pageserver_ids,
)?;
}
"start" => {
@@ -746,29 +816,14 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
.get_one::<String>("endpoint_id")
.ok_or_else(|| anyhow!("No endpoint ID was provided to start"))?;
let pageserver_id =
if let Some(id_str) = sub_args.get_one::<String>("endpoint-pageserver-id") {
NodeId(id_str.parse().context("while parsing pageserver id")?)
} else {
DEFAULT_PAGESERVER_ID
};
let pageservers = parse_ids_arg(sub_args, "endpoint-pageserver-id")?
.unwrap_or(vec![DEFAULT_PAGESERVER_ID]);
let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
// If --safekeepers argument is given, use only the listed safekeeper nodes.
let safekeepers =
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
let mut safekeepers: Vec<NodeId> = Vec::new();
for sk_id in safekeepers_str.split(',').map(str::trim) {
let sk_id = NodeId(u64::from_str(sk_id).map_err(|_| {
anyhow!("invalid node ID \"{sk_id}\" in --safekeepers list")
})?);
safekeepers.push(sk_id);
}
safekeepers
} else {
env.safekeepers.iter().map(|sk| sk.id).collect()
};
let safekeepers = parse_ids_arg(sub_args, "safekeepers")?
.unwrap_or_else(|| env.safekeepers.iter().map(|sk| sk.id).collect());
let endpoint = cplane
.endpoints
@@ -781,7 +836,8 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
endpoint.timeline_id,
)?;
let ps_conf = env.get_pageserver_conf(pageserver_id)?;
// We assume that all pageservers have the same auth conf
let ps_conf = env.get_pageserver_conf(pageservers[0])?;
let auth_token = if matches!(ps_conf.pg_auth_type, AuthType::NeonJWT) {
let claims = Claims::new(Some(endpoint.tenant_id), Scope::Tenant);
@@ -801,15 +857,21 @@ fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<(
.endpoints
.get(endpoint_id.as_str())
.with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?;
let pageserver_id =
if let Some(id_str) = sub_args.get_one::<String>("endpoint-pageserver-id") {
Some(NodeId(
id_str.parse().context("while parsing pageserver id")?,
))
} else {
None
};
endpoint.reconfigure(pageserver_id)?;
let pageserver_ids: Option<Result<Vec<NodeId>, _>> = sub_args
.get_many::<String>("endpoint-pageserver-id")
.map(|ids| {
ids.map(|id_str| id_str.parse().context("while parsing pageserver id"))
.map(|r| r.map(NodeId))
.collect()
});
let pageserver_ids = match pageserver_ids {
Some(Ok(v)) => Ok(Some(v)),
Some(Err(e)) => Err(e),
None => Ok(None),
}?;
endpoint.reconfigure(pageserver_ids)?;
}
"stop" => {
let endpoint_id = sub_args
@@ -1313,6 +1375,7 @@ fn cli() -> Command {
.arg(pg_version_arg.clone())
.arg(Arg::new("set-default").long("set-default").action(ArgAction::SetTrue).required(false)
.help("Use this tenant in future CLI commands where tenant_id is needed, but not specified"))
.arg(Arg::new("shard-count").value_parser(value_parser!(u8)).long("shard-count").action(ArgAction::Set).help("Number of shards in the new tenant (default 1)"))
)
.subcommand(Command::new("set-default").arg(tenant_id_arg.clone().required(true))
.about("Set a particular tenant as default in future CLI commands where tenant_id is needed, but not specified"))

View File

@@ -67,7 +67,7 @@ pub struct EndpointConf {
http_port: u16,
pg_version: u32,
skip_pg_catalog_updates: bool,
pageserver_id: NodeId,
pageservers: Vec<NodeId>,
}
//
@@ -82,6 +82,33 @@ pub struct ComputeControlPlane {
env: LocalEnv,
}
fn load_pageservers(
env: &LocalEnv,
pageserver_ids: &Vec<NodeId>,
) -> anyhow::Result<Vec<PageServerNode>> {
let mut pageservers = Vec::new();
for ps_id in pageserver_ids {
let pageserver = env
.get_pageserver_conf(*ps_id)
.map(|conf| PageServerNode::from_env(env, conf))?;
pageservers.push(pageserver);
}
Ok(pageservers)
}
fn build_pageserver_connstr(pageservers: &[PageServerNode]) -> String {
pageservers
.iter()
.map(|ps| {
let config = ps.pg_connection_config.clone();
let (host, port) = (config.host(), config.port());
// NOTE: avoid spaces in connection string, because it is less error prone if we forward it somewhere.
format!("postgresql://no_user@{host}:{port}")
})
.collect::<Vec<_>>()
.join(",")
}
impl ComputeControlPlane {
// Load current endpoints from the endpoints/ subdirectories
pub fn load(env: LocalEnv) -> Result<ComputeControlPlane> {
@@ -119,19 +146,16 @@ impl ComputeControlPlane {
http_port: Option<u16>,
pg_version: u32,
mode: ComputeMode,
pageserver_id: NodeId,
pageservers: Vec<NodeId>,
) -> Result<Arc<Endpoint>> {
let pg_port = pg_port.unwrap_or_else(|| self.get_port());
let http_port = http_port.unwrap_or_else(|| self.get_port() + 1);
let pageserver =
PageServerNode::from_env(&self.env, self.env.get_pageserver_conf(pageserver_id)?);
let ep = Arc::new(Endpoint {
endpoint_id: endpoint_id.to_owned(),
pg_address: SocketAddr::new("127.0.0.1".parse().unwrap(), pg_port),
http_address: SocketAddr::new("127.0.0.1".parse().unwrap(), http_port),
env: self.env.clone(),
pageserver,
pageservers: load_pageservers(&self.env, &pageservers)?,
timeline_id,
mode,
tenant_id,
@@ -157,7 +181,7 @@ impl ComputeControlPlane {
pg_port,
pg_version,
skip_pg_catalog_updates: true,
pageserver_id,
pageservers,
})?,
)?;
std::fs::write(
@@ -216,7 +240,7 @@ pub struct Endpoint {
// These are not part of the endpoint as such, but the environment
// the endpoint runs in.
pub env: LocalEnv,
pageserver: PageServerNode,
pageservers: Vec<PageServerNode>,
// Optimizations
skip_pg_catalog_updates: bool,
@@ -239,15 +263,14 @@ impl Endpoint {
let conf: EndpointConf =
serde_json::from_slice(&std::fs::read(entry.path().join("endpoint.json"))?)?;
let pageserver =
PageServerNode::from_env(env, env.get_pageserver_conf(conf.pageserver_id)?);
let pageservers: Vec<PageServerNode> = load_pageservers(env, &conf.pageservers)?;
Ok(Endpoint {
pg_address: SocketAddr::new("127.0.0.1".parse().unwrap(), conf.pg_port),
http_address: SocketAddr::new("127.0.0.1".parse().unwrap(), conf.http_port),
endpoint_id,
env: env.clone(),
pageserver,
pageservers,
timeline_id: conf.timeline_id,
mode: conf.mode,
tenant_id: conf.tenant_id,
@@ -482,13 +505,7 @@ impl Endpoint {
std::fs::remove_dir_all(self.pgdata())?;
}
let pageserver_connstring = {
let config = &self.pageserver.pg_connection_config;
let (host, port) = (config.host(), config.port());
// NOTE: avoid spaces in connection string, because it is less error prone if we forward it somewhere.
format!("postgresql://no_user@{host}:{port}")
};
let pageserver_connstring = build_pageserver_connstr(&self.pageservers);
let mut safekeeper_connstrings = Vec::new();
if self.mode == ComputeMode::Primary {
for sk_id in safekeepers {
@@ -658,7 +675,7 @@ impl Endpoint {
}
}
pub fn reconfigure(&self, pageserver_id: Option<NodeId>) -> Result<()> {
pub fn reconfigure(&self, pageservers: Option<Vec<NodeId>>) -> Result<()> {
let mut spec: ComputeSpec = {
let spec_path = self.endpoint_path().join("spec.json");
let file = std::fs::File::open(spec_path)?;
@@ -668,23 +685,20 @@ impl Endpoint {
let postgresql_conf = self.read_postgresql_conf()?;
spec.cluster.postgresql_conf = Some(postgresql_conf);
if let Some(pageserver_id) = pageserver_id {
if let Some(pageservers) = pageservers {
let endpoint_config_path = self.endpoint_path().join("endpoint.json");
let mut endpoint_conf: EndpointConf = {
let file = std::fs::File::open(&endpoint_config_path)?;
serde_json::from_reader(file)?
};
endpoint_conf.pageserver_id = pageserver_id;
endpoint_conf.pageservers = pageservers.clone();
std::fs::write(
endpoint_config_path,
serde_json::to_string_pretty(&endpoint_conf)?,
)?;
let pageserver =
PageServerNode::from_env(&self.env, self.env.get_pageserver_conf(pageserver_id)?);
let ps_http_conf = &pageserver.pg_connection_config;
let (host, port) = (ps_http_conf.host(), ps_http_conf.port());
spec.pageserver_connstring = Some(format!("postgresql://no_user@{host}:{port}"));
let pageservers = load_pageservers(&self.env, &pageservers)?;
spec.pageserver_connstring = Some(build_pageserver_connstr(&pageservers));
}
let client = reqwest::blocking::Client::new();

View File

@@ -340,15 +340,8 @@ impl PageServerNode {
.json()?)
}
pub fn tenant_create(
&self,
new_tenant_id: TenantId,
generation: Option<u32>,
settings: HashMap<&str, &str>,
) -> anyhow::Result<TenantId> {
let mut settings = settings.clone();
let config = models::TenantConfig {
pub fn build_config(mut settings: HashMap<&str, &str>) -> anyhow::Result<models::TenantConfig> {
Ok(models::TenantConfig {
checkpoint_distance: settings
.remove("checkpoint_distance")
.map(|x| x.parse::<u64>())
@@ -407,8 +400,16 @@ impl PageServerNode {
.map(|x| x.parse::<bool>())
.transpose()
.context("Failed to parse 'gc_feedback' as bool")?,
};
})
}
pub fn tenant_create(
&self,
new_tenant_id: TenantId,
generation: Option<u32>,
settings: HashMap<&str, &str>,
) -> anyhow::Result<TenantId> {
let config = Self::build_config(settings.clone())?;
let request = models::TenantCreateRequest {
new_tenant_id: TenantShardId::unsharded(new_tenant_id),
generation,
@@ -521,15 +522,18 @@ impl PageServerNode {
pub fn location_config(
&self,
tenant_id: TenantId,
tenant_shard_id: TenantShardId,
config: LocationConfig,
flush_ms: Option<Duration>,
) -> anyhow::Result<()> {
let req_body = TenantLocationConfigRequest { tenant_id, config };
let req_body = TenantLocationConfigRequest {
tenant_shard_id,
config,
};
let path = format!(
"{}/tenant/{}/location_config",
self.http_base_url, tenant_id
self.http_base_url, tenant_shard_id
);
let path = if let Some(flush_ms) = flush_ms {
format!("{}?flush_ms={}", path, flush_ms.as_millis())
@@ -560,7 +564,7 @@ impl PageServerNode {
pub fn timeline_create(
&self,
tenant_id: TenantId,
tenant_shard_id: TenantShardId,
new_timeline_id: Option<TimelineId>,
ancestor_start_lsn: Option<Lsn>,
ancestor_timeline_id: Option<TimelineId>,
@@ -572,7 +576,7 @@ impl PageServerNode {
self.http_request(
Method::POST,
format!("{}/tenant/{}/timeline", self.http_base_url, tenant_id),
format!("{}/tenant/{}/timeline", self.http_base_url, tenant_shard_id),
)?
.json(&models::TimelineCreateRequest {
new_timeline_id,
@@ -585,11 +589,11 @@ impl PageServerNode {
.error_from_body()?
.json::<Option<TimelineInfo>>()
.with_context(|| {
format!("Failed to parse timeline creation response for tenant id: {tenant_id}")
format!("Failed to parse timeline creation response for tenant id: {tenant_shard_id}")
})?
.with_context(|| {
format!(
"No timeline id was found in the timeline creation response for tenant {tenant_id}"
"No timeline id was found in the timeline creation response for tenant {tenant_shard_id}"
)
})
}

View File

@@ -11,6 +11,7 @@ use crate::{
use pageserver_api::models::{
LocationConfig, LocationConfigMode, LocationConfigSecondary, TenantConfig,
};
use pageserver_api::shard::TenantShardId;
use std::collections::HashMap;
use std::time::Duration;
use utils::{
@@ -108,6 +109,9 @@ pub fn migrate_tenant(
}
}
// No support for sharding in this function yet
let tenant_shard_id = TenantShardId::unsharded(tenant_id);
let previous = attachment_service.inspect(tenant_id)?;
let mut baseline_lsns = None;
if let Some((generation, origin_ps_id)) = &previous {
@@ -117,7 +121,7 @@ pub fn migrate_tenant(
println!("🔁 Already attached to {origin_ps_id}, freshening...");
let gen = attachment_service.attach_hook(tenant_id, dest_ps.conf.id)?;
let dest_conf = build_location_config(LocationConfigMode::AttachedSingle, gen, None);
dest_ps.location_config(tenant_id, dest_conf, None)?;
dest_ps.location_config(tenant_shard_id, dest_conf, None)?;
println!("✅ Migration complete");
return Ok(());
}
@@ -126,7 +130,7 @@ pub fn migrate_tenant(
let stale_conf =
build_location_config(LocationConfigMode::AttachedStale, Some(*generation), None);
origin_ps.location_config(tenant_id, stale_conf, Some(Duration::from_secs(10)))?;
origin_ps.location_config(tenant_shard_id, stale_conf, Some(Duration::from_secs(10)))?;
baseline_lsns = Some(get_lsns(tenant_id, &origin_ps)?);
}
@@ -135,7 +139,7 @@ pub fn migrate_tenant(
let dest_conf = build_location_config(LocationConfigMode::AttachedMulti, gen, None);
println!("🔁 Attaching to pageserver {}", dest_ps.conf.id);
dest_ps.location_config(tenant_id, dest_conf, None)?;
dest_ps.location_config(tenant_shard_id, dest_conf, None)?;
if let Some(baseline) = baseline_lsns {
println!("🕑 Waiting for LSN to catch up...");
@@ -149,7 +153,7 @@ pub fn migrate_tenant(
"🔁 Reconfiguring endpoint {} to use pageserver {}",
endpoint_name, dest_ps.conf.id
);
endpoint.reconfigure(Some(dest_ps.conf.id))?;
endpoint.reconfigure(Some(vec![dest_ps.conf.id]))?;
}
}
@@ -181,7 +185,7 @@ pub fn migrate_tenant(
"💤 Switching to secondary mode on pageserver {}",
other_ps.conf.id
);
other_ps.location_config(tenant_id, secondary_conf, None)?;
other_ps.location_config(tenant_shard_id, secondary_conf, None)?;
}
println!(
@@ -189,7 +193,7 @@ pub fn migrate_tenant(
dest_ps.conf.id
);
let dest_conf = build_location_config(LocationConfigMode::AttachedSingle, gen, None);
dest_ps.location_config(tenant_id, dest_conf, None)?;
dest_ps.location_config(tenant_shard_id, dest_conf, None)?;
println!("✅ Migration complete");

View File

@@ -293,7 +293,7 @@ pub struct StatusResponse {
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TenantLocationConfigRequest {
pub tenant_id: TenantId,
pub tenant_shard_id: TenantShardId,
#[serde(flatten)]
pub config: LocationConfig, // as we have a flattened field, we should reject all unknown fields in it
}