Skip to content

fix: the datagram gateway is its own application, release and image - #514

Merged
Taure merged 2 commits into
mainfrom
fix/dgram-gw-release
Aug 19, 2026
Merged

Taure merged 2 commits into
mainfrom
fix/dgram-gw-release

Conversation

@Taure

@Taure Taure commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Closes #513.

Why a role could never do this

The two-role split was a switch read inside asobi_app:start/2. OTP starts an application's dependencies before its start callback runs, so by the time ASOBI_ROLE was read, the container parsing packets from the internet had already:

  • opened a pool with the tenant credentials (kura, via kura_app:start/2 → ensure_pools())
  • run schema migrations and started job workers, cron and pruner against the shared database (shigoto)
  • bound an HTTP port with no auth subsystem behind it (nova)

None of that can be undone from inside start/2. Only a release that never listed those applications can avoid it.

What this is

asobi_dgram_gw is an OTP application holding the thirteen modules that face the internet plus the codec both roles share. Its dependency list is kernel, stdlib, crypto, telemetry, seki. asobi depends on it; the reverse dependency does not exist, which is what lets the gateway ship without the engine.

Two relx releases follow, and docker build --target gateway produces an image whose entire lib directory is:

asobi_dgram_gw  crypto  kernel  sasl  seki  stdlib  telemetry

Verified by booting it, not by reading the release spec:

started: [asobi_dgram_gw,crypto,seki,telemetry]
kura/nova/shigoto loaded: {false,false,false}
gateway tree: [asobi_dgram_canary,asobi_dgram_link_server,asobi_dgram_rx_sup,
               asobi_dgram_sender,asobi_dgram_table]
canary ready: true

That last line is the one that matters: the readiness canary sends itself a real datagram and waits for it to come back, so the receive loop, the sender, the shards and the binding table all work with no engine anywhere.

Three things the split needed beyond moving files

Telemetry. asobi_dgram_telemetry (in the new app) owns every [asobi, dgram, *] event, because a module in the asobi application is not reachable from a release that does not contain it. asobi_telemetry:events/0 folds that list in, so an operator still attaches to one list and every event still has exactly one definition — link_up is emitted by both roles and defined once. The coverage test now derives the emitted set from both modules, and the drift script scans both trees.

A log formatter. nova_jsonlogger lives inside the nova application; pulling nova in for a formatter would bring cowboy, ranch, erlydtl and routing_tree with it and undo the whole thing. asobi_dgram_gw_logger is the same contract in the smallest form that honours it — one JSON object per line, carrying the fields the observability guide tells operators to query on, encoded with OTP's json. It is total on report contents, because a formatter that raises takes the handler down and the node goes quiet.

Its own sys.config. Four lines. A gateway container must not be handed ${ASOBI_DB_*} just to render a config file it never reads.

Compatibility

  • ASOBI_ROLE=dgram_gw on the engine image still starts the gateway and only the gateway. It still carries the flaw above; the guides now say so plainly instead of claiming an isolation the code could not provide.
  • rebar3 as prod release now needs -n asobi or -n asobi_gw — relx refuses to guess once a project declares more than one release. The Dockerfile is updated; the only other mention in the repo is a snippet about a consumer's own release.
  • The {asobi, [{dgram, ...}]} configuration keys are unchanged, deliberately. asobi_dgram_env moved into the gateway app but still reads and writes the asobi application's env, because those keys are the documented surface and every existing sys.config spells them that way. ELP flags the cross-app read (W0011); it is the same pattern asobi_lua_env already has, and the moduledoc explains it.

For whoever publishes images

This adds a second build target. The guides name it ghcr.io/widgrensit/asobi-dgram. Nothing in this repository publishes either image, so that tag needs building wherever ghcr.io/widgrensit/asobi is built — until it exists, self-hosters can build it themselves with docker build --target gateway, which the guides say.

Checks

fmt --check / xref / dialyzer / elp eqwalize (new modules clean; the 2 in asobi_sup and 1 in asobi_telemetry are pre-existing) / elp lint / eunit 2147, 0 failures / ct 388, 0 failures / ex_doc (2 pre-existing warnings) / all three drift scripts.

Closes #513.

The two-role split was a switch read inside `asobi_app:start/2`, and that is
too late to isolate anything. OTP starts an application's dependencies before
its start callback runs, so by the time `ASOBI_ROLE` was read the container
parsing packets from the internet had already opened a pool with the tenant
credentials (`kura`), run schema migrations and started job workers
(`shigoto`), and bound an HTTP port (`nova`). No code inside `start/2` can
undo any of it. Only a release that never listed those applications can.

So the gateway is one now. `asobi_dgram_gw` is an OTP application holding the
thirteen modules that face the internet plus the codec both roles share, and
its dependency list is kernel, stdlib, crypto, telemetry and the rate limiter.
`asobi` depends on it, never the reverse. Two relx releases follow, and
`docker build --target gateway` produces an image whose whole lib directory is
asobi_dgram_gw, crypto, kernel, sasl, seki, stdlib, telemetry.

