use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
pub const META_FORMAT: &str = "chorus.format";
pub const FORMAT_VERSION: &str = "1";
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ListedObject {
pub zone: usize,
pub name: String,
pub generation: i64,
pub finalized: bool,
pub metadata: HashMap<String, String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ReplicaSnapshot {
pub zone: usize,
pub generation: i64,
pub metageneration: i64,
pub persisted_size: i64,
pub finalized: bool,
pub crc32c: Option<u32>,
pub metadata: HashMap<String, String>,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AppendToken {
pub zone: usize,
pub generation: Option<i64>,
pub metageneration: Option<i64>,
pub persisted_size: i64,
pub write_handle: Option<Bytes>,
}
#[derive(Clone, Debug)]
pub(crate) struct PackedAppendMessage {
pub(crate) relative_offset: i64,
pub(crate) content: Bytes,
pub(crate) crc32c: u32,
}
#[derive(Clone, Debug)]
pub struct PackedAppend {
chunks: Box<[Bytes]>,
messages: Box<[PackedAppendMessage]>,
len: usize,
}
impl PackedAppend {
pub(crate) fn new(chunks: Vec<Bytes>, messages: Vec<PackedAppendMessage>, len: usize) -> Self {
Self {
chunks: chunks.into_boxed_slice(),
messages: messages.into_boxed_slice(),
len,
}
}
pub(crate) fn chunks(&self) -> &[Bytes] {
&self.chunks
}
pub(crate) fn messages(&self) -> &[PackedAppendMessage] {
&self.messages
}
pub(crate) fn len(&self) -> usize {
self.len
}
pub(crate) fn is_empty(&self) -> bool {
self.chunks.is_empty()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum TransportCode {
NotFound,
AlreadyExists,
InvalidArgument,
FailedPrecondition,
Aborted,
OutOfRange,
ResourceExhausted,
Unimplemented,
DataLoss,
Ambiguous,
Unauthenticated,
PermissionDenied,
Unavailable,
DeadlineExceeded,
Internal,
}
impl TransportCode {
pub fn transient(self) -> bool {
matches!(
self,
Self::ResourceExhausted | Self::Unavailable | Self::DeadlineExceeded | Self::Internal
)
}
pub fn fences_writer(self) -> bool {
matches!(self, Self::FailedPrecondition | Self::Aborted)
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("zone {zone}: {code:?}: {message}")]
pub struct TransportError {
pub zone: usize,
pub code: TransportCode,
pub message: String,
}
#[derive(Clone, Debug)]
pub struct LaneDurableChange {
pub persisted_size: i64,
pub error: Option<TransportError>,
}
#[async_trait]
pub trait ReplicaFactory: Send + Sync {
fn bucket_name(&self) -> &str;
fn replica(&self, object: &str) -> Arc<dyn Replica>;
async fn list(&self, prefix: &str) -> Result<Vec<ListedObject>, TransportError>;
}
#[async_trait]
pub trait Replica: Send + Sync {
async fn snapshot(&self) -> Result<ReplicaSnapshot, TransportError>;
async fn stat(&self) -> Result<ReplicaSnapshot, TransportError>;
async fn create_appendable(
&self,
metadata: HashMap<String, String>,
) -> Result<ReplicaSnapshot, TransportError>;
async fn create_append_session(
&self,
metadata: HashMap<String, String>,
) -> Result<AppendToken, TransportError>;
async fn create_register(
&self,
metadata: HashMap<String, String>,
) -> Result<ReplicaSnapshot, TransportError>;
async fn update_register(
&self,
metageneration: i64,
metadata: HashMap<String, String>,
) -> Result<ReplicaSnapshot, TransportError>;
async fn resume_tail(&self, token: &mut AppendToken) -> Result<i64, TransportError>;
async fn takeover(&self, observed: &ReplicaSnapshot) -> Result<AppendToken, TransportError>;
async fn takeover_current(&self) -> Result<AppendToken, TransportError> {
let observed = self.stat().await?;
match self.takeover(&observed).await {
Err(error) if error.code == TransportCode::NotFound => Err(TransportError {
zone: error.zone,
code: TransportCode::Ambiguous,
message: format!(
"exact-generation takeover returned NOT_FOUND after current object lookup: {}",
error.message
),
}),
result => result,
}
}
async fn replace_appendable(
&self,
observed: &ReplicaSnapshot,
data: Bytes,
metadata: HashMap<String, String>,
) -> Result<AppendToken, TransportError>;
async fn append(
&self,
token: &AppendToken,
write_offset: i64,
data: Vec<u8>,
) -> Result<i64, TransportError>;
async fn lane_send(&self, write_offset: i64, chunks: &[Bytes]) -> Result<(), TransportError>;
async fn lane_send_packed(
&self,
write_offset: i64,
packed: &PackedAppend,
) -> Result<(), TransportError> {
self.lane_send(write_offset, packed.chunks()).await
}
async fn lane_durable_change(&self, seen: i64) -> Result<LaneDurableChange, TransportError>;
async fn delete(&self, generation: i64) -> Result<(), TransportError>;
async fn finalize(
&self,
token: &mut AppendToken,
write_offset: i64,
) -> Result<ReplicaSnapshot, TransportError>;
async fn shutdown(&self) {}
}
#[cfg(test)]
mod tests {
use super::TransportCode;
#[test]
fn failed_precondition_is_a_terminal_writer_fence() {
assert!(!TransportCode::FailedPrecondition.transient());
assert!(TransportCode::FailedPrecondition.fences_writer());
}
#[test]
fn terminal_request_rejections_are_not_retried() {
assert!(!TransportCode::InvalidArgument.transient());
assert!(!TransportCode::Unimplemented.transient());
}
#[test]
fn resource_exhaustion_is_retried() {
assert!(TransportCode::ResourceExhausted.transient());
}
}