use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("durability reload required after `{operation}`: {reason}")]
pub(crate) struct DurabilityReloadRequired {
operation: String,
reason: String,
}
impl DurabilityReloadRequired {
fn new(operation: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
operation: operation.into(),
reason: reason.into(),
}
}
pub(crate) fn operation(&self) -> &str {
&self.operation
}
pub(crate) fn reason(&self) -> &str {
&self.reason
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DurabilityHealthState {
Ready,
ReloadRequired(DurabilityReloadRequired),
}
#[derive(Debug)]
struct DurabilityHealthRecord {
state: DurabilityHealthState,
cold_install_open: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct DurabilityHealthHandle {
inner: Arc<Mutex<DurabilityHealthRecord>>,
}
impl DurabilityHealthHandle {
pub(crate) fn require_ready(&self) -> Result<(), DurabilityReloadRequired> {
match &self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.state
{
DurabilityHealthState::Ready => Ok(()),
DurabilityHealthState::ReloadRequired(required) => Err(required.clone()),
}
}
pub(crate) fn mark_reload_required(
&self,
operation: &'static str,
reason: impl Into<String>,
) -> bool {
let mut record = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !record.cold_install_open
&& matches!(&record.state, DurabilityHealthState::ReloadRequired(_))
{
return false;
}
record.state =
DurabilityHealthState::ReloadRequired(DurabilityReloadRequired::new(operation, reason));
record.cold_install_open = false;
true
}
#[cfg(test)]
fn state(&self) -> DurabilityHealthState {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.state
.clone()
}
}
pub(super) struct DurabilityRehydrationAuthority {
handle: DurabilityHealthHandle,
}
impl DurabilityRehydrationAuthority {
pub(super) fn mark_ready(self) -> Result<(), DurabilityReloadRequired> {
let mut record = self
.handle
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !record.cold_install_open {
return match &record.state {
DurabilityHealthState::Ready => Ok(()),
DurabilityHealthState::ReloadRequired(required) => Err(required.clone()),
};
}
record.state = DurabilityHealthState::Ready;
record.cold_install_open = false;
Ok(())
}
}
pub(super) fn begin_registration_cold_install()
-> (DurabilityHealthHandle, DurabilityRehydrationAuthority) {
let handle = DurabilityHealthHandle {
inner: Arc::new(Mutex::new(DurabilityHealthRecord {
state: DurabilityHealthState::ReloadRequired(DurabilityReloadRequired::new(
"registration_cold_install",
"persistent runtime cold install has not published readiness",
)),
cold_install_open: true,
})),
};
let authority = DurabilityRehydrationAuthority {
handle: handle.clone(),
};
(handle, authority)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cold_install_is_the_only_ready_transition_and_degradation_is_monotonic() {
let (health, rehydration) = begin_registration_cold_install();
assert!(health.require_ready().is_err());
rehydration.mark_ready().unwrap();
assert_eq!(health.state(), DurabilityHealthState::Ready);
assert!(health.require_ready().is_ok());
assert!(health.mark_reload_required("atomic_apply", "outcome unknown"));
assert!(!health.mark_reload_required("later_retry", "must not replace first cause"));
let required = health.require_ready().unwrap_err();
assert_eq!(required.operation(), "atomic_apply");
assert_eq!(required.reason(), "outcome unknown");
}
#[test]
fn degradation_during_cold_install_revokes_rehydration_authority() {
let (health, rehydration) = begin_registration_cold_install();
assert!(health.mark_reload_required("recovery_cas", "durable image changed"));
let error = rehydration.mark_ready().unwrap_err();
assert_eq!(error.operation(), "recovery_cas");
assert_eq!(health.require_ready().unwrap_err(), error);
}
}