Skip to content

Share LFS multipart upload sessions across replicas - #328

Merged
PaxMachinaOne merged 7 commits into
KafScale:mainfrom
novatechflow:fix/321-lfs-multipart-shared-state
Nov 13, 2026
Merged

PaxMachinaOne merged 7 commits into
KafScale:mainfrom
novatechflow:fix/321-lfs-multipart-shared-state

Conversation

@novatechflow

@novatechflow novatechflow commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #321. Authoritative multipart sessions and checksums live in trusted etcd, so requests can move between LFS proxy replicas or survive a pod restart. S3 remains blob storage. Per-upload leased locks and revision/owner comparisons fence stale session writes and deletes, and lock-key/lease cleanup remains bounded after cancellation.

Part identities are persisted before S3 writes. All retries for a part must use identical bytes; conflicting bytes return 409 part_conflict, and failed attempts do not advance the checksum. Completion requires every part exactly once in order and checks the expected checksum before finalizing S3. A lost successful S3 completion response can be recovered by checking the existing unique object key and size. Durable publication/discard markers prevent stale cleanup from deleting possibly Kafka-referenced blobs. Kafka retry semantics remain at-least-once.

Expiry uses persisted deadlines. The orphan sweep checks a trusted upload index, preserves active sessions through TTL changes, and performs no abort when ownership cannot be read. Session records/indexes are updated atomically. Record-size headroom supports metadata for all 10,000 S3 parts within default etcd request limits.

Deployment and release notes

  • Multipart requires KAFSCALE_PROXY_ETCD_ENDPOINTS, even with static brokers. Operator and Docker Compose deployments already configure it.
  • Restart uploads from the earlier unsigned S3 session implementation or development etcd format without part identities (410 upload_restart_required). Unknown future session versions are preserved rather than modified.
  • etcd credentials need the session, index, lock-prefix, and lease permissions documented in docs/operations.md.
  • Orphan cleanup needs s3:ListBucketMultipartUploads and s3:AbortMultipartUpload; ambiguous-completion recovery uses s3:GetObject.
  • Parts remain sequential. Ambiguous Kafka acknowledgements can lead to duplicate envelopes; possibly referenced blobs are preserved.
  • docs/releases/unreleased.md, operations documentation, and multipart API responses are updated.

Validation

  • Fault-injection regressions cover S3-session tampering, deferred metadata transactions, changed retry bytes, cancellation during lock cleanup, invalid completion lists, checksum mismatch cleanup, lost S3 completion responses, and missing/wrong-size recovery objects.
  • A real TCP Kafka acknowledgement followed by failed metadata deletion and a later abort verifies the acknowledged blob remains intact.
  • Further tests cover TTL differences, ownership-read outages, revoked leases, legacy/future formats, 10,000-part metadata, and cross-replica SHA-256/MD5/CRC32 uploads.
  • Tests use real embedded etcd and shared fake S3.
  • go vet ./... and the published Go API compatibility gate pass.
  • Full go test -race ./..., go vet ./..., and the published API gate pass on implementation commit 78f6ca5. Follow-up f43dc3d adds a stale-discard regression for the 100% security coverage gate.
  • Hosted CI and CodeQL pass on f43dc3d, including the full race suite, 100% authentication coverage, Kafka/librdkafka/schema-registry/consumer-group end-to-end tests, Kubernetes API tests, and nested modules. Dedicated MinIO/kind multipart fault-injection QA was not run locally.

Multipart upload sessions lived in the memory of the proxy pod that started
them. With more than one replica behind the Service, the part and complete
calls could reach another pod and get 404 upload_not_found, and the started
S3 multipart upload was never aborted.

The session is now a small JSON record in the LFS bucket at
<namespace>/@lfs-uploads/<id>.json with the upload id, the parts and the
checksum state, written again after every part, so any replica can continue
an upload. A part or complete call on an expired session aborts the S3
multipart upload. Each replica with the HTTP API also removes expired sessions
and aborts their uploads, and aborts LFS multipart uploads older than the
session TTL plus one hour. Responses stay the same, apart from 502
s3_upload_failed when the session cannot be read from S3.

Fixes KafScale#321
@PaxMachinaOne
PaxMachinaOne marked this pull request as ready for review November 13, 2026 10:35

@novatechflow novatechflow left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed on f43dc3d. The review/fix/regression cycle found no remaining blockers in the multipart flow.

Validation: full go test -race ./..., full go vet ./..., the published Go API gate, and hosted CI/CodeQL pass. The authentication gate covers 100% of its required statements (736/736 across 15 files). Regression coverage uses real embedded etcd, shared fake S3, and a TCP Kafka acknowledgement fixture, including delayed RPCs, lost responses, stale owners, cancellation, preservation of an acknowledged blob after failed metadata deletion, and stale discard rejection before any S3 cleanup. Dedicated live MinIO/kind multipart fault injection was not run locally.

Release-note draft updated, with matching operations/API docs:

  • Multipart sessions and authoritative checksums are shared through trusted etcd, allowing requests across replicas and pod restarts. etcd is required even with static brokers; operator and Compose deployments already configure it.
  • Revision/owner fences, bounded cancellation cleanup, immutable per-part hashes, and identical-byte retries prevent stale writes and conflicting retries. Conflicting bytes return 409 part_conflict.
  • Completion requires every part exactly once in order and checks the expected checksum before S3 finalization. Lost successful S3 completion responses can be recovered; publication/discard markers prevent cleanup from deleting possibly Kafka-referenced blobs. Kafka retries remain at-least-once.
  • Expiry/orphan cleanup honors persisted deadlines, protects active uploads through TTL changes, and fails closed on ownership-read failure. Required S3 permissions and etcd prefix permissions are documented.
  • Restart uploads from earlier unsigned S3 sessions or development etcd records without durable part identities (410 upload_restart_required).

@PaxMachinaOne
PaxMachinaOne merged commit 9827221 into KafScale:main Nov 13, 2026
14 checks passed
@novatechflow
novatechflow deleted the fix/321-lfs-multipart-shared-state branch November 13, 2026 15:10
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.

LFS proxy multipart uploads fail with more than one proxy replica (sessions are kept in pod memory)

2 participants