mcl_om_service behaviour (mcl_om v0.2.2)

View Source

The behaviour every hecate-service implements.

Six callbacks are required: info/0, start/1, stop/1, health/0, capabilities/0 and identity_spec/0. The optional callbacks let a service own a reckon-db store, a barrel_docdb read model, mesh subscriptions and human-facing capability descriptions. Health wiring, capability advertisement and the optional stores are handled by the rest of mcl_om; a new service repository (release, Containerfile, CI workflows, compose file) is generated with rebar3 new hecate_service from priv/templates/hecate_service/.

When a service exports the optional store_id/0 and data_dir/0 callbacks, mcl_om:boot/1 does the following before it calls ServiceMod:start/1:

Producer-only services (no event store) omit both callbacks. See mcl_om_store for the helper module.

Summary

Callbacks

Capabilities this service exposes, to be advertised on the mesh. Other services find this one by these names. A capability whose map includes handler => {HandlerModule, Args} (HandlerModule implementing the macula_response behaviour) is advertised via macula_response:advertise_direct/7 -- discoverable AND directly callable. A capability with no handler key is written as a bare discovery record only (today's behavior, kept for services that advertise a capability another mechanism serves).

OPTIONAL. The on-disk root for this service's reckon-db store. The store data lands at data_dir/store_id.

OPTIONAL. Human-facing documentation for the pubsub topics this service publishes or subscribes to -- topic name, a plain-language description, and optionally the payload shape. Distinct from subscriptions/0, which wires actual subscriber processes and carries no description. See describe_rpc_capabilities/0's own doc for how this is exposed on the mesh once exported.

OPTIONAL. Human-facing documentation for this service's RPC capabilities -- name, a plain-language description, and optionally the params a caller sends / an example payload. Distinct from capabilities/0's own dispatch-wiring metadata (name/version/handler/auth/kind), which says how to route a call, never what it does. When exported (alongside either this or describe_pubsub_capabilities/0), mcl_om:boot/1 advertises a synthetic <service-name>.describe_capabilities RPC that returns both lists live -- so a mesh consumer (e.g. a tooling catalog) can call it instead of hand-maintaining descriptions.

Snapshot of current health. Called every /health hit.

UCAN this service wants minted by hecate-realm at boot. Until UCAN-delegation lands in realm, this is informational only.

Static metadata about the service. Reported on /health.

OPTIONAL. The barrel_docdb database name this service's read model lives in (lowercase alphanumerics/underscore/hyphen, 1-63 chars — see barrel_docdb:validate_db_name/1). When exported alongside data_dir/0, mcl_om:boot/1 opens the database at data_dir/read_model_id before calling ServiceMod:start/1. PRJ code writes to it with barrel_docdb directly, using this same name as the database handle — there is no separate accessor to call first. Independent of store_id/0: a service may have a read model, an event store, both, or neither.

OPTIONAL. The barrel_docdb TTL sweep config for this service's read model: disabled (default, no automatic expiry -- today's behavior for every service), or #{interval_ms := pos_integer(), batch := pos_integer()} to turn on barrel_docdb's native per-document TTL sweeper. interval_ms is how often it folds the expiry index; batch caps how many due documents it hard-deletes per pass. This only arms the sweeper -- a document still needs expires_at set (a unix-ms deadline) in its own barrel_docdb:put_doc/3 Opts to ever expire; omitting it on a write preserves whatever expiry the document already had. When exported alongside read_model_id/0 + data_dir/0, mcl_om:boot/1 threads this into the database's create_db config. Omit for a read model where nothing expires.

Start the service's supervision tree. Called once on boot.

Stop the service. Called on shutdown.

OPTIONAL. The reckon-db store_id this service owns. When exported alongside data_dir/0, mcl_om:boot/1 auto-starts the store and the per-store evoq subscription before the service module's own start/1 fires.

OPTIONAL. The reckon-db secondary index declarations this service's store maintains, e.g. [tags, event_type, {payload, <<"plate">>}, {payload_hash, [<<"lot_id">>, <<"plate">>]}]. When exported alongside store_id/0 + data_dir/0, mcl_om:boot/1 installs these on the auto-started store so CCC payload indexes are declared. Omit for a store with no secondary indexes.

