diff --git a/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java new file mode 100644 index 0000000000000..29145f07d4ea5 --- /dev/null +++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java @@ -0,0 +1,209 @@ +/* + * 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.benchmarks.jmh.cache; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.ignite.Ignite; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.IgniteDataStreamer; +import org.apache.ignite.Ignition; +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.benchmarks.jmh.runner.JmhIdeBenchmarkRunner; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Measures the impact of size-aware page eviction on an in-memory (non-persistent) data region. + *

+ * Two benchmark methods: + *

+ */ +@State(Scope.Benchmark) +@Fork(1) +@Threads(4) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 3, time = 5) +@Measurement(iterations = 5, time = 10) +public class JmhPageEvictionBenchmark { + /** Default cache name. */ + private static final String CACHE_NAME = "default"; + + /** Empty pages pool size (kept low so that the LARGE scenario reliably exceeds it). */ + private static final int POOL_SIZE = 100; + + /** Small value size (bytes): a single data page, far below the empty-pages pool. */ + private static final int SMALL_VALUE_SIZE = 1024; + + /** Large value size (bytes): larger than the empty-pages pool in page terms. */ + private static final int LARGE_VALUE_SIZE = 2 * 1024 * 1024; + + /** + * Number of pre-fill small entries for the LARGE scenario. Chosen so that the total written data + * (400k x 1 KiB) far exceeds the region capacity: threshold eviction then pins the region at the eviction + * threshold (default ~90% of {@code maxSize}), leaving the free list with only its empty-pages pool. At that + * point a {@link #LARGE_VALUE_SIZE} put cannot take the fast path and must actually run the size-aware eviction + * loop. (A modest pre-fill such as 48k x 1 KiB would leave the region only ~19% full and let every large put + * fit into the headroom via the fast path, so it would never exercise the code under measurement.) + */ + private static final int PRE_FILL_ENTRIES = 400_000; + + /** + * Bounded key range for {@link #putSmall()}. Each {@link #SMALL_VALUE_SIZE} value occupies one data page, so a + * working set of this many resident keys (~32k x 4 KiB ~ 128 MiB) stays comfortably below the eviction + * threshold (~90% of the 256 MiB region). Overwriting within this bounded range (instead of append-style fresh + * keys) keeps the region from filling up and drifting into steady-state threshold eviction during measurement, + * so the run isolates the per-put cost of the size-aware-reserve fast path. + */ + private static final int SMALL_KEY_RANGE = 32_000; + + /** Benchmark scenario: selects the value size and the pre-fill strategy. */ + @Param({"SMALL", "LARGE"}) + private String scenario; + + /** Ignite cache. */ + private IgniteCache cache; + + /** Pre-allocated small value (reused to avoid allocation noise in the hot path). */ + private final byte[] smallVal = new byte[SMALL_VALUE_SIZE]; + + /** Pre-allocated large value (reused to avoid allocation noise in the hot path). */ + private final byte[] largeVal = new byte[LARGE_VALUE_SIZE]; + + /** Monotonic key source: bounded (mod {@link #SMALL_KEY_RANGE}) for {@link #putSmall()} to keep the region below + * the eviction threshold, and unbounded (append-style) for {@link #putLarge()} to avoid overwriting entries. */ + private final AtomicInteger keyGen = new AtomicInteger(); + + /** Page eviction mode used for the data region. */ + @Param("RANDOM_LRU") + private String evictionMode; + + /** Put of a small value (hot path, size-aware reserve takes its fast path). Keys are wrapped within a bounded + * range ({@link #SMALL_KEY_RANGE}) so the resident working set stays below the eviction threshold and the run + * isolates the fast-path cost instead of drifting into steady-state threshold eviction. */ + @Benchmark + public void putSmall() { + int key = keyGen.incrementAndGet() % SMALL_KEY_RANGE; + + cache.put(key, smallVal); + } + + /** + * Put of a large value against a nearly-full region (runs the size-aware eviction loop). + *

+ * Pinned to a single thread: the size-aware reserve accumulates {@code requiredPages} real empty pages + * in the shared free list before writing, and with multiple concurrent writers those free pages are consumed + * by rivals as fast as they are freed, so no thread ever accumulates enough and the loop exhausts its + * no-progress budget into an out-of-memory. At one thread the free-page count grows monotonically and the + * reserve completes, measuring the honest per-put cost of eviction. + */ + @Benchmark + @Threads(1) + public void putLarge() { + int key = keyGen.incrementAndGet(); + + cache.put(key, largeVal); + } + + /** Starts Ignite with an in-memory, eviction-enabled data region and pre-fills it for the LARGE scenario. */ + @Setup(Level.Trial) + public void setup() { + long regionSize = 256 * 1024L * 1024L; + + DataStorageConfiguration dsCfg = new DataStorageConfiguration() + .setDefaultDataRegionConfiguration(new DataRegionConfiguration() + .setPersistenceEnabled(false) + .setMaxSize(regionSize) + .setEmptyPagesPoolSize(POOL_SIZE) + .setPageEvictionMode(DataPageEvictionMode.valueOf(evictionMode))); + + IgniteConfiguration cfg = new IgniteConfiguration() + .setIgniteInstanceName("test") + .setLocalHost("127.0.0.1") + .setDataStorageConfiguration(dsCfg); + + Ignite ignite = Ignition.start(cfg); + + cache = ignite.getOrCreateCache(new CacheConfiguration(CACHE_NAME).setBackups(0)); + + // Pre-fill the region with small entries for the LARGE scenario until threshold eviction pins it at the + // eviction threshold, so that a large put has no headroom to grow into and must actually evict. + if ("LARGE".equalsIgnoreCase(scenario)) { + try (IgniteDataStreamer ldr = ignite.dataStreamer(CACHE_NAME)) { + ldr.perNodeBufferSize(1024); + + for (int i = 0; i < PRE_FILL_ENTRIES; i++) + ldr.addData(i, smallVal); + } + + // The pre-fill consumed keys [0, PRE_FILL_ENTRIES). Start large puts after that range so they write + // brand-new keys (true append), leaving the pre-filled small entries in place to be the eviction + // candidates, instead of overwriting them in place. + keyGen.set(PRE_FILL_ENTRIES); + } + } + + /** @return Test data. */ + @Override public String toString() { + return "JmhPageEvictionBenchmark[scenario=" + scenario + ", evictionMode=" + evictionMode + ']'; + } + + /** Stops all Ignite instances started by this benchmark. */ + @TearDown + public void tearDown() { + Ignition.stopAll(true); + } + + /** + * Runs this benchmark over both {@code SMALL} and {@code LARGE} scenarios (configured by {@code @Param}). + * + * @param args Ignored. + * @throws Exception If failed. + */ + public static void main(String[] args) throws Exception { + JmhIdeBenchmarkRunner.create() + .benchmarks(JmhPageEvictionBenchmark.class.getSimpleName()) + .run(); + } +} 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..042e9f78a323d 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,30 @@ public interface GridCacheEntryEx { public boolean evictInternal(GridCacheVersion obsoleteVer, @Nullable CacheEntryPredicate[] filter, boolean evictOffheap) throws IgniteCheckedException; + /** + * Same as {@link #evictInternal(GridCacheVersion, CacheEntryPredicate[], boolean)}, but when {@code tryLock} is + * {@code true} the entry lock is acquired non-blockingly: the entry is skipped (this method returns {@code false}) + * whenever its lock is contended or already held by the current thread (the self-hold case), instead of blocking. + * 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 ignores {@code tryLock} and 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 or self-held 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..dc34161f9b3ba 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 @@ -3658,7 +3658,10 @@ protected void removeValue() throws IgniteCheckedException { * Evicts necessary number of data pages if per-page eviction is configured in current {@link DataRegion}. */ private void ensureFreeSpace() throws IgniteCheckedException { - // Deadlock alert: evicting data page causes removing (and locking) all entries on the page one by one. + // Deadlock alert: evicting a data page removes (and locks) all entries on the page one by one, so this must + // only run while NOT holding this entry's lock (all call sites run before lockEntry()). The size-aware path + // (RowStore.addRow -> ensureFreeSpaceForInsert) runs under the lock and instead uses the non-blocking + // tryLockEntry(0) inside evictInternal. assert !lock.isHeldByCurrentThread(); cctx.shared().database().ensureFreeSpace(cctx.dataRegion()); @@ -3684,14 +3687,26 @@ private CacheEntryImplEx wrapVersionedWithValue() { @Override public boolean evictInternal( GridCacheVersion obsoleteVer, @Nullable CacheEntryPredicate[] filter, - boolean evictOffheap) - throws IgniteCheckedException { + 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 the lock is taken non-blockingly and a contended/self-held entry is skipped + // (returns false) to avoid a lock-ordering deadlock; the tracker then picks another page. + if (!lockEntry(tryLock)) + return false; try { if (evictionDisabled()) { @@ -3728,7 +3743,8 @@ private CacheEntryImplEx wrapVersionedWithValue() { while (true) { GridCacheVersion v; - lockEntry(); + if (!lockEntry(tryLock)) + return false; try { v = ver; @@ -3740,7 +3756,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 +4199,23 @@ private int extrasSize() { lock.lock(); } + /** + * Acquires the entry lock either blocking ({@code tryLock == false}) or non-blockingly with an immediate + * {@code tryLock(0)} ({@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 !lock.isHeldByCurrentThread() && tryLockEntry(0); + + 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..5139d9f263c72 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 @@ -26,6 +26,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -74,6 +76,7 @@ import org.apache.ignite.internal.processors.cache.persistence.metastorage.MetaStorage; import org.apache.ignite.internal.processors.cache.persistence.metastorage.MetastorageLifecycleListener; import org.apache.ignite.internal.processors.cache.persistence.pagemem.PageReadWriteManager; +import org.apache.ignite.internal.processors.cache.persistence.tree.io.AbstractDataPageIO; import org.apache.ignite.internal.processors.cache.persistence.tree.reuse.ReuseList; import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer; import org.apache.ignite.internal.processors.cache.warmup.WarmUpStrategy; @@ -137,6 +140,33 @@ public class IgniteCacheDatabaseSharedManager extends GridCacheSharedManagerAdap /** Maximum initial size on 32-bit JVM */ private static final long MAX_PAGE_MEMORY_INIT_SIZE_32_BIT = 2L * 1024 * 1024 * 1024; + /** Initial backoff (nanoseconds) between consecutive no-progress eviction attempts. */ + private static final long EVICTION_BACKOFF_START_NANOS = 50_000L; + + /** Upper bound (nanoseconds) for the backoff between no-progress eviction attempts. */ + private static final long EVICTION_BACKOFF_MAX_NANOS = 1_000_000L; + + /** + * Maximum time (milliseconds) the size-aware eviction guard is willing to wait without either an eviction or a + * new highest empty-pages count before failing with an out-of-memory. Being time-based (measured from the last + * progress) rather than a fixed attempt count means a slow-but-progressing eviction is never torn down, while a + * genuinely stuck eviction (nothing evictable, or contenders that never release their locks) still terminates in + * bounded time instead of busy-spinning forever. The overall cycle is additionally capped by + * {@link #EVICTION_MAX_CYCLE_TIME_MILLIS}. + */ + private static final long EVICTION_NO_PROGRESS_TIMEOUT_MILLIS = 1_000L; + + /** + * Hard upper bound (milliseconds) on the total duration of one size-aware eviction cycle, regardless of + * per-iteration progress. {@link #EVICTION_NO_PROGRESS_TIMEOUT_MILLIS} bounds the period with no progress, but + * eviction that keeps making partial progress (e.g. evicting a contended page's available entries one-by-one + * without ever emptying a page, so the empty-pages count never reaches the target) could otherwise extend the + * loop indefinitely; this absolute deadline guarantees the cycle still fails with an out-of-memory in bounded + * time while the per-progress timeout above allows a genuinely progressing eviction to run to its natural end. + * It also caps the worst-case time the reserve can hold the caller's entry lock. + */ + private static final long EVICTION_MAX_CYCLE_TIME_MILLIS = 5 * EVICTION_NO_PROGRESS_TIMEOUT_MILLIS; + /** {@code True} to reuse memory on deactive. */ protected final boolean reuseMemory = IgniteSystemProperties.getBoolean(IGNITE_REUSE_MEMORY_ON_DEACTIVATE); @@ -1172,39 +1202,63 @@ public WALPointer latestWalPointerReservedForPreloading() { } /** - * Checks that the given {@code region} has enough space for putting a new entry. - * - * 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}. - * - * 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. + * Checks that the given {@code region} has enough space for putting a new entry of {@code dataRowSize} bytes. + *

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

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

+ * Worst case: when called while the entry being written is locked (single-row insertion), the eviction loop can + * hold that lock for up to {@link #EVICTION_MAX_CYCLE_TIME_MILLIS} — only if eviction cannot consolidate enough + * empty pages within that time (e.g. a long-running transaction holding all evictable entries, or only partial + * progress), after which the call fails with {@link IgniteOutOfMemoryException} (reported as a critical failure + * to the configured failure handler). * * @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; - long memorySize = regCfg.getMaxSize(); + 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 { PageMemory pageMem = region.pageMemory(); - CacheFreeList freeList = freeListMap.get(regCfg.getName()); - - long nonEmptyPages = (pageMem.loadedPages() - freeList.emptyDataPages()); + long nonEmptyPages = (pageMem.loadedPages() - freeListMap.get(regCfg.getName()).emptyDataPages()); // The maximum number of pages that can be allocated (memorySize / systemPageSize) // should be greater or equal to pages required for inserting a new entry plus @@ -1213,27 +1267,159 @@ public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) throws // Note that not the whole page can be used to storing links, // see PagesListNodeIO and PagesListMetaIO#getCapacity(), so we pessimistically multiply the result on 1.5, // in any way, the number of required pages is less than 1 percent. - boolean oomThreshold = (memorySize / pageMem.systemPageSize()) < + boolean oomThreshold = (regCfg.getMaxSize() / pageMem.systemPageSize()) < ((double)dataRowSize / pageMem.pageSize() + nonEmptyPages * (8.0 * 1.5 / pageMem.pageSize() + 1) + 256 /*one page per bucket*/); - if (oomThreshold) { - IgniteOutOfMemoryException oom = new IgniteOutOfMemoryException("Out of memory in data region [" + - "name=" + regCfg.getName() + - ", initSize=" + U.readableSize(regCfg.getInitialSize(), false) + - ", maxSize=" + U.readableSize(regCfg.getMaxSize(), false) + - ", persistenceEnabled=" + regCfg.isPersistenceEnabled() + "] Try the following:" + U.nl() + - " ^-- Increase maximum off-heap memory size (DataRegionConfiguration.maxSize)" + U.nl() + - " ^-- Enable Ignite persistence (DataRegionConfiguration.persistenceEnabled)" + U.nl() + - " ^-- Enable eviction or expiration policies" - ); + if (oomThreshold) + throw outOfMemory(regCfg); + } + + /** + * Size-aware reserve for an eviction-enabled non-persistent region. Runs eviction until the free list holds + * enough real empty pages to accommodate the row, or throws {@link IgniteOutOfMemoryException} if the goal is + * unreachable / no progress can be made. Progress is measured against the number of empty pages in the free list + * (the only resource a subsequent fragmented write can reliably consume once the region is effectively full); the + * region's spare capacity (headroom) is only trusted in the fast path while the region is below the eviction + * threshold. + * + * @param region Data region. + * @param regCfg Data region configuration. + * @param dataRowSize Size of data row to be inserted. + * @throws IgniteOutOfMemoryException If the target cannot be reached (row too large for the region or eviction + * makes no progress). + * @throws IgniteCheckedException If failed to evict data pages. + */ + private void ensureFreeSpaceForEviction( + DataRegion region, + DataRegionConfiguration regCfg, + int dataRowSize + ) throws IgniteOutOfMemoryException, IgniteCheckedException { + PageMemory pageMem = region.pageMemory(); + + // Maximum payload bytes that a single data page can hold for a fragmented row. + long pagePayload = pageMem.pageSize() - AbstractDataPageIO.MIN_DATA_PAGE_OVERHEAD; - if (cctx.kernalContext() != null) - cctx.kernalContext().failure().process(new FailureContext(FailureType.CRITICAL_ERROR, oom)); + // A row that fits into the steady-state empty-pages pool is satisfied by normal threshold eviction, so the + // fast path is a single comparison (no page computation, free-list lookup or page-memory reads on the hot + // small-put path). + if (dataRowSize <= regCfg.getEmptyPagesPoolSize() * pagePayload) + return; - throw oom; + CacheFreeList freeList = freeListMap.get(regCfg.getName()); + + if (freeList == null) + return; + + long totalPages = regCfg.getMaxSize() / pageMem.systemPageSize(); + + // Pages the row will actually occupy once written, and which the free list must hand out on demand during + // the fragmented write. + long requiredPages = (dataRowSize + pagePayload - 1) / pagePayload; + + // The row fundamentally cannot fit into the whole region. + if (requiredPages > totalPages) + throw outOfMemory(regCfg); + + // The reserve must guarantee `requiredPages` REAL empty pages, not just apparent headroom. Both are shared and + // non-exclusive (emptyDataPages() is a snapshot; any writer can consume them), but once the region is full + // (loadedPages == totalPages) headroom can no longer grow it (fresh allocateDataPage -> raw OOM), while empty + // pages in the reuse bucket stay reachable via takePage(). So empty pages are the only resource the fragmented + // write can consume on a full region. The TOCTOU between this reserve and the actual write is closed by the + // lazy re-reserve in AbstractFreeList#writeSinglePage. + long emptyPages = freeList.emptyDataPages(); + + // The gate reuses evictionThreshold as a regime boundary, not as "when to start eviction" (evictionRequired() + // does that, stopping on emptyPages >= poolSize; no last 10% of page memory is left unusable). Below the + // threshold the region has real slack, so a row fitting into the combined spare space is satisfied without + // eviction (live, e.g. short-TTL, entries are not evicted just to accumulate empty pages). At/above it headroom + // is no longer trustworthy (concurrent writers could commit the same headroom - TOCTOU), so only real empty + // pages are counted and eviction is driven below. + boolean evictionRegime = pageMem.loadedPages() >= (long)(totalPages * regCfg.getEvictionThreshold()); + + // Fast path: skip eviction when (a) enough real empty pages exist, or (b) below the regime with enough spare + // space to grow into. This is a snapshot and only necessary, not sufficient: under contention two writers can + // both pass and consume the same pages - recovered by the lazy re-reserve in AbstractFreeList#writeSinglePage. + if (emptyPages >= requiredPages || (!evictionRegime && emptyPages + (totalPages - pageMem.loadedPages()) >= requiredPages)) + return; + + PageEvictionTracker evictionTracker = region.evictionTracker(); + + // Evict until the free list holds enough real empty pages. Progress counts as either an actual eviction this + // iteration (evictDataPage returned true) or a new high-water empty-pages count. The count alone is unreliable + // under concurrency: writers consume the shared counter as fast as eviction frees pages, so a thread may never + // see its high-water exceeded while eviction still makes real global progress - tearing down there would be a + // false OOM. Counting the actual eviction prevents that; when eviction genuinely cannot proceed the method + // returns false, so the loop still fails with OOM in bounded time. + long bestEmptyPages = emptyPages; + + long lastProgressNanos = System.nanoTime(); + + long cycleDeadlineNanos = lastProgressNanos + TimeUnit.MILLISECONDS.toNanos(EVICTION_MAX_CYCLE_TIME_MILLIS); + + long backoffNanos = EVICTION_BACKOFF_START_NANOS; + + while (freeList.emptyDataPages() < requiredPages) { + if (region.metrics().onPageEvictionsStarted()) { + U.warn(log, "Page-based evictions started." + + " Consider increasing 'maxSize' on Data Region configuration: " + regCfg.getName()); + } + + // tryLock=true: skip contended/self-held entries (the reserve can run while the current thread already + // holds entry locks on the single-row path), avoiding a lock-ordering deadlock. + boolean evicted = evictionTracker.evictDataPage(true); + + region.metrics().updateEvictionRate(); + + long curEmptyPages = freeList.emptyDataPages(); + + // A new high-water count or an actual eviction re-arms the no-progress timeout; a stalled iteration backs + // off rather than busy-spinning. + if (evicted || curEmptyPages > bestEmptyPages) { + if (curEmptyPages > bestEmptyPages) + bestEmptyPages = curEmptyPages; + + lastProgressNanos = System.nanoTime(); + + backoffNanos = EVICTION_BACKOFF_START_NANOS; + } + else { + LockSupport.parkNanos(backoffNanos); + + backoffNanos = Math.min(backoffNanos << 1, EVICTION_BACKOFF_MAX_NANOS); + } + + // OOM when there was no eviction/count growth for EVICTION_NO_PROGRESS_TIMEOUT_MILLIS (stuck eviction) or + // the whole cycle exceeds EVICTION_MAX_CYCLE_TIME_MILLIS (only partial progress, target never reached). + long nowNanos = System.nanoTime(); + + long maxNoProgress = TimeUnit.MILLISECONDS.toNanos(EVICTION_NO_PROGRESS_TIMEOUT_MILLIS); + + if (nowNanos - lastProgressNanos > maxNoProgress || nowNanos > cycleDeadlineNanos) + throw outOfMemory(regCfg); } } + /** + * @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..916496363036d 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; @@ -128,12 +129,32 @@ public void addRow(CacheDataRow row, IoStatisticsHolder statHolder) throws Ignit } /** + * Size-aware pre-reserve for the rebalance batch, done when persistence is off and page eviction is enabled + * (single puts get the same guarantee via the per-put reserve in {@link #addRow}). Reserving "for each row" + * collapses to reserving for the largest one: all reserves run before any insert and only enforce a lower bound + * on the shared empty-pages counter, so a single reserve for the max row is equivalent and is what is done here. + * The reserve evicts non-blockingly even though the batch path holds no entry locks (blocking would be safe here): + * the path is shared with single-row insertion, which runs under an entry lock and must not block. + * * @param rows Rows. * @param statHolder Statistics holder to track IO operations. * @throws IgniteCheckedException If failed. */ - public void addRows(Collection rows, - IoStatisticsHolder statHolder) throws IgniteCheckedException { + public void addRows(Collection rows, IoStatisticsHolder statHolder) throws IgniteCheckedException { + if (!persistenceEnabled && grp.dataRegion().config().getPageEvictionMode() != DataPageEvictionMode.DISABLED) { + int maxRowSize = 0; + + for (CacheDataRow row : rows) { + int rowSize = row.size(); + + if (rowSize > maxRowSize) + maxRowSize = rowSize; + } + + 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/FairFifoPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java index 04454d05acbac..26035641d5539 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/FairFifoPageEvictionTracker.java @@ -65,8 +65,10 @@ public FairFifoPageEvictionTracker( } /** {@inheritDoc} */ - @Override public synchronized void evictDataPage() throws IgniteCheckedException { - evictDataPage(pageUsageList.pollFirst()); + @Override public synchronized boolean evictDataPage(boolean tryLock) throws IgniteCheckedException { + Integer pageIdx = pageUsageList.pollFirst(); + + return pageIdx != null && evictDataPage(pageIdx, tryLock); } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java index 4409e35e7446b..0e85f17148bf4 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/NoOpPageEvictionTracker.java @@ -38,8 +38,8 @@ public class NoOpPageEvictionTracker implements PageEvictionTracker { } /** {@inheritDoc} */ - @Override public void evictDataPage() { - // No-op. + @Override public boolean evictDataPage(boolean tryLock) { + return false; } /** {@inheritDoc} */ 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..d97a2b95e1c64 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 @@ -32,7 +32,8 @@ import org.apache.ignite.internal.util.typedef.internal.U; /** - * + * Base for page eviction trackers sharing the data-page eviction logic + * ({@link #evictDataPage(int, boolean)}). */ public abstract class PageAbstractEvictionTracker implements PageEvictionTracker { /** This number of least significant bits is dropped from timestamp. */ @@ -89,10 +90,13 @@ public abstract class PageAbstractEvictionTracker implements PageEvictionTracker /** * @param pageIdx Page index. + * @param tryLock {@code true} to acquire entry locks non-blockingly, skipping contended or already-held entries + * (e.g. when size-aware eviction runs while the current thread already holds entry locks), avoiding a + * lock-ordering deadlock. * @return true if at least one data row has been evicted * @throws IgniteCheckedException If failed. */ - final boolean evictDataPage(int pageIdx) throws IgniteCheckedException { + final boolean evictDataPage(int pageIdx, boolean tryLock) throws IgniteCheckedException { long fakePageId = PageIdUtils.pageId(0, (byte)0, pageIdx); long page = pageMem.acquirePage(0, fakePageId); @@ -144,7 +148,7 @@ 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, tryLock); } return evictionDone; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java index 82970fb52521c..49d6760608541 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/PageEvictionTracker.java @@ -38,15 +38,32 @@ public interface PageEvictionTracker extends LifecycleAware { */ public boolean evictionRequired(); + /** + * Evicts one data page (blocking flavor, equivalent to {@link #evictDataPage(boolean) evictDataPage(false)}). + * In most cases, all entries will be removed from the page. + * Method guarantees removing at least one entry from "evicted" data page. Removing all entries may be + * not possible, as some of them can be used by active transactions. + * + * @return {@code true} if at least one data row has been evicted. + * @throws IgniteCheckedException In case of page memory error. + */ + public default boolean evictDataPage() throws IgniteCheckedException { + return evictDataPage(false); + } + /** * Evicts one data page. * In most cases, all entries will be removed from the page. * Method guarantees removing at least one entry from "evicted" data page. Removing all entries may be * not possible, as some of them can be used by active transactions. * + * @param tryLock {@code true} to acquire entry locks non-blockingly, skipping contended or already-held entries + * instead of blocking on them. Used by size-aware eviction that may run while the current thread already + * holds other entry locks, to avoid a lock-ordering deadlock. + * @return {@code true} if at least one data row has been evicted. * @throws IgniteCheckedException In case of page memory error. */ - public void evictDataPage() throws IgniteCheckedException; + public boolean evictDataPage(boolean tryLock) throws IgniteCheckedException; /** * Call this method when last entry is removed from data page. diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java index 01300ffb73fa0..ac361fcb7b10d 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/Random2LruPageEvictionTracker.java @@ -123,7 +123,7 @@ public Random2LruPageEvictionTracker( } /** {@inheritDoc} */ - @Override public void evictDataPage() throws IgniteCheckedException { + @Override public boolean evictDataPage(boolean tryLock) throws IgniteCheckedException { ThreadLocalRandom rnd = ThreadLocalRandom.current(); int evictAttemptsCnt = 0; @@ -187,17 +187,19 @@ public Random2LruPageEvictionTracker( if (sampleSpinCnt > SAMPLE_SPIN_LIMIT) { LT.warn(log, "Too many attempts to choose data page: " + SAMPLE_SPIN_LIMIT); - return; + return false; } } - if (evictDataPage(pageIdx(lruTrackingIdx))) - return; + if (evictDataPage(pageIdx(lruTrackingIdx), tryLock)) + return true; evictAttemptsCnt++; } LT.warn(log, "Too many failed attempts to evict page: " + EVICT_ATTEMPTS_LIMIT); + + return false; } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java index b940681403c15..ff94ac714b3e9 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/evict/RandomLruPageEvictionTracker.java @@ -109,7 +109,7 @@ public RandomLruPageEvictionTracker( } /** {@inheritDoc} */ - @Override public void evictDataPage() throws IgniteCheckedException { + @Override public boolean evictDataPage(boolean tryLock) throws IgniteCheckedException { ThreadLocalRandom rnd = ThreadLocalRandom.current(); int evictAttemptsCnt = 0; @@ -161,17 +161,19 @@ public RandomLruPageEvictionTracker( if (sampleSpinCnt > SAMPLE_SPIN_LIMIT) { LT.warn(log, "Too many attempts to choose data page: " + SAMPLE_SPIN_LIMIT); - return; + return false; } } - if (evictDataPage(pageIdx(lruTrackingIdx))) - return; + if (evictDataPage(pageIdx(lruTrackingIdx), tryLock)) + return true; evictAttemptsCnt++; } LT.warn(log, "Too many failed attempts to evict page: " + EVICT_ATTEMPTS_LIMIT); + + return false; } /** {@inheritDoc} */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java index 62452095631cf..f6d5df5448bbd 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/AbstractFreeList.java @@ -24,6 +24,7 @@ import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.internal.GridKernalContext; +import org.apache.ignite.internal.mem.IgniteOutOfMemoryException; import org.apache.ignite.internal.metric.IoStatisticsHolder; import org.apache.ignite.internal.metric.IoStatisticsHolderNoOp; import org.apache.ignite.internal.pagemem.PageIdAllocator; @@ -36,6 +37,7 @@ import org.apache.ignite.internal.pagemem.wal.record.delta.DataPageUpdateRecord; import org.apache.ignite.internal.processors.cache.persistence.DataRegion; import org.apache.ignite.internal.processors.cache.persistence.DataRegionMetricsImpl; +import org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager; import org.apache.ignite.internal.processors.cache.persistence.Storable; import org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.PageLockTrackerManager; import org.apache.ignite.internal.processors.cache.persistence.evict.PageEvictionTracker; @@ -74,6 +76,13 @@ public abstract class AbstractFreeList extends PagesList imp /** */ private static final int MIN_PAGE_FREE_SPACE = 8; + /** + * Bounded number of lazy size-aware re-reserve attempts on a fragmented write before falling back to + * allocating a brand-new page. Each attempt runs size-aware eviction (which itself fails with a clean OOM when + * eviction cannot progress), so this bounds the retry even under heavy contention. + */ + private static final int RE_RESERVE_ATTEMPTS = 4; + /** * Step between buckets in free list, measured in powers of two. * For example, for page size 4096 and 256 buckets, shift is 4 and step is 16 bytes. @@ -98,6 +107,12 @@ public abstract class AbstractFreeList extends PagesList imp /** */ private final PageEvictionTracker evictionTracker; + /** Data region this free list belongs to (used for lazy size-aware re-reserve on fragmented writes). */ + private final DataRegion dataRegion; + + /** Database shared manager (used for lazy size-aware re-reserve on fragmented writes). */ + private final IgniteCacheDatabaseSharedManager dbMgr; + /** Page list cache limit. */ private final AtomicLong pageListCacheLimit; @@ -462,6 +477,10 @@ public AbstractFreeList( rmvRow = new RemoveRowHandler(cacheGrpId == 0); this.evictionTracker = dataRegion.evictionTracker(); + this.dataRegion = dataRegion; + // dbMgr is needed only for the on-demand re-reserve (eviction-enabled in-memory region); null in unit tests + // without a cache processor (where eviction is disabled and the re-reserve never fires). + dbMgr = ctx.cache() == null ? null : ctx.cache().context().database(); this.reuseList = reuseList == null ? this : reuseList; int pageSize = pageMem.pageSize(); @@ -591,6 +610,44 @@ private long allocateDataPage(int part) throws IgniteCheckedException { return pageMem.allocatePage(grpId, part, FLAG_DATA); } + /** + * @return {@code true} when the region has effectively no headroom left (allocated pages reached the configured + * max), so a fresh {@code allocateDataPage} could no longer grow it. + */ + private boolean regionEffectivelyFull() { + return pageMem.loadedPages() >= dataRegion.config().getMaxSize() / pageMem.systemPageSize(); + } + + /** + * Take a page, and if the free list cannot hand one out, re-run the size-aware reserve and retry. The reserve only + * bounds the shared empty-pages counter and does not pin pages to this thread, so a concurrent writer may consume + * them before this allocation; retrying closes that TOCTOU instead of falling straight to a raw + * {@code allocateDataPage}. How hard to retry depends on whether the region can still grow: on an effectively-full + * region re-reserving is bounded (each attempt itself fails with a clean OOM when eviction cannot progress), while + * with headroom a single re-reserve suffices and the subsequent {@code allocateDataPage} grows the region. + * + * @param size Free space required on the page. + * @param row Row to write. + * @param statHolder Statistics holder to track IO operations. + * @return Page identifier or 0 if no page could be obtained after re-reserving. + * @throws IgniteCheckedException If failed. + */ + private long takePageWithReserve(int size, T row, IoStatisticsHolder statHolder) throws IgniteCheckedException { + long pageId = takePage(size, row, statHolder); + + if (pageId == 0L && dbMgr != null) { + int reReserveAttempts = regionEffectivelyFull() ? RE_RESERVE_ATTEMPTS : 1; + + for (int i = 0; pageId == 0L && i < reReserveAttempts; i++) { + dbMgr.ensureFreeSpaceForInsert(dataRegion, size); + + pageId = takePage(size, row, statHolder); + } + } + + return pageId; + } + /** {@inheritDoc} */ @Override public void insertDataRow(T row, IoStatisticsHolder statHolder) throws IgniteCheckedException { int written = 0; @@ -607,6 +664,9 @@ private long allocateDataPage(int part) throws IgniteCheckedException { catch (IgniteCheckedException | Error e) { throw e; } + catch (IgniteOutOfMemoryException e) { + throw e; + } catch (Throwable t) { throw new CorruptedFreeListException("Failed to insert data row", t, grpId); } @@ -648,7 +708,7 @@ private long allocateDataPage(int part) throws IgniteCheckedException { AbstractDataPageIO initIo = null; - long pageId = takePage(row.size() - written, row, statHolder); + long pageId = takePageWithReserve(row.size() - written, row, statHolder); if (pageId == 0L) { pageId = allocateDataPage(row.partition()); @@ -661,6 +721,9 @@ private long allocateDataPage(int part) throws IgniteCheckedException { assert written != FAIL_I; // We can't fail here. } } + catch (IgniteOutOfMemoryException e) { + throw e; + } catch (RuntimeException e) { throw new CorruptedFreeListException("Failed to insert data rows", e, grpId); } @@ -693,6 +756,15 @@ private int writeWholePages(T row, IoStatisticsHolder statHolder) throws IgniteC /** * Take a page and write row on it. + *

+ * The page is acquired via {@link #takePageWithReserve}: the size-aware reserve (RowStore.addRow/addRows) only + * bounds the shared empty-pages counter and does not pin pages to this thread, so a concurrent writer may consume + * them before this allocation — the lazy re-reserve closes that gap instead of falling straight to a raw + * {@code allocateDataPage}. Reached from the BPlusTree.invoke row-creation closure, this re-reserve is an inline + * demand-eviction that removes entries with no data-tree page locks held (the search releases the read lock before + * the closure; the leaf write lock is taken only afterwards — see BPlusTree.invokeDown). The entry-level tryLock + * only skips contended/self-held entries and never blocks; the residual risk is that the TTL expiration worker can + * still deadlock via cross-tree lock ordering (data->pending here vs pending->data there) — a known limitation. * * @param row Row to write. * @param written Written size. @@ -703,7 +775,7 @@ private int writeWholePages(T row, IoStatisticsHolder statHolder) throws IgniteC private int writeSinglePage(T row, int written, IoStatisticsHolder statHolder) throws IgniteCheckedException { AbstractDataPageIO initIo = null; - long pageId = takePage(row.size() - written, row, statHolder); + long pageId = takePageWithReserve(row.size() - written, row, statHolder); if (pageId == 0L) { pageId = allocateDataPage(row.partition()); 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..b25114b08dd2b --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionConcurrentWritesAbstractTest.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.cache.eviction.paged; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +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 = 256 * 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 (larger than the empty-pages pool so that each write is size-aware). */ + private static final int LARGE_RECORD_SIZE = 2 * 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, leaving a buffer that is exceeded by the total of the large writes, so that + * the last of them can only be stored by freeing pages via size-aware eviction. The large records are small + * enough that concurrent size-aware eviction reliably frees the required pages (no spurious guard OOM). */ + private static final int SMALL_ENTRIES = 48_000; + + /** Number of writer threads. */ + private static final int THREADS = 2; + + /** Large rows inserted per thread. Their total (threads x rows) exceeds the buffer left by the pre-fill, so the + * last large writes overflow the region and require size-aware eviction to free small entry pages. */ + private static final int LARGE_ROWS_PER_THREAD = 20; + + /** 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(); + + AtomicReference firstErr = new AtomicReference<>(); + + 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(); + + firstErr.compareAndSet(null, e); + + log.error("Unexpected error in writer thread", e); + } + }, "paged-writer-" + i); + + threads[i].start(); + } + + startLatch.countDown(); + + long start = System.currentTimeMillis(); + + 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) { + if (t.isAlive()) { + log.error("Writer thread " + t.getName() + " is still alive after " + + (System.currentTimeMillis() - start) + "ms, state=" + t.getState()); + + for (StackTraceElement frame : t.getStackTrace()) + log.error(" at " + frame); + } + } + + for (Thread t : threads) + assertFalse("Writer thread " + t.getName() + " did not finish (possible deadlock)", t.isAlive()); + + assertEquals("Writer threads reported errors, reason: " + firstErr.get(), 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..4c706d7347f2b --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionGuardOomTest.java @@ -0,0 +1,176 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.cache.eviction.paged; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.CacheAtomicityMode; +import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.DataPageEvictionMode; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE; +import static org.apache.ignite.internal.processors.cache.eviction.paged.PageEvictionSizeAwareAbstractTest.isOutOfMemory; + +/** + * Negative test for the size-aware eviction progress guard. + *

+ * When every resident entry is locked by another thread/transaction, page eviction cannot free any page: the guarded + * {@code tryLockEntry(0)} in {@code evictInternal} fails for every candidate, so {@code ensureFreeSpaceForEviction} + * makes no progress and must fail with an + * {@code IgniteOutOfMemoryException} within bounded time instead of busy-spinning forever (deadlock). + *

+ * 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) + 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(); + + try { + 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)); + } + } +} 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..6efd5b6bffbd0 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,36 @@ 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..b3ea9d0e87df8 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionSizeAwareAbstractTest.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.cache.eviction.paged; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.CacheAtomicityMode; +import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.mem.IgniteOutOfMemoryException; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE; + +/** + * Tests size-aware page eviction on in-memory (non-persistent) data regions. + *

+ * Verifies that a row larger than the configured {@code emptyPagesPoolSize} (in pages) is still written successfully + * when page eviction is enabled, by evicting old entries to free enough space. Also verifies that a row + * which fundamentally cannot fit into the region fails with OOM instead of hanging in an infinite eviction loop. + * The batch path ({@code putAll} of large rows) and the update path (growing a row) are covered as well. + */ +public abstract class PageEvictionSizeAwareAbstractTest extends GridCommonAbstractTest { + /** Off-heap region size (large enough to hold cache structural pages with the configured partition count). */ + private static final int SIZE = 128 * 1024 * 1024; + + /** Partition count (kept low so that index-tree structures do not exhaust the region). */ + private static final int PARTITIONS = 32; + + /** Record size: chosen so that a single row requires more pages than are left free when the region is kept at the + * eviction threshold ({@code (1 - threshold) * totalPages}). This guarantees a large put cannot take the fast + * path of {@code ensureFreeSpaceForInsert} and must actually run the size-aware eviction reserve + * ({@code ensureFreeSpaceForEviction}), which is the scenario these tests are meant to cover. */ + private static final int RECORD_SIZE = 32 * 1024 * 1024; + + /** Empty pages pool size. */ + private static final int POOL_SIZE = 100; + + /** Entry count to accumulate beyond the region capacity. */ + private static final int ENTRIES = 40; + + /** Small record size used to pre-fill the region with evictable data (for putAll tests). */ + private static final int SMALL_RECORD_SIZE = 4096; + + /** + * Small pre-fill entries count. Chosen to fill the 128 MiB region close to capacity so that less than one large + * record ({@link #RECORD_SIZE}) remains available, forcing {@code ensureFreeSpaceForInsert} to actually evict + * prefilled entries rather than taking its fast path. + */ + private static final int SMALL_ENTRIES = 28_000; + + /** Large rows written via putAll. */ + private static final int PUT_ALL_LARGE_ROWS = 3; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return super.getConfiguration(gridName) + .setDataStorageConfiguration(new DataStorageConfiguration() + .setDefaultDataRegionConfiguration(new DataRegionConfiguration() + .setInitialSize(SIZE) + .setMaxSize(SIZE) + .setEmptyPagesPoolSize(POOL_SIZE)) + .setPageSize(DFLT_PAGE_SIZE)); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + } + + /** + * @param ignite Ignite node. + * @return Cache with a small partition count (reduces structural page overhead). + */ + private IgniteCache 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. + *

