How to Build a Peer with LibbyDaemon#

This guide shows you how to create a peer using the Libby and the LibbyDaemon base class. You’ll wire a transport, discovery, RPC handlers, and pub/sub with just a few overrides.

Libby is transport-agnostic. Examples below show both ZMQ (peer-to-peer) and RabbitMQ (brokered) transports.


Install (dev)#

python -m venv .venv
source .venv/bin/activate

# editable install
pip install -e .

For RabbitMQ transport, you’ll also need a RabbitMQ server running:

# macOS with Homebrew
brew install rabbitmq
brew services start rabbitmq

# Ubuntu/Debian
sudo apt-get install rabbitmq-server
sudo systemctl start rabbitmq-server

1) What LibbyDaemon gives you#

  • Lifecycle: easy start/stop, and proper termination handling

  • Transport: choose ZMQ (peer-to-peer) or RabbitMQ (brokered) with a single line

  • Discovery: optional periodic HELLO

  • RPC (REQ/RESP): register keys and handle requests in one method

  • Pub/Sub (PUB): register event listeners and subscribe to topics

You subclass LibbyDaemon, override a few config methods and hooks then call .serve().


2) Extending the daemon base class#

ZMQ Transport (default - peer-to-peer)#

from libby.daemon import LibbyDaemon

class MyPeer(LibbyDaemon):
    # Required
    peer_id = "peer-X"

    # ZMQ config (default transport)
    transport = "zmq"  # optional, this is the default
    bind = "tcp://*:5555"
    address_book = {
        "peer-Y": "tcp://127.0.0.1:5556",
    }

    # Optional
    discovery_enabled = True
    discovery_interval_s = 5.0

    # REQ/RESP
    services = {
        "my.service": lambda p: {"ok": True, "echo": p},
    }

    # PUB/SUB
    topics = {
        "alerts.status": lambda payload: print("[X] status:", payload),
    }

    def on_start(self, libby):
        # Manually teach peers about each other (discovery workaround)
        libby.learn_peer_keys("peer-Y", ["service1", "service2"])

if __name__ == "__main__":
    MyPeer().serve()

RabbitMQ Transport (brokered)#

from libby.daemon import LibbyDaemon

class MyPeer(LibbyDaemon):
    # Required
    peer_id = "peer-X"

    # RabbitMQ config
    transport = "rabbitmq"
    rabbitmq_url = "amqp://localhost"  # or "amqp://user:pass@host:5672/"
    group_id = "workers"  # optional

    # No bind or address_book needed - broker handles routing!

    # Optional
    discovery_enabled = True
    discovery_interval_s = 5.0

    # REQ/RESP
    services = {
        "my.service": lambda p: {"ok": True, "echo": p},
    }

    # PUB/SUB
    topics = {
        "alerts.status": lambda payload: print("[X] status:", payload),
    }

    def on_start(self, libby):
        # Manually teach peers about each other (discovery workaround)
        libby.learn_peer_keys("peer-Y", ["service1", "service2"])

if __name__ == "__main__":
    MyPeer().serve()

Note: Currently, you need to manually specify peer keys using learn_peer_keys(). Automatic discovery is not yet fully working with either transport.#

3) A simple peer#

Serves one RPC key perf.echo and subscribes to alerts.status.

import time
from typing import Dict, Any
from libby.daemon import LibbyDaemon

def handle_echo(p: Dict[str, Any]):
    # dict return
    return {"ok": True, "t0": p.get("t0"), "t1": time.time()}

def handle_ping(_p):
    # string
    return "pong"

def handle_answer(_p):
    # number
    return 42

def on_status(payload: Dict[str, Any]) -> None:
    print("[PeerB] alerts.status:", payload)

class PeerB(LibbyDaemon):
    peer_id = "peer-B"
    bind = "tcp://*:5556"
    address_book = {
        "peer-A": "tcp://127.0.0.1:5555",
        "peer-C": "tcp://127.0.0.1:5557",
    }
    discovery_enabled = True
    discovery_interval_s = 2.0

    services = {
        "perf.echo": handle_echo,
        "ping.txt":  handle_ping,
        "answer":    handle_answer,
    }
    topics = {
        "alerts.status": on_status,
    }

if __name__ == "__main__":
    PeerB().serve()

Run:

python peer_b.py

4) Another simple peer#

Sends one REQ to peer-B and publishes a status event. Uses discovery.