OPTIONAL. The reckon-db integrity config for the store: disabled (default), or #{enabled => true, key_source => {env_var, Name} | {sealed_file, Path}} to enable per-store HMAC event tamper-resistance. When exported, mcl_om:boot/1 threads it into the store config. The store refuses to start if integrity is enabled but the key cannot be loaded, so provision the key before enabling.

OPTIONAL. The reckon-db store mode: single (default) or cluster. cluster makes reckon-db discover peers and form a Ra cluster across every node that starts the same store_id (RF = number of such nodes). When exported alongside store_id/0 + data_dir/0, mcl_om:boot/1 auto-starts the store in this mode. Omit for a standalone single-node store.

OPTIONAL. Topics this service subscribes to at boot: a list of {Topic, HandlerModule, Args} triples, HandlerModule implementing the macula_subscriber behaviour. mcl_om:boot/1 wires each into a supervised macula_subscriber under mcl_om_pubsub_sup before the service module's own start/1 runs. Call mcl_om_pubsub:ensure_subscriptions/1 again whenever the desired set changes at runtime (e.g. a new topic per newly-registered entity) -- it diffs against what's currently running and starts/stops only the delta.

Types

capability()

-type capability() ::
          #{name := binary(),
            version := pos_integer(),
            handler => {module(), term()},
            auth => open | {ucan_required, <<_:256>>} | {realm_member_required, <<_:256>>, binary()},
            kind => response | streamer,
            stream_opts => #{mode => server_stream | client_stream}}.

health()

-type health() :: ok | {degraded, term()} | {down, term()}.

identity_spec()

-type identity_spec() ::
          #{scope := binary(),
            actions := [binary()],
            resources := [binary()],
            ttl_days := pos_integer()}.

info()

-type info() :: #{name := binary(), version := binary(), description := binary()}.

pubsub_capability_doc()

-type pubsub_capability_doc() :: #{topic := binary(), description := binary(), payload_shape => map()}.

rpc_capability_doc()

-type rpc_capability_doc() ::
          #{name := binary(), description := binary(), params => [binary()], example_payload => map()}.

Callbacks

capabilities()

-callback capabilities() -> [capability()].

Capabilities this service exposes, to be advertised on the mesh. Other services find this one by these names. A capability whose map includes handler => {HandlerModule, Args} (HandlerModule implementing the macula_response behaviour) is advertised via macula_response:advertise_direct/7 -- discoverable AND directly callable. A capability with no handler key is written as a bare discovery record only (today's behavior, kept for services that advertise a capability another mechanism serves).

data_dir()

(optional)
-callback data_dir() -> string().

OPTIONAL. The on-disk root for this service's reckon-db store. The store data lands at data_dir/store_id.

describe_pubsub_capabilities()

(optional)
-callback describe_pubsub_capabilities() -> [pubsub_capability_doc()].

OPTIONAL. Human-facing documentation for the pubsub topics this service publishes or subscribes to -- topic name, a plain-language description, and optionally the payload shape. Distinct from subscriptions/0, which wires actual subscriber processes and carries no description. See describe_rpc_capabilities/0's own doc for how this is exposed on the mesh once exported.

describe_rpc_capabilities()

(optional)
-callback describe_rpc_capabilities() -> [rpc_capability_doc()].

OPTIONAL. Human-facing documentation for this service's RPC capabilities -- name, a plain-language description, and optionally the params a caller sends / an example payload. Distinct from capabilities/0's own dispatch-wiring metadata (name/version/handler/auth/kind), which says how to route a call, never what it does. When exported (alongside either this or describe_pubsub_capabilities/0), mcl_om:boot/1 advertises a synthetic <service-name>.describe_capabilities RPC that returns both lists live -- so a mesh consumer (e.g. a tooling catalog) can call it instead of hand-maintaining descriptions.

health()

-callback health() -> health().

Snapshot of current health. Called every /health hit.

identity_spec()

-callback identity_spec() -> identity_spec().

UCAN this service wants minted by hecate-realm at boot. Until UCAN-delegation lands in realm, this is informational only.

info()

-callback info() -> info().

Static metadata about the service. Reported on /health.

read_model_id()

(optional)
-callback read_model_id() -> binary().

OPTIONAL. The barrel_docdb database name this service's read model lives in (lowercase alphanumerics/underscore/hyphen, 1-63 chars — see barrel_docdb:validate_db_name/1). When exported alongside data_dir/0, mcl_om:boot/1 opens the database at data_dir/read_model_id before calling ServiceMod:start/1. PRJ code writes to it with barrel_docdb directly, using this same name as the database handle — there is no separate accessor to call first. Independent of store_id/0: a service may have a read model, an event store, both, or neither.

read_model_ttl_sweep()

(optional)
-callback read_model_ttl_sweep() -> disabled | #{interval_ms := pos_integer(), batch := pos_integer()}.

OPTIONAL. The barrel_docdb TTL sweep config for this service's read model: disabled (default, no automatic expiry -- today's behavior for every service), or #{interval_ms := pos_integer(), batch := pos_integer()} to turn on barrel_docdb's native per-document TTL sweeper. interval_ms is how often it folds the expiry index; batch caps how many due documents it hard-deletes per pass. This only arms the sweeper -- a document still needs expires_at set (a unix-ms deadline) in its own barrel_docdb:put_doc/3 Opts to ever expire; omitting it on a write preserves whatever expiry the document already had. When exported alongside read_model_id/0 + data_dir/0, mcl_om:boot/1 threads this into the database's create_db config. Omit for a read model where nothing expires.

