Repository navigation
feat: run artifact moves as a durable restate service with pull pins in durable state - #376
Conversation
| id, err := d.MoveArtifact(waitCtx, job) | ||
| if err != nil { | ||
| return "", wrap(err) |
There was a problem hiding this comment.
Cleanup deletes active move files
runArtifactMove returns an error when its wait ends, but the Restate move keeps running. For checkpoint copies, advanceChain responds by deleting the source checkpoint and retiring the chain, which deletes the destination base without checking durable pins. A dropped connection to Restate or the one-hour wait limit can therefore remove files that the running move still needs, causing the copy to fail. Keep those files until the move finishes or has been cancelled.
| if record, err := fm.getSnapshotRecord(ctx, job.LocalID); err == nil && record.HostID == job.HostID { | ||
| return nil | ||
| } | ||
| if err := fm.deleteFreeArtifact(ctx, job.HostID, job.LocalID); err != nil && !IsNotFound(err) { |
There was a problem hiding this comment.
Cleanup deletes a recorded copy
ArtifactDiscard treats a failed record lookup as permission to delete the destination copy. If a record write commits but its answer is lost, later write attempts can fail until retries run out. A database read failure during discard then deletes the bytes while leaving the ready record behind. Later creates or moves trust that record and fail when they need the files.
getSnapshotRecord hides database errors as ErrSnapshotNotFound, so this check cannot tell those cases apart. Preserve read errors and retry cleanup unless the record is confirmed absent.
| if _, err := restate.Object[restate.Void](ctx, ArtifactPinObject, id, "Pin"). | ||
| Request(PinRequest{Holder: holder, TTL: movePinTTL}); err != nil { |
There was a problem hiding this comment.
Move pins expire 24 hours after acquisition even when the move is still live. The handler acquires them only once, and replay uses those completed calls rather than renewing them. After a long endpoint outage, or with retry delays that span more than a day, Expire can remove the pins before the move finishes. Layer cleanup can then delete its source, leaving the move unable to finish. Renew active move pins, or check whether their move has ended before expiring them.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
fuse | 26e448d | Commit Preview URL Branch Preview URL |
Nov 15 2026, 06:15 AM |
…and a durable mover hook
…y still be running
…il the move releases them
ea87679 to
26e448d
Compare
Runs host-to-host artifact moves as a durable restate service, with the pull pins kept in restate state. Bottom of the restate stack (#346 -> #345 -> #349 -> #348 -> #342).
Artifacts/{destination snapshot id}.Move: check (a copy already recorded there ends the move), pin, pull, record, release. used byensureArtifactOnHost(create seeding, off-host fork, migrate) andmoveDeltaToHost(migrate deltas, checkpoint chain).durable.Runstep, so it carries the [restate] idempotent host-agent mutations in fc-agent and qemu-agent #338 idempotency key: a retry after a lost response or a restart gets the agent's first answer instead of a second pull. 4xx, immovable and wrong-kind failures are terminal; the rest retry under theARTIFACTSpolicy.ErrArtifactMoveUnsettled; the checkpoint loop then forgets its chain instead of deleting files the move may still use.ArtifactPin/{snapshot id}holds pins (holder -> deadline) held until the move releases them on any ending (done, failed, cancelled); only a lazy migrate's seed pin has a delayed-send lapse (35m). artifact gc asks it before evicting and skips anything whose pin it cannot read. the lazy-migrate seed pin moved onto it too; checkpoint chain base pins stay in memory until [restate] snapshot and restore on the Environment object #345.ORCH_ARTIFACT_PULL_TIMEOUT_SECONDSis the pull step's timeout on restate, not the request's. theMovehandler's abort timeout is set from it.orchestrator_artifact_move_{duration_seconds,bytes_total,failures_total}by source/target host (both modes).FUSE_RESTATE_MODE=offkeeps the in-process path and in-memory pins unchanged.no api surface change, so no sdk/openapi changes.
Docs:
docs/runbooks/restate.mdx,docs/content/docs/cloud/orchestrator-deploy.mdx(pull timeout row, retry service names),docs/content/docs/cloud/monitoring.mdx.verified locally
go build ./...,go vet ./...,gofmt -l ., golangci-lint v2.12.2 (0 issues),go test ./... -count=1, and-raceon durable/orchestrator/metrics.bun run buildpasses;bun run lintfails only on pre-existing biome formatting in meta.ts files this PR does not touch.not run
kill -9mid-move e2e and the gc-across-restart check on real hosts.Closes #346