Copilot commented on code in PR #13554:
URL: https://github.com/apache/ignite/pull/13554#discussion_r3941217775
##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java:
##########
@@ -4182,6 +4205,23 @@ private int extrasSize() {
lock.lock();
}
+ /**
+ * Acquires the entry lock either blocking ({@code tryLock == false}) or
non-blockingly with the configured
+ * {@link #ENTRY_LOCK_TIMEOUT} ({@code tryLock == true}). Used by {@link
#evictInternal} to let size-aware
+ * eviction skip contended entries instead of blocking, avoiding a
lock-ordering deadlock.
+ *
+ * @param tryLock {@code true} to acquire the lock non-blockingly.
+ * @return {@code true} if the lock was acquired (always {@code true} when
{@code tryLock == false}).
+ */
+ private boolean lockEntry(boolean tryLock) {
+ if (tryLock)
+ return tryLockEntry(ENTRY_LOCK_TIMEOUT);
Review Comment:
This is neither truly non-blocking nor safe for the entry already owned by
the insertion thread: the default timeout waits up to one second per candidate,
and `ReentrantLock.tryLock` succeeds reentrantly for the current entry,
allowing an update's old row to be evicted while it is being replaced. Skip
entries locked by this thread and use an immediate try-lock; this also prevents
the 300-attempt guard from stalling for minutes.
##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -1216,24 +1241,128 @@ public void ensureFreeSpaceForInsert(DataRegion
region, int dataRowSize) throws
boolean oomThreshold = (memorySize / pageMem.systemPageSize()) <
((double)dataRowSize / pageMem.pageSize() + nonEmptyPages * (8.0 *
1.5 / pageMem.pageSize() + 1) + 256 /*one page per bucket*/);
- if (oomThreshold) {
- IgniteOutOfMemoryException oom = new
IgniteOutOfMemoryException("Out of memory in data region [" +
- "name=" + regCfg.getName() +
- ", initSize=" + U.readableSize(regCfg.getInitialSize(), false)
+
- ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) +
- ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "]
Try the following:" + U.nl() +
- " ^-- Increase maximum off-heap memory size
(DataRegionConfiguration.maxSize)" + U.nl() +
- " ^-- Enable Ignite persistence
(DataRegionConfiguration.persistenceEnabled)" + U.nl() +
- " ^-- Enable eviction or expiration policies"
- );
+ if (oomThreshold)
+ throw outOfMemory(regCfg);
+ }
+
+ /**
+ * Size-aware reserve for an eviction-enabled non-persistent region. Runs
eviction until the region has enough
+ * available pages to accommodate the row, or throws {@link
IgniteOutOfMemoryException} if the goal is
+ * unreachable / no progress can be made.
+ *
+ * @param region Data region.
+ * @param regCfg Data region configuration.
+ * @param dataRowSize Size of data row to be inserted.
+ * @throws IgniteOutOfMemoryException If the target cannot be reached (row
too large for the region or eviction
+ * makes no progress).
+ * @throws IgniteCheckedException If failed to evict data pages.
+ */
+ private void ensureFreeSpaceForEviction(DataRegion region,
DataRegionConfiguration regCfg, int dataRowSize)
+ throws IgniteOutOfMemoryException, IgniteCheckedException {
+ PageMemory pageMem = region.pageMemory();
+
+ CacheFreeList freeList = freeListMap.get(regCfg.getName());
- if (cctx.kernalContext() != null)
- cctx.kernalContext().failure().process(new
FailureContext(FailureType.CRITICAL_ERROR, oom));
+ if (freeList == null)
+ return;
+
+ long sysPageSize = pageMem.systemPageSize();
+ long pageSize = pageMem.pageSize();
+
+ long totalPages = regCfg.getMaxSize() / sysPageSize;
+
+ // Pages required to place the row (rounded up) plus a margin for the
page header and fragmentation.
+ long requiredPages = (dataRowSize + pageSize - 1) / pageSize + 1;
+
+ // If the row fits into the configured steady-state empty-pages pool,
normal threshold eviction is enough.
+ if (requiredPages <= regCfg.getEmptyPagesPoolSize())
+ return;
- throw oom;
+ // The row fundamentally cannot fit into the whole region.
+ if (requiredPages > totalPages)
+ throw outOfMemory(regCfg);
+
+ long availablePages = (totalPages - pageMem.loadedPages()) +
freeList.emptyDataPages();
+
+ // Fast path: enough pages are already available, no eviction is
needed.
+ if (availablePages >= requiredPages)
+ return;
+
+ PageEvictionTracker evictionTracker = region.evictionTracker();
+
+ // Evict data pages until enough free space is available. Progress is
measured against the overall available
+ // space, so pages freed concurrently (e.g. by TTL cleanup) also count
as progress. The loop is bounded to
+ // avoid an infinite busy-spin when there is nothing more to evict.
Eviction here runs while the current
+ // thread may already hold entry locks (single-row insertion), so
entries whose locks are contended are
+ // skipped (non-blocking) rather than blocked upon, to avoid a
lock-ordering deadlock.
+ final int maxAttemptsWithoutProgress = 300;
+
+ long bestAvailable = availablePages;
+ int attemptsWithoutProgress = 0;
+
+ while (bestAvailable < requiredPages) {
+ evictDataPageNonBlocking(evictionTracker);
+
+ long curAvailable = (totalPages - pageMem.loadedPages()) +
freeList.emptyDataPages();
+
+ // Progress is measured against the best available space observed
so far. Concurrent inserts may
+ // temporarily reduce available (loadedPages grows) even while
eviction is freeing pages, so a drop below
+ // the running best is not treated as "no progress". Only when
available fails to exceed the best value
+ // over many attempts we conclude that no more space can be freed
(e.g. all candidate entries are locked
+ // by other threads/transactions).
+ if (curAvailable > bestAvailable) {
+ bestAvailable = curAvailable;
+
+ attemptsWithoutProgress = 0;
+ }
+ else if (curAvailable < bestAvailable) {
+ // A transient drop caused by concurrent activity: keep the
best value, do not penalize.
+ }
+ else
+ attemptsWithoutProgress++;
Review Comment:
A drop below `bestAvailable` never increments the no-progress counter. If
another writer consumes pages after an initial improvement and eviction then
cannot free anything, `curAvailable` remains below the historical best forever
and this supposedly bounded loop busy-spins indefinitely. Count every iteration
that does not establish a new best toward the guard.
##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataPageEvictionMode;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
+import
org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Negative test for the size-aware eviction progress guard.
+ * <p>
+ * When every resident entry is locked by another thread/transaction, page
eviction cannot free any page: the guarded
+ * {@code tryLockEntry} in {@code evictInternal} fails for every candidate, so
{@link
+ * IgniteCacheDatabaseSharedManager#ensureFreeSpaceForEviction} makes no
progress and must fail with an
+ * {@code IgniteOutOfMemoryException} within bounded time instead of
busy-spinning forever (deadlock).
+ * <p>
+ * The lock timeout is reduced via {@code -DENTRY_LOCK_TIMEOUT=1} (applied
through {@code @WithSystemProperty} before
+ * the node starts) so that each non-blocking lock attempt fails quickly and
the whole guard run stays within a few
+ * seconds. The test is self-guarded by {@code @Test(timeout = ...)}: a
deadlock or unbounded busy-spin would fail the
+ * deadline.
+ */
+public class PageEvictionGuardOomTest extends GridCommonAbstractTest {
+ /** Off-heap region size. */
+ private static final int SIZE = 12 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust
the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Small record size chosen to occupy roughly one data page ({@link
DFLT_PAGE_SIZE}) each. */
+ private static final int FILL_VALUE_SIZE = 3_800;
+
+ /**
+ * Number of resident entries (each ~one page) filling the region to ~55%
of its capacity. This keeps the region
+ * comfortably below the eviction threshold (so the ordinary
threshold-based {@code ensureFreeSpace} path is a
+ * no-op) while leaving less free space than a single large record needs,
so the size-aware eviction guard is
+ * exercised.
+ */
+ private static final int FILL_ENTRIES = 1_600;
+
+ /** Large record size that does not fit into the remaining free space
(requires eviction to be stored). */
+ private static final int LARGE_RECORD_SIZE = 8 * 1024 * 1024;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName)
throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new
DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ .setPageEvictionMode(DataPageEvictionMode.RANDOM_LRU)
+ )
+ .setPageSize(DFLT_PAGE_SIZE)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page
overhead).
+ */
+ private IgniteCache<Integer, Object> createCache(IgniteEx ignite) {
+ // TRANSACTIONAL is required so that cache.lockAll(...) can hold entry
locks (the root cause of the
+ // "no evictable page" scenario this test exercises).
+ return ignite.createCache(new CacheConfiguration<Integer,
Object>(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS))
+ .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));
+ }
+
+ /**
+ * Filling the region with locked entries and then writing a row that
needs more free pages than remain must fail
+ * with OOM (bounded time), not hang: eviction cannot free any page
because every candidate entry is locked.
+ *
+ * @throws Exception If failed.
+ */
+ @Test(timeout = 180_000)
+ @WithSystemProperty(key = "ENTRY_LOCK_TIMEOUT", value = "1")
Review Comment:
This method-level property is applied after `GridCacheMapEntry` has already
been initialized by the earlier eviction tests in
`IgniteCacheEvictionSelfTestSuite`, while `ENTRY_LOCK_TIMEOUT` is a static
final value read at class initialization. In the normal suite this test
therefore uses the 1000 ms default, making the 300-attempt guard exceed the
180-second test timeout. Set the property before the class can be initialized
or remove the test's dependence on that cached static value.
##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -1216,24 +1241,128 @@ public void ensureFreeSpaceForInsert(DataRegion
region, int dataRowSize) throws
boolean oomThreshold = (memorySize / pageMem.systemPageSize()) <
((double)dataRowSize / pageMem.pageSize() + nonEmptyPages * (8.0 *
1.5 / pageMem.pageSize() + 1) + 256 /*one page per bucket*/);
- if (oomThreshold) {
- IgniteOutOfMemoryException oom = new
IgniteOutOfMemoryException("Out of memory in data region [" +
- "name=" + regCfg.getName() +
- ", initSize=" + U.readableSize(regCfg.getInitialSize(), false)
+
- ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) +
- ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "]
Try the following:" + U.nl() +
- " ^-- Increase maximum off-heap memory size
(DataRegionConfiguration.maxSize)" + U.nl() +
- " ^-- Enable Ignite persistence
(DataRegionConfiguration.persistenceEnabled)" + U.nl() +
- " ^-- Enable eviction or expiration policies"
- );
+ if (oomThreshold)
+ throw outOfMemory(regCfg);
+ }
+
+ /**
+ * Size-aware reserve for an eviction-enabled non-persistent region. Runs
eviction until the region has enough
+ * available pages to accommodate the row, or throws {@link
IgniteOutOfMemoryException} if the goal is
+ * unreachable / no progress can be made.
+ *
+ * @param region Data region.
+ * @param regCfg Data region configuration.
+ * @param dataRowSize Size of data row to be inserted.
+ * @throws IgniteOutOfMemoryException If the target cannot be reached (row
too large for the region or eviction
+ * makes no progress).
+ * @throws IgniteCheckedException If failed to evict data pages.
+ */
+ private void ensureFreeSpaceForEviction(DataRegion region,
DataRegionConfiguration regCfg, int dataRowSize)
+ throws IgniteOutOfMemoryException, IgniteCheckedException {
+ PageMemory pageMem = region.pageMemory();
+
+ CacheFreeList freeList = freeListMap.get(regCfg.getName());
- if (cctx.kernalContext() != null)
- cctx.kernalContext().failure().process(new
FailureContext(FailureType.CRITICAL_ERROR, oom));
+ if (freeList == null)
+ return;
+
+ long sysPageSize = pageMem.systemPageSize();
+ long pageSize = pageMem.pageSize();
+
+ long totalPages = regCfg.getMaxSize() / sysPageSize;
+
+ // Pages required to place the row (rounded up) plus a margin for the
page header and fragmentation.
+ long requiredPages = (dataRowSize + pageSize - 1) / pageSize + 1;
Review Comment:
The reserve divides by the raw page size and adds only one page, but every
fragment carries at most `pageSize - AbstractDataPageIO.MIN_DATA_PAGE_OVERHEAD`
bytes. With 4 KiB pages, for example, a 4 MiB row needs about 1041 data pages
while this computes 1025, so under pressure insertion can still run out of
pages after this method reports sufficient space. Compute the requirement from
the actual data-page payload capacity instead of using a fixed one-page margin.
##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java:
##########
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import javax.cache.expiry.CreatedExpiryPolicy;
+import javax.cache.expiry.Duration;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Tests the synergy between ExpiryPolicy (TTL cleanup) and size-aware page
eviction on an in-memory data region.
+ * Verifies that concurrent TTL cleanup and eviction do not deadlock, that a
large row larger than the
+ * empty-pages pool is still written when eviction is enabled, and that
TTL-freed space is accounted for by eviction
+ * (a row that only fits after expired entries are removed is still written
without OOM).
+ */
+public abstract class PageEvictionWithExpiryPolicyAbstractTest extends
GridCommonAbstractTest {
+ /** Off-heap region size. */
+ private static final int SIZE = 128 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust
the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Large record size (much larger than the empty-pages pool). */
+ private static final int RECORD_SIZE = 8 * 1024 * 1024;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Short TTL applied to some entries. */
+ private static final long TTL = 1500;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName)
throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new
DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ )
+ .setPageSize(DFLT_PAGE_SIZE)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @param ttl TTL in milliseconds ({@code 0} for no expiry).
+ * @return Cache with a small partition count and, if {@code ttl > 0},
eager TTL expiry.
+ */
+ private IgniteCache<Integer, Object> createCache(IgniteEx ignite, long
ttl) {
+ CacheConfiguration<Integer, Object> ccfg = new
CacheConfiguration<Integer, Object>(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS));
+
+ if (ttl > 0) {
+ ccfg.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new
Duration(MILLISECONDS, ttl)))
+ .setEagerTtl(true);
+ }
+
+ return ignite.createCache(ccfg);
+ }
+
+ /**
+ * Concurrent TTL cleanup and eviction must not deadlock, and a large
record (larger than the empty-pages pool)
+ * must still be stored on a region with enabled eviction even in the
presence of short-TTL entries.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testLargePutWithExpiryNoDeadlock() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ // Short-TTL entries keep the TTL worker actively freeing pages while
eviction runs.
+ IgniteCache<Integer, Object> cache = createCache(ignite, TTL);
+
+ Object val = new byte[RECORD_SIZE];
+
+ // Writing more data than the region can hold forces eviction;
concurrent expiry of short-TTL entries must not
+ // deadlock with it. The test itself is protected against a hang by
the framework test timeout.
+ for (int i = 0; i < 30; i++)
+ cache.put(i, val);
+
+ cache.get(0);
+ }
+
+ /**
+ * Space freed by TTL cleanup must be taken into account by size-aware
eviction: a large record written after some
+ * entries have expired must be accepted (no OOM) because their pages
become available.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testTtlFreedSpaceAccountedForByEviction() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite, TTL);
+
+ // Fill the region up to its capacity with short-TTL large records.
+ Object val = new byte[RECORD_SIZE];
+
+ for (int i = 0; i < 10; i++)
+ cache.put(i, val);
Review Comment:
This setup writes roughly 80 MiB into a 128 MiB region and then inserts only
one additional 8 MiB value, so the final put has ample space even if no TTL
entry was ever removed. The test therefore passes without exercising the
claimed TTL-freed-space accounting; fill the region until the final row cannot
fit, verify the expiring entries are gone, and only then perform the final put.
##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Tests size-aware page eviction on in-memory (non-persistent) data regions.
+ *
+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize}
(in pages) is still written successfully
+ * when page eviction is enabled, by evicting old entries to free enough
space. Also verifies that a row
+ * which fundamentally cannot fit into the region fails with OOM instead of
hanging in an infinite eviction loop.
+ *
+ * Note: the atomic DHT batch path (putAll of many large rows overflowing a
small region) is out of scope here — it is
+ * handled by a separate size-aware reserve in the batch store path and
already fails on the original code.
+ */
+public abstract class PageEvictionSizeAwareAbstractTest extends
GridCommonAbstractTest {
+ /** Off-heap region size (large enough to hold cache structural pages with
the configured partition count). */
+ private static final int SIZE = 128 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust
the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Record size: chosen to be much larger than {@code emptyPagesPoolSize}
pages. */
+ private static final int RECORD_SIZE = 4 * 1024 * 1024;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Entry count to accumulate beyond the region capacity. */
+ private static final int ENTRIES = 40;
+
+ /** Small record size used to pre-fill the region with evictable data (for
putAll tests). */
+ private static final int SMALL_RECORD_SIZE = 4096;
+
+ /** Small pre-fill entries count. */
+ private static final int SMALL_ENTRIES = 8000;
+
+ /** Large rows written via putAll. */
+ private static final int PUT_ALL_LARGE_ROWS = 3;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName)
throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new
DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ )
+ .setPageSize(DFLT_PAGE_SIZE)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page
overhead).
+ */
+ private IgniteCache<Integer, Object> createCache(IgniteEx ignite) {
+ return ignite.createCache(new CacheConfiguration<Integer,
Object>(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+ }
+
+ /**
+ * A large record (larger than the empty-pages pool) must be stored
without OOM when there is evictable data,
+ * by evicting previously stored records to free enough space.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testPutLargeObjectsDoesNotOom() throws Exception {
+ IgniteEx ignite = startGrids(2);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ Object val = new byte[RECORD_SIZE];
+
+ // Total data (ENTRIES * RECORD_SIZE) exceeds the region size, so at
least some records must be evicted.
+ for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME),
ENTRIES))
+ cache.put(key, val);
+
+ // Eviction must have bounded the number of resident entries.
+ assertTrue("Expected some entries to be evicted, but cache.size()=" +
cache.size(),
+ cache.size() > 0 && cache.size() < ENTRIES);
+ }
+
+ /**
+ * A large record written must be readable right away (the just-written
entry is the most recently used and is not
+ * a candidate for eviction before the write completes).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testLargeObjectReadBack() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ byte[] val = new byte[RECORD_SIZE];
+
+ Arrays.fill(val, (byte)42);
+
+ cache.put(1, val);
+
+ byte[] read = (byte[])cache.get(1);
+
+ assertNotNull("Large value must be readable after put", read);
+
+ assertTrue("Value read back must equal the stored value",
Arrays.equals(val, read));
+ }
+
+ /**
+ * A record larger than the whole region must fail (not hang) even when
size-aware eviction is enabled.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testRecordLargerThanRegionOom() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ boolean rejected = false;
+
+ try {
+ cache.put(1, new byte[SIZE * 2]);
+ }
+ catch (Exception e) {
+ // OOM (possibly wrapped) because the row cannot fit into the
region.
+ rejected = true;
+ }
+
+ assertTrue("Record larger than the region must be rejected (no hang),
but put succeeded", rejected);
+ }
+
+ /**
+ * A batch putAll of several large records (each larger than the
empty-pages pool) must be stored successfully when
+ * page eviction is enabled. Exercises the size-aware reserve in the batch
store path ({@code RowStore.addRows}).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testPutAllLargeRows() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ // Pre-fill with small evictable entries so large rows below region
capacity fit via the reserve path.
+ byte[] small = new byte[SMALL_RECORD_SIZE];
+
+ for (int i = 0; i < SMALL_ENTRIES; i++)
+ cache.put(SMALL_ENTRIES + i, small);
+
+ Map<Integer, Object> large = new HashMap<>();
+
+ Object val = new byte[RECORD_SIZE];
+
+ for (int i = 0; i < PUT_ALL_LARGE_ROWS; i++)
+ large.put(i, val);
+
+ cache.putAll(large);
+
+ for (int i = 0; i < PUT_ALL_LARGE_ROWS; i++)
+ assertNotNull("Large row " + i + " must be readable after putAll",
cache.get(i));
+ }
+
+ /**
+ * Updating a record from a small to a large value (larger than the
empty-pages pool) must succeed with page
+ * eviction enabled: the update goes through the same size-aware reserve
as an insert.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testUpdateRowGrows() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ cache.put(1, new byte[1024]);
+
+ byte[] big = new byte[RECORD_SIZE];
+
+ Arrays.fill(big, (byte)7);
+
+ cache.put(1, big);
Review Comment:
This update runs in an otherwise empty 128 MiB region, so growing from 1 KiB
to 4 MiB never needs size-aware eviction. It would pass through the old path as
well and does not cover the lock-sensitive update scenario described by the
test. Prefill with evictable entries until the grown value cannot fit without
eviction before updating key 1.
##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java:
##########
@@ -1216,24 +1241,128 @@ public void ensureFreeSpaceForInsert(DataRegion
region, int dataRowSize) throws
boolean oomThreshold = (memorySize / pageMem.systemPageSize()) <
((double)dataRowSize / pageMem.pageSize() + nonEmptyPages * (8.0 *
1.5 / pageMem.pageSize() + 1) + 256 /*one page per bucket*/);
- if (oomThreshold) {
- IgniteOutOfMemoryException oom = new
IgniteOutOfMemoryException("Out of memory in data region [" +
- "name=" + regCfg.getName() +
- ", initSize=" + U.readableSize(regCfg.getInitialSize(), false)
+
- ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) +
- ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "]
Try the following:" + U.nl() +
- " ^-- Increase maximum off-heap memory size
(DataRegionConfiguration.maxSize)" + U.nl() +
- " ^-- Enable Ignite persistence
(DataRegionConfiguration.persistenceEnabled)" + U.nl() +
- " ^-- Enable eviction or expiration policies"
- );
+ if (oomThreshold)
+ throw outOfMemory(regCfg);
+ }
+
+ /**
+ * Size-aware reserve for an eviction-enabled non-persistent region. Runs
eviction until the region has enough
+ * available pages to accommodate the row, or throws {@link
IgniteOutOfMemoryException} if the goal is
+ * unreachable / no progress can be made.
+ *
+ * @param region Data region.
+ * @param regCfg Data region configuration.
+ * @param dataRowSize Size of data row to be inserted.
+ * @throws IgniteOutOfMemoryException If the target cannot be reached (row
too large for the region or eviction
+ * makes no progress).
+ * @throws IgniteCheckedException If failed to evict data pages.
+ */
+ private void ensureFreeSpaceForEviction(DataRegion region,
DataRegionConfiguration regCfg, int dataRowSize)
+ throws IgniteOutOfMemoryException, IgniteCheckedException {
+ PageMemory pageMem = region.pageMemory();
+
+ CacheFreeList freeList = freeListMap.get(regCfg.getName());
- if (cctx.kernalContext() != null)
- cctx.kernalContext().failure().process(new
FailureContext(FailureType.CRITICAL_ERROR, oom));
+ if (freeList == null)
+ return;
+
+ long sysPageSize = pageMem.systemPageSize();
+ long pageSize = pageMem.pageSize();
+
+ long totalPages = regCfg.getMaxSize() / sysPageSize;
+
+ // Pages required to place the row (rounded up) plus a margin for the
page header and fragmentation.
+ long requiredPages = (dataRowSize + pageSize - 1) / pageSize + 1;
+
+ // If the row fits into the configured steady-state empty-pages pool,
normal threshold eviction is enough.
+ if (requiredPages <= regCfg.getEmptyPagesPoolSize())
+ return;
- throw oom;
+ // The row fundamentally cannot fit into the whole region.
+ if (requiredPages > totalPages)
+ throw outOfMemory(regCfg);
+
+ long availablePages = (totalPages - pageMem.loadedPages()) +
freeList.emptyDataPages();
+
+ // Fast path: enough pages are already available, no eviction is
needed.
+ if (availablePages >= requiredPages)
+ return;
+
+ PageEvictionTracker evictionTracker = region.evictionTracker();
+
+ // Evict data pages until enough free space is available. Progress is
measured against the overall available
+ // space, so pages freed concurrently (e.g. by TTL cleanup) also count
as progress. The loop is bounded to
+ // avoid an infinite busy-spin when there is nothing more to evict.
Eviction here runs while the current
+ // thread may already hold entry locks (single-row insertion), so
entries whose locks are contended are
+ // skipped (non-blocking) rather than blocked upon, to avoid a
lock-ordering deadlock.
+ final int maxAttemptsWithoutProgress = 300;
+
+ long bestAvailable = availablePages;
+ int attemptsWithoutProgress = 0;
+
+ while (bestAvailable < requiredPages) {
+ evictDataPageNonBlocking(evictionTracker);
+
+ long curAvailable = (totalPages - pageMem.loadedPages()) +
freeList.emptyDataPages();
Review Comment:
This new eviction path bypasses the existing metric bookkeeping in
`ensureFreeSpace`: actual size-aware evictions neither set
`isEvictionsStarted()` nor increment the eviction rate. Record the start and
rate here as well so operational metrics reflect all page eviction.
##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Tests size-aware page eviction on in-memory (non-persistent) data regions.
+ *
+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize}
(in pages) is still written successfully
+ * when page eviction is enabled, by evicting old entries to free enough
space. Also verifies that a row
+ * which fundamentally cannot fit into the region fails with OOM instead of
hanging in an infinite eviction loop.
+ *
+ * Note: the atomic DHT batch path (putAll of many large rows overflowing a
small region) is out of scope here — it is
+ * handled by a separate size-aware reserve in the batch store path and
already fails on the original code.
+ */
+public abstract class PageEvictionSizeAwareAbstractTest extends
GridCommonAbstractTest {
+ /** Off-heap region size (large enough to hold cache structural pages with
the configured partition count). */
+ private static final int SIZE = 128 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust
the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Record size: chosen to be much larger than {@code emptyPagesPoolSize}
pages. */
+ private static final int RECORD_SIZE = 4 * 1024 * 1024;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Entry count to accumulate beyond the region capacity. */
+ private static final int ENTRIES = 40;
+
+ /** Small record size used to pre-fill the region with evictable data (for
putAll tests). */
+ private static final int SMALL_RECORD_SIZE = 4096;
+
+ /** Small pre-fill entries count. */
+ private static final int SMALL_ENTRIES = 8000;
+
+ /** Large rows written via putAll. */
+ private static final int PUT_ALL_LARGE_ROWS = 3;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName)
throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new
DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ )
+ .setPageSize(DFLT_PAGE_SIZE)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page
overhead).
+ */
+ private IgniteCache<Integer, Object> createCache(IgniteEx ignite) {
+ return ignite.createCache(new CacheConfiguration<Integer,
Object>(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+ }
+
+ /**
+ * A large record (larger than the empty-pages pool) must be stored
without OOM when there is evictable data,
+ * by evicting previously stored records to free enough space.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testPutLargeObjectsDoesNotOom() throws Exception {
+ IgniteEx ignite = startGrids(2);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ Object val = new byte[RECORD_SIZE];
+
+ // Total data (ENTRIES * RECORD_SIZE) exceeds the region size, so at
least some records must be evicted.
+ for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME),
ENTRIES))
+ cache.put(key, val);
+
+ // Eviction must have bounded the number of resident entries.
+ assertTrue("Expected some entries to be evicted, but cache.size()=" +
cache.size(),
+ cache.size() > 0 && cache.size() < ENTRIES);
+ }
+
+ /**
+ * A large record written must be readable right away (the just-written
entry is the most recently used and is not
+ * a candidate for eviction before the write completes).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testLargeObjectReadBack() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ byte[] val = new byte[RECORD_SIZE];
+
+ Arrays.fill(val, (byte)42);
+
+ cache.put(1, val);
+
+ byte[] read = (byte[])cache.get(1);
+
+ assertNotNull("Large value must be readable after put", read);
+
+ assertTrue("Value read back must equal the stored value",
Arrays.equals(val, read));
+ }
+
+ /**
+ * A record larger than the whole region must fail (not hang) even when
size-aware eviction is enabled.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testRecordLargerThanRegionOom() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ boolean rejected = false;
+
+ try {
+ cache.put(1, new byte[SIZE * 2]);
+ }
+ catch (Exception e) {
+ // OOM (possibly wrapped) because the row cannot fit into the
region.
+ rejected = true;
+ }
+
+ assertTrue("Record larger than the region must be rejected (no hang),
but put succeeded", rejected);
+ }
+
+ /**
+ * A batch putAll of several large records (each larger than the
empty-pages pool) must be stored successfully when
+ * page eviction is enabled. Exercises the size-aware reserve in the batch
store path ({@code RowStore.addRows}).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testPutAllLargeRows() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ // Pre-fill with small evictable entries so large rows below region
capacity fit via the reserve path.
+ byte[] small = new byte[SMALL_RECORD_SIZE];
+
+ for (int i = 0; i < SMALL_ENTRIES; i++)
+ cache.put(SMALL_ENTRIES + i, small);
Review Comment:
The prefill is only about 32 MiB and the three large rows add about 12 MiB
in a 128 MiB region. Consequently `ensureFreeSpaceForInsert` takes its fast
path and this test succeeds without exercising the new batch reserve. Prefill
until less than one large row remains available, then verify `putAll` succeeds
by evicting the prefilled entries.
##########
modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java:
##########
@@ -134,6 +135,20 @@ public void addRow(CacheDataRow row, IoStatisticsHolder
statHolder) throws Ignit
*/
public void addRows(Collection<? extends CacheDataRow> rows,
IoStatisticsHolder statHolder) throws IgniteCheckedException {
+ if (!persistenceEnabled &&
grp.dataRegion().config().getPageEvictionMode() !=
DataPageEvictionMode.DISABLED) {
+ // Size-aware reserve for the largest row in the batch. Eviction
performed here runs without entry locks
+ // (see AbstractFreeList#insertDataRows), so reserving space for
any single large row is safe and keeps the
+ // batch path consistent with the single-row path. Smaller rows
are covered by the regular
+ // threshold eviction loop inside insertDataRows.
+ int maxRowSize = 0;
+
+ for (CacheDataRow row : rows)
+ maxRowSize = Math.max(maxRowSize, row.size());
+
+ if (maxRowSize > 0)
+ ctx.database().ensureFreeSpaceForInsert(grp.dataRegion(),
maxRowSize);
Review Comment:
Reserving only the largest row does not make a batch safe: `insertDataRows`
consumes that reserve while writing the first large row, and its per-row
threshold loop only restores `emptyPagesPoolSize`, which is explicitly smaller
than these rows. A later large row can therefore still exhaust page memory.
Perform the size-aware check before each large row is written (or reserve a
safely calculated aggregate for the batch).
##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CacheAtomicityMode;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataPageEvictionMode;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
+import
org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
+import org.apache.ignite.testframework.junits.WithSystemProperty;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Negative test for the size-aware eviction progress guard.
+ * <p>
+ * When every resident entry is locked by another thread/transaction, page
eviction cannot free any page: the guarded
+ * {@code tryLockEntry} in {@code evictInternal} fails for every candidate, so
{@link
+ * IgniteCacheDatabaseSharedManager#ensureFreeSpaceForEviction} makes no
progress and must fail with an
+ * {@code IgniteOutOfMemoryException} within bounded time instead of
busy-spinning forever (deadlock).
+ * <p>
+ * The lock timeout is reduced via {@code -DENTRY_LOCK_TIMEOUT=1} (applied
through {@code @WithSystemProperty} before
+ * the node starts) so that each non-blocking lock attempt fails quickly and
the whole guard run stays within a few
+ * seconds. The test is self-guarded by {@code @Test(timeout = ...)}: a
deadlock or unbounded busy-spin would fail the
+ * deadline.
+ */
+public class PageEvictionGuardOomTest extends GridCommonAbstractTest {
+ /** Off-heap region size. */
+ private static final int SIZE = 12 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust
the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Small record size chosen to occupy roughly one data page ({@link
DFLT_PAGE_SIZE}) each. */
+ private static final int FILL_VALUE_SIZE = 3_800;
+
+ /**
+ * Number of resident entries (each ~one page) filling the region to ~55%
of its capacity. This keeps the region
+ * comfortably below the eviction threshold (so the ordinary
threshold-based {@code ensureFreeSpace} path is a
+ * no-op) while leaving less free space than a single large record needs,
so the size-aware eviction guard is
+ * exercised.
+ */
+ private static final int FILL_ENTRIES = 1_600;
+
+ /** Large record size that does not fit into the remaining free space
(requires eviction to be stored). */
+ private static final int LARGE_RECORD_SIZE = 8 * 1024 * 1024;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName)
throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new
DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ .setPageEvictionMode(DataPageEvictionMode.RANDOM_LRU)
+ )
+ .setPageSize(DFLT_PAGE_SIZE)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page
overhead).
+ */
+ private IgniteCache<Integer, Object> createCache(IgniteEx ignite) {
+ // TRANSACTIONAL is required so that cache.lockAll(...) can hold entry
locks (the root cause of the
+ // "no evictable page" scenario this test exercises).
+ return ignite.createCache(new CacheConfiguration<Integer,
Object>(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS))
+ .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));
+ }
+
+ /**
+ * Filling the region with locked entries and then writing a row that
needs more free pages than remain must fail
+ * with OOM (bounded time), not hang: eviction cannot free any page
because every candidate entry is locked.
+ *
+ * @throws Exception If failed.
+ */
+ @Test(timeout = 180_000)
+ @WithSystemProperty(key = "ENTRY_LOCK_TIMEOUT", value = "1")
+ public void testGuardOomWhenAllEntriesLocked() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ // Pre-fill the region so that less than one large record of free
space remains, without overflowing it.
+ byte[] fillVal = new byte[FILL_VALUE_SIZE];
+
+ for (int i = 1; i <= FILL_ENTRIES; i++)
+ cache.put(i, fillVal);
+
+ Collection<Integer> keys = new ArrayList<>(FILL_ENTRIES);
+
+ for (int i = 1; i <= FILL_ENTRIES; i++)
+ keys.add(i);
+
+ CountDownLatch ready = new CountDownLatch(1);
+
+ CountDownLatch release = new CountDownLatch(1);
+
+ AtomicReference<Throwable> lockerErr = new AtomicReference<>();
+
+ // Hold entry locks on every resident key from a background thread so
that eviction has no evictable page.
+ Thread locker = new Thread(() -> {
+ try {
+ Lock lock = cache.lockAll(keys);
+
+ lock.lock();
+
+ ready.countDown();
+
+ release.await();
+
+ lock.unlock();
+ }
+ catch (Throwable e) {
+ lockerErr.set(e);
+
+ ready.countDown();
+ }
+ }, "size-aware-guard-locker");
+
+ locker.start();
+
+ assertTrue("Timed out waiting for entries to be locked",
ready.await(60, TimeUnit.SECONDS));
+
+ assertNull("Unexpected error while locking entries: " +
lockerErr.get(), lockerErr.get());
Review Comment:
The setup assertions execute before the `finally` that releases the locker.
If lock acquisition times out or the locker reports an error, the test exits
while a non-daemon thread remains blocked on `release.await()`, which can hang
the suite. Enclose both assertions and the put in the release/join
`try/finally`.
##########
modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java:
##########
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.internal.processors.cache.eviction.paged;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.DataRegionConfiguration;
+import org.apache.ignite.configuration.DataStorageConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+
+import static
org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE;
+
+/**
+ * Tests size-aware page eviction on in-memory (non-persistent) data regions.
+ *
+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize}
(in pages) is still written successfully
+ * when page eviction is enabled, by evicting old entries to free enough
space. Also verifies that a row
+ * which fundamentally cannot fit into the region fails with OOM instead of
hanging in an infinite eviction loop.
+ *
+ * Note: the atomic DHT batch path (putAll of many large rows overflowing a
small region) is out of scope here — it is
+ * handled by a separate size-aware reserve in the batch store path and
already fails on the original code.
+ */
+public abstract class PageEvictionSizeAwareAbstractTest extends
GridCommonAbstractTest {
+ /** Off-heap region size (large enough to hold cache structural pages with
the configured partition count). */
+ private static final int SIZE = 128 * 1024 * 1024;
+
+ /** Partition count (kept low so that index-tree structures do not exhaust
the region). */
+ private static final int PARTITIONS = 32;
+
+ /** Record size: chosen to be much larger than {@code emptyPagesPoolSize}
pages. */
+ private static final int RECORD_SIZE = 4 * 1024 * 1024;
+
+ /** Empty pages pool size. */
+ private static final int POOL_SIZE = 100;
+
+ /** Entry count to accumulate beyond the region capacity. */
+ private static final int ENTRIES = 40;
+
+ /** Small record size used to pre-fill the region with evictable data (for
putAll tests). */
+ private static final int SMALL_RECORD_SIZE = 4096;
+
+ /** Small pre-fill entries count. */
+ private static final int SMALL_ENTRIES = 8000;
+
+ /** Large rows written via putAll. */
+ private static final int PUT_ALL_LARGE_ROWS = 3;
+
+ /** {@inheritDoc} */
+ @Override protected IgniteConfiguration getConfiguration(String gridName)
throws Exception {
+ return super.getConfiguration(gridName)
+ .setDataStorageConfiguration(new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new
DataRegionConfiguration()
+ .setInitialSize(SIZE)
+ .setMaxSize(SIZE)
+ .setEmptyPagesPoolSize(POOL_SIZE)
+ )
+ .setPageSize(DFLT_PAGE_SIZE)
+ );
+ }
+
+ /** {@inheritDoc} */
+ @Override protected void afterTest() throws Exception {
+ stopAllGrids();
+ }
+
+ /**
+ * @param ignite Ignite node.
+ * @return Cache with a small partition count (reduces structural page
overhead).
+ */
+ private IgniteCache<Integer, Object> createCache(IgniteEx ignite) {
+ return ignite.createCache(new CacheConfiguration<Integer,
Object>(DEFAULT_CACHE_NAME)
+ .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)));
+ }
+
+ /**
+ * A large record (larger than the empty-pages pool) must be stored
without OOM when there is evictable data,
+ * by evicting previously stored records to free enough space.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testPutLargeObjectsDoesNotOom() throws Exception {
+ IgniteEx ignite = startGrids(2);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ Object val = new byte[RECORD_SIZE];
+
+ // Total data (ENTRIES * RECORD_SIZE) exceeds the region size, so at
least some records must be evicted.
+ for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME),
ENTRIES))
+ cache.put(key, val);
+
+ // Eviction must have bounded the number of resident entries.
+ assertTrue("Expected some entries to be evicted, but cache.size()=" +
cache.size(),
+ cache.size() > 0 && cache.size() < ENTRIES);
+ }
+
+ /**
+ * A large record written must be readable right away (the just-written
entry is the most recently used and is not
+ * a candidate for eviction before the write completes).
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testLargeObjectReadBack() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ byte[] val = new byte[RECORD_SIZE];
+
+ Arrays.fill(val, (byte)42);
+
+ cache.put(1, val);
+
+ byte[] read = (byte[])cache.get(1);
+
+ assertNotNull("Large value must be readable after put", read);
+
+ assertTrue("Value read back must equal the stored value",
Arrays.equals(val, read));
+ }
+
+ /**
+ * A record larger than the whole region must fail (not hang) even when
size-aware eviction is enabled.
+ *
+ * @throws Exception If failed.
+ */
+ @Test
+ public void testRecordLargerThanRegionOom() throws Exception {
+ IgniteEx ignite = startGrid(1);
+
+ IgniteCache<Integer, Object> cache = createCache(ignite);
+
+ boolean rejected = false;
+
+ try {
+ cache.put(1, new byte[SIZE * 2]);
+ }
+ catch (Exception e) {
+ // OOM (possibly wrapped) because the row cannot fit into the
region.
+ rejected = true;
+ }
Review Comment:
Any exception is treated as the expected OOM, so cache, topology,
serialization, or assertion failures make this regression test pass
incorrectly. Assert that the exception or one of its causes is specifically
`IgniteOutOfMemoryException`, as the guard test in this PR already does.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]