Skip to content

fix: return currentBuffer and prevTail to dstPool in init to stop leak#69

Merged
klauspost merged 2 commits into
klauspost:masterfrom
SAY-5:fix/init-returns-buffer-to-pool-56
Apr 22, 2026
Merged

fix: return currentBuffer and prevTail to dstPool in init to stop leak#69
klauspost merged 2 commits into
klauspost:masterfrom
SAY-5:fix/init-returns-buffer-to-pool-56

Conversation

@SAY-5
Copy link
Copy Markdown
Contributor

@SAY-5 SAY-5 commented Apr 21, 2026

What

Fixes #56.

Writer.init unconditionally cleared z.currentBuffer and z.prevTail to nil. On a Reset / Close cycle that dropped a fully-allocated buffer (and an optional tail buffer) on the floor every iteration: both had just been fetched from z.dstPool on the preceding Close path (compressCurrent at gzip.go:272, Close calling compressCurrent with flush=true), so the pool never saw them again.

Long-running hot loops that reuse a single Writer with Reset(buf); Write; Close; ... grew allocations without bound as the sync.Pool hit-rate collapsed.

Fix

Put both buffers back before zeroing them. The existing // Put in .compressBlock and Put p / Put prevTail call sites (lines 272, 397, 441, 458) show the pattern already in use for the other Get locations - this change just extends it to the init path so the pool is complete.

Behaviour

  • NewWriterinit on a fresh *Writer: both fields are zero-value nil, the new guards short-circuit, nothing changes.
  • ResetSetConcurrencyinit after a Close: both fields hold live pool buffers, they are returned, and the pool stays healthy.

Verification

Locally on macOS, go 1.26.2:

  • gofmt -s -l: clean
  • go vet ./...: clean
  • go test -race -count=1 ./...: pass (full suite, ~130s under -race)

Closes #56

Writer.init unconditionally cleared z.currentBuffer and z.prevTail to
nil, which on a Reset/Close cycle dropped a fully-allocated buffer and
an optional tail buffer on the floor every iteration. Both had just
been fetched from z.dstPool on the preceding Close path
(compressCurrent at gzip.go:272 / Close calling compressCurrent with
flush=true), so the pool never saw them again. Long-running hot loops
that reuse a single Writer with Reset(buf); Write; Close; ... grew
allocations without bound as the sync.Pool hit-rate collapsed (klauspost#56).

Put both buffers back before zeroing them. The existing 'Put in
.compressBlock' and 'Put p / Put prevTail' call sites show the pattern
already in use for the other Get locations - this change just extends
it to the init path so the pool is complete.

Behavioural note: init runs only from NewWriter and from Reset (via
SetConcurrency). In NewWriter the fields are the zero-value nil, the
new guards short-circuit, and nothing changes. Only the Reset path
benefits, which is the path that users complained about.

Closes klauspost#56

Signed-off-by: SAY-5 <SAY-5@users.noreply.github.com>
Copy link
Copy Markdown
Owner

@klauspost klauspost left a comment

Choose a reason for hiding this comment

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

lgtm

Comment thread gzip.go Outdated
No need to describe what can easily be seen in code.
@klauspost klauspost merged commit 89611d0 into klauspost:master Apr 22, 2026
12 checks passed
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.

buf leak in (*Writer).Close()

2 participants