Repository navigation
fix: consume the parts a failed merge assembled from - #7
Conversation
A chunked upload that reached the point of assembling its payload and was then rejected (digest mismatch, declared byte count mismatch) kept every part resource it had consumed. The service drops them together with the refusal; only requests turned down before any part was read — a create whose target already exists, a manifest that names nothing — keep the parts, because nothing was assembled yet. The divergence was visible to clients: the Java SDK reuses deterministic part names (<schema>.<resource>.part.tmp.<index>), so a refused duplicate create leaves a temp resource behind, and any later listing that sweeps for ".part.tmp." blames it on an unrelated upload. That is how the PyODPS contract probe started failing its merge-cleanup case whenever the Java suite had run first against the same instance: the leftover was the Java suite's, and the assertion was project-wide. - merge: delete the declared parts on the post-read verification failures, keep the existing consumption on success, and leave parts alone for the pre-read refusals (now pinned by tests on both sides of that line). - pyodps probe: scope the leftover check to the resource the case uploaded, assert a refused merge consumes its own part instead of the opposite, and let cleanup() sweep parts left by an aborted run of the probe itself. Verified: go test -race -count=1 ./internal/... (server 18.5s, engine 3.1s, wire 1.2s) all ok; PyODPS probe 30/30 against the built binary with a Java-style leftover seeded, and the pre-change probe still reports the old assertions verbatim, so the fix and the expectation moved together.
1b90689 to
ffb1ec9
Compare
|
Re-based onto
Failure on main: The third row is the reason this PR changes both files, and it cuts against "the assertions were loosened to go green":
Still not verified, unchanged from the PR description: whether a real service endpoint consumes the parts of a failed merge. The CI on the rebased head: acceptance success (3m44s, run 35682893201). Main's own push run 35678348028 also went green with the probe step executing ( |
|
Re-checked the server-side premise of this PR tonight against a live MaxCompute service
So the line this PR draws is the line the service draws: a merge that reads parts and Two consequences worth recording, both outside this PR's scope and deliberately not
One operational note for anyone writing a residual-part check: after a refused duplicate Scope of the measurement, so it is not over-read: single part per merge, 2 KiB payloads, |
|
Follow-up to my previous comment: yesterday's live-service run used a single part, and that cannot
With one part A and B give the same answer, so that measurement did not actually pin down which Same script, same cells, only
So the service is A as well: P1 clears every declared part, and in P2/P3 it keeps everything — if it One finding worth re-rating: P4. The service ignores the declared byte count on the multi-part path Scope, so this is not over-read: 3 × 256 B parts (768 B merged), default schema, sequential requests. |
dingxin-tech
left a comment
There was a problem hiding this comment.
This changes the externally visible contract of a failed merge by deleting uploaded part resources after a size or MD5 mismatch. The PR says the previous retention assertion was a guess, but the new consumption behavior also lacks a real MaxCompute service observation. Because the parts cannot be recovered after deletion, please pin the service's behavior for both failure types (and document any intentional emulator divergence) before changing this contract. The Python probe should follow that verified behavior rather than define it.
…d service Review comment on this pull request: the consumption behaviour was asserted, not observed, and the parts cannot be recovered once deleted. It is observed now, on a live MaxCompute project (three-tier, temp resources created and deleted by the probe itself, 2026-10-04 19:23-19:26), and the emulator moved to match it in two places. Measured, per failure shape, with the part resources read back afterwards: | merge request | service answer | its parts afterwards | | --- | --- | --- | | correct digest and size | accepted, target published | consumed | | digest does not match the assembled payload | `ODPS-0421213 Save resource error - Merge part temp files failed!` | **consumed** | | manifest names a part that was never uploaded | `ODPS-0421111 Resource not found` | kept (the one real part survives) | | target already exists | `ODPS-0421121 The resource has already existed` | kept | | declared `x-odps-resource-merge-total-bytes` wrong, digest right (4400 declared for 304 bytes) | **accepted**, target is the correct 304 bytes | consumed | Two conclusions, and they cut in different directions from what this branch claimed before: - consuming the parts on a digest-refused merge is right - the service really does it, so an SDK that retries with the same deterministic part names has to re-upload them, and the emulator should not be the place that quietly keeps stale chunks; - the declared-byte-count check is not a service check at all. The service compares the digest, and only uses the declared total against the project's maximum. A refusal on that local check therefore has no service precedent for what it does to the parts, so the emulator now refuses and **keeps** them: it should not destroy an upload on the strength of a rule the service does not have. That divergence is written into docs/protocol.md rather than left implicit; the HTTP status difference on the existing-target path (service `ODPS-0421121`, emulator 400) is pre-existing and deliberately untouched here, because that status was not read back. Pinned in tests, both directions: - `internal/server/resources_test.go`: the digest-refused merge consumes its part (as before), the declared-byte-count refusal now asserts the part is still readable (200), and the comment says which of the two is service behaviour and which is the emulator's own strictness. - `tests/python/run.py`: three new cases - digest refused/consumed, refusal decided before any part is read/kept, declared mismatch/kept. The existing-target case asserts the refusal and the retention, and explicitly does not assert a status code, because that one was not measured. Verified: `go test -race -count=1 ./internal/...` (engine 3.03s, server 35.81s, wire 1.28s) all ok, and the PyODPS probe against a freshly built binary from this tree: 33 passed, 0 failed (previously 30 cases). The probe's leftover sweep is scoped to the case's own resource names, so one client's aborted upload cannot be blamed on another's merge. No merge, no tag, no release.
|
这条我按你说的顺序做了:先去钉服务端的真实行为,再让探针跟着结果走。结论跟这个分支之前的说法两处不一致,两处都改了。 在真实 MaxCompute 项目上实测(三层项目、临时资源由探针自己创建并删除,2026-10-04 19:23–19:26),合并请求被拒之后把分片读回来:
两个结论:
一处我明确没测也没改:目标已存在时服务端给的错误码是 测试两边都钉住: 验证: 没合并、没 tag,等你复审。 |
What
A chunked upload whose merge fails verification (digest mismatch, declared
x-odps-resource-merge-total-bytesmismatch) used to leave every part resource it had just assembled from. Now the merge consumes the parts it read in that case too, the same way it already consumes them on success.Refusals decided before any part is read still keep the parts, deliberately:
<md5>|<parts>createwhile the target resource already existsWhy it matters to clients
The Java SDK names its chunks deterministically (
<schema>.<resource>.part.tmp.<index>), so a refused duplicate create legitimately leaves a temp resource, and until now a refused merge leaked one per attempt with no way for a client to notice. Any check that sweeps a project for.part.tmp.then blames an unrelated upload.That is exactly how the PyODPS contract probe started failing
stream write (part+merge) lands byte-exactwhenever the Java acceptance suite had run first against the same emulator instance: the leftover part belonged to the Java suite'sduplicateResourceCreateIsRejected, not to the merge being asserted.Changes
internal/server/resources.go— oneconsumeParts()helper, called on the two post-read failures and on success.internal/server/resources_test.go— pins both sides of the line: a refused merge consumes its part (404 after), a refused create keeps it (200 after, target payload untouched).tests/python/run.py— scope the leftover sweep to the resource the case uploaded (it was project-wide, which made the case order-dependent); assert a refused merge consumes its own part (the old assertion pinned the opposite, and it was a guess, not an observed contract);cleanup()now also sweeps parts left by an aborted run of the probe itself.Verification
go test -race -count=1 ./internal/...— ok (server18.5s,engine3.1s,wire1.2s).resources.goreverted, the new assertion fails asrefused merge must consume its part: 200 x, so the test does pin the change.go build ./cmd/emulator, then the PyODPS probe (pyodps 0.13.2) against the running binary with a Java-style leftover seeded (POST ?rIsPartasdefault.dup_seedcheck.py.part.tmp.000000): 30 passed, 0 failed with this branch's probe; the pre-change probe against the same server fails only the two expectations this PR moves together.Scope
Base is
main@31af5c5. Touchesresources.go/resources_test.go/tests/python/run.py— disjoint from the files in the currently open MCQA and REST-fault branches, except that all three append one line underUnreleasedinCHANGELOG.md.Not included, spotted while reading this path: a merge that declares a total size differing from the payload is refused here, while the signature is what actually decides the payload — this emulator is stricter than the service on that one header. And refused-merge / refused-create errors are answered
400 InvalidParameter, whereas the service distinguishes a save failure from an authorization failure. Both are error-semantics changes and want the same contract comparison before touching them, so they are left for a separate decision.