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

Self-Organization

One of mosaik’s defining features is that nodes self-organize into the correct topology without manual configuration. This chapter explains how that works step by step.

The Self-Organization Loop

When a new node joins the network, the following sequence happens automatically:

1. Bootstrap       Node connects to a known bootstrap peer
                       │
2. Gossip          Announce protocol broadcasts presence
                       │
3. Catalog Sync    Full catalog exchange with bootstrap peer
                       │
4. Discovery       Node learns about all other peers, their
                   tags, streams, and groups
                       │
5. Streams         Consumer discovers matching producers,
                   opens subscriptions automatically
                       │
6. Groups          Node finds peers with matching group keys,
                   forms bonds, joins Raft cluster
                       │
7. Convergence     Network reaches a stable topology where
                   all nodes are connected to the right peers

Step 1: Bootstrap & Gossip

A new node starts with at least one bootstrap peer address. It connects and begins participating in the gossip protocol:

let network = Network::builder(network_id)
    .with_discovery(
        discovery::Config::builder()
            .with_bootstrap(bootstrap_addr)
            .with_tags("matcher")
    )
    .build()
    .await?;

The node immediately:

  • Announces itself via the gossip protocol (/mosaik/announce), broadcasting its PeerEntry (identity, tags, streams, groups)
  • Receives announcements from other peers
  • Triggers a full catalog sync (/mosaik/catalog-sync) with the bootstrap peer to catch up on all known peers

Step 2: Catalog Convergence

The catalog converges through two complementary protocols:

Real-time: Gossip Announcements

Every node periodically re-announces its PeerEntry via iroh-gossip. The announce interval is configurable (default: 15 seconds) with jitter to avoid thundering herds:

announce_interval = 15s
announce_jitter = 0.5  →  actual interval: 7.5s – 22.5s

When a node changes (adds a tag, creates a stream, joins a group), it re-announces immediately.

Catch-up: Full Catalog Sync

When a new node connects, it performs a bidirectional catalog sync with its peer. Both nodes exchange their complete catalogs, and entries are merged:

Node A                          Node B
  │                               │
  │── CatalogSyncRequest ───────► │
  │                               │
  │◄── CatalogSyncResponse ──────│
  │                               │
  │  (both merge received         │
  │   entries into local catalog) │

Signed Entries

Each PeerEntry is cryptographically signed by its owner. This proves authenticity — you can trust that a peer entry’s tags, streams, and groups are genuine, even when received via gossip through intermediaries.

Staleness & Purging

Entries that haven’t been updated within the purge_after duration (default: 300 seconds) are considered stale and hidden from the public catalog API. This ensures departed nodes are eventually removed.

Step 3: Automatic Stream Connections

Once discovery populates the catalog, the Streams subsystem automatically connects producers and consumers:

1. Node A creates Producer<Order>
   → Discovery advertises: "I produce stream 'Order'"

2. Node B creates Consumer<Order>
   → Discovery observes: "Node A produces 'Order'"
   → Consumer worker opens subscription to Node A

3. Data flows: Node A ──[Order]──► Node B

This is fully automatic. The consumer’s background worker monitors the catalog for matching producers and establishes connections as they appear.

Filtering can restrict which connections form:

// Producer only accepts nodes tagged "authorized"
let producer = network.streams().producer::<Order>()
    .accept_if(|peer| peer.tags.contains(&"authorized".into()))
    .build()?;

// Consumer only subscribes to nodes tagged "primary"
let consumer = network.streams().consumer::<Order>()
    .subscribe_if(|peer| peer.tags.contains(&"primary".into()))
    .build();

Step 4: Automatic Group Formation

Groups form through a similar discovery-driven process:

1. Node A joins group with key K
   → Discovery advertises: "I'm in group G" (where G = hash(K, config, ...))

2. Node B joins group with same key K
   → Discovery observes: "Node A is in group G"
   → Bond worker opens connection to Node A

3. Mutual handshake proves both know key K
   → Bond established

4. Raft consensus begins
   → Leader elected among bonded peers
   → Commands can be replicated

The bond handshake uses HMAC over the TLS session secrets combined with the group key. This proves knowledge of the group secret without transmitting it.

Step 5: Late Joiners

A node joining an already-running network catches up automatically:

Stream Catch-up

Consumers connecting to an active producer receive data from the point of subscription. There’s no historical replay — streams are real-time.

Group Catch-up

A node joining an existing Raft group:

  1. Forms bonds with existing members
  2. Receives the current log from peers (distributed across multiple peers for efficiency)
  3. Applies all log entries to bring its state machine up to date
  4. Begins participating normally (can vote, can become leader)

For collections, catch-up can use either log replay or snapshot sync:

  • Log replay (default): Replays the entire log from the beginning
  • Snapshot sync: Transfers a point-in-time snapshot of the state, avoiding log replay for large states

Topology Example

Consider a distributed trading system with three roles:

Tags: "trader"          Tags: "matcher"         Tags: "reporter"
┌──────────┐            ┌──────────┐            ┌──────────┐
│  Node 1  │            │  Node 3  │            │  Node 5  │
│ Producer │──[Order]──►│ Consumer │            │ Consumer │
│ <Order>  │            │ <Order>  │            │  <Fill>  │
└──────────┘            │          │            └──────────┘
                        │ Group:   │──[Fill]──►      ▲
┌──────────┐            │ OrderBook│            ┌──────────┐
│  Node 2  │            │ (Raft)   │            │  Node 6  │
│ Producer │──[Order]──►│          │            │ Consumer │
│ <Order>  │            │ Producer │──[Fill]──►│  <Fill>  │
└──────────┘            │  <Fill>  │            └──────────┘
                        └──────────┘
                        ┌──────────┐
                        │  Node 4  │
                        │ (matcher │
                        │  replica)│
                        └──────────┘

All of this topology forms automatically from:

  • Node 1–2: with_tags("trader"), creates Producer<Order>
  • Node 3–4: with_tags("matcher"), creates Consumer<Order>, joins orderbook group, creates Producer<Fill>
  • Node 5–6: with_tags("reporter"), creates Consumer<Fill>

No node needs to know the addresses of any other node except one bootstrap peer.