Skip to content

Commit 693e5b1

Browse files
authored
Fix an extensions bug related to channels not sharing assigns (#1843)
We don't know of a devices firmware metadata until it connects for the first time. When it does connect, the `DeviceSocket` will fetch the device and the `firmware_metadata` field will be nil. This device is the one shared with other channels. In `Extensions.Health` we are using `socket.assigns.device.firmware_metadata` which is going to be nil during its first connection to NervesHub, and will continue to exit sending reports and creating Sentry errors. This test replicates the scenario of a device connecting for the first time, then its firmware metadata being updated, and then connecting to the health extension, exposing the error. Sadly, in tests, channels share assigns, so this was much harder to replicate.
1 parent 6371d93 commit 693e5b1

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

lib/nerves_hub/extensions/health.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ defmodule NervesHub.Extensions.Health do
5454
do: {to_string(key), to_string(val)}
5555

5656
# Separate metrics from health report to store in metrics table
57-
metrics = device_status["metrics"]
57+
metrics = device_status["metrics"] || %{}
5858

5959
health_report =
6060
device_status

lib/nerves_hub_web/channels/extensions_channel.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ defmodule NervesHubWeb.ExtensionsChannel do
33

44
alias NervesHub.Extensions
55
alias NervesHub.Helpers.Logging
6+
alias NervesHub.Repo
67
alias Phoenix.PubSub
78
alias Phoenix.Socket.Broadcast
89

910
require Logger
1011

1112
@impl Phoenix.Channel
1213
def join("extensions", extension_versions, socket) do
14+
# the assigns are not shared between channels, so if we don't
15+
# reload the device we are likely to have incorrect data, especially
16+
# after the first connect event
17+
socket = reload_device(socket)
18+
1319
extensions = parse_extensions(socket.assigns.device, extension_versions)
1420
socket = assign(socket, :extensions, extensions)
1521

@@ -115,4 +121,13 @@ defmodule NervesHubWeb.ExtensionsChannel do
115121
end
116122

117123
def handle_info(_msg, socket), do: {:noreply, socket}
124+
125+
defp reload_device(%{assigns: %{device: device}} = socket) do
126+
device =
127+
device
128+
|> Repo.reload()
129+
|> Repo.preload(:product)
130+
131+
assign(socket, :device, device)
132+
end
118133
end

test/nerves_hub_web/channels/extensions_channel_test.exs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ defmodule NervesHubWeb.ExtensionsChannelTest do
22
use NervesHubWeb.ChannelCase
33
use DefaultMocks
44

5+
alias NervesHub.Devices
56
alias NervesHub.Fixtures
67
alias NervesHub.Products
8+
alias NervesHub.Repo
79
alias NervesHub.Support.Utils
810
alias NervesHubWeb.DeviceChannel
911
alias NervesHubWeb.DeviceSocket
@@ -23,6 +25,57 @@ defmodule NervesHubWeb.ExtensionsChannelTest do
2325
assert_push("extensions:get", _extensions)
2426
end
2527

28+
test "joining extensions channel works when the device has connected for the first time" do
29+
user = Fixtures.user_fixture()
30+
org = Fixtures.org_fixture(user)
31+
product = Fixtures.product_fixture(user, org)
32+
33+
{:ok, device} =
34+
Devices.create_device(%{
35+
product_id: product.id,
36+
org_id: org.id,
37+
identifier: Ecto.UUID.generate()
38+
})
39+
40+
%{db_cert: certificate, cert: _cert} = Fixtures.device_certificate_fixture(device)
41+
42+
{:ok, socket} =
43+
connect(DeviceSocket, %{}, connect_info: %{peer_data: %{ssl_cert: certificate.der}})
44+
45+
# simulate the device channel updating the params
46+
params = %{
47+
"nerves_fw_uuid" => Ecto.UUID.generate(),
48+
"nerves_fw_product" => product.name,
49+
"nerves_fw_architecture" => "arm64",
50+
"nerves_fw_version" => "0.0.0",
51+
"nerves_fw_platform" => "test_host"
52+
}
53+
54+
# taken from `DeviceChannel`, I don't love just stealing this, but it will do for now
55+
with {:ok, metadata} <- NervesHub.Firmwares.metadata_from_device(params),
56+
{:ok, device} <- NervesHub.Devices.update_firmware_metadata(device, metadata) do
57+
NervesHub.Devices.firmware_update_successful(device)
58+
end
59+
60+
assert {:ok, ["health"], extensions_channel} =
61+
subscribe_and_join_with_default_device_api_version(
62+
socket,
63+
ExtensionsChannel,
64+
"extensions",
65+
%{"health" => "0.0.1"}
66+
)
67+
68+
push(extensions_channel, "health:attached")
69+
assert_push("health:check", _)
70+
71+
@endpoint.subscribe("device:#{device.id}:extensions")
72+
73+
push(extensions_channel, "health:report", %{"value" => dummy_health_report()})
74+
assert_broadcast("health_check_report", _)
75+
76+
assert Repo.aggregate(Devices.DeviceHealth, :count) == 1
77+
end
78+
2679
test "joining extensions channel suggests attaching geo and health" do
2780
user = Fixtures.user_fixture()
2881
{device, _firmware, _deployment} = device_fixture(user, %{identifier: "123"})
@@ -332,6 +385,16 @@ defmodule NervesHubWeb.ExtensionsChannelTest do
332385
{device, firmware, deployment}
333386
end
334387

388+
def dummy_health_report() do
389+
%{
390+
alarms: %{},
391+
checks: %{},
392+
metadata: %{},
393+
timestamp: "2025-01-20T20:00:37.106480Z",
394+
connectivity: %{}
395+
}
396+
end
397+
335398
defp subscribe_and_join_with_default_device_api_version(socket, channel, topic),
336399
do: subscribe_and_join(socket, channel, topic, %{"device_api_version" => "2.2.0"})
337400

0 commit comments

Comments
 (0)