Skip to content

feat: download mode for the chunk stream endpoint - #5618

Open
martinconic wants to merge 1 commit into
masterfrom
feat/chunk-stream-download
Open

martinconic wants to merge 1 commit into
masterfrom
feat/chunk-stream-download

Conversation

@martinconic

Copy link
Copy Markdown
Contributor

Checklist

  • I have read the coding guide.
  • My change requires a documentation update, and I have done it.
  • I have added tests to cover my changes.
  • I have filled out the description and linked the related issues.

Description

Adds a download mode to GET /chunks/stream. A client opens one websocket and pulls many chunks over it, instead of paying for an HTTP request per chunk.

Closes #5417, closes #5599.

Protocol

Mode is fixed at handshake — via Sec-WebSocket-Protocol: swarm-chunk-download, or ?mode=download for browser clients that cannot set headers. A connection is an upload stream or a download stream, never both.

Request frame: ['D'][32-byte address]..., up to 256 addresses. 'D' is the command byte from #5417; batching sits under it so the two compose, and it leaves room to add commands later without breaking clients.

Reply frame: [status][32-byte address][payload]0x00 success, 0x01 not found, 0x02 error. Replies arrive out of order as retrievals complete, so clients match on the address. Exactly one reply per requested address.

swarm-cache is honoured on the download path, matching GET /chunks/{address}.

From #5599

Batched requests (256/frame), a bounded worker pool rather than an uncontrolled request storm, streamed partial results, per-address success/failure, and relief from the browser's six-connections-per-host limit. Implemented over websocket — one of the response options that issue listed — rather than POST /chunks/batch.

The video-streaming motivation in #5599 is not served by this endpoint and has been split out. Playback wants HTTP Range and server-side read-ahead on /bzz; this endpoint has no cancellation and strict FIFO delivery, so a seek would mean dropping the connection. It suits random-access and bulk workloads: manifest traversal, SQLite/Parquet-style page reads, bulk sync, pinning.

Notes for review

  • topology.ErrNotFound maps to 0x01, matching how bzz.go treats the same error from the same storer.Download call.
  • Per-request timeout is getter.DefaultFetchTimeout, the constant the joiner already uses, so one unreachable chunk can't park a worker indefinitely.
  • The delivery write deadline is 5 minutes. A client that buffers ahead stops reading while its buffer drains; a short deadline tears the stream down for it. Verified: with a 2s deadline a 6s pause killed the stream after 28 of 2000 chunks.
  • Control frames use a separate 5s deadline and don't take the write mutex — WriteControl is safe concurrently, and holding the mutex would let a slow delivery delay the close frame.
  • Shutdown closes the connection so the blocked ReadMessage returns, instead of api.Close() timing out against a 15-minute read deadline.
  • OpenAPI bumped to 8.2.0.

AI Disclosure

  • This PR contains code that has been generated by an LLM.
  • I have reviewed the AI generated code thoroughly.
  • I possess the technical expertise to responsibly review the code generated in this PR.

@martinconic
martinconic marked this pull request as draft September 16, 2026 12:44
@martinconic
martinconic marked this pull request as ready for review September 16, 2026 15:30
Comment thread pkg/api/chunk_stream.go
s.metrics.ChunkStreamOpenConnections.WithLabelValues("download").Inc()
defer s.metrics.ChunkStreamOpenConnections.WithLabelValues("download").Dec()

ctx, cancel := context.WithCancel(context.Background())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you probably also want to have another goroutine that selects on s.quit. if the node gets shut down, this context doesn't cancel. ideally you cancel the context once if there's a shutdown or if there's an error, then you save selecting on s.quit later on in the method

Comment thread pkg/api/chunk_stream.go
return
}

if len(msg) < 1+swarm.HashSize || (len(msg)-1)%swarm.HashSize != 0 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

some comment as to why is this off by one needed?

Comment thread pkg/api/chunk_stream.go
// because ReadMessage allocates a fresh buffer per message; a pooled or
// reused read buffer would corrupt addresses across concurrent workers.
addrs := make([]swarm.Address, 0, batchCount)
for i := 0; i < len(payload); i += swarm.HashSize {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what about if there's an encrypted chunk address here? won't work no..?

Comment thread pkg/api/chunk_stream.go
func (s *Service) fetchAndSendChunk(
streamCtx context.Context,
logger log.Logger,
loggerV1 log.Logger,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why two loggers?

Comment thread pkg/api/chunk_stream.go
s.metrics.ChunkStreamDeliveryCount.WithLabelValues("success").Inc()

chunkData := chunk.Data()
resp := make([]byte, 1+swarm.HashSize+len(chunkData))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not sure about this custom serialization by hand thing... it is just specced out in the openapi spec and assumed that implementers should implement it by hand. it is fragile and breakable. why not use some sort of standard serialization format to both decode the request and encode the response?

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.

Download chunks with websocket feat(api): Add batched chunk retrieval endpoint (e.g., POST /chunks/batch)

2 participants