use serde::de::Error as _;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(try_from = "CompletionWire")]
pub struct BackfillCompletion {
version: u32,
from: String,
to: String,
total: usize,
counts: BackfillCounts,
stop_reason: BackfillStop,
overwrite: bool,
}
impl BackfillCompletion {
pub fn new(
from: String,
to: String,
total: usize,
counts: BackfillCounts,
stop_reason: BackfillStop,
overwrite: bool,
) -> Result<Self, serde_json::Error> {
Self::try_from(CompletionWire {
version: COMPLETION_VERSION,
from,
to,
total,
counts,
stop_reason,
overwrite,
})
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
#[must_use]
pub fn from(&self) -> &str {
&self.from
}
#[must_use]
pub fn to(&self) -> &str {
&self.to
}
#[must_use]
pub fn counts(&self) -> &BackfillCounts {
&self.counts
}
#[must_use]
pub fn stop_reason(&self) -> BackfillStop {
self.stop_reason
}
#[must_use]
pub fn overwrite(&self) -> bool {
self.overwrite
}
}
impl TryFrom<CompletionWire> for BackfillCompletion {
type Error = serde_json::Error;
fn try_from(raw: CompletionWire) -> Result<Self, Self::Error> {
let counts = &raw.counts;
let total = [
counts.stored,
counts.skipped,
counts.empty,
counts.failed,
counts.deferred,
]
.into_iter()
.try_fold(0_usize, usize::checked_add);
let stop_matches = match raw.stop_reason {
BackfillStop::RangeExhausted => counts.deferred == 0,
BackfillStop::CommitLimit => counts.deferred > 0,
BackfillStop::BenchmarkFailure => counts.failed > 0,
};
if raw.version != COMPLETION_VERSION
|| !full_commit(&raw.from)
|| !full_commit(&raw.to)
|| raw.total == 0
|| total != Some(raw.total)
|| !stop_matches
{
return Err(Self::Error::custom("inconsistent backfill completion"));
}
Ok(Self {
version: raw.version,
from: raw.from,
to: raw.to,
total: raw.total,
counts: raw.counts,
stop_reason: raw.stop_reason,
overwrite: raw.overwrite,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BackfillCounts {
pub stored: usize,
pub skipped: usize,
pub empty: usize,
pub failed: usize,
pub deferred: usize,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BackfillStop {
RangeExhausted,
CommitLimit,
BenchmarkFailure,
}
const COMPLETION_VERSION: u32 = 1;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CompletionWire {
version: u32,
from: String,
to: String,
total: usize,
counts: BackfillCounts,
stop_reason: BackfillStop,
overwrite: bool,
}
fn full_commit(value: &str) -> bool {
const COMMIT_HEX_LENGTH: usize = 40;
value.len() == COMMIT_HEX_LENGTH && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::panic::{RefUnwindSafe, UnwindSafe};
use static_assertions::assert_impl_all;
use super::*;
assert_impl_all!(BackfillCompletion: Send, Sync, UnwindSafe, RefUnwindSafe);
#[test]
fn completion_roundtrips_each_stop_reason() {
for (stop, deferred) in [
(BackfillStop::RangeExhausted, 0),
(BackfillStop::CommitLimit, 1),
(BackfillStop::BenchmarkFailure, 1),
] {
let completion = BackfillCompletion::new(
"a".repeat(40),
"b".repeat(40),
4 + deferred,
BackfillCounts {
stored: 1,
skipped: 1,
empty: 1,
failed: 1,
deferred,
},
stop,
true,
)
.unwrap();
assert_eq!(
BackfillCompletion::from_slice(completion.to_json().unwrap().as_bytes()).unwrap(),
completion
);
assert_eq!(completion.from(), "a".repeat(40));
assert_eq!(completion.to(), "b".repeat(40));
assert_eq!(completion.counts().deferred, deferred);
assert_eq!(completion.stop_reason(), stop);
assert!(completion.overwrite());
}
}
#[test]
fn inconsistent_counts_and_stops_are_rejected() {
for (total, counts, stop) in [
(0, BackfillCounts::default(), BackfillStop::RangeExhausted),
(1, BackfillCounts::default(), BackfillStop::RangeExhausted),
(
1,
BackfillCounts {
stored: 1,
..BackfillCounts::default()
},
BackfillStop::CommitLimit,
),
(
1,
BackfillCounts {
deferred: 1,
..BackfillCounts::default()
},
BackfillStop::RangeExhausted,
),
(
1,
BackfillCounts {
stored: 1,
..BackfillCounts::default()
},
BackfillStop::BenchmarkFailure,
),
(
1,
BackfillCounts {
stored: usize::MAX,
skipped: 2,
..BackfillCounts::default()
},
BackfillStop::RangeExhausted,
),
] {
BackfillCompletion::new("a".repeat(40), "b".repeat(40), total, counts, stop, false)
.unwrap_err();
}
}
#[test]
fn malformed_identity_and_unknown_schema_are_rejected() {
let completion = BackfillCompletion::new(
"a".repeat(40),
"b".repeat(40),
1,
BackfillCounts {
skipped: 1,
..BackfillCounts::default()
},
BackfillStop::RangeExhausted,
false,
)
.unwrap();
let raw: serde_json::Value = serde_json::from_str(&completion.to_json().unwrap()).unwrap();
for (key, value) in [
("version", serde_json::json!(2)),
("from", serde_json::json!("HEAD")),
("to", serde_json::json!("g".repeat(40))),
("extra", serde_json::json!(true)),
] {
let mut raw = raw.clone();
raw.as_object_mut().unwrap().insert(key.to_owned(), value);
BackfillCompletion::from_slice(raw.to_string().as_bytes()).unwrap_err();
}
}
}