diff --git a/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java b/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java index 1293ed7989486..9666295a5203b 100644 --- a/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java +++ b/modules/core/src/main/java/org/apache/ignite/configuration/DataRegionConfiguration.java @@ -18,7 +18,6 @@ import java.io.Serializable; import org.apache.ignite.DataRegionMetrics; -import org.apache.ignite.internal.mem.IgniteOutOfMemoryException; import org.apache.ignite.internal.util.typedef.internal.S; import org.apache.ignite.mem.MemoryAllocator; import org.apache.ignite.mxbean.MetricsMxBean; @@ -346,9 +345,9 @@ public DataRegionConfiguration setEvictionThreshold(double evictionThreshold) { * Specifies the minimal number of empty pages to be present in reuse lists for this data region. * This parameter ensures that Ignite will be able to successfully evict old data entries when the size of * (key, value) pair is slightly larger than page size / 2. - * Increase this parameter if cache can contain very big entries (total size of pages in this pool should be enough - * to contain largest cache entry). - * Increase this parameter if {@link IgniteOutOfMemoryException} occurred with enabled page eviction. + * Since size-aware eviction automatically frees additional pages when the inserted row is larger than this pool, + * it is no longer required to increase this parameter up to the size of the largest cache entry; + * it may be kept at its default as the steady-state reserve of empty pages. * * @return Minimum number of empty pages in reuse list. */ @@ -360,9 +359,9 @@ public int getEmptyPagesPoolSize() { * Specifies the minimal number of empty pages to be present in reuse lists for this data region. * This parameter ensures that Ignite will be able to successfully evict old data entries when the size of * (key, value) pair is slightly larger than page size / 2. - * Increase this parameter if cache can contain very big entries (total size of pages in this pool should be enough - * to contain largest cache entry). - * Increase this parameter if {@link IgniteOutOfMemoryException} occurred with enabled page eviction. + * Since size-aware eviction automatically frees additional pages when the inserted row is larger than this pool, + * it is no longer required to increase this parameter up to the size of the largest cache entry; + * it may be kept at its default as the steady-state reserve of empty pages. * * @param emptyPagesPoolSize Empty pages pool size. * @return {@code this} for chaining. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java index bda9d3fbdc198..0d07d70602c11 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheEntryEx.java @@ -201,6 +201,24 @@ public interface GridCacheEntryEx { public boolean evictInternal(GridCacheVersion obsoleteVer, @Nullable CacheEntryPredicate[] filter, boolean evictOffheap) throws IgniteCheckedException; + /** + * Same as {@link #evictInternal(GridCacheVersion, CacheEntryPredicate[], boolean)}, but acquires the entry lock + * non-blockingly when {@code tryLock} is {@code true}, returning {@code false} (instead of blocking) if the entry + * lock is contended. Used by size-aware page eviction which may run while the current thread already holds other + * entry locks, to avoid a lock-ordering deadlock. The default implementation uses the blocking variant. + * + * @param obsoleteVer Version for eviction. + * @param filter Optional filter. + * @param evictOffheap Evict offheap value flag. + * @param tryLock {@code true} to acquire the entry lock non-blockingly (skip contended entries). + * @return {@code True} if entry could be evicted. + * @throws IgniteCheckedException In case of error. + */ + public default boolean evictInternal(GridCacheVersion obsoleteVer, @Nullable CacheEntryPredicate[] filter, + boolean evictOffheap, boolean tryLock) throws IgniteCheckedException { + return evictInternal(obsoleteVer, filter, evictOffheap); + } + /** * This method should be called each time entry is marked obsolete * other than by calling {@link #markObsolete(GridCacheVersion)}. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java index 021fdd0408d48..4e5eec7bd999c 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMapEntry.java @@ -3659,6 +3659,10 @@ protected void removeValue() throws IgniteCheckedException { */ private void ensureFreeSpace() throws IgniteCheckedException { // Deadlock alert: evicting data page causes removing (and locking) all entries on the page one by one. + // This entry-level eviction is invoked only while NOT holding this entry's lock (all call sites run before + // lockEntry()). The separately invoked size-aware path (see RowStore.addRow -> + // IgniteCacheDatabaseSharedManager#ensureFreeSpaceForInsert) runs while the lock IS held, so it relies on + // the non-blocking tryLockEntry(ENTRY_LOCK_TIMEOUT) inside evictInternal to avoid a lock-ordering deadlock. assert !lock.isHeldByCurrentThread(); cctx.shared().database().ensureFreeSpace(cctx.dataRegion()); @@ -3687,11 +3691,28 @@ private CacheEntryImplEx wrapVersionedWithValue() { boolean evictOffheap) throws IgniteCheckedException { + return evictInternal(obsoleteVer, filter, evictOffheap, false); + } + + /** {@inheritDoc} */ + @Override public boolean evictInternal( + GridCacheVersion obsoleteVer, + @Nullable CacheEntryPredicate[] filter, + boolean evictOffheap, + boolean tryLock) + throws IgniteCheckedException { + boolean marked = false; try { if (F.isEmptyOrNulls(filter)) { - lockEntry(); + // With tryLock=true (size-aware eviction running while the current thread already holds entry locks) + // the lock is acquired non-blockingly: a contended entry is skipped (returning false) rather than + // blocking, which prevents a lock-ordering deadlock between concurrent evictions. The eviction tracker + // will then pick another page. For all other paths (tryLock=false) the original + // blocking lockEntry() is preserved. + if (!lockEntry(tryLock)) + return false; try { if (evictionDisabled()) { @@ -3728,7 +3749,8 @@ private CacheEntryImplEx wrapVersionedWithValue() { while (true) { GridCacheVersion v; - lockEntry(); + if (!lockEntry(tryLock)) + return false; try { v = ver; @@ -3740,7 +3762,8 @@ private CacheEntryImplEx wrapVersionedWithValue() { if (!cctx.isAll(/*version needed for sync evicts*/this, filter)) return false; - lockEntry(); + if (!lockEntry(tryLock)) + return false; try { if (evictionDisabled()) { @@ -4182,6 +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); + + lockEntry(); + + return true; + } + /** {@inheritDoc} */ @Override public boolean tryLockEntry(long timeout) { try { diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java index b22a6682957c4..4e6bc990281b2 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/IgniteCacheDatabaseSharedManager.java @@ -63,6 +63,7 @@ import org.apache.ignite.internal.processors.cache.persistence.checkpoint.CheckpointProgress; import org.apache.ignite.internal.processors.cache.persistence.evict.FairFifoPageEvictionTracker; import org.apache.ignite.internal.processors.cache.persistence.evict.NoOpPageEvictionTracker; +import org.apache.ignite.internal.processors.cache.persistence.evict.PageAbstractEvictionTracker; import org.apache.ignite.internal.processors.cache.persistence.evict.PageEvictionTracker; import org.apache.ignite.internal.processors.cache.persistence.evict.Random2LruPageEvictionTracker; import org.apache.ignite.internal.processors.cache.persistence.evict.RandomLruPageEvictionTracker; @@ -1172,32 +1173,56 @@ public WALPointer latestWalPointerReservedForPreloading() { } /** - * Checks that the given {@code region} has enough space for putting a new entry. + * Checks that the given {@code region} has enough space for putting a new entry of {@code dataRowSize} bytes. * - * This method makes sense then and only then - * the data region is not persisted {@link DataRegionConfiguration#isPersistenceEnabled()} - * and page eviction is disabled {@link DataPageEvictionMode#DISABLED}. + * For a non-persistent region with page eviction disabled, verifies that the region reserves enough pages to + * support a free list {@link AbstractFreeList}. For example, removing a row from underlying store may require + * allocating a new data page in order to move a tracked page from one bucket to another one which does not have + * a free space for a new stripe. See {@link AbstractFreeList#removeDataRowByLink}. Therefore, inserting a new + * entry should be prevented in case of some threshold is exceeded. * - * The non-persistent region should reserve a number of pages to support a free list {@link AbstractFreeList}. - * For example, removing a row from underlying store may require allocating a new data page - * in order to move a tracked page from one bucket to another one which does not have a free space for a new stripe. - * See {@link AbstractFreeList#removeDataRowByLink}. - * Therefore, inserting a new entry should be prevented in case of some threshold is exceeded. + * For a non-persistent region with page eviction enabled, additionally performs size-aware eviction: when the + * row does not fit into the currently available page space, data pages are evicted until either enough space is + * freed or it becomes clear that the goal is unreachable (in which case an + * {@link IgniteOutOfMemoryException} is thrown). + * + * The size-aware reserve is required because page eviction by itself only keeps a steady-state pool of empty pages + * ({@link DataRegionConfiguration#getEmptyPagesPoolSize()}) and does not guarantee enough space for a single row + * larger than this pool. * * @param region Data region to be checked. * @param dataRowSize Size of data row to be inserted. - * @throws IgniteOutOfMemoryException In case of the given data region does not have enough free space - * for putting a new entry. + * @throws IgniteOutOfMemoryException In case the given data region does not have enough free space + * for putting a new entry, even after eviction. + * @throws IgniteCheckedException If failed to evict data pages. */ - public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) throws IgniteOutOfMemoryException { + public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) + throws IgniteOutOfMemoryException, IgniteCheckedException { if (region == null) return; DataRegionConfiguration regCfg = region.config(); - if (regCfg.getPageEvictionMode() != DataPageEvictionMode.DISABLED || regCfg.isPersistenceEnabled()) + if (regCfg.isPersistenceEnabled()) return; + if (regCfg.getPageEvictionMode() == DataPageEvictionMode.DISABLED) + checkOomThreshold(region, regCfg, dataRowSize); + else + ensureFreeSpaceForEviction(region, regCfg, dataRowSize); + } + + /** + * Checks that a non-persistent region with disabled page eviction has enough pages for a new row, taking into + * account the pages required to support the free list. + * + * @param region Data region. + * @param regCfg Data region configuration. + * @param dataRowSize Size of data row to be inserted. + * @throws IgniteOutOfMemoryException If the region does not have enough free space for the new entry. + */ + private void checkOomThreshold(DataRegion region, DataRegionConfiguration regCfg, int dataRowSize) + throws IgniteOutOfMemoryException { long memorySize = regCfg.getMaxSize(); PageMemory pageMem = region.pageMemory(); @@ -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++; + + if (attemptsWithoutProgress >= maxAttemptsWithoutProgress) + throw outOfMemory(regCfg); } } + /** + * Invokes a single page eviction, acquiring entry locks non-blockingly so that contended entries are skipped. + * This is required when eviction runs while the current thread already holds entry locks (size-aware eviction + * from a single-row insertion) to avoid a lock-ordering deadlock. {@link NoOpPageEvictionTracker} + * (disabled eviction, never reaching this path) falls back to the plain {@code evictDataPage()}. + * + * @param evictionTracker Page eviction tracker. + * @throws IgniteCheckedException If failed to evict a data page. + */ + private void evictDataPageNonBlocking(PageEvictionTracker evictionTracker) throws IgniteCheckedException { + if (evictionTracker instanceof PageAbstractEvictionTracker) + ((PageAbstractEvictionTracker)evictionTracker).evictDataPageNonBlocking(); + else + evictionTracker.evictDataPage(); + } + + /** + * @param regCfg Data region configuration. + * @return New {@link IgniteOutOfMemoryException} (also reported as a critical failure) for the given region. + */ + private IgniteOutOfMemoryException outOfMemory(DataRegionConfiguration regCfg) { + IgniteOutOfMemoryException oom = new IgniteOutOfMemoryException("Out of memory in data region [" + + "name=" + regCfg.getName() + + ", initSize=" + U.readableSize(regCfg.getInitialSize(), false) + + ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) + + ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "] Try the following:" + U.nl() + + " ^-- Increase maximum off-heap memory size (DataRegionConfiguration.maxSize)" + U.nl() + + " ^-- Enable Ignite persistence (DataRegionConfiguration.persistenceEnabled)" + U.nl() + + " ^-- Enable eviction or expiration policies" + ); + + if (cctx.kernalContext() != null) + cctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, oom)); + + return oom; + } + /** * See {@code GridCacheMapEntry#ensureFreeSpace()} * diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java index cffcf9b1e5be0..86e5e94595d1d 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/RowStore.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.function.Supplier; import org.apache.ignite.IgniteCheckedException; +import org.apache.ignite.configuration.DataPageEvictionMode; import org.apache.ignite.internal.metric.IoStatisticsHolder; import org.apache.ignite.internal.pagemem.PageIdUtils; import org.apache.ignite.internal.pagemem.PageMemory; @@ -134,6 +135,20 @@ public void addRow(CacheDataRow row, IoStatisticsHolder statHolder) throws Ignit */ public void addRows(Collection 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); + } + assert ctx.database().checkpointLockIsHeldByThread(); freeList.insertDataRows(rows, statHolder); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java index 2330c0942662d..fade3443b109c 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageAbstractEvictionTracker.java @@ -41,6 +41,13 @@ public abstract class PageAbstractEvictionTracker implements PageEvictionTracker /** Millis in day. */ private static final int DAY = 24 * 60 * 60 * 1000; + /** + * Thread-local marker that the current eviction is requested by size-aware eviction, which may run + * while the calling thread already holds entry locks. When set, entries whose locks are contended are skipped + * (via a non-blocking {@code evictInternal}) instead of blocking, avoiding a lock-ordering deadlock. + */ + private static final ThreadLocal EVICT_NON_BLOCKING = new ThreadLocal<>(); + /** Page memory. */ protected final PageMemoryNoStoreImpl pageMem; @@ -87,6 +94,26 @@ public abstract class PageAbstractEvictionTracker implements PageEvictionTracker return pageMem.loadedPages() > pagesThreshold && freeList.emptyDataPages() < regCfg.getEmptyPagesPoolSize(); } + /** + * Evicts a data page, acquiring entry locks in a non-blocking way so that contended entries are skipped instead + * of blocked upon. Used by size-aware eviction which may run while the calling thread already holds + * entry locks, to avoid a lock-ordering deadlock. + * + * @throws IgniteCheckedException If failed. + */ + public void evictDataPageNonBlocking() throws IgniteCheckedException { + Boolean prev = EVICT_NON_BLOCKING.get(); + + EVICT_NON_BLOCKING.set(Boolean.TRUE); + + try { + evictDataPage(); + } + finally { + EVICT_NON_BLOCKING.set(prev); + } + } + /** * @param pageIdx Page index. * @return true if at least one data row has been evicted @@ -144,7 +171,8 @@ final boolean evictDataPage(int pageIdx) throws IgniteCheckedException { GridCacheEntryEx entryEx = cacheCtx.isNear() ? cacheCtx.near().dht().entryEx(dataRow.key()) : cacheCtx.cache().entryEx(dataRow.key()); - evictionDone |= entryEx.evictInternal(GridCacheVersionManager.EVICT_VER, null, true); + evictionDone |= entryEx.evictInternal(GridCacheVersionManager.EVICT_VER, null, true, + Boolean.TRUE.equals(EVICT_NON_BLOCKING.get())); } return evictionDone; diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java new file mode 100644 index 0000000000000..e6d5d518fb7e3 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.ignite.internal.processors.cache.eviction.paged; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE; + +/** + * Concurrent deadlock test for size-aware page eviction. + *

+ * The region is first filled with a large number of small entries (so there is plenty of evictable page space), then + * several threads concurrently insert large rows (larger than the empty-pages pool). Each large insert goes through + * the size-aware reserve and, for the single-row path, eviction under the new entry lock with the non-blocking + * {@code tryLockEntry}. The average data volume is kept within the region capacity, so eviction frees already-stored + * small entries rather than overrunning the free list. The test asserts that no deadlock occurs (all threads finish + * within a global deadline). + */ +public abstract class PageEvictionConcurrentWritesAbstractTest extends GridCommonAbstractTest { + /** Off-heap region size. */ + private static final int SIZE = 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 LARGE_RECORD_SIZE = 4 * 1024 * 1024; + + /** Small record size used to pre-fill the region with evictable data. */ + private static final int SMALL_RECORD_SIZE = 4096; + + /** Empty pages pool size. */ + private static final int POOL_SIZE = 100; + + /** Number of small pre-fill entries. */ + private static final int SMALL_ENTRIES = 10_000; + + /** Number of writer threads. */ + private static final int THREADS = 4; + + /** Large rows inserted per thread (moderate total, kept within region capacity after eviction of small rows). */ + private static final int LARGE_ROWS_PER_THREAD = 3; + + /** Global deadline for the whole test (protects against a deadlock/busy-spin hang). */ + private static final long DEADLINE = TimeUnit.MINUTES.toMillis(3); + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return super.getConfiguration(gridName) + .setDataStorageConfiguration(new DataStorageConfiguration() + .setDefaultDataRegionConfiguration(new DataRegionConfiguration() + .setInitialSize(SIZE) + .setMaxSize(SIZE) + .setEmptyPagesPoolSize(POOL_SIZE) + ) + .setPageSize(DFLT_PAGE_SIZE) + ); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + } + + /** + * @param ignite Ignite node. + * @return Cache with a small partition count (reduces structural page overhead). + */ + private IgniteCache createCache(IgniteEx ignite) { + return ignite.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME) + .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS))); + } + + /** + * Concurrent large inserts into a region pre-filled with small entries must complete within the deadline without + * deadlock, and without corrupting the free list (eviction frees small entries rather than overrunning the region). + * + * @throws Exception If failed. + */ + @Test + public void testConcurrentLargeWritesNoDeadlock() throws Exception { + IgniteEx ignite = startGrid(1); + + IgniteCache cache = createCache(ignite); + + // Pre-fill the region with many small entries so that eviction always has evictable pages to free. + for (int i = 0; i < SMALL_ENTRIES; i++) + cache.put(i, new byte[SMALL_RECORD_SIZE]); + + byte[] largeVal = new byte[LARGE_RECORD_SIZE]; + + AtomicLong errors = new AtomicLong(); + + CountDownLatch startLatch = new CountDownLatch(1); + + long deadline = System.currentTimeMillis() + DEADLINE; + + Thread[] threads = new Thread[THREADS]; + + for (int i = 0; i < THREADS; i++) { + final int threadIdx = i; + + threads[i] = new Thread(() -> { + try { + startLatch.await(); + + for (int k = 0; k < LARGE_ROWS_PER_THREAD; k++) + cache.put(SMALL_ENTRIES + threadIdx * LARGE_ROWS_PER_THREAD + k, largeVal); + } + catch (Throwable e) { + errors.incrementAndGet(); + + log.error("Unexpected error in writer thread", e); + } + }, "paged-writer-" + i); + + threads[i].start(); + } + + startLatch.countDown(); + + for (Thread t : threads) + t.join(Math.max(1, deadline - System.currentTimeMillis())); + + // The core assertion of this deadlock test: every writer must have completed (no thread is stuck waiting on + // an entry lock held by size-aware eviction running under another entry lock). + for (Thread t : threads) + assertFalse("Writer thread " + t.getName() + " did not finish (possible deadlock)", t.isAlive()); + + assertEquals("Writer threads reported errors", 0, errors.get()); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java new file mode 100644 index 0000000000000..03af63f8ff0e6 --- /dev/null +++ b/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. + *

+ * 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). + *

