From f95fdf5b44b084ad961d3439dc82c5cb95b46a3d Mon Sep 17 00:00:00 2001 From: Vlad Lazar Date: Thu, 3 Jul 2025 17:35:46 +0100 Subject: [PATCH] pageserver: fix duplicate tombstones in ancestor detach (#12460) ## Problem Ancestor detach from a previously detached parent when there were no writes panics since it tries to upload the tombstone layer twice. ## Summary of Changes If we're gonna copy the tombstone from the ancestor, don't bother creating it. Fixes https://github.com/neondatabase/neon/issues/12458 --- .../src/tenant/timeline/detach_ancestor.rs | 18 ++++++++++++- .../regress/test_timeline_detach_ancestor.py | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/pageserver/src/tenant/timeline/detach_ancestor.rs b/pageserver/src/tenant/timeline/detach_ancestor.rs index f20a1343df..223e888e27 100644 --- a/pageserver/src/tenant/timeline/detach_ancestor.rs +++ b/pageserver/src/tenant/timeline/detach_ancestor.rs @@ -182,6 +182,7 @@ pub(crate) async fn generate_tombstone_image_layer( detached: &Arc, ancestor: &Arc, ancestor_lsn: Lsn, + historic_layers_to_copy: &Vec, ctx: &RequestContext, ) -> Result, Error> { tracing::info!( @@ -199,6 +200,20 @@ pub(crate) async fn generate_tombstone_image_layer( let image_lsn = ancestor_lsn; { + for layer in historic_layers_to_copy { + let desc = layer.layer_desc(); + if !desc.is_delta + && desc.lsn_range.start == image_lsn + && overlaps_with(&key_range, &desc.key_range) + { + tracing::info!( + layer=%layer, "will copy tombstone from ancestor instead of creating a new one" + ); + + return Ok(None); + } + } + let layers = detached .layers .read(LayerManagerLockHolder::DetachAncestor) @@ -450,7 +465,8 @@ pub(super) async fn prepare( Vec::with_capacity(straddling_branchpoint.len() + rest_of_historic.len() + 1); if let Some(tombstone_layer) = - generate_tombstone_image_layer(detached, &ancestor, ancestor_lsn, ctx).await? + generate_tombstone_image_layer(detached, &ancestor, ancestor_lsn, &rest_of_historic, ctx) + .await? { new_layers.push(tombstone_layer.into()); } diff --git a/test_runner/regress/test_timeline_detach_ancestor.py b/test_runner/regress/test_timeline_detach_ancestor.py index 22be3d61ba..c0f163db32 100644 --- a/test_runner/regress/test_timeline_detach_ancestor.py +++ b/test_runner/regress/test_timeline_detach_ancestor.py @@ -1889,6 +1889,31 @@ def test_timeline_detach_with_aux_files_with_detach_v1( assert set(http.list_aux_files(env.initial_tenant, branch_timeline_id, lsn1).keys()) == set([]) +def test_detach_ancestors_with_no_writes( + neon_env_builder: NeonEnvBuilder, +): + env = neon_env_builder.init_start() + + endpoint = env.endpoints.create_start("main", tenant_id=env.initial_tenant) + wait_for_last_flush_lsn(env, endpoint, env.initial_tenant, env.initial_timeline) + endpoint.safe_psql( + "SELECT pg_create_logical_replication_slot('test_slot_parent_1', 'pgoutput')" + ) + wait_for_last_flush_lsn(env, endpoint, env.initial_tenant, env.initial_timeline) + endpoint.stop() + + for i in range(0, 5): + if i == 0: + ancestor_name = "main" + else: + ancestor_name = f"b{i}" + + tlid = env.create_branch(f"b{i + 1}", ancestor_branch_name=ancestor_name) + + client = env.pageserver.http_client() + client.detach_ancestor(tenant_id=env.initial_tenant, timeline_id=tlid) + + # TODO: # - branch near existing L1 boundary, image layers? # - investigate: why are layers started at uneven lsn? not just after branching, but in general.