mod apply;
mod diff;
mod types;
pub use apply::{ApplyOptions, ApplyResult};
pub use diff::ConfigDiff;
pub use types::*;
use super::{connection::Connection, error::Result, protocol::Route};
impl NetworkConfig {
pub async fn diff(&self, conn: &Connection<Route>) -> Result<ConfigDiff> {
diff::compute_diff(self, conn).await
}
pub async fn apply(&self, conn: &Connection<Route>) -> Result<ApplyResult> {
self.apply_with_options(conn, ApplyOptions::default()).await
}
pub async fn apply_with_options(
&self,
conn: &Connection<Route>,
options: ApplyOptions,
) -> Result<ApplyResult> {
apply::apply_config(self, conn, options).await
}
pub async fn apply_reconcile(
&self,
conn: &Connection<Route>,
opts: crate::netlink::nftables::config::ReconcileOptions,
) -> Result<crate::netlink::nftables::config::ReconcileReport> {
let mut attempt: usize = 0;
let mut cumulative_changes: usize = 0;
loop {
let diff = self.diff(conn).await?;
if diff.is_empty() {
return Ok(crate::netlink::nftables::config::ReconcileReport {
attempts: attempt + 1,
change_count: cumulative_changes,
});
}
match apply::apply_diff(&diff, conn, apply::ApplyOptions::default()).await {
Ok(result) => {
cumulative_changes += result.changes_made;
return Ok(crate::netlink::nftables::config::ReconcileReport {
attempts: attempt + 1,
change_count: cumulative_changes,
});
}
Err(e) if (e.is_busy() || e.is_try_again()) && attempt < opts.max_retries => {
let backoff = opts.backoff.saturating_mul(1u32 << attempt.min(10));
tokio::time::sleep(backoff).await;
attempt += 1;
continue;
}
Err(e) => return Err(e),
}
}
}
}
#[cfg(test)]
mod apply_reconcile_tests {
use crate::Error;
use std::time::Duration;
#[test]
fn classify_io_ebusy_as_retryable() {
let io_ebusy = Error::Io(std::io::Error::from_raw_os_error(libc::EBUSY));
assert!(io_ebusy.is_busy(), "Io(EBUSY) must trigger apply_reconcile retry");
assert!(!io_ebusy.is_no_buffer_space(), "wrong predicate must NOT match");
}
#[test]
fn classify_io_eagain_as_retryable() {
let io_eagain = Error::Io(std::io::Error::from_raw_os_error(libc::EAGAIN));
assert!(io_eagain.is_try_again(), "Io(EAGAIN) must trigger apply_reconcile retry");
}
#[test]
fn classify_kernel_einval_as_terminal() {
let einval = Error::from_errno_ext_ack(libc::EINVAL, None, None);
assert!(!einval.is_busy());
assert!(!einval.is_try_again());
}
#[test]
fn reconcile_options_default_caps_at_3_retries() {
use crate::netlink::nftables::config::ReconcileOptions;
let opts = ReconcileOptions::default();
assert_eq!(opts.max_retries, 3);
assert_eq!(opts.backoff, Duration::from_millis(100));
}
}