Skip to content

GridDhtPartitionsFullMessage.partsSizes (and partCntrs) are retained fully-expanded and boxed per historical exchange future — heap regression vs 2.17.0 on topology-churning workloads. #13314

Description

@khrysto

Environment

  • Apache Ignite 2.18.0 vs 2.17.0 (identical config and JVM args)
  • JDK 21.0.6, -Xmx1G, persistent data region
  • Workload: continuous create + destroy of many short-lived REPLICATED persisted caches (each its own cache group, default 1024 partitions). Every create/destroy triggers a topology exchange.

Description

After upgrading from 2.17.0 to 2.18.0 with no configuration change, server nodes exhaust -Xmx1G and hit OutOfMemoryError under a workload that repeatedly creates and destroys short-lived REPLICATED caches. 2.17.0 runs the same workload indefinitely with a stable sawtooth heap; 2.18.0 grows monotonically until OOM.

Heap-dump analysis (Eclipse MAT, merge-shortest-paths-to-GC-roots) shows ~930 MB retained through the exchange-history path:

exchange-worker thread
  -> GridCachePartitionExchangeManager.exchFuts (ExchangeFutureSet)
    -> ~596 GridDhtPartitionsExchangeFuture
      -> finishState (FinishState)
        -> msg (GridDhtPartitionsFullMessage)
          -> partsSizes : HashMap<Integer, IntLongMap>
            -> IntLongMap  (34,033 instances, ~893 MB)

IntLongMap (formerly PartitionSizesMap) implements Message but is backed by a boxed HashMap<Integer, Long>. Retaining one full message per historical exchange therefore pins a fully-exploded, boxed per-partition map for every retained topology version, producing ~17.4M HashMap$Node + ~13M Integer in the dump.

Suspected cause (regression introduced in 2.18.0)

The message serialization rework (umbrella IGNITE-25490, "new ser/der scheme") converted the partition sub-maps of GridDhtPartitionsFullMessage from @GridDirectTransient live objects paired with a compact serialized byte[] twin (which was GC-able after marshalling) into first-class @Order Message fields whose live boxed object is now the permanently retained state:

  • IGNITE-26517 (92074fd) — renamed PartitionSizesMapIntLongMap and made partsSizes an @Order(11) field. This owns the ~893 MB. Primary contributor.
  • IGNITE-26839 (f935f99) — partCntrs promoted to an @Order(12) Message field. Secondary (~21 MB long[] in this dump).

Combined with the retained exchange history (IGNITE_EXCHANGE_HISTORY_SIZE, default 1000; ~596 futures live at OOM), the node now keeps the fully-expanded boxed maps for every historical topology version instead of a few KB of serialized bytes as in 2.17.0.

Steps to reproduce

  1. Start a node with a persistent data region and -Xmx1G.
  2. In a loop, create then destroy REPLICATED persisted caches, each in its own cache group with 1024 partitions.
  3. Observe heap growth; take a heap dump and inspect retained size of GridDhtPartitionsExchangeFuturefinishState.msg.partsSizes.

Expected vs actual

  • Expected: Retained heap of exchange history stays bounded and small (as in 2.17.0).
  • Actual: Retained heap grows with topology-version count × groups × partitions until OOM.

Suggested fixes (any of)

  • Clear/null partsSizes and partCntrs on GridDhtPartitionsFullMessage once it is stored into FinishState for history (they are not needed after the exchange completes).
  • Back IntLongMap with primitive storage (e.g. an intlong primitive map) instead of boxed HashMap<Integer, Long>.
  • Retain only a compact serialized form on historical futures, rehydrating on demand (restore the 2.17.0 behavior).

Workaround

-DIGNITE_EXCHANGE_HISTORY_SIZE=100 -DIGNITE_AFFINITY_HISTORY_SIZE=50 caps the retained history (bounds the leak to ~1/6th), and reducing partition count shrinks each IntLongMap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions