upload_spool: spool 403 (node not approved yet) instead of deleting (node-26#44)
A node waiting on approval would otherwise lose every call it records. The 512 MiB / 72h spool caps bound what a never-approved node keeps. 400 and 413 are still deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a4a575ec9f
commit
b62912b6fe
@@ -15,8 +15,8 @@ Failure classes (see `classify`):
|
|||||||
|
|
||||||
retry network error, timeout, 5xx, 2xx with no URL (C2 accepted the
|
retry network error, timeout, 5xx, 2xx with no URL (C2 accepted the
|
||||||
request but storage failed), 401 (a reissued key is on its way
|
request but storage failed), 401 (a reissued key is on its way
|
||||||
over MQTT/key pickup), 408, 429
|
over MQTT/key pickup), 403 (not approved yet), 408, 429
|
||||||
reject any other 4xx — 400 empty file, 403 not approved, 413 too large.
|
reject any other 4xx — 400 empty file, 413 too large.
|
||||||
Retrying the same bytes cannot change the answer, so the file is
|
Retrying the same bytes cannot change the answer, so the file is
|
||||||
deleted with an ERROR log naming the status.
|
deleted with an ERROR log naming the status.
|
||||||
|
|
||||||
@@ -54,7 +54,9 @@ from app.internal.logger import logger
|
|||||||
SIDECAR_SUFFIX = ".json"
|
SIDECAR_SUFFIX = ".json"
|
||||||
UPLOAD_TIMEOUT_SECONDS = 120.0
|
UPLOAD_TIMEOUT_SECONDS = 120.0
|
||||||
# Statuses in the 4xx range that are worth retrying unchanged.
|
# Statuses in the 4xx range that are worth retrying unchanged.
|
||||||
RETRYABLE_4XX = {401, 408, 429}
|
# 403 = node not approved yet: approval can arrive later, and the spool caps
|
||||||
|
# bound what a never-approved node keeps.
|
||||||
|
RETRYABLE_4XX = {401, 403, 408, 429}
|
||||||
|
|
||||||
OK, RETRY, REJECT = "ok", "retry", "reject"
|
OK, RETRY, REJECT = "ok", "retry", "reject"
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def _spool(tmp_path: Path, uploader, clock=None, **kw) -> UploadSpool:
|
|||||||
(500, None, RETRY),
|
(500, None, RETRY),
|
||||||
(503, None, RETRY),
|
(503, None, RETRY),
|
||||||
(400, None, REJECT),
|
(400, None, REJECT),
|
||||||
(403, None, REJECT),
|
(403, None, RETRY), # not approved yet: approval may still come
|
||||||
(413, None, REJECT),
|
(413, None, REJECT),
|
||||||
])
|
])
|
||||||
def test_classify(status, url, kind):
|
def test_classify(status, url, kind):
|
||||||
@@ -111,7 +111,7 @@ async def test_successful_upload_deletes_the_file(tmp_path, recorder, monkeypatc
|
|||||||
|
|
||||||
|
|
||||||
async def test_rejected_upload_is_not_spooled(tmp_path, recorder, monkeypatch):
|
async def test_rejected_upload_is_not_spooled(tmp_path, recorder, monkeypatch):
|
||||||
_patch_post(monkeypatch, UploadResult(REJECT, status=403, detail="HTTP 403: Node is not approved"))
|
_patch_post(monkeypatch, UploadResult(REJECT, status=413, detail="HTTP 413: File too large"))
|
||||||
path = _recording(tmp_path)
|
path = _recording(tmp_path)
|
||||||
assert await recorder.upload_recording(path, "call-1") is None
|
assert await recorder.upload_recording(path, "call-1") is None
|
||||||
assert not path.exists()
|
assert not path.exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user