use crate::operation::codec::encode_atomic;
use ozzy_proto::append::{
AppendDisposition, AppendOutcome, OUTCOME_RUN_BYTES, OutcomeRun, OutcomeRuns,
};
use ozzy_proto::{Offset, ProducerSequence};
use super::{
Encoder, OperationCodecError, OperationLimits, OperationOutput, Reader, RetryFingerprint,
enforce_limit, validate_limits,
};
#[derive(Debug, Clone, Copy)]
enum Runs<'a> {
Classified(&'a [AppendOutcome]),
Encoded(&'a [[u8; OUTCOME_RUN_BYTES]]),
}
#[derive(Debug, Clone, Copy)]
enum Proofs<'a> {
Classified(&'a [RetryFingerprint]),
Encoded(&'a [[u8; 32]]),
}
#[derive(Debug, Clone, Copy)]
pub struct AppendResults<'a> {
logical_time: u64,
attempts: u32,
stored: u32,
run_count: u32,
runs: Runs<'a>,
proofs: Proofs<'a>,
}
impl PartialEq for AppendResults<'_> {
fn eq(&self, other: &Self) -> bool {
self.logical_time == other.logical_time
&& self.attempts == other.attempts
&& self.runs().eq(other.runs())
&& self
.duplicate_fingerprints()
.eq(other.duplicate_fingerprints())
}
}
impl Eq for AppendResults<'_> {}
impl<'a> AppendResults<'a> {
pub fn classified(
logical_time: u64,
outcomes: &'a [AppendOutcome],
duplicate_fingerprints: &'a [RetryFingerprint],
limits: OperationLimits,
) -> Result<Self, OperationCodecError> {
validate_limits(limits)?;
enforce_limit("append attempt count", outcomes.len(), limits.max_records)?;
if outcomes.is_empty() {
return Err(OperationCodecError::InvalidAppendResults);
}
let mut stored = 0u32;
let mut duplicates = 0usize;
let mut run_count = 0u32;
for run in OutcomeRuns::new(outcomes) {
run.encode()
.map_err(|_| OperationCodecError::InvalidAppendResults)?;
run_count = run_count
.checked_add(1)
.ok_or(OperationCodecError::LengthOverflow)?;
match run.disposition {
AppendDisposition::Stored => {
stored = stored
.checked_add(run.records)
.ok_or(OperationCodecError::LengthOverflow)?;
}
AppendDisposition::Duplicate => {
duplicates = duplicates
.checked_add(run.records as usize)
.ok_or(OperationCodecError::LengthOverflow)?;
}
_ => return Err(OperationCodecError::InvalidAppendResults),
}
}
if duplicates != duplicate_fingerprints.len() {
return Err(OperationCodecError::InvalidAppendResults);
}
let results = Self {
logical_time,
attempts: u32::try_from(outcomes.len())
.map_err(|_| OperationCodecError::LengthOverflow)?,
stored,
run_count,
runs: Runs::Classified(outcomes),
proofs: Proofs::Classified(duplicate_fingerprints),
};
enforce_limit(
"append result bytes",
results.encoded_bytes()?,
limits.max_body_bytes,
)?;
Ok(results)
}
pub const fn logical_time(self) -> u64 {
self.logical_time
}
pub const fn attempts(self) -> u32 {
self.attempts
}
pub const fn stored(self) -> u32 {
self.stored
}
pub fn validate_positions(
self,
first_sequence: ProducerSequence,
first_offset: Offset,
stored_records: usize,
) -> Result<(ProducerSequence, Offset), OperationCodecError> {
if stored_records != self.stored as usize {
return Err(OperationCodecError::InvalidAppendResults);
}
let sequence = first_sequence
.get()
.checked_add(u64::from(self.attempts))
.ok_or(OperationCodecError::AppendPositionOverflow)?;
let mut next = first_offset.get();
for run in self.runs() {
let end = run
.first_offset
.get()
.checked_add(u64::from(run.records))
.ok_or(OperationCodecError::AppendPositionOverflow)?;
match run.disposition {
AppendDisposition::Stored if run.first_offset.get() == next => {
next = end;
}
AppendDisposition::Duplicate if end <= next => {}
_ => return Err(OperationCodecError::InvalidAppendResults),
}
}
Ok((ProducerSequence::new(sequence), Offset::new(next)))
}
pub fn attempts_with_fingerprints(
self,
) -> impl Iterator<Item = (AppendOutcome, Option<RetryFingerprint>)> + Clone + 'a {
let mut proofs = self.duplicate_fingerprints();
self.runs()
.flat_map(OutcomeRun::outcomes)
.map(move |outcome| {
let proof = if outcome.disposition == AppendDisposition::Duplicate {
proofs.next()
} else {
None
};
(outcome, proof)
})
}
#[expect(
clippy::expect_used,
reason = "read_results validated every encoded run"
)]
pub fn runs(self) -> impl Iterator<Item = OutcomeRun> + Clone + 'a {
let (classified, encoded) = match self.runs {
Runs::Classified(rows) => (Some(OutcomeRuns::new(rows)), None),
Runs::Encoded(rows) => (None, Some(rows.iter())),
};
classified.into_iter().flatten().chain(
encoded
.into_iter()
.flatten()
.map(|bytes| OutcomeRun::decode(bytes).expect("validated append result run")),
)
}
pub fn duplicate_fingerprints(self) -> impl Iterator<Item = RetryFingerprint> + Clone + 'a {
let (classified, encoded) = match self.proofs {
Proofs::Classified(rows) => (Some(rows.iter().copied()), None),
Proofs::Encoded(rows) => (
None,
Some(
rows.iter()
.map(|bytes| RetryFingerprint::from_bytes(*bytes)),
),
),
};
classified
.into_iter()
.flatten()
.chain(encoded.into_iter().flatten())
}
pub(super) fn encoded_bytes(self) -> Result<usize, OperationCodecError> {
(self.run_count as usize)
.checked_mul(OUTCOME_RUN_BYTES)
.and_then(|bytes| {
((self.attempts - self.stored) as usize)
.checked_mul(32)?
.checked_add(bytes)
})
.and_then(|bytes| bytes.checked_add(16))
.ok_or(OperationCodecError::LengthOverflow)
}
}
pub fn append_results(
output: &mut impl OperationOutput,
results: AppendResults<'_>,
limits: OperationLimits,
) -> Result<(), OperationCodecError> {
encode_atomic(output, limits, |encoder| {
write_results(encoder, results, limits)
})
.map(|_| ())
}
pub(super) fn write_results(
encoder: &mut Encoder<'_, impl OperationOutput>,
results: AppendResults<'_>,
limits: OperationLimits,
) -> Result<(), OperationCodecError> {
enforce_limit(
"append attempt count",
results.attempts as usize,
limits.max_records,
)?;
encoder.check_length(results.encoded_bytes()?)?;
encoder.u64(results.logical_time)?;
encoder.u32(results.attempts)?;
encoder.u32(results.run_count)?;
for run in results.runs() {
encoder.bytes(
&run.encode()
.map_err(|_| OperationCodecError::InvalidAppendResults)?,
)?;
}
for proof in results.duplicate_fingerprints() {
encoder.bytes(proof.as_bytes())?;
}
Ok(())
}
pub fn decode_append_results(
input: &[u8],
limits: OperationLimits,
) -> Result<AppendResults<'_>, OperationCodecError> {
validate_limits(limits)?;
enforce_limit("append result bytes", input.len(), limits.max_body_bytes)?;
let mut decoder = Reader::new(input);
let results = read_results(&mut decoder, limits)?;
decoder.finish()?;
Ok(results)
}
pub(super) fn read_results<'a>(
decoder: &mut Reader<'a>,
limits: OperationLimits,
) -> Result<AppendResults<'a>, OperationCodecError> {
let logical_time = decoder.u64()?;
let attempts = decoder.u32()?;
let run_count = decoder.u32()?;
enforce_limit(
"append attempt count",
attempts as usize,
limits.max_records,
)?;
if attempts == 0 || run_count == 0 || run_count > attempts {
return Err(OperationCodecError::InvalidAppendResults);
}
let bytes = (run_count as usize)
.checked_mul(OUTCOME_RUN_BYTES)
.ok_or(OperationCodecError::LengthOverflow)?;
let runs = decoder.take(bytes)?.as_chunks::<OUTCOME_RUN_BYTES>().0;
let (mut counted, mut stored) = (0u32, 0u32);
for bytes in runs {
let run =
OutcomeRun::decode(bytes).map_err(|_| OperationCodecError::InvalidAppendResults)?;
counted = counted
.checked_add(run.records)
.ok_or(OperationCodecError::LengthOverflow)?;
if run.disposition == AppendDisposition::Stored {
stored = stored
.checked_add(run.records)
.ok_or(OperationCodecError::LengthOverflow)?;
}
}
if counted != attempts {
return Err(OperationCodecError::InvalidAppendResults);
}
let bytes = ((attempts - stored) as usize)
.checked_mul(32)
.ok_or(OperationCodecError::LengthOverflow)?;
let proofs = decoder.take(bytes)?.as_chunks::<32>().0;
Ok(AppendResults {
logical_time,
attempts,
stored,
run_count,
runs: Runs::Encoded(runs),
proofs: Proofs::Encoded(proofs),
})
}
#[cfg(test)]
mod tests;