From b62912b6fe33bc8f08deff9e27527cdfe8b7f86c Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Tue, 27 Oct 2026 20:17:53 -0400 Subject: [PATCH] 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 --- drb-edge-node/app/internal/upload_spool.py | 8 +++++--- drb-edge-node/tests/test_upload_spool.py | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/drb-edge-node/app/internal/upload_spool.py b/drb-edge-node/app/internal/upload_spool.py index ddc2015..351f68d 100644 --- a/drb-edge-node/app/internal/upload_spool.py +++ b/drb-edge-node/app/internal/upload_spool.py @@ -15,8 +15,8 @@ Failure classes (see `classify`): retry network error, timeout, 5xx, 2xx with no URL (C2 accepted the request but storage failed), 401 (a reissued key is on its way - over MQTT/key pickup), 408, 429 - reject any other 4xx — 400 empty file, 403 not approved, 413 too large. + over MQTT/key pickup), 403 (not approved yet), 408, 429 + reject any other 4xx — 400 empty file, 413 too large. Retrying the same bytes cannot change the answer, so the file is deleted with an ERROR log naming the status. @@ -54,7 +54,9 @@ from app.internal.logger import logger SIDECAR_SUFFIX = ".json" UPLOAD_TIMEOUT_SECONDS = 120.0 # 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" diff --git a/drb-edge-node/tests/test_upload_spool.py b/drb-edge-node/tests/test_upload_spool.py index 8976e6d..da518f1 100644 --- a/drb-edge-node/tests/test_upload_spool.py +++ b/drb-edge-node/tests/test_upload_spool.py @@ -61,7 +61,7 @@ def _spool(tmp_path: Path, uploader, clock=None, **kw) -> UploadSpool: (500, None, RETRY), (503, None, RETRY), (400, None, REJECT), - (403, None, REJECT), + (403, None, RETRY), # not approved yet: approval may still come (413, None, REJECT), ]) 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): - _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) assert await recorder.upload_recording(path, "call-1") is None assert not path.exists()