use connectrpc::client::{ClientConfig, ClientTransport};
use polyc_proto::proto::polychrome::state::v1 as pb;
use polyc_state::{
error::StateError,
id::{CommandId, PartitionId},
journal::{
self, CommitJournalBatch, DestroyPartition, ExcisePartitionRecords, GetJournalHead,
GetJournalProof, GetJournalRoot, GetPartitionLastModified, JournalAttestation,
JournalProof, JournalRange, ListJournalPartitions, PartitionLastModified, PartitionPage,
QuarantinedRecord, ReadJournalRange, RepairOutcome, RepairPartition,
},
receipt::Receipt,
revision::JournalHead,
};
use crate::{
MAX_JOURNAL_WIRE_MESSAGE_BYTES,
error::{TransportFallback, from_connect_error},
journal::verify::{PartitionVerification, VerifyPartitionReplay},
trace::{bounded_traced_options, bounded_traced_options_from_headers},
wire::{DeclaredCall, Kernel},
};
pub struct JournalClient<T> {
inner: pb::StateJournalServiceClient<T>,
}
impl<T> JournalClient<T>
where
T: ClientTransport,
<T::ResponseBody as connectrpc::http_body::Body>::Error: std::fmt::Display,
{
pub fn new(transport: T, config: ClientConfig) -> Self {
Self {
inner: pb::StateJournalServiceClient::new(
transport,
config.with_default_max_message_size(MAX_JOURNAL_WIRE_MESSAGE_BYTES),
),
}
}
fn fallback(attempted_bytes: usize) -> TransportFallback {
TransportFallback::new(
journal::family(),
MAX_JOURNAL_WIRE_MESSAGE_BYTES as u64,
attempted_bytes as u64,
)
}
pub async fn commit(
&self,
declared: &DeclaredCall,
batch: &CommitJournalBatch,
) -> Result<Receipt, StateError> {
let (request, attempted) = Self::commit_request(declared, batch);
let reply = self
.inner
.commit_journal_batch_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
receipt_of(reply.receipt)
}
pub async fn commit_with_trace_headers(
&self,
declared: &DeclaredCall,
batch: &CommitJournalBatch,
headers: http::HeaderMap,
) -> Result<Receipt, StateError> {
let (request, attempted) = Self::commit_request(declared, batch);
let reply = self
.inner
.commit_journal_batch_with_options(
request,
bounded_traced_options_from_headers(declared, headers),
)
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
receipt_of(reply.receipt)
}
fn commit_request(
declared: &DeclaredCall,
batch: &CommitJournalBatch,
) -> (pb::CommitJournalBatchRequest, usize) {
let request = pb::CommitJournalBatchRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
batch: buffa::MessageField::some(Kernel(batch).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
(request, attempted)
}
pub async fn receipt(
&self,
declared: &DeclaredCall,
partition: &PartitionId,
command_id: &CommandId,
) -> Result<Option<Receipt>, StateError> {
let request = pb::GetJournalReceiptRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
command_id: command_id.as_str().to_owned(),
partition: partition.as_str().to_owned(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.get_journal_receipt_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
reply
.receipt
.into_option()
.map(|receipt| Kernel::<Receipt>::try_from(receipt).map(Kernel::into_inner))
.transpose()
}
pub async fn read_range(
&self,
declared: &DeclaredCall,
range: &ReadJournalRange,
) -> Result<JournalRange, StateError> {
let request = pb::ReadJournalRangeRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
range: buffa::MessageField::some(Kernel(range).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.read_journal_range_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(Kernel::<JournalRange>::try_from(
reply
.range
.into_option()
.ok_or_else(|| missing("range", "a successful read carries its range"))?,
)?
.into_inner())
}
pub async fn head(
&self,
declared: &DeclaredCall,
request: &GetJournalHead,
) -> Result<JournalHead, StateError> {
let request = pb::GetJournalHeadRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
partition: String::from(Kernel(request)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.get_journal_head_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(Kernel::<JournalHead>::try_from(
reply
.head
.into_option()
.ok_or_else(|| missing("head", "a successful read carries its head"))?,
)?
.into_inner())
}
pub async fn list_partitions(
&self,
declared: &DeclaredCall,
request: &ListJournalPartitions,
) -> Result<PartitionPage, StateError> {
let request = pb::ListJournalPartitionsRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
start_after: request
.start_after()
.map(|partition| partition.as_str().to_owned()),
limit: request.limit(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.list_journal_partitions_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(Kernel::<PartitionPage>::try_from(
reply
.page
.into_option()
.ok_or_else(|| missing("page", "a successful listing carries its page"))?,
)?
.into_inner())
}
pub async fn verify_replay(
&self,
declared: &DeclaredCall,
request: &VerifyPartitionReplay,
) -> Result<PartitionVerification, StateError> {
let request = pb::VerifyPartitionReplayRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
partition: request.partition().as_str().to_owned(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.verify_partition_replay_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(Kernel::<PartitionVerification>::try_from(reply)?.into_inner())
}
pub async fn last_modified(
&self,
declared: &DeclaredCall,
request: &GetPartitionLastModified,
) -> Result<PartitionLastModified, StateError> {
let request = pb::GetPartitionLastModifiedRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
partition: request.partition().as_str().to_owned(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.get_partition_last_modified_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(PartitionLastModified::new(reply.at_ms))
}
pub async fn root(
&self,
declared: &DeclaredCall,
request: &GetJournalRoot,
) -> Result<JournalAttestation, StateError> {
let request = pb::GetJournalRootRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
partition: String::from(Kernel(request)),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.get_journal_root_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(Kernel::<JournalAttestation>::try_from(
reply.attestation.into_option().ok_or_else(|| {
missing("attestation", "a successful read carries its attestation")
})?,
)?
.into_inner())
}
pub async fn proof(
&self,
declared: &DeclaredCall,
request: &GetJournalProof,
) -> Result<JournalProof, StateError> {
let request = pb::GetJournalProofRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
partition: request.partition().as_str().to_owned(),
position: request.position().get(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.get_journal_proof_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
Ok(Kernel::<JournalProof>::try_from(
reply
.proof
.into_option()
.ok_or_else(|| missing("proof", "a successful read carries its proof"))?,
)?
.into_inner())
}
pub async fn destroy(
&self,
declared: &DeclaredCall,
command: &DestroyPartition,
) -> Result<Receipt, StateError> {
let request = pb::DestroyJournalPartitionRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
command: buffa::MessageField::some(Kernel(command).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.destroy_journal_partition_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
receipt_of(reply.receipt)
}
pub async fn stage_destroy(
&self,
declared: &DeclaredCall,
command: &DestroyPartition,
) -> Result<(), StateError> {
let request = pb::StageJournalDestructionRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
command: buffa::MessageField::some(Kernel(command).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
self.inner
.stage_journal_destruction_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?;
Ok(())
}
pub async fn complete_destroy(
&self,
declared: &DeclaredCall,
command: &DestroyPartition,
) -> Result<Receipt, StateError> {
let request = pb::CompleteJournalDestructionRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
command: buffa::MessageField::some(Kernel(command).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.complete_journal_destruction_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
receipt_of(reply.receipt)
}
pub async fn excise(
&self,
declared: &DeclaredCall,
command: &ExcisePartitionRecords,
) -> Result<Receipt, StateError> {
let request = pb::ExciseJournalRecordsRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
command: buffa::MessageField::some(Kernel(command).into()),
decisions: command
.decisions()
.iter()
.map(|decision| pb::RecordDecision::from(Kernel(decision)))
.collect(),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.excise_journal_records_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
receipt_of(reply.receipt)
}
pub async fn repair(
&self,
declared: &DeclaredCall,
command: &RepairPartition,
) -> Result<RepairOutcome, StateError> {
let request = pb::RepairJournalPartitionRequest {
context: buffa::MessageField::some(Kernel(declared).into()),
command: buffa::MessageField::some(Kernel(command).into()),
__buffa_unknown_fields: buffa::UnknownFields::default(),
};
let attempted = buffa::Message::encoded_len(&request) as usize;
let reply = self
.inner
.repair_journal_partition_with_options(request, bounded_traced_options(declared))
.await
.map_err(|e| from_connect_error(&e, &Self::fallback(attempted)))?
.into_owned();
let quarantined = reply
.quarantined
.into_iter()
.map(|record| Kernel::<QuarantinedRecord>::from(record).into_inner())
.collect();
Ok(RepairOutcome::new(receipt_of(reply.receipt)?, quarantined))
}
}
fn receipt_of(receipt: impl Into<Option<pb::Receipt>>) -> Result<Receipt, StateError> {
Ok(Kernel::<Receipt>::try_from(
receipt
.into()
.ok_or_else(|| missing("receipt", "a committed command carries its receipt"))?,
)?
.into_inner())
}
fn missing(field: &str, reason: &str) -> StateError {
StateError::Malformed {
field: field.to_owned(),
reason: reason.to_owned(),
}
}