mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-24 21:26:28 +00:00
fix(scanner): fence movement generation publication (#6461)
* feat(scanner): add movement generation fencing * fix(scanner): prioritize unverified cycle deferral * feat(ecstore): add scanner publication lease fence * feat(rpc): add scanner publication lease protocol * feat(scanner): hold remote leases through usage publish * test(scanner): cover publication lease fencing * fix(scanner): fence remote leases across restart and delay * feat(rpc): fence scanner publication rename writes * fix(scanner): fence observed cleanup deletes * fix(proto): qualify lease release test types * fix(scanner): pin movement notifications * fix(scanner): clean publication imports * fix(ecstore): satisfy scanner fence clippy * refactor(scanner): group wait and publication options * fix(scanner): satisfy final lint and facade guards * fix(rpc): resolve facade export conflicts * fix(ci): remove unused decommission and healing facades * fix(ci): cfg-gate test-only usage overlay import * fix(scanner): wake on remote scanner restart
This commit is contained in:
@@ -142,6 +142,9 @@ pub struct DeleteRequest {
|
||||
pub path: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "4")]
|
||||
pub options: ::prost::alloc::string::String,
|
||||
/// Optional scanner publication lease token.
|
||||
#[prost(bytes = "bytes", tag = "5")]
|
||||
pub scanner_publication_lease_token: ::prost::bytes::Bytes,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct DeleteResponse {
|
||||
@@ -393,6 +396,8 @@ pub struct RenameDataRequest {
|
||||
pub dst_path: ::prost::alloc::string::String,
|
||||
#[prost(bytes = "bytes", tag = "7")]
|
||||
pub file_info_bin: ::prost::bytes::Bytes,
|
||||
#[prost(bytes = "bytes", tag = "8")]
|
||||
pub scanner_publication_lease_token: ::prost::bytes::Bytes,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct RenameDataResponse {
|
||||
@@ -1213,6 +1218,62 @@ pub struct ScannerActivityResponse {
|
||||
pub dirty_usage_generation: u64,
|
||||
#[prost(bool, tag = "9")]
|
||||
pub dirty_usage_pending: bool,
|
||||
#[prost(uint64, optional, tag = "10")]
|
||||
pub movement_generation: ::core::option::Option<u64>,
|
||||
#[prost(bool, optional, tag = "11")]
|
||||
pub publication_blocked: ::core::option::Option<bool>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ScannerPublicationLeaseRequest {
|
||||
#[prost(bytes = "bytes", tag = "1")]
|
||||
pub challenge: ::prost::bytes::Bytes,
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub expected_movement_generation: u64,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub ttl_ms: u64,
|
||||
#[prost(string, tag = "4")]
|
||||
pub expected_session_id: ::prost::alloc::string::String,
|
||||
#[prost(bytes = "bytes", tag = "5")]
|
||||
pub token: ::prost::bytes::Bytes,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ScannerPublicationLeaseResponse {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub success: bool,
|
||||
#[prost(bytes = "bytes", tag = "2")]
|
||||
pub token: ::prost::bytes::Bytes,
|
||||
#[prost(uint64, tag = "3")]
|
||||
pub movement_generation: u64,
|
||||
#[prost(uint64, tag = "4")]
|
||||
pub lease_ttl_ms: u64,
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub error: ::core::option::Option<Error>,
|
||||
#[prost(bytes = "bytes", tag = "6")]
|
||||
pub response_proof: ::prost::bytes::Bytes,
|
||||
#[prost(string, tag = "7")]
|
||||
pub owner_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "8")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ScannerPublicationLeaseReleaseRequest {
|
||||
#[prost(bytes = "bytes", tag = "1")]
|
||||
pub challenge: ::prost::bytes::Bytes,
|
||||
#[prost(bytes = "bytes", tag = "2")]
|
||||
pub token: ::prost::bytes::Bytes,
|
||||
#[prost(string, tag = "3")]
|
||||
pub owner_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "4")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ScannerPublicationLeaseReleaseResponse {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub success: bool,
|
||||
#[prost(message, optional, tag = "2")]
|
||||
pub error: ::core::option::Option<Error>,
|
||||
#[prost(bytes = "bytes", tag = "3")]
|
||||
pub response_proof: ::prost::bytes::Bytes,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct BackgroundHealStatusRequest {
|
||||
@@ -2695,6 +2756,36 @@ pub mod node_service_client {
|
||||
.insert(GrpcMethod::new("node_service.NodeService", "ScannerActivity"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn acquire_scanner_publication_lease(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ScannerPublicationLeaseRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ScannerPublicationLeaseResponse>, tonic::Status> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unknown(format!("Service was not ready: {}", e.into())))?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static("/node_service.NodeService/AcquireScannerPublicationLease");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("node_service.NodeService", "AcquireScannerPublicationLease"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn release_scanner_publication_lease(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ScannerPublicationLeaseReleaseRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ScannerPublicationLeaseReleaseResponse>, tonic::Status> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| tonic::Status::unknown(format!("Service was not ready: {}", e.into())))?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static("/node_service.NodeService/ReleaseScannerPublicationLease");
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("node_service.NodeService", "ReleaseScannerPublicationLease"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn background_heal_status(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::BackgroundHealStatusRequest>,
|
||||
@@ -3208,6 +3299,18 @@ pub mod node_service_server {
|
||||
&self,
|
||||
request: tonic::Request<super::ScannerActivityRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ScannerActivityResponse>, tonic::Status>;
|
||||
async fn acquire_scanner_publication_lease(
|
||||
&self,
|
||||
_request: tonic::Request<super::ScannerPublicationLeaseRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ScannerPublicationLeaseResponse>, tonic::Status> {
|
||||
Err(tonic::Status::unimplemented("scanner publication leases are unsupported"))
|
||||
}
|
||||
async fn release_scanner_publication_lease(
|
||||
&self,
|
||||
_request: tonic::Request<super::ScannerPublicationLeaseReleaseRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::ScannerPublicationLeaseReleaseResponse>, tonic::Status> {
|
||||
Err(tonic::Status::unimplemented("scanner publication leases are unsupported"))
|
||||
}
|
||||
async fn background_heal_status(
|
||||
&self,
|
||||
request: tonic::Request<super::BackgroundHealStatusRequest>,
|
||||
@@ -5488,6 +5591,67 @@ pub mod node_service_server {
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/node_service.NodeService/AcquireScannerPublicationLease" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct AcquireScannerPublicationLeaseSvc<T: NodeService>(pub Arc<T>);
|
||||
impl<T: NodeService> tonic::server::UnaryService<super::ScannerPublicationLeaseRequest> for AcquireScannerPublicationLeaseSvc<T> {
|
||||
type Response = super::ScannerPublicationLeaseResponse;
|
||||
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
|
||||
fn call(&mut self, request: tonic::Request<super::ScannerPublicationLeaseRequest>) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move { <T as NodeService>::acquire_scanner_publication_lease(&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 method = AcquireScannerPublicationLeaseSvc(inner);
|
||||
let codec = tonic_prost::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)
|
||||
}
|
||||
"/node_service.NodeService/ReleaseScannerPublicationLease" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ReleaseScannerPublicationLeaseSvc<T: NodeService>(pub Arc<T>);
|
||||
impl<T: NodeService> tonic::server::UnaryService<super::ScannerPublicationLeaseReleaseRequest>
|
||||
for ReleaseScannerPublicationLeaseSvc<T>
|
||||
{
|
||||
type Response = super::ScannerPublicationLeaseReleaseResponse;
|
||||
type Future = BoxFuture<tonic::Response<Self::Response>, tonic::Status>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::ScannerPublicationLeaseReleaseRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move { <T as NodeService>::release_scanner_publication_lease(&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 method = ReleaseScannerPublicationLeaseSvc(inner);
|
||||
let codec = tonic_prost::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)
|
||||
}
|
||||
"/node_service.NodeService/BackgroundHealStatus" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct BackgroundHealStatusSvc<T: NodeService>(pub Arc<T>);
|
||||
|
||||
+261
-2
@@ -486,6 +486,160 @@ pub fn canonical_scanner_activity_response_body(
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Builds the protocol-v7 response body. The optional movement fields are
|
||||
/// presence-bound so a missing terminal-generation proof cannot authenticate
|
||||
/// as the value zero.
|
||||
pub fn canonical_scanner_activity_v7_response_body(
|
||||
challenge: &[u8],
|
||||
response: &proto_gen::node_service::ScannerActivityResponse,
|
||||
) -> Result<Vec<u8>, std::num::TryFromIntError> {
|
||||
const DOMAIN: &[u8] = b"rustfs-scanner-activity-response-v3\0";
|
||||
|
||||
let instance_id = response.instance_id.as_bytes();
|
||||
let topology_digest = response.topology_digest.as_ref();
|
||||
let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + instance_id.len() + topology_digest.len() + 4 + 8 * 8 + 4);
|
||||
body.extend_from_slice(DOMAIN);
|
||||
body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
|
||||
body.extend_from_slice(challenge);
|
||||
body.extend_from_slice(&u64::try_from(instance_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(instance_id);
|
||||
body.extend_from_slice(&response.namespace_generation.to_be_bytes());
|
||||
body.extend_from_slice(&response.maintenance_generation.to_be_bytes());
|
||||
body.extend_from_slice(&response.protocol_version.to_be_bytes());
|
||||
body.extend_from_slice(&u64::try_from(topology_digest.len())?.to_be_bytes());
|
||||
body.extend_from_slice(topology_digest);
|
||||
body.push(u8::from(response.data_movement_active));
|
||||
body.extend_from_slice(&response.dirty_usage_generation.to_be_bytes());
|
||||
body.push(u8::from(response.dirty_usage_pending));
|
||||
body.push(u8::from(response.movement_generation.is_some()));
|
||||
if let Some(generation) = response.movement_generation {
|
||||
body.extend_from_slice(&generation.to_be_bytes());
|
||||
}
|
||||
body.push(u8::from(response.publication_blocked.is_some()));
|
||||
if let Some(blocked) = response.publication_blocked {
|
||||
body.push(u8::from(blocked));
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Builds the body authenticated by the short-lived remote scanner publication
|
||||
/// lease request. This is a separate domain from ScannerActivity so v6/v7
|
||||
/// observation proofs remain byte-for-byte compatible.
|
||||
pub fn canonical_scanner_publication_lease_request_body(
|
||||
request: &proto_gen::node_service::ScannerPublicationLeaseRequest,
|
||||
) -> Result<Vec<u8>, std::num::TryFromIntError> {
|
||||
const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-request-v1\0";
|
||||
let challenge = request.challenge.as_ref();
|
||||
let session_id = request.expected_session_id.as_bytes();
|
||||
let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + session_id.len() + 40);
|
||||
body.extend_from_slice(DOMAIN);
|
||||
body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
|
||||
body.extend_from_slice(challenge);
|
||||
body.extend_from_slice(&request.expected_movement_generation.to_be_bytes());
|
||||
body.extend_from_slice(&request.ttl_ms.to_be_bytes());
|
||||
body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(session_id);
|
||||
// Empty keeps the original acquire body byte-for-byte compatible. A
|
||||
// non-empty token is the authenticated validation form used immediately
|
||||
// before a coordinator commits its final publication.
|
||||
let token = request.token.as_ref();
|
||||
if !token.is_empty() {
|
||||
body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
|
||||
body.extend_from_slice(token);
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Builds the body authenticated by a remote scanner publication lease
|
||||
/// release request.
|
||||
pub fn canonical_scanner_publication_lease_release_request_body(
|
||||
request: &proto_gen::node_service::ScannerPublicationLeaseReleaseRequest,
|
||||
) -> Result<Vec<u8>, std::num::TryFromIntError> {
|
||||
const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-release-request-v1\0";
|
||||
let challenge = request.challenge.as_ref();
|
||||
let token = request.token.as_ref();
|
||||
let mut body = Vec::with_capacity(DOMAIN.len() + challenge.len() + token.len() + 16);
|
||||
body.extend_from_slice(DOMAIN);
|
||||
body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
|
||||
body.extend_from_slice(challenge);
|
||||
body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
|
||||
body.extend_from_slice(token);
|
||||
let owner_id = request.owner_id.as_bytes();
|
||||
let session_id = request.session_id.as_bytes();
|
||||
body.extend_from_slice(&u64::try_from(owner_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(owner_id);
|
||||
body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(session_id);
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub fn canonical_scanner_publication_lease_response_body(
|
||||
challenge: &[u8],
|
||||
response: &proto_gen::node_service::ScannerPublicationLeaseResponse,
|
||||
) -> Result<Vec<u8>, std::num::TryFromIntError> {
|
||||
const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-response-v1\0";
|
||||
let token = response.token.as_ref();
|
||||
let owner_id = response.owner_id.as_bytes();
|
||||
let session_id = response.session_id.as_bytes();
|
||||
let error_info = response.error.as_ref().map(|error| error.error_info.as_bytes());
|
||||
let error_code = response.error.as_ref().map_or(0, |error| error.code);
|
||||
let mut body = Vec::with_capacity(
|
||||
DOMAIN.len() + challenge.len() + token.len() + owner_id.len() + session_id.len() + error_info.map_or(0, |v| v.len()) + 72,
|
||||
);
|
||||
body.extend_from_slice(DOMAIN);
|
||||
body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
|
||||
body.extend_from_slice(challenge);
|
||||
body.push(u8::from(response.success));
|
||||
body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
|
||||
body.extend_from_slice(token);
|
||||
body.extend_from_slice(&response.movement_generation.to_be_bytes());
|
||||
body.extend_from_slice(&response.lease_ttl_ms.to_be_bytes());
|
||||
body.extend_from_slice(&u64::try_from(owner_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(owner_id);
|
||||
body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(session_id);
|
||||
body.push(u8::from(response.error.is_some()));
|
||||
body.extend_from_slice(&error_code.to_be_bytes());
|
||||
body.extend_from_slice(&u64::try_from(error_info.map_or(0, |value| value.len()))?.to_be_bytes());
|
||||
if let Some(error_info) = error_info {
|
||||
body.extend_from_slice(error_info);
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
pub fn canonical_scanner_publication_lease_release_response_body(
|
||||
challenge: &[u8],
|
||||
request: &proto_gen::node_service::ScannerPublicationLeaseReleaseRequest,
|
||||
response: &proto_gen::node_service::ScannerPublicationLeaseReleaseResponse,
|
||||
) -> Result<Vec<u8>, std::num::TryFromIntError> {
|
||||
const DOMAIN: &[u8] = b"rustfs-scanner-publication-lease-release-response-v1\0";
|
||||
let token = request.token.as_ref();
|
||||
let owner_id = request.owner_id.as_bytes();
|
||||
let session_id = request.session_id.as_bytes();
|
||||
let error_info = response.error.as_ref().map(|error| error.error_info.as_bytes());
|
||||
let error_code = response.error.as_ref().map_or(0, |error| error.code);
|
||||
let mut body = Vec::with_capacity(
|
||||
DOMAIN.len() + challenge.len() + token.len() + owner_id.len() + session_id.len() + error_info.map_or(0, |v| v.len()) + 56,
|
||||
);
|
||||
body.extend_from_slice(DOMAIN);
|
||||
body.extend_from_slice(&u64::try_from(challenge.len())?.to_be_bytes());
|
||||
body.extend_from_slice(challenge);
|
||||
body.extend_from_slice(&u64::try_from(token.len())?.to_be_bytes());
|
||||
body.extend_from_slice(token);
|
||||
body.extend_from_slice(&u64::try_from(owner_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(owner_id);
|
||||
body.extend_from_slice(&u64::try_from(session_id.len())?.to_be_bytes());
|
||||
body.extend_from_slice(session_id);
|
||||
body.push(u8::from(response.success));
|
||||
body.push(u8::from(response.error.is_some()));
|
||||
body.extend_from_slice(&error_code.to_be_bytes());
|
||||
body.extend_from_slice(&u64::try_from(error_info.map_or(0, |value| value.len()))?.to_be_bytes());
|
||||
if let Some(error_info) = error_info {
|
||||
body.extend_from_slice(error_info);
|
||||
}
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
/// Length-prefixed, domain-separated byte builder for the disk-mutation canonical bodies below.
|
||||
/// Every variable-length field is u64-length-prefixed and every list u64-count-prefixed, so
|
||||
/// distinct field values can never collide into the same canonical bytes.
|
||||
@@ -758,6 +912,11 @@ pub fn canonical_rename_data_request_body(
|
||||
body.push_str(&request.dst_volume)?;
|
||||
body.push_str(&request.dst_path)?;
|
||||
body.push_bytes(&request.file_info_bin)?;
|
||||
// Keep legacy rename requests byte-for-byte compatible. The optional
|
||||
// token is included only for the scanner's target-side lease fence.
|
||||
if !request.scanner_publication_lease_token.is_empty() {
|
||||
body.push_bytes(&request.scanner_publication_lease_token)?;
|
||||
}
|
||||
Ok(body.finish())
|
||||
}
|
||||
|
||||
@@ -840,6 +999,9 @@ pub fn canonical_delete_request_body(
|
||||
body.push_str(&request.volume)?;
|
||||
body.push_str(&request.path)?;
|
||||
body.push_str(&request.options)?;
|
||||
if !request.scanner_publication_lease_token.is_empty() {
|
||||
body.push_bytes(&request.scanner_publication_lease_token)?;
|
||||
}
|
||||
Ok(body.finish())
|
||||
}
|
||||
|
||||
@@ -1002,6 +1164,7 @@ mod disk_mutation_canonical_tests {
|
||||
dst_volume: "dst-vol".into(),
|
||||
dst_path: "dst-path".into(),
|
||||
file_info_bin: vec![0x81, 0x01].into(),
|
||||
scanner_publication_lease_token: Vec::new().into(),
|
||||
};
|
||||
let mut bodies = vec![canonical_rename_data_request_body(&baseline).unwrap()];
|
||||
for mutate in [
|
||||
@@ -1013,6 +1176,7 @@ mod disk_mutation_canonical_tests {
|
||||
|r: &mut RenameDataRequest| r.dst_path = "dst-path2".into(),
|
||||
|r: &mut RenameDataRequest| r.file_info_bin = vec![0x81, 0x02].into(),
|
||||
|r: &mut RenameDataRequest| r.file_info_bin = Vec::new().into(),
|
||||
|r: &mut RenameDataRequest| r.scanner_publication_lease_token = vec![0x01; 16].into(),
|
||||
] {
|
||||
let mut request = baseline.clone();
|
||||
mutate(&mut request);
|
||||
@@ -1173,6 +1337,7 @@ mod disk_mutation_canonical_tests {
|
||||
volume: "v".into(),
|
||||
path: "p".into(),
|
||||
options: "{\"o\":1}".into(),
|
||||
scanner_publication_lease_token: Vec::new().into(),
|
||||
};
|
||||
let mut bodies = vec![canonical_delete_request_body(&delete).unwrap()];
|
||||
for mutate in [
|
||||
@@ -1180,6 +1345,7 @@ mod disk_mutation_canonical_tests {
|
||||
|r: &mut DeleteRequest| r.volume = "v2".into(),
|
||||
|r: &mut DeleteRequest| r.path = "p2".into(),
|
||||
|r: &mut DeleteRequest| r.options = "{\"recursive\":true}".into(),
|
||||
|r: &mut DeleteRequest| r.scanner_publication_lease_token = vec![0x01; 16].into(),
|
||||
] {
|
||||
let mut request = delete.clone();
|
||||
mutate(&mut request);
|
||||
@@ -1565,8 +1731,12 @@ mod non_disk_mutation_canonical_tests {
|
||||
mod scanner_activity_tests {
|
||||
use super::{
|
||||
canonical_scanner_activity_request_body, canonical_scanner_activity_response_body,
|
||||
canonical_scanner_activity_v4_response_body,
|
||||
proto_gen::node_service::{ScannerActivityRequest, ScannerActivityResponse},
|
||||
canonical_scanner_activity_v4_response_body, canonical_scanner_activity_v7_response_body,
|
||||
canonical_scanner_publication_lease_release_request_body, canonical_scanner_publication_lease_request_body,
|
||||
canonical_scanner_publication_lease_response_body,
|
||||
proto_gen::node_service::{
|
||||
ScannerActivityRequest, ScannerActivityResponse, ScannerPublicationLeaseRequest, ScannerPublicationLeaseResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1617,6 +1787,8 @@ mod scanner_activity_tests {
|
||||
response_proof: Vec::new().into(),
|
||||
dirty_usage_generation: 11,
|
||||
dirty_usage_pending: true,
|
||||
movement_generation: Some(19),
|
||||
publication_blocked: Some(false),
|
||||
};
|
||||
let baseline =
|
||||
canonical_scanner_activity_response_body(&[1; 16], &response).expect("scanner activity response should encode");
|
||||
@@ -1667,6 +1839,25 @@ mod scanner_activity_tests {
|
||||
canonical_scanner_activity_response_body(&[2; 16], &response)
|
||||
.expect("scanner activity response with a different challenge should encode")
|
||||
);
|
||||
|
||||
let v7_baseline =
|
||||
canonical_scanner_activity_v7_response_body(&[1; 16], &response).expect("scanner activity v7 response should encode");
|
||||
for variant in [
|
||||
ScannerActivityResponse {
|
||||
movement_generation: Some(20),
|
||||
..response.clone()
|
||||
},
|
||||
ScannerActivityResponse {
|
||||
publication_blocked: Some(true),
|
||||
..response
|
||||
},
|
||||
] {
|
||||
assert_ne!(
|
||||
v7_baseline,
|
||||
canonical_scanner_activity_v7_response_body(&[1; 16], &variant)
|
||||
.expect("scanner activity v7 response variant should encode")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1681,6 +1872,8 @@ mod scanner_activity_tests {
|
||||
response_proof: Vec::new().into(),
|
||||
dirty_usage_generation: 0,
|
||||
dirty_usage_pending: false,
|
||||
movement_generation: None,
|
||||
publication_blocked: None,
|
||||
};
|
||||
let baseline =
|
||||
canonical_scanner_activity_v4_response_body(&[1; 16], &response).expect("scanner activity v4 response should encode");
|
||||
@@ -1714,6 +1907,72 @@ mod scanner_activity_tests {
|
||||
.expect("scanner activity v4 response should ignore v5 fields")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scanner_publication_lease_canonical_bodies_bind_session_owner_and_generation() {
|
||||
let request = ScannerPublicationLeaseRequest {
|
||||
challenge: vec![1; 16].into(),
|
||||
expected_movement_generation: 7,
|
||||
ttl_ms: 60_000,
|
||||
expected_session_id: "session-a".to_string(),
|
||||
token: Vec::new().into(),
|
||||
};
|
||||
let baseline = canonical_scanner_publication_lease_request_body(&request).unwrap();
|
||||
for variant in [
|
||||
ScannerPublicationLeaseRequest {
|
||||
expected_movement_generation: 8,
|
||||
..request.clone()
|
||||
},
|
||||
ScannerPublicationLeaseRequest {
|
||||
expected_session_id: "session-b".to_string(),
|
||||
..request.clone()
|
||||
},
|
||||
ScannerPublicationLeaseRequest {
|
||||
ttl_ms: 30_000,
|
||||
..request.clone()
|
||||
},
|
||||
ScannerPublicationLeaseRequest {
|
||||
token: vec![3; 16].into(),
|
||||
..request
|
||||
},
|
||||
] {
|
||||
assert_ne!(baseline, canonical_scanner_publication_lease_request_body(&variant).unwrap());
|
||||
}
|
||||
|
||||
let release_a = crate::proto_gen::node_service::ScannerPublicationLeaseReleaseRequest {
|
||||
challenge: vec![2; 16].into(),
|
||||
token: vec![3; 16].into(),
|
||||
owner_id: "owner-a".to_string(),
|
||||
session_id: "session-a".to_string(),
|
||||
};
|
||||
let release_b = crate::proto_gen::node_service::ScannerPublicationLeaseReleaseRequest {
|
||||
owner_id: "owner-b".to_string(),
|
||||
..release_a.clone()
|
||||
};
|
||||
assert_ne!(
|
||||
canonical_scanner_publication_lease_release_request_body(&release_a).unwrap(),
|
||||
canonical_scanner_publication_lease_release_request_body(&release_b).unwrap()
|
||||
);
|
||||
|
||||
let response = ScannerPublicationLeaseResponse {
|
||||
success: true,
|
||||
token: vec![4; 16].into(),
|
||||
movement_generation: 7,
|
||||
lease_ttl_ms: 60_000,
|
||||
error: None,
|
||||
response_proof: Vec::new().into(),
|
||||
owner_id: "owner-a".to_string(),
|
||||
session_id: "session-a".to_string(),
|
||||
};
|
||||
let response_changed = ScannerPublicationLeaseResponse {
|
||||
owner_id: "owner-b".to_string(),
|
||||
..response.clone()
|
||||
};
|
||||
assert_ne!(
|
||||
canonical_scanner_publication_lease_response_body(&[1; 16], &response).unwrap(),
|
||||
canonical_scanner_publication_lease_response_body(&[1; 16], &response_changed).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -111,6 +111,9 @@ message DeleteRequest {
|
||||
string volume = 2;
|
||||
string path = 3;
|
||||
string options = 4;
|
||||
// Optional scanner publication lease token. When present, the target binds
|
||||
// the complete delete operation to its movement read admission.
|
||||
bytes scanner_publication_lease_token = 5;
|
||||
}
|
||||
|
||||
message DeleteResponse {
|
||||
@@ -281,6 +284,10 @@ message RenameDataRequest {
|
||||
string dst_volume = 5;
|
||||
string dst_path = 6;
|
||||
bytes file_info_bin = 7;
|
||||
// Optional target-side scanner publication lease. Empty preserves the
|
||||
// legacy rename request body; a non-empty token is checked at the target's
|
||||
// rename linearization point.
|
||||
bytes scanner_publication_lease_token = 8;
|
||||
}
|
||||
|
||||
message RenameDataResponse {
|
||||
@@ -844,6 +851,52 @@ message ScannerActivityResponse {
|
||||
bytes response_proof = 7;
|
||||
uint64 dirty_usage_generation = 8;
|
||||
bool dirty_usage_pending = 9;
|
||||
// v7 fields. They are optional so v6 peers can continue to decode the
|
||||
// response shape while newer readers fail closed when they are absent.
|
||||
optional uint64 movement_generation = 10;
|
||||
optional bool publication_blocked = 11;
|
||||
}
|
||||
|
||||
// A short-lived storage-owned read admission used only around a final
|
||||
// scanner metadata publication. It is intentionally separate from the
|
||||
// ScannerActivity observation wire so v6/v7 rolling compatibility remains
|
||||
// unchanged.
|
||||
message ScannerPublicationLeaseRequest {
|
||||
bytes challenge = 1;
|
||||
uint64 expected_movement_generation = 2;
|
||||
uint64 ttl_ms = 3;
|
||||
// The activity instance is a process session nonce. It is intentionally
|
||||
// separate from the storage-owned deployment identity returned by the
|
||||
// lease response so a restart cannot reuse an old session token.
|
||||
string expected_session_id = 4;
|
||||
// A non-empty token turns the acquire RPC into an in-place validation of an
|
||||
// existing lease. Keeping this on the existing RPC lets old peers reject
|
||||
// the proof without changing the v7 activity wire shape.
|
||||
bytes token = 5;
|
||||
}
|
||||
|
||||
message ScannerPublicationLeaseResponse {
|
||||
bool success = 1;
|
||||
bytes token = 2;
|
||||
uint64 movement_generation = 3;
|
||||
uint64 lease_ttl_ms = 4;
|
||||
optional Error error = 5;
|
||||
bytes response_proof = 6;
|
||||
string owner_id = 7;
|
||||
string session_id = 8;
|
||||
}
|
||||
|
||||
message ScannerPublicationLeaseReleaseRequest {
|
||||
bytes challenge = 1;
|
||||
bytes token = 2;
|
||||
string owner_id = 3;
|
||||
string session_id = 4;
|
||||
}
|
||||
|
||||
message ScannerPublicationLeaseReleaseResponse {
|
||||
bool success = 1;
|
||||
optional Error error = 2;
|
||||
bytes response_proof = 3;
|
||||
}
|
||||
|
||||
message BackgroundHealStatusRequest {
|
||||
@@ -1096,6 +1149,8 @@ service NodeService {
|
||||
// rpc CommitBinary() returns () {};
|
||||
rpc SignalService(SignalServiceRequest) returns (SignalServiceResponse) {}; // auth-policy: body-bound
|
||||
rpc ScannerActivity(ScannerActivityRequest) returns (ScannerActivityResponse) {}; // auth-policy: body-bound
|
||||
rpc AcquireScannerPublicationLease(ScannerPublicationLeaseRequest) returns (ScannerPublicationLeaseResponse) {}; // auth-policy: body-bound
|
||||
rpc ReleaseScannerPublicationLease(ScannerPublicationLeaseReleaseRequest) returns (ScannerPublicationLeaseReleaseResponse) {}; // auth-policy: body-bound
|
||||
rpc BackgroundHealStatus(BackgroundHealStatusRequest) returns (BackgroundHealStatusResponse) {}; // auth-policy: read-only
|
||||
rpc ReplacementRecoveryStatus(ReplacementRecoveryStatusRequest) returns (ReplacementRecoveryStatusResponse) {}; // auth-policy: read-only
|
||||
rpc GetMetacacheListing(GetMetacacheListingRequest) returns (GetMetacacheListingResponse) {}; // auth-policy: unimplemented
|
||||
|
||||
Reference in New Issue
Block a user