use super::resources::{ResourceModel, aggregate_version, content_version, ordered_resource_types};
use crate::proto::udb::core::control::entity::v1::ResourceType;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct Ledger {
accepted_version: String,
last_good_version: String,
last_response_nonce: String,
nack_error_detail: String,
}
impl Ledger {
fn send(&mut self, nonce: &str) {
self.last_response_nonce = nonce.to_string();
}
fn record_ack(&mut self, accepted_version: &str, nonce: &str) -> bool {
if self.last_response_nonce != nonce {
return false; }
self.accepted_version = accepted_version.to_string();
self.last_good_version = accepted_version.to_string();
self.nack_error_detail.clear();
true
}
fn record_nack(&mut self, nonce: &str, detail: &str) -> bool {
if self.last_response_nonce != nonce {
return false;
}
self.nack_error_detail = detail.to_string();
true
}
}
#[test]
fn ack_advances_accepted_version_and_clears_nack() {
let mut led = Ledger::default();
led.send("n1");
assert!(led.record_ack("v1", "n1"));
assert_eq!(led.accepted_version, "v1");
assert_eq!(led.last_good_version, "v1");
assert!(led.nack_error_detail.is_empty());
led.send("n2");
assert!(led.record_nack("n2", "boom"));
assert_eq!(led.nack_error_detail, "boom");
led.send("n3");
assert!(led.record_ack("v3", "n3"));
assert_eq!(led.accepted_version, "v3");
assert!(
led.nack_error_detail.is_empty(),
"a successful ACK must clear the prior NACK error"
);
}
#[test]
fn nack_keeps_last_good_and_does_not_advance_accepted() {
let mut led = Ledger::default();
led.send("n1");
assert!(led.record_ack("v1", "n1"));
assert_eq!(led.accepted_version, "v1");
assert_eq!(led.last_good_version, "v1");
led.send("n2");
assert!(led.record_nack("n2", r#"{"code":3,"message":"invalid policy"}"#));
assert_eq!(
led.accepted_version, "v1",
"NACK must not advance accepted_version"
);
assert_eq!(
led.last_good_version, "v1",
"NACK must preserve last_good_version"
);
assert!(
!led.nack_error_detail.is_empty(),
"NACK must record the structured error detail"
);
}
#[test]
fn stale_nonce_ack_and_nack_are_ignored() {
let mut led = Ledger::default();
led.send("n5");
assert!(!led.record_ack("vX", "n4"));
assert_eq!(led.accepted_version, "");
assert!(!led.record_nack("n4", "stale"));
assert!(led.nack_error_detail.is_empty());
assert!(led.record_ack("v5", "n5"));
assert_eq!(led.accepted_version, "v5");
}
#[test]
fn world_version_matches_pushed_then_acked_version() {
let resources = vec![
ResourceModel {
name: "pg-primary".into(),
resource_type: "RESOURCE_TYPE_BACKEND_TARGET_DEFINITION".into(),
content_hash: content_version(r#"{"host":"a"}"#),
payload_json: r#"{"host":"a"}"#.into(),
..Default::default()
},
ResourceModel {
name: "pg-replica".into(),
resource_type: "RESOURCE_TYPE_BACKEND_TARGET_DEFINITION".into(),
content_hash: content_version(r#"{"host":"b"}"#),
payload_json: r#"{"host":"b"}"#.into(),
..Default::default()
},
];
let world = aggregate_version(&resources);
let mut led = Ledger::default();
led.send("n1");
assert!(led.record_ack(&world, "n1"));
assert_eq!(led.accepted_version, world);
let mut edited = resources.clone();
edited[1].payload_json = r#"{"host":"c"}"#.into();
edited[1].content_hash = content_version(&edited[1].payload_json);
let world2 = aggregate_version(&edited);
assert_ne!(world, world2);
assert_ne!(
led.accepted_version, world2,
"node is out of sync until it ACKs the new world version"
);
}
#[test]
fn push_order_sends_definitions_before_referencing_policies() {
let order = ordered_resource_types();
let pos = |rt: ResourceType| order.iter().position(|t| *t == rt).unwrap();
assert!(pos(ResourceType::BackendTargetDefinition) < pos(ResourceType::RoutingPolicy));
assert!(pos(ResourceType::BackendTargetDefinition) < pos(ResourceType::RlsTenantPolicy));
}