Skip to content

Store-gateway: drain chunk range reader before close to allow connection reuse - #16338

Open
omkar619-dev wants to merge 3 commits into
grafana:mainfrom
omkar619-dev:drain-chunk-range-reader
Open

Store-gateway: drain chunk range reader before close to allow connection reuse#16338
omkar619-dev wants to merge 3 commits into
grafana:mainfrom
omkar619-dev:drain-chunk-range-reader

Conversation

@omkar619-dev

@omkar619-dev omkar619-dev commented Aug 10, 2026

Copy link
Copy Markdown

What this PR does

Drains the chunks range reader before closing it in bucketChunkReader.loadChunks, so the underlying HTTP connection is returned to the idle pool instead of being discarded.

The range is fetched using estimated chunk lengths, so it frequently extends past the end of the last chunk actually read. net/http cannot reuse a connection whose response body has unread bytes, so every chunk range fetch was costing a fresh TCP (and TLS) connection.

Reproduced in isolation — 100 identical 1 MiB ranged GETs against MinIO, TCP dials counted by hooking Transport.DialContext:

TCP dials
body fully drained 1
4 KiB read, then Close 100

There is no size threshold: 8 KiB through 4 MiB ranges all produce 100 dials when the body is abandoned. Repro: https://gist.github.com/omkar619-dev/23460d8af1d421479be9804c857ca327

Same class of fix as #16303 (Memcached connection reuse).

Which issue(s) this PR fixes

Fixes #13841

Approach confirmed by @narqo in #13841 (comment) ("simply draining the connection in-place will be enough").

Notes for reviewers

  • fetchChunkRemainder uses the same non-draining close but reads an exact length via
    io.ReadFull, so it likely consumes fully. Left unchanged to keep this minimal — happy to include it if you'd prefer.
  • The drained tail is bounded by chunk-length estimation error, not range size: gaps inside the part are already consumed by SkipTo, so only the segment past the last chunk remains, on the order of EstimatedMaxChunkSize.

Checklist

  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX]

@omkar619-dev
omkar619-dev requested a review from a team as a code owner August 10, 2026 13:07
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Signed commits report

All 3 commits between main and drain-chunk-range-reader have verified signatures. ✅

@github-actions

Copy link
Copy Markdown
Contributor

Signed commits report

1 of 1 commit between main and drain-chunk-range-reader could not be fully verified:

Commit Author Reason Message
8cad8fe2 Omkar Shendge unsigned Store-gateway: drain chunk range reader before close

This repository requires all commits to be signed. See GitHub docs on commit signature verification.

@cla-assistant

cla-assistant Bot commented Aug 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cla-assistant

cla-assistant Bot commented Aug 10, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

The chunks range reader is fetched using estimated chunk lengths, so the
requested range frequently extends past the end of the last chunk actually
read. net/http cannot return a connection to the idle pool when the response
body has unread bytes, so every chunk range fetch discarded its connection
and dialed a new one.

Drain the remaining bytes before closing so the connection can be reused.

Signed-off-by: Omkar Shendge <omkarshendge619@gmail.com>
Signed-off-by: Omkar Shendge <omkarshendge619@gmail.com>

@aknuds1 aknuds1 left a comment

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.

I think we shouldn't be draining the reader in error cases.

Comment thread pkg/storegateway/bucket_chunk_reader.go Outdated
Comment on lines +137 to +144
// Drain any unread bytes of the fetched range before closing it, so the underlying HTTP
// connection can be returned to the idle pool and reused. The range is fetched using
// estimated chunk lengths, so it frequently extends past the last chunk actually read;
// closing a response body with unread bytes causes net/http to discard the connection.
defer func() {
_, _ = io.Copy(io.Discard, bucketReader)
runutil.CloseWithLogOnErr(r.block.logger, bucketReader, "readChunkRange close range reader")
}()

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.

Please undo this change, and only add the following before returning nil at the end of this function, so the reader is only drained in the success case:

	// Chunk lengths are estimated, so drain any overfetched tail to make the connection reusable.
	_, _ = io.Copy(io.Discard, reader)

I would also suggest adding a corresponding regression test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point actually — the deferred version ran on every error return too, which is wasted work when we're
aborting anyway. Reverted the defer and moved the drain to just before the success return nil,
using reader as you suggested so the buffered bytes are accounted for.

Also added TestBucketChunkReader_loadChunks_drainsOverfetchedTail. It over-estimates the chunk
length so the fetched range includes a tail, then asserts every served byte was consumed. Worth
noting the tail has to exceed EstimatedMaxChunkSize — with a smaller one, bufio read-ahead
consumes it incidentally and the test passes even without the fix. Verified against unpatched code,
where it fails with 16000 of 64106 bytes consumed, i.e. exactly one buffer fill.

Address review feedback: draining in a defer also ran on the error returns,
which is wasted work when the request is being aborted anyway. Drain
immediately before the success return instead.

Add TestBucketChunkReader_loadChunks_drainsOverfetchedTail, which
over-estimates the chunk length so the fetched range includes a tail larger
than the reader's bufio buffer, then asserts the whole range is consumed.
Without the drain it consumes only 16000 of 64106 bytes, which is exactly
one EstimatedMaxChunkSize buffer fill.

Signed-off-by: Omkar Shendge <omkarshendge619@gmail.com>
@pitasi

pitasi commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

please rebase this PR on latest main, otherwise it can't merge — it's blocked on required checks that will never report.

context: #16335 changed the unit test CI matrix and the required checks got renamed (test (N, 10)test (N, 5)). CI runs from before that change only report the old names, so the branch needs to pick up the new workflow from main. sorry for the churn.

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.

Bug: Mimir creates thousands of TCP connections and DNS requests to/for S3 storage and does not use existing connections.

3 participants