Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Runtime configuration is read from environment variables in `config/runtime.exs`
| `MN_NODE_GPU` | Auto-detected | Optional override for whether this runtime node advertises GPU capacity. |
| `MN_CORE_HOST` | `localhost` | Host/IP used by the gRPC listener. |
| `MN_GRPC_PORT` | `50051` | gRPC port. |
| `MN_API_ENABLED` | `true` | Enables API-related runtime config. |
| `MN_API_ENABLED` | `true` | Enables API-related runtime config, including the gRPC control-plane listener. Set to `false` to prevent the listener from starting. |
| `MN_API_PORT` | `4000` | Core API config port. The separate `mn-api` package uses its own defaults. |
| `MN_TEMP_DIR` | `/tmp/mirror_neuron` | Temporary runtime directory. |
| `MN_OPENSHELL_BIN` | `openshell` | OpenShell executable path or command name. |
Expand Down Expand Up @@ -297,7 +297,7 @@ MirrorNeuron Core includes protobuf definitions and generated Elixir modules for
- `proto/cluster.proto`
- `proto/observability.proto`

Generated modules live under `lib/mirror_neuron_grpc/`.
Generated modules live under `lib/mirror_neuron_grpc/`. The gRPC listener is controlled by `MN_API_ENABLED` and binds to `MN_CORE_HOST`, which defaults to loopback-only `localhost`.

The separate REST API package is maintained in [`mn-api`](https://github.com/MirrorNeuronLab/mn-api). The Python SDK is maintained in [`mn-python-sdk`](https://github.com/MirrorNeuronLab/mn-python-sdk).

Expand Down
27 changes: 18 additions & 9 deletions lib/mirror_neuron/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,13 @@ defmodule MirrorNeuron.Application do

role = node_role()

grpc_port = String.to_integer(System.get_env("MN_GRPC_PORT", "50051"))
grpc_host = System.get_env("MN_CORE_HOST", "localhost")
grpc_bind_opts = grpc_bind_opts(grpc_host)

common_children =
[
{Registry, keys: :duplicate, name: MirrorNeuron.Runtime.EventRegistry},
{Cluster.Supervisor, [topologies, [name: MirrorNeuron.ClusterSupervisor]]},
MirrorNeuron.Redis,
MirrorNeuron.Persistence.Retention,
{GRPC.Server.Supervisor,
[endpoint: MirrorNeuron.Grpc.Endpoint, port: grpc_port, start_server: true] ++
grpc_bind_opts}
]
MirrorNeuron.Persistence.Retention
] ++ grpc_child_specs()

children =
case role do
Expand Down Expand Up @@ -73,6 +66,22 @@ defmodule MirrorNeuron.Application do
System.get_env("MN_NODE_ROLE", "runtime")
end

@doc false
def grpc_child_specs do
if Config.boolean("MN_API_ENABLED", :api_enabled) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip gRPC port validation when API is disabled

When MN_API_ENABLED=false, this gate suppresses the gRPC child, but start/2 still calls Config.validate!/0 first, and that unconditionally validates MN_GRPC_PORT in lib/mirror_neuron/config.ex. In a deployment that disables the API listener and has an intentionally unset/non-port placeholder for MN_GRPC_PORT, the application still fails during startup even though the port will never be used, so the new disable flag does not fully prevent gRPC startup requirements from applying.

Useful? React with 👍 / 👎.

grpc_port = String.to_integer(System.get_env("MN_GRPC_PORT", "50051"))
grpc_host = System.get_env("MN_CORE_HOST", "localhost")

[
{GRPC.Server.Supervisor,
[endpoint: MirrorNeuron.Grpc.Endpoint, port: grpc_port, start_server: true] ++
grpc_bind_opts(grpc_host)}
]
else
[]
end
end

defp grpc_bind_opts(host) when host in ["", "localhost"] do
[ip: {127, 0, 0, 1}]
end
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/application_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule MirrorNeuron.ApplicationTest do
use ExUnit.Case, async: false

setup do
previous_api_enabled_env = System.get_env("MN_API_ENABLED")
previous_core_host_env = System.get_env("MN_CORE_HOST")
previous_grpc_port_env = System.get_env("MN_GRPC_PORT")
previous_api_enabled_config = Application.get_env(:mirror_neuron, :api_enabled)

on_exit(fn ->
restore_env("MN_API_ENABLED", previous_api_enabled_env)
restore_env("MN_CORE_HOST", previous_core_host_env)
restore_env("MN_GRPC_PORT", previous_grpc_port_env)
restore_config(:api_enabled, previous_api_enabled_config)
end)

:ok
end

test "does not start the gRPC control plane when the API is disabled" do
System.put_env("MN_API_ENABLED", "false")

assert MirrorNeuron.Application.grpc_child_specs() == []
end

test "starts the gRPC control plane on loopback when the API is enabled" do
System.put_env("MN_API_ENABLED", "true")
System.put_env("MN_GRPC_PORT", "50055")
System.delete_env("MN_CORE_HOST")

assert [
{GRPC.Server.Supervisor,
[
endpoint: MirrorNeuron.Grpc.Endpoint,
port: 50055,
start_server: true,
ip: {127, 0, 0, 1}
]}
] = MirrorNeuron.Application.grpc_child_specs()
end

defp restore_env(name, nil), do: System.delete_env(name)
defp restore_env(name, value), do: System.put_env(name, value)

defp restore_config(key, nil), do: Application.delete_env(:mirror_neuron, key)
defp restore_config(key, value), do: Application.put_env(:mirror_neuron, key, value)
end
Loading