macula_streamer behaviour (macula v11.1.0)
View SourceBehaviour for supervised streaming RPC providers.
advertise_stream/5 on the raw SDK takes a bare handler fun invoked as Handler(StreamPid, Args) in a transient process spawned per inbound STREAM_OPEN (see the internal macula_station_link advertise_stream/5 — "this link spawns a server-side macula_stream and dispatches Handler(StreamPid, Args) in a transient process"). This is the provider-side counterpart to macula_stream_sink: each inbound stream starts one supervised macula_streamer child (under a simple_one_for_one factory this module owns), threading state through Module:init/1 and Module:handle_open/2, and publishing streaming.started_v1 / streaming.completed_v1 mesh facts around the stream's lifetime.
Sending is push-based and driven from outside the callback: once Module:handle_open/2 has done whatever registration it needs (e.g. stashing self() in a registry keyed by some connection id), any process holding this streamer's pid can call send/2,3 / close/1 on it. This module does not prescribe the discovery mechanism.
For client_stream mode — a consumer pushing chunks INTO the provider, e.g. a batch upload — export the optional handle_chunk/2 callback (mirroring macula_stream_sink's consumer-side callback exactly) and this module drives the same linked-reader recv/2 loop for you, on the provider side. A server_stream-mode module that doesn't export it is unaffected.
A client_stream provider that also needs to hand the consumer a terminal result (not just accept chunks) exports the optional handle_eof/1 callback: called once, when the consumer's own close_send/1 surfaces here as end-of-stream, in place of the default unconditional {stop, normal, State}. Returning {reply, Result, NewState} sets the stream's terminal reply (macula_stream:set_reply/2 for {ok, Value}, set_error/2 for {error, Reason}) so the consumer's own macula:await_reply/1,2 unblocks with it, before stopping. A module that doesn't export handle_eof/1 keeps the exact prior behavior — no reply is ever set, eof just stops the stream.
This is the general-purpose RPC streaming feature (call_stream/5, advertise_stream/5, e.g. a logs.tail_v1-style procedure) — unrelated to content sharing's own chunked-transfer protocol; see macula_feeder / macula_download for that.
Cancel
Stopping this gen_server for any non-normal reason (a crash, the underlying stream dying, Module:handle_open/2/handle_chunk/2 returning a non-normal stop) sends the peer an explicit macula_stream:abort/3 STREAM_ERROR, not just a graceful close — the peer learns the transfer was cancelled/failed instead of mistaking it for an ordinary end-of-stream. A normal stop closes both sides cleanly instead.
Direct-dial
advertise/5,6 registers the handler with the pool's advertise- gossip mechanism only — nothing published lets a caller on another station find this procedure without a route having propagated between the two stations first. advertise_direct/6,7 does that AND publishes a signed procedure_advertisement DHT record naming this pool's currently-connected station as the server — the exact same record type and publish function macula_response:advertise_direct/6,7 uses for plain RPC (a procedure_advertisement does not distinguish RPC from streaming), so a caller using macula_stream_sink:start_link_direct/5,6 can resolve and dial here directly, in one hop, regardless of whether the two stations have a routing edge between them.
Stream I/O
A streamer advertises its procedure with advertise_stream, a function of arity 6, macula:advertise_stream/6 by default, which gets the procedure's auth policy and no other option; advertise_direct/6,7 publishes its DHT record with publish_advertisement, macula_direct_dial:publish_advertisement/5 by default; and each streamer announces its facts with fact_publish, macula:publish/4 by default. Each streamer runs its stream on seven macula_stream:stream_io() functions, recv/2, send/3, close_send/1, close/1, abort/3, set_reply/2 and set_error/2, which are macula:recv/2 and the macula_stream ones by default. advertise/6 and advertise_direct/7 take all of these in their options, the seven as stream_io, checked by macula_stream:stream_io/2, and refuse a function of another arity with function_clause before anything is advertised.
Example
-module(log_tailer_provider).
-behaviour(macula_streamer).
-export([init/1, handle_open/2]).
init(Registry) -> {ok, Registry}.
handle_open(#{topic := Topic}, Registry) ->
Registry ! {tailer_ready, Topic, self()},
{ok, Registry}. {ok, _Sup} = macula_streamer:advertise(Pool, Realm,
<<"logs.tail_v1">>, log_tailer_provider, self()).
%% elsewhere, once the provider has announced its pid:
ok = macula_streamer:send(TailerPid, <<"a log line\n">>).A client_stream-mode provider exports handle_chunk/2 instead, and never calls send/2,3 itself — the consumer is the one pushing:
-module(batch_upload_provider).
-behaviour(macula_streamer).
-export([init/1, handle_open/2, handle_chunk/2]).
init(Parent) -> {ok, {Parent, []}}.
handle_open(_StreamArgs, State) -> {ok, State}.
handle_chunk(Chunk, {Parent, Acc}) ->
{noreply, {Parent, [Chunk | Acc]}}. {ok, _Sup} = macula_streamer:advertise(Pool, Realm,
<<"bulk.ingest">>, batch_upload_provider, self(),
#{mode => client_stream}).
Summary
Types
What each streamer runs its stream on and announces its facts with.
Functions
Advertise Procedure on Pool/Realm. Starts a private factory supervisor for per-stream provider children and registers a dispatch handler with macula:advertise_stream/5,6. Returns the supervisor pid so the caller can supervise it (or ignore it).
As advertise/5. Opts may include announce (default true), mode (default server_stream), auth (the procedure's auth policy, default open, see macula:advertise_stream/6), and reuse_sup — an existing supervisor pid (as returned by a prior advertise/5,6 call) to register the handler again with, without starting a new factory supervisor. Use this for a periodic re-advertise (see advertise_direct/6,7's own doc) — calling plain advertise/5,6 on a timer would leak one orphaned supervisor per tick, since each call otherwise starts a fresh one. The functions a streamer runs on come from Opts too; see "Stream I/O" above.
As advertise/5, and additionally publishes a signed procedure_advertisement DHT record naming this pool's connected station as the server, so macula_stream_sink:start_link_direct/5,6 can resolve and dial here directly. NodeIdentity signs it and must be the node identity key Pool was started with: a caller targets that node_id, and the station knows the pool's connection by it.
As advertise_direct/6, with Opts forwarded BOTH to advertise/6 (so mode/announce/reuse_sup and the functions apply here too, e.g. mode => client_stream) and, without the functions, to the advertisement publish, publish_advertisement in Opts or macula_direct_dial:publish_advertisement/5 (e.g. authorization, the provider authorization an org namespaced procedure needs): each side reads only the keys it recognizes, so one Opts map serves both. reuse_sup matters here specifically: the procedure's DHT record expires with its TTL, and callers reach the provider only through that record, so the provider republishes it — a periodic re-advertise with reuse_sup => Sup (the pid this function returned the first time) registers the handler again and republishes the DHT record without leaking a new supervisor per tick. cert_chain, a 10.x option authorization replaces, is refused with {error, {removed_option, cert_chain}} before the handler is registered.
Close the send side of the stream.
Send a chunk out on the stream this streamer owns.
As send/2, with an explicit encoding.
Stop advertising. Does not stop the factory supervisor returned by advertise/5,6 — callers that want to tear it down should exit(Sup, shutdown) themselves.
Types
-type advertise_opts() :: #{advertise_stream => advertise_stream(), publish_advertisement => publish_advertisement(), fact_publish => macula_lifetime_announcer:publish(), stream_io => macula_stream:stream_io(), atom() => term()}.
What each streamer runs its stream on and announces its facts with.
-type advertise_stream() :: fun((macula:pool(), macula:realm(), macula:procedure(), macula_stream:mode(), fun((pid(), term()) -> ok), map()) -> ok | {error, term()}).
-type functions() :: #{stream_io := macula_stream:stream_io(), fact_publish := macula_lifetime_announcer:publish()}.
-type publish_advertisement() :: fun((macula:pool(), macula:realm(), macula:procedure(), macula_node_keys:node_key(), map()) -> ok | {error, term()}).
Callbacks
Functions
-spec advertise(macula:pool(), macula:realm(), macula:procedure(), module(), term()) -> {ok, pid()} | {error, term()}.
Advertise Procedure on Pool/Realm. Starts a private factory supervisor for per-stream provider children and registers a dispatch handler with macula:advertise_stream/5,6. Returns the supervisor pid so the caller can supervise it (or ignore it).
-spec advertise(macula:pool(), macula:realm(), macula:procedure(), module(), term(), advertise_opts()) -> {ok, pid()} | {error, term()}.
As advertise/5. Opts may include announce (default true), mode (default server_stream), auth (the procedure's auth policy, default open, see macula:advertise_stream/6), and reuse_sup — an existing supervisor pid (as returned by a prior advertise/5,6 call) to register the handler again with, without starting a new factory supervisor. Use this for a periodic re-advertise (see advertise_direct/6,7's own doc) — calling plain advertise/5,6 on a timer would leak one orphaned supervisor per tick, since each call otherwise starts a fresh one. The functions a streamer runs on come from Opts too; see "Stream I/O" above.
-spec advertise_direct(macula:pool(), macula:realm(), macula:procedure(), module(), term(), macula_node_keys:node_key()) -> {ok, pid()} | {error, term()}.
As advertise/5, and additionally publishes a signed procedure_advertisement DHT record naming this pool's connected station as the server, so macula_stream_sink:start_link_direct/5,6 can resolve and dial here directly. NodeIdentity signs it and must be the node identity key Pool was started with: a caller targets that node_id, and the station knows the pool's connection by it.
The DHT publish is best-effort: if it fails, the handler is still advertised and reachable via the ordinary pooled path — direct-dial callers just won't be able to resolve it until a later publish succeeds. "Best-effort" still means the failure is logged, not silently discarded — a caller that only ever calls this once (never retries) has no other way to learn its handler is pooled-only, and "a later publish succeeds" cannot happen if nothing ever tries again.
-spec advertise_direct(macula:pool(), macula:realm(), macula:procedure(), module(), term(), macula_node_keys:node_key(), advertise_opts()) -> {ok, pid()} | {error, term()}.
As advertise_direct/6, with Opts forwarded BOTH to advertise/6 (so mode/announce/reuse_sup and the functions apply here too, e.g. mode => client_stream) and, without the functions, to the advertisement publish, publish_advertisement in Opts or macula_direct_dial:publish_advertisement/5 (e.g. authorization, the provider authorization an org namespaced procedure needs): each side reads only the keys it recognizes, so one Opts map serves both. reuse_sup matters here specifically: the procedure's DHT record expires with its TTL, and callers reach the provider only through that record, so the provider republishes it — a periodic re-advertise with reuse_sup => Sup (the pid this function returned the first time) registers the handler again and republishes the DHT record without leaking a new supervisor per tick. cert_chain, a 10.x option authorization replaces, is refused with {error, {removed_option, cert_chain}} before the handler is registered.
-spec close(pid()) -> ok.
Close the send side of the stream.
Send a chunk out on the stream this streamer owns.
-spec send(pid(), binary() | term(), macula_stream:encoding()) -> ok | {error, term()}.
As send/2, with an explicit encoding.
-spec unadvertise(macula:pool(), macula:realm(), macula:procedure()) -> ok.
Stop advertising. Does not stop the factory supervisor returned by advertise/5,6 — callers that want to tear it down should exit(Sup, shutdown) themselves.