Skip to content

DeflateDecompressor: fix inflater memory leak#1137

Open
pjfanning wants to merge 4 commits into
apache:mainfrom
pjfanning:copilot/fix-deflatedecompressor-issue
Open

DeflateDecompressor: fix inflater memory leak#1137
pjfanning wants to merge 4 commits into
apache:mainfrom
pjfanning:copilot/fix-deflatedecompressor-issue

Conversation

@pjfanning

@pjfanning pjfanning commented Jul 6, 2026

Copy link
Copy Markdown
Member

Motivation:
DeflateDecompressor allocates an Inflater per stream but never explicitly releases its native memory. Under GCs with relaxed off-heap reclamation (e.g. ZGC), repeated request decompression can accumulate unreclaimed native buffers and eventually OOM.

Modification:

  • Add createInflater(noWrap) factory method to DeflateDecompressor to allow test injection of a custom Inflater
  • Track the current inflater in a var within createLogic
  • Add idempotent cleanupInflater() that calls inflater.end()
  • Override postStop() to call cleanupInflater(), covering normal completion, failure, and cancellation
  • Add focused tests in DeflateSpec verifying the inflater is released on successful decode, early cancellation, and truncation

Result:
The Inflater's native memory is reclaimed promptly when the stage terminates, regardless of termination path.

References:
Refs #1134 and #1133

Motivation:
DeflateDecompressor allocates an Inflater per stream but never explicitly
releases its native memory. Under GCs with relaxed off-heap reclamation
(e.g. ZGC), repeated request decompression can accumulate unreclaimed
native buffers and eventually OOM.

Modification:
- Add createInflater(noWrap) factory method to DeflateDecompressor to
  allow test injection of a custom Inflater
- Track the current inflater in a var within createLogic
- Add idempotent cleanupInflater() that calls inflater.end()
- Override postStop() to call cleanupInflater(), covering normal
  completion, failure, and cancellation
- Add focused tests in DeflateSpec verifying the inflater is released
  on successful decode, early cancellation, and truncation

Result:
The Inflater's native memory is reclaimed promptly when the stage
terminates, regardless of termination path.

Tests:
- Not run - sbt/builds skipped per user request to conserve credits

References:
Refs apache#1134

@He-Pin He-Pin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the fix. I found two things that should be fixed before merge: one is the Scala 2.13 compile failure, and the other is a lifecycle hole when the parser loops back for another deflate stream.

Comment thread http-tests/src/test/scala/org/apache/pekko/http/scaladsl/coding/DeflateSpec.scala Outdated
The Sink.ignore future completes when take(1) sends Complete downstream,
but the Cancel signal that triggers postStop() (and thus end()) is a
separate actor message dispatched afterwards. This means awaitResult can
return before end() has been called, yielding endCalls = 0.

Add a CountDownLatch to TrackingInflater that is counted down inside
end(), and call inflater.awaitEnd() after awaitResult() in the
cancellation test to eliminate the race.
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.

3 participants