Description
ActorGraphInterpreter holds a strong reference to its initial GraphInterpreterShell via the _initial constructor val. When the actor outlives the initial shell (e.g., while hosting subfused interpreters registered via registerShell), the initial shell and all its GraphStageLogic instances cannot be garbage collected for the actor's lifetime.
Affected code
stream/src/main/scala/org/apache/pekko/stream/impl/fusing/ActorGraphInterpreter.scala:
- Line 732:
_initial is a constructor val (not a var), stored as a private final field
- Line 792:
preStart() calls tryInit(_initial) but never releases the reference
- Line 848:
postStop() clears activeInterpreters but does not null out _initial
Reproduction scenario
The SubFusingActorMaterializerImpl (line 739) reuses the same ActorGraphInterpreter actor to host additional shells via registerShell (line 768). When the initial shell's stream completes, it is removed from activeInterpreters (line 822), but _initial still holds a reference to it. Any operator that uses subfusing (e.g., flatMapConcat, PrefixAndTail-derived operators) will trigger this pattern.
Impact
In long-running streams with subfused operators, the initial shell and all its graph stage logics remain pinned in memory for the actor's entire lifetime, even after the initial stream completes. This is a memory leak in subfusing scenarios.
Suggested fix
- Convert
_initial from a constructor val to private var _initial
- Set
_initial = null at the end of preStart() after tryInit completes
- Update any references to
_initial in debug logging to use the shell parameter instead
Description
ActorGraphInterpreterholds a strong reference to its initialGraphInterpreterShellvia the_initialconstructor val. When the actor outlives the initial shell (e.g., while hosting subfused interpreters registered viaregisterShell), the initial shell and all itsGraphStageLogicinstances cannot be garbage collected for the actor's lifetime.Affected code
stream/src/main/scala/org/apache/pekko/stream/impl/fusing/ActorGraphInterpreter.scala:_initialis a constructor val (not avar), stored as a private final fieldpreStart()callstryInit(_initial)but never releases the referencepostStop()clearsactiveInterpretersbut does not null out_initialReproduction scenario
The
SubFusingActorMaterializerImpl(line 739) reuses the sameActorGraphInterpreteractor to host additional shells viaregisterShell(line 768). When the initial shell's stream completes, it is removed fromactiveInterpreters(line 822), but_initialstill holds a reference to it. Any operator that uses subfusing (e.g.,flatMapConcat,PrefixAndTail-derived operators) will trigger this pattern.Impact
In long-running streams with subfused operators, the initial shell and all its graph stage logics remain pinned in memory for the actor's entire lifetime, even after the initial stream completes. This is a memory leak in subfusing scenarios.
Suggested fix
_initialfrom a constructor val toprivate var _initial_initial = nullat the end ofpreStart()aftertryInitcompletes_initialin debug logging to use theshellparameter instead