+ * 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 createCache(IgniteEx ignite) { + // TRANSACTIONAL is required so that cache.lockAll(...) can hold entry locks (the root cause of the + // "no evictable page" scenario this test exercises). + return ignite.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME) + .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)) + .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)); + } + + /** + * Filling the region with locked entries and then writing a row that needs more free pages than remain must fail + * with OOM (bounded time), not hang: eviction cannot free any page because every candidate entry is locked. + * + * @throws Exception If failed. + */ + @Test(timeout = 180_000) + @WithSystemProperty(key = "ENTRY_LOCK_TIMEOUT", value = "1") + public void testGuardOomWhenAllEntriesLocked() throws Exception { + IgniteEx ignite = startGrid(1); + + IgniteCache cache = createCache(ignite); + + // Pre-fill the region so that less than one large record of free space remains, without overflowing it. + byte[] fillVal = new byte[FILL_VALUE_SIZE]; + + for (int i = 1; i <= FILL_ENTRIES; i++) + cache.put(i, fillVal); + + Collection keys = new ArrayList<>(FILL_ENTRIES); + + for (int i = 1; i <= FILL_ENTRIES; i++) + keys.add(i); + + CountDownLatch ready = new CountDownLatch(1); + + CountDownLatch release = new CountDownLatch(1); + + AtomicReference lockerErr = new AtomicReference<>(); + + // Hold entry locks on every resident key from a background thread so that eviction has no evictable page. + Thread locker = new Thread(() -> { + try { + Lock lock = cache.lockAll(keys); + + lock.lock(); + + ready.countDown(); + + release.await(); + + lock.unlock(); + } + catch (Throwable e) { + lockerErr.set(e); + + ready.countDown(); + } + }, "size-aware-guard-locker"); + + locker.start(); + + assertTrue("Timed out waiting for entries to be locked", ready.await(60, TimeUnit.SECONDS)); + + assertNull("Unexpected error while locking entries: " + lockerErr.get(), lockerErr.get()); + + try { + cache.put(FILL_ENTRIES + 1, new byte[LARGE_RECORD_SIZE]); + + fail("Expected out-of-memory because all resident entries are locked, but put succeeded"); + } + catch (Exception e) { + assertTrue("Expected an out-of-memory (progress guard) failure, but got: " + e, isOutOfMemory(e)); + } + finally { + release.countDown(); + + locker.join(TimeUnit.SECONDS.toMillis(10)); + } + } + + /** + * @param t Throwable. + * @return {@code True} if {@code t} or any of its causes is an out-of-memory. + */ + private static boolean isOutOfMemory(Throwable t) { + for (Throwable cur = t; cur != null; cur = cur.getCause()) { + if (cur instanceof IgniteOutOfMemoryException) + return true; + } + + return false; + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java index 9f40cf4958431..b7a86a2459611 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionMetricTest.java @@ -49,6 +49,37 @@ public void testPageEvictionMetric() throws Exception { checkPageEvictionMetric(CacheAtomicityMode.ATOMIC); } + /** + * Regression: ordinary small records that keep the region below the eviction threshold must not trigger page + * eviction at all (eviction is not started, eviction rate stays zero). + * + * @throws Exception If failed. + */ + @Test + public void testNoEvictionBelowThreshold() throws Exception { + IgniteEx ignite = startGrid(0); + + DataRegionMetricsImpl metrics = + ignite.context().cache().context().database().dataRegion(null).metrics(); + + metrics.enableMetrics(); + + CacheConfiguration cfg = cacheConfig("no-evict-below-threshold", null, + CacheMode.PARTITIONED, CacheAtomicityMode.ATOMIC, CacheWriteSynchronizationMode.PRIMARY_SYNC); + + IgniteCache cache = ignite.getOrCreateCache(cfg); + + // A small number of records far below the eviction threshold and empty-pages pool pressure. + for (int i = 1; i <= 500; i++) + cache.put(i, new TestObject(PAGE_SIZE / 6)); + + assertFalse("Page eviction must not start while the region is below the eviction threshold", + metrics.isEvictionsStarted()); + + assertEquals("Eviction rate must be zero while the region is below the eviction threshold", + 0f, metrics.getEvictionRate(), 0f); + } + /** * @throws Exception If failed. */ diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionPutLargeObjectsAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionPutLargeObjectsAbstractTest.java index b141fa82bb357..4ece99be95f6e 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionPutLargeObjectsAbstractTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionPutLargeObjectsAbstractTest.java @@ -70,6 +70,9 @@ public void testPutLargeObjects() throws Exception { for (Integer key : primaryKeys(grid(1).cache(DEFAULT_CACHE_NAME), ENTRIES)) cache.put(key, val); - assertTrue(cache.size() < ENTRIES); + // With size-aware eviction the large records do not fail with OOM: older records are evicted to + // make room for the newer ones. The resident set must therefore be bounded well below the total written + // (50 x 80MB >> 1GB region) but stay non-empty (at least the most recently written entries survive). + assertTrue(cache.size() > 0 && cache.size() < ENTRIES); } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java new file mode 100644 index 0000000000000..6731287fae957 --- /dev/null +++ b/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 createCache(IgniteEx ignite) { + return ignite.createCache(new CacheConfiguration(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 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 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 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 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 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 cache = createCache(ignite); + + 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)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java new file mode 100644 index 0000000000000..c6dd438367de1 --- /dev/null +++ b/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 createCache(IgniteEx ignite, long ttl) { + CacheConfiguration ccfg = new CacheConfiguration(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 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 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); + + // Wait for the TTL worker to expire and free the short-TTL entries. + Thread.sleep(TTL + 1500); + + // A fresh large record must now be accepted (space freed by TTL counts as available for eviction). + cache.put(100, val); + + assertNotNull(cache.get(100)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionConcurrentWritesTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionConcurrentWritesTest.java new file mode 100644 index 0000000000000..320d48cdde1cd --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionConcurrentWritesTest.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.IgniteConfiguration; + +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionAbstractTest.setEvictionMode; + +/** + * Concurrent eviction/insertion test for {@link DataPageEvictionMode#RANDOM_2_LRU}. + */ +public class Random2LruPageEvictionConcurrentWritesTest extends PageEvictionConcurrentWritesAbstractTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return setEvictionMode(DataPageEvictionMode.RANDOM_2_LRU, super.getConfiguration(gridName)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionSizeAwareTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionSizeAwareTest.java new file mode 100644 index 0000000000000..f9b9e405905cf --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionSizeAwareTest.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.IgniteConfiguration; + +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionAbstractTest.setEvictionMode; + +/** + * Size-aware page eviction test for {@link DataPageEvictionMode#RANDOM_2_LRU}. + */ +public class Random2LruPageEvictionSizeAwareTest extends PageEvictionSizeAwareAbstractTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return setEvictionMode(DataPageEvictionMode.RANDOM_2_LRU, super.getConfiguration(gridName)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionWithExpiryPolicyTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionWithExpiryPolicyTest.java new file mode 100644 index 0000000000000..cdbb5218c05b1 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionWithExpiryPolicyTest.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.IgniteConfiguration; + +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionAbstractTest.setEvictionMode; + +/** + * ExpiryPolicy + page eviction synergy test for {@link DataPageEvictionMode#RANDOM_2_LRU}. + */ +public class Random2LruPageEvictionWithExpiryPolicyTest extends PageEvictionWithExpiryPolicyAbstractTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return setEvictionMode(DataPageEvictionMode.RANDOM_2_LRU, super.getConfiguration(gridName)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionConcurrentWritesTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionConcurrentWritesTest.java new file mode 100644 index 0000000000000..d3dc5b99d0437 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionConcurrentWritesTest.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.IgniteConfiguration; + +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionAbstractTest.setEvictionMode; + +/** + * Concurrent eviction/insertion test for {@link DataPageEvictionMode#RANDOM_LRU}. + */ +public class RandomLruPageEvictionConcurrentWritesTest extends PageEvictionConcurrentWritesAbstractTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return setEvictionMode(DataPageEvictionMode.RANDOM_LRU, super.getConfiguration(gridName)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionSizeAwareTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionSizeAwareTest.java new file mode 100644 index 0000000000000..b2bb394b65f0e --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionSizeAwareTest.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.IgniteConfiguration; + +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionAbstractTest.setEvictionMode; + +/** + * Size-aware page eviction test for {@link DataPageEvictionMode#RANDOM_LRU}. + */ +public class RandomLruPageEvictionSizeAwareTest extends PageEvictionSizeAwareAbstractTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return setEvictionMode(DataPageEvictionMode.RANDOM_LRU, super.getConfiguration(gridName)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionWithExpiryPolicyTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionWithExpiryPolicyTest.java new file mode 100644 index 0000000000000..ff5a209da1eb5 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionWithExpiryPolicyTest.java @@ -0,0 +1,32 @@ +/* + * 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 org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.IgniteConfiguration; + +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionAbstractTest.setEvictionMode; + +/** + * ExpiryPolicy + page eviction synergy test for {@link DataPageEvictionMode#RANDOM_LRU}. + */ +public class RandomLruPageEvictionWithExpiryPolicyTest extends PageEvictionWithExpiryPolicyAbstractTest { + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return setEvictionMode(DataPageEvictionMode.RANDOM_LRU, super.getConfiguration(gridName)); + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheEvictionSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheEvictionSelfTestSuite.java index 03d95e5fabbcb..0b240cf0f23dd 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheEvictionSelfTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteCacheEvictionSelfTestSuite.java @@ -38,19 +38,26 @@ import org.apache.ignite.internal.processors.cache.eviction.lru.LruEvictionPolicySelfTest; import org.apache.ignite.internal.processors.cache.eviction.lru.LruNearEvictionPolicySelfTest; import org.apache.ignite.internal.processors.cache.eviction.lru.LruNearOnlyNearEvictionPolicySelfTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionGuardOomTest; import org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionMetricTest; import org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionPagesRecyclingAndReusingTest; import org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionReadThroughTest; import org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionTouchOrderTest; import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruNearEnabledPageEvictionMultinodeTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionConcurrentWritesTest; import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionDataStreamerTest; import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionMultinodeTest; import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionPutLargeObjectsTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionSizeAwareTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionWithExpiryPolicyTest; import org.apache.ignite.internal.processors.cache.eviction.paged.Random2LruPageEvictionWithRebalanceTest; import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruNearEnabledPageEvictionMultinodeTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionConcurrentWritesTest; import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionDataStreamerTest; import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionMultinodeTest; import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionPutLargeObjectsTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionSizeAwareTest; +import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionWithExpiryPolicyTest; import org.apache.ignite.internal.processors.cache.eviction.paged.RandomLruPageEvictionWithRebalanceTest; import org.apache.ignite.internal.processors.cache.eviction.sorted.SortedEvictionPolicyFactorySelfTest; import org.apache.ignite.internal.processors.cache.eviction.sorted.SortedEvictionPolicySelfTest; @@ -100,8 +107,19 @@ public static List> suite(Collection ignoredTests) { GridTestUtils.addTestIfNeeded(suite, RandomLruPageEvictionPutLargeObjectsTest.class, ignoredTests); GridTestUtils.addTestIfNeeded(suite, Random2LruPageEvictionPutLargeObjectsTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, RandomLruPageEvictionSizeAwareTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, Random2LruPageEvictionSizeAwareTest.class, ignoredTests); + + GridTestUtils.addTestIfNeeded(suite, RandomLruPageEvictionWithExpiryPolicyTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, Random2LruPageEvictionWithExpiryPolicyTest.class, ignoredTests); + + GridTestUtils.addTestIfNeeded(suite, RandomLruPageEvictionConcurrentWritesTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, Random2LruPageEvictionConcurrentWritesTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, PageEvictionMetricTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, PageEvictionGuardOomTest.class, ignoredTests); + GridTestUtils.addTestIfNeeded(suite, PageEvictionPagesRecyclingAndReusingTest.class, ignoredTests); GridTestUtils.addTestIfNeeded(suite, DhtAndNearEvictionTest.class, ignoredTests);