Do not re-send non-idempotent writes after ambiguous failures in the b2 package
Los mantenedores suelen responder en 1 día
@ffumero2003 ya está trabajando en esto.
Desde el 8/11/2026.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 45/100
Línea de trabajo
Start with b2/backend.go's withBackoff and the write methods listed in the issue, then trace retry classification in base/base.go. Read b2/compose.go and the existing Compose test in b2/compose_extended_wire_test.go for finish cleanup behavior. Run the recording-server cases described in the issue; done means ambiguous writes are not resent, errors carry the known identifiers, and Writer.Close and Compose do not cancel after an ambiguous finish.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Context
This is a follow-up to Backblaze/blazer#167 (merged, closes #165). Since #167, base.Action returns Retry for 408 on non-upload calls and for 502 and 504 on all calls, in addition to 429, 500 and 503. Every control-plane call in the b2 package goes through withBackoff, which re-sends the request whenever base.Action returns Retry, so non-idempotent writes are now also sent again after 408, 502 and 504, as they already were after 500 and after a transport error once the request was sent. A 502 or 504 does not say whether B2 applied the write. When it did, the second attempt either repeats the effect or turns a success into an error:
Client.CreateKey,Bucket.CreateKey: a second key is created; the caller receives only the second key's secret, and the first key stays active until deleted manually.Client.NewBucket:duplicate_bucket_namealthough the bucket was created.WriterandBucket.Composelarge-file start: an extra unfinished large file.Writer.Close(finish): an error although the object exists; withWithCancelOnError, a cancel call on a finished file.Bucket.Compose(finish): an error although the object exists, followed by a deferred cancel of the file it may already have finished.Bucket.Copy(documented at b2/copy.go:109) andObject.Hide: an extra version or hide marker.Object.Delete,Object.Cancel: an error although the operation succeeded.Bucket.Update: a 409 because the revision already moved.
None of these errors says the outcome is unknown, and none carries the identifiers needed to check. This issue asks that the retries be limited to calls that are safe to repeat.
Current behavior
At a5e0d13:
- b2/backend.go:1056-1082:
withBackoffapplies one policy to every method; it retries whenbase.Action(err) == base.Retry(b2/baseline.go:240-242). - Writes wrapped in it:
createBucket(b2/backend.go:329-349),createKey(:375-395),createKeyMultiBucket(:397-417),updateBucket(:450-458),startLargeFile(:518-538),hideFile(:688-708),copyFile(:710-727),deleteFileVersion(:786-794),finishLargeFile(:954-974),cancel(:976-984),Key.del(:1043-1048). - b2/compose.go:237-274:
Composestarts and finishes through the same policy; after a failed finish, the deferred cleanup (:241-248) cancels the large file. b2/compose_extended_wire_test.go:902 expects that cancel. - Classification: base/base.go:94-100 (408 on non-upload calls, and 429, 500, 502, 503 and 504 on all calls, return
Retry), :552-556 (post-send transport errors marked retryable), :213-224 (MaxRetries, 5 for these calls).
Reproduced at a5e0d13 with a recording httptest server that fails the first call once and then succeeds:
status b2_create_key b2_copy_file b2_hide_file b2_update_bucket b2_delete_file_version
408 2, err=nil 2, err=nil 2, err=nil 2, err=nil 2, err=nil
500 2, err=nil 2, err=nil 2, err=nil 2, err=nil 2, err=nil
502 2, err=nil 2, err=nil 2, err=nil 2, err=nil 2, err=nil
504 2, err=nil 2, err=nil 2, err=nil 2, err=nil 2, err=nil
Before #167 (7888c5b), 408, 502 and 504 sent each call once and returned the error. With Compose, a 408, 500, 502 or 504 on the first b2_finish_large_file sends it again; if the second finish fails because the first one was applied (a 400 in the test), Compose returns that error and calls b2_cancel_large_file.
Expected behavior
A write that may have been applied is not repeated automatically, and the caller gets an error that says so, with enough context to check the result. Reads keep the retries from #167.
Acceptance criteria
- R1. A 500, 502 or 504 after an applied
createKey,finishLargeFile,copyFile,hideFile,deleteFileVersionorcreateBucketis not re-sent; the caller gets the unknown-outcome error with the identifiers above. - R2. A dial error before send is still retried, and reads still retry on 408, 500, 502, 503 and 504.
- R3.
Writer.CloseandComposeafter an ambiguous finish return the unknown-outcome error and do not callb2_cancel_large_file.
In scope
Proposed change
- Keep
withBackofffor reads and idempotent updates; add a write policy that retries only when the request provably did not reach B2 (pre-send dial/TLS errors, 429/503), never after a post-send transport error or a bare 500, 502 or 504. If repeatingb2_copy_partwith the same part number is safe (not confirmed here), it keeps the retries from #167. - Return a typed error, for example
*b2.UnknownOutcomeError, withMethodand any knownFileID,KeyName,BucketNameorSourceFileID, usable witherrors.As. - Add reconcile helpers: look up a key by name after an ambiguous
CreateKey; callb2_get_file_infoon the large file ID after an ambiguous finish; treatfile_not_presenton a repeated delete as success only when the first outcome was unknown. - In
Compose, do not cancel after an unknown-outcome finish. - Keep current retries for
b2_upload_file,b2_upload_partand reads.
This works best with a method-aware base.Action, so base can tell callers which failures are safe to retry for each method.
Out of scope
- Unrelated work outside the components and behavior described in In scope.
Verification
Run the repository regression checks with live credentials unset:
unset B2_APPLICATION_KEY_ID B2_APPLICATION_KEY B2_ACCOUNT_ID B2_ACCOUNT_KEY
go build ./...
go vet ./...
go test -race -count=1 ./b2/... ./base
- Script an applied non-idempotent write followed by 500/502/504 or an ambiguous transport failure. Assert the literal request count, unknown-outcome error/identifiers, and absence of cancel after ambiguous finish.
- Prove pre-send retry and read-retry controls still behave as specified; restore each independent regression after its red proof.
- Expected result: the checks above succeed and the observable outcomes in Acceptance criteria are demonstrated. If a check cannot run, report
Could not run because: ...; if it starts but fails, reportCould not complete because: .... A skipped check is not a pass.
Links
Backblaze/blazer#167 (merged, closes #165) added the 408, 502 and 504 retries described above. Backblaze/blazer#101 (open) covers re-authentication on 401 unauthorized, not 5xx or transport retries. Backblaze/blazer#145 (merged, closes #144) synchronized authorization state; a request is still sent again once after a 401. Backblaze/blazer#129 added Compose, and Backblaze/blazer#171 (merged, closes #164) added Compose options and kept its cancel after a failed finish.
- Lenguaje dominante
- Go
- Estrellas
- 66
- Forks
- 18
- Merge medio
- 1 d 10 h
- PR fusionados (30 d)
- 132
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de Backblaze/blazer
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
Bucket.Attrs never returns ReplicationConfig: replication configuration is write-onlyQuizá libre de nuevo Un pull request para esta issue se cerró sin fusionarse. Abiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
test: skip the fuzz non-vacuity guard when replaying a single seedPosiblemente ocupada @ffumero2003 la tomó hace 5 días. Abiertotesting
Backblaze/blazer#330 · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de Backblaze/blazer
Issues similares
-
needs-triage typo
Dificultad 1/5 Menos de una hora Aptitud para principiantes 88/100
llm-d-incubation/llm-d-scaling-manager#122 ·
Los mantenedores suelen responder en 1 día
-
good first issue severity: low tech-debt
Dificultad 2/5 Medio día Aptitud para principiantes 75/100
TMHSDigital/subenum#329 ·
Los mantenedores suelen responder en 1 día
-
[Bug] Backend summarize min() / max() panics when a field has no numeric valuesPosiblemente ocupada @weng5e la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
grafana/grafana-infinity-datasource#1832 ·
Los mantenedores suelen responder en 1 día
-
tech-debt
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
GillMacedo/godebugdiff#13 ·