diff --git a/control_plane/attachment_service/src/lib.rs b/control_plane/attachment_service/src/lib.rs index d8f996952a..e4ca9aa304 100644 --- a/control_plane/attachment_service/src/lib.rs +++ b/control_plane/attachment_service/src/lib.rs @@ -17,6 +17,8 @@ enum PlacementPolicy { /// Production-ready way to attach a tenant: one attached pageserver and /// some number of secondaries. Double(usize), + /// Do not attach to any pageservers + Detached, } #[derive(Ord, PartialOrd, Eq, PartialEq, Copy, Clone)] diff --git a/control_plane/attachment_service/src/persistence.rs b/control_plane/attachment_service/src/persistence.rs index 4a3fc8c779..e944a2e9ed 100644 --- a/control_plane/attachment_service/src/persistence.rs +++ b/control_plane/attachment_service/src/persistence.rs @@ -245,6 +245,7 @@ impl Persistence { anyhow::bail!("Tried to increment generation of unknown shard"); }; shard.generation_pageserver = None; + shard.placement_policy = serde_json::to_string(&PlacementPolicy::Detached).unwrap(); Ok(()) }) .await diff --git a/control_plane/attachment_service/src/service.rs b/control_plane/attachment_service/src/service.rs index 5b44c097b1..c9ed07ae5f 100644 --- a/control_plane/attachment_service/src/service.rs +++ b/control_plane/attachment_service/src/service.rs @@ -381,6 +381,11 @@ impl Service { if let Some(new_generation) = new_generation { tenant_state.generation = new_generation; + } else { + // This is a detach notification. We must update placement policy to avoid re-attaching + // during background scheduling/reconciliation, or during attachment service restart. + assert!(attach_req.node_id.is_none()); + tenant_state.policy = PlacementPolicy::Detached; } if let Some(attaching_pageserver) = attach_req.node_id.as_ref() { @@ -870,7 +875,6 @@ impl Service { } else { let old_attached = shard.intent.attached; - shard.intent.attached = Some(migrate_req.node_id); match shard.policy { PlacementPolicy::Single => { shard.intent.secondary.clear(); @@ -884,7 +888,13 @@ impl Service { shard.intent.secondary.push(old_attached); } } + PlacementPolicy::Detached => { + return Err(ApiError::BadRequest(anyhow::anyhow!( + "Cannot migrate a tenant that is PlacementPolicy::Detached: configure it to an attached policy first" + ))) + } } + shard.intent.attached = Some(migrate_req.node_id); tracing::info!("Migrating: new intent {:?}", shard.intent); shard.sequence = shard.sequence.next(); diff --git a/control_plane/attachment_service/src/tenant_state.rs b/control_plane/attachment_service/src/tenant_state.rs index a907628eff..5290197d84 100644 --- a/control_plane/attachment_service/src/tenant_state.rs +++ b/control_plane/attachment_service/src/tenant_state.rs @@ -312,6 +312,18 @@ impl TenantState { modified = true; } } + Detached => { + // Should have no attached or secondary pageservers + if self.intent.attached.is_some() { + self.intent.attached = None; + modified = true; + } + + if !self.intent.secondary.is_empty() { + self.intent.secondary.clear(); + modified = true; + } + } } if modified {