// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 #[cfg(feature = "etcd")] mod etcd; #[cfg(feature = "etcd")] pub use etcd::{EtcdConfig, EtcdConfigBuilder}; #[cfg(feature = "p2p")] mod p2p; #[cfg(feature = "p2p")] pub use p2p::{P2pConfig, P2pConfigBuilder}; #[cfg(test)] pub(crate) mod test_support; mod validation; use std::sync::Arc; use futures::future::BoxFuture; pub use crate::peer::PeerDiscovery; use crate::peer::{ DiscoveryError, DiscoveryQueryError, InstanceId, PeerInfo, WorkerAddress, WorkerId, }; /// Validates cluster ID format. /// /// Cluster IDs must contain only: /// - Lowercase letters (a-z) /// - Numbers (0-9) /// - Hyphens (-) /// - Underscores (_) /// /// No uppercase, spaces, slashes, or special characters allowed. /// /// # Errors /// /// Returns a validation error if: /// - The cluster_id is empty /// - The cluster_id contains invalid characters pub fn validate_cluster_id(cluster_id: &str) -> Result<(), validator::ValidationError> { if cluster_id.is_empty() { return Err(validator::ValidationError::new("cluster_id_empty")); } for ch in cluster_id.chars() { if !matches!(ch, 'a'..='z' | '0'..='9' | '-' | '_') { return Err(validator::ValidationError::new("cluster_id_invalid_chars")); } } Ok(()) } /// A [`DiscoverySystem`] should provide one or more concrete implementations of discovery traits in this crate. pub trait DiscoverySystem: Send + Sync + std::fmt::Debug { /// Returns a [`PeerDiscoveryExt`] implementation if available. fn peer_discovery(&self) -> Option>; /// Gracefully shutdown the discovery system. /// /// This should stop background tasks (like keep-alive), close connections, /// and clean up resources. Implementations should make this idempotent. /// /// Default implementation does nothing (no-op). fn shutdown(&self) { // Default no-op for implementations that don't need explicit shutdown } } /// Attach a [`DiscoverySystem`] to its peer discovery implementation while keeping the system alive. #[allow(dead_code)] pub(crate) fn peer_discovery_handle( system: Arc, ) -> Option> { system.peer_discovery().map(|inner| { Arc::new(SystemBackedPeerDiscovery::new(system, inner)) as Arc }) } #[derive(Clone)] #[allow(dead_code)] struct SystemBackedPeerDiscovery { system: Arc, inner: Arc, } impl SystemBackedPeerDiscovery { fn new(system: Arc, inner: Arc) -> Self { Self { system, inner } } } impl std::fmt::Debug for SystemBackedPeerDiscovery { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SystemBackedPeerDiscovery").finish() } } impl PeerDiscovery for SystemBackedPeerDiscovery { fn discover_by_worker_id( &self, worker_id: WorkerId, ) -> BoxFuture<'static, Result> { self.inner.discover_by_worker_id(worker_id) } fn discover_by_instance_id( &self, instance_id: InstanceId, ) -> BoxFuture<'static, Result> { self.inner.discover_by_instance_id(instance_id) } fn register_instance( &self, instance_id: InstanceId, worker_address: WorkerAddress, ) -> BoxFuture<'static, Result<(), DiscoveryError>> { self.inner.register_instance(instance_id, worker_address) } fn unregister_instance( &self, instance_id: InstanceId, ) -> BoxFuture<'static, Result<(), DiscoveryError>> { self.inner.unregister_instance(instance_id) } }