Skip to content

Commit bc7d32f

Browse files
committed
Add fallback region for rpt s3
1 parent 2fcc8be commit bc7d32f

File tree

2 files changed

+126
-7
lines changed
  • lib/remote_persistent_term/fetcher
  • test/remote_persistent_term/fetcher

2 files changed

+126
-7
lines changed

lib/remote_persistent_term/fetcher/s3.ex

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
99
@type t :: %__MODULE__{
1010
bucket: String.t(),
1111
key: String.t(),
12-
region: String.t()
12+
region: String.t(),
13+
failover_region: String.t() | nil
1314
}
14-
defstruct [:bucket, :key, :region]
15+
defstruct [:bucket, :key, :region, :failover_region]
1516

1617
@opts_schema [
1718
bucket: [
@@ -28,6 +29,11 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
2829
type: :string,
2930
required: true,
3031
doc: "The AWS region of the s3 bucket."
32+
],
33+
failover_region: [
34+
type: :string,
35+
required: false,
36+
doc: "The AWS region to use if calls to the default region fail."
3137
]
3238
]
3339

@@ -50,7 +56,8 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
5056
%__MODULE__{
5157
bucket: valid_opts[:bucket],
5258
key: valid_opts[:key],
53-
region: valid_opts[:region]
59+
region: valid_opts[:region],
60+
failover_region: valid_opts[:failover_region]
5461
}}
5562
end
5663
end
@@ -94,7 +101,7 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
94101
res =
95102
state.bucket
96103
|> ExAws.S3.get_bucket_object_versions(prefix: state.key)
97-
|> aws_client_request(state.region)
104+
|> aws_client_request(state)
98105

99106
with {:ok, %{body: %{versions: versions}}} <- res do
100107
{:ok, versions}
@@ -104,7 +111,7 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
104111
defp get_object(state) do
105112
state.bucket
106113
|> ExAws.S3.get_object(state.key)
107-
|> aws_client_request(state.region)
114+
|> aws_client_request(state)
108115
end
109116

110117
defp find_latest([_ | _] = contents) do
@@ -123,8 +130,17 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
123130

124131
defp find_latest(_), do: {:error, :not_found}
125132

126-
defp aws_client_request(op, region) do
127-
client().request(op, region: region)
133+
defp aws_client_request(op, %{region: region, failover_region: nil}),
134+
do: client().request(op, region: region)
135+
136+
defp aws_client_request(op, %{region: region, failover_region: failover_region}) do
137+
with {:error, reason} <- client().request(op, region: region) do
138+
Logger.error(
139+
"Failed to fetch from primary region #{region}: #{inspect(reason)}, will try failover region #{failover_region}"
140+
)
141+
142+
client().request(op, region: failover_region)
143+
end
128144
end
129145

130146
defp client, do: Application.get_env(:remote_persistent_term, :aws_client, ExAws)

test/remote_persistent_term/fetcher/s3_test.exs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ defmodule RemotePersistentTerm.Fetcher.S3Test do
55
setup :verify_on_exit!
66
import ExUnit.CaptureLog
77

8+
@bucket "test-bucket"
9+
@key "test-key"
10+
@region "test-region"
11+
@failover_region "failover-region"
12+
@version "F76V.weh4uOlU15f7a2OLHPgCLXkDpm4"
13+
814
test "Unknown error returns an error for current_version/1" do
915
expect(AwsClientMock, :request, fn _op, _opts ->
1016
{:error, :unknown_error}
@@ -26,4 +32,101 @@ defmodule RemotePersistentTerm.Fetcher.S3Test do
2632
S3.init(bucket: bucket, key: key, region: region)
2733
end
2834
end
35+
36+
describe "failover_region" do
37+
test "current_identifiers/1 tries failover region when primary region fails" do
38+
# Setup state with failover region
39+
state = %S3{
40+
bucket: @bucket,
41+
key: @key,
42+
region: @region,
43+
failover_region: @failover_region
44+
}
45+
46+
# Mock the AWS client to fail for primary region but succeed for failover region
47+
expect(AwsClientMock, :request, 2, fn _op, opts ->
48+
case opts do
49+
[region: @region] ->
50+
{:error, "Primary region connection error"}
51+
52+
[region: @failover_region] ->
53+
{:ok,
54+
%{
55+
body: %{
56+
versions: [
57+
%{version_id: @version, etag: "current-etag", is_latest: "true"}
58+
]
59+
}
60+
}}
61+
end
62+
end)
63+
64+
log =
65+
capture_log(fn ->
66+
result = S3.current_version(state)
67+
assert {:ok, "current-etag"} = result
68+
end)
69+
70+
assert log =~ "Failed to fetch from primary region #{@region}"
71+
assert log =~ "will try failover region #{@failover_region}"
72+
end
73+
74+
test "download/1 tries failover region when primary region fails" do
75+
state = %S3{
76+
bucket: @bucket,
77+
key: @key,
78+
region: @region,
79+
failover_region: @failover_region
80+
}
81+
82+
# Mock the AWS client to fail for primary region but succeed for failover region
83+
expect(AwsClientMock, :request, 2, fn _op, opts ->
84+
case opts do
85+
[region: @region] ->
86+
{:error, "Primary region connection error"}
87+
88+
[region: @failover_region] ->
89+
{:ok, %{body: "content from failover region"}}
90+
end
91+
end)
92+
93+
log =
94+
capture_log(fn ->
95+
result = S3.download(state)
96+
assert {:ok, "content from failover region"} = result
97+
end)
98+
99+
assert log =~ "Failed to fetch from primary region #{@region}"
100+
assert log =~ "will try failover region #{@failover_region}"
101+
end
102+
103+
test "returns error when both primary and failover regions fail" do
104+
state = %S3{
105+
bucket: @bucket,
106+
key: @key,
107+
region: @region,
108+
failover_region: @failover_region
109+
}
110+
111+
# Mock the AWS client to fail for both regions
112+
expect(AwsClientMock, :request, 2, fn _op, opts ->
113+
case opts do
114+
[region: @region] ->
115+
{:error, "Primary region connection error"}
116+
117+
[region: @failover_region] ->
118+
{:error, "Failover region connection error"}
119+
end
120+
end)
121+
122+
log =
123+
capture_log(fn ->
124+
result = S3.download(state)
125+
assert {:error, _} = result
126+
end)
127+
128+
assert log =~ "Failed to fetch from primary region #{@region}"
129+
assert log =~ "will try failover region #{@failover_region}"
130+
end
131+
end
29132
end

0 commit comments

Comments
 (0)