fix(static-sites): retry Bunny zone credential propagation
This commit is contained in:
parent
86964aa187
commit
d9ea6cf9b7
2 changed files with 55 additions and 18 deletions
|
|
@ -11,6 +11,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
@ -21,6 +22,8 @@ from typing import Any
|
||||||
|
|
||||||
MANIFEST_NAME = ".static-site-manifest.json"
|
MANIFEST_NAME = ".static-site-manifest.json"
|
||||||
MANIFEST_VERSION = 1
|
MANIFEST_VERSION = 1
|
||||||
|
BUNNY_RETRY_DELAYS = (1, 2, 4, 8, 16)
|
||||||
|
BUNNY_TRANSIENT_STATUS_CODES = {401, 408, 429, 500, 502, 503, 504}
|
||||||
|
|
||||||
|
|
||||||
class DeployError(RuntimeError):
|
class DeployError(RuntimeError):
|
||||||
|
|
@ -113,6 +116,7 @@ class BunnyStorage:
|
||||||
) -> bytes | None:
|
) -> bytes | None:
|
||||||
url_path = urllib.parse.quote(path, safe="/")
|
url_path = urllib.parse.quote(path, safe="/")
|
||||||
request_headers = {"AccessKey": self.password, **(headers or {})}
|
request_headers = {"AccessKey": self.password, **(headers or {})}
|
||||||
|
for attempt in range(len(BUNNY_RETRY_DELAYS) + 1):
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
f"{self.base_url}/{url_path}",
|
f"{self.base_url}/{url_path}",
|
||||||
data=body,
|
data=body,
|
||||||
|
|
@ -126,11 +130,20 @@ class BunnyStorage:
|
||||||
if missing_ok and exc.code == 404:
|
if missing_ok and exc.code == 404:
|
||||||
return None
|
return None
|
||||||
detail = exc.read().decode("utf-8", errors="replace")
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
|
if (
|
||||||
|
exc.code in BUNNY_TRANSIENT_STATUS_CODES
|
||||||
|
and attempt < len(BUNNY_RETRY_DELAYS)
|
||||||
|
):
|
||||||
|
time.sleep(BUNNY_RETRY_DELAYS[attempt])
|
||||||
|
continue
|
||||||
raise DeployError(
|
raise DeployError(
|
||||||
f"Bunny Storage {method} {path} failed: HTTP {exc.code}: {detail}"
|
f"Bunny Storage {method} {path} failed: HTTP {exc.code}: {detail}"
|
||||||
) from exc
|
) from exc
|
||||||
except urllib.error.URLError as exc:
|
except urllib.error.URLError as exc:
|
||||||
raise DeployError(f"Bunny Storage {method} {path} failed: {exc.reason}") from exc
|
raise DeployError(
|
||||||
|
f"Bunny Storage {method} {path} failed: {exc.reason}"
|
||||||
|
) from exc
|
||||||
|
raise AssertionError("unreachable Bunny Storage retry state")
|
||||||
|
|
||||||
def get_manifest(self) -> bytes | None:
|
def get_manifest(self) -> bytes | None:
|
||||||
return self._request("GET", MANIFEST_NAME, missing_ok=True)
|
return self._request("GET", MANIFEST_NAME, missing_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
from io import BytesIO
|
||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -91,6 +92,29 @@ class StaticSiteTests(unittest.TestCase):
|
||||||
with self.assertRaisesRegex(DeployError, "HTTPS origin"):
|
with self.assertRaisesRegex(DeployError, "HTTPS origin"):
|
||||||
purge_bunny_gateway(endpoint, "example-org", "password")
|
purge_bunny_gateway(endpoint, "example-org", "password")
|
||||||
|
|
||||||
|
@mock.patch.object(static_site.time, "sleep")
|
||||||
|
@mock.patch.object(static_site.urllib.request, "urlopen")
|
||||||
|
def test_storage_retries_transient_new_zone_authentication(
|
||||||
|
self, urlopen: mock.Mock, sleep: mock.Mock
|
||||||
|
) -> None:
|
||||||
|
unauthorized = static_site.urllib.error.HTTPError(
|
||||||
|
"https://storage.example/example/.static-site-manifest.json",
|
||||||
|
401,
|
||||||
|
"Unauthorized",
|
||||||
|
{},
|
||||||
|
BytesIO(b'{"HttpCode":401,"Message":"Unauthorized"}'),
|
||||||
|
)
|
||||||
|
response = mock.MagicMock()
|
||||||
|
response.__enter__.return_value.read.return_value = b"manifest"
|
||||||
|
urlopen.side_effect = [unauthorized, response]
|
||||||
|
storage = static_site.BunnyStorage(
|
||||||
|
"example", "new-password", "storage.example"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(storage.get_manifest(), b"manifest")
|
||||||
|
sleep.assert_called_once_with(1)
|
||||||
|
self.assertEqual(urlopen.call_count, 2)
|
||||||
|
|
||||||
@mock.patch.object(static_site.urllib.request, "urlopen")
|
@mock.patch.object(static_site.urllib.request, "urlopen")
|
||||||
def test_gateway_reuses_storage_password(self, urlopen: mock.Mock) -> None:
|
def test_gateway_reuses_storage_password(self, urlopen: mock.Mock) -> None:
|
||||||
urlopen.return_value.__enter__.return_value = object()
|
urlopen.return_value.__enter__.return_value = object()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue