use std::fmt;
use serde::de::{self, Deserializer, Visitor};
use serde::{Deserialize, Serialize, Serializer};
use super::effect::{Digest, wire_opaque_ref};
use super::scalar::{SCALAR_ERROR_MARKER, WireScalarError, WireU64};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KernelFaultCode {
MalformedEnvelope,
OperationMismatch,
ClockRegression,
InvalidLifecycle,
InvalidConfig,
InvalidAuthority,
ResourceLimitExceeded,
DuplicateInputConflict,
UnexpectedEffectOutcome,
TransactionConflict,
CheckpointIncompatible,
CheckpointCorrupted,
RecordCorrupted,
CheckpointRequired,
UnsupportedEffect,
}
impl KernelFaultCode {
pub const ALL: [Self; 15] = [
Self::MalformedEnvelope,
Self::OperationMismatch,
Self::ClockRegression,
Self::InvalidLifecycle,
Self::InvalidConfig,
Self::InvalidAuthority,
Self::ResourceLimitExceeded,
Self::DuplicateInputConflict,
Self::UnexpectedEffectOutcome,
Self::TransactionConflict,
Self::CheckpointIncompatible,
Self::CheckpointCorrupted,
Self::RecordCorrupted,
Self::CheckpointRequired,
Self::UnsupportedEffect,
];
pub fn as_str(self) -> &'static str {
match self {
Self::MalformedEnvelope => "malformed_envelope",
Self::OperationMismatch => "operation_mismatch",
Self::ClockRegression => "clock_regression",
Self::InvalidLifecycle => "invalid_lifecycle",
Self::InvalidConfig => "invalid_config",
Self::InvalidAuthority => "invalid_authority",
Self::ResourceLimitExceeded => "resource_limit_exceeded",
Self::DuplicateInputConflict => "duplicate_input_conflict",
Self::UnexpectedEffectOutcome => "unexpected_effect_outcome",
Self::TransactionConflict => "transaction_conflict",
Self::CheckpointIncompatible => "checkpoint_incompatible",
Self::CheckpointCorrupted => "checkpoint_corrupted",
Self::RecordCorrupted => "record_corrupted",
Self::CheckpointRequired => "checkpoint_required",
Self::UnsupportedEffect => "unsupported_effect",
}
}
pub fn is_retryable(self) -> bool {
matches!(self, Self::CheckpointRequired)
}
}
impl fmt::Display for KernelFaultCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KernelFault {
pub code: KernelFaultCode,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub message: String,
}
impl KernelFault {
pub fn new(code: KernelFaultCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
}
}
pub fn is_retryable(&self) -> bool {
self.code.is_retryable()
}
}
impl fmt::Display for KernelFault {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.message.is_empty() {
f.write_str(self.code.as_str())
} else {
write!(f, "{}: {}", self.code.as_str(), self.message)
}
}
}
impl std::error::Error for KernelFault {}
wire_opaque_ref!(
PrepareToken,
"prepare token"
);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum KernelPreparation<Record, Step> {
Prepared(PreparedTransition<Record, Step>),
Replayed(ReplayedTransition<Record, Step>),
Rejected(RejectedTransition),
}
impl<Record, Step> KernelPreparation<Record, Step> {
pub fn record(&self) -> Option<&Record> {
match self {
Self::Prepared(prepared) => Some(&prepared.record),
Self::Replayed(replayed) => replayed.record.as_ref(),
Self::Rejected(_) => None,
}
}
pub fn token(&self) -> Option<&PrepareToken> {
match self {
Self::Prepared(prepared) => Some(&prepared.token),
Self::Replayed(_) | Self::Rejected(_) => None,
}
}
pub fn step(&self) -> Option<&Step> {
match self {
Self::Prepared(prepared) => Some(&prepared.planned_step),
Self::Replayed(replayed) => replayed.committed_step.as_ref(),
Self::Rejected(_) => None,
}
}
pub fn step_seq(&self) -> Option<WireU64> {
match self {
Self::Replayed(replayed) => Some(replayed.step_seq),
Self::Prepared(_) | Self::Rejected(_) => None,
}
}
pub fn fault(&self) -> Option<&KernelFault> {
match self {
Self::Rejected(rejected) => Some(&rejected.fault),
Self::Prepared(_) | Self::Replayed(_) => None,
}
}
pub fn is_zero_mutation(&self) -> bool {
matches!(self, Self::Rejected(_))
}
pub fn is_retryable(&self) -> bool {
self.fault().is_some_and(KernelFault::is_retryable)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PreparedTransition<Record, Step> {
pub token: PrepareToken,
pub record: Record,
pub planned_step: Step,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReplayedTransition<Record, Step> {
#[serde(default = "Option::default")]
pub record: Option<Record>,
pub record_digest: Digest,
#[serde(default = "Option::default")]
pub committed_step: Option<Step>,
pub step_seq: WireU64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RejectedTransition {
pub fault: KernelFault,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use serde_json::json;
use super::super::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StubRecord {
step_seq: WireU64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct StubStep {
effects: u32,
}
type Preparation = KernelPreparation<StubRecord, StubStep>;
fn prepared() -> Preparation {
KernelPreparation::Prepared(PreparedTransition {
token: PrepareToken::new("prepare-1").unwrap(),
record: StubRecord {
step_seq: WireU64::new(4),
},
planned_step: StubStep { effects: 1 },
})
}
fn replayed() -> Preparation {
KernelPreparation::Replayed(ReplayedTransition {
record: Some(StubRecord {
step_seq: WireU64::new(2),
}),
record_digest: Digest::new("sha256:replayed").unwrap(),
committed_step: Some(StubStep { effects: 1 }),
step_seq: WireU64::new(2),
})
}
fn rejected(code: KernelFaultCode) -> Preparation {
KernelPreparation::Rejected(RejectedTransition {
fault: KernelFault::new(code, "rejected"),
})
}
#[test]
fn the_fault_taxonomy_is_the_fifteen_declared_codes() {
let labels: BTreeSet<&str> = KernelFaultCode::ALL.iter().map(|c| c.as_str()).collect();
assert_eq!(
labels,
BTreeSet::from([
"malformed_envelope",
"operation_mismatch",
"clock_regression",
"invalid_lifecycle",
"invalid_config",
"invalid_authority",
"resource_limit_exceeded",
"duplicate_input_conflict",
"unexpected_effect_outcome",
"transaction_conflict",
"checkpoint_incompatible",
"checkpoint_corrupted",
"record_corrupted",
"checkpoint_required",
"unsupported_effect",
])
);
assert_eq!(KernelFaultCode::ALL.len(), 15);
for code in KernelFaultCode::ALL {
let text = serde_json::to_string(&code).unwrap();
assert_eq!(text, format!("\"{}\"", code.as_str()));
let back: KernelFaultCode = serde_json::from_str(&text).unwrap();
assert_eq!(back, code);
}
}
#[test]
fn checkpoint_required_is_the_only_retryable_fault_code() {
for code in KernelFaultCode::ALL {
assert_eq!(
code.is_retryable(),
code == KernelFaultCode::CheckpointRequired,
"{} must{} be retryable",
code.as_str(),
if code == KernelFaultCode::CheckpointRequired {
""
} else {
" not"
}
);
}
assert!(KernelFault::new(KernelFaultCode::CheckpointRequired, "").is_retryable());
assert!(!KernelFault::new(KernelFaultCode::DuplicateInputConflict, "").is_retryable());
}
#[test]
fn unknown_fault_codes_are_rejected() {
for raw in ["\"snapshot_overflow\"", "\"ok\"", "3", "null"] {
assert!(
serde_json::from_str::<KernelFaultCode>(raw).is_err(),
"{raw} must not decode as a fault code"
);
}
}
#[test]
fn a_rejected_preparation_carries_no_record_no_token_and_no_step() {
for code in KernelFaultCode::ALL {
let preparation = rejected(code);
assert!(preparation.record().is_none(), "{}", code.as_str());
assert!(preparation.token().is_none(), "{}", code.as_str());
assert!(preparation.step().is_none(), "{}", code.as_str());
assert!(preparation.step_seq().is_none(), "{}", code.as_str());
assert_eq!(preparation.fault().map(|f| f.code), Some(code));
assert!(preparation.is_zero_mutation());
}
}
#[test]
fn a_successful_preparation_can_never_carry_a_fault() {
for preparation in [prepared(), replayed()] {
assert!(preparation.fault().is_none());
assert!(!preparation.is_zero_mutation());
let mut all = BTreeSet::new();
let value = serde_json::to_value(&preparation).unwrap();
if let serde_json::Value::Object(map) = &value {
for key in map.keys() {
all.insert(key.clone());
}
}
assert!(
!all.contains("fault") && !all.contains("faults"),
"a fault-bearing success step must not be constructible: {value}"
);
}
}
#[test]
fn preparation_has_exactly_three_shapes() {
let statuses: BTreeSet<String> = [
prepared(),
replayed(),
rejected(KernelFaultCode::InvalidLifecycle),
]
.iter()
.map(|preparation| {
serde_json::to_value(preparation).unwrap()["status"]
.as_str()
.unwrap()
.to_string()
})
.collect();
assert_eq!(
statuses,
BTreeSet::from([
"prepared".to_string(),
"replayed".to_string(),
"rejected".to_string(),
])
);
for shape in ["accepted", "deferred", "prepared_with_faults"] {
let raw = json!({ "status": shape });
assert!(
serde_json::from_value::<Preparation>(raw).is_err(),
"{shape} is not a preparation shape"
);
}
}
#[test]
fn replayed_points_at_the_existing_record_step_seq() {
let preparation = replayed();
assert_eq!(preparation.step_seq(), Some(WireU64::new(2)));
assert_eq!(
preparation.record().map(|record| record.step_seq),
Some(WireU64::new(2)),
"a replay must point at the record that already exists, not mint a new one"
);
assert!(
preparation.token().is_none(),
"a replay has nothing to commit, so it hands out no prepare token"
);
}
#[test]
fn preparation_round_trips_and_rejects_unknown_fields() {
for preparation in [
prepared(),
replayed(),
rejected(KernelFaultCode::CheckpointRequired),
] {
let value = serde_json::to_value(&preparation).unwrap();
let back: Preparation = serde_json::from_value(value).unwrap();
assert_eq!(back, preparation);
}
let extra = json!({
"status": "rejected",
"fault": { "code": "invalid_lifecycle", "message": "terminal already committed" },
"retry_after_ms": 500,
});
assert!(serde_json::from_value::<Preparation>(extra).is_err());
}
}