diff --git a/pageserver/src/http/openapi_spec.yml b/pageserver/src/http/openapi_spec.yml index 566086c527..7ea148971f 100644 --- a/pageserver/src/http/openapi_spec.yml +++ b/pageserver/src/http/openapi_spec.yml @@ -212,6 +212,12 @@ paths: schema: type: string format: date-time + "412": + description: No timestamp is found for given LSN, e.g. if there had been no commits till LSN + content: + application/json: + schema: + $ref: "#/components/schemas/PreconditionFailedError" /v1/tenant/{tenant_id}/timeline/{timeline_id}/get_lsn_by_timestamp: parameters: diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index bce590016e..e979a70aec 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -989,7 +989,7 @@ async fn get_lsn_by_timestamp_handler( if !tenant_shard_id.is_shard_zero() { // Requires SLRU contents, which are only stored on shard zero return Err(ApiError::BadRequest(anyhow!( - "Size calculations are only available on shard zero" + "Lsn calculations by timestamp are only available on shard zero" ))); } @@ -1064,7 +1064,7 @@ async fn get_timestamp_of_lsn_handler( if !tenant_shard_id.is_shard_zero() { // Requires SLRU contents, which are only stored on shard zero return Err(ApiError::BadRequest(anyhow!( - "Size calculations are only available on shard zero" + "Timestamp calculations by lsn are only available on shard zero" ))); } @@ -1090,8 +1090,8 @@ async fn get_timestamp_of_lsn_handler( .to_string(); json_response(StatusCode::OK, time) } - None => Err(ApiError::NotFound( - anyhow::anyhow!("Timestamp for lsn {} not found", lsn).into(), + None => Err(ApiError::PreconditionFailed( + format!("Timestamp for lsn {} not found", lsn).into(), )), } } diff --git a/pageserver/src/pgdatadir_mapping.rs b/pageserver/src/pgdatadir_mapping.rs index e3e06ab91a..4c5a07ba57 100644 --- a/pageserver/src/pgdatadir_mapping.rs +++ b/pageserver/src/pgdatadir_mapping.rs @@ -691,7 +691,7 @@ impl Timeline { Ok(buf.get_u32_le()) } - /// Get size of an SLRU segment + /// Does the slru segment exist? pub(crate) async fn get_slru_segment_exists( &self, kind: SlruKind, @@ -844,9 +844,9 @@ impl Timeline { .await } - /// Obtain the possible timestamp range for the given lsn. + /// Obtain the timestamp for the given lsn. /// - /// If the lsn has no timestamps, returns None. returns `(min, max, median)` if it has timestamps. + /// If the lsn has no timestamps (e.g. no commits), returns None. pub(crate) async fn get_timestamp_for_lsn( &self, probe_lsn: Lsn, diff --git a/test_runner/regress/test_lsn_mapping.py b/test_runner/regress/test_lsn_mapping.py index 7280a91a12..c5a1bf0d16 100644 --- a/test_runner/regress/test_lsn_mapping.py +++ b/test_runner/regress/test_lsn_mapping.py @@ -276,3 +276,34 @@ def test_ts_of_lsn_api(neon_env_builder: NeonEnvBuilder): if i > 1: before_timestamp = tbl[i - step_size][1] assert timestamp >= before_timestamp, "before_timestamp before timestamp" + + +def test_timestamp_of_lsn_empty_branch(neon_env_builder: NeonEnvBuilder): + """ + Test that getting the timestamp of the head LSN of a newly created branch works. + This verifies that we don't get a 404 error when trying to get the timestamp + of the head LSN of a branch that was just created. + We now return a special status code 412 to indicate if there is no timestamp found for lsn. + + Reproducer for https://github.com/neondatabase/neon/issues/11439 + """ + env = neon_env_builder.init_start() + + # Create a new branch + new_timeline_id = env.create_branch("test_timestamp_of_lsn_empty_branch") + + # Retrieve the commit LSN of the empty branch, which we have never run postgres on + detail = env.pageserver.http_client().timeline_detail( + tenant_id=env.initial_tenant, timeline_id=new_timeline_id + ) + head_lsn = detail["last_record_lsn"] + + # Verify that we get 412 status code + with env.pageserver.http_client() as client: + with pytest.raises(PageserverApiException) as err: + client.timeline_get_timestamp_of_lsn( + env.initial_tenant, + new_timeline_id, + head_lsn, + ) + assert err.value.status_code == 412