Verified by booting it: the application starts standalone, kura, nova and
shigoto are not merely unstarted but not loaded, the full supervision tree
comes up, and the readiness canary round-trips a real datagram through the
receive loop with no engine anywhere.

The split needed three things beyond moving files. The gateway's telemetry
moved with it - `asobi_dgram_telemetry` owns every `[asobi, dgram, *]` event,
because a module in the `asobi` application is not reachable from a release
that does not contain it; `asobi_telemetry:events/0` folds that list in, so an
operator still attaches to one list and every event still has one definition.
It needed a log formatter, because `nova_jsonlogger` lives inside nova and
pulling nova in for a formatter would bring cowboy, ranch and erlydtl with it;
`asobi_dgram_gw_logger` emits the same JSON-per-line contract with the fields
the observability guide documents. And it needed its own sys.config, four
lines long, because a gateway container must not be handed ${ASOBI_DB_*} just
to render a file it never reads.

`ASOBI_ROLE=dgram_gw` on the engine image still starts the gateway and only
the gateway, for deployments already using it - and still carries the flaw
above, which the guides now say plainly rather than claiming an isolation the
code could not provide.

Note for whoever publishes images: this adds a second target. The guides name
it `ghcr.io/widgrensit/asobi-dgram`; nothing in this repository publishes
either image, so that tag needs building wherever `ghcr.io/widgrensit/asobi`
is built. `rebar3 as prod release` now requires `-n asobi` or `-n asobi_gw`,
which relx enforces once a project declares more than one release.
@github-actions

github-actions Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

🟡 Code Coverage — 76.2%

8591 of 11280 lines covered.


🟡 ELP Lint — 192 warnings

210 diagnostics found. See job logs for details.

The gateway release had no CI behind it: the reusable workflow builds no
releases, so nothing checked that the closure this split exists for stays
what it is, and the image the guides name did not get built at all.

Two jobs, and the guard is the point of both.

`gateway-release` in CI assembles both releases on every PR and asserts the
gateway's lib directory: asobi_dgram_gw, crypto, kernel, sasl, seki, stdlib,
telemetry, and none of nova, kura, kura_postgres, shigoto, luerl, nova_auth,
nova_resilience, cowboy or ranch. That property belongs to relx's output
rather than to any module, so a unit test cannot hold it - the closest a test
can get is the gateway app's declared dependency list, which is necessary and
not sufficient. Assembling the engine release alongside keeps the other half
of the spec honest for the same two minutes. Verified both ways: it passes on
the gateway release and fails on the engine one.

`docker-publish-gateway.yml` builds the image locally, runs the same closure
check inside it, and only then pushes. The engine image is deliberately not
built here - it comes from asobi_bundle, because it has to carry the full
extension closure (#471). The gateway's requirement is the exact inverse, so
the bundle is the wrong source for it and this repository is the only place
that defines what belongs in it. That contrast is now in the self-hosting
guide next to the existing provenance note.

Tags follow the retired workflow's scheme, including the {{major}} tag and the
reason it is emitted rather than left orphaned.
@Taure
Taure merged commit c57134f into main Aug 19, 2026
18 checks passed
@Taure
Taure deleted the fix/dgram-gw-release branch August 19, 2026 04:33
Taure added a commit that referenced this pull request Aug 19, 2026
Three names for one artifact, which is two too many: the OTP application is
`asobi_dgram_gw`, the release was `asobi_gw` and the image was `asobi-dgram`.
The hyphen is also wrong twice over - Erlang names use underscores, and the
org's published images use them too.

The release and the image are now both `asobi_dgram`, so a self-hoster reads
one name in the compose file, the Dockerfile and the release directory.

The OTP application keeps `_gw`. Inside it, `asobi_dgram` is already the
codec module that both roles share, and the application is specifically the
gateway half - so the suffix is carrying meaning there rather than noise.

`ghcr.io/widgrensit/asobi-dgram` was published once, from the #514 merge.
It is orphaned now and worth deleting: an image nobody should pull is worse
sitting in the registry than absent from it.
Taure added a commit that referenced this pull request Aug 19, 2026
Three names for one artifact, which is two too many: the OTP application is
`asobi_dgram_gw`, the release was `asobi_gw` and the image was `asobi-dgram`.
The hyphen is also wrong twice over - Erlang names use underscores, and the
org's published images use them too.

The release and the image are now both `asobi_dgram`, so a self-hoster reads
one name in the compose file, the Dockerfile and the release directory.

The OTP application keeps `_gw`. Inside it, `asobi_dgram` is already the
codec module that both roles share, and the application is specifically the
gateway half - so the suffix is carrying meaning there rather than noise.

`ghcr.io/widgrensit/asobi-dgram` was published once, from the #514 merge.
It is orphaned now and worth deleting: an image nobody should pull is worse
sitting in the registry than absent from it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dgram_gw role starts a database pool: the role switch is not a release boundary

1 participant