Skip to content

fix(quark): recover ambiguous delete failures - #9651

Draft
NovumOrbis wants to merge 5 commits into
AlistGo:mainfrom
NovumOrbis:fix/quark-delete-reliability
Draft

NovumOrbis wants to merge 5 commits into
AlistGo:mainfrom
NovumOrbis:fix/quark-delete-reliability

Conversation

@NovumOrbis

Copy link
Copy Markdown

Problem

Quark can return a transient provider error from /file/delete even when the
delete may already have been accepted.

In a Synology Hyper Backup -> AList -> Quark workload this can surface as a
failed WebDAV DELETE after the requested state may already have been reached.

The observed transient form is narrowly matched as:

inner error, requestId ...

The result is ambiguous: immediately returning the error can report failure
after success, while blindly replaying a destructive request can repeat an
operation that already completed.

Fix

Add bounded recovery for ambiguous Quark delete failures:

  • retry only the narrowly matched inner error ... requestId provider error
  • use at most 3 attempts with bounded, context-aware backoff
  • after an ambiguous delete response, verify the exact immutable FID before
    replaying the delete
  • after an earlier transient, also verify a later non-transient response so an
    already-completed delete is not converted back into a failure
  • keep unrelated and non-transient errors fail-closed

Remove previously did not attach the caller context to the /file/delete
request. This change makes in-flight delete requests honor cancellation as
well as the verification requests, so cancellation behavior changes on the
normal delete path too.

Presence / absence verification

Real-provider testing showed that:

GET /file?fids=<fid>

is not a reliable per-FID existence oracle. A FID independently confirmed
present through /file/sort was omitted from the returned list: the request
succeeded with HTTP 200 / provider status 200 / code 0, returned two list
entries, and contained zero exact matches for that known-present FID.

The recovery path therefore uses:

GET /file/info?fid=<fid>

with fail-closed semantics.

PRESENT is accepted only when all of these are explicit:

  • HTTP request succeeds
  • provider status == 200
  • provider code == 0
  • data exists
  • data.fid exactly equals the requested FID

ABSENT is accepted only for the exact real-provider signature observed for a
real recycle-bin FID:

  • HTTP 404
  • provider status == 404
  • provider code == 21001

A separately tested synthetic invalid FID returned:

  • HTTP 400
  • provider status == 400
  • provider code == 14001

and is treated as an error, not as absence.

Malformed or incomplete envelopes, authentication errors, rate limits,
gateway/server errors, missing data, and mismatched FIDs all fail closed.

The verifier does not infer absence from "file not found" message text.

Request error handling

The Quark request helper previously reduced provider errors to
errors.New(message), losing the HTTP status and provider status/code.

It now preserves those fields in an internal structured error while keeping
Error() equal to the original provider message, so existing callers that use
the error text retain their behavior.

Validation

Regression coverage includes:

  • exact PRESENT response
  • exact real-provider ABSENT signature
  • invalid FID rejection
  • 401 / 403 / 429 responses
  • 5xx and gateway responses
  • malformed JSON
  • missing/null response data
  • missing/null provider status/code
  • explicit invalid provider status values
  • mismatched and empty FIDs
  • incorrect 404/provider-code combinations
  • rejection of message-string-based absence matching
  • verifier request cancellation
  • delete request cancellation
  • cancellation during retry backoff
  • regression protection against reuse of the old /file?fids= oracle

Validated on the final candidate with:

  • gofmt
  • go vet ./drivers/quark_uc
  • go test ./drivers/quark_uc
  • go test -race ./drivers/quark_uc
  • go test -race -shuffle=on -count=2 ./drivers/quark_uc
  • go test ./internal/op
  • go test ./server/webdav

Final Quark driver suite: 68 PASS / 0 FAIL.

Provider evidence / residuals

The PRESENT and recycle-bin ABSENT semantics above were verified directly
against the real provider with GET-only probes.

A permanently-purged FID was not independently probed. If Quark represents
that state differently, the verifier fails closed rather than reporting a
false success.

The shared Resty transport-level retry policy is pre-existing and is not
changed by this PR.

Related fixes from the same Hyper Backup incident:

This PR addresses a separate delete-reliability failure mode.

Recover the observed Quark `inner error, requestId ...` response from
`/file/delete` with a bounded driver-layer retry loop.

After an ambiguous provider error, query the immutable FID before replaying
the destructive request. If the FID is already absent, treat the requested
delete state as satisfied; otherwise retry only the narrowly matched
transient.

Keep unrelated provider and transport errors fail-closed, propagate request
cancellation to the delete call, and cover retry, ambiguity, cancellation,
and exact-FID verification with regression tests.
The delete verifier derived "FID absent" from an empty file list, but
request only classifies errors it can read from the HTTP status line.
A non-JSON gateway page, a 204, or an application-level error delivered
with HTTP 200 therefore reached the verifier as a zero-valued response
and was read as proof of absence, turning a failed delete into a
reported success. That failure is correlated with the exact provider
degradation this path exists to recover from.

Require positive evidence instead: reject a non-zero code or an error
status, and distinguish an explicit empty list from a missing one by
making the list a pointer. Absence is now derived only from a
well-formed file query.

Also propagate the request context to the verifier query so it responds
to cancellation like the delete call already does.

Retry budget, backoff, the error classifier, action_type, the successful
delete path, and the WebDAV layer are unchanged.
Real-provider testing disproved the presence oracle the verifier relied on:
GET /file?fids=<fid> does not answer per-FID existence, so an empty list was
never evidence that a delete had completed.

Resolve the FID through /file/info instead, which answers existence directly.
Presence now requires a successful envelope carrying a data object whose fid
equals the requested one. Absence requires the exact signature observed
against the provider for a FID that no longer exists: HTTP 404 with provider
status 404 and code 21001. Everything else fails closed, including an
invalid-FID rejection, an auth failure, a rate limit, a gateway page,
malformed JSON, a missing data object, and a mismatched fid.

Matching the not-found response needs the provider envelope rather than its
message text, so request failures now carry the HTTP status alongside the
provider status, code and message. Error returns the message unchanged, so
existing callers are unaffected.

The envelope fields are pointers because a missing status or code must not
collapse into a successful zero value.

Retry budget, backoff, the error classifier, action_type, the successful
delete path, context propagation, and the WebDAV layer are unchanged.
Make delete attempts fail closed on malformed or contradictory responses, and
run destructive /file/delete calls through a no-retry Resty wrapper so transport
failures return to the delete state machine before any replay.

Treat transport failures as ambiguous, verify the immutable FID before retrying,
and stop rather than replay if that verification itself fails. Preserve normal
transport/TLS/timeout behavior by reusing the underlying concurrency-safe
http.Client without mutating the shared Resty client.

Also require the actual /file/info HTTP status to be 200 before accepting a
PRESENT result, and add regression coverage for malformed delete responses,
HTTP/provider disagreement, and production-like transport retry composition.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant