Skip to content
Open
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
6 changes: 6 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,9 @@ path = ".github/CODEOWNERS"
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 Seco Mind Srl"
SPDX-License-Identifier = "Apache-2.0"

[[annotations]]
path = "backend/rel/env.sh.eex"
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 Seco Mind Srl"
SPDX-License-Identifier = "Apache-2.0"
7 changes: 6 additions & 1 deletion backend/lib/edgehog/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ defmodule Edgehog.Application do
Router.Helpers.astarte_trigger_url(Endpoint, :process_event, slug)
end

clustering_opts = [Config.clustering_topologies!(), [name: Edgehog.Cluster.Supervisor]]

children = [
# Prometheus metrics
Edgehog.PromEx,
# Start the Ecto repository
Edgehog.Repo,
# Clustering supervisor
{Cluster.Supervisor, clustering_opts},
# Start the Telemetry supervisor
EdgehogWeb.Telemetry,
# Start the PubSub system
Expand All @@ -63,7 +67,8 @@ defmodule Edgehog.Application do
# Start the Tenant Reconciler Supervisor
{Edgehog.Tenants.Reconciler.Supervisor, tenant_to_trigger_url_fun: tenant_to_trigger_url_fun},
# Start Containers reconciler
{Registry, keys: :unique, name: Edgehog.Containers.Reconciler.Registry},
{Horde.Registry, keys: :unique, name: Edgehog.Containers.Reconciler.Registry},
{Horde.Registry, keys: :unique, name: Edgehog.Devices.Reconciler.Registry},
# Start the Endpoint (http/https)
Endpoint,
# Fetch Astarte devices
Expand Down
57 changes: 57 additions & 0 deletions backend/lib/edgehog/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule Edgehog.Config do
"""
use Skogsra

alias Edgehog.Config.ClusteringStrategy
alias Edgehog.Config.GeocodingProviders
alias Edgehog.Config.GeolocationProviders
alias Edgehog.Config.JWTPublicKeyPEMType
Expand Down Expand Up @@ -96,6 +97,26 @@ defmodule Edgehog.Config do
type: GeocodingProviders,
default: [GoogleGeocoding]

@envdoc "The Erlang cluster strategy to use. One of `none`, `kubernetes`, `docker-compose`. Defaults to `none`."
app_env :clustering_strategy, :edgehog, :clustering_strategy,
os_env: "EDGEHOG_CLUSTERING_STRATEGY",
type: ClusteringStrategy,
default: :none

@envdoc "The endpoint label to query to get other edgehog instances. Defaults to `app=edgehog-backend`."
app_env :edgehog_clustering_kubernetes_selector,
:edgehog,
:edgehog_clustering_kubernetes_selector,
os_env: "EDGEHOG_CLUSTERING_KUBERNETES_SELECTOR",
type: :binary,
default: "app=edgehog-backend"

@envdoc "The Kubernetes namespace to use when `kubernetes` Erlang clustering strategy is used. Defaults to `edgehog`."
app_env :clustering_kubernetes_namespace, :edgehog, :clustering_kubernetes_namespace,
os_env: "EDGEHOG_CLUSTERING_KUBERNETES_NAMESPACE",
type: :binary,
default: "edgehog"

@doc """
Returns true if edgehog should use an ssl connection with the database.
"""
Expand Down Expand Up @@ -190,4 +211,40 @@ defmodule Edgehog.Config do
admin_jwk!()
:ok
end

@doc """
The clustering topology to use. The topology sets up node discovery.
"""
def clustering_topologies! do
case clustering_strategy!() do
:none ->
[]

:kubernetes ->
[
edgehog_k8s: [
strategy: Cluster.Strategy.Kubernetes,
config: [
mode: :ip,
kubernetes_node_basename: "edgehog",
kubernetes_selector: edgehog_clustering_kubernetes_selector!(),
kubernetes_namespace: clustering_kubernetes_namespace!(),
polling_interval: 10_000
]
]
]

:docker_compose ->
[
edgehog: [
strategy: Cluster.Strategy.DNSPoll,
config: [
polling_interval: 5_000,
query: "edgehog-backend",
node_basename: "edgehog"
]
]
]
end
end
end
46 changes: 46 additions & 0 deletions backend/lib/edgehog/config/clustering_strategy.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# This file is part of Edgehog.
#
# Copyright 2026 SECO Mind Srl
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#

defmodule Edgehog.Config.ClusteringStrategy do
@moduledoc """
The clustering strategy that the node should use to discover other nodes.
"""

use Skogsra.Type

@strategies [:none, :kubernetes, :docker_compose]
@allowed_strategies ~w(none kubernetes docker-compose)
@strategy_map @allowed_strategies |> Enum.zip(@strategies) |> Map.new()

@impl Skogsra.Type
def cast(value) when is_binary(value) do
Map.fetch(@strategy_map, value)
end

@impl Skogsra.Type
def cast(value) when value in @strategies do
{:ok, value}
end

@impl Skogsra.Type
def cast(_) do
:error
end
end
2 changes: 1 addition & 1 deletion backend/lib/edgehog/containers/reconciler/reconciler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ defmodule Edgehog.Containers.Reconciler do
end

defp name(tenant) do
{:via, Registry, {Edgehog.Containers.Reconciler.Registry, tenant.tenant_id}}
{:via, Horde.Registry, {Edgehog.Containers.Reconciler.Registry, tenant.tenant_id}}
end

defp do_register_device(device, state) do
Expand Down
28 changes: 27 additions & 1 deletion backend/lib/edgehog/devices/reconciler/reconciler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defmodule Edgehog.Devices.Reconciler do
require Logger

def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name())
end

@impl GenServer
Expand Down Expand Up @@ -79,10 +79,36 @@ defmodule Edgehog.Devices.Reconciler do
{:noreply, state}
end

@impl GenServer
def handle_info({:EXIT, _pid, {:name_conflict, {_name, _value}, _registry, _winning_pid}}, state) do
_ =
Logger.warning(
"Received a :name_conflict signal from the outer space, maybe a netsplit occurred? Gracefully shutting down.",
tag: "RPC exit"
)

{:stop, :shutdown, state}
end

@impl GenServer
def handle_info({:EXIT, _pid, :shutdown}, state) do
_ =
Logger.warning(
"Received a :shutdown signal from the outer space, maybe the supervisor is mad? Gracefully shutting down.",
tag: "RPC exit"
)

{:stop, :shutdown, state}
end

defp spawn_reconciliation_task(tenant) do
Task.Supervisor.async(Reconciler.Supervisor, fn ->
Logger.info("Reconciling tenant #{tenant.slug}")
Reconciler.Core.reconcile(tenant)
end)
end

defp name do
{:via, Horde.Registry, {Edgehog.Devices.Reconciler.Registry, :auto}}
end
end
4 changes: 3 additions & 1 deletion backend/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ defmodule Edgehog.MixProject do
{:absinthe_phoenix, "~> 2.0"},
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.24", only: :dev}
{:ex_doc, "~> 0.24", only: :dev},
{:libcluster, "~> 3.5"},
{:horde, "~> 0.10"}
]
end

Expand Down
5 changes: 5 additions & 0 deletions backend/mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"dataloader": {:hex, :dataloader, "1.0.11", "49bbfc7dd8a1990423c51000b869b1fecaab9e3ccd6b29eab51616ae8ad0a2f5", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba0b0ec532ec68e9d033d03553561d693129bd7cbd5c649dc7903f07ffba08fe"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"delta_crdt": {:hex, :delta_crdt, "0.6.5", "c7bb8c2c7e60f59e46557ab4e0224f67ba22f04c02826e273738f3dcc4767adc", [:mix], [{:merkle_map, "~> 0.2.0", [hex: :merkle_map, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c6ae23a525d30f96494186dd11bf19ed9ae21d9fe2c1f1b217d492a7cc7294ae"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
Expand All @@ -46,6 +47,7 @@
"goth": {:hex, :goth, "1.4.5", "ee37f96e3519bdecd603f20e7f10c758287088b6d77c0147cd5ee68cf224aade", [:mix], [{:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "0fc2dce5bd710651ed179053d0300ce3a5d36afbdde11e500d57f05f398d5ed5"},
"guardian": {:hex, :guardian, "2.4.0", "efbbb397ecca881bb548560169922fc4433a05bc98c2eb96a7ed88ede9e17d64", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "5c80103a9c538fbc2505bf08421a82e8f815deba9eaedb6e734c66443154c518"},
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
"horde": {:hex, :horde, "0.10.0", "31c6a633057c3ec4e73064d7b11ba409c9f3c518aa185377d76bee441b76ceb0", [:mix], [{:delta_crdt, "~> 0.6.2", [hex: :delta_crdt, repo: "hexpm", optional: false]}, {:libring, "~> 1.7", [hex: :libring, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 0.5.0 or ~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "0b51c435cb698cac9bf9c17391dce3ebb1376ae6154c81f077fc61db771b9432"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
Expand All @@ -55,11 +57,14 @@
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
"json_xema": {:hex, :json_xema, "0.6.5", "060459c9c9152650edb4427b1acbc61fa43a23bcea0301d200cafa76e0880f37", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.16", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "b8ffdbc2f67aa8b91b44e1ba0ab77eb5c0b0142116f8fbb804977fb939d470ef"},
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"libring": {:hex, :libring, "1.7.0", "4f245d2f1476cd7ed8f03740f6431acba815401e40299208c7f5c640e1883bda", [:mix], [], "hexpm", "070e3593cb572e04f2c8470dd0c119bc1817a7a0a7f88229f43cf0345268ec42"},
"logfmt": {:hex, :logfmt, "3.3.3", "6521ee4a5c532088e15d487fab9f736c07bdd161d643560c73cd4b10685deb65", [:mix], [], "hexpm", "dbd51cd3fe37c3429b9bd687bad1f531a533505f4a641592129e7a47e24104d1"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"},
"merkle_map": {:hex, :merkle_map, "0.2.2", "f36ff730cca1f2658e317a3c73406f50bbf5ac8aff54cf837d7ca2069a6e251c", [:mix], [], "hexpm", "383107f0503f230ac9175e0631647c424efd027e89ea65ab5ea12eeb54257aaf"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
Expand Down
11 changes: 11 additions & 0 deletions backend/rel/env.sh.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh

export RELEASE_DISTRIBUTION=name

if [ -z "$MY_POD_IP" ]
then
HOSTNAME=`hostname -i`
export RELEASE_NODE="<%= @release.name %>@$HOSTNAME"
else
export RELEASE_NODE="<%= @release.name %>@$MY_POD_IP"
fi
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do
timestamp = utc_now_second()
event = disconnection_trigger(device.device_id, timestamp)

Registry.register(Edgehog.Containers.Reconciler.Registry, tenant.tenant_id, self())
Horde.Registry.register(Edgehog.Containers.Reconciler.Registry, tenant.tenant_id, self())

stub(DeviceStatusMock, :get, fn _client, _device_id -> flunk() end)
assert conn |> post(path, event) |> response(200)
Expand Down
Loading