Search documentation of macula

# Macula SDK - RPC Guide

View Source

Request/response over the relay mesh with DHT-based service discovery

## Overview

Macula provides decentralized RPC (remote procedure call). SDK clients advertise procedures and call procedures through their relay connection. Relays handle cross-relay routing via Kademlia DHT procedure discovery.

From the SDK perspective: advertise a handler, call a procedure, get a result.

## Advertising a Procedure

Build the procedure name via `macula_topic` — inline strings are rejected by the client validator.

    %% Advertise an app-tier procedure
    Procedure = macula_topic:app_hope(Realm, Org, App,
                                      <<"math">>, <<"add">>, 1),
    {ok, Ref} = macula:advertise(Client, Procedure, fun(#{a := A, b := B}) ->
        {ok, #{result => A + B}}
    end).

    %% Advertise a realm-tier procedure (e.g. realm authority's check_health)
    HealthProc = macula_topic:realm_hope(Realm, <<"auth">>, <<"check_health">>, 1),
    {ok, Ref} = macula:advertise(Client, HealthProc, fun(_args) ->
        {ok, #{status => <<"ok">>}}
    end, #{
        description => <<"Realm health check">>
    }).

    %% Stop advertising
    ok = macula:unadvertise(Client, Ref).

### What Happens Under the Hood

1. SDK sends `register_procedure` message to connected relay
2. Relay registers the handler in gproc (local lookup)
3. Relay stores procedure in Kademlia DHT (cross-relay discovery)
4. Other relays can now find this procedure via DHT

## Calling a Procedure

    %% Synchronous call with default timeout (5s)
    Procedure = macula_topic:app_hope(Realm, Org, App, <<"math">>, <<"add">>, 1),
    {ok, #{result := 5}} = macula:call(Client, Procedure, #{a => 2, b => 3}).

    %% Call with explicit timeout
    WeatherProc = macula_topic:app_hope(Realm, Org, App,
                                        <<"weather">>, <<"get_current">>, 1),
    {ok, Weather} = macula:call(Client, WeatherProc,
        #{location => <<"berlin">>}, 10000).  %% 10 second timeout

### Call Flow

    Your Node                    Your Relay              Remote Relay
    ─────────                    ──────────              ────────────
    call(math.add, Args)
      └─► CALL message ──────► local gproc lookup
                                  ├─ found locally? ──► invoke handler ──► REPLY
                                  └─ not found?
                                      └─► DHT find_procedure
                                            └─► found on remote relay
                                                 └─► forward via peering ──► invoke ──► REPLY
                                                                                          │
      ◄── REPLY ◄──────────────────────────────────────────────────────────────────────────┘

## Error Handling

    case macula:call(Client, Procedure, Args, Timeout) of
        {ok, Result} ->
            %% Success
            Result;
        {error, no_handler} ->
            %% No node has registered this procedure (local)
            not_available;
        {error, procedure_not_found} ->
            %% No node found via DHT either
            not_available;
        {error, timeout} ->
            %% Handler didn't respond in time
            retry_later;
        {error, cross_relay_error} ->
            %% Remote relay forwarding failed
            retry_later;
        {error, Reason} ->
            logger:warning("RPC failed: ~p", [Reason])
    end.

## Procedure Naming Conventions

See TOPIC_NAMING_GUIDE.md — RPC procedures and pub/sub topics share the same canonical 5-segment format.

Quick summary:

- Procedure names are mesh topics with present-tense names: `{realm}/{publisher}/{publisher}/{domain}/{verb_subject}_v{N}`
- Pick a tier: `realm_hope` (realm authority), `org_hope` (org-spanning), `app_hope` (app-specific)
- Build via `macula_topic` — never inline strings
- IDs in args, never in procedure name
- No CRUD verbs

Examples:

- App: `io.macula/beam-campus/hecate/math/add_v1`
- Realm: `io.macula/_realm/_realm/auth/check_health_v1`

## Best Practices

1. Return `{ok, Result}` from handlers -- consistent with Erlang conventions
2. Keep handlers fast -- spawn workers for heavy computation, reply later
3. Set appropriate timeouts -- default 5s is often too short for external APIs
4. Handle all error cases -- `no_handler`, `timeout`, `cross_relay_error`
5. Idempotent procedures -- calls may be retried on timeout
6. One procedure per handler -- each advertise registers one procedure name

## Provider Selection

When multiple nodes advertise the same procedure, the relay selects a provider:

- Local first -- handlers on the same relay are preferred (zero network hop)
- DHT discovery -- if not local, relay queries Kademlia DHT for remote providers
- Peer fallback -- if DHT miss, relay tries each peered relay sequentially

## How It Works (Relay Side)

For relay operators and contributors: RPC routing, DHT procedure store, cross-relay call forwarding, and provider selection are documented in macula-station.

← Previous Page Topic Naming Convention
Next Page → Distribution Over Mesh