This commit is contained in:
Christian Schwarz
2025-07-20 18:51:36 +00:00
parent ca82b739d3
commit 0d2c100048
6 changed files with 374 additions and 54 deletions

View File

@@ -70,6 +70,18 @@ service PageService {
// Acquires or extends a lease on the given LSN. This guarantees that the Pageserver won't garbage
// collect the LSN until the lease expires. Must be acquired on all relevant shards.
rpc LeaseLsn (LeaseLsnRequest) returns (LeaseLsnResponse);
// Upserts a standby_horizon lease. RO replicas rely on this type of lease.
// In slightly more detail: RO replicas always lag to some degree behind the
// primary, and request pages at their respective apply LSN. The standby horizon mechanism
// ensures that the Pageserver does not garbage-collect old page versions in
// the interval between `min(valid standby horizon leases)` and the most recent page version.
//
// Each RO replica call this method continuously as it applies more WAL.
// It identifies its lease through an opaque "lease_id" across these requests.
// The response contains the lease expiration time.
// Status `FailedPrecondition` is returned if the lease cannot be granted.
rpc LeaseStandbyHorizon(LeaseStandbyHorizonRequest) returns (LeaseStandbyHorizonResponse);
}
// The LSN a request should read at.
@@ -272,3 +284,16 @@ message LeaseLsnResponse {
// The lease expiration time.
google.protobuf.Timestamp expires = 1;
}
// Request for LeaseStandbyHorizon rpc.
// The lease_id identifies the lease in subsequent requests.
// The lsn must be monotonic; the request will fail if it is not.
message LeaseStandbyHorizonRequest {
string lease_id = 1;
uint64 lsn = 2;
}
// Response for the success case of LeaseStandbyHorizon rpc.
message LeaseStandbyHorizonResponse {
google.protobuf.Timestamp expiration = 1;
}

View File

@@ -143,6 +143,12 @@ impl Client {
let resp = self.inner.lease_lsn(req).await?.into_inner();
Ok(resp.try_into()?)
}
pub async fn lease_standby_horizon(&mut self, req: LeaseStandbyHorizonRequest) -> tonic::Result<LeaseStandbyHorizonResponse> {
let req = proto::LeaseStandbyHorizonRequest::from(req);
let resp = self.inner.lease_standby_horizon(req).await?.into_inner();
Ok(resp.try_into()?)
}
}
/// Adds authentication metadata to gRPC requests.

View File

@@ -755,3 +755,64 @@ impl From<LeaseLsnResponse> for proto::LeaseLsnResponse {
}
}
}
pub struct LeaseStandbyHorizonRequest {
pub lease_id: String,
pub lsn: Lsn,
}
impl TryFrom<proto::LeaseStandbyHorizonRequest> for LeaseStandbyHorizonRequest {
type Error = ProtocolError;
fn try_from(pb: proto::LeaseStandbyHorizonRequest) -> Result<Self, Self::Error> {
if pb.lsn == 0 {
return Err(ProtocolError::Missing("lsn"));
}
if pb.lease_id.len() == 0 {
return Err(ProtocolError::Invalid("lease_id", pb.lease_id));
}
Ok(Self {
lease_id: pb.lease_id,
lsn: Lsn(pb.lsn),
})
}
}
impl From<LeaseStandbyHorizonRequest> for proto::LeaseStandbyHorizonRequest {
fn from(request: LeaseStandbyHorizonRequest) -> Self {
Self {
lease_id: request.lease_id,
lsn: request.lsn.0,
}
}
}
/// Lease expiration time. If the lease could not be granted because the LSN has already been
/// garbage collected, a FailedPrecondition status will be returned instead.
pub type LeaseStandbyHorizonResponse = SystemTime;
impl TryFrom<proto::LeaseStandbyHorizonResponse> for LeaseStandbyHorizonResponse {
type Error = ProtocolError;
fn try_from(pb: proto::LeaseStandbyHorizonResponse) -> Result<Self, Self::Error> {
let expiration = pb.expiration.ok_or(ProtocolError::Missing("expiration"))?;
UNIX_EPOCH
.checked_add(Duration::new(
expiration.seconds as u64,
expiration.nanos as u32,
))
.ok_or_else(|| ProtocolError::invalid("expiration", expiration))
}
}
impl From<LeaseStandbyHorizonResponse> for proto::LeaseStandbyHorizonResponse {
fn from(response: LeaseStandbyHorizonResponse) -> Self {
let expiration = response.duration_since(UNIX_EPOCH).unwrap_or_default();
Self {
expiration: Some(prost_types::Timestamp {
seconds: expiration.as_secs() as i64,
nanos: expiration.subsec_nanos() as i32,
}),
}
}
}