Copilot commented on code in PR #13554:
URL: https://github.com/apache/ignite/pull/13554#discussion_r3941217775


##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java:
##########
@@ -4182,6 +4205,23 @@ private int extrasSize() {
         lock.lock();
     }
 
+    /**
+     * Acquires the entry lock either blocking ({@code tryLock == false}) or 
non-blockingly with the configured
+     * {@link #ENTRY_LOCK_TIMEOUT} ({@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 tryLockEntry(ENTRY_LOCK_TIMEOUT);

Review Comment:
   This is neither truly non-blocking nor safe for the entry already owned by 
the insertion thread: the default timeout waits up to one second per candidate, 
and `ReentrantLock.tryLock` succeeds reentrantly for the current entry, 
allowing an update's old row to be evicted while it is being replaced. Skip 
entries locked by this thread and use an immediate try-lock; this also prevents 
the 300-attempt guard from stalling for minutes.



##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -1216,24 +1241,128 @@ public void ensureFreeSpaceForInsert(DataRegion 
region, int dataRowSize) throws
         boolean oomThreshold = (memorySize / 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 region has enough
+     * available pages to accommodate the row, or throws {@link 
IgniteOutOfMemoryException} if the goal is
+     * unreachable / no progress can be made.
+     *
+     * @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();
+
+        CacheFreeList freeList = freeListMap.get(regCfg.getName());
 
-            if (cctx.kernalContext() != null)
-                cctx.kernalContext().failure().process(new 
FailureContext(FailureType.CRITICAL_ERROR, oom));
+        if (freeList == null)
+            return;
+
+        long sysPageSize = pageMem.systemPageSize();
+        long pageSize = pageMem.pageSize();
+
+        long totalPages = regCfg.getMaxSize() / sysPageSize;
+
+        // Pages required to place the row (rounded up) plus a margin for the 
page header and fragmentation.
+        long requiredPages = (dataRowSize + pageSize - 1) / pageSize + 1;
+
+        // If the row fits into the configured steady-state empty-pages pool, 
normal threshold eviction is enough.
+        if (requiredPages <= regCfg.getEmptyPagesPoolSize())
+            return;
 
-            throw oom;
+        // The row fundamentally cannot fit into the whole region.
+        if (requiredPages > totalPages)
+            throw outOfMemory(regCfg);
+
+        long availablePages = (totalPages - pageMem.loadedPages()) + 
freeList.emptyDataPages();
+
+        // Fast path: enough pages are already available, no eviction is 
needed.
+        if (availablePages >= requiredPages)
+            return;
+
+        PageEvictionTracker evictionTracker = region.evictionTracker();
+
+        // Evict data pages until enough free space is available. Progress is 
measured against the overall available
+        // space, so pages freed concurrently (e.g. by TTL cleanup) also count 
as progress. The loop is bounded to
+        // avoid an infinite busy-spin when there is nothing more to evict. 
Eviction here runs while the current
+        // thread may already hold entry locks (single-row insertion), so 
entries whose locks are contended are
+        // skipped (non-blocking) rather than blocked upon, to avoid a 
lock-ordering deadlock.
+        final int maxAttemptsWithoutProgress = 300;
+
+        long bestAvailable = availablePages;
+        int attemptsWithoutProgress = 0;
+
+        while (bestAvailable < requiredPages) {
+            evictDataPageNonBlocking(evictionTracker);
+
+            long curAvailable = (totalPages - pageMem.loadedPages()) + 
freeList.emptyDataPages();
+
+            // Progress is measured against the best available space observed 
so far. Concurrent inserts may
+            // temporarily reduce available (loadedPages grows) even while 
eviction is freeing pages, so a drop below
+            // the running best is not treated as "no progress". Only when 
available fails to exceed the best value
+            // over many attempts we conclude that no more space can be freed 
(e.g. all candidate entries are locked
+            // by other threads/transactions).
+            if (curAvailable > bestAvailable) {
+                bestAvailable = curAvailable;
+
+                attemptsWithoutProgress = 0;
+            }
+            else if (curAvailable < bestAvailable) {
+                // A transient drop caused by concurrent activity: keep the 
best value, do not penalize.
+            }
+            else
+                attemptsWithoutProgress++;

Review Comment:
   A drop below `bestAvailable` never increments the no-progress counter. If 
another writer consumes pages after an initial improvement and eviction then 
cannot free anything, `curAvailable` remains below the historical best forever 
and this supposedly bounded loop busy-spins indefinitely. Count every iteration 
that does not establish a new best toward the guard.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java:
##########
@@ -0,0 +1,193 @@
+/*
+ * 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.internal.mem.IgniteOutOfMemoryException;
+import 
org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static 
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Negative test for the size-aware eviction progress guard.
+ * <p>
+ * When every resident entry is locked by another thread/transaction, page 
eviction cannot free any page: the guarded
+ * {@code tryLockEntry} in {@code evictInternal} fails for every candidate, so 
{@link
+ * IgniteCacheDatabaseSharedManager#ensureFreeSpaceForEviction} makes no 
progress and must fail with an
+ * {@code IgniteOutOfMemoryException} within bounded time instead of 
busy-spinning forever (deadlock).
+ * <p>
+ * The lock timeout is reduced via {@code -DENTRY_LOCK_TIMEOUT=1} (applied 
through {@code @WithSystemProperty} before
+ * the node starts) so that each non-blocking lock attempt fails quickly and 
the whole guard run stays within a few
+ * seconds. 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<Integer, Object> 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<Integer, 
Object>(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)
+    @WithSystemProperty(key = "ENTRY_LOCK_TIMEOUT", value = "1")

Review Comment:
   This method-level property is applied after `GridCacheMapEntry` has already 
been initialized by the earlier eviction tests in 
`IgniteCacheEvictionSelfTestSuite`, while `ENTRY_LOCK_TIMEOUT` is a static 
final value read at class initialization. In the normal suite this test 
therefore uses the 1000 ms default, making the 300-attempt guard exceed the 
180-second test timeout. Set the property before the class can be initialized 
or remove the test's dependence on that cached static value.



##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -1216,24 +1241,128 @@ public void ensureFreeSpaceForInsert(DataRegion 
region, int dataRowSize) throws
         boolean oomThreshold = (memorySize / 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 region has enough
+     * available pages to accommodate the row, or throws {@link 
IgniteOutOfMemoryException} if the goal is
+     * unreachable / no progress can be made.
+     *
+     * @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();
+
+        CacheFreeList freeList = freeListMap.get(regCfg.getName());
 
-            if (cctx.kernalContext() != null)
-                cctx.kernalContext().failure().process(new 
FailureContext(FailureType.CRITICAL_ERROR, oom));
+        if (freeList == null)
+            return;
+
+        long sysPageSize = pageMem.systemPageSize();
+        long pageSize = pageMem.pageSize();
+
+        long totalPages = regCfg.getMaxSize() / sysPageSize;
+
+        // Pages required to place the row (rounded up) plus a margin for the 
page header and fragmentation.
+        long requiredPages = (dataRowSize + pageSize - 1) / pageSize + 1;

Review Comment:
   The reserve divides by the raw page size and adds only one page, but every 
fragment carries at most `pageSize - AbstractDataPageIO.MIN_DATA_PAGE_OVERHEAD` 
bytes. With 4 KiB pages, for example, a 4 MiB row needs about 1041 data pages 
while this computes 1025, so under pressure insertion can still run out of 
pages after this method reports sufficient space. Compute the requirement from 
the actual data-page payload capacity instead of using a fixed one-page margin.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java:
##########
@@ -0,0 +1,140 @@
+/*
+ * 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 javax.cache.expiry.CreatedExpiryPolicy;
+import javax.cache.expiry.Duration;
+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 java.util.concurrent.TimeUnit.MILLISECONDS;
+import static 
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Tests the synergy between ExpiryPolicy (TTL cleanup) and size-aware page 
eviction on an in-memory data region.
+ * Verifies that concurrent TTL cleanup and eviction do not deadlock, that a 
large row larger than the
+ * empty-pages pool is still written when eviction is enabled, and that 
TTL-freed space is accounted for by eviction
+ * (a row that only fits after expired entries are removed is still written 
without OOM).
+ */
+public abstract class PageEvictionWithExpiryPolicyAbstractTest extends 
GridCommonAbstractTest {
+    /** Off-heap region size. */
+    private static final int SIZE = 128 * 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 (much larger than the empty-pages pool). */
+    private static final int RECORD_SIZE = 8 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Short TTL applied to some entries. */
+    private static final long TTL = 1500;
+
+    /** {@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.
+     * @param ttl TTL in milliseconds ({@code 0} for no expiry).
+     * @return Cache with a small partition count and, if {@code ttl > 0}, 
eager TTL expiry.
+     */
+    private IgniteCache<Integer, Object> createCache(IgniteEx ignite, long 
ttl) {
+        CacheConfiguration<Integer, Object> ccfg = new 
CacheConfiguration<Integer, Object>(DEFAULT_CACHE_NAME)
+            .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS));
+
+        if (ttl > 0) {
+            ccfg.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new 
Duration(MILLISECONDS, ttl)))
+                .setEagerTtl(true);
+        }
+
+        return ignite.createCache(ccfg);
+    }
+
+    /**
+     * Concurrent TTL cleanup and eviction must not deadlock, and a large 
record (larger than the empty-pages pool)
+     * must still be stored on a region with enabled eviction even in the 
presence of short-TTL entries.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testLargePutWithExpiryNoDeadlock() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        // Short-TTL entries keep the TTL worker actively freeing pages while 
eviction runs.
+        IgniteCache<Integer, Object> cache = createCache(ignite, TTL);
+
+        Object val = new byte[RECORD_SIZE];
+
+        // Writing more data than the region can hold forces eviction; 
concurrent expiry of short-TTL entries must not
+        // deadlock with it. The test itself is protected against a hang by 
the framework test timeout.
+        for (int i = 0; i < 30; i++)
+            cache.put(i, val);
+
+        cache.get(0);
+    }
+
+    /**
+     * Space freed by TTL cleanup must be taken into account by size-aware 
eviction: a large record written after some
+     * entries have expired must be accepted (no OOM) because their pages 
become available.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testTtlFreedSpaceAccountedForByEviction() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite, TTL);
+
+        // Fill the region up to its capacity with short-TTL large records.
+        Object val = new byte[RECORD_SIZE];
+
+        for (int i = 0; i < 10; i++)
+            cache.put(i, val);

Review Comment:
   This setup writes roughly 80 MiB into a 128 MiB region and then inserts only 
one additional 8 MiB value, so the final put has ample space even if no TTL 
entry was ever removed. The test therefore passes without exercising the 
claimed TTL-freed-space accounting; fill the region until the final row cannot 
fit, verify the expiring entries are gone, and only then perform the final put.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,225 @@
+/*
+ * 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.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+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;
+
+/**
+ * Tests size-aware page eviction on in-memory (non-persistent) data regions.
+ *
+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize} 
(in pages) is still written successfully
+ * when page eviction is enabled, by evicting old entries to free enough 
space. Also verifies that a row
+ * which fundamentally cannot fit into the region fails with OOM instead of 
hanging in an infinite eviction loop.
+ *
+ * Note: the atomic DHT batch path (putAll of many large rows overflowing a 
small region) is out of scope here — it is
+ * handled by a separate size-aware reserve in the batch store path and 
already fails on the original code.
+ */
+public abstract class PageEvictionSizeAwareAbstractTest extends 
GridCommonAbstractTest {
+    /** Off-heap region size (large enough to hold cache structural pages with 
the configured partition count). */
+    private static final int SIZE = 128 * 1024 * 1024;
+
+    /** Partition count (kept low so that index-tree structures do not exhaust 
the region). */
+    private static final int PARTITIONS = 32;
+
+    /** Record size: chosen to be much larger than {@code emptyPagesPoolSize} 
pages. */
+    private static final int RECORD_SIZE = 4 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Entry count to accumulate beyond the region capacity. */
+    private static final int ENTRIES = 40;
+
+    /** Small record size used to pre-fill the region with evictable data (for 
putAll tests). */
+    private static final int SMALL_RECORD_SIZE = 4096;
+
+    /** Small pre-fill entries count. */
+    private static final int SMALL_ENTRIES = 8000;
+
+    /** Large rows written via putAll. */
+    private static final int PUT_ALL_LARGE_ROWS = 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<Integer, Object> createCache(IgniteEx ignite) {
+        return ignite.createCache(new CacheConfiguration<Integer, 
Object>(DEFAULT_CACHE_NAME)
+            .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+    }
+
+    /**
+     * A large record (larger than the empty-pages pool) must be stored 
without OOM when there is evictable data,
+     * by evicting previously stored records to free enough space.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPutLargeObjectsDoesNotOom() throws Exception {
+        IgniteEx ignite = startGrids(2);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        Object val = new byte[RECORD_SIZE];
+
+        // Total data (ENTRIES * RECORD_SIZE) exceeds the region size, so at 
least some records must be evicted.
+        for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME), 
ENTRIES))
+            cache.put(key, val);
+
+        // Eviction must have bounded the number of resident entries.
+        assertTrue("Expected some entries to be evicted, but cache.size()=" + 
cache.size(),
+            cache.size() > 0 && cache.size() < ENTRIES);
+    }
+
+    /**
+     * A large record written must be readable right away (the just-written 
entry is the most recently used and is not
+     * a candidate for eviction before the write completes).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testLargeObjectReadBack() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        byte[] val = new byte[RECORD_SIZE];
+
+        Arrays.fill(val, (byte)42);
+
+        cache.put(1, val);
+
+        byte[] read = (byte[])cache.get(1);
+
+        assertNotNull("Large value must be readable after put", read);
+
+        assertTrue("Value read back must equal the stored value", 
Arrays.equals(val, read));
+    }
+
+    /**
+     * A record larger than the whole region must fail (not hang) even when 
size-aware eviction is enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRecordLargerThanRegionOom() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        boolean rejected = false;
+
+        try {
+            cache.put(1, new byte[SIZE * 2]);
+        }
+        catch (Exception e) {
+            // OOM (possibly wrapped) because the row cannot fit into the 
region.
+            rejected = true;
+        }
+
+        assertTrue("Record larger than the region must be rejected (no hang), 
but put succeeded", rejected);
+    }
+
+    /**
+     * A batch putAll of several large records (each larger than the 
empty-pages pool) must be stored successfully when
+     * page eviction is enabled. Exercises the size-aware reserve in the batch 
store path ({@code RowStore.addRows}).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPutAllLargeRows() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        // Pre-fill with small evictable entries so large rows below region 
capacity fit via the reserve path.
+        byte[] small = new byte[SMALL_RECORD_SIZE];
+
+        for (int i = 0; i < SMALL_ENTRIES; i++)
+            cache.put(SMALL_ENTRIES + i, small);
+
+        Map<Integer, Object> large = new HashMap<>();
+
+        Object val = new byte[RECORD_SIZE];
+
+        for (int i = 0; i < PUT_ALL_LARGE_ROWS; i++)
+            large.put(i, val);
+
+        cache.putAll(large);
+
+        for (int i = 0; i < PUT_ALL_LARGE_ROWS; i++)
+            assertNotNull("Large row " + i + " must be readable after putAll", 
cache.get(i));
+    }
+
+    /**
+     * Updating a record from a small to a large value (larger than the 
empty-pages pool) must succeed with page
+     * eviction enabled: the update goes through the same size-aware reserve 
as an insert.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testUpdateRowGrows() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        cache.put(1, new byte[1024]);
+
+        byte[] big = new byte[RECORD_SIZE];
+
+        Arrays.fill(big, (byte)7);
+
+        cache.put(1, big);

Review Comment:
   This update runs in an otherwise empty 128 MiB region, so growing from 1 KiB 
to 4 MiB never needs size-aware eviction. It would pass through the old path as 
well and does not cover the lock-sensitive update scenario described by the 
test. Prefill with evictable entries until the grown value cannot fit without 
eviction before updating key 1.



##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -1216,24 +1241,128 @@ public void ensureFreeSpaceForInsert(DataRegion 
region, int dataRowSize) throws
         boolean oomThreshold = (memorySize / 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 region has enough
+     * available pages to accommodate the row, or throws {@link 
IgniteOutOfMemoryException} if the goal is
+     * unreachable / no progress can be made.
+     *
+     * @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();
+
+        CacheFreeList freeList = freeListMap.get(regCfg.getName());
 
-            if (cctx.kernalContext() != null)
-                cctx.kernalContext().failure().process(new 
FailureContext(FailureType.CRITICAL_ERROR, oom));
+        if (freeList == null)
+            return;
+
+        long sysPageSize = pageMem.systemPageSize();
+        long pageSize = pageMem.pageSize();
+
+        long totalPages = regCfg.getMaxSize() / sysPageSize;
+
+        // Pages required to place the row (rounded up) plus a margin for the 
page header and fragmentation.
+        long requiredPages = (dataRowSize + pageSize - 1) / pageSize + 1;
+
+        // If the row fits into the configured steady-state empty-pages pool, 
normal threshold eviction is enough.
+        if (requiredPages <= regCfg.getEmptyPagesPoolSize())
+            return;
 
-            throw oom;
+        // The row fundamentally cannot fit into the whole region.
+        if (requiredPages > totalPages)
+            throw outOfMemory(regCfg);
+
+        long availablePages = (totalPages - pageMem.loadedPages()) + 
freeList.emptyDataPages();
+
+        // Fast path: enough pages are already available, no eviction is 
needed.
+        if (availablePages >= requiredPages)
+            return;
+
+        PageEvictionTracker evictionTracker = region.evictionTracker();
+
+        // Evict data pages until enough free space is available. Progress is 
measured against the overall available
+        // space, so pages freed concurrently (e.g. by TTL cleanup) also count 
as progress. The loop is bounded to
+        // avoid an infinite busy-spin when there is nothing more to evict. 
Eviction here runs while the current
+        // thread may already hold entry locks (single-row insertion), so 
entries whose locks are contended are
+        // skipped (non-blocking) rather than blocked upon, to avoid a 
lock-ordering deadlock.
+        final int maxAttemptsWithoutProgress = 300;
+
+        long bestAvailable = availablePages;
+        int attemptsWithoutProgress = 0;
+
+        while (bestAvailable < requiredPages) {
+            evictDataPageNonBlocking(evictionTracker);
+
+            long curAvailable = (totalPages - pageMem.loadedPages()) + 
freeList.emptyDataPages();

Review Comment:
   This new eviction path bypasses the existing metric bookkeeping in 
`ensureFreeSpace`: actual size-aware evictions neither set 
`isEvictionsStarted()` nor increment the eviction rate. Record the start and 
rate here as well so operational metrics reflect all page eviction.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,225 @@
+/*
+ * 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.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+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;
+
+/**
+ * Tests size-aware page eviction on in-memory (non-persistent) data regions.
+ *
+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize} 
(in pages) is still written successfully
+ * when page eviction is enabled, by evicting old entries to free enough 
space. Also verifies that a row
+ * which fundamentally cannot fit into the region fails with OOM instead of 
hanging in an infinite eviction loop.
+ *
+ * Note: the atomic DHT batch path (putAll of many large rows overflowing a 
small region) is out of scope here — it is
+ * handled by a separate size-aware reserve in the batch store path and 
already fails on the original code.
+ */
+public abstract class PageEvictionSizeAwareAbstractTest extends 
GridCommonAbstractTest {
+    /** Off-heap region size (large enough to hold cache structural pages with 
the configured partition count). */
+    private static final int SIZE = 128 * 1024 * 1024;
+
+    /** Partition count (kept low so that index-tree structures do not exhaust 
the region). */
+    private static final int PARTITIONS = 32;
+
+    /** Record size: chosen to be much larger than {@code emptyPagesPoolSize} 
pages. */
+    private static final int RECORD_SIZE = 4 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Entry count to accumulate beyond the region capacity. */
+    private static final int ENTRIES = 40;
+
+    /** Small record size used to pre-fill the region with evictable data (for 
putAll tests). */
+    private static final int SMALL_RECORD_SIZE = 4096;
+
+    /** Small pre-fill entries count. */
+    private static final int SMALL_ENTRIES = 8000;
+
+    /** Large rows written via putAll. */
+    private static final int PUT_ALL_LARGE_ROWS = 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<Integer, Object> createCache(IgniteEx ignite) {
+        return ignite.createCache(new CacheConfiguration<Integer, 
Object>(DEFAULT_CACHE_NAME)
+            .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+    }
+
+    /**
+     * A large record (larger than the empty-pages pool) must be stored 
without OOM when there is evictable data,
+     * by evicting previously stored records to free enough space.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPutLargeObjectsDoesNotOom() throws Exception {
+        IgniteEx ignite = startGrids(2);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        Object val = new byte[RECORD_SIZE];
+
+        // Total data (ENTRIES * RECORD_SIZE) exceeds the region size, so at 
least some records must be evicted.
+        for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME), 
ENTRIES))
+            cache.put(key, val);
+
+        // Eviction must have bounded the number of resident entries.
+        assertTrue("Expected some entries to be evicted, but cache.size()=" + 
cache.size(),
+            cache.size() > 0 && cache.size() < ENTRIES);
+    }
+
+    /**
+     * A large record written must be readable right away (the just-written 
entry is the most recently used and is not
+     * a candidate for eviction before the write completes).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testLargeObjectReadBack() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        byte[] val = new byte[RECORD_SIZE];
+
+        Arrays.fill(val, (byte)42);
+
+        cache.put(1, val);
+
+        byte[] read = (byte[])cache.get(1);
+
+        assertNotNull("Large value must be readable after put", read);
+
+        assertTrue("Value read back must equal the stored value", 
Arrays.equals(val, read));
+    }
+
+    /**
+     * A record larger than the whole region must fail (not hang) even when 
size-aware eviction is enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRecordLargerThanRegionOom() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        boolean rejected = false;
+
+        try {
+            cache.put(1, new byte[SIZE * 2]);
+        }
+        catch (Exception e) {
+            // OOM (possibly wrapped) because the row cannot fit into the 
region.
+            rejected = true;
+        }
+
+        assertTrue("Record larger than the region must be rejected (no hang), 
but put succeeded", rejected);
+    }
+
+    /**
+     * A batch putAll of several large records (each larger than the 
empty-pages pool) must be stored successfully when
+     * page eviction is enabled. Exercises the size-aware reserve in the batch 
store path ({@code RowStore.addRows}).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPutAllLargeRows() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        // Pre-fill with small evictable entries so large rows below region 
capacity fit via the reserve path.
+        byte[] small = new byte[SMALL_RECORD_SIZE];
+
+        for (int i = 0; i < SMALL_ENTRIES; i++)
+            cache.put(SMALL_ENTRIES + i, small);

Review Comment:
   The prefill is only about 32 MiB and the three large rows add about 12 MiB 
in a 128 MiB region. Consequently `ensureFreeSpaceForInsert` takes its fast 
path and this test succeeds without exercising the new batch reserve. Prefill 
until less than one large row remains available, then verify `putAll` succeeds 
by evicting the prefilled entries.



##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java:
##########
@@ -134,6 +135,20 @@ public void addRow(CacheDataRow row, IoStatisticsHolder 
statHolder) throws Ignit
      */
     public void addRows(Collection<? extends CacheDataRow> rows,
         IoStatisticsHolder statHolder) throws IgniteCheckedException {
+        if (!persistenceEnabled && 
grp.dataRegion().config().getPageEvictionMode() != 
DataPageEvictionMode.DISABLED) {
+            // Size-aware reserve for the largest row in the batch. Eviction 
performed here runs without entry locks
+            // (see AbstractFreeList#insertDataRows), so reserving space for 
any single large row is safe and keeps the
+            // batch path consistent with the single-row path. Smaller rows 
are covered by the regular
+            // threshold eviction loop inside insertDataRows.
+            int maxRowSize = 0;
+
+            for (CacheDataRow row : rows)
+                maxRowSize = Math.max(maxRowSize, row.size());
+
+            if (maxRowSize > 0)
+                ctx.database().ensureFreeSpaceForInsert(grp.dataRegion(), 
maxRowSize);

Review Comment:
   Reserving only the largest row does not make a batch safe: `insertDataRows` 
consumes that reserve while writing the first large row, and its per-row 
threshold loop only restores `emptyPagesPoolSize`, which is explicitly smaller 
than these rows. A later large row can therefore still exhaust page memory. 
Perform the size-aware check before each large row is written (or reserve a 
safely calculated aggregate for the batch).



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java:
##########
@@ -0,0 +1,193 @@
+/*
+ * 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.internal.mem.IgniteOutOfMemoryException;
+import 
org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static 
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Negative test for the size-aware eviction progress guard.
+ * <p>
+ * When every resident entry is locked by another thread/transaction, page 
eviction cannot free any page: the guarded
+ * {@code tryLockEntry} in {@code evictInternal} fails for every candidate, so 
{@link
+ * IgniteCacheDatabaseSharedManager#ensureFreeSpaceForEviction} makes no 
progress and must fail with an
+ * {@code IgniteOutOfMemoryException} within bounded time instead of 
busy-spinning forever (deadlock).
+ * <p>
+ * The lock timeout is reduced via {@code -DENTRY_LOCK_TIMEOUT=1} (applied 
through {@code @WithSystemProperty} before
+ * the node starts) so that each non-blocking lock attempt fails quickly and 
the whole guard run stays within a few
+ * seconds. 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<Integer, Object> 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<Integer, 
Object>(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)
+    @WithSystemProperty(key = "ENTRY_LOCK_TIMEOUT", value = "1")
+    public void testGuardOomWhenAllEntriesLocked() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> 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<Integer> 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<Throwable> 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();
+
+        assertTrue("Timed out waiting for entries to be locked", 
ready.await(60, TimeUnit.SECONDS));
+
+        assertNull("Unexpected error while locking entries: " + 
lockerErr.get(), lockerErr.get());

Review Comment:
   The setup assertions execute before the `finally` that releases the locker. 
If lock acquisition times out or the locker reports an error, the test exits 
while a non-daemon thread remains blocked on `release.await()`, which can hang 
the suite. Enclose both assertions and the put in the release/join 
`try/finally`.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,225 @@
+/*
+ * 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.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+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;
+
+/**
+ * Tests size-aware page eviction on in-memory (non-persistent) data regions.
+ *
+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize} 
(in pages) is still written successfully
+ * when page eviction is enabled, by evicting old entries to free enough 
space. Also verifies that a row
+ * which fundamentally cannot fit into the region fails with OOM instead of 
hanging in an infinite eviction loop.
+ *
+ * Note: the atomic DHT batch path (putAll of many large rows overflowing a 
small region) is out of scope here — it is
+ * handled by a separate size-aware reserve in the batch store path and 
already fails on the original code.
+ */
+public abstract class PageEvictionSizeAwareAbstractTest extends 
GridCommonAbstractTest {
+    /** Off-heap region size (large enough to hold cache structural pages with 
the configured partition count). */
+    private static final int SIZE = 128 * 1024 * 1024;
+
+    /** Partition count (kept low so that index-tree structures do not exhaust 
the region). */
+    private static final int PARTITIONS = 32;
+
+    /** Record size: chosen to be much larger than {@code emptyPagesPoolSize} 
pages. */
+    private static final int RECORD_SIZE = 4 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Entry count to accumulate beyond the region capacity. */
+    private static final int ENTRIES = 40;
+
+    /** Small record size used to pre-fill the region with evictable data (for 
putAll tests). */
+    private static final int SMALL_RECORD_SIZE = 4096;
+
+    /** Small pre-fill entries count. */
+    private static final int SMALL_ENTRIES = 8000;
+
+    /** Large rows written via putAll. */
+    private static final int PUT_ALL_LARGE_ROWS = 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<Integer, Object> createCache(IgniteEx ignite) {
+        return ignite.createCache(new CacheConfiguration<Integer, 
Object>(DEFAULT_CACHE_NAME)
+            .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+    }
+
+    /**
+     * A large record (larger than the empty-pages pool) must be stored 
without OOM when there is evictable data,
+     * by evicting previously stored records to free enough space.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testPutLargeObjectsDoesNotOom() throws Exception {
+        IgniteEx ignite = startGrids(2);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        Object val = new byte[RECORD_SIZE];
+
+        // Total data (ENTRIES * RECORD_SIZE) exceeds the region size, so at 
least some records must be evicted.
+        for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME), 
ENTRIES))
+            cache.put(key, val);
+
+        // Eviction must have bounded the number of resident entries.
+        assertTrue("Expected some entries to be evicted, but cache.size()=" + 
cache.size(),
+            cache.size() > 0 && cache.size() < ENTRIES);
+    }
+
+    /**
+     * A large record written must be readable right away (the just-written 
entry is the most recently used and is not
+     * a candidate for eviction before the write completes).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testLargeObjectReadBack() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        byte[] val = new byte[RECORD_SIZE];
+
+        Arrays.fill(val, (byte)42);
+
+        cache.put(1, val);
+
+        byte[] read = (byte[])cache.get(1);
+
+        assertNotNull("Large value must be readable after put", read);
+
+        assertTrue("Value read back must equal the stored value", 
Arrays.equals(val, read));
+    }
+
+    /**
+     * A record larger than the whole region must fail (not hang) even when 
size-aware eviction is enabled.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRecordLargerThanRegionOom() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        boolean rejected = false;
+
+        try {
+            cache.put(1, new byte[SIZE * 2]);
+        }
+        catch (Exception e) {
+            // OOM (possibly wrapped) because the row cannot fit into the 
region.
+            rejected = true;
+        }

Review Comment:
   Any exception is treated as the expected OOM, so cache, topology, 
serialization, or assertion failures make this regression test pass 
incorrectly. Assert that the exception or one of its causes is specifically 
`IgniteOutOfMemoryException`, as the guard test in this PR already does.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to