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]
