mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
node: introduce Reloadable trait and migrate host map to a lock-free snapshot
Add a `Reloadable` trait that normalizes the node's reloadable configuration/resource pattern onto a single contract built around an `arc_swap::ArcSwap` snapshot: a lock-free `load()` for the hot read path and an async `reload()` that re-reads the backing source and atomically swaps in a fresh snapshot. The trait carries the canonical Arc-wrapper template documentation (single-writer node tick, many-reader hot path, whole-snapshot swap so readers never observe a partial update). Migrate the host map to this trait via a new `HostMapReloadable` that reuses the existing load/merge/mtime helpers in upper::hosts. The Node `host_map` field changes from `Arc<HostMap>` to `HostMapReloadable`, and `peer_display_name` reads through a lock-free guard. The initial snapshot is byte-identical to the previous construction, so behavior is unchanged. The host map is still snapshotted once at construction and not polled; `reload()` is exercised only by unit tests for now. Wiring the periodic poll into the node tick, and deduplicating the hosts-file stat against the ACL reloader's embedded copy, is left as a follow-up. Add `arc-swap` as a dependency. Unit tests cover initial load (base + file, base only), change/no-change/deletion/creation detection, base-preserved-on-reload, and equivalence of the initial snapshot to the pre-migration construction.
This commit is contained in:
Generated
+10
@@ -98,6 +98,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -1063,6 +1072,7 @@ checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
|
|||||||
name = "fips"
|
name = "fips"
|
||||||
version = "0.4.0-dev"
|
version = "0.4.0-dev"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
"bech32",
|
"bech32",
|
||||||
"bluer",
|
"bluer",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ portable-atomic = { version = "1", features = ["std"] }
|
|||||||
|
|
||||||
nostr = { version = "0.44", features = ["std", "nip59"] }
|
nostr = { version = "0.44", features = ["std", "nip59"] }
|
||||||
nostr-sdk = "0.44"
|
nostr-sdk = "0.44"
|
||||||
|
arc-swap = "1"
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
tun = { version = "0.8.7", features = ["async"] }
|
tun = { version = "0.8.7", features = ["async"] }
|
||||||
|
|||||||
+13
-15
@@ -15,6 +15,7 @@ mod handlers;
|
|||||||
mod lifecycle;
|
mod lifecycle;
|
||||||
mod rate_limit;
|
mod rate_limit;
|
||||||
pub(crate) mod reject;
|
pub(crate) mod reject;
|
||||||
|
mod reloadable;
|
||||||
mod retry;
|
mod retry;
|
||||||
mod routing_error_rate_limit;
|
mod routing_error_rate_limit;
|
||||||
pub(crate) mod session;
|
pub(crate) mod session;
|
||||||
@@ -28,6 +29,7 @@ pub(crate) mod wire;
|
|||||||
|
|
||||||
use self::discovery_rate_limit::{DiscoveryBackoff, DiscoveryForwardRateLimiter};
|
use self::discovery_rate_limit::{DiscoveryBackoff, DiscoveryForwardRateLimiter};
|
||||||
use self::rate_limit::HandshakeRateLimiter;
|
use self::rate_limit::HandshakeRateLimiter;
|
||||||
|
use self::reloadable::Reloadable;
|
||||||
use self::routing_error_rate_limit::RoutingErrorRateLimiter;
|
use self::routing_error_rate_limit::RoutingErrorRateLimiter;
|
||||||
|
|
||||||
/// Half-range of the symmetric jitter applied to the per-session rekey timer.
|
/// Half-range of the symmetric jitter applied to the per-session rekey timer.
|
||||||
@@ -501,8 +503,9 @@ pub struct Node {
|
|||||||
|
|
||||||
// === Host Map ===
|
// === Host Map ===
|
||||||
/// Static hostname → npub mapping for DNS resolution.
|
/// Static hostname → npub mapping for DNS resolution.
|
||||||
/// Built at construction from peer aliases and /etc/fips/hosts.
|
/// Built at construction from peer aliases and /etc/fips/hosts, and
|
||||||
host_map: Arc<HostMap>,
|
/// published through a lock-free snapshot for the display path.
|
||||||
|
host_map: reloadable::HostMapReloadable,
|
||||||
|
|
||||||
/// Off-task FMP-encrypt + UDP-send worker pool. Unix-only —
|
/// Off-task FMP-encrypt + UDP-send worker pool. Unix-only —
|
||||||
/// the worker issues direct sendmmsg(2) / sendmsg+UDP_GSO calls
|
/// the worker issues direct sendmmsg(2) / sendmsg+UDP_GSO calls
|
||||||
@@ -595,13 +598,9 @@ impl Node {
|
|||||||
let forward_min_interval_secs = config.node.discovery.forward_min_interval_secs;
|
let forward_min_interval_secs = config.node.discovery.forward_min_interval_secs;
|
||||||
|
|
||||||
let base_host_map = HostMap::from_peer_configs(config.peers());
|
let base_host_map = HostMap::from_peer_configs(config.peers());
|
||||||
let mut host_map = base_host_map.clone();
|
|
||||||
let hosts_path = std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
|
let hosts_path = std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
|
||||||
let hosts_file = HostMap::load_hosts_file(std::path::Path::new(
|
let host_map =
|
||||||
crate::upper::hosts::DEFAULT_HOSTS_PATH,
|
reloadable::HostMapReloadable::new(base_host_map.clone(), hosts_path.clone());
|
||||||
));
|
|
||||||
host_map.merge(hosts_file);
|
|
||||||
let host_map = Arc::new(host_map);
|
|
||||||
let peer_acl = acl::PeerAclReloader::with_alias_sources(
|
let peer_acl = acl::PeerAclReloader::with_alias_sources(
|
||||||
std::path::PathBuf::from(acl::DEFAULT_PEERS_ALLOW_PATH),
|
std::path::PathBuf::from(acl::DEFAULT_PEERS_ALLOW_PATH),
|
||||||
std::path::PathBuf::from(acl::DEFAULT_PEERS_DENY_PATH),
|
std::path::PathBuf::from(acl::DEFAULT_PEERS_DENY_PATH),
|
||||||
@@ -744,16 +743,14 @@ impl Node {
|
|||||||
let coords_response_interval_ms = config.node.session.coords_response_interval_ms;
|
let coords_response_interval_ms = config.node.session.coords_response_interval_ms;
|
||||||
|
|
||||||
let base_host_map = HostMap::from_peer_configs(config.peers());
|
let base_host_map = HostMap::from_peer_configs(config.peers());
|
||||||
let mut host_map = base_host_map.clone();
|
let hosts_path = std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH);
|
||||||
host_map.merge(HostMap::load_hosts_file(std::path::Path::new(
|
let host_map =
|
||||||
crate::upper::hosts::DEFAULT_HOSTS_PATH,
|
reloadable::HostMapReloadable::new(base_host_map.clone(), hosts_path.clone());
|
||||||
)));
|
|
||||||
let host_map = Arc::new(host_map);
|
|
||||||
let peer_acl = acl::PeerAclReloader::with_alias_sources(
|
let peer_acl = acl::PeerAclReloader::with_alias_sources(
|
||||||
std::path::PathBuf::from(acl::DEFAULT_PEERS_ALLOW_PATH),
|
std::path::PathBuf::from(acl::DEFAULT_PEERS_ALLOW_PATH),
|
||||||
std::path::PathBuf::from(acl::DEFAULT_PEERS_DENY_PATH),
|
std::path::PathBuf::from(acl::DEFAULT_PEERS_DENY_PATH),
|
||||||
base_host_map,
|
base_host_map,
|
||||||
std::path::PathBuf::from(crate::upper::hosts::DEFAULT_HOSTS_PATH),
|
hosts_path,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -1084,7 +1081,8 @@ impl Node {
|
|||||||
/// 4. Session endpoint's short npub (end-to-end, may not be direct peer)
|
/// 4. Session endpoint's short npub (end-to-end, may not be direct peer)
|
||||||
/// 5. Truncated NodeAddr hex (unknown address)
|
/// 5. Truncated NodeAddr hex (unknown address)
|
||||||
pub(crate) fn peer_display_name(&self, addr: &NodeAddr) -> String {
|
pub(crate) fn peer_display_name(&self, addr: &NodeAddr) -> String {
|
||||||
if let Some(hostname) = self.host_map.lookup_hostname(addr) {
|
let hosts = self.host_map.load();
|
||||||
|
if let Some(hostname) = hosts.lookup_hostname(addr) {
|
||||||
return hostname.to_string();
|
return hostname.to_string();
|
||||||
}
|
}
|
||||||
if let Some(name) = self.peer_aliases.get(addr) {
|
if let Some(name) = self.peer_aliases.get(addr) {
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
//! Lock-free reloadable configuration / resource snapshots.
|
||||||
|
//!
|
||||||
|
//! Several node-owned resources are loaded from disk at startup and may be
|
||||||
|
//! re-read when the backing file changes (for example the `/etc/fips/hosts`
|
||||||
|
//! map). Historically each one carried its own ad-hoc reloader with a
|
||||||
|
//! slightly different shape. The [`Reloadable`] trait normalizes them onto a
|
||||||
|
//! single contract built around an [`arc_swap::ArcSwap`] snapshot.
|
||||||
|
//!
|
||||||
|
//! # Canonical Arc-wrapper template
|
||||||
|
//!
|
||||||
|
//! These resources follow a single-writer / many-reader pattern: the node
|
||||||
|
//! tick task is the only writer, while the hot path reads the current value
|
||||||
|
//! frequently and must never block.
|
||||||
|
//!
|
||||||
|
//! - The reader-facing immutable snapshot lives in an
|
||||||
|
//! [`arc_swap::ArcSwap<T>`]. Readers call [`Reloadable::load`], which yields
|
||||||
|
//! a lock-free [`arc_swap::Guard<Arc<T>>`] that derefs straight to the
|
||||||
|
//! snapshot — no mutex, no clone on the read path.
|
||||||
|
//! - The owning struct also holds the change-detection state (file mtime,
|
||||||
|
//! immutable base data, source path). That state is touched only by
|
||||||
|
//! [`Reloadable::reload`], which runs on the single writer task.
|
||||||
|
//! - `reload` builds a brand-new `T` and then stores `Arc::new(new)` into the
|
||||||
|
//! `ArcSwap`, so a reader either sees the entire old snapshot or the entire
|
||||||
|
//! new one — never a partial update.
|
||||||
|
//! - Construction performs the initial synchronous load so the snapshot is
|
||||||
|
//! valid before the node starts serving reads.
|
||||||
|
//!
|
||||||
|
//! `reload` returns `bool` rather than a value or `Result`: the underlying
|
||||||
|
//! loaders already absorb I/O errors internally (a missing or unreadable
|
||||||
|
//! source degrades to an empty/base snapshot plus a warning), so callers have
|
||||||
|
//! nothing to handle. The return flag reports only whether the snapshot was
|
||||||
|
//! replaced, which is all the tick loop needs for logging.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::upper::hosts::{HostMap, file_mtime};
|
||||||
|
|
||||||
|
/// A resource backed by a lock-free [`arc_swap::ArcSwap`] snapshot that can be
|
||||||
|
/// re-read from its source on demand.
|
||||||
|
///
|
||||||
|
/// See the [module documentation](self) for the canonical Arc-wrapper
|
||||||
|
/// template that implementors follow.
|
||||||
|
pub trait Reloadable: Send {
|
||||||
|
/// The immutable snapshot type readers observe.
|
||||||
|
type Snapshot;
|
||||||
|
|
||||||
|
/// Re-read the backing source and replace the snapshot if it changed.
|
||||||
|
///
|
||||||
|
/// Returns `true` if a new snapshot was stored, `false` if nothing
|
||||||
|
/// changed. I/O errors are absorbed internally (degrading to an
|
||||||
|
/// empty/base snapshot with a warning) rather than surfaced.
|
||||||
|
///
|
||||||
|
/// Not yet driven from the node tick — the host map snapshot is still
|
||||||
|
/// taken once at construction and not polled. Wiring the periodic poll
|
||||||
|
/// into the tick is a follow-up.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
async fn reload(&mut self) -> bool;
|
||||||
|
|
||||||
|
/// Acquire a lock-free guard over the current snapshot.
|
||||||
|
///
|
||||||
|
/// This is the hot-path read: it performs no locking and no allocation.
|
||||||
|
fn load(&self) -> arc_swap::Guard<Arc<Self::Snapshot>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reloadable hostname → npub map (base peer aliases merged with the operator
|
||||||
|
/// hosts file).
|
||||||
|
///
|
||||||
|
/// Holds the immutable base map (from peer-config aliases) plus the
|
||||||
|
/// change-detection state for the hosts file. The effective map (base merged
|
||||||
|
/// with the hosts file) is published through an [`arc_swap::ArcSwap`] so the
|
||||||
|
/// display path can read it without locking.
|
||||||
|
pub struct HostMapReloadable {
|
||||||
|
/// Reader-facing effective snapshot (base merged with hosts file).
|
||||||
|
snapshot: arc_swap::ArcSwap<HostMap>,
|
||||||
|
/// Base map from peer-config aliases (never changes). Read only by
|
||||||
|
/// `reload`, which is not yet driven from the node tick.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
base: HostMap,
|
||||||
|
/// Path to the operator hosts file. Read only by `reload`.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
/// Last observed modification time of the hosts file (`None` if absent).
|
||||||
|
/// Read only by `reload`.
|
||||||
|
#[cfg_attr(not(test), allow(dead_code))]
|
||||||
|
last_mtime: Option<std::time::SystemTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostMapReloadable {
|
||||||
|
/// Create a reloadable host map.
|
||||||
|
///
|
||||||
|
/// Performs the initial load of the hosts file and merges it over the
|
||||||
|
/// base map so the published snapshot is valid immediately.
|
||||||
|
pub fn new(base: HostMap, path: std::path::PathBuf) -> Self {
|
||||||
|
let last_mtime = file_mtime(&path);
|
||||||
|
let hosts_file = HostMap::load_hosts_file(&path);
|
||||||
|
let mut effective = base.clone();
|
||||||
|
effective.merge(hosts_file);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
snapshot: arc_swap::ArcSwap::from(Arc::new(effective)),
|
||||||
|
base,
|
||||||
|
path,
|
||||||
|
last_mtime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Reloadable for HostMapReloadable {
|
||||||
|
type Snapshot = HostMap;
|
||||||
|
|
||||||
|
async fn reload(&mut self) -> bool {
|
||||||
|
let current_mtime = file_mtime(&self.path);
|
||||||
|
|
||||||
|
if current_mtime == self.last_mtime {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File appeared, disappeared, or was modified.
|
||||||
|
self.last_mtime = current_mtime;
|
||||||
|
let hosts_file = HostMap::load_hosts_file(&self.path);
|
||||||
|
let mut new_effective = self.base.clone();
|
||||||
|
new_effective.merge(hosts_file);
|
||||||
|
|
||||||
|
let count = new_effective.len();
|
||||||
|
self.snapshot.store(Arc::new(new_effective));
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
path = %self.path.display(),
|
||||||
|
entries = count,
|
||||||
|
"Reloaded hosts file"
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&self) -> arc_swap::Guard<Arc<HostMap>> {
|
||||||
|
self.snapshot.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::Identity;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_initial_load_base_and_file() {
|
||||||
|
let id_base = Identity::generate();
|
||||||
|
let id_file = Identity::generate();
|
||||||
|
|
||||||
|
let mut base = HostMap::new();
|
||||||
|
base.insert("core", &id_base.npub()).unwrap();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id_file.npub())).unwrap();
|
||||||
|
|
||||||
|
let reloadable = HostMapReloadable::new(base, path);
|
||||||
|
let snapshot = reloadable.load();
|
||||||
|
assert_eq!(snapshot.len(), 2);
|
||||||
|
assert!(snapshot.lookup_npub("core").is_some());
|
||||||
|
assert!(snapshot.lookup_npub("gateway").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_initial_load_no_file_base_only() {
|
||||||
|
let id = Identity::generate();
|
||||||
|
let mut base = HostMap::new();
|
||||||
|
base.insert("core", &id.npub()).unwrap();
|
||||||
|
|
||||||
|
let reloadable =
|
||||||
|
HostMapReloadable::new(base, std::path::PathBuf::from("/nonexistent/hosts"));
|
||||||
|
let snapshot = reloadable.load();
|
||||||
|
assert_eq!(snapshot.len(), 1);
|
||||||
|
assert!(snapshot.lookup_npub("core").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reload_detects_file_change() {
|
||||||
|
let id1 = Identity::generate();
|
||||||
|
let id2 = Identity::generate();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id1.npub())).unwrap();
|
||||||
|
|
||||||
|
let mut reloadable = HostMapReloadable::new(HostMap::new(), path.clone());
|
||||||
|
assert_eq!(reloadable.load().len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
reloadable.load().lookup_npub("gateway"),
|
||||||
|
Some(id1.npub().as_str())
|
||||||
|
);
|
||||||
|
|
||||||
|
// No change yet.
|
||||||
|
assert!(!reloadable.reload().await);
|
||||||
|
|
||||||
|
// Bump mtime by rewriting; sleep for filesystem mtime granularity.
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
format!("gateway {}\nnew-host {}\n", id1.npub(), id2.npub()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(reloadable.reload().await);
|
||||||
|
let snapshot = reloadable.load();
|
||||||
|
assert_eq!(snapshot.len(), 2);
|
||||||
|
assert!(snapshot.lookup_npub("new-host").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reload_no_change_returns_false() {
|
||||||
|
let id = Identity::generate();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id.npub())).unwrap();
|
||||||
|
|
||||||
|
let mut reloadable = HostMapReloadable::new(HostMap::new(), path);
|
||||||
|
assert!(!reloadable.reload().await);
|
||||||
|
assert!(!reloadable.reload().await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reload_detects_file_deletion() {
|
||||||
|
let id = Identity::generate();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id.npub())).unwrap();
|
||||||
|
|
||||||
|
let mut reloadable = HostMapReloadable::new(HostMap::new(), path.clone());
|
||||||
|
assert_eq!(reloadable.load().len(), 1);
|
||||||
|
|
||||||
|
std::fs::remove_file(&path).unwrap();
|
||||||
|
|
||||||
|
assert!(reloadable.reload().await);
|
||||||
|
assert!(reloadable.load().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reload_detects_file_creation() {
|
||||||
|
let id = Identity::generate();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
|
||||||
|
let mut reloadable = HostMapReloadable::new(HostMap::new(), path.clone());
|
||||||
|
assert!(reloadable.load().is_empty());
|
||||||
|
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id.npub())).unwrap();
|
||||||
|
|
||||||
|
assert!(reloadable.reload().await);
|
||||||
|
let snapshot = reloadable.load();
|
||||||
|
assert_eq!(snapshot.len(), 1);
|
||||||
|
assert!(snapshot.lookup_npub("gateway").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_reload_preserves_base() {
|
||||||
|
let id_base = Identity::generate();
|
||||||
|
let id_file = Identity::generate();
|
||||||
|
|
||||||
|
let mut base = HostMap::new();
|
||||||
|
base.insert("core", &id_base.npub()).unwrap();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id_file.npub())).unwrap();
|
||||||
|
|
||||||
|
let mut reloadable = HostMapReloadable::new(base, path.clone());
|
||||||
|
assert_eq!(reloadable.load().len(), 2);
|
||||||
|
|
||||||
|
std::fs::remove_file(&path).unwrap();
|
||||||
|
assert!(reloadable.reload().await);
|
||||||
|
let snapshot = reloadable.load();
|
||||||
|
assert_eq!(snapshot.len(), 1);
|
||||||
|
assert!(snapshot.lookup_npub("core").is_some());
|
||||||
|
assert!(snapshot.lookup_npub("gateway").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The initial published snapshot must be byte-for-byte equivalent to the
|
||||||
|
/// pre-migration `Arc<HostMap>` built by `base.clone()` + `merge(file)`.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_initial_snapshot_matches_pre_migration_construction() {
|
||||||
|
let id_base = Identity::generate();
|
||||||
|
let id_file = Identity::generate();
|
||||||
|
|
||||||
|
let mut base = HostMap::new();
|
||||||
|
base.insert("core", &id_base.npub()).unwrap();
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("hosts");
|
||||||
|
std::fs::write(&path, format!("gateway {}\n", id_file.npub())).unwrap();
|
||||||
|
|
||||||
|
// Pre-migration construction.
|
||||||
|
let mut expected = base.clone();
|
||||||
|
expected.merge(HostMap::load_hosts_file(&path));
|
||||||
|
|
||||||
|
// Post-migration construction.
|
||||||
|
let reloadable = HostMapReloadable::new(base, path);
|
||||||
|
let snapshot = reloadable.load();
|
||||||
|
|
||||||
|
assert_eq!(snapshot.len(), expected.len());
|
||||||
|
for key in ["core", "gateway"] {
|
||||||
|
assert_eq!(snapshot.lookup_npub(key), expected.lookup_npub(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user