+ * This scenario intentionally overlaps with + * {@link PageEvictionPutLargeObjectsAbstractTest#testPutLargeObjects}; the added value of this class is the + * additional coverage below (putAll, update-growth, read-back and the larger-than-region OOM case). + * + * @throws Exception If failed. + */ + @Test + public void testPutLargeObjectsDoesNotOom() throws Exception { + IgniteEx ignite = startGrids(2); + + IgniteCache 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. + * Uses a transactional cache so that the size-aware OOM propagates directly (in an atomic cache it is wrapped in + * a {@code CachePartialUpdateException} and would not be detectable as the specific OOM). + * + * @throws Exception If failed. + */ + @Test + public void testRecordLargerThanRegionOom() throws Exception { + IgniteEx ignite = startGrid(1); + + IgniteCache cache = ignite.createCache(new CacheConfiguration(DEFAULT_CACHE_NAME) + .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)) + .setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL)); + + boolean rejected = false; + + try { + cache.put(1, new byte[SIZE * 2]); + } + catch (Exception e) { + assertTrue("Expected IgniteOutOfMemoryException because the row cannot fit into the region, but got: " + e, + isOutOfMemory(e)); + + rejected = true; + } + + assertTrue("Record larger than the region must be rejected (no hang), but put succeeded", rejected); + } + + /** + * A batch putAll of several large records (each larger than the empty-pages pool) must be stored successfully when + * page eviction is enabled. Exercises the size-aware reserve in the batch store path ({@code RowStore.addRows}). + * + * @throws Exception If failed. + */ + @Test + public void testPutAllLargeRows() throws Exception { + IgniteEx ignite = startGrid(1); + + IgniteCache cache = createCache(ignite); + + // Pre-fill the region to near capacity with small evictable entries so that less than one large row remains + // available. This forces ensureFreeSpaceForInsert to evict prefilled entries rather than taking its fast path. + byte[] small = new byte[SMALL_RECORD_SIZE]; + + for (int i = 0; i < SMALL_ENTRIES; i++) + cache.put(SMALL_ENTRIES + i, small); + + Map large = new HashMap<>(); + + Object val = new byte[RECORD_SIZE]; + + for (int i = 0; i < PUT_ALL_LARGE_ROWS; i++) + large.put(i, val); + + cache.putAll(large); + + for (int i = 0; i < PUT_ALL_LARGE_ROWS; i++) + assertNotNull("Large row " + i + " must be readable after putAll", cache.get(i)); + } + + /** + * Updating a record from a small to a large value (larger than the empty-pages pool) must succeed with page + * eviction enabled: the update goes through the same size-aware reserve as an insert. The region is pre-filled + * to near capacity so that the grown value cannot fit without evicting prefilled entries. + * + * @throws Exception If failed. + */ + @Test + public void testUpdateRowGrows() throws Exception { + IgniteEx ignite = startGrid(1); + + IgniteCache cache = createCache(ignite); + + // Pre-fill the region to near capacity with small evictable entries so that the grown value below cannot + // fit without eviction. + byte[] small = new byte[SMALL_RECORD_SIZE]; + + for (int i = 0; i < SMALL_ENTRIES; i++) + cache.put(SMALL_ENTRIES + i, small); + + // Insert key 1 with a small value, then update it to a large value that requires size-aware eviction. + cache.put(1, new byte[1024]); + + byte[] big = new byte[RECORD_SIZE]; + + Arrays.fill(big, (byte)7); + + cache.put(1, big); + + byte[] read = (byte[])cache.get(1); + + assertNotNull("Updated large value must be readable", read); + + assertTrue("Updated value must equal the stored value", Arrays.equals(big, read)); + } + + /** + * @param t Throwable. + * @return {@code True} if {@code t} or any of its causes is an out-of-memory. + */ + static boolean isOutOfMemory(Throwable t) { + 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/PageEvictionWithExpiryPolicyAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java new file mode 100644 index 0000000000000..82c05e4a05e66 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionWithExpiryPolicyAbstractTest.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.cache.eviction.paged; + +import javax.cache.expiry.CreatedExpiryPolicy; +import javax.cache.expiry.Duration; +import org.apache.ignite.IgniteCache; +import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction; +import org.apache.ignite.configuration.CacheConfiguration; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.util.typedef.internal.U; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_DATA_REG_DEFAULT_NAME; +import static org.apache.ignite.configuration.DataStorageConfiguration.DFLT_PAGE_SIZE; + +/** + * Tests the synergy between ExpiryPolicy (TTL cleanup) and size-aware page eviction on an in-memory data region. + * Verifies that concurrent TTL cleanup and eviction do not deadlock, that a large row larger than the + * empty-pages pool is still written when eviction is enabled, and that TTL-freed space is accounted for by eviction + * (a row that only fits after expired entries are removed is still written without OOM). + */ +public abstract class PageEvictionWithExpiryPolicyAbstractTest extends GridCommonAbstractTest { + /** Off-heap region size. */ + private static final int SIZE = 128 * 1024 * 1024; + + /** Partition count (kept low so that index-tree structures do not exhaust the region). */ + private static final int PARTITIONS = 32; + + /** Large record size (much larger than the empty-pages pool, and larger than the space left free when the region + * is held at the eviction threshold {@code (1 - threshold) * totalPages}) so that a large put cannot take the + * fast path of {@code ensureFreeSpaceForInsert} and must actually run the size-aware eviction reserve. */ + private static final int RECORD_SIZE = 32 * 1024 * 1024; + + /** Empty pages pool size. */ + private static final int POOL_SIZE = 100; + + /** Size of a single non-expiring pre-fill record (well below a data-page payload so each record surely occupies + * exactly one data page). */ + private static final int SMALL_RECORD_SIZE = 1024; + + /** Number of non-expiring pre-fill records. Deliberately conservative: the pre-fill keeps a comfortable margin to + * the region capacity so that the large short-TTL records are guaranteed not to be evicted before they expire. */ + private static final int SMALL_ENTRIES = 6_000; + + /** Fresh large records written after TTL expiry. Together with the pre-fill ({@link #SMALL_ENTRIES} small entries, + * each occupying roughly one data page) plus the index structures, they exceed the region capacity (~32768 pages + * for a 128 MiB region with 4 KiB pages), forcing size-aware eviction that accounts for the TTL-freed space. */ + private static final int FRESH_RECORDS = 12; + + /** Short TTL applied to some entries. */ + private static final long TTL = 8000; + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { + return super.getConfiguration(gridName) + .setDataStorageConfiguration(new DataStorageConfiguration() + .setDefaultDataRegionConfiguration(new DataRegionConfiguration() + .setInitialSize(SIZE) + .setMaxSize(SIZE) + .setEmptyPagesPoolSize(POOL_SIZE)) + .setPageSize(DFLT_PAGE_SIZE)); + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + } + + /** + * @param ignite Ignite node. + * @param cacheName Cache name. + * @param ttl TTL in milliseconds ({@code 0} for no expiry). + * @return Cache with a small partition count and, if {@code ttl > 0}, eager TTL expiry. + */ + private IgniteCache createCache(IgniteEx ignite, String cacheName, long ttl) { + CacheConfiguration ccfg = new CacheConfiguration(cacheName) + .setAffinity(new RendezvousAffinityFunction(false, PARTITIONS)); + + if (ttl > 0) { + ccfg.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(MILLISECONDS, ttl))) + .setEagerTtl(true); + } + + return ignite.createCache(ccfg); + } + + /** + * Concurrent TTL cleanup and eviction must not deadlock, and a large record (larger than the empty-pages pool) + * must still be stored on a region with enabled eviction even in the presence of short-TTL entries. + * + * @throws Exception If failed. + */ + @Test + public void testLargePutWithExpiryNoDeadlock() throws Exception { + IgniteEx ignite = startGrid(1); + + // Short-TTL entries keep the TTL worker actively freeing pages while eviction runs. + IgniteCache cache = createCache(ignite, DEFAULT_CACHE_NAME, TTL); + + Object val = new byte[RECORD_SIZE]; + + // Writing more data than the region can hold forces eviction; concurrent expiry of short-TTL entries must not + // deadlock with it. The test itself is protected against a hang by the framework test timeout. + for (int i = 0; i < 30; i++) + cache.put(i, val); + + // The most recently written entry cannot have expired yet (TTL is far larger than this read), so a non-null + // read both verifies the cache is responsive after concurrent expiry/eviction (the test's goal) and is not + // racy. Reading the first written key would be racy (it may already have expired under the TTL). + assertNotNull("Cache must remain responsive after concurrent expiry and eviction", cache.get(29)); + } + + /** + * Space freed by TTL cleanup must be taken into account by size-aware eviction: large records written after some + * entries have expired must be accepted (no OOM) because their pages become available. + *

+ * The region is pre-filled with non-expiring small entries and large short-TTL entries that later expire and free + * their pages. After expiry, fresh large records are written — totalling more than the space freed by TTL, so that + * the pre-fill plus the fresh records exceed the region size. The writes can only succeed because size-aware + * eviction accounts for the TTL-freed pages (as available) and frees further pages for the rest. + * + * @throws Exception If failed. + */ + @Test + public void testTtlFreedSpaceAccountedForByEviction() throws Exception { + IgniteEx ignite = startGrid(1); + + // Non-expiring cache for prefill and the final large put. + IgniteCache plainCache = createCache(ignite, "plain-cache", 0); + + // Short-TTL cache for entries that will expire and free pages. + IgniteCache ttlCache = createCache(ignite, "ttl-cache", TTL); + + // Pre-fill the region with small non-expiring entries, but leave enough room for the large TTL entries to be + // written without evicting them (so they are guaranteed to be present until they expire). + byte[] small = new byte[SMALL_RECORD_SIZE]; + + for (int i = 0; i < SMALL_ENTRIES; i++) + plainCache.put(i, small); + + info("Pre-fill done [entries=" + SMALL_ENTRIES + ", plainSize=" + plainCache.size() + + ", loadedPages=" + ignite.dataRegionMetrics(DFLT_DATA_REG_DEFAULT_NAME).getTotalAllocatedPages() + ']'); + + // Add large short-TTL entries that occupy significant space and will expire. They fit in the remaining free + // space, and being the freshest entries they are not evicted while they are being stored. + Object val = new byte[RECORD_SIZE]; + + for (int i = 0; i < 2; i++) + ttlCache.put(i, val); + + // Verify the TTL entries are present before expiry. + assertNotNull("TTL entry must be present before expiry", ttlCache.get(0)); + + // Wait for the TTL worker to expire and free the short-TTL entries. Polled instead of a fixed sleep so that a + // slow CI machine does not proceed before the entries have actually expired. + long expiryDeadline = System.currentTimeMillis() + 30_000; + + while (ttlCache.get(0) != null && System.currentTimeMillis() < expiryDeadline) + U.sleep(200); + + // Verify the TTL entries have expired. + assertNull("TTL entry must be expired", ttlCache.get(0)); + + // Now write large records to the non-expiring cache, totalling more than the space freed by TTL (the prefill + // plus the fresh large records exceed the region size). The put can only succeed because size-aware eviction + // accounts for the pages freed by TTL (as available) and frees further pages for the rest; this verifies the + // synergy between TTL cleanup and size-aware eviction — a row that only fits once expired entries are removed + // is written without OOM. + for (int i = 0; i < FRESH_RECORDS; i++) + plainCache.put(100 + i, val); + + // The most recently written large record is still present: it was accepted without OOM thanks to the space + // freed by TTL and the additional pages freed by size-aware eviction. (Earlier large records may already have + // been evicted to make room for the subsequent ones, so only the freshest is asserted.) + assertNotNull("Fresh large record must be present after TTL-assisted eviction", + plainCache.get(100 + FRESH_RECORDS - 1)); + } +} 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..347c724007c3f --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionConcurrentWritesTest.java @@ -0,0 +1,31 @@ +/* + * 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..8feebf712572a --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionSizeAwareTest.java @@ -0,0 +1,31 @@ +/* + * 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..128549bfa67cb --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/Random2LruPageEvictionWithExpiryPolicyTest.java @@ -0,0 +1,31 @@ +/* + * 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..8b2fd795e8485 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionConcurrentWritesTest.java @@ -0,0 +1,31 @@ +/* + * 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..eac6af507b879 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionSizeAwareTest.java @@ -0,0 +1,31 @@ +/* + * 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..0709c75eafee3 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/RandomLruPageEvictionWithExpiryPolicyTest.java @@ -0,0 +1,31 @@ +/* + * 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);