Some checks failed
Run Check Script / check (pull_request) Failing after 10s
115 lines
4.5 KiB
Markdown
115 lines
4.5 KiB
Markdown
# 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<T>`) compose/orchestrate capabilities *across* multiple underlying topologies (e.g., primary+replica `T`).
|
||
|
||
Naive design requires manual `impl Capability for HigherOrderTopology<T>` *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<T> {
|
||
/// 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<T: PostgreSQL> PostgreSQL for FailoverTopology<T> {
|
||
async fn deploy(&self, config: &PostgreSQLConfig) -> Result<String, String> {
|
||
// 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<ReplicationCerts, String> {
|
||
self.primary.get_replication_certs(cluster_name).await
|
||
}
|
||
// ...
|
||
}
|
||
|
||
/// Similarly for other capabilities.
|
||
#[async_trait]
|
||
impl<T: Docker> Docker for FailoverTopology<T> {
|
||
// Failover Docker orchestration.
|
||
}
|
||
````
|
||
|
||
**Key properties:**
|
||
- **Auto-derivation**: `Failover<K8sAnywhere>` 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<K8sAnywhereTopology> = ...;
|
||
pg_failover.deploy_pg(config).await;
|
||
|
||
// ✅ Works: LinuxHost: Docker → Failover provides failover Docker
|
||
let docker_failover: FailoverTopology<LinuxHostTopology> = ...;
|
||
docker_failover.deploy_docker(...).await;
|
||
|
||
// ❌ Compile fail: K8sAnywhere !: Docker
|
||
let invalid: FailoverTopology<K8sAnywhereTopology>;
|
||
invalid.deploy_docker(...); // `T: Docker` bound unsatisfied
|
||
````
|
||
|
||
## Consequences
|
||
|
||
**Pros:**
|
||
- **Extensible**: New topology `AWSTopology: PostgreSQL` → instant `Failover<AWSTopology>: 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**<br>`impl PG for Failover<K8s> {..}`<br>`impl PG for Failover<Linux> {..}` | Explicit control | N×M explosion; violates ISP; hard to extend. |
|
||
| **Dynamic trait objects**<br>`Box<dyn AnyCapability>` | Runtime flex | Perf hit; type erasure; error-prone dispatch. |
|
||
| **Mega-topology trait**<br>All-in-one `OrchestratedTopology` | Simple wiring | Monolithic; poor composition. |
|
||
| **Registry dispatch**<br>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<T>`, `ShardedTopology<T>`, etc.
|
||
- `FailoverTopology` in `failover.rs` is first implementation.
|