feat: route PUT and TUS through the upload coordinator - #714
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
| } | ||
|
|
||
| // finishSync commits the staged bytes, then unmarks processing and cleans up. | ||
| func (c *coordinator) finishSync(ctx context.Context, session Session) (*provider.ResourceInfo, error) { |
There was a problem hiding this comment.
I am wondering if we should implement a second method finishAsync here. I'm not sure whether the purpose of this method here is only for synchronous flow, or also after postprocessing.
In case this method is only meant for sync flow:
scanResult, scanDate := session.ScanData() would not make sense here, we only have a scan result, if it's async. And then we would need another method for async flow. I added one in this commit, we could add this here as well.
In case this method is meant for sync & async flow:
In the previous decomposedfs implementation, if session.Finalize(ctx) fails, we don't delete the session & rollback. From what I see the reason is that an admin can then decide what to do with it. E.g. trigger the RestartPostprocessing or CleanUpload event. If we want to keep that behavior, we need to change the implementation
There was a problem hiding this comment.
Right now finishSync only has the sync caller, the async one lands with the postprocessing consumer in a follow-up PR.
We can create a separate FinishAsync function later
There was a problem hiding this comment.
Should we then remove scanResult, scanDate := session.ScanData()?
There was a problem hiding this comment.
we ca nremove it later once we implement the async function
Adds the driver-agnostic upload coordinator:
pkg/upload/coordinator.go(886) andpkg/upload/tus_adapter.go(148). Specs are in a separate PR against this branch, so thisdiff stays readable; they should land together.
Dormant. Nothing outside
pkg/uploadimports the package yet — PR 7 wires itinto the dataprovider. Merging this cannot change runtime behaviour, which is why
the divergences below are safe to land and settle in review rather than block on.
What it does
Three phases, because the HTTP context is gone by the time the bytes are committed:
InitiateUploadresolves the target, checks quota and locks, and persists asession.
Uploadfor PUT,tusAdapter.WriteChunkfor TUS.finishUploadtouches the node, marks it processing, verifies checksums,prepares, then either commits inline or hands off to postprocessing.
The driver seam is 11
storage.FSmethods:GetMD,GetLock,GetQuota,GetPathByID,ListGrants,TouchFile,MarkProcessing,PrepareUpload,CommitUpload,RollbackUpload,Delete.Cleanup, and why the orderings differ
Three paths, deliberately not collapsed into one:
deleteTouchedNodeDelete— there is no processing id forRollbackUploadto key offrollbackMarkedPrepareUploadRollbackUploadwithsizeDiff 0, then unmarkrollbackPreparedPrepareUploadRollbackUploadwith the propagatedsizeDiff, then unmarkBoth rollbacks call
RollbackUploadbefore the unmark, because it keys off theprocessing id (
decomposedfs/upload.go:664-670).deleteTouchedNoderuns onlyafter a failed mark, so the node is unflagged and
Delete'sprocessing-refusal (
decomposedfs.go:1121) does not bite. The orderings areopposite on purpose.
tusAdapter.Terminateduplicates ~8 lines rather than reuserollbackMarked.Sharing them was the original bug:
rollbackMarkedhardcodessizeDiff 0anddecrements the processing gauge, both wrong for a cancelled transfer. A client
cancelling after
PrepareUploadleaked the propagated size, and every cancelledupload drove
reva_upload_processingnegative. The two callers have genuinelydifferent preconditions.
Declared divergences from main
checkQuotaproceeds whenGetQuotaerrors (coordinator.go:650). Main usednode.CheckQuota, which propagates. The permissive form is deliberate — driverswithout quota support must not be blocked — but it is a behaviour change, stated
here rather than hidden.
describeExistingcheck order and the droppedPersistbetween checksumverification and
prepare.uploadRefreuse inuff.(
coordinator.go:532) and deny-grantNotFound(:606), whereGetMDreports a filehidden by a deny-grant as missing, so the upload fails late with 409 instead of 403.
Inherited from main, not introduced
sizedeferred+uploadLength == 0. A third-party copy sendssizedeferred: trueand noUpload-Length, so the zero-length branch finishes itwith no bytes. Main has the identical shape at
decomposedfs/upload.go:331-337.Faithful parity — flagging it so it does not read as a porting mistake.
chunking.GetChunkBLOBInfo's latent panic is pre-existing and unreachable fromhere.
Fixes made during self-review
Terminateno longer decrements the processing gauge or passessizeDiff 0, andlogs the expected unmark failure at Debug rather than Error — it was Error on every
cancelled upload.
c.async && c.pub != nil. Maingates publish and commit on two independent conditions; collapsing them meant a
deployment with async on but no publisher wired hit a nil-interface panic in the
request path instead of committing inline.
Coordinatorinterface assertion — unneeded until PR 7, and the idiomappears nowhere else in the repo.
Fixes from review
OpaqueIdresolves tothe space root (
lookup.go:191) andTerminatecan run beforeTouchFile, so rollbackand unmark would target the root instead of missing. It only has to match nothing.
RollbackUploadpurges a node the upload created (
decomposedfs/upload.go:676), so the unmark that followscorrectly finds no node and returns
NotFound(:352) — every failed upload of a new filewas logging an error for the expected outcome. Demoted, not dropped. The unmark after a
successful commit stays at Error, where a missing node is genuinely unexpected.
Tests
233 Ginkgo specs, on
OCISDEV-900-pr6b-specsand opened against this branch.coordinator.goandtus_adapter.goat 100% statement coverage, package at 99.5%; the threeuncovered statements are unreachable (NFS
ESTALE, atusd.FileInfothat cannot fail tomarshal, HMAC signing that cannot fail on a
[]bytekey). Verified by mutation testing ratherthan coverage alone: ~33 injected bugs, all killed.
The specs cover the async fork, which had none before, and the
UploadProcessinggauge, whichhad none anywhere.
Re-verified against the three review fixes above: all 233 still pass. Coverage dips to 97.6%,
the whole gap being
spaceOwnerOrManager's grant fallback (untestable whilefakeFShas noListGrants) andunmarkProcessing's newNotFoundbranch.Not addressed here
finishSyncdestroys the session when the commit fails, which is right for a waiting clientbut wrong for the async caller, which wants the upload parked for
RestartPostprocessing.It only gets that second caller in the postprocessing PR, which splits the commit path in two.
No changelog — one goes in at the end of the coordinator stack, not per PR.