diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java
new file mode 100644
index 0000000000000..29145f07d4ea5
--- /dev/null
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.benchmarks.jmh.cache;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteDataStreamer;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataPageEvictionMode;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.benchmarks.jmh.runner.JmhIdeBenchmarkRunner;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Measures the impact of size-aware page eviction on an in-memory (non-persistent) data region.
+ *
+ * Two benchmark methods:
+ *
+ *
{@link #putSmall()} - puts of small values (well below the empty-pages pool, so the size-aware reserve
+ * in {@code RowStore.addRow} hits its fast path) within a bounded key range that keeps the region below the
+ * eviction threshold. This is the hot path whose per-operation cost the patch adds on every put, and is the
+ * primary A/B metric for detecting a performance regression between the unpatched baseline and this branch.
+ *
{@link #putLarge()} - puts of large values (larger than the empty-pages pool) against a region that has
+ * been pre-filled to near capacity, so that each large put must actually run the size-aware eviction loop.
+ * This exercises the new eviction behavior; on an unpatched build such a put fails with an out-of-memory
+ * error, so this benchmark only runs meaningfully on the patched build.
+ *
+ */
+@State(Scope.Benchmark)
+@Fork(1)
+@Threads(4)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 3, time = 5)
+@Measurement(iterations = 5, time = 10)
+public class JmhPageEvictionBenchmark {
+ /** Default cache name. */
+ private static final String CACHE_NAME = "default";
+
+ /** Empty pages pool size (kept low so that the LARGE scenario reliably exceeds it). */
+ private static final int POOL_SIZE = 100;
+
+ /** Small value size (bytes): a single data page, far below the empty-pages pool. */
+ private static final int SMALL_VALUE_SIZE = 1024;
+
+ /** Large value size (bytes): larger than the empty-pages pool in page terms. */
+ private static final int LARGE_VALUE_SIZE = 2 * 1024 * 1024;
+
+ /**
+ * Number of pre-fill small entries for the LARGE scenario. Chosen so that the total written data
+ * (400k x 1 KiB) far exceeds the region capacity: threshold eviction then pins the region at the eviction
+ * threshold (default ~90% of {@code maxSize}), leaving the free list with only its empty-pages pool. At that
+ * point a {@link #LARGE_VALUE_SIZE} put cannot take the fast path and must actually run the size-aware eviction
+ * loop. (A modest pre-fill such as 48k x 1 KiB would leave the region only ~19% full and let every large put
+ * fit into the headroom via the fast path, so it would never exercise the code under measurement.)
+ */
+ private static final int PRE_FILL_ENTRIES = 400_000;
+
+ /**
+ * Bounded key range for {@link #putSmall()}. Each {@link #SMALL_VALUE_SIZE} value occupies one data page, so a
+ * working set of this many resident keys (~32k x 4 KiB ~ 128 MiB) stays comfortably below the eviction
+ * threshold (~90% of the 256 MiB region). Overwriting within this bounded range (instead of append-style fresh
+ * keys) keeps the region from filling up and drifting into steady-state threshold eviction during measurement,
+ * so the run isolates the per-put cost of the size-aware-reserve fast path.
+ */
+ private static final int SMALL_KEY_RANGE = 32_000;
+
+ /** Benchmark scenario: selects the value size and the pre-fill strategy. */
+ @Param({"SMALL", "LARGE"})
+ private String scenario;
+
+ /** Ignite cache. */
+ private IgniteCache cache;
+
+ /** Pre-allocated small value (reused to avoid allocation noise in the hot path). */
+ private final byte[] smallVal = new byte[SMALL_VALUE_SIZE];
+
+ /** Pre-allocated large value (reused to avoid allocation noise in the hot path). */
+ private final byte[] largeVal = new byte[LARGE_VALUE_SIZE];
+
+ /** Monotonic key source: bounded (mod {@link #SMALL_KEY_RANGE}) for {@link #putSmall()} to keep the region below
+ * the eviction threshold, and unbounded (append-style) for {@link #putLarge()} to avoid overwriting entries. */
+ private final AtomicInteger keyGen = new AtomicInteger();
+
+ /** Page eviction mode used for the data region. */
+ @Param("RANDOM_LRU")
+ private String evictionMode;
+
+ /** Put of a small value (hot path, size-aware reserve takes its fast path). Keys are wrapped within a bounded
+ * range ({@link #SMALL_KEY_RANGE}) so the resident working set stays below the eviction threshold and the run
+ * isolates the fast-path cost instead of drifting into steady-state threshold eviction. */
+ @Benchmark
+ public void putSmall() {
+ int key = keyGen.incrementAndGet() % SMALL_KEY_RANGE;
+
+ cache.put(key, smallVal);
+ }
+
+ /**
+ * Put of a large value against a nearly-full region (runs the size-aware eviction loop).
+ *
+ * Pinned to a single thread: the size-aware reserve accumulates {@code requiredPages} real empty pages
+ * in the shared free list before writing, and with multiple concurrent writers those free pages are consumed
+ * by rivals as fast as they are freed, so no thread ever accumulates enough and the loop exhausts its
+ * no-progress budget into an out-of-memory. At one thread the free-page count grows monotonically and the
+ * reserve completes, measuring the honest per-put cost of eviction.
+ */
+ @Benchmark
+ @Threads(1)
+ public void putLarge() {
+ int key = keyGen.incrementAndGet();
+
+ cache.put(key, largeVal);
+ }
+
+ /** Starts Ignite with an in-memory, eviction-enabled data region and pre-fills it for the LARGE scenario. */
+ @Setup(Level.Trial)
+ public void setup() {
+ long regionSize = 256 * 1024L * 1024L;
+
+ DataStorageConfiguration dsCfg = new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+ .setPersistenceEnabled(false)
+ .setMaxSize(regionSize)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ .setPageEvictionMode(DataPageEvictionMode.valueOf(evictionMode)));
+
+ IgniteConfiguration cfg = new IgniteConfiguration()
+ .setIgniteInstanceName("test")
+ .setLocalHost("127.0.0.1")
+ .setDataStorageConfiguration(dsCfg);
+
+ Ignite ignite = Ignition.start(cfg);
+
+ cache = ignite.getOrCreateCache(new CacheConfiguration(CACHE_NAME).setBackups(0));
+
+ // Pre-fill the region with small entries for the LARGE scenario until threshold eviction pins it at the
+ // eviction threshold, so that a large put has no headroom to grow into and must actually evict.
+ if ("LARGE".equalsIgnoreCase(scenario)) {
+ try (IgniteDataStreamer ldr = ignite.dataStreamer(CACHE_NAME)) {
+ ldr.perNodeBufferSize(1024);
+
+ for (int i = 0; i < PRE_FILL_ENTRIES; i++)
+ ldr.addData(i, smallVal);
+ }
+
+ // The pre-fill consumed keys [0, PRE_FILL_ENTRIES). Start large puts after that range so they write
+ // brand-new keys (true append), leaving the pre-filled small entries in place to be the eviction
+ // candidates, instead of overwriting them in place.
+ keyGen.set(PRE_FILL_ENTRIES);
+ }
+ }
+
+ /** @return Test data. */
+ @Override public String toString() {
+ return "JmhPageEvictionBenchmark[scenario=" + scenario + ", evictionMode=" + evictionMode + ']';
+ }
+
+ /** Stops all Ignite instances started by this benchmark. */
+ @TearDown
+ public void tearDown() {
+ Ignition.stopAll(true);
+ }
+
+ /**
+ * Runs this benchmark over both {@code SMALL} and {@code LARGE} scenarios (configured by {@code @Param}).
+ *
+ * @param args Ignored.
+ * @throws Exception If failed.
+ */
+ public static void main(String[] args) throws Exception {
+ JmhIdeBenchmarkRunner.create()
+ .benchmarks(JmhPageEvictionBenchmark.class.getSimpleName())
+ .run();
+ }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java
index 1293ed7989486..9666295a5203b 100644
--- a/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java
+++ b/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java
@@ -18,7 +18,6 @@
import java.io.Serializable;
import org.apache.ignite.DataRegionMetrics;
-import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.mem.MemoryAllocator;
import org.apache.ignite.mxbean.MetricsMxBean;
@@ -346,9 +345,9 @@ public DataRegionConfiguration setEvictionThreshold(double evictionThreshold) {
* Specifies the minimal number of empty pages to be present in reuse lists for this data region.
* This parameter ensures that Ignite will be able to successfully evict old data entries when the size of
* (key, value) pair is slightly larger than page size / 2.
- * Increase this parameter if cache can contain very big entries (total size of pages in this pool should be enough
- * to contain largest cache entry).
- * Increase this parameter if {@link IgniteOutOfMemoryException} occurred with enabled page eviction.
+ * Since size-aware eviction automatically frees additional pages when the inserted row is larger than this pool,
+ * it is no longer required to increase this parameter up to the size of the largest cache entry;
+ * it may be kept at its default as the steady-state reserve of empty pages.
*
* @return Minimum number of empty pages in reuse list.
*/
@@ -360,9 +359,9 @@ public int getEmptyPagesPoolSize() {
* Specifies the minimal number of empty pages to be present in reuse lists for this data region.
* This parameter ensures that Ignite will be able to successfully evict old data entries when the size of
* (key, value) pair is slightly larger than page size / 2.
- * Increase this parameter if cache can contain very big entries (total size of pages in this pool should be enough
- * to contain largest cache entry).
- * Increase this parameter if {@link IgniteOutOfMemoryException} occurred with enabled page eviction.
+ * Since size-aware eviction automatically frees additional pages when the inserted row is larger than this pool,
+ * it is no longer required to increase this parameter up to the size of the largest cache entry;
+ * it may be kept at its default as the steady-state reserve of empty pages.
*
* @param emptyPagesPoolSize Empty pages pool size.
* @return {@code this} for chaining.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java
index bda9d3fbdc198..042e9f78a323d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java
@@ -201,6 +201,30 @@ public interface GridCacheEntryEx {
public boolean evictInternal(GridCacheVersion obsoleteVer, @Nullable CacheEntryPredicate[] filter,
boolean evictOffheap) throws IgniteCheckedException;
+ /**
+ * Same as {@link #evictInternal(GridCacheVersion, CacheEntryPredicate[], boolean)}, but when {@code tryLock} is
+ * {@code true} the entry lock is acquired non-blockingly: the entry is skipped (this method returns {@code false})
+ * whenever its lock is contended or already held by the current thread (the self-hold case), instead of blocking.
+ * Used by size-aware page eviction which may run while the current thread already holds other entry locks, to
+ * avoid a lock-ordering deadlock. The default implementation ignores {@code tryLock} and uses the blocking
+ * variant.
+ *
+ * @param obsoleteVer Version for eviction.
+ * @param filter Optional filter.
+ * @param evictOffheap Evict offheap value flag.
+ * @param tryLock {@code true} to acquire the entry lock non-blockingly (skip contended or self-held entries).
+ * @return {@code True} if entry could be evicted.
+ * @throws IgniteCheckedException In case of error.
+ */
+ public default boolean evictInternal(
+ GridCacheVersion obsoleteVer,
+ @Nullable CacheEntryPredicate[] filter,
+ boolean evictOffheap,
+ boolean tryLock
+ ) throws IgniteCheckedException {
+ return evictInternal(obsoleteVer, filter, evictOffheap);
+ }
+
/**
* This method should be called each time entry is marked obsolete
* other than by calling {@link #markObsolete(GridCacheVersion)}.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
index 021fdd0408d48..dc34161f9b3ba 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java
@@ -3658,7 +3658,10 @@ protected void removeValue() throws IgniteCheckedException {
* Evicts necessary number of data pages if per-page eviction is configured in current {@link DataRegion}.
*/
private void ensureFreeSpace() throws IgniteCheckedException {
- // Deadlock alert: evicting data page causes removing (and locking) all entries on the page one by one.
+ // Deadlock alert: evicting a data page removes (and locks) all entries on the page one by one, so this must
+ // only run while NOT holding this entry's lock (all call sites run before lockEntry()). The size-aware path
+ // (RowStore.addRow -> ensureFreeSpaceForInsert) runs under the lock and instead uses the non-blocking
+ // tryLockEntry(0) inside evictInternal.
assert !lock.isHeldByCurrentThread();
cctx.shared().database().ensureFreeSpace(cctx.dataRegion());
@@ -3684,14 +3687,26 @@ private CacheEntryImplEx wrapVersionedWithValue() {
@Override public boolean evictInternal(
GridCacheVersion obsoleteVer,
@Nullable CacheEntryPredicate[] filter,
- boolean evictOffheap)
- throws IgniteCheckedException {
+ boolean evictOffheap
+ ) throws IgniteCheckedException {
+ return evictInternal(obsoleteVer, filter, evictOffheap, false);
+ }
+ /** {@inheritDoc} */
+ @Override public boolean evictInternal(
+ GridCacheVersion obsoleteVer,
+ @Nullable CacheEntryPredicate[] filter,
+ boolean evictOffheap,
+ boolean tryLock
+ ) throws IgniteCheckedException {
boolean marked = false;
try {
if (F.isEmptyOrNulls(filter)) {
- lockEntry();
+ // With tryLock=true the lock is taken non-blockingly and a contended/self-held entry is skipped
+ // (returns false) to avoid a lock-ordering deadlock; the tracker then picks another page.
+ if (!lockEntry(tryLock))
+ return false;
try {
if (evictionDisabled()) {
@@ -3728,7 +3743,8 @@ private CacheEntryImplEx wrapVersionedWithValue() {
while (true) {
GridCacheVersion v;
- lockEntry();
+ if (!lockEntry(tryLock))
+ return false;
try {
v = ver;
@@ -3740,7 +3756,8 @@ private CacheEntryImplEx wrapVersionedWithValue() {
if (!cctx.isAll(/*version needed for sync evicts*/this, filter))
return false;
- lockEntry();
+ if (!lockEntry(tryLock))
+ return false;
try {
if (evictionDisabled()) {
@@ -4182,6 +4199,23 @@ private int extrasSize() {
lock.lock();
}
+ /**
+ * Acquires the entry lock either blocking ({@code tryLock == false}) or non-blockingly with an immediate
+ * {@code tryLock(0)} ({@code tryLock == true}). Used by {@link #evictInternal} to let size-aware
+ * eviction skip contended entries instead of blocking, avoiding a lock-ordering deadlock.
+ *
+ * @param tryLock {@code true} to acquire the lock non-blockingly.
+ * @return {@code true} if the lock was acquired (always {@code true} when {@code tryLock == false}).
+ */
+ private boolean lockEntry(boolean tryLock) {
+ if (tryLock)
+ return !lock.isHeldByCurrentThread() && tryLockEntry(0);
+
+ lockEntry();
+
+ return true;
+ }
+
/** {@inheritDoc} */
@Override public boolean tryLockEntry(long timeout) {
try {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
index b22a6682957c4..5139d9f263c72 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java
@@ -26,6 +26,8 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.LockSupport;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -74,6 +76,7 @@
import org.apache.ignite.internal.processors.cache.persistence.metastorage.MetaStorage;
import org.apache.ignite.internal.processors.cache.persistence.metastorage.MetastorageLifecycleListener;
import org.apache.ignite.internal.processors.cache.persistence.pagemem.PageReadWriteManager;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.AbstractDataPageIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.reuse.ReuseList;
import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
import org.apache.ignite.internal.processors.cache.warmup.WarmUpStrategy;
@@ -137,6 +140,33 @@ public class IgniteCacheDatabaseSharedManager extends GridCacheSharedManagerAdap
/** Maximum initial size on 32-bit JVM */
private static final long MAX_PAGE_MEMORY_INIT_SIZE_32_BIT = 2L * 1024 * 1024 * 1024;
+ /** Initial backoff (nanoseconds) between consecutive no-progress eviction attempts. */
+ private static final long EVICTION_BACKOFF_START_NANOS = 50_000L;
+
+ /** Upper bound (nanoseconds) for the backoff between no-progress eviction attempts. */
+ private static final long EVICTION_BACKOFF_MAX_NANOS = 1_000_000L;
+
+ /**
+ * Maximum time (milliseconds) the size-aware eviction guard is willing to wait without either an eviction or a
+ * new highest empty-pages count before failing with an out-of-memory. Being time-based (measured from the last
+ * progress) rather than a fixed attempt count means a slow-but-progressing eviction is never torn down, while a
+ * genuinely stuck eviction (nothing evictable, or contenders that never release their locks) still terminates in
+ * bounded time instead of busy-spinning forever. The overall cycle is additionally capped by
+ * {@link #EVICTION_MAX_CYCLE_TIME_MILLIS}.
+ */
+ private static final long EVICTION_NO_PROGRESS_TIMEOUT_MILLIS = 1_000L;
+
+ /**
+ * Hard upper bound (milliseconds) on the total duration of one size-aware eviction cycle, regardless of
+ * per-iteration progress. {@link #EVICTION_NO_PROGRESS_TIMEOUT_MILLIS} bounds the period with no progress, but
+ * eviction that keeps making partial progress (e.g. evicting a contended page's available entries one-by-one
+ * without ever emptying a page, so the empty-pages count never reaches the target) could otherwise extend the
+ * loop indefinitely; this absolute deadline guarantees the cycle still fails with an out-of-memory in bounded
+ * time while the per-progress timeout above allows a genuinely progressing eviction to run to its natural end.
+ * It also caps the worst-case time the reserve can hold the caller's entry lock.
+ */
+ private static final long EVICTION_MAX_CYCLE_TIME_MILLIS = 5 * EVICTION_NO_PROGRESS_TIMEOUT_MILLIS;
+
/** {@code True} to reuse memory on deactive. */
protected final boolean reuseMemory = IgniteSystemProperties.getBoolean(IGNITE_REUSE_MEMORY_ON_DEACTIVATE);
@@ -1172,39 +1202,63 @@ public WALPointer latestWalPointerReservedForPreloading() {
}
/**
- * Checks that the given {@code region} has enough space for putting a new entry.
- *
- * This method makes sense then and only then
- * the data region is not persisted {@link DataRegionConfiguration#isPersistenceEnabled()}
- * and page eviction is disabled {@link DataPageEvictionMode#DISABLED}.
- *
- * The non-persistent region should reserve a number of pages to support a free list {@link AbstractFreeList}.
- * For example, removing a row from underlying store may require allocating a new data page
- * in order to move a tracked page from one bucket to another one which does not have a free space for a new stripe.
- * See {@link AbstractFreeList#removeDataRowByLink}.
- * Therefore, inserting a new entry should be prevented in case of some threshold is exceeded.
+ * Checks that the given {@code region} has enough space for putting a new entry of {@code dataRowSize} bytes.
+ *
+ * For a non-persistent region with page eviction disabled, verifies that the region reserves enough pages to
+ * support a free list {@link AbstractFreeList}. For example, removing a row from underlying store may require
+ * allocating a new data page in order to move a tracked page from one bucket to another one which does not have
+ * a free space for a new stripe. See {@link AbstractFreeList#removeDataRowByLink}. Therefore, inserting a new
+ * entry should be prevented in case of some threshold is exceeded.
+ *
+ * For a non-persistent region with page eviction enabled, additionally performs size-aware eviction: when the
+ * row does not fit into the currently available page space, data pages are evicted until either enough space is
+ * freed or it becomes clear that the goal is unreachable (in which case an
+ * {@link IgniteOutOfMemoryException} is thrown).
+ *
+ * The size-aware reserve is required because page eviction by itself only keeps a steady-state pool of empty pages
+ * ({@link DataRegionConfiguration#getEmptyPagesPoolSize()}) and does not guarantee enough space for a single row
+ * larger than this pool.
+ *
+ * Worst case: when called while the entry being written is locked (single-row insertion), the eviction loop can
+ * hold that lock for up to {@link #EVICTION_MAX_CYCLE_TIME_MILLIS} — only if eviction cannot consolidate enough
+ * empty pages within that time (e.g. a long-running transaction holding all evictable entries, or only partial
+ * progress), after which the call fails with {@link IgniteOutOfMemoryException} (reported as a critical failure
+ * to the configured failure handler).
*
* @param region Data region to be checked.
* @param dataRowSize Size of data row to be inserted.
- * @throws IgniteOutOfMemoryException In case of the given data region does not have enough free space
- * for putting a new entry.
+ * @throws IgniteOutOfMemoryException In case the given data region does not have enough free space
+ * for putting a new entry, even after eviction.
+ * @throws IgniteCheckedException If failed to evict data pages.
*/
- public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) throws IgniteOutOfMemoryException {
+ public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) throws IgniteOutOfMemoryException, IgniteCheckedException {
if (region == null)
return;
DataRegionConfiguration regCfg = region.config();
- if (regCfg.getPageEvictionMode() != DataPageEvictionMode.DISABLED || regCfg.isPersistenceEnabled())
+ if (regCfg.isPersistenceEnabled())
return;
- long memorySize = regCfg.getMaxSize();
+ if (regCfg.getPageEvictionMode() == DataPageEvictionMode.DISABLED)
+ checkOomThreshold(region, regCfg, dataRowSize);
+ else
+ ensureFreeSpaceForEviction(region, regCfg, dataRowSize);
+ }
+ /**
+ * Checks that a non-persistent region with disabled page eviction has enough pages for a new row, taking into
+ * account the pages required to support the free list.
+ *
+ * @param region Data region.
+ * @param regCfg Data region configuration.
+ * @param dataRowSize Size of data row to be inserted.
+ * @throws IgniteOutOfMemoryException If the region does not have enough free space for the new entry.
+ */
+ private void checkOomThreshold(DataRegion region, DataRegionConfiguration regCfg, int dataRowSize) throws IgniteOutOfMemoryException {
PageMemory pageMem = region.pageMemory();
- CacheFreeList freeList = freeListMap.get(regCfg.getName());
-
- long nonEmptyPages = (pageMem.loadedPages() - freeList.emptyDataPages());
+ long nonEmptyPages = (pageMem.loadedPages() - freeListMap.get(regCfg.getName()).emptyDataPages());
// The maximum number of pages that can be allocated (memorySize / systemPageSize)
// should be greater or equal to pages required for inserting a new entry plus
@@ -1213,27 +1267,159 @@ public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) throws
// Note that not the whole page can be used to storing links,
// see PagesListNodeIO and PagesListMetaIO#getCapacity(), so we pessimistically multiply the result on 1.5,
// in any way, the number of required pages is less than 1 percent.
- boolean oomThreshold = (memorySize / pageMem.systemPageSize()) <
+ boolean oomThreshold = (regCfg.getMaxSize() / pageMem.systemPageSize()) <
((double)dataRowSize / pageMem.pageSize() + nonEmptyPages * (8.0 * 1.5 / pageMem.pageSize() + 1) + 256 /*one page per bucket*/);
- if (oomThreshold) {
- IgniteOutOfMemoryException oom = new IgniteOutOfMemoryException("Out of memory in data region [" +
- "name=" + regCfg.getName() +
- ", initSize=" + U.readableSize(regCfg.getInitialSize(), false) +
- ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) +
- ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "] Try the following:" + U.nl() +
- " ^-- Increase maximum off-heap memory size (DataRegionConfiguration.maxSize)" + U.nl() +
- " ^-- Enable Ignite persistence (DataRegionConfiguration.persistenceEnabled)" + U.nl() +
- " ^-- Enable eviction or expiration policies"
- );
+ if (oomThreshold)
+ throw outOfMemory(regCfg);
+ }
+
+ /**
+ * Size-aware reserve for an eviction-enabled non-persistent region. Runs eviction until the free list holds
+ * enough real empty pages to accommodate the row, or throws {@link IgniteOutOfMemoryException} if the goal is
+ * unreachable / no progress can be made. Progress is measured against the number of empty pages in the free list
+ * (the only resource a subsequent fragmented write can reliably consume once the region is effectively full); the
+ * region's spare capacity (headroom) is only trusted in the fast path while the region is below the eviction
+ * threshold.
+ *
+ * @param region Data region.
+ * @param regCfg Data region configuration.
+ * @param dataRowSize Size of data row to be inserted.
+ * @throws IgniteOutOfMemoryException If the target cannot be reached (row too large for the region or eviction
+ * makes no progress).
+ * @throws IgniteCheckedException If failed to evict data pages.
+ */
+ private void ensureFreeSpaceForEviction(
+ DataRegion region,
+ DataRegionConfiguration regCfg,
+ int dataRowSize
+ ) throws IgniteOutOfMemoryException, IgniteCheckedException {
+ PageMemory pageMem = region.pageMemory();
+
+ // Maximum payload bytes that a single data page can hold for a fragmented row.
+ long pagePayload = pageMem.pageSize() - AbstractDataPageIO.MIN_DATA_PAGE_OVERHEAD;
- if (cctx.kernalContext() != null)
- cctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, oom));
+ // A row that fits into the steady-state empty-pages pool is satisfied by normal threshold eviction, so the
+ // fast path is a single comparison (no page computation, free-list lookup or page-memory reads on the hot
+ // small-put path).
+ if (dataRowSize <= regCfg.getEmptyPagesPoolSize() * pagePayload)
+ return;
- throw oom;
+ CacheFreeList freeList = freeListMap.get(regCfg.getName());
+
+ if (freeList == null)
+ return;
+
+ long totalPages = regCfg.getMaxSize() / pageMem.systemPageSize();
+
+ // Pages the row will actually occupy once written, and which the free list must hand out on demand during
+ // the fragmented write.
+ long requiredPages = (dataRowSize + pagePayload - 1) / pagePayload;
+
+ // The row fundamentally cannot fit into the whole region.
+ if (requiredPages > totalPages)
+ throw outOfMemory(regCfg);
+
+ // The reserve must guarantee `requiredPages` REAL empty pages, not just apparent headroom. Both are shared and
+ // non-exclusive (emptyDataPages() is a snapshot; any writer can consume them), but once the region is full
+ // (loadedPages == totalPages) headroom can no longer grow it (fresh allocateDataPage -> raw OOM), while empty
+ // pages in the reuse bucket stay reachable via takePage(). So empty pages are the only resource the fragmented
+ // write can consume on a full region. The TOCTOU between this reserve and the actual write is closed by the
+ // lazy re-reserve in AbstractFreeList#writeSinglePage.
+ long emptyPages = freeList.emptyDataPages();
+
+ // The gate reuses evictionThreshold as a regime boundary, not as "when to start eviction" (evictionRequired()
+ // does that, stopping on emptyPages >= poolSize; no last 10% of page memory is left unusable). Below the
+ // threshold the region has real slack, so a row fitting into the combined spare space is satisfied without
+ // eviction (live, e.g. short-TTL, entries are not evicted just to accumulate empty pages). At/above it headroom
+ // is no longer trustworthy (concurrent writers could commit the same headroom - TOCTOU), so only real empty
+ // pages are counted and eviction is driven below.
+ boolean evictionRegime = pageMem.loadedPages() >= (long)(totalPages * regCfg.getEvictionThreshold());
+
+ // Fast path: skip eviction when (a) enough real empty pages exist, or (b) below the regime with enough spare
+ // space to grow into. This is a snapshot and only necessary, not sufficient: under contention two writers can
+ // both pass and consume the same pages - recovered by the lazy re-reserve in AbstractFreeList#writeSinglePage.
+ if (emptyPages >= requiredPages || (!evictionRegime && emptyPages + (totalPages - pageMem.loadedPages()) >= requiredPages))
+ return;
+
+ PageEvictionTracker evictionTracker = region.evictionTracker();
+
+ // Evict until the free list holds enough real empty pages. Progress counts as either an actual eviction this
+ // iteration (evictDataPage returned true) or a new high-water empty-pages count. The count alone is unreliable
+ // under concurrency: writers consume the shared counter as fast as eviction frees pages, so a thread may never
+ // see its high-water exceeded while eviction still makes real global progress - tearing down there would be a
+ // false OOM. Counting the actual eviction prevents that; when eviction genuinely cannot proceed the method
+ // returns false, so the loop still fails with OOM in bounded time.
+ long bestEmptyPages = emptyPages;
+
+ long lastProgressNanos = System.nanoTime();
+
+ long cycleDeadlineNanos = lastProgressNanos + TimeUnit.MILLISECONDS.toNanos(EVICTION_MAX_CYCLE_TIME_MILLIS);
+
+ long backoffNanos = EVICTION_BACKOFF_START_NANOS;
+
+ while (freeList.emptyDataPages() < requiredPages) {
+ if (region.metrics().onPageEvictionsStarted()) {
+ U.warn(log, "Page-based evictions started." +
+ " Consider increasing 'maxSize' on Data Region configuration: " + regCfg.getName());
+ }
+
+ // tryLock=true: skip contended/self-held entries (the reserve can run while the current thread already
+ // holds entry locks on the single-row path), avoiding a lock-ordering deadlock.
+ boolean evicted = evictionTracker.evictDataPage(true);
+
+ region.metrics().updateEvictionRate();
+
+ long curEmptyPages = freeList.emptyDataPages();
+
+ // A new high-water count or an actual eviction re-arms the no-progress timeout; a stalled iteration backs
+ // off rather than busy-spinning.
+ if (evicted || curEmptyPages > bestEmptyPages) {
+ if (curEmptyPages > bestEmptyPages)
+ bestEmptyPages = curEmptyPages;
+
+ lastProgressNanos = System.nanoTime();
+
+ backoffNanos = EVICTION_BACKOFF_START_NANOS;
+ }
+ else {
+ LockSupport.parkNanos(backoffNanos);
+
+ backoffNanos = Math.min(backoffNanos << 1, EVICTION_BACKOFF_MAX_NANOS);
+ }
+
+ // OOM when there was no eviction/count growth for EVICTION_NO_PROGRESS_TIMEOUT_MILLIS (stuck eviction) or
+ // the whole cycle exceeds EVICTION_MAX_CYCLE_TIME_MILLIS (only partial progress, target never reached).
+ long nowNanos = System.nanoTime();
+
+ long maxNoProgress = TimeUnit.MILLISECONDS.toNanos(EVICTION_NO_PROGRESS_TIMEOUT_MILLIS);
+
+ if (nowNanos - lastProgressNanos > maxNoProgress || nowNanos > cycleDeadlineNanos)
+ throw outOfMemory(regCfg);
}
}
+ /**
+ * @param regCfg Data region configuration.
+ * @return New {@link IgniteOutOfMemoryException} (also reported as a critical failure) for the given region.
+ */
+ private IgniteOutOfMemoryException outOfMemory(DataRegionConfiguration regCfg) {
+ IgniteOutOfMemoryException oom = new IgniteOutOfMemoryException("Out of memory in data region [" +
+ "name=" + regCfg.getName() +
+ ", initSize=" + U.readableSize(regCfg.getInitialSize(), false) +
+ ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) +
+ ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "] Try the following:" + U.nl() +
+ " ^-- Increase maximum off-heap memory size (DataRegionConfiguration.maxSize)" + U.nl() +
+ " ^-- Enable Ignite persistence (DataRegionConfiguration.persistenceEnabled)" + U.nl() +
+ " ^-- Enable eviction or expiration policies"
+ );
+
+ if (cctx.kernalContext() != null)
+ cctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, oom));
+
+ return oom;
+ }
+
/**
* See {@code GridCacheMapEntry#ensureFreeSpace()}
*
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java
index cffcf9b1e5be0..916496363036d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java
@@ -20,6 +20,7 @@
import java.util.Collection;
import java.util.function.Supplier;
import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.configuration.DataPageEvictionMode;
import org.apache.ignite.internal.metric.IoStatisticsHolder;
import org.apache.ignite.internal.pagemem.PageIdUtils;
import org.apache.ignite.internal.pagemem.PageMemory;
@@ -128,12 +129,32 @@ public void addRow(CacheDataRow row, IoStatisticsHolder statHolder) throws Ignit
}
/**
+ * Size-aware pre-reserve for the rebalance batch, done when persistence is off and page eviction is enabled
+ * (single puts get the same guarantee via the per-put reserve in {@link #addRow}). Reserving "for each row"
+ * collapses to reserving for the largest one: all reserves run before any insert and only enforce a lower bound
+ * on the shared empty-pages counter, so a single reserve for the max row is equivalent and is what is done here.
+ * The reserve evicts non-blockingly even though the batch path holds no entry locks (blocking would be safe here):
+ * the path is shared with single-row insertion, which runs under an entry lock and must not block.
+ *
* @param rows Rows.
* @param statHolder Statistics holder to track IO operations.
* @throws IgniteCheckedException If failed.
*/
- public void addRows(Collection extends CacheDataRow> rows,
- IoStatisticsHolder statHolder) throws IgniteCheckedException {
+ public void addRows(Collection extends CacheDataRow> rows, IoStatisticsHolder statHolder) throws IgniteCheckedException {
+ if (!persistenceEnabled && grp.dataRegion().config().getPageEvictionMode() != DataPageEvictionMode.DISABLED) {
+ int maxRowSize = 0;
+
+ for (CacheDataRow row : rows) {
+ int rowSize = row.size();
+
+ if (rowSize > maxRowSize)
+ maxRowSize = rowSize;
+ }
+
+ if (maxRowSize > 0)
+ ctx.database().ensureFreeSpaceForInsert(grp.dataRegion(), maxRowSize);
+ }
+
assert ctx.database().checkpointLockIsHeldByThread();
freeList.insertDataRows(rows, statHolder);
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java
index 04454d05acbac..26035641d5539 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java
@@ -65,8 +65,10 @@ public FairFifoPageEvictionTracker(
}
/** {@inheritDoc} */
- @Override public synchronized void evictDataPage() throws IgniteCheckedException {
- evictDataPage(pageUsageList.pollFirst());
+ @Override public synchronized boolean evictDataPage(boolean tryLock) throws IgniteCheckedException {
+ Integer pageIdx = pageUsageList.pollFirst();
+
+ return pageIdx != null && evictDataPage(pageIdx, tryLock);
}
/** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java
index 4409e35e7446b..0e85f17148bf4 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java
@@ -38,8 +38,8 @@ public class NoOpPageEvictionTracker implements PageEvictionTracker {
}
/** {@inheritDoc} */
- @Override public void evictDataPage() {
- // No-op.
+ @Override public boolean evictDataPage(boolean tryLock) {
+ return false;
}
/** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java
index 2330c0942662d..d97a2b95e1c64 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java
@@ -32,7 +32,8 @@
import org.apache.ignite.internal.util.typedef.internal.U;
/**
- *
+ * Base for page eviction trackers sharing the data-page eviction logic
+ * ({@link #evictDataPage(int, boolean)}).
*/
public abstract class PageAbstractEvictionTracker implements PageEvictionTracker {
/** This number of least significant bits is dropped from timestamp. */
@@ -89,10 +90,13 @@ public abstract class PageAbstractEvictionTracker implements PageEvictionTracker
/**
* @param pageIdx Page index.
+ * @param tryLock {@code true} to acquire entry locks non-blockingly, skipping contended or already-held entries
+ * (e.g. when size-aware eviction runs while the current thread already holds entry locks), avoiding a
+ * lock-ordering deadlock.
* @return true if at least one data row has been evicted
* @throws IgniteCheckedException If failed.
*/
- final boolean evictDataPage(int pageIdx) throws IgniteCheckedException {
+ final boolean evictDataPage(int pageIdx, boolean tryLock) throws IgniteCheckedException {
long fakePageId = PageIdUtils.pageId(0, (byte)0, pageIdx);
long page = pageMem.acquirePage(0, fakePageId);
@@ -144,7 +148,7 @@ final boolean evictDataPage(int pageIdx) throws IgniteCheckedException {
GridCacheEntryEx entryEx = cacheCtx.isNear() ? cacheCtx.near().dht().entryEx(dataRow.key()) :
cacheCtx.cache().entryEx(dataRow.key());
- evictionDone |= entryEx.evictInternal(GridCacheVersionManager.EVICT_VER, null, true);
+ evictionDone |= entryEx.evictInternal(GridCacheVersionManager.EVICT_VER, null, true, tryLock);
}
return evictionDone;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java
index 82970fb52521c..49d6760608541 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java
@@ -38,15 +38,32 @@ public interface PageEvictionTracker extends LifecycleAware {
*/
public boolean evictionRequired();
+ /**
+ * Evicts one data page (blocking flavor, equivalent to {@link #evictDataPage(boolean) evictDataPage(false)}).
+ * In most cases, all entries will be removed from the page.
+ * Method guarantees removing at least one entry from "evicted" data page. Removing all entries may be
+ * not possible, as some of them can be used by active transactions.
+ *
+ * @return {@code true} if at least one data row has been evicted.
+ * @throws IgniteCheckedException In case of page memory error.
+ */
+ public default boolean evictDataPage() throws IgniteCheckedException {
+ return evictDataPage(false);
+ }
+
/**
* Evicts one data page.
* In most cases, all entries will be removed from the page.
* Method guarantees removing at least one entry from "evicted" data page. Removing all entries may be
* not possible, as some of them can be used by active transactions.
*
+ * @param tryLock {@code true} to acquire entry locks non-blockingly, skipping contended or already-held entries
+ * instead of blocking on them. Used by size-aware eviction that may run while the current thread already
+ * holds other entry locks, to avoid a lock-ordering deadlock.
+ * @return {@code true} if at least one data row has been evicted.
* @throws IgniteCheckedException In case of page memory error.
*/
- public void evictDataPage() throws IgniteCheckedException;
+ public boolean evictDataPage(boolean tryLock) throws IgniteCheckedException;
/**
* Call this method when last entry is removed from data page.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java
index 01300ffb73fa0..ac361fcb7b10d 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java
@@ -123,7 +123,7 @@ public Random2LruPageEvictionTracker(
}
/** {@inheritDoc} */
- @Override public void evictDataPage() throws IgniteCheckedException {
+ @Override public boolean evictDataPage(boolean tryLock) throws IgniteCheckedException {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
int evictAttemptsCnt = 0;
@@ -187,17 +187,19 @@ public Random2LruPageEvictionTracker(
if (sampleSpinCnt > SAMPLE_SPIN_LIMIT) {
LT.warn(log, "Too many attempts to choose data page: " + SAMPLE_SPIN_LIMIT);
- return;
+ return false;
}
}
- if (evictDataPage(pageIdx(lruTrackingIdx)))
- return;
+ if (evictDataPage(pageIdx(lruTrackingIdx), tryLock))
+ return true;
evictAttemptsCnt++;
}
LT.warn(log, "Too many failed attempts to evict page: " + EVICT_ATTEMPTS_LIMIT);
+
+ return false;
}
/** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java
index b940681403c15..ff94ac714b3e9 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java
@@ -109,7 +109,7 @@ public RandomLruPageEvictionTracker(
}
/** {@inheritDoc} */
- @Override public void evictDataPage() throws IgniteCheckedException {
+ @Override public boolean evictDataPage(boolean tryLock) throws IgniteCheckedException {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
int evictAttemptsCnt = 0;
@@ -161,17 +161,19 @@ public RandomLruPageEvictionTracker(
if (sampleSpinCnt > SAMPLE_SPIN_LIMIT) {
LT.warn(log, "Too many attempts to choose data page: " + SAMPLE_SPIN_LIMIT);
- return;
+ return false;
}
}
- if (evictDataPage(pageIdx(lruTrackingIdx)))
- return;
+ if (evictDataPage(pageIdx(lruTrackingIdx), tryLock))
+ return true;
evictAttemptsCnt++;
}
LT.warn(log, "Too many failed attempts to evict page: " + EVICT_ATTEMPTS_LIMIT);
+
+ return false;
}
/** {@inheritDoc} */
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java
index 62452095631cf..f6d5df5448bbd 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java
@@ -24,6 +24,7 @@
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
import org.apache.ignite.internal.metric.IoStatisticsHolder;
import org.apache.ignite.internal.metric.IoStatisticsHolderNoOp;
import org.apache.ignite.internal.pagemem.PageIdAllocator;
@@ -36,6 +37,7 @@
import org.apache.ignite.internal.pagemem.wal.record.delta.DataPageUpdateRecord;
import org.apache.ignite.internal.processors.cache.persistence.DataRegion;
import org.apache.ignite.internal.processors.cache.persistence.DataRegionMetricsImpl;
+import org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
import org.apache.ignite.internal.processors.cache.persistence.Storable;
import org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.PageLockTrackerManager;
import org.apache.ignite.internal.processors.cache.persistence.evict.PageEvictionTracker;
@@ -74,6 +76,13 @@ public abstract class AbstractFreeList extends PagesList imp
/** */
private static final int MIN_PAGE_FREE_SPACE = 8;
+ /**
+ * Bounded number of lazy size-aware re-reserve attempts on a fragmented write before falling back to
+ * allocating a brand-new page. Each attempt runs size-aware eviction (which itself fails with a clean OOM when
+ * eviction cannot progress), so this bounds the retry even under heavy contention.
+ */
+ private static final int RE_RESERVE_ATTEMPTS = 4;
+
/**
* Step between buckets in free list, measured in powers of two.
* For example, for page size 4096 and 256 buckets, shift is 4 and step is 16 bytes.
@@ -98,6 +107,12 @@ public abstract class AbstractFreeList extends PagesList imp
/** */
private final PageEvictionTracker evictionTracker;
+ /** Data region this free list belongs to (used for lazy size-aware re-reserve on fragmented writes). */
+ private final DataRegion dataRegion;
+
+ /** Database shared manager (used for lazy size-aware re-reserve on fragmented writes). */
+ private final IgniteCacheDatabaseSharedManager dbMgr;
+
/** Page list cache limit. */
private final AtomicLong pageListCacheLimit;
@@ -462,6 +477,10 @@ public AbstractFreeList(
rmvRow = new RemoveRowHandler(cacheGrpId == 0);
this.evictionTracker = dataRegion.evictionTracker();
+ this.dataRegion = dataRegion;
+ // dbMgr is needed only for the on-demand re-reserve (eviction-enabled in-memory region); null in unit tests
+ // without a cache processor (where eviction is disabled and the re-reserve never fires).
+ dbMgr = ctx.cache() == null ? null : ctx.cache().context().database();
this.reuseList = reuseList == null ? this : reuseList;
int pageSize = pageMem.pageSize();
@@ -591,6 +610,44 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
return pageMem.allocatePage(grpId, part, FLAG_DATA);
}
+ /**
+ * @return {@code true} when the region has effectively no headroom left (allocated pages reached the configured
+ * max), so a fresh {@code allocateDataPage} could no longer grow it.
+ */
+ private boolean regionEffectivelyFull() {
+ return pageMem.loadedPages() >= dataRegion.config().getMaxSize() / pageMem.systemPageSize();
+ }
+
+ /**
+ * Take a page, and if the free list cannot hand one out, re-run the size-aware reserve and retry. The reserve only
+ * bounds the shared empty-pages counter and does not pin pages to this thread, so a concurrent writer may consume
+ * them before this allocation; retrying closes that TOCTOU instead of falling straight to a raw
+ * {@code allocateDataPage}. How hard to retry depends on whether the region can still grow: on an effectively-full
+ * region re-reserving is bounded (each attempt itself fails with a clean OOM when eviction cannot progress), while
+ * with headroom a single re-reserve suffices and the subsequent {@code allocateDataPage} grows the region.
+ *
+ * @param size Free space required on the page.
+ * @param row Row to write.
+ * @param statHolder Statistics holder to track IO operations.
+ * @return Page identifier or 0 if no page could be obtained after re-reserving.
+ * @throws IgniteCheckedException If failed.
+ */
+ private long takePageWithReserve(int size, T row, IoStatisticsHolder statHolder) throws IgniteCheckedException {
+ long pageId = takePage(size, row, statHolder);
+
+ if (pageId == 0L && dbMgr != null) {
+ int reReserveAttempts = regionEffectivelyFull() ? RE_RESERVE_ATTEMPTS : 1;
+
+ for (int i = 0; pageId == 0L && i < reReserveAttempts; i++) {
+ dbMgr.ensureFreeSpaceForInsert(dataRegion, size);
+
+ pageId = takePage(size, row, statHolder);
+ }
+ }
+
+ return pageId;
+ }
+
/** {@inheritDoc} */
@Override public void insertDataRow(T row, IoStatisticsHolder statHolder) throws IgniteCheckedException {
int written = 0;
@@ -607,6 +664,9 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
catch (IgniteCheckedException | Error e) {
throw e;
}
+ catch (IgniteOutOfMemoryException e) {
+ throw e;
+ }
catch (Throwable t) {
throw new CorruptedFreeListException("Failed to insert data row", t, grpId);
}
@@ -648,7 +708,7 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
AbstractDataPageIO initIo = null;
- long pageId = takePage(row.size() - written, row, statHolder);
+ long pageId = takePageWithReserve(row.size() - written, row, statHolder);
if (pageId == 0L) {
pageId = allocateDataPage(row.partition());
@@ -661,6 +721,9 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
assert written != FAIL_I; // We can't fail here.
}
}
+ catch (IgniteOutOfMemoryException e) {
+ throw e;
+ }
catch (RuntimeException e) {
throw new CorruptedFreeListException("Failed to insert data rows", e, grpId);
}
@@ -693,6 +756,15 @@ private int writeWholePages(T row, IoStatisticsHolder statHolder) throws IgniteC
/**
* Take a page and write row on it.
+ *
+ * The page is acquired via {@link #takePageWithReserve}: the size-aware reserve (RowStore.addRow/addRows) only
+ * bounds the shared empty-pages counter and does not pin pages to this thread, so a concurrent writer may consume
+ * them before this allocation — the lazy re-reserve closes that gap instead of falling straight to a raw
+ * {@code allocateDataPage}. Reached from the BPlusTree.invoke row-creation closure, this re-reserve is an inline
+ * demand-eviction that removes entries with no data-tree page locks held (the search releases the read lock before
+ * the closure; the leaf write lock is taken only afterwards — see BPlusTree.invokeDown). The entry-level tryLock
+ * only skips contended/self-held entries and never blocks; the residual risk is that the TTL expiration worker can
+ * still deadlock via cross-tree lock ordering (data->pending here vs pending->data there) — a known limitation.
*
* @param row Row to write.
* @param written Written size.
@@ -703,7 +775,7 @@ private int writeWholePages(T row, IoStatisticsHolder statHolder) throws IgniteC
private int writeSinglePage(T row, int written, IoStatisticsHolder statHolder) throws IgniteCheckedException {
AbstractDataPageIO initIo = null;
- long pageId = takePage(row.size() - written, row, statHolder);
+ long pageId = takePageWithReserve(row.size() - written, row, statHolder);
if (pageId == 0L) {
pageId = allocateDataPage(row.partition());
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java
new file mode 100644
index 0000000000000..b25114b08dd2b
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Concurrent deadlock test for size-aware page eviction.
+ *
+ * The region is first filled with a large number of small entries (so there is plenty of evictable page space), then
+ * several threads concurrently insert large rows (larger than the empty-pages pool). Each large insert goes through
+ * the size-aware reserve and, for the single-row path, eviction under the new entry lock with the non-blocking
+ * {@code tryLockEntry}. The average data volume is kept within the region capacity, so eviction frees already-stored
+ * small entries rather than overrunning the free list. The test asserts that no deadlock occurs (all threads finish
+ * within a global deadline).
+ */
+public abstract class PageEvictionConcurrentWritesAbstractTest extends GridCommonAbstractTest {
+ /** Off-heap region size. */
+ private static final int SIZE = 256 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Large record size (larger than the empty-pages pool so that each write is size-aware). */
+ private static final int LARGE_RECORD_SIZE = 2 * 1024 * 1024;
+
+ /** Small record size used to pre-fill the region with evictable data. */
+ private static final int SMALL_RECORD_SIZE = 4096;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Number of small pre-fill entries, leaving a buffer that is exceeded by the total of the large writes, so that
+ * the last of them can only be stored by freeing pages via size-aware eviction. The large records are small
+ * enough that concurrent size-aware eviction reliably frees the required pages (no spurious guard OOM). */
+ private static final int SMALL_ENTRIES = 48_000;
+
+ /** Number of writer threads. */
+ private static final int THREADS = 2;
+
+ /** Large rows inserted per thread. Their total (threads x rows) exceeds the buffer left by the pre-fill, so the
+ * last large writes overflow the region and require size-aware eviction to free small entry pages. */
+ private static final int LARGE_ROWS_PER_THREAD = 20;
+
+ /** Global deadline for the whole test (protects against a deadlock/busy-spin hang). */
+ private static final long DEADLINE = TimeUnit.MINUTES.toMillis(3);
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE))
+ .setPageSize(DFLT_PAGE_SIZE));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page overhead).
+ */
+ private IgniteCache createCache(IgniteEx ignite) {
+ return ignite.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+ }
+
+ /**
+ * Concurrent large inserts into a region pre-filled with small entries must complete within the deadline without
+ * deadlock, and without corrupting the free list (eviction frees small entries rather than overrunning the region).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testConcurrentLargeWritesNoDeadlock() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache cache = createCache(ignite);
+
+ // Pre-fill the region with many small entries so that eviction always has evictable pages to free.
+ for (int i = 0; i < SMALL_ENTRIES; i++)
+ cache.put(i, new byte[SMALL_RECORD_SIZE]);
+
+ byte[] largeVal = new byte[LARGE_RECORD_SIZE];
+
+ AtomicLong errors = new AtomicLong();
+
+ AtomicReference firstErr = new AtomicReference<>();
+
+ CountDownLatch startLatch = new CountDownLatch(1);
+
+ long deadline = System.currentTimeMillis() + DEADLINE;
+
+ Thread[] threads = new Thread[THREADS];
+
+ for (int i = 0; i < THREADS; i++) {
+ final int threadIdx = i;
+
+ threads[i] = new Thread(() -> {
+ try {
+ startLatch.await();
+
+ for (int k = 0; k < LARGE_ROWS_PER_THREAD; k++)
+ cache.put(SMALL_ENTRIES + threadIdx * LARGE_ROWS_PER_THREAD + k, largeVal);
+ }
+ catch (Throwable e) {
+ errors.incrementAndGet();
+
+ firstErr.compareAndSet(null, e);
+
+ log.error("Unexpected error in writer thread", e);
+ }
+ }, "paged-writer-" + i);
+
+ threads[i].start();
+ }
+
+ startLatch.countDown();
+
+ long start = System.currentTimeMillis();
+
+ for (Thread t : threads)
+ t.join(Math.max(1, deadline - System.currentTimeMillis()));
+
+ // The core assertion of this deadlock test: every writer must have completed (no thread is stuck waiting on
+ // an entry lock held by size-aware eviction running under another entry lock).
+ for (Thread t : threads) {
+ if (t.isAlive()) {
+ log.error("Writer thread " + t.getName() + " is still alive after " +
+ (System.currentTimeMillis() - start) + "ms, state=" + t.getState());
+
+ for (StackTraceElement frame : t.getStackTrace())
+ log.error(" at " + frame);
+ }
+ }
+
+ for (Thread t : threads)
+ assertFalse("Writer thread " + t.getName() + " did not finish (possible deadlock)", t.isAlive());
+
+ assertEquals("Writer threads reported errors, reason: " + firstErr.get(), 0, errors.get());
+ }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java
new file mode 100644
index 0000000000000..4c706d7347f2b
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataPageEvictionMode;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionSizeAwareAbstractTest.isOutOfMemory;
+
+/**
+ * Negative test for the size-aware eviction progress guard.
+ *
+ * When every resident entry is locked by another thread/transaction, page eviction cannot free any page: the guarded
+ * {@code tryLockEntry(0)} in {@code evictInternal} fails for every candidate, so {@code ensureFreeSpaceForEviction}
+ * makes no progress and must fail with an
+ * {@code IgniteOutOfMemoryException} within bounded time instead of busy-spinning forever (deadlock).
+ *
+ * The test is self-guarded by {@code @Test(timeout = ...)}: a deadlock or unbounded busy-spin would fail the
+ * deadline.
+ */
+public class PageEvictionGuardOomTest extends GridCommonAbstractTest {
+ /** Off-heap region size. */
+ private static final int SIZE = 12 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Small record size chosen to occupy roughly one data page ({@link DFLT_PAGE_SIZE}) each. */
+ private static final int FILL_VALUE_SIZE = 3_800;
+
+ /**
+ * Number of resident entries (each ~one page) filling the region to ~55% of its capacity. This keeps the region
+ * comfortably below the eviction threshold (so the ordinary threshold-based {@code ensureFreeSpace} path is a
+ * no-op) while leaving less free space than a single large record needs, so the size-aware eviction guard is
+ * exercised.
+ */
+ private static final int FILL_ENTRIES = 1_600;
+
+ /** Large record size that does not fit into the remaining free space (requires eviction to be stored). */
+ private static final int LARGE_RECORD_SIZE = 8 * 1024 * 1024;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ .setPageEvictionMode(DataPageEvictionMode.RANDOM_LRU))
+ .setPageSize(DFLT_PAGE_SIZE));
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page overhead).
+ */
+ private IgniteCache createCache(IgniteEx ignite) {
+ // TRANSACTIONAL is required so that cache.lockAll(...) can hold entry locks (the root cause of the
+ // "no evictable page" scenario this test exercises).
+ return ignite.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS))
+ .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));
+ }
+
+ /**
+ * Filling the region with locked entries and then writing a row that needs more free pages than remain must fail
+ * with OOM (bounded time), not hang: eviction cannot free any page because every candidate entry is locked.
+ *
+ * @throws Exception If failed.
+ */
+ @Test(timeout = 180_000)
+ public void testGuardOomWhenAllEntriesLocked() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache cache = createCache(ignite);
+
+ // Pre-fill the region so that less than one large record of free space remains, without overflowing it.
+ byte[] fillVal = new byte[FILL_VALUE_SIZE];
+
+ for (int i = 1; i <= FILL_ENTRIES; i++)
+ cache.put(i, fillVal);
+
+ Collection keys = new ArrayList<>(FILL_ENTRIES);
+
+ for (int i = 1; i <= FILL_ENTRIES; i++)
+ keys.add(i);
+
+ CountDownLatch ready = new CountDownLatch(1);
+
+ CountDownLatch release = new CountDownLatch(1);
+
+ AtomicReference lockerErr = new AtomicReference<>();
+
+ // Hold entry locks on every resident key from a background thread so that eviction has no evictable page.
+ Thread locker = new Thread(() -> {
+ try {
+ Lock lock = cache.lockAll(keys);
+
+ lock.lock();
+
+ ready.countDown();
+
+ release.await();
+
+ lock.unlock();
+ }
+ catch (Throwable e) {
+ lockerErr.set(e);
+
+ ready.countDown();
+ }
+ }, "size-aware-guard-locker");
+
+ locker.start();
+
+ try {
+ assertTrue("Timed out waiting for entries to be locked", ready.await(60, TimeUnit.SECONDS));
+
+ assertNull("Unexpected error while locking entries: " + lockerErr.get(), lockerErr.get());
+
+ try {
+ cache.put(FILL_ENTRIES + 1, new byte[LARGE_RECORD_SIZE]);
+
+ fail("Expected out-of-memory because all resident entries are locked, but put succeeded");
+ }
+ catch (Exception e) {
+ assertTrue("Expected an out-of-memory (progress guard) failure, but got: " + e, isOutOfMemory(e));
+ }
+ }
+ finally {
+ release.countDown();
+
+ locker.join(TimeUnit.SECONDS.toMillis(10));
+ }
+ }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java
index 9f40cf4958431..6efd5b6bffbd0 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java
@@ -49,6 +49,36 @@ public void testPageEvictionMetric() throws Exception {
checkPageEvictionMetric(CacheAtomicityMode.ATOMIC);
}
+ /**
+ * Regression: ordinary small records that keep the region below the eviction threshold must not trigger page
+ * eviction at all (eviction is not started, eviction rate stays zero).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testNoEvictionBelowThreshold() throws Exception {
+ IgniteEx ignite = startGrid(0);
+
+ DataRegionMetricsImpl metrics = ignite.context().cache().context().database().dataRegion(null).metrics();
+
+ metrics.enableMetrics();
+
+ CacheConfiguration