diff --git a/adr/015-higher-order-topologies.md b/adr/015-higher-order-topologies.md new file mode 100644 index 0000000..41c3172 --- /dev/null +++ b/adr/015-higher-order-topologies.md @@ -0,0 +1,114 @@ +# Architecture Decision Record: Higher-Order Topologies + +**Initial Author:** Jean-Gabriel Gill-Couture +**Initial Date:** 2025-12-08 +**Last Updated Date:** 2025-12-08 + +## Status + +Implemented + +## Context + +Harmony models infrastructure as **Topologies** (deployment targets like `K8sAnywhereTopology`, `LinuxHostTopology`) implementing **Capabilities** (tech traits like `PostgreSQL`, `Docker`). + +**Higher-Order Topologies** (e.g., `FailoverTopology`) compose/orchestrate capabilities *across* multiple underlying topologies (e.g., primary+replica `T`). + +Naive design requires manual `impl Capability for HigherOrderTopology` *per T per capability*, causing: +- **Impl explosion**: N topologies × M capabilities = N×M boilerplate. +- **ISP violation**: Topologies forced to impl unrelated capabilities. +- **Maintenance hell**: New topology needs impls for *all* orchestrated capabilities; new capability needs impls for *all* topologies/higher-order. +- **Barrier to extension**: Users can't easily add topologies without todos/panics. + +This makes scaling Harmony impractical as ecosystem grows. + +## Decision + +Use **blanket trait impls** on higher-order topologies to *automatically* derive orchestration: + +````rust +/// Higher-Order Topology: Orchestrates capabilities across sub-topologies. +pub struct FailoverTopology { + /// Primary sub-topology. + primary: T, + /// Replica sub-topology. + replica: T, +} + +/// Automatically provides PostgreSQL failover for *any* `T: PostgreSQL`. +/// Delegates to primary for queries; orchestrates deploy across both. +#[async_trait] +impl PostgreSQL for FailoverTopology { + async fn deploy(&self, config: &PostgreSQLConfig) -> Result { + // Deploy primary; extract certs/endpoint; + // deploy replica with pg_basebackup + TLS passthrough. + // (Full impl logged/elaborated.) + } + + // Delegate queries to primary. + async fn get_replication_certs(&self, cluster_name: &str) -> Result { + self.primary.get_replication_certs(cluster_name).await + } + // ... +} + +/// Similarly for other capabilities. +#[async_trait] +impl Docker for FailoverTopology { + // Failover Docker orchestration. +} +```` + +**Key properties:** +- **Auto-derivation**: `Failover` gets `PostgreSQL` iff `K8sAnywhere: PostgreSQL`. +- **No boilerplate**: One blanket impl per capability *per higher-order type*. + +## Rationale + +- **Composition via generics**: Rust trait solver auto-selects impls; zero runtime cost. +- **Compile-time safety**: Missing `T: Capability` → compile error (no panics). +- **Scalable**: O(capabilities) impls per higher-order; new `T` auto-works. +- **ISP-respecting**: Capabilities only surface if sub-topology provides. +- **Centralized logic**: Orchestration (e.g., cert propagation) in one place. + +**Example usage:** +````rust +// ✅ Works: K8sAnywhere: PostgreSQL → Failover provides failover PG +let pg_failover: FailoverTopology = ...; +pg_failover.deploy_pg(config).await; + +// ✅ Works: LinuxHost: Docker → Failover provides failover Docker +let docker_failover: FailoverTopology = ...; +docker_failover.deploy_docker(...).await; + +// ❌ Compile fail: K8sAnywhere !: Docker +let invalid: FailoverTopology; +invalid.deploy_docker(...); // `T: Docker` bound unsatisfied +```` + +## Consequences + +**Pros:** +- **Extensible**: New topology `AWSTopology: PostgreSQL` → instant `Failover: PostgreSQL`. +- **Lean**: No useless impls (e.g., no `K8sAnywhere: Docker`). +- **Observable**: Logs trace every step. + +**Cons:** +- **Monomorphization**: Generics generate code per T (mitigated: few Ts). +- **Delegation opacity**: Relies on rustdoc/logs for internals. + +## Alternatives considered + +| Approach | Pros | Cons | +|----------|------|------| +| **Manual per-T impls**
`impl PG for Failover {..}`
`impl PG for Failover {..}` | Explicit control | N×M explosion; violates ISP; hard to extend. | +| **Dynamic trait objects**
`Box` | Runtime flex | Perf hit; type erasure; error-prone dispatch. | +| **Mega-topology trait**
All-in-one `OrchestratedTopology` | Simple wiring | Monolithic; poor composition. | +| **Registry dispatch**
Runtime capability lookup | Decoupled | Complex; no compile safety; perf/debug overhead. | + +**Selected**: Blanket impls leverage Rust generics for safe, zero-cost composition. + +## Additional Notes + +- Applies to `MultisiteTopology`, `ShardedTopology`, etc. +- `FailoverTopology` in `failover.rs` is first implementation. diff --git a/adr/015-higher-order-topologies/example.rs b/adr/015-higher-order-topologies/example.rs new file mode 100644 index 0000000..8c8911d --- /dev/null +++ b/adr/015-higher-order-topologies/example.rs @@ -0,0 +1,153 @@ +//! Example of Higher-Order Topologies in Harmony. +//! Demonstrates how `FailoverTopology` automatically provides failover for *any* capability +//! supported by a sub-topology `T` via blanket trait impls. +//! +//! Key insight: No manual impls per T or capability -- scales effortlessly. +//! Users can: +//! - Write new `Topology` (impl capabilities on a struct). +//! - Compose with `FailoverTopology` (gets capabilities if T has them). +//! - Compile fails if capability missing (safety). + +use async_trait::async_trait; +use tokio; + +/// Capability trait: Deploy and manage PostgreSQL. +#[async_trait] +pub trait PostgreSQL { + async fn deploy(&self, config: &PostgreSQLConfig) -> Result; + async fn get_replication_certs(&self, cluster_name: &str) -> Result; +} + +/// Capability trait: Deploy Docker. +#[async_trait] +pub trait Docker { + async fn deploy_docker(&self) -> Result; +} + +/// Configuration for PostgreSQL deployments. +#[derive(Clone)] +pub struct PostgreSQLConfig; + +/// Replication certificates. +#[derive(Clone)] +pub struct ReplicationCerts; + +/// Concrete topology: Kubernetes Anywhere (supports PostgreSQL). +#[derive(Clone)] +pub struct K8sAnywhereTopology; + +#[async_trait] +impl PostgreSQL for K8sAnywhereTopology { + async fn deploy(&self, _config: &PostgreSQLConfig) -> Result { + // Real impl: Use k8s helm chart, operator, etc. + Ok("K8sAnywhere PostgreSQL deployed".to_string()) + } + + async fn get_replication_certs(&self, _cluster_name: &str) -> Result { + Ok(ReplicationCerts) + } +} + +/// Concrete topology: Linux Host (supports Docker). +#[derive(Clone)] +pub struct LinuxHostTopology; + +#[async_trait] +impl Docker for LinuxHostTopology { + async fn deploy_docker(&self) -> Result { + // Real impl: Install/configure Docker on host. + Ok("LinuxHost Docker deployed".to_string()) + } +} + +/// Higher-Order Topology: Composes multiple sub-topologies (primary + replica). +/// Automatically derives *all* capabilities of `T` with failover orchestration. +/// +/// - If `T: PostgreSQL`, then `FailoverTopology: PostgreSQL` (blanket impl). +/// - Same for `Docker`, etc. No boilerplate! +/// - Compile-time safe: Missing `T: Capability` → error. +#[derive(Clone)] +pub struct FailoverTopology { + /// Primary sub-topology. + pub primary: T, + /// Replica sub-topology. + pub replica: T, +} + +/// Blanket impl: Failover PostgreSQL if T provides PostgreSQL. +/// Delegates reads to primary; deploys to both. +#[async_trait] +impl PostgreSQL for FailoverTopology { + async fn deploy(&self, config: &PostgreSQLConfig) -> Result { + // Orchestrate: Deploy primary first, then replica (e.g., via pg_basebackup). + let primary_result = self.primary.deploy(config).await?; + let replica_result = self.replica.deploy(config).await?; + Ok(format!("Failover PG deployed: {} | {}", primary_result, replica_result)) + } + + async fn get_replication_certs(&self, cluster_name: &str) -> Result { + // Delegate to primary (replica follows). + self.primary.get_replication_certs(cluster_name).await + } +} + +/// Blanket impl: Failover Docker if T provides Docker. +#[async_trait] +impl Docker for FailoverTopology { + async fn deploy_docker(&self) -> Result { + // Orchestrate across primary + replica. + let primary_result = self.primary.deploy_docker().await?; + let replica_result = self.replica.deploy_docker().await?; + Ok(format!("Failover Docker deployed: {} | {}", primary_result, replica_result)) + } +} + +#[tokio::main] +async fn main() { + let config = PostgreSQLConfig; + + println!("=== ✅ PostgreSQL Failover (K8sAnywhere supports PG) ==="); + let pg_failover = FailoverTopology { + primary: K8sAnywhereTopology, + replica: K8sAnywhereTopology, + }; + let result = pg_failover.deploy(&config).await.unwrap(); + println!("Result: {}", result); + + println!("\n=== ✅ Docker Failover (LinuxHost supports Docker) ==="); + let docker_failover = FailoverTopology { + primary: LinuxHostTopology, + replica: LinuxHostTopology, + }; + let result = docker_failover.deploy_docker().await.unwrap(); + println!("Result: {}", result); + + println!("\n=== ❌ Would fail to compile (K8sAnywhere !: Docker) ==="); + // let invalid = FailoverTopology { + // primary: K8sAnywhereTopology, + // replica: K8sAnywhereTopology, + // }; + // invalid.deploy_docker().await.unwrap(); // Error: `K8sAnywhereTopology: Docker` not satisfied! + // Very clear error message : + // error[E0599]: the method `deploy_docker` exists for struct `FailoverTopology`, but its trait bounds were not satisfied + // --> src/main.rs:90:9 + // | + // 4 | pub struct FailoverTopology { + // | ------------------------------ method `deploy_docker` not found for this struct because it doesn't satisfy `FailoverTopology: Docker` + // ... + // 37 | struct K8sAnywhereTopology; + // | -------------------------- doesn't satisfy `K8sAnywhereTopology: Docker` + // ... + // 90 | invalid.deploy_docker(); // `T: Docker` bound unsatisfied + // | ^^^^^^^^^^^^^ method cannot be called on `FailoverTopology` due to unsatisfied trait bounds + // | + // note: trait bound `K8sAnywhereTopology: Docker` was not satisfied + // --> src/main.rs:61:9 + // | + // 61 | impl Docker for FailoverTopology { + // | ^^^^^^ ------ ------------------- + // | | + // | unsatisfied trait bound introduced here + // note: the trait `Docker` must be implemented +} +