Skip to content

Commit 34c3b57

Browse files
authored
Track firmware delta generation in more detail (#2235)
Add status field to firmware deltas: - Starting the creation of a firmware delta always creates an entry with status "processing" - Successfully creating the delta set status "completed" - Failure sets status "failed" Add firmware delta generation timeout worker Fetching firmware deltas can now fall back to full because a delta is not done.
1 parent 612dcf4 commit 34c3b57

File tree

11 files changed

+386
-51
lines changed

11 files changed

+386
-51
lines changed

config/config.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ config :nerves_hub, Oban,
6363
delete_firmware: 1,
6464
device: 1,
6565
firmware_delta_builder: 2,
66+
firmware_delta_timeout: 1,
6667
truncate: 1,
6768
# temporary, will remove in November
6869
truncation: 1
@@ -74,6 +75,7 @@ config :nerves_hub, Oban,
7475
crontab: [
7576
{"0 * * * *", NervesHub.Workers.ScheduleOrgAuditLogTruncation},
7677
{"*/1 * * * *", NervesHub.Workers.CleanStaleDeviceConnections},
78+
{"* * * * *", NervesHub.Workers.FirmwareDeltaTimeout},
7779
{"1,16,31,46 * * * *", NervesHub.Workers.DeleteOldDeviceConnections},
7880
{"*/5 * * * *", NervesHub.Workers.ExpireInflightUpdates},
7981
{"*/15 * * * *", NervesHub.Workers.DeviceHealthTruncation}

lib/nerves_hub/devices.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1734,7 +1734,7 @@ defmodule NervesHub.Devices do
17341734
{:delta_updatable, delta_updatable?(device, target)},
17351735
{:firmware, {:ok, source}} <-
17361736
{:firmware, Firmwares.get_firmware_by_product_id_and_uuid(product_id, source_uuid)},
1737-
{:delta, {:ok, delta}} <-
1737+
{:delta, {:ok, %{status: :completed} = delta}} <-
17381738
{:delta, Firmwares.get_firmware_delta_by_source_and_target(source, target)} do
17391739
Logger.info(
17401740
"Delivering firmware delta",
@@ -1766,6 +1766,17 @@ defmodule NervesHub.Devices do
17661766

17671767
Firmwares.get_firmware_url(target)
17681768

1769+
{:delta, {:ok, %{status: status} = delta}} ->
1770+
Logger.info(
1771+
"Delivering full firmware as delta had status #{status}",
1772+
device_id: device.id,
1773+
source_firmware: source_uuid,
1774+
target_firmware: target.uuid,
1775+
delta: delta.id
1776+
)
1777+
1778+
Firmwares.get_firmware_url(target)
1779+
17691780
{:delta, {:error, :not_found}} ->
17701781
Logger.info(
17711782
"Delivering full firmware as no delta can be found",

lib/nerves_hub/firmwares.ex

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule NervesHub.Firmwares do
1313
alias NervesHub.Products
1414
alias NervesHub.Products.Product
1515
alias NervesHub.Workers.DeleteFirmware
16+
alias NervesHub.Workers.FirmwareDeltaBuilder
1617

1718
alias NervesHub.Repo
1819

@@ -391,10 +392,12 @@ defmodule NervesHub.Firmwares do
391392
FirmwareDelta
392393
|> where([fd], source_id: ^source_id)
393394
|> where([fd], target_id: ^target_id)
394-
|> Repo.one()
395+
|> order_by(desc: :inserted_at)
396+
|> limit(1)
397+
|> Repo.all()
395398
|> case do
396-
nil -> {:error, :not_found}
397-
firmware_delta -> {:ok, firmware_delta}
399+
[] -> {:error, :not_found}
400+
[firmware_delta] -> {:ok, firmware_delta}
398401
end
399402
end
400403

@@ -405,11 +408,11 @@ defmodule NervesHub.Firmwares do
405408
firmware_upload_config().download_file(fw_or_delta)
406409
end
407410

408-
@spec create_firmware_delta(Firmware.t(), Firmware.t()) ::
411+
@spec generate_firmware_delta(FirmwareDelta.t(), Firmware.t(), Firmware.t()) ::
409412
:ok
410413
| {:error, Changeset.t()}
411414

412-
def create_firmware_delta(source_firmware, target_firmware) do
415+
def generate_firmware_delta(firmware_delta, source_firmware, target_firmware) do
413416
Logger.info(
414417
"Creating firmware delta between #{source_firmware.uuid} and #{target_firmware.uuid}."
415418
)
@@ -428,16 +431,15 @@ defmodule NervesHub.Firmwares do
428431
with upload_metadata <-
429432
firmware_upload_config().metadata(org.id, firmware_delta_filename),
430433
{:ok, firmware_delta} <-
431-
insert_firmware_delta(%{
432-
source_id: source_firmware.id,
433-
target_id: target_firmware.id,
434-
tool: created.tool,
435-
tool_metadata: created.tool_metadata,
436-
size: created.size,
437-
source_size: created.source_size,
438-
target_size: created.target_size,
439-
upload_metadata: upload_metadata
440-
}),
434+
complete_firmware_delta(
435+
firmware_delta,
436+
created.tool,
437+
created.size,
438+
created.source_size,
439+
created.target_size,
440+
created.tool_metadata,
441+
upload_metadata
442+
),
441443
{:ok, firmware_delta} <- get_firmware_delta(firmware_delta.id),
442444
:ok <-
443445
firmware_upload_config().upload_file(created.filepath, upload_metadata),
@@ -468,8 +470,12 @@ defmodule NervesHub.Firmwares do
468470
)
469471

470472
case result do
471-
{:ok, _delta} -> :ok
472-
{:error, err} -> {:error, err}
473+
{:ok, _delta} ->
474+
:ok
475+
476+
{:error, err} ->
477+
_ = fail_firmware_delta(firmware_delta)
478+
{:error, err}
473479
end
474480

475481
{:error, :no_delta_support_in_firmware} ->
@@ -496,9 +502,82 @@ defmodule NervesHub.Firmwares do
496502
|> preload([d, p], product: p)
497503
end
498504

505+
@spec attempt_firmware_delta(
506+
source_id :: non_neg_integer(),
507+
target_id :: non_neg_integer()
508+
) :: {:ok, FirmwareDelta.t()} | {:error, Ecto.Changeset.t()}
509+
def attempt_firmware_delta(source_id, target_id) do
510+
Repo.transaction(fn ->
511+
with {:error, :not_found} <- get_firmware_delta_by_source_and_target(source_id, target_id),
512+
{:ok, firmware_delta} <- start_firmware_delta(source_id, target_id) do
513+
FirmwareDeltaBuilder.start(source_id, target_id)
514+
firmware_delta
515+
end
516+
end)
517+
end
518+
519+
@spec start_firmware_delta(
520+
source :: Firmware.t() | non_neg_integer(),
521+
target :: Firmware.t() | non_neg_integer()
522+
) :: {:ok, FirmwareDelta.t()} | {:error, Ecto.Changeset.t()}
523+
def start_firmware_delta(%Firmware{id: source_id}, %Firmware{id: target_id}) do
524+
start_firmware_delta(source_id, target_id)
525+
end
526+
527+
def start_firmware_delta(source_id, target_id) do
528+
FirmwareDelta.start_changeset(source_id, target_id)
529+
|> Repo.insert()
530+
end
531+
532+
@spec complete_firmware_delta(
533+
firmware_delta :: FirmwareDelta.t(),
534+
tool :: String.t(),
535+
size :: non_neg_integer(),
536+
source_size :: non_neg_integer(),
537+
target_size :: non_neg_integer(),
538+
tool_metadata :: map(),
539+
upload_metadata :: map()
540+
) :: {:ok, FirmwareDelta.t()} | {:error, Ecto.Changeset.t()}
541+
def complete_firmware_delta(
542+
%FirmwareDelta{} = firmware_delta,
543+
tool,
544+
size,
545+
source_size,
546+
target_size,
547+
tool_metadata,
548+
upload_metadata
549+
) do
550+
firmware_delta
551+
|> FirmwareDelta.complete_changeset(
552+
tool,
553+
size,
554+
source_size,
555+
target_size,
556+
tool_metadata,
557+
upload_metadata
558+
)
559+
|> Repo.update()
560+
end
561+
562+
@spec fail_firmware_delta(FirmwareDelta.t()) ::
563+
{:ok, FirmwareDelta.t()} | {:error, Ecto.Changeset.t()}
564+
def fail_firmware_delta(%FirmwareDelta{} = firmware_delta) do
565+
firmware_delta
566+
|> FirmwareDelta.fail_changeset()
567+
|> Repo.update()
568+
end
569+
570+
@spec time_out_firmware_delta(FirmwareDelta.t()) ::
571+
{:ok, FirmwareDelta.t()} | {:error, Ecto.Changeset.t()}
572+
def time_out_firmware_delta(%FirmwareDelta{} = firmware_delta) do
573+
firmware_delta
574+
|> FirmwareDelta.time_out_changeset()
575+
|> Repo.update()
576+
end
577+
499578
def insert_firmware_delta(params) do
500579
%FirmwareDelta{}
501-
|> FirmwareDelta.changeset(params)
580+
|> FirmwareDelta.create_changeset(params)
502581
|> Repo.insert()
503582
end
504583

@@ -521,6 +600,21 @@ defmodule NervesHub.Firmwares do
521600
end
522601
end
523602

603+
@spec time_out_firmware_delta_generations(
604+
age :: non_neg_integer(),
605+
unit :: :second | :millisecond | :minute
606+
) ::
607+
:ok | {:error, any()}
608+
def time_out_firmware_delta_generations(age_seconds, unit) do
609+
cutoff = DateTime.add(DateTime.utc_now(), -age_seconds, unit)
610+
611+
from(fd in FirmwareDelta,
612+
where: fd.status == :processing,
613+
where: fd.inserted_at < ^cutoff
614+
)
615+
|> Repo.update_all(set: [status: :timed_out])
616+
end
617+
524618
@spec build_firmware_params(Org.t(), Path.t()) :: {:ok, map()} | {:error, any()}
525619
defp build_firmware_params(%{id: org_id} = org, filepath) do
526620
org = NervesHub.Repo.preload(org, :org_keys)

lib/nerves_hub/firmwares/firmware_delta.ex

Lines changed: 118 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,12 @@ defmodule NervesHub.Firmwares.FirmwareDelta do
88
alias __MODULE__
99

1010
@type t :: %__MODULE__{}
11-
@optional_params []
12-
@required_params [
13-
:source_id,
14-
:target_id,
15-
:upload_metadata,
16-
:tool,
17-
:tool_metadata,
18-
:size,
19-
:source_size,
20-
:target_size
21-
]
2211

2312
schema "firmware_deltas" do
2413
belongs_to(:source, Firmware)
2514
belongs_to(:target, Firmware)
2615

16+
field(:status, Ecto.Enum, values: [:processing, :completed, :failed, :timed_out])
2717
field(:tool, :string)
2818
# Metadata about the delta that the update tool needs to operate
2919
field(:tool_metadata, :map)
@@ -35,10 +25,124 @@ defmodule NervesHub.Firmwares.FirmwareDelta do
3525
timestamps()
3626
end
3727

38-
def changeset(%FirmwareDelta{} = firmware_delta, params) do
28+
@spec start_changeset(source_id :: integer(), target_id :: integer()) :: Ecto.Changeset.t()
29+
def start_changeset(source_id, target_id) do
30+
params = %{
31+
status: :processing,
32+
source_id: source_id,
33+
target_id: target_id,
34+
tool: "pending",
35+
tool_metadata: %{},
36+
upload_metadata: %{}
37+
}
38+
39+
%FirmwareDelta{}
40+
|> cast(params, [:status, :source_id, :target_id, :tool, :tool_metadata, :upload_metadata])
41+
|> validate_required([
42+
:status,
43+
:source_id,
44+
:target_id,
45+
:tool,
46+
:tool_metadata,
47+
:upload_metadata
48+
])
49+
|> unique_constraint(:unique_firmware_delta, name: :source_id_target_id_unique_index)
50+
|> foreign_key_constraint(:source_id, name: :firmware_deltas_source_id_fkey)
51+
|> foreign_key_constraint(:target_id, name: :firmware_deltas_target_id_fkey)
52+
end
53+
54+
@spec complete_changeset(
55+
firmware_delta :: FirmwareDelta.t(),
56+
tool :: String.t(),
57+
size :: non_neg_integer(),
58+
source_size :: non_neg_integer(),
59+
target_size :: non_neg_integer(),
60+
tool_metadata :: map(),
61+
upload_metadata :: map()
62+
) :: Ecto.Changeset.t()
63+
def complete_changeset(
64+
%FirmwareDelta{} = firmware_delta,
65+
tool,
66+
size,
67+
source_size,
68+
target_size,
69+
tool_metadata,
70+
upload_metadata
71+
) do
72+
firmware_delta
73+
|> cast(
74+
%{
75+
status: :completed,
76+
tool: tool,
77+
size: size,
78+
source_size: source_size,
79+
target_size: target_size,
80+
tool_metadata: tool_metadata,
81+
upload_metadata: upload_metadata
82+
},
83+
[
84+
:status,
85+
:tool,
86+
:size,
87+
:source_size,
88+
:target_size,
89+
:tool_metadata,
90+
:upload_metadata
91+
]
92+
)
93+
|> validate_required([
94+
:status,
95+
:tool,
96+
:size,
97+
:source_size,
98+
:target_size,
99+
:tool_metadata,
100+
:upload_metadata
101+
])
102+
|> unique_constraint(:unique_firmware_delta, name: :source_id_target_id_unique_index)
103+
|> foreign_key_constraint(:source_id, name: :firmware_deltas_source_id_fkey)
104+
|> foreign_key_constraint(:target_id, name: :firmware_deltas_target_id_fkey)
105+
end
106+
107+
@spec fail_changeset(FirmwareDelta.t()) :: Ecto.Changeset.t()
108+
def fail_changeset(%FirmwareDelta{} = firmware_delta) do
109+
firmware_delta
110+
|> cast(%{status: :failed}, [:status])
111+
|> validate_required([:status])
112+
end
113+
114+
@spec time_out_changeset(FirmwareDelta.t()) :: Ecto.Changeset.t()
115+
def time_out_changeset(%FirmwareDelta{} = firmware_delta) do
116+
firmware_delta
117+
|> cast(%{status: :timed_out}, [:status])
118+
|> validate_required([:status])
119+
end
120+
121+
@spec create_changeset(FirmwareDelta.t(), map()) :: Ecto.Changeset.t()
122+
def create_changeset(%FirmwareDelta{} = firmware_delta, params) do
39123
firmware_delta
40-
|> cast(params, @required_params ++ @optional_params)
41-
|> validate_required(@required_params)
124+
|> cast(params, [
125+
:source_id,
126+
:target_id,
127+
:status,
128+
:upload_metadata,
129+
:tool,
130+
:tool_metadata,
131+
:size,
132+
:source_size,
133+
:target_size
134+
])
135+
|> validate_required([
136+
:source_id,
137+
:target_id,
138+
:status,
139+
:upload_metadata,
140+
:tool,
141+
:tool_metadata,
142+
:size,
143+
:source_size,
144+
:target_size
145+
])
42146
|> unique_constraint(:unique_firmware_delta, name: :source_id_target_id_unique_index)
43147
|> foreign_key_constraint(:source_id, name: :firmware_deltas_source_id_fkey)
44148
|> foreign_key_constraint(:target_id, name: :firmware_deltas_target_id_fkey)

lib/nerves_hub/firmwares/update_tool/fwup.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,10 @@ defmodule NervesHub.Firmwares.UpdateTool.Fwup do
301301
|> Unzip.LocalFile.open()
302302
|> Unzip.new()
303303

304-
unzip
305-
|> Unzip.file_stream!("meta.conf")
306-
|> Enum.into(stream, &IO.iodata_to_binary/1)
304+
_ =
305+
unzip
306+
|> Unzip.file_stream!("meta.conf")
307+
|> Enum.into(stream, &IO.iodata_to_binary/1)
307308

308309
{:ok, path}
309310
rescue

0 commit comments

Comments
 (0)