openrtc 0.2.1

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
//! Shared iroh connection install/replacement policy.
//!
//! Browser and native nodes both see the same symmetric-dial and post-restart
//! races. Keeping this decision table target-neutral prevents wasm/native
//! behavior from drifting as each wrapper evolves.

use crate::lifecycle_reason::{
    REASON_DUPLICATE_ACCEPT_SUPERSEDED, REASON_DUPLICATE_DIAL_SUPERSEDED,
    REASON_REPLACED_BY_NEW_INBOUND, REASON_REPLACED_BY_NEW_OUTBOUND,
    REASON_ZOMBIE_REPLACED_BY_FRESH_ACCEPT, REASON_ZOMBIE_REPLACED_BY_FRESH_DIAL,
};

/// Threshold beyond which a stored "alive" connection is treated as likely
/// zombie when a fresh connection with a different stable id arrives.
///
/// Symmetric-dial races settle within a few RTTs. A different stable id that
/// appears after this window is much more likely to be a peer restart whose old
/// QUIC connection has not reached idle timeout yet.
pub(crate) const ZOMBIE_REPLACE_AGE_MS: u64 = 2_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ExistingConnectionState {
    pub same_stable_id: bool,
    pub alive: bool,
    pub age_ms: u64,
    /// True when deterministic duplicate resolution says the fresh connection
    /// direction should win inside the symmetric-dial window.
    pub prefer_fresh_duplicate: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum IrohConnectionInstallDecision {
    /// Adopt the fresh connection into the endpoint map.
    Install,
    /// Keep the existing endpoint map entry and close the fresh connection.
    KeepExisting { close_fresh_reason: &'static str },
    /// Replace the existing endpoint map entry and close the previous
    /// connection with the supplied reason.
    ReplaceExisting { close_previous_reason: &'static str },
}

pub(crate) fn should_redial_without_precheck(existing_alive: bool) -> bool {
    !existing_alive
}

pub(crate) fn decide_inbound_install(
    existing: Option<ExistingConnectionState>,
) -> IrohConnectionInstallDecision {
    decide_install(existing, InstallDirection::Inbound)
}

pub(crate) fn decide_outbound_install(
    existing: Option<ExistingConnectionState>,
) -> IrohConnectionInstallDecision {
    decide_install(existing, InstallDirection::Outbound)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallDirection {
    Inbound,
    Outbound,
}

fn decide_install(
    existing: Option<ExistingConnectionState>,
    direction: InstallDirection,
) -> IrohConnectionInstallDecision {
    let Some(existing) = existing else {
        return IrohConnectionInstallDecision::Install;
    };
    if existing.same_stable_id || !existing.alive {
        return IrohConnectionInstallDecision::Install;
    }
    if existing.age_ms < ZOMBIE_REPLACE_AGE_MS {
        if existing.prefer_fresh_duplicate {
            return IrohConnectionInstallDecision::ReplaceExisting {
                close_previous_reason: match direction {
                    InstallDirection::Inbound => REASON_REPLACED_BY_NEW_INBOUND,
                    InstallDirection::Outbound => REASON_REPLACED_BY_NEW_OUTBOUND,
                },
            };
        }
        return IrohConnectionInstallDecision::KeepExisting {
            close_fresh_reason: match direction {
                InstallDirection::Inbound => REASON_DUPLICATE_ACCEPT_SUPERSEDED,
                InstallDirection::Outbound => REASON_DUPLICATE_DIAL_SUPERSEDED,
            },
        };
    }
    IrohConnectionInstallDecision::ReplaceExisting {
        close_previous_reason: match direction {
            InstallDirection::Inbound => REASON_ZOMBIE_REPLACED_BY_FRESH_ACCEPT,
            InstallDirection::Outbound => REASON_ZOMBIE_REPLACED_BY_FRESH_DIAL,
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn install_without_existing_or_for_same_stable_id() {
        assert_eq!(
            decide_inbound_install(None),
            IrohConnectionInstallDecision::Install
        );
        assert_eq!(
            decide_outbound_install(Some(ExistingConnectionState {
                same_stable_id: true,
                alive: true,
                age_ms: 1,
                prefer_fresh_duplicate: true,
            })),
            IrohConnectionInstallDecision::Install
        );
    }

    #[test]
    fn install_over_dead_existing_connection() {
        assert_eq!(
            decide_inbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: false,
                age_ms: 1,
                prefer_fresh_duplicate: true,
            })),
            IrohConnectionInstallDecision::Install
        );
    }

    #[test]
    fn keep_existing_live_connection_inside_symmetric_dial_window() {
        assert_eq!(
            decide_inbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: true,
                age_ms: ZOMBIE_REPLACE_AGE_MS - 1,
                prefer_fresh_duplicate: false,
            })),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_ACCEPT_SUPERSEDED,
            }
        );
        assert_eq!(
            decide_outbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: true,
                age_ms: ZOMBIE_REPLACE_AGE_MS - 1,
                prefer_fresh_duplicate: false,
            })),
            IrohConnectionInstallDecision::KeepExisting {
                close_fresh_reason: REASON_DUPLICATE_DIAL_SUPERSEDED,
            }
        );
    }

    #[test]
    fn tie_break_can_choose_fresh_connection_inside_symmetric_dial_window() {
        assert_eq!(
            decide_inbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: true,
                age_ms: ZOMBIE_REPLACE_AGE_MS - 1,
                prefer_fresh_duplicate: true,
            })),
            IrohConnectionInstallDecision::ReplaceExisting {
                close_previous_reason: REASON_REPLACED_BY_NEW_INBOUND,
            }
        );
        assert_eq!(
            decide_outbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: true,
                age_ms: ZOMBIE_REPLACE_AGE_MS - 1,
                prefer_fresh_duplicate: true,
            })),
            IrohConnectionInstallDecision::ReplaceExisting {
                close_previous_reason: REASON_REPLACED_BY_NEW_OUTBOUND,
            }
        );
    }

    #[test]
    fn replace_old_live_connection_as_zombie() {
        assert_eq!(
            decide_inbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: true,
                age_ms: ZOMBIE_REPLACE_AGE_MS,
                prefer_fresh_duplicate: false,
            })),
            IrohConnectionInstallDecision::ReplaceExisting {
                close_previous_reason: REASON_ZOMBIE_REPLACED_BY_FRESH_ACCEPT,
            }
        );
        assert_eq!(
            decide_outbound_install(Some(ExistingConnectionState {
                same_stable_id: false,
                alive: true,
                age_ms: ZOMBIE_REPLACE_AGE_MS,
                prefer_fresh_duplicate: false,
            })),
            IrohConnectionInstallDecision::ReplaceExisting {
                close_previous_reason: REASON_ZOMBIE_REPLACED_BY_FRESH_DIAL,
            }
        );
    }

    #[test]
    fn redial_precheck_only_dials_when_existing_is_not_alive() {
        assert!(!should_redial_without_precheck(true));
        assert!(should_redial_without_precheck(false));
    }
}