diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index cb7c2b60ef..f18f0b730c 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -324,6 +324,9 @@ impl From for ApiError { match value { NotFound => ApiError::NotFound(anyhow::anyhow!("timeline not found").into()), Timeout => ApiError::Timeout("hit pageserver internal timeout".into()), + e @ HasArchivedParent(_) => { + ApiError::PreconditionFailed(e.to_string().into_boxed_str()) + } HasUnarchivedChildren(children) => ApiError::PreconditionFailed( format!( "Cannot archive timeline which has non-archived child timelines: {children:?}" diff --git a/pageserver/src/tenant.rs b/pageserver/src/tenant.rs index 60ab242ffc..fb30857ddf 100644 --- a/pageserver/src/tenant.rs +++ b/pageserver/src/tenant.rs @@ -509,6 +509,9 @@ pub enum TimelineArchivalError { #[error("Timeout")] Timeout, + #[error("ancestor is archived: {}", .0)] + HasArchivedParent(TimelineId), + #[error("HasUnarchivedChildren")] HasUnarchivedChildren(Vec), @@ -524,6 +527,7 @@ impl Debug for TimelineArchivalError { match self { Self::NotFound => write!(f, "NotFound"), Self::Timeout => write!(f, "Timeout"), + Self::HasArchivedParent(p) => f.debug_tuple("HasArchivedParent").field(p).finish(), Self::HasUnarchivedChildren(c) => { f.debug_tuple("HasUnarchivedChildren").field(c).finish() } @@ -1369,11 +1373,20 @@ impl Tenant { let timeline = { let timelines = self.timelines.lock().unwrap(); - let timeline = match timelines.get(&timeline_id) { - Some(t) => t, - None => return Err(TimelineArchivalError::NotFound), + let Some(timeline) = timelines.get(&timeline_id) else { + return Err(TimelineArchivalError::NotFound); }; + if state == TimelineArchivalState::Unarchived { + if let Some(ancestor_timeline) = timeline.ancestor_timeline() { + if ancestor_timeline.is_archived() == Some(true) { + return Err(TimelineArchivalError::HasArchivedParent( + ancestor_timeline.timeline_id, + )); + } + } + } + // Ensure that there are no non-archived child timelines let children: Vec = timelines .iter() diff --git a/pageserver/src/tenant/timeline.rs b/pageserver/src/tenant/timeline.rs index 8096a0d18c..63d59e06a5 100644 --- a/pageserver/src/tenant/timeline.rs +++ b/pageserver/src/tenant/timeline.rs @@ -867,6 +867,11 @@ impl Timeline { .map(|ancestor| ancestor.timeline_id) } + /// Get the ancestor timeline + pub(crate) fn ancestor_timeline(&self) -> Option<&Arc> { + self.ancestor_timeline.as_ref() + } + /// Get the bytes written since the PITR cutoff on this branch, and /// whether this branch's ancestor_lsn is within its parent's PITR. pub(crate) fn get_pitr_history_stats(&self) -> (u64, bool) { diff --git a/test_runner/regress/test_timeline_archive.py b/test_runner/regress/test_timeline_archive.py index b774c7c9fe..7f158ad251 100644 --- a/test_runner/regress/test_timeline_archive.py +++ b/test_runner/regress/test_timeline_archive.py @@ -94,3 +94,29 @@ def test_timeline_archive(neon_simple_env: NeonEnv): timeline_id=parent_timeline_id, state=TimelineArchivalState.ARCHIVED, ) + + # Test that the leaf can't be unarchived + with pytest.raises( + PageserverApiException, + match="ancestor is archived", + ) as exc: + assert timeline_path.exists() + + ps_http.timeline_archival_config( + tenant_id=env.initial_tenant, + timeline_id=leaf_timeline_id, + state=TimelineArchivalState.UNARCHIVED, + ) + + # Unarchive works for the leaf if the parent gets unarchived first + ps_http.timeline_archival_config( + tenant_id=env.initial_tenant, + timeline_id=parent_timeline_id, + state=TimelineArchivalState.UNARCHIVED, + ) + + ps_http.timeline_archival_config( + tenant_id=env.initial_tenant, + timeline_id=leaf_timeline_id, + state=TimelineArchivalState.UNARCHIVED, + )