Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Topology

Topology separates protocol logic from deployment configuration. Choreographies remain location free while topology definitions specify where roles execute.

Core Types

Locations capture how a role is deployed.

#![allow(unused)]
fn main() {
pub enum Location {
    Local,
    Remote(TopologyEndpoint),
    Colocated(RoleName),
}
}

The Local variant means in process execution. The Remote variant stores a network endpoint. The Colocated variant ties a role to another role location.

Topology state holds role mappings, constraints, and optional mode shortcuts.

#![allow(unused)]
fn main() {
pub struct Topology {
    pub mode: Option<TopologyMode>,
    pub locations: BTreeMap<RoleName, Location>,
    pub channel_capacities: BTreeMap<(RoleName, RoleName), ChannelCapacity>,
    pub constraints: Vec<TopologyConstraint>,
    pub role_constraints: BTreeMap<String, RoleFamilyConstraint>,
}
}

TopologyMode provides common presets. TopologyConstraint and RoleFamilyConstraint add placement and role family rules.

#![allow(unused)]
fn main() {
pub enum TopologyMode {
    Local,
    PerRole,
    Kubernetes(Namespace),
    Consul(Datacenter),
}

pub enum TopologyConstraint {
    Colocated(RoleName, RoleName),
    Separated(RoleName, RoleName),
    Pinned(RoleName, Location),
    Region(RoleName, Region),
}

pub struct RoleFamilyConstraint {
    pub min: u32,
    pub max: Option<u32>,
}
}

TopologyMode is parsed from DSL values like local and kubernetes(ns). Role family constraints are used to validate wildcard and range roles.

RoleFamilyConstraint provides helper constructors and a validation method:

#![allow(unused)]
fn main() {
// Minimum-only constraint (unbounded max)
let unbounded = RoleFamilyConstraint::min_only(3);

// Bounded constraint with min and max
let bounded = RoleFamilyConstraint::bounded(2, 5);

// Validate a role count against the constraint
bounded.validate(4)?;  // Ok
bounded.validate(1)?;  // Err(BelowMinimum)
bounded.validate(6)?;  // Err(AboveMaximum)
}

These helpers let generated runners fail fast on invalid family cardinalities before any transport wiring or handler startup occurs.

DSL Syntax

Topologies are defined in .topology files or parsed from strings.

topology TwoPhaseCommit_Prod for TwoPhaseCommit {
    mode: per_role

    Coordinator: coordinator.prod.internal:9000
    ParticipantA: participant-a.prod.internal:9000
    ParticipantB: participant-b.prod.internal:9000

    role_constraints {
        Participant: min = 2, max = 5
    }

    channel_capacities {
        Coordinator -> ParticipantA: 4
        Coordinator -> ParticipantB: 4
    }

    constraints {
        separated: Coordinator, ParticipantA
        separated: Coordinator, ParticipantB
        region: Coordinator -> us_east_1
    }
}

The role_constraints block controls acceptable family sizes. The channel_capacities block sets per-edge capacity in bits (used for branching feasibility checks). The constraints block encodes separation, pinning, and region requirements.

Rust API

You can build topologies programmatically and validate them against choreography roles.

#![allow(unused)]
fn main() {
use telltale_choreography::{RoleName, Topology, TopologyEndpoint};

let topology = Topology::builder()
    .local_role(RoleName::from_static("Coordinator"))
    .remote_role(
        RoleName::from_static("ParticipantA"),
        TopologyEndpoint::new("localhost:9001").unwrap(),
    )
    .remote_role(
        RoleName::from_static("ParticipantB"),
        TopologyEndpoint::new("localhost:9002").unwrap(),
    )
    .build();

let roles = [
    RoleName::from_static("Coordinator"),
    RoleName::from_static("ParticipantA"),
    RoleName::from_static("ParticipantB"),
];
let validation = topology.validate(&roles);
assert!(validation.is_valid());
}

Topology::builder produces a TopologyBuilder with fluent helpers. Topology::validate checks that all roles referenced in the topology and constraints exist in the choreography.

Topologies can also be loaded from DSL files.

#![allow(unused)]
fn main() {
let parsed = Topology::load("deploy/prod.topology")?;
let topology = parsed.topology;
}

Topology::load returns a ParsedTopology that includes both the topology and the target protocol name.

TopologyHandler

TopologyHandler wraps transport selection and routing for a role.

#![allow(unused)]
fn main() {
use telltale_choreography::{RoleName, TopologyHandler};

let handler = TopologyHandler::local(RoleName::from_static("Alice"));
handler.initialize().await?;
}

The local constructor sets TopologyMode::Local and creates in process transports. For custom layouts, use TopologyHandler::new or the builder with a Topology.

Generated protocols include helpers under Protocol::topology, including Protocol::topology::handler(role) and Protocol::topology::with_topology(topology, role). These return a TopologyHandler for the selected role.

Generated Topology Helper Surface

choreography! emits a topology helper module per protocol. The generated surface follows this shape:

#![allow(unused)]
fn main() {
pub mod topology {
    pub fn handler(role: Role) -> TopologyHandler;
    pub fn with_topology(topology: Topology, role: Role) -> Result<TopologyHandler, String>;

    pub mod topologies {
        pub fn dev() -> Topology;
        pub fn dev_handler(role: Role) -> Result<TopologyHandler, String>;
        pub fn prod() -> Topology;
        pub fn prod_handler(role: Role) -> Result<TopologyHandler, String>;
    }
}
}

handler(role) builds a local topology handler. with_topology(topology, role) validates role coverage and returns a role-bound handler. The topologies submodule is emitted when inline topology definitions are present in the DSL.

Usage pattern:

#![allow(unused)]
fn main() {
let local = MyProtocol::topology::handler(Role::Alice);
let prod = MyProtocol::topology::topologies::prod_handler(Role::Alice)?;
let custom = MyProtocol::topology::with_topology(custom_topology, Role::Alice)?;
}

This pattern shows local default setup, named topology setup, and custom topology setup in one place.

Transport Selection

Transport selection is based on role locations.

#![allow(unused)]
fn main() {
use telltale_choreography::topology::{TransportFactory, TransportType};

let transport_type = TransportFactory::transport_for_location(
    &RoleName::from_static("Alice"),
    &RoleName::from_static("Bob"),
    &topology,
)?;
assert!(matches!(transport_type, TransportType::Tcp));
}

TransportFactory::create currently returns an InMemoryChannelTransport for all modes. The TransportType value signals intent but remote transports are placeholders.

Lean Correspondence

The Lean formalization for topology is in lean/Protocol/Spatial.lean. Projection correctness does not depend on topology data, so location checks are enforced during deployment instead of compilation.

See Choreographic DSL for role declarations and Choreography Effect Handlers for choreography handler usage patterns.