fix(submit): 分块上传的预算检查复用一分钟内读过的 blob 大小,存储读不到时回 503 让面板自动重试
This commit is contained in:
13 files changed
+335
-25
No files matched your search
@@ -824,6 +824,7 @@ func (f *fakeRepo) SetAllowlistWake(_ context.Context, n, uuid string, canWake b
|
||||
}
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
// RequestRetire and CancelRetire mirror PGRepo's: the first request time is
|
||||
// kept, a deletion stays a deletion, and only an admin cancels a deletion.
|
||||
func (f *fakeRepo) RequestRetire(_ context.Context, n string, del bool) (RetireState, error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"felis.lolicon.best/internal/submit"
|
||||
)
|
||||
@@ -502,6 +503,9 @@ func (a *API) handleDeleteSubmission(w http.ResponseWriter, r *http.Request) {
|
||||
// errSubmissionsUnavailable is returned when the approval lane is not configured
|
||||
// on this api instance (a nil Submissions service), so the admin/app boundary is
|
||||
// still exercised before the subsystem is wired in.
|
||||
// uploadsStoreRetry is the Retry-After on uploads_store_unavailable.
|
||||
const uploadsStoreRetry = 5 * time.Second
|
||||
|
||||
var errSubmissionsUnavailable = newError(http.StatusServiceUnavailable, "submissions_unavailable",
|
||||
"modpack submission subsystem is not configured")
|
||||
|
||||
@@ -511,7 +515,10 @@ var errSubmissionsUnavailable = newError(http.StatusServiceUnavailable, "submiss
|
||||
// allowance is 403 (the same status the server-resource quota answers with), a
|
||||
// full uploads store (every user's uploads together at their cap, or the volume
|
||||
// short of free space) is 507, and an unconfigured upload transport is 503 (the store this deployment set has no
|
||||
// implemented transport — an honest "not available here", not a client error). Everything
|
||||
// implemented transport — an honest "not available here", not a client error). A
|
||||
// blob store that did not answer the budget check is 503 uploads_store_unavailable
|
||||
// with Retry-After: nothing was written and the same request can be sent again.
|
||||
// Everything
|
||||
// else — including a build.ErrInvalid raised by the pre-CAS build.Validate (a
|
||||
// platform registry/context MISCONFIGURATION, never client input, since every
|
||||
// build input is platform-derived) and a post-CAS Submit hand-off failure — is a
|
||||
@@ -541,6 +548,12 @@ func writeSubmitError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
case errors.Is(err, submit.ErrUploadsUnavailable):
|
||||
writeError(w, r, newError(http.StatusServiceUnavailable, "uploads_unavailable",
|
||||
"modpack upload transport is not configured"))
|
||||
case errors.Is(err, submit.ErrStoreUnavailable):
|
||||
// The budget could not be checked; nothing was written. The panel's upload
|
||||
// loop sends the same request again.
|
||||
log.Printf("api: %s %s: %v", r.Method, r.URL.Path, err)
|
||||
writeError(w, r, newError(http.StatusServiceUnavailable, "uploads_store_unavailable",
|
||||
"the uploads store did not answer; send the request again").retryAfter(uploadsStoreRetry))
|
||||
case errors.Is(err, submit.ErrUploadBusy):
|
||||
writeError(w, r, newError(http.StatusConflict, "upload_busy",
|
||||
"another request is still writing this upload; read where it stands and continue from there"))
|
||||
|
||||
@@ -60,12 +60,16 @@ func TestContextUploadErrors(t *testing.T) {
|
||||
code int
|
||||
want string
|
||||
msg string
|
||||
// retry is the Retry-After the answer must carry, if any.
|
||||
retry string
|
||||
}{
|
||||
{"offset mismatch", &submit.OffsetMismatchError{Received: 12}, 409, "upload_offset_mismatch", "the upload holds 12 bytes"},
|
||||
{"busy", submit.ErrUploadBusy, 409, "upload_busy", ""},
|
||||
{"part too large", fmt.Errorf("submit: write upload part: %w", submit.ErrPartTooLarge), 413, "part_too_large", ""},
|
||||
{"not owned", submit.ErrNotFound, 404, "not_found", ""},
|
||||
{"no part store", submit.ErrUploadsUnavailable, 503, "uploads_unavailable", ""},
|
||||
{"offset mismatch", &submit.OffsetMismatchError{Received: 12}, 409, "upload_offset_mismatch", "the upload holds 12 bytes", ""},
|
||||
{"busy", submit.ErrUploadBusy, 409, "upload_busy", "", ""},
|
||||
{"part too large", fmt.Errorf("submit: write upload part: %w", submit.ErrPartTooLarge), 413, "part_too_large", "", ""},
|
||||
{"not owned", submit.ErrNotFound, 404, "not_found", "", ""},
|
||||
{"no part store", submit.ErrUploadsUnavailable, 503, "uploads_unavailable", "", ""},
|
||||
{"store did not answer", fmt.Errorf("%w: size of the context of sub-3: dial tcp: i/o timeout", submit.ErrStoreUnavailable),
|
||||
503, "uploads_store_unavailable", "send the request again", "5"},
|
||||
} {
|
||||
fs := &fakeSubmissions{chunkErr: tc.err}
|
||||
w := do(appSubAPI(fs).ExternalHandler(), "PUT", "/api/v1/me/submissions/sub-9/context/upload?offset=12", "abcd", nil)
|
||||
@@ -75,6 +79,9 @@ func TestContextUploadErrors(t *testing.T) {
|
||||
if tc.msg != "" && !strings.Contains(w.Body.String(), tc.msg) {
|
||||
t.Errorf("%s: body %s does not say %q", tc.name, w.Body.String(), tc.msg)
|
||||
}
|
||||
if got := w.Header().Get("Retry-After"); got != tc.retry {
|
||||
t.Errorf("%s: Retry-After = %q, want %q", tc.name, got, tc.retry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user