mirror of
https://github.com/neondatabase/neon.git
synced 2026-05-25 00:50:36 +00:00
WIP
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2318,6 +2318,7 @@ pub(crate) enum ComputeCommandKind {
|
||||
Basebackup,
|
||||
Fullbackup,
|
||||
LeaseLsn,
|
||||
LeaseStandbyHorizon,
|
||||
}
|
||||
|
||||
pub(crate) struct ComputeCommandCounters {
|
||||
|
||||
@@ -76,6 +76,7 @@ use crate::pgdatadir_mapping::{LsnRange, Version};
|
||||
use crate::span::{
|
||||
debug_assert_current_span_has_tenant_and_timeline_id,
|
||||
debug_assert_current_span_has_tenant_and_timeline_id_no_shard_id,
|
||||
debug_assert_current_span_has_tenant_id,
|
||||
};
|
||||
use crate::task_mgr::{self, COMPUTE_REQUEST_RUNTIME, TaskKind};
|
||||
use crate::tenant::mgr::{
|
||||
@@ -2218,7 +2219,7 @@ impl PageServerHandler {
|
||||
valid_until_str.as_deref().unwrap_or("<unknown>")
|
||||
);
|
||||
|
||||
let bytes = valid_until_str.as_ref().map(|x| x.as_bytes());
|
||||
let bytes: Option<&[u8]> = valid_until_str.as_ref().map(|x| x.as_bytes());
|
||||
|
||||
pgb.write_message_noflush(&BeMessage::RowDescription(&[RowDescriptor::text_col(
|
||||
b"valid_until",
|
||||
@@ -2228,6 +2229,51 @@ impl PageServerHandler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(shard_id, %lsn))]
|
||||
async fn handle_lease_standby_horizon<IO>(
|
||||
&mut self,
|
||||
pgb: &mut PostgresBackend<IO>,
|
||||
tenant_shard_id: TenantShardId,
|
||||
timeline_id: TimelineId,
|
||||
lease_id: String,
|
||||
lsn: Lsn,
|
||||
ctx: &RequestContext,
|
||||
) -> Result<(), QueryError>
|
||||
where
|
||||
IO: AsyncRead + AsyncWrite + Send + Sync + Unpin,
|
||||
{
|
||||
debug_assert_current_span_has_tenant_id();
|
||||
|
||||
let timeline = self
|
||||
.timeline_handles
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.get(
|
||||
tenant_shard_id.tenant_id,
|
||||
timeline_id,
|
||||
ShardSelector::Known(tenant_shard_id.to_index()),
|
||||
)
|
||||
.await?;
|
||||
set_tracing_field_shard_id(&timeline);
|
||||
|
||||
let result: Option<SystemTime> = todo!();
|
||||
|
||||
// Encode result as Option<millis since epoch>
|
||||
let bytes = result.map(|t| {
|
||||
t.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("we wouldn't allow a lease at epoch, system time would be horribly off")
|
||||
.as_millis()
|
||||
.to_string()
|
||||
.into_bytes()
|
||||
});
|
||||
pgb.write_message_noflush(&BeMessage::RowDescription(&[RowDescriptor::text_col(
|
||||
b"expiration",
|
||||
)]))?
|
||||
.write_message_noflush(&BeMessage::DataRow(&[bytes.as_deref()]))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(shard_id))]
|
||||
async fn handle_get_rel_exists_request(
|
||||
timeline: &Timeline,
|
||||
@@ -2718,6 +2764,14 @@ struct LeaseLsnCmd {
|
||||
lsn: Lsn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct LeaseStandbyHorizonCmd {
|
||||
tenant_shard_id: TenantShardId,
|
||||
timeline_id: TimelineId,
|
||||
lease_id: String,
|
||||
lsn: Lsn,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
enum PageServiceCmd {
|
||||
Set,
|
||||
@@ -2725,6 +2779,7 @@ enum PageServiceCmd {
|
||||
BaseBackup(BaseBackupCmd),
|
||||
FullBackup(FullBackupCmd),
|
||||
LeaseLsn(LeaseLsnCmd),
|
||||
LeaseStandbyHorizon(LeaseStandbyHorizonCmd),
|
||||
}
|
||||
|
||||
impl PageStreamCmd {
|
||||
@@ -2874,6 +2929,31 @@ impl LeaseLsnCmd {
|
||||
}
|
||||
}
|
||||
|
||||
impl LeaseStandbyHorizonCmd {
|
||||
fn parse(query: &str) -> anyhow::Result<Self> {
|
||||
let parameters = query.split_whitespace().collect_vec();
|
||||
if parameters.len() != 4 {
|
||||
bail!(
|
||||
"invalid number of parameters for lease lsn command: {}",
|
||||
query
|
||||
);
|
||||
}
|
||||
let tenant_shard_id = TenantShardId::from_str(parameters[0])
|
||||
.with_context(|| format!("Failed to parse tenant id from {}", parameters[0]))?;
|
||||
let timeline_id = TimelineId::from_str(parameters[1])
|
||||
.with_context(|| format!("Failed to parse timeline id from {}", parameters[1]))?;
|
||||
let lease_id = parameters[2].to_string();
|
||||
let standby_horizon = Lsn::from_str(parameters[3])
|
||||
.with_context(|| format!("Failed to parse lsn from {}", parameters[2]))?;
|
||||
Ok(Self {
|
||||
tenant_shard_id,
|
||||
timeline_id,
|
||||
lease_id,
|
||||
lsn: standby_horizon,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PageServiceCmd {
|
||||
fn parse(query: &str) -> anyhow::Result<Self> {
|
||||
let query = query.trim();
|
||||
@@ -2898,6 +2978,10 @@ impl PageServiceCmd {
|
||||
let cmd2 = cmd2.to_ascii_lowercase();
|
||||
if cmd2 == "lsn" {
|
||||
Ok(Self::LeaseLsn(LeaseLsnCmd::parse(other)?))
|
||||
} else if cmd2 == "standby_horizon" {
|
||||
Ok(Self::LeaseStandbyHorizon(LeaseStandbyHorizonCmd::parse(
|
||||
other,
|
||||
)?))
|
||||
} else {
|
||||
bail!("invalid lease command: {cmd}");
|
||||
}
|
||||
@@ -3161,6 +3245,45 @@ where
|
||||
}
|
||||
};
|
||||
}
|
||||
PageServiceCmd::LeaseStandbyHorizon(LeaseStandbyHorizonCmd {
|
||||
tenant_shard_id,
|
||||
timeline_id,
|
||||
lease_id,
|
||||
lsn,
|
||||
}) => {
|
||||
tracing::Span::current()
|
||||
.record("tenant_id", field::display(tenant_shard_id))
|
||||
.record("timeline_id", field::display(timeline_id));
|
||||
|
||||
self.check_permission(Some(tenant_shard_id.tenant_id))?;
|
||||
|
||||
COMPUTE_COMMANDS_COUNTERS
|
||||
.for_command(ComputeCommandKind::LeaseStandbyHorizon)
|
||||
.inc();
|
||||
|
||||
match self
|
||||
.handle_lease_standby_horizon(
|
||||
pgb,
|
||||
tenant_shard_id,
|
||||
timeline_id,
|
||||
lease_id,
|
||||
lsn,
|
||||
&ctx,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
pgb.write_message_noflush(&BeMessage::CommandComplete(b"SELECT 1"))?
|
||||
}
|
||||
Err(e) => {
|
||||
error!("error obtaining standby_horizon lease for {lsn}: {e:?}");
|
||||
pgb.write_message_noflush(&BeMessage::ErrorResponse(
|
||||
&e.to_string(),
|
||||
Some(e.pg_error_code()),
|
||||
))?
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -3801,6 +3924,11 @@ impl proto::PageService for GrpcPageServiceHandler {
|
||||
|
||||
Ok(tonic::Response::new(expires.into()))
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(lease_id, lsn))]
|
||||
async fn lease_standby_horizon(&self, req: tonic::Request<proto::LeaseStandbyHorizonRequest>) -> Result<tonic::Response<proto::LeaseStandbyHorizonResponse>, tonic::Status> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// gRPC middleware layer that handles observability concerns:
|
||||
|
||||
Reference in New Issue
Block a user