Skip to content

Declarative multi-listener configuration and per-listener plugins #409

Description

@Taure

#405 gives every listener its own routing table, so binding a second port is possible for the first time. Running through a concrete case — a public API on 8080 and an admin interface on 8081 — the machinery holds up but the developer-facing side does not. Filing separately rather than growing #405, which is already large.

What it takes today

%% sys.config - only the first port lives here
{nova, [{bootstrap_application, my_backend},
        {cowboy_configuration, #{port => 8080}}]}
%% and then, imperatively, somewhere after boot
nova_sup:add_application(my_admin, #{port => 8081}).

Problems, roughly in order of how much they bite

Plugins are node-wide. nova_plugins is a single persistent_term key (nova_router.erl:65), resolved per route at compile time. #405 scoped the routing table to a listener but not the plugin chain, so the admin listener runs the same pre-request chain as the public one. Different auth on the admin port is the main reason to split ports at all, so this is the half that matters most and it is currently missing.

Half the configuration is declarative and half is not. The second port cannot be set in sys.config, so it cannot vary by environment the way the first one can. There is also no natural place to make the call - it ends up in a start phase or wedged into an application's start/2.

No supervision. setup_cowboy/1 runs from init/1 for side effect and only logs on failure (nova_sup.erl:225-235). Nova reports itself started with no admin listener bound. For an admin port that should be a hard boot failure.

No stable listener name. The ref is {nova_listener, my_admin, 8081}. Changing the port changes every log line and every remove_application/1 call along with it.

ip is node-wide. It is a cowboy_configuration key, so the admin port cannot bind to loopback while the public API listens on 0.0.0.0. That is arguably the single most valuable thing a separate admin port buys.

Suggested shape

{nova, [
   {listeners, [
      #{name         => backend,
        port         => 8080,
        applications => [my_backend]},

      #{name         => admin,
        port         => 8081,
        ip           => {127,0,0,1},
        applications => [my_admin],
        plugins      => [{pre_request, my_admin_auth, #{}}]}
   ]}
]}
  • bootstrap_application together with cowboy_configuration desugars into a single entry named default, so existing configurations keep working untouched.
  • Each entry becomes a real supervised child, so a failed bind fails the boot instead of logging.
  • name is chosen by the user and stable. Log lines carry listener => admin, and nova_sup:stop_listener(admin) reads better than matching on host and port.
  • plugins falls back to the global list when absent, so it stays opt-in.
  • add_application/2 remains for genuinely dynamic cases and grows a listener => admin key, so an application can join a named listener instead of being matched onto one by host and port.

Most of the machinery already exists after #405: per-listener dispatch keys, the listener registry, and effective_port/1. What is missing is the declarative front end and per-listener plugin resolution.

Open question

Whether applications => [a, b] on one entry should take over what nova_apps does for the mount-into-my-listener case, leaving nova_apps to mean prefix mounting only. That is a larger change and probably should not ride along with this.

@burbas - would like your read on the config shape before anyone builds it, particularly whether per-listener plugins belong here or should be their own change.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions