Add gRPC API to set shard cutoff point (#3661)

* Add functions to propagate updating cutoff point from collection level

* Add gRPC endpoint to set cutoff point

* Lock highest and cutoff clock maps separately
This commit is contained in:
Tim Visée
2024-02-22 13:29:09 +01:00
committed by GitHub
parent 0f0c357e05
commit e7fbc39ae3
9 changed files with 232 additions and 9 deletions

View File

@@ -159,6 +159,7 @@ fn configure_validation(builder: Builder) -> Builder {
("WaitForShardStateRequest.collection_name", "length(min = 1, max = 255)"),
("WaitForShardStateRequest.timeout", "range(min = 1)"),
("GetShardRecoveryPointRequest.collection_name", "length(min = 1, max = 255)"),
("UpdateShardCutoffPointRequest.collection_name", "length(min = 1, max = 255)"),
], &[])
// Service: points.proto
.validates(&[

View File

@@ -23,6 +23,10 @@ service CollectionsInternal {
Get shard recovery point
*/
rpc GetShardRecoveryPoint (GetShardRecoveryPointRequest) returns (GetShardRecoveryPointResponse) {}
/*
Update shard cutoff point
*/
rpc UpdateShardCutoffPoint (UpdateShardCutoffPointRequest) returns (CollectionOperationResponse) {}
}
message GetCollectionInfoRequestInternal {
@@ -61,3 +65,9 @@ message RecoveryPointClockTag {
uint32 clock_id = 2;
uint64 clock_tick = 3;
}
message UpdateShardCutoffPointRequest {
string collection_name = 1; // Name of the collection
uint32 shard_id = 2; // Id of the shard
RecoveryPoint cutoff = 3; // Cutoff point of the shard
}

View File

@@ -6732,6 +6732,22 @@ pub struct RecoveryPointClockTag {
#[prost(uint64, tag = "3")]
pub clock_tick: u64,
}
#[derive(validator::Validate)]
#[derive(serde::Serialize)]
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UpdateShardCutoffPointRequest {
/// Name of the collection
#[prost(string, tag = "1")]
#[validate(length(min = 1, max = 255))]
pub collection_name: ::prost::alloc::string::String,
/// Id of the shard
#[prost(uint32, tag = "2")]
pub shard_id: u32,
/// Cutoff point of the shard
#[prost(message, optional, tag = "3")]
pub cutoff: ::core::option::Option<RecoveryPoint>,
}
/// Generated client implementations.
pub mod collections_internal_client {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
@@ -6932,6 +6948,38 @@ pub mod collections_internal_client {
);
self.inner.unary(req, path, codec).await
}
///
/// Update shard cutoff point
pub async fn update_shard_cutoff_point(
&mut self,
request: impl tonic::IntoRequest<super::UpdateShardCutoffPointRequest>,
) -> std::result::Result<
tonic::Response<super::CollectionOperationResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/qdrant.CollectionsInternal/UpdateShardCutoffPoint",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"qdrant.CollectionsInternal",
"UpdateShardCutoffPoint",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
@@ -6977,6 +7025,15 @@ pub mod collections_internal_server {
tonic::Response<super::GetShardRecoveryPointResponse>,
tonic::Status,
>;
///
/// Update shard cutoff point
async fn update_shard_cutoff_point(
&self,
request: tonic::Request<super::UpdateShardCutoffPointRequest>,
) -> std::result::Result<
tonic::Response<super::CollectionOperationResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct CollectionsInternalServer<T: CollectionsInternal> {
@@ -7252,6 +7309,56 @@ pub mod collections_internal_server {
};
Box::pin(fut)
}
"/qdrant.CollectionsInternal/UpdateShardCutoffPoint" => {
#[allow(non_camel_case_types)]
struct UpdateShardCutoffPointSvc<T: CollectionsInternal>(pub Arc<T>);
impl<
T: CollectionsInternal,
> tonic::server::UnaryService<super::UpdateShardCutoffPointRequest>
for UpdateShardCutoffPointSvc<T> {
type Response = super::CollectionOperationResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::UpdateShardCutoffPointRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as CollectionsInternal>::update_shard_cutoff_point(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = UpdateShardCutoffPointSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
Ok(

View File

@@ -416,6 +416,23 @@ impl Collection {
replica_set.shard_recovery_point().await
}
pub async fn update_shard_cutoff_point(
&self,
shard_id: ShardId,
cutoff: &RecoveryPoint,
) -> CollectionResult<()> {
let shard_holder_read = self.shards_holder.read().await;
let shard = shard_holder_read.get_shard(&shard_id);
let Some(replica_set) = shard else {
return Err(CollectionError::NotFound {
what: "Shard {shard_id}".into(),
});
};
replica_set.update_shard_cutoff_point(cutoff).await
}
pub async fn state(&self) -> State {
let shards_holder = self.shards_holder.read().await;
let transfers = shards_holder.shard_transfers.read().clone();

View File

@@ -907,6 +907,13 @@ impl LocalShard {
pub async fn recovery_point(&self) -> RecoveryPoint {
self.wal.recovery_point().await
}
/// Update the cutoff point on the current shard
///
/// This also updates the highest seen clocks.
pub async fn update_cutoff(&self, cutoff: &RecoveryPoint) {
self.wal.update_cutoff(cutoff).await
}
}
impl Drop for LocalShard {

View File

@@ -829,6 +829,21 @@ impl ShardReplicaSet {
local_shard.shard_recovery_point().await
}
/// Update the cutoff point for the local shard.
pub(crate) async fn update_shard_cutoff_point(
&self,
cutoff: &RecoveryPoint,
) -> CollectionResult<()> {
let local_shard = self.local.read().await;
let Some(local_shard) = local_shard.as_ref() else {
return Err(CollectionError::NotFound {
what: "Peer does not have local shard".into(),
});
};
local_shard.update_cutoff(cutoff).await
}
}
/// Represents a replica set state

View File

@@ -163,6 +163,22 @@ impl Shard {
}
}
pub async fn update_cutoff(&self, cutoff: &RecoveryPoint) -> CollectionResult<()> {
match self {
Self::Local(local_shard) => local_shard.update_cutoff(cutoff).await,
Self::ForwardProxy(proxy_shard) => {
proxy_shard.wrapped_shard.update_cutoff(cutoff).await
}
Self::Proxy(_) | Self::QueueProxy(_) | Self::Dummy(_) => {
return Err(CollectionError::service_error(format!(
"Setting cutoff point not supported on {}",
self.variant_name(),
)));
}
}
Ok(())
}
pub async fn resolve_wal_delta(
&self,
recovery_point: RecoveryPoint,

View File

@@ -77,19 +77,27 @@ impl RecoverableWal {
wal_lock.write(operation).map(|op_num| (op_num, wal_lock))
}
/// Update the cutoff clock map based on the given recovery point.
/// Update the cutoff clock map based on the given recovery point
///
/// This can only increase clock ticks in the cutoff clock map. If there already are higher
/// clock ticks, they're kept.
///
/// It updates the highest seen clocks alongside with it.
pub async fn update_cutoff(&self, recovery_point: &RecoveryPoint) {
let mut highest_clocks = self.highest_clocks.lock().await;
let mut cutoff_clocks = self.cutoff_clocks.lock().await;
recovery_point.clock_tag_iter().for_each(|clock_tag| {
highest_clocks.advance_clock(clock_tag);
cutoff_clocks.advance_clock(clock_tag);
});
pub async fn update_cutoff(&self, cutoff: &RecoveryPoint) {
// Lock highest and cutoff maps separately to avoid deadlocks
{
let mut highest_clocks = self.highest_clocks.lock().await;
for clock_tag in cutoff.clock_tag_iter() {
highest_clocks.advance_clock(clock_tag);
}
}
{
let mut cutoff_clocks = self.cutoff_clocks.lock().await;
for clock_tag in cutoff.clock_tag_iter() {
cutoff_clocks.advance_clock(clock_tag);
}
}
}
/// Get a recovery point for this WAL.

View File

@@ -5,7 +5,7 @@ use api::grpc::qdrant::collections_internal_server::CollectionsInternal;
use api::grpc::qdrant::{
CollectionOperationResponse, GetCollectionInfoRequestInternal, GetCollectionInfoResponse,
GetShardRecoveryPointRequest, GetShardRecoveryPointResponse, InitiateShardTransferRequest,
WaitForShardStateRequest,
UpdateShardCutoffPointRequest, WaitForShardStateRequest,
};
use storage::content_manager::conversions::error_to_status;
use storage::content_manager::toc::TableOfContent;
@@ -155,4 +155,46 @@ impl CollectionsInternal for CollectionsInternalService {
};
Ok(Response::new(response))
}
async fn update_shard_cutoff_point(
&self,
request: Request<UpdateShardCutoffPointRequest>,
) -> Result<Response<CollectionOperationResponse>, Status> {
validate_and_log(request.get_ref());
let timing = Instant::now();
let UpdateShardCutoffPointRequest {
collection_name,
shard_id,
cutoff,
} = request.into_inner();
let cutoff = cutoff.ok_or_else(|| Status::invalid_argument("Missing cutoff point"))?;
let collection_read = self
.toc
.get_collection(&collection_name)
.await
.map_err(|err| {
Status::not_found(format!(
"Collection {collection_name} could not be found: {err}"
))
})?;
// Set the shard cutoff point
collection_read
.update_shard_cutoff_point(shard_id, &cutoff.into())
.await
.map_err(|err| {
Status::internal(format!(
"Failed to set shard cutoff point for shard {shard_id}: {err}"
))
})?;
let response = CollectionOperationResponse {
result: true,
time: timing.elapsed().as_secs_f64(),
};
Ok(Response::new(response))
}
}