From f0fea0dd3e86fde28ce8a75b30d7af7373716e46 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 18 Feb 2026 10:34:36 -0600 Subject: [PATCH] fix: retry firmware fetch with exponential backoff on network errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Increase max_attempts from 3 to 10 and add custom backoff/1 that uses quadratic growth (attempt² × 60s) capped at 2 hours, so transient network failures retry over ~9 hours rather than being discarded after 3 quick attempts. --- .../firmware_version_fetcher_worker.ex | 7 ++++++- .../firmware_version_fetcher_worker_test.exs | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/towerops/workers/firmware_version_fetcher_worker.ex b/lib/towerops/workers/firmware_version_fetcher_worker.ex index c47fe325..37ea048f 100644 --- a/lib/towerops/workers/firmware_version_fetcher_worker.ex +++ b/lib/towerops/workers/firmware_version_fetcher_worker.ex @@ -5,7 +5,7 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do Runs daily to check for new firmware releases and updates the firmware_releases table. """ - use Oban.Worker, queue: :maintenance, max_attempts: 3 + use Oban.Worker, queue: :maintenance, max_attempts: 10 import SweetXml @@ -13,6 +13,11 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorker do require Logger + @impl Oban.Worker + def backoff(%Oban.Job{attempt: attempt}) do + min(attempt * attempt * 60, 7_200) + end + @rss_url "https://cdn.mikrotik.com/routeros/latest-stable.rss" @changelog_url "https://mikrotik.com/download/changelogs" diff --git a/test/towerops/workers/firmware_version_fetcher_worker_test.exs b/test/towerops/workers/firmware_version_fetcher_worker_test.exs index 6acc4eff..13e3387a 100644 --- a/test/towerops/workers/firmware_version_fetcher_worker_test.exs +++ b/test/towerops/workers/firmware_version_fetcher_worker_test.exs @@ -20,6 +20,26 @@ defmodule Towerops.Workers.FirmwareVersionFetcherWorkerTest do end end + describe "backoff/1" do + test "early attempts have shorter delays than later attempts" do + attempt_1 = FirmwareVersionFetcherWorker.backoff(%Oban.Job{attempt: 1}) + attempt_5 = FirmwareVersionFetcherWorker.backoff(%Oban.Job{attempt: 5}) + assert attempt_1 < attempt_5 + end + + test "backoff is capped at 2 hours" do + high_attempt = FirmwareVersionFetcherWorker.backoff(%Oban.Job{attempt: 100}) + assert high_attempt <= 7_200 + end + + test "all attempts return positive values" do + for attempt <- 1..10 do + backoff = FirmwareVersionFetcherWorker.backoff(%Oban.Job{attempt: attempt}) + assert backoff > 0, "attempt #{attempt} returned non-positive backoff" + end + end + end + describe "parse_rss_feed/1" do test "parses valid RSS feed XML" do valid_rss = """