pub mod builder;
pub mod error;
pub mod macros;
pub mod registry;
pub mod servlet_registry;
pub use builder::{ClusterConfBuilder, HeartbeatConfBuilder};
pub use error::ClusterError;
pub use registry::{HiveEntry, HiveRegistry, SharedId};
pub use servlet_registry::{PheromoneConf, ServletEntry, ServletRegistry};
use core::future::Future;
use core::marker::PhantomData;
use core::time::Duration;
use std::sync::Arc;
use crate::crypto::hash::{Digest, Sha3_256};
use crate::crypto::key::SigningKeyProvider;
use crate::policy::GatePolicy;
use crate::trace::TraceCollector;
use crate::transport::client::pool::PoolConfig;
use crate::transport::{Protocol, TightBeamAddress};
#[cfg(feature = "x509")]
use crate::crypto::x509::{policy::CertificateValidation, CertificateSpec};
use super::common::LeastLoaded;
use super::hive::LoadBalancer;
pub(crate) const DEFAULT_HEARTBEAT_INTERVAL_SECS: u64 = 5;
pub(crate) const DEFAULT_HEARTBEAT_TIMEOUT_SECS: u64 = 15;
pub(crate) const DEFAULT_MAX_CONCURRENT: usize = 10;
pub(crate) const DEFAULT_MAX_FAILURES: u32 = 3;
pub struct HeartbeatConf {
pub interval: Duration,
pub timeout: Duration,
pub max_concurrent: usize,
pub max_failures: u32,
pub on_heartbeat: Option<HeartbeatCallback>,
}
impl Default for HeartbeatConf {
fn default() -> Self {
Self {
interval: Duration::from_secs(DEFAULT_HEARTBEAT_INTERVAL_SECS),
timeout: Duration::from_secs(DEFAULT_HEARTBEAT_TIMEOUT_SECS),
max_concurrent: DEFAULT_MAX_CONCURRENT,
max_failures: DEFAULT_MAX_FAILURES,
on_heartbeat: None,
}
}
}
impl core::fmt::Debug for HeartbeatConf {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("HeartbeatConf")
.field("interval", &self.interval)
.field("timeout", &self.timeout)
.field("max_concurrent", &self.max_concurrent)
.field("max_failures", &self.max_failures)
.field("on_heartbeat", &self.on_heartbeat.as_ref().map(|_| "Some(...)"))
.finish()
}
}
impl HeartbeatConf {
pub fn with_callback(mut self, callback: HeartbeatCallback) -> Self {
self.on_heartbeat = Some(callback);
self
}
}
#[derive(Debug, Clone)]
pub struct HeartbeatEvent {
pub hive_addr: Arc<[u8]>,
pub success: bool,
pub utilization: Option<crate::utils::BasisPoints>,
}
pub type HeartbeatCallback = Arc<dyn Fn(HeartbeatEvent) + Send + Sync>;
#[cfg(feature = "x509")]
pub struct ClusterTlsConfig {
pub certificate: CertificateSpec,
pub key: Arc<dyn SigningKeyProvider>,
pub validators: Vec<Arc<dyn CertificateValidation>>,
pub client_validators: Vec<Arc<dyn CertificateValidation>>,
pub hive_trust: Option<Arc<dyn crate::crypto::x509::store::CertificateTrust>>,
}
#[cfg(feature = "x509")]
impl Clone for ClusterTlsConfig {
fn clone(&self) -> Self {
Self {
certificate: self.certificate.clone(),
key: Arc::clone(&self.key),
validators: self.validators.iter().map(Arc::clone).collect(),
client_validators: self.client_validators.iter().map(Arc::clone).collect(),
hive_trust: self.hive_trust.as_ref().map(Arc::clone),
}
}
}
#[cfg(feature = "x509")]
impl core::fmt::Debug for ClusterTlsConfig {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ClusterTlsConfig")
.field("certificate", &self.certificate)
.field("key", &"<KeyProvider>")
.field("validators", &format!("[{} validators]", self.validators.len()))
.field("client_validators", &format!("[{} validators]", self.client_validators.len()))
.field("hive_trust", &self.hive_trust.as_ref().map(|_| "Some(<TrustStore>)"))
.finish()
}
}
pub struct ClusterConf<L: LoadBalancer = LeastLoaded, D: Digest = Sha3_256> {
pub load_balancer: L,
pub heartbeat: HeartbeatConf,
pub pheromone: PheromoneConf,
pub policies: Vec<Arc<dyn GatePolicy + Send + Sync>>,
pub pool_config: PoolConfig,
pub control_freshness_window_ms: u64,
#[cfg(feature = "x509")]
pub tls: ClusterTlsConfig,
pub(crate) _digest: PhantomData<D>,
}
#[cfg(feature = "x509")]
impl ClusterConf {
pub fn new(tls: ClusterTlsConfig) -> Self {
Self::builder(tls).build()
}
}
#[cfg(feature = "x509")]
impl<L: LoadBalancer, D: Digest> core::fmt::Debug for ClusterConf<L, D> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ClusterConfig")
.field("heartbeat", &self.heartbeat)
.field("pheromone", &self.pheromone)
.field("policies", &format!("[{} policies]", self.policies.len()))
.field("pool_config", &self.pool_config)
.field("control_freshness_window_ms", &self.control_freshness_window_ms)
.field("tls", &self.tls)
.finish()
}
}
pub use crate::colony::common::{ClusterRequest, ClusterWorkRequest, ClusterWorkResponse};
pub trait Cluster: Sized + Send + Sync {
type Protocol: Protocol;
type Address: TightBeamAddress;
fn start(
trace: Arc<TraceCollector>,
config: ClusterConf,
) -> impl Future<Output = Result<Self, crate::TightBeamError>> + Send;
fn addr(&self) -> Self::Address;
fn available_servlets(&self) -> Vec<Vec<u8>>;
fn hive_count(&self) -> usize;
fn trace(&self) -> Arc<TraceCollector>;
fn stop(self);
fn join(self) -> impl Future<Output = Result<(), crate::colony::servlet::servlet_runtime::rt::JoinError>> + Send;
fn registry(&self) -> &Arc<HiveRegistry>;
fn heartbeat_config(&self) -> &HeartbeatConf;
fn send_heartbeat(
&self,
addr: Self::Address,
) -> impl Future<Output = Result<super::common::HeartbeatResult, ClusterError>> + Send;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::colony::common::RegisterHiveRequest;
use crate::colony::hive::ServletInfo;
use crate::crypto::key::Secp256k1KeyProvider;
use crate::crypto::sign::ecdsa::Secp256k1SigningKey;
use crate::policy::TransitStatus;
use crate::testing::create_test_signing_key;
use crate::utils::BasisPoints;
fn test_tls_config() -> ClusterTlsConfig {
let key: Secp256k1SigningKey = create_test_signing_key();
ClusterTlsConfig {
certificate: CertificateSpec::Der(&[]),
key: Arc::new(Secp256k1KeyProvider::from(key)),
validators: Vec::new(),
client_validators: Vec::new(),
hive_trust: None,
}
}
fn test_registry() -> HiveRegistry {
HiveRegistry::new(Duration::from_secs(15))
}
fn request(addr: &[u8], servlets: &[&[u8]]) -> RegisterHiveRequest {
RegisterHiveRequest {
issued_at_ms: 0,
hive_addr: addr.to_vec(),
metadata: None,
servlet_addresses: servlets
.iter()
.map(|s| ServletInfo { servlet_id: s.to_vec(), address: addr.to_vec() })
.collect(),
}
}
fn request_with_meta(addr: &[u8], servlets: &[&[u8]], meta: &[u8]) -> RegisterHiveRequest {
RegisterHiveRequest {
issued_at_ms: 0,
hive_addr: addr.to_vec(),
metadata: Some(meta.to_vec()),
servlet_addresses: servlets
.iter()
.map(|s| ServletInfo { servlet_id: s.to_vec(), address: addr.to_vec() })
.collect(),
}
}
#[test]
fn cluster_conf_defaults() {
let config = ClusterConf::new(test_tls_config());
assert_eq!(config.heartbeat.interval, Duration::from_secs(5));
assert_eq!(config.heartbeat.timeout, Duration::from_secs(15));
assert!(config.policies.is_empty());
}
#[test]
fn work_response_ok() {
let response = ClusterWorkResponse::ok(b"test".to_vec());
assert_eq!(response.status, TransitStatus::Accepted);
assert_eq!(response.payload, Some(b"test".to_vec()));
}
#[test]
fn work_response_err() {
let response = ClusterWorkResponse::err(TransitStatus::Forbidden);
assert_eq!(response.status, TransitStatus::Forbidden);
assert!(response.payload.is_none());
}
#[test]
fn registry_register_and_lookup() -> Result<(), ClusterError> {
let registry = test_registry();
registry.register(request(b"127.0.0.1:8080", &[b"ping", b"calc"]))?;
assert_eq!(registry.hives_for_type(b"ping")?.len(), 1);
assert_eq!(registry.hives_for_type(b"calc")?.len(), 1);
assert_eq!(registry.hives_for_type(b"ping")?[0].address.as_ref(), b"127.0.0.1:8080");
assert!(registry.hives_for_type(b"unknown")?.is_empty());
Ok(())
}
#[test]
fn registry_unregister() -> Result<(), ClusterError> {
let registry = test_registry();
registry.register(request(b"127.0.0.1:8080", &[b"ping"]))?;
assert_eq!(registry.len()?, 1);
assert!(registry.unregister(b"127.0.0.1:8080")?.is_some());
assert_eq!(registry.len()?, 0);
assert!(registry.hives_for_type(b"ping")?.is_empty());
Ok(())
}
#[test]
fn registry_update_utilization() -> Result<(), ClusterError> {
let registry = test_registry();
registry.register(request(b"127.0.0.1:8080", &[b"ping"]))?;
assert!(registry.update_utilization(b"127.0.0.1:8080", BasisPoints::new(5000))?);
assert_eq!(registry.hives_for_type(b"ping")?[0].utilization.get(), 5000);
Ok(())
}
#[test]
fn registry_available_servlets_deduplicated() -> Result<(), ClusterError> {
let registry = test_registry();
registry.register(request(b"hive1", &[b"ping", b"calc"]))?;
registry.register(request(b"hive2", &[b"ping", b"worker"]))?;
assert_eq!(registry.to_available_servlets()?.len(), 3);
Ok(())
}
#[test]
fn registry_multiple_hives_same_type() -> Result<(), ClusterError> {
let registry = test_registry();
registry.register(request(b"hive1", &[b"ping"]))?;
registry.register(request(b"hive2", &[b"ping"]))?;
assert_eq!(registry.hives_for_type(b"ping")?.len(), 2);
Ok(())
}
#[test]
fn registry_all_hives() -> Result<(), ClusterError> {
let registry = test_registry();
registry.register(request(b"hive1", &[b"ping"]))?;
registry.register(request_with_meta(b"hive2", &[b"calc"], b"metadata"))?;
let all = registry.all_hives()?;
assert_eq!(all.len(), 2);
let addrs: Vec<_> = all.iter().map(|e| e.address.as_ref()).collect();
assert!(addrs.contains(&b"hive1".as_slice()));
assert!(addrs.contains(&b"hive2".as_slice()));
Ok(())
}
}