import time
from libby.daemon import LibbyDaemon

class PeerA(LibbyDaemon):
    peer_id = "peer-A"
    bind = "tcp://*:5555"
    address_book = {
        "peer-B": "tcp://127.0.0.1:5556",
        "peer-C": "tcp://127.0.0.1:5557",
    }
    discovery_enabled = True
    discovery_interval_s = 2.0

    def on_start(self, libby):
        try:
            if not libby.wait_for_key("peer-B", "perf.echo", timeout_s=2.5):
                libby.learn_peer_keys("peer-B", ["perf.echo", "ping.txt", "answer"])
        except AttributeError:
            pass

        print("[PeerA] asking B: perf.echo …")
        res = libby.rpc("peer-B", "perf.echo", {"t0": time.time()}, ttl_ms=8000)
        print("[PeerA] result:", res)

        libby.publish("alerts.status", {"source": "peer-A", "ok": True})
        print("[PeerA] published alerts.status")

if __name__ == "__main__":
    PeerA().serve()

Run:

python peer_a.py

5) Multi-Key Client (Peer C), kept simple#

import time
from typing import Dict, Any
from libby.daemon import LibbyDaemon

def info(_p: Dict[str, Any]):
    return {"ok": True, "info": "peer-C", "time": time.time()}

def math_add(p: Dict[str, Any]):
    a, b = p.get("a"), p.get("b")
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        return {"ok": False, "error": "need numeric a and b"}
    return {"ok": True, "sum": a + b}

class PeerC(LibbyDaemon):
    peer_id = "peer-C"
    bind = "tcp://*:5557"
    address_book = {
        "peer-A": "tcp://127.0.0.1:5555",
        "peer-B": "tcp://127.0.0.1:5556",
    }
    discovery_enabled = True
    discovery_interval_s = 2.0

    services = {
        "clientC.info": info,
        "math.add":     math_add,
    }

    def on_start(self, libby):
        # Proxy service needing live libby handle
        def echo_proxy(_p: Dict[str, Any]):
            res = libby.rpc("peer-B", "perf.echo", {"t0": time.time()}, ttl_ms=6000)
            return {"ok": True, "forwarded_to": "peer-B", "result": res}

        self.add_service("perf.echo.proxy", echo_proxy)

        print("[PeerC] math.add(2,5) ->",
              libby.rpc(self.peer_id, "math.add", {"a": 2, "b": 5}, ttl_ms=2000))

        libby.publish("alerts.status", {"source": "peer-C", "ok": True})

if __name__ == "__main__":
    PeerC().serve()

6) Transport Selection#

Libby supports two transports that you can switch between with a single line:

ZMQ Transport (default)#

  • Peer-to-peer: Direct socket connections between peers

  • Requires: bind address and address_book with peer locations

  • Best for: Low-latency, direct communication, no infrastructure

  • Setup: No server needed, just run the peers

RabbitMQ Transport#

  • Brokered: All messages go through RabbitMQ server

  • Requires: RabbitMQ server running, rabbitmq_url

  • No address book needed: Broker handles all routing automatically

  • Best for: Dynamic scaling, easier deployment, built-in monitoring

  • Setup: Install and start RabbitMQ server

To switch transports: Just change transport = "zmq" to transport = "rabbitmq" in your peer class. Everything else stays the same!

Group ID (optional)#

Optional group_id for grouping peers:

class MyPeer(LibbyDaemon):
    peer_id = "worker-1"
    transport = "zmq"  # or "rabbitmq"
    group_id = "workers"    # group this peer belongs to
  • RabbitMQ: The group ID is included in the queue name (e.g., libby.group.workers.peer.worker-1) and enables group-aware features.

  • ZMQ: The group ID is stored as metadata and can be used for group-aware routing functions (turn on all devices in a group).


7) Design Notes#

  • No retries (protocol choice): sender waits for ACK and optionally RESP; you can handle retries at the app level if needed.

  • Transport-agnostic: Both ZMQ and RabbitMQ implement Bamboo’s Transport interface. The same peer code works with either transport.

  • Encapsulation goal: application peers only implement business logic (on_req, on_event) and a few config methods.

  • Handlers are payload-only. They receive a Python dict and return anything JSON-serializable. If the return is not a dict, LibbyDaemon auto-wraps it as {"data": <value>}.

  • Discovery workaround: Currently, automatic peer discovery needs manual key learning via libby.learn_peer_keys() in on_start().