diff --git a/control_plane/src/bin/neon_local.rs b/control_plane/src/bin/neon_local.rs index ef308cb2d2..2af79bed90 100644 --- a/control_plane/src/bin/neon_local.rs +++ b/control_plane/src/bin/neon_local.rs @@ -278,6 +278,14 @@ fn parse_tenant_id(sub_match: &ArgMatches) -> anyhow::Result> { .context("Failed to parse tenant id from the argument string") } +fn parse_generation(sub_match: &ArgMatches) -> anyhow::Result> { + sub_match + .get_one::("generation") + .map(|tenant_id| u32::from_str(tenant_id)) + .transpose() + .context("Failed to parse generation rom the argument string") +} + fn parse_timeline_id(sub_match: &ArgMatches) -> anyhow::Result> { sub_match .get_one::("timeline-id") @@ -343,11 +351,13 @@ fn handle_tenant(tenant_match: &ArgMatches, env: &mut local_env::LocalEnv) -> an } Some(("create", create_match)) => { let initial_tenant_id = parse_tenant_id(create_match)?; + let generation = parse_generation(create_match)?; let tenant_conf: HashMap<_, _> = create_match .get_many::("config") .map(|vals| vals.flat_map(|c| c.split_once(':')).collect()) .unwrap_or_default(); - let new_tenant_id = pageserver.tenant_create(initial_tenant_id, tenant_conf)?; + let new_tenant_id = + pageserver.tenant_create(initial_tenant_id, generation, tenant_conf)?; println!("tenant {new_tenant_id} successfully created on the pageserver"); // Create an initial timeline for the new tenant diff --git a/control_plane/src/pageserver.rs b/control_plane/src/pageserver.rs index 2ff09021e5..c24ea7f717 100644 --- a/control_plane/src/pageserver.rs +++ b/control_plane/src/pageserver.rs @@ -317,6 +317,7 @@ impl PageServerNode { pub fn tenant_create( &self, new_tenant_id: Option, + generation: Option, settings: HashMap<&str, &str>, ) -> anyhow::Result { let mut settings = settings.clone(); @@ -387,6 +388,7 @@ impl PageServerNode { let request = models::TenantCreateRequest { new_tenant_id, + generation, config, }; if !settings.is_empty() { diff --git a/libs/pageserver_api/src/models.rs b/libs/pageserver_api/src/models.rs index 2f4c21326e..5f92556f64 100644 --- a/libs/pageserver_api/src/models.rs +++ b/libs/pageserver_api/src/models.rs @@ -194,6 +194,9 @@ pub struct TimelineCreateRequest { pub struct TenantCreateRequest { #[serde_as(as = "DisplayFromStr")] pub new_tenant_id: TenantId, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub generation: Option, #[serde(flatten)] pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it } @@ -241,15 +244,6 @@ pub struct StatusResponse { pub id: NodeId, } -impl TenantCreateRequest { - pub fn new(new_tenant_id: TenantId) -> TenantCreateRequest { - TenantCreateRequest { - new_tenant_id, - config: TenantConfig::default(), - } - } -} - #[serde_as] #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] @@ -293,9 +287,11 @@ impl TenantConfigRequest { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct TenantAttachRequest { pub config: TenantAttachConfig, + #[serde(default)] + pub generation: Option, } /// Newtype to enforce deny_unknown_fields on TenantConfig for diff --git a/pageserver/src/http/openapi_spec.yml b/pageserver/src/http/openapi_spec.yml index 38e07f172d..4988641d6a 100644 --- a/pageserver/src/http/openapi_spec.yml +++ b/pageserver/src/http/openapi_spec.yml @@ -383,7 +383,6 @@ paths: schema: type: string format: hex - post: description: | Schedules attach operation to happen in the background for the given tenant. @@ -1020,6 +1019,9 @@ components: properties: config: $ref: '#/components/schemas/TenantConfig' + generation: + type: integer + description: Attachment generation number. TenantConfigRequest: allOf: - $ref: '#/components/schemas/TenantConfig' diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index f86657fa77..e8181fef27 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -32,11 +32,13 @@ use crate::tenant::mgr::{ }; use crate::tenant::size::ModelInputs; use crate::tenant::storage_layer::LayerAccessStatsReset; -use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError, Timeline}; +use crate::tenant::timeline::Timeline; +use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError}; use crate::{config::PageServerConf, tenant::mgr}; use crate::{disk_usage_eviction_task, tenant}; use utils::{ auth::JwtAuth, + generation::Generation, http::{ endpoint::{self, attach_openapi_ui, auth_middleware, check_permission_with}, error::{ApiError, HttpErrorBody}, @@ -472,11 +474,19 @@ async fn tenant_attach_handler( check_permission(&request, Some(tenant_id))?; let maybe_body: Option = json_request_or_empty_body(&mut request).await?; - let tenant_conf = match maybe_body { + let tenant_conf = match &maybe_body { Some(request) => TenantConfOpt::try_from(&*request.config).map_err(ApiError::BadRequest)?, None => TenantConfOpt::default(), }; + // TODO: make generation mandatory here once control plane supports it + let generation = maybe_body + .as_ref() + .map(|tar| tar.generation) + .flatten() + .map(|g| Generation::new(g)) + .unwrap_or(Generation::none()); + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn); info!("Handling tenant attach {tenant_id}"); @@ -487,6 +497,7 @@ async fn tenant_attach_handler( mgr::attach_tenant( state.conf, tenant_id, + generation, tenant_conf, state.broker_client.clone(), remote_storage.clone(), @@ -867,6 +878,12 @@ async fn tenant_create_handler( let tenant_conf = TenantConfOpt::try_from(&request_data.config).map_err(ApiError::BadRequest)?; + // TODO: make generation mandatory here once control plane supports it. + let generation = request_data + .generation + .map(|g| Generation::new(g)) + .unwrap_or(Generation::none()); + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Warn); let state = get_state(&request); @@ -875,6 +892,7 @@ async fn tenant_create_handler( state.conf, tenant_conf, target_tenant_id, + generation, state.broker_client.clone(), state.remote_storage.clone(), &ctx, diff --git a/pageserver/src/tenant/mgr.rs b/pageserver/src/tenant/mgr.rs index 87617b544c..38c87a67a9 100644 --- a/pageserver/src/tenant/mgr.rs +++ b/pageserver/src/tenant/mgr.rs @@ -126,6 +126,9 @@ pub async fn init_tenant_mgr( match schedule_local_tenant_processing( conf, &tenant_dir_path, + // TODO: call into control plane, don't start Tenants + // purely because they are on disk + Generation::none(), resources.clone(), Some(init_order.clone()), &TENANTS, @@ -162,6 +165,7 @@ pub async fn init_tenant_mgr( pub(crate) fn schedule_local_tenant_processing( conf: &'static PageServerConf, tenant_path: &Path, + generation: Generation, resources: TenantSharedResources, init_order: Option, tenants: &'static tokio::sync::RwLock, @@ -203,7 +207,7 @@ pub(crate) fn schedule_local_tenant_processing( match Tenant::spawn_attach( conf, tenant_id, - Generation::none(), + generation, resources.broker_client, tenants, remote_storage, @@ -357,6 +361,7 @@ pub async fn create_tenant( conf: &'static PageServerConf, tenant_conf: TenantConfOpt, tenant_id: TenantId, + generation: Generation, broker_client: storage_broker::BrokerClientChannel, remote_storage: Option, ctx: &RequestContext, @@ -374,7 +379,8 @@ pub async fn create_tenant( remote_storage, }; let created_tenant = - schedule_local_tenant_processing(conf, &tenant_directory, tenant_resources, None, &TENANTS, ctx)?; + schedule_local_tenant_processing(conf, &tenant_directory, + generation, tenant_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 @@ -537,7 +543,9 @@ pub async fn load_tenant( broker_client, remote_storage, }; - let new_tenant = schedule_local_tenant_processing(conf, &tenant_path, resources, None, &TENANTS, ctx) + // TODO: remove the `/load` API once generation support is complete: + // it becomes equivalent to attaching. + let new_tenant = schedule_local_tenant_processing(conf, &tenant_path, Generation::none(), resources, None, &TENANTS, ctx) .with_context(|| { format!("Failed to schedule tenant processing in path {tenant_path:?}") })?; @@ -601,6 +609,7 @@ pub async fn list_tenants() -> Result, TenantMapLis pub async fn attach_tenant( conf: &'static PageServerConf, tenant_id: TenantId, + generation: Generation, tenant_conf: TenantConfOpt, broker_client: storage_broker::BrokerClientChannel, remote_storage: GenericRemoteStorage, @@ -622,7 +631,7 @@ pub async fn attach_tenant( broker_client, remote_storage: Some(remote_storage), }; - let attached_tenant = schedule_local_tenant_processing(conf, &tenant_dir, resources, None, &TENANTS, ctx)?; + let attached_tenant = schedule_local_tenant_processing(conf, &tenant_dir, generation, 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