From 8a2b812e8e5bb33b520782984b433042bca9a745 Mon Sep 17 00:00:00 2001 From: Chantepierre Date: Fri, 10 Jan 2025 15:29:45 +0100 Subject: [PATCH] Exponential backoff (default to 3 attempts) --- lib/nodelix/version_manager.ex | 39 +++++++++++++---- test/nodelix/version_manager_test.exs | 60 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 test/nodelix/version_manager_test.exs diff --git a/lib/nodelix/version_manager.ex b/lib/nodelix/version_manager.ex index 1090ad7..19f9207 100644 --- a/lib/nodelix/version_manager.ex +++ b/lib/nodelix/version_manager.ex @@ -55,15 +55,17 @@ defmodule Nodelix.VersionManager do @doc """ Installs the specified Node.js version. + Setting max_attempts to an integer greater than 1 will make the process sleep for + 1500ms * attempt_n^2, so : 1.5s, 6s, 13.5s, 24s... """ - @spec install(String.t(), String.t()) :: :ok - def install(version, archive_base_url \\ @default_archive_base_url) + @spec install(String.t(), String.t(), integer()) :: :ok + def install(version, archive_base_url \\ @default_archive_base_url, max_attempts \\ 3) when is_binary(version) and is_binary(archive_base_url) do %{nodelix: base_path} = paths(version) File.mkdir_p!(base_path) - fetch_archive(version, archive_base_url) + :ok = fetch_archive(version, archive_base_url, max_attempts, &HttpUtils.fetch_body!/1) fetch_checksums(version) verify_archive!(version) unpack_archive(version) @@ -187,13 +189,36 @@ defmodule Nodelix.VersionManager do computed_checksum == checksum or raise "invalid checksum" end - defp fetch_archive(version, archive_base_url) do + defp fetch_archive(version, _url, max_retries, tries, _fetch_fun) when tries >= max_retries do + Logger.debug("[Nodelix] Fetching node #{version}  failed after #{tries} attempts.") + {:error, :max_retries_exceeded} + end + + defp fetch_archive(version, archive_base_url, max_retries, tries, fetch_fun) do archive_url = get_url(archive_base_url, version) %{archive: archive_path} = paths(version) - Logger.debug("Downloading Node.js from #{archive_url}") - binary = HttpUtils.fetch_body!(archive_url) - File.write!(archive_path, binary, [:binary]) + Logger.debug("[Nodelix] Downloading Node.js from #{archive_url}") + + try do + binary = fetch_fun.(archive_url) + File.write!(archive_path, binary, [:binary]) + :ok + rescue + _ -> + sleep_time = trunc(1_500 * :math.pow(2, tries)) + + Logger.debug( + "[Nodelix] Fetching node #{version}  failed after #{tries} attempts. New attempt in #{sleep_time}ms" + ) + + Process.sleep(sleep_time) + fetch_archive(version, archive_base_url, max_retries, tries + 1, fetch_fun) + end + end + + def fetch_archive(version, archive_base_url, max_retries, fetch_fun) do + fetch_archive(version, archive_base_url, max_retries, 0, fetch_fun) end defp fetch_checksums(version) do diff --git a/test/nodelix/version_manager_test.exs b/test/nodelix/version_manager_test.exs new file mode 100644 index 0000000..dc119b6 --- /dev/null +++ b/test/nodelix/version_manager_test.exs @@ -0,0 +1,60 @@ +defmodule Nodelix.VersionManagerTest do + use ExUnit.Case, async: true + + setup do + :ets.new(:test_state, [:set, :public, :named_table]) + :ok + end + + @version "14.0.0" + @archive_base_url "https://nodejs.org/dist" + + test "fetch_archive handles max retries exceeded" do + fetch_impl = fn _url -> + raise "Simulated failure" + end + + assert {:error, :max_retries_exceeded} = + Nodelix.VersionManager.fetch_archive( + @version, + @archive_base_url, + 2, + fetch_impl + ) + end + + test "fetch_archive succeeds after a few retries" do + :ets.insert(:test_state, {:fails_remaining, 2}) + + fetch_impl = fn _url -> + [{:fails_remaining, fails}] = :ets.lookup(:test_state, :fails_remaining) + + if fails > 0 do + :ets.insert(:test_state, {:fails_remaining, fails - 1}) + raise "Simulated failure" + else + "Simulated success" + end + end + + assert :ok = + Nodelix.VersionManager.fetch_archive( + @version, + @archive_base_url, + 3, + fetch_impl + ) + end + + test "fetch_archive succeeds immediately" do + fetch_impl = fn _url -> "simulated success" end + + assert :ok = + Nodelix.VersionManager.fetch_archive( + @version, + @archive_base_url, + 3, + fetch_impl + ) + end +end