Skip to content

Commit

Permalink
Exponential backoff (default to 3 attempts)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucassifoni committed Jan 10, 2025
1 parent 7002da2 commit 8a2b812
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 7 deletions.
39 changes: 32 additions & 7 deletions lib/nodelix/version_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions test/nodelix/version_manager_test.exs
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 8a2b812

Please sign in to comment.