start/1

-callback start(map()) -> {ok, pid()} | {error, term()}.

Start the service's supervision tree. Called once on boot.

stop/1

-callback stop(term()) -> ok.

Stop the service. Called on shutdown.

store_id()

(optional)
-callback store_id() -> atom().

OPTIONAL. The reckon-db store_id this service owns. When exported alongside data_dir/0, mcl_om:boot/1 auto-starts the store and the per-store evoq subscription before the service module's own start/1 fires.

store_indexes()

(optional)
-callback store_indexes() -> [term()].

OPTIONAL. The reckon-db secondary index declarations this service's store maintains, e.g. [tags, event_type, {payload, <<"plate">>}, {payload_hash, [<<"lot_id">>, <<"plate">>]}]. When exported alongside store_id/0 + data_dir/0, mcl_om:boot/1 installs these on the auto-started store so CCC payload indexes are declared. Omit for a store with no secondary indexes.

store_integrity()

(optional)
-callback store_integrity() -> disabled | map().

OPTIONAL. The reckon-db integrity config for the store: disabled (default), or #{enabled => true, key_source => {env_var, Name} | {sealed_file, Path}} to enable per-store HMAC event tamper-resistance. When exported, mcl_om:boot/1 threads it into the store config. The store refuses to start if integrity is enabled but the key cannot be loaded, so provision the key before enabling.

store_mode()

(optional)
-callback store_mode() -> single | cluster.

OPTIONAL. The reckon-db store mode: single (default) or cluster. cluster makes reckon-db discover peers and form a Ra cluster across every node that starts the same store_id (RF = number of such nodes). When exported alongside store_id/0 + data_dir/0, mcl_om:boot/1 auto-starts the store in this mode. Omit for a standalone single-node store.

subscriptions()

(optional)
-callback subscriptions() -> [{binary(), module(), term()}].

OPTIONAL. Topics this service subscribes to at boot: a list of {Topic, HandlerModule, Args} triples, HandlerModule implementing the macula_subscriber behaviour. mcl_om:boot/1 wires each into a supervised macula_subscriber under mcl_om_pubsub_sup before the service module's own start/1 runs. Call mcl_om_pubsub:ensure_subscriptions/1 again whenever the desired set changes at runtime (e.g. a new topic per newly-registered entity) -- it diffs against what's currently running and starts/stops only the delta.