alex-plekhanov commented on code in PR #13554:
URL: https://github.com/apache/ignite/pull/13554#discussion_r4069145477


##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** 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));

Review Comment:
   4 tests duplicate the same block, redefining these properties, some of them 
are default values. Maybe it's worth to move these blocks to some abstract 
class? Why do we need to redefine properties to default value?



##########
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.
+ * <p>
+ * 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).
+ * <p>
+ * 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)
+    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(() -> {

Review Comment:
   Can be simplified using GridTestUtils.runAsync (we don't need lockerErr in 
this case, and thread managing)



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java:
##########
@@ -0,0 +1,199 @@
+/*
+ * 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.internal.util.typedef.internal.U;
+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_DATA_REG_DEFAULT_NAME;
+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, and larger 
than the space left free when the region
+     * is held at the eviction threshold {@code (1 - threshold) * totalPages}) 
so that a large put cannot take the
+     * fast path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve. */
+    private static final int RECORD_SIZE = 32 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Size of a single non-expiring pre-fill record (well below a data-page 
payload so each record surely occupies
+     * exactly one data page). */
+    private static final int SMALL_RECORD_SIZE = 1024;
+
+    /** Number of non-expiring pre-fill records. Deliberately conservative: 
the pre-fill keeps a comfortable margin to
+     * the region capacity so that the large short-TTL records are guaranteed 
not to be evicted before they expire. */
+    private static final int SMALL_ENTRIES = 6_000;
+
+    /** Fresh large records written after TTL expiry. Together with the 
pre-fill ({@link #SMALL_ENTRIES} small entries,
+     * each occupying roughly one data page) plus the index structures, they 
exceed the region capacity (~32768 pages
+     * for a 128 MiB region with 4 KiB pages), forcing size-aware eviction 
that accounts for the TTL-freed space. */
+    private static final int FRESH_RECORDS = 12;
+
+    /** Short TTL applied to some entries. */
+    private static final long TTL = 8000;
+
+    /** {@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 cacheName Cache name.
+     * @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, String 
cacheName, long ttl) {
+        CacheConfiguration<Integer, Object> ccfg = new 
CacheConfiguration<Integer, Object>(cacheName)
+            .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, 
DEFAULT_CACHE_NAME, 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);

Review Comment:
   There can't be concurrent expire with 8 seconds TTL. Also chance to face 
with concurrency problem is miserable with only 30 entries.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java:
##########
@@ -0,0 +1,199 @@
+/*
+ * 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.internal.util.typedef.internal.U;
+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_DATA_REG_DEFAULT_NAME;
+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, and larger 
than the space left free when the region
+     * is held at the eviction threshold {@code (1 - threshold) * totalPages}) 
so that a large put cannot take the
+     * fast path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve. */
+    private static final int RECORD_SIZE = 32 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Size of a single non-expiring pre-fill record (well below a data-page 
payload so each record surely occupies
+     * exactly one data page). */
+    private static final int SMALL_RECORD_SIZE = 1024;
+
+    /** Number of non-expiring pre-fill records. Deliberately conservative: 
the pre-fill keeps a comfortable margin to
+     * the region capacity so that the large short-TTL records are guaranteed 
not to be evicted before they expire. */
+    private static final int SMALL_ENTRIES = 6_000;
+
+    /** Fresh large records written after TTL expiry. Together with the 
pre-fill ({@link #SMALL_ENTRIES} small entries,
+     * each occupying roughly one data page) plus the index structures, they 
exceed the region capacity (~32768 pages
+     * for a 128 MiB region with 4 KiB pages), forcing size-aware eviction 
that accounts for the TTL-freed space. */
+    private static final int FRESH_RECORDS = 12;

Review Comment:
   The test class contains two tests, but these global constants used only by 
one. Maybe it's worth to move it to local variables, since they are related 
only to one test



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java:
##########
@@ -0,0 +1,199 @@
+/*
+ * 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.internal.util.typedef.internal.U;
+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_DATA_REG_DEFAULT_NAME;
+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, and larger 
than the space left free when the region
+     * is held at the eviction threshold {@code (1 - threshold) * totalPages}) 
so that a large put cannot take the
+     * fast path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve. */
+    private static final int RECORD_SIZE = 32 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Size of a single non-expiring pre-fill record (well below a data-page 
payload so each record surely occupies
+     * exactly one data page). */
+    private static final int SMALL_RECORD_SIZE = 1024;
+
+    /** Number of non-expiring pre-fill records. Deliberately conservative: 
the pre-fill keeps a comfortable margin to
+     * the region capacity so that the large short-TTL records are guaranteed 
not to be evicted before they expire. */
+    private static final int SMALL_ENTRIES = 6_000;
+
+    /** Fresh large records written after TTL expiry. Together with the 
pre-fill ({@link #SMALL_ENTRIES} small entries,
+     * each occupying roughly one data page) plus the index structures, they 
exceed the region capacity (~32768 pages
+     * for a 128 MiB region with 4 KiB pages), forcing size-aware eviction 
that accounts for the TTL-freed space. */
+    private static final int FRESH_RECORDS = 12;
+
+    /** Short TTL applied to some entries. */
+    private static final long TTL = 8000;
+
+    /** {@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 cacheName Cache name.
+     * @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, String 
cacheName, long ttl) {
+        CacheConfiguration<Integer, Object> ccfg = new 
CacheConfiguration<Integer, Object>(cacheName)
+            .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, 
DEFAULT_CACHE_NAME, 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);
+
+        // The most recently written entry cannot have expired yet (TTL is far 
larger than this read), so a non-null
+        // read both verifies the cache is responsive after concurrent 
expiry/eviction (the test's goal) and is not
+        // racy. Reading the first written key would be racy (it may already 
have expired under the TTL).
+        assertNotNull("Cache must remain responsive after concurrent expiry 
and eviction", cache.get(29));
+    }
+
+    /**
+     * Space freed by TTL cleanup must be taken into account by size-aware 
eviction: large records written after some
+     * entries have expired must be accepted (no OOM) because their pages 
become available.
+     * <p>
+     * The region is pre-filled with non-expiring small entries and large 
short-TTL entries that later expire and free
+     * their pages. After expiry, fresh large records are written — totalling 
more than the space freed by TTL, so that
+     * the pre-fill plus the fresh records exceed the region size. The writes 
can only succeed because size-aware
+     * eviction accounts for the TTL-freed pages (as available) and frees 
further pages for the rest.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testTtlFreedSpaceAccountedForByEviction() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        // Non-expiring cache for prefill and the final large put.
+        IgniteCache<Integer, Object> plainCache = createCache(ignite, 
"plain-cache", 0);
+
+        // Short-TTL cache for entries that will expire and free pages.
+        IgniteCache<Integer, Object> ttlCache = createCache(ignite, 
"ttl-cache", TTL);
+
+        // Pre-fill the region with small non-expiring entries, but leave 
enough room for the large TTL entries to be
+        // written without evicting them (so they are guaranteed to be present 
until they expire).
+        byte[] small = new byte[SMALL_RECORD_SIZE];
+
+        for (int i = 0; i < SMALL_ENTRIES; i++)
+            plainCache.put(i, small);
+
+        info("Pre-fill done [entries=" + SMALL_ENTRIES + ", plainSize=" + 
plainCache.size() +
+            ", loadedPages=" + 
ignite.dataRegionMetrics(DFLT_DATA_REG_DEFAULT_NAME).getTotalAllocatedPages() + 
']');
+
+        // Add large short-TTL entries that occupy significant space and will 
expire. They fit in the remaining free
+        // space, and being the freshest entries they are not evicted while 
they are being stored.
+        Object val = new byte[RECORD_SIZE];
+
+        for (int i = 0; i < 2; i++)
+            ttlCache.put(i, val);
+
+        // Verify the TTL entries are present before expiry.
+        assertNotNull("TTL entry must be present before expiry", 
ttlCache.get(0));
+
+        // Wait for the TTL worker to expire and free the short-TTL entries. 
Polled instead of a fixed sleep so that a
+        // slow CI machine does not proceed before the entries have actually 
expired.
+        long expiryDeadline = System.currentTimeMillis() + 30_000;
+
+        while (ttlCache.get(0) != null && System.currentTimeMillis() < 
expiryDeadline)
+            U.sleep(200);
+
+        // Verify the TTL entries have expired.
+        assertNull("TTL entry must be expired", ttlCache.get(0));
+
+        // Now write large records to the non-expiring cache, totalling more 
than the space freed by TTL (the prefill

Review Comment:
   > totalling more than the space freed by TTL 
   
   Inserted 12 records * 1 Kb (12 Kb total), expired 3 records * 32Mb (96 Mb 
total)
   
   Moreover entries in plain cache are replaced (keys used by preloaded entries 
0-6000, keys for fresh inserted entries 100 - 111). 
   
   Please validate comments after AI agents or decrease amount of such 
comments, since comments are also should be validated by reviewer, and 
sometimes they don't add any value, but add work to do.
   
   Constants are used for all values, but 100 is hardcoded (and looks like 
hardcoded with incorrect value)
   
   IMO the more relevant test scenario: after expiring, put the same large 
values to plain cache and check that preloaded SMALL_ENTRIES are not evicted.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** 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.
+     * <p>
+     * This scenario intentionally overlaps with

Review Comment:
   Additional coverage is gained by other tests, why do we need to duplicate 
this test?



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java:
##########
@@ -0,0 +1,199 @@
+/*
+ * 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.internal.util.typedef.internal.U;
+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_DATA_REG_DEFAULT_NAME;
+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, and larger 
than the space left free when the region
+     * is held at the eviction threshold {@code (1 - threshold) * totalPages}) 
so that a large put cannot take the
+     * fast path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve. */
+    private static final int RECORD_SIZE = 32 * 1024 * 1024;
+
+    /** Empty pages pool size. */
+    private static final int POOL_SIZE = 100;
+
+    /** Size of a single non-expiring pre-fill record (well below a data-page 
payload so each record surely occupies
+     * exactly one data page). */
+    private static final int SMALL_RECORD_SIZE = 1024;
+
+    /** Number of non-expiring pre-fill records. Deliberately conservative: 
the pre-fill keeps a comfortable margin to
+     * the region capacity so that the large short-TTL records are guaranteed 
not to be evicted before they expire. */
+    private static final int SMALL_ENTRIES = 6_000;
+
+    /** Fresh large records written after TTL expiry. Together with the 
pre-fill ({@link #SMALL_ENTRIES} small entries,
+     * each occupying roughly one data page) plus the index structures, they 
exceed the region capacity (~32768 pages
+     * for a 128 MiB region with 4 KiB pages), forcing size-aware eviction 
that accounts for the TTL-freed space. */
+    private static final int FRESH_RECORDS = 12;
+
+    /** Short TTL applied to some entries. */
+    private static final long TTL = 8000;
+
+    /** {@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 cacheName Cache name.
+     * @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, String 
cacheName, long ttl) {
+        CacheConfiguration<Integer, Object> ccfg = new 
CacheConfiguration<Integer, Object>(cacheName)
+            .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, 
DEFAULT_CACHE_NAME, 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);
+
+        // The most recently written entry cannot have expired yet (TTL is far 
larger than this read), so a non-null
+        // read both verifies the cache is responsive after concurrent 
expiry/eviction (the test's goal) and is not
+        // racy. Reading the first written key would be racy (it may already 
have expired under the TTL).
+        assertNotNull("Cache must remain responsive after concurrent expiry 
and eviction", cache.get(29));
+    }
+
+    /**
+     * Space freed by TTL cleanup must be taken into account by size-aware 
eviction: large records written after some
+     * entries have expired must be accepted (no OOM) because their pages 
become available.
+     * <p>
+     * The region is pre-filled with non-expiring small entries and large 
short-TTL entries that later expire and free
+     * their pages. After expiry, fresh large records are written — totalling 
more than the space freed by TTL, so that
+     * the pre-fill plus the fresh records exceed the region size. The writes 
can only succeed because size-aware
+     * eviction accounts for the TTL-freed pages (as available) and frees 
further pages for the rest.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testTtlFreedSpaceAccountedForByEviction() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        // Non-expiring cache for prefill and the final large put.
+        IgniteCache<Integer, Object> plainCache = createCache(ignite, 
"plain-cache", 0);
+
+        // Short-TTL cache for entries that will expire and free pages.
+        IgniteCache<Integer, Object> ttlCache = createCache(ignite, 
"ttl-cache", TTL);
+
+        // Pre-fill the region with small non-expiring entries, but leave 
enough room for the large TTL entries to be
+        // written without evicting them (so they are guaranteed to be present 
until they expire).
+        byte[] small = new byte[SMALL_RECORD_SIZE];
+
+        for (int i = 0; i < SMALL_ENTRIES; i++)
+            plainCache.put(i, small);
+
+        info("Pre-fill done [entries=" + SMALL_ENTRIES + ", plainSize=" + 
plainCache.size() +
+            ", loadedPages=" + 
ignite.dataRegionMetrics(DFLT_DATA_REG_DEFAULT_NAME).getTotalAllocatedPages() + 
']');
+
+        // Add large short-TTL entries that occupy significant space and will 
expire. They fit in the remaining free
+        // space, and being the freshest entries they are not evicted while 
they are being stored.
+        Object val = new byte[RECORD_SIZE];
+
+        for (int i = 0; i < 2; i++)
+            ttlCache.put(i, val);
+
+        // Verify the TTL entries are present before expiry.
+        assertNotNull("TTL entry must be present before expiry", 
ttlCache.get(0));
+
+        // Wait for the TTL worker to expire and free the short-TTL entries. 
Polled instead of a fixed sleep so that a
+        // slow CI machine does not proceed before the entries have actually 
expired.
+        long expiryDeadline = System.currentTimeMillis() + 30_000;
+
+        while (ttlCache.get(0) != null && System.currentTimeMillis() < 
expiryDeadline)
+            U.sleep(200);

Review Comment:
   We have waitForCondition, please use it. 8 seconds for til and 30 seconds 
for wait looks too time consuming



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** 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.
+     * <p>
+     * This scenario intentionally overlaps with
+     * {@link PageEvictionPutLargeObjectsAbstractTest#testPutLargeObjects}; 
the added value of this class is the
+     * additional coverage below (putAll, update-growth, read-back and the 
larger-than-region OOM case).
+     *
+     * @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.
+     * Uses a transactional cache so that the size-aware OOM propagates 
directly (in an atomic cache it is wrapped in

Review Comment:
   Use `X.hasCause(e, IgniteOutOfMemoryException.class)` and test will pass for 
atomic cache too.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** 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.
+     * <p>
+     * This scenario intentionally overlaps with
+     * {@link PageEvictionPutLargeObjectsAbstractTest#testPutLargeObjects}; 
the added value of this class is the
+     * additional coverage below (putAll, update-growth, read-back and the 
larger-than-region OOM case).
+     *
+     * @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.
+     * Uses a transactional cache so that the size-aware OOM propagates 
directly (in an atomic cache it is wrapped in
+     * a {@code CachePartialUpdateException} and would not be detectable as 
the specific OOM).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRecordLargerThanRegionOom() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = ignite.createCache(new 
CacheConfiguration<Integer, Object>(DEFAULT_CACHE_NAME)
+            .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS))
+            .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));
+
+        boolean rejected = false;
+
+        try {
+            cache.put(1, new byte[SIZE * 2]);
+        }
+        catch (Exception e) {
+            assertTrue("Expected IgniteOutOfMemoryException because the row 
cannot fit into the region, but got: " + e,
+                isOutOfMemory(e));
+
+            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 the region to near capacity with small evictable entries 
so that less than one large row remains
+        // available. This forces ensureFreeSpaceForInsert to evict prefilled 
entries rather than taking its fast 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. The region is pre-filled
+     * to near capacity so that the grown value cannot fit without evicting 
prefilled entries.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testUpdateRowGrows() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = createCache(ignite);
+
+        // Pre-fill the region to near capacity with small evictable entries 
so that the grown value below cannot
+        // fit without eviction.
+        byte[] small = new byte[SMALL_RECORD_SIZE];
+
+        for (int i = 0; i < SMALL_ENTRIES; i++)
+            cache.put(SMALL_ENTRIES + i, small);
+
+        // Insert key 1 with a small value, then update it to a large value 
that requires size-aware eviction.
+        cache.put(1, new byte[1024]);
+
+        byte[] big = new byte[RECORD_SIZE];
+
+        Arrays.fill(big, (byte)7);
+
+        cache.put(1, big);
+
+        byte[] read = (byte[])cache.get(1);
+
+        assertNotNull("Updated large value must be readable", read);
+
+        assertTrue("Updated value must equal the stored value", 
Arrays.equals(big, read));
+    }
+
+    /**
+     * @param t Throwable.
+     * @return {@code True} if {@code t} or any of its causes is an 
out-of-memory.
+     */
+    static boolean isOutOfMemory(Throwable t) {

Review Comment:
   X.hasCause()



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** 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.
+     * <p>
+     * This scenario intentionally overlaps with
+     * {@link PageEvictionPutLargeObjectsAbstractTest#testPutLargeObjects}; 
the added value of this class is the
+     * additional coverage below (putAll, update-growth, read-back and the 
larger-than-region OOM case).
+     *
+     * @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).

Review Comment:
   But there is no eviction at all in this test.



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** 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.
+     * <p>
+     * This scenario intentionally overlaps with
+     * {@link PageEvictionPutLargeObjectsAbstractTest#testPutLargeObjects}; 
the added value of this class is the
+     * additional coverage below (putAll, update-growth, read-back and the 
larger-than-region OOM case).
+     *
+     * @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.
+     * Uses a transactional cache so that the size-aware OOM propagates 
directly (in an atomic cache it is wrapped in
+     * a {@code CachePartialUpdateException} and would not be detectable as 
the specific OOM).
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testRecordLargerThanRegionOom() throws Exception {
+        IgniteEx ignite = startGrid(1);
+
+        IgniteCache<Integer, Object> cache = ignite.createCache(new 
CacheConfiguration<Integer, Object>(DEFAULT_CACHE_NAME)
+            .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS))
+            .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));
+
+        boolean rejected = false;
+
+        try {
+            cache.put(1, new byte[SIZE * 2]);

Review Comment:
   Lets also check putAll with 4 rows of RECORD_SIZE



##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -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());

Review Comment:
   Any test for this behavior (that we can grow beyond EvictionThreshold)?



##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,264 @@
+/*
+ * 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.CacheAtomicityMode;
+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.internal.mem.IgniteOutOfMemoryException;
+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.
+ * <p>
+ * 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.
+ * The batch path ({@code putAll} of large rows) and the update path (growing 
a row) are covered as well.
+ */
+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 so that a single row requires more pages than are 
left free when the region is kept at the
+     * eviction threshold ({@code (1 - threshold) * totalPages}). This 
guarantees a large put cannot take the fast
+     * path of {@code ensureFreeSpaceForInsert} and must actually run the 
size-aware eviction reserve
+     * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests 
are meant to cover. */
+    private static final int RECORD_SIZE = 32 * 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. Chosen to fill the 128 MiB region close 
to capacity so that less than one large
+     * record ({@link #RECORD_SIZE}) remains available, forcing {@code 
ensureFreeSpaceForInsert} to actually evict
+     * prefilled entries rather than taking its fast path.
+     */
+    private static final int SMALL_ENTRIES = 28_000;
+
+    /** Large rows written via putAll. */
+    private static final int PUT_ALL_LARGE_ROWS = 3;

Review Comment:
   Used only by 1 test



-- 
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