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..6ec06422a8a79
--- /dev/null
+++ b/modules/benchmarks/src/main/java/org/apache/ignite/internal/benchmarks/jmh/cache/JmhPageEvictionBenchmark.java
@@ -0,0 +1,224 @@
+/*
+ * 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.Map;
+import java.util.TreeMap;
+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.cache.CacheAtomicityMode;
+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.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+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 overhead of page eviction on the write paths of an in-memory data region with enabled page eviction.
+ *
+ *
{@link #putBelowThreshold} — puts within a bounded key range that keeps the region below the eviction
+ * threshold: the admission fast path, no eviction.
+ *
{@link #putAtThreshold}, {@link #putAllAtThreshold}, {@link #txPutAtThreshold} — new rows inserted into a
+ * region pre-filled up to the eviction threshold: steady-state eviction on the atomic single put, atomic batch and
+ * transactional commit paths.
+ *
+ * The benchmark uses the public API only, so the same class can be run on another branch (e.g. {@code master}) with the
+ * same parameters to compare the results.
+ */
+@Fork(1)
+@Threads(4)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@Warmup(iterations = 3, time = 5)
+@Measurement(iterations = 5, time = 10)
+public class JmhPageEvictionBenchmark {
+ /** Atomic cache name. */
+ private static final String ATOMIC_CACHE = "atomic";
+
+ /** Transactional cache name. */
+ private static final String TX_CACHE = "tx";
+
+ /** Value size (bytes): several rows per data page. */
+ private static final int VALUE_SIZE = 1024;
+
+ /** Data region size (bytes). */
+ private static final long REGION_SIZE = 256 * 1024L * 1024L;
+
+ /** Bounded key range of {@link #putBelowThreshold}: about 15% of the region, far below the eviction threshold. */
+ private static final int BELOW_THRESHOLD_KEYS = 32_000;
+
+ /** Pre-filled entries of the at-threshold scenarios: more than the region holds, so eviction has already started. */
+ private static final int PRE_FILL_ENTRIES = 400_000;
+
+ /** Batch size of {@link #putAllAtThreshold}. */
+ private static final int BATCH_SIZE = 100;
+
+ /** Value (reused to avoid allocation noise in the hot path). */
+ private static final byte[] VAL = new byte[VALUE_SIZE];
+
+ /** Node with an empty data region. */
+ @State(Scope.Benchmark)
+ public static class BelowThreshold {
+ /** Atomic cache. */
+ private IgniteCache cache;
+
+ /** Key source. */
+ private final AtomicInteger keyGen = new AtomicInteger();
+
+ /** */
+ @Setup
+ public void setup() {
+ cache = startNode().cache(ATOMIC_CACHE);
+ }
+
+ /** */
+ @TearDown
+ public void tearDown() {
+ Ignition.stopAll(true);
+ }
+ }
+
+ /** Node with a data region pre-filled up to the eviction threshold. */
+ @State(Scope.Benchmark)
+ public static class AtThreshold {
+ /** Atomic cache. */
+ private IgniteCache atomicCache;
+
+ /** Transactional cache. */
+ private IgniteCache txCache;
+
+ /** Key source: every key is new, so every put inserts a new row. */
+ private final AtomicInteger keyGen = new AtomicInteger(PRE_FILL_ENTRIES);
+
+ /** */
+ @Setup
+ public void setup() {
+ Ignite ignite = startNode();
+
+ atomicCache = ignite.cache(ATOMIC_CACHE);
+ txCache = ignite.cache(TX_CACHE);
+
+ try (IgniteDataStreamer ldr = ignite.dataStreamer(ATOMIC_CACHE)) {
+ for (int i = 0; i < PRE_FILL_ENTRIES; i++)
+ ldr.addData(i, VAL);
+ }
+ }
+
+ /** */
+ @TearDown
+ public void tearDown() {
+ Ignition.stopAll(true);
+ }
+ }
+
+ /**
+ * Atomic put below the eviction threshold (updates of a bounded key range).
+ *
+ * @param state State.
+ */
+ @Benchmark
+ public void putBelowThreshold(BelowThreshold state) {
+ state.cache.put(state.keyGen.incrementAndGet() % BELOW_THRESHOLD_KEYS, VAL);
+ }
+
+ /**
+ * Atomic put of a new row into a region at the eviction threshold.
+ *
+ * @param state State.
+ */
+ @Benchmark
+ public void putAtThreshold(AtThreshold state) {
+ state.atomicCache.put(state.keyGen.incrementAndGet(), VAL);
+ }
+
+ /**
+ * Atomic putAll of new rows into a region at the eviction threshold.
+ *
+ * @param state State.
+ */
+ @Benchmark
+ public void putAllAtThreshold(AtThreshold state) {
+ int base = state.keyGen.getAndAdd(BATCH_SIZE);
+
+ Map batch = new TreeMap<>();
+
+ for (int i = 0; i < BATCH_SIZE; i++)
+ batch.put(base + i, VAL);
+
+ state.atomicCache.putAll(batch);
+ }
+
+ /**
+ * Transactional put (implicit transaction) of a new row into a region at the eviction threshold.
+ *
+ * @param state State.
+ */
+ @Benchmark
+ public void txPutAtThreshold(AtThreshold state) {
+ state.txCache.put(state.keyGen.incrementAndGet(), VAL);
+ }
+
+ /** @return Started node with an atomic and a transactional cache in an in-memory region with page eviction. */
+ private static Ignite startNode() {
+ DataStorageConfiguration dsCfg = new DataStorageConfiguration()
+ .setDefaultDataRegionConfiguration(new DataRegionConfiguration()
+ .setPersistenceEnabled(false)
+ .setInitialSize(REGION_SIZE)
+ .setMaxSize(REGION_SIZE)
+ .setPageEvictionMode(DataPageEvictionMode.RANDOM_LRU));
+
+ IgniteConfiguration cfg = new IgniteConfiguration()
+ .setIgniteInstanceName("page-eviction-benchmark")
+ .setLocalHost("127.0.0.1")
+ .setDataStorageConfiguration(dsCfg)
+ .setCacheConfiguration(
+ new CacheConfiguration(ATOMIC_CACHE),
+ new CacheConfiguration(TX_CACHE).setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL));
+
+ return Ignition.start(cfg);
+ }
+
+ /**
+ * Runs benchmark.
+ *
+ * @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/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java
index a0bac1118da80..75d7733309ea1 100644
--- a/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java
+++ b/modules/binary/impl/src/main/java/org/apache/ignite/internal/binary/BinaryObjectImpl.java
@@ -218,6 +218,8 @@ public BinaryObjectImpl(BinaryContext ctx, byte[] arr, int start) {
/** {@inheritDoc} */
@Override public int valueBytesLength(CacheObjectValueContext ctx) throws IgniteCheckedException {
+ marshal(ctx);
+
return CacheObjectUtils.objectPutSize(valBytes.length);
}
diff --git a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
index 611195665f10b..33bd6b8f8d8cb 100644
--- a/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
+++ b/modules/core/src/main/java/org/apache/ignite/IgniteSystemProperties.java
@@ -88,6 +88,7 @@
import static org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.PageLockTrackerFactory.DFLT_PAGE_LOCK_TRACKER_CAPACITY;
import static org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.PageLockTrackerFactory.HEAP_LOG;
import static org.apache.ignite.internal.processors.cache.persistence.diagnostic.pagelocktracker.SharedPageLockTracker.DFLT_PAGE_LOCK_TRACKER_CHECK_INTERVAL;
+import static org.apache.ignite.internal.processors.cache.persistence.freelist.AbstractFreeList.DFLT_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS;
import static org.apache.ignite.internal.processors.cache.persistence.pagemem.FullPageIdTable.DFLT_LONG_LONG_HASH_MAP_LOAD_FACTOR;
import static org.apache.ignite.internal.processors.cache.persistence.pagemem.PageMemoryImpl.DFLT_DELAYED_REPLACED_PAGE_WRITE;
import static org.apache.ignite.internal.processors.cache.persistence.pagemem.PageMemoryImpl.DFLT_LOADED_PAGES_BACKWARD_SHIFT_MAP;
@@ -1638,6 +1639,24 @@ public final class IgniteSystemProperties extends IgniteCommonsSystemProperties
type = Integer.class, defaults = "" + DFLT_PAGE_LOCK_TRACKER_CHECK_INTERVAL)
public static final String IGNITE_PAGE_LOCK_TRACKER_CHECK_INTERVAL = "IGNITE_PAGE_LOCK_TRACKER_CHECK_INTERVAL";
+ /**
+ * Maximum time in milliseconds the size-aware eviction guard is willing to wait without a deficit decrease. This is
+ * a liveness guard against a true stall (all evictable entries locked); a progressing eviction that is simply slow
+ * (e.g. a large putAll on a nearly-full region) is not interrupted by this timeout. When it expires, an insert that
+ * can be refused (e.g. a primary atomic update) fails with an out-of-memory, while an insert that can not be safely
+ * refused (a backup update, a transaction commit, rebalancing) proceeds without a page reservation. After that,
+ * until the eviction makes progress again, such inserts proceed without waiting for the timeout.
+ *
+ * {@code 0} or a negative value disables the grace period: the guard gives up on the first no-progress iteration.
+ *
+ * Default is {@code 5000} (5 seconds).
+ */
+ @SystemProperty(value = "Maximum time in milliseconds the size-aware eviction guard waits without progress. " +
+ "Then an insert that can be refused fails with out-of-memory, other inserts proceed without a page " +
+ "reservation. 0 or a negative value disables the grace period", type = Long.class,
+ defaults = "" + DFLT_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS)
+ public static final String IGNITE_PAGE_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS = "IGNITE_PAGE_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS";
+
/**
* Enables threads locks dumping on critical node failure.
*
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..0c0cc6485f7a5 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;
@@ -323,6 +322,9 @@ public DataRegionConfiguration setPageReplacementMode(PageReplacementMode replac
/**
* Gets a threshold for memory pages eviction initiation. For instance, if the threshold is 0.9 it means that the
* page memory will start the eviction only after 90% of the data region is occupied.
+ * The threshold applies to every insert into the region (primary and backup updates, transactions, rebalancing,
+ * data streaming): the eviction keeps the empty pages pool plus the space of the rows being inserted free once
+ * the threshold is reached. A row that does not fit below the threshold may use the space above it.
*
* @return Memory pages eviction threshold.
*/
@@ -346,9 +348,10 @@ 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 row plus this pool exceeds the available
+ * free pages, it is no longer required to increase this parameter up to the size of the largest cache entry.
+ * In addition, size-aware eviction keeps this many empty pages free as a single region-wide margin on top of the
+ * pages reserved by in-flight inserts (the margin is not multiplied by the number of concurrent writers).
*
* @return Minimum number of empty pages in reuse list.
*/
@@ -360,9 +363,10 @@ 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 row plus this pool exceeds the available
+ * free pages, it is no longer required to increase this parameter up to the size of the largest cache entry.
+ * In addition, size-aware eviction keeps this many empty pages free as a single region-wide margin on top of the
+ * pages reserved by in-flight inserts (the margin is not multiplied by the number of concurrent writers).
*
* @param emptyPagesPoolSize Empty pages pool size.
* @return {@code this} for chaining.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/pagemem/impl/PageMemoryNoStoreImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/pagemem/impl/PageMemoryNoStoreImpl.java
index bd5b48f8d8d8a..d9bd2b8bacaf0 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/pagemem/impl/PageMemoryNoStoreImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/pagemem/impl/PageMemoryNoStoreImpl.java
@@ -109,7 +109,7 @@ public class PageMemoryNoStoreImpl implements PageMemory {
private static final int SEG_BITS = 4;
/** Number of bits required to store segment index. */
- private static final int SEG_CNT = (1 << SEG_BITS);
+ public static final int SEG_CNT = (1 << SEG_BITS);
/** Number of bits left to store page index. */
private static final int IDX_BITS = PageIdUtils.PAGE_IDX_SIZE - SEG_BITS;
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 816a66f250572..0067bbbccb5da 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
@@ -55,9 +55,10 @@
import org.apache.ignite.internal.processors.cache.extras.GridCacheTtlEntryExtras;
import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow;
import org.apache.ignite.internal.processors.cache.persistence.CacheDataRowAdapter;
-import org.apache.ignite.internal.processors.cache.persistence.DataRegion;
+import org.apache.ignite.internal.processors.cache.persistence.IgniteCacheDatabaseSharedManager;
import org.apache.ignite.internal.processors.cache.persistence.StorageException;
import org.apache.ignite.internal.processors.cache.persistence.snapshot.dump.DumpEntryChangeListener;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.CacheVersionIO;
import org.apache.ignite.internal.processors.cache.persistence.wal.WALPointer;
import org.apache.ignite.internal.processors.cache.query.continuous.CacheContinuousQueryListener;
import org.apache.ignite.internal.processors.cache.transactions.IgniteInternalTx;
@@ -885,8 +886,10 @@ private EntryGetResult entryGetResult(CacheObject val, GridCacheVersion ver, boo
boolean touch = false;
+ long reserved;
+
try {
- ensureFreeSpace();
+ reserved = reserveEntrySpace(ret, false);
lockEntry();
@@ -932,6 +935,8 @@ private EntryGetResult entryGetResult(CacheObject val, GridCacheVersion ver, boo
}
finally {
unlockEntry();
+
+ releaseReserved(reserved);
}
touch = true;
@@ -988,7 +993,8 @@ protected void recordNodeId(UUID nodeId, AffinityTopologyVersion topVer) {
long updateCntr0;
- ensureFreeSpace();
+ // Best-effort: a refused commit is not safe, other entries of the transaction may already be committed.
+ long reserved = reserveEntrySpace(val, true);
lockListenerReadLock();
lockEntry();
@@ -1141,6 +1147,8 @@ else if (interceptorVal != val0)
finally {
unlockEntry();
unlockListenerReadLock();
+
+ releaseReserved(reserved);
}
onUpdateFinished(updateCntr0);
@@ -1445,8 +1453,11 @@ else if (log.isDebugEnabled())
AtomicCacheUpdateClosure c;
- if (!primary && !isNear())
- ensureFreeSpace();
+ long reserved = 0;
+
+ if (!primary && !isNear() && op == GridCacheOperation.UPDATE)
+ // Best-effort: a refused backup update is not safe, the primary has already applied it.
+ reserved = reserveEntrySpace((CacheObject)writeObj, true);
lockListenerReadLock();
lockEntry();
@@ -1706,6 +1717,8 @@ else if (updateMetrics && REMOVE_NO_VAL.equals(updateRes.outcome())
finally {
unlockEntry();
unlockListenerReadLock();
+
+ releaseReserved(reserved);
}
onUpdateFinished(c.updateRes.updateCounter());
@@ -2439,7 +2452,8 @@ private boolean skipInterceptor(@Nullable GridCacheVersion explicitVer) {
) throws IgniteCheckedException, GridCacheEntryRemovedException {
assert !primary || !(preload || fromStore);
- ensureFreeSpace();
+ // A pre-created row (rebalancing) is already inserted into the free list, so there is nothing to reserve.
+ long reserved = row == null ? reserveEntrySpace(val, false) : 0;
boolean deferred = false;
boolean obsolete = false;
@@ -2581,6 +2595,8 @@ else if (deletedUnlocked())
unlockEntry();
unlockListenerReadLock();
+ releaseReserved(reserved);
+
// It is necessary to execute these callbacks outside of lock to avoid deadlocks.
if (obsolete) {
@@ -2677,6 +2693,9 @@ protected long nextPartitionCounter(IgniteInternalTx tx, @Nullable Long updateCn
@Nullable IgniteCacheExpiryPolicy loadExpiryPlc,
@Nullable ReaderArguments readerArgs
) throws IgniteCheckedException, GridCacheEntryRemovedException {
+ // The atomic cache reloads values while holding the entry locks, a reservation is not possible then.
+ long reserved = lockedByCurrentThread() ? 0 : reserveEntrySpace(val, false);
+
lockEntry();
try {
@@ -2731,6 +2750,8 @@ protected long nextPartitionCounter(IgniteInternalTx tx, @Nullable Long updateCn
}
finally {
unlockEntry();
+
+ releaseReserved(reserved);
}
}
@@ -3557,13 +3578,51 @@ protected void removeValue() throws IgniteCheckedException {
}
/**
- * Evicts necessary number of data pages if per-page eviction is configured in current {@link DataRegion}.
+ * Reserves pages for the row about to be inserted. Must be called before {@link #lockEntry()} — eviction may
+ * block on other entries' locks.
+ *
+ * @param val Value of the row, or {@code null} for a removal: no row is inserted, so nothing is reserved.
+ * @param bestEffort {@code True} if the insert can not be safely refused (see {@code FreeList#reserve}).
+ * @return Number of reserved pages, or {@code 0} if nothing is reserved.
+ * @throws IgniteCheckedException If failed.
*/
- private void ensureFreeSpace() throws IgniteCheckedException {
- // Deadlock alert: evicting data page causes removing (and locking) all entries on the page one by one.
+ private long reserveEntrySpace(@Nullable CacheObject val, boolean bestEffort) throws IgniteCheckedException {
+ // Deadlock alert: evicting a data page locks all entries on it.
assert !lock.isHeldByCurrentThread();
- cctx.shared().database().ensureFreeSpace(cctx.dataRegion());
+ IgniteCacheDatabaseSharedManager db = cctx.shared().database();
+
+ // Near entries keep values on heap (see GridNearCacheEntry#storeValue), so they are not reserved for.
+ // The row size is computed only if the reservation is engaged: it may require marshalling the value.
+ if (val == null || isNear() || !db.isReserveEnabled(cctx.dataRegion()))
+ return 0;
+
+ return db.reserveInRegion(cctx.dataRegion(), rowSize(val), bestEffort);
+ }
+
+ /**
+ * Releases a reservation made by {@link #reserveEntrySpace(CacheObject, boolean)}. The whole claim is
+ * released as a unit: the insert does not consume the reservation page by page, so the release is exact on any
+ * outcome (including an exception in the middle of the row insert).
+ *
+ * @param reserved Number of reserved pages.
+ */
+ private void releaseReserved(long reserved) {
+ cctx.shared().database().releaseInRegion(cctx.dataRegion(), reserved);
+ }
+
+ /**
+ * Computes the data row size for the given value. Used to reserve pages before the entry lock is acquired, when the
+ * version of the row may be not known yet: {@link CacheVersionIO#SIZE_V2} is the maximum of both version protocols,
+ * so the result is an upper bound.
+ *
+ * @param val Value.
+ * @return Estimated row size in bytes.
+ * @throws IgniteCheckedException If failed.
+ */
+ private int rowSize(CacheObject val) throws IgniteCheckedException {
+ return CacheDataRowAdapter.rowSize(key, val, CacheVersionIO.SIZE_V2, cctx.cacheObjectContext(),
+ cctx.group().storeCacheIdInDataPage());
}
/**
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java
index ec2e09483c0d1..d4980f7863123 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtCacheAdapter.java
@@ -886,14 +886,6 @@ else if (storeEnabled)
while (true) {
GridCacheEntryEx entry = null;
- try {
- ctx.shared().database().ensureFreeSpace(ctx.dataRegion());
- }
- catch (IgniteCheckedException e) {
- // Wrap errors (will be unwrapped).
- throw new GridClosureException(e);
- }
-
ctx.shared().database().checkpointReadLock();
try {
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java
index 496f579a33fcf..2e65f36022b8a 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicCache.java
@@ -55,6 +55,7 @@
import org.apache.ignite.internal.processors.cache.CacheInvokeResult;
import org.apache.ignite.internal.processors.cache.CacheLazyEntry;
import org.apache.ignite.internal.processors.cache.CacheObject;
+import org.apache.ignite.internal.processors.cache.CacheObjectValueContext;
import org.apache.ignite.internal.processors.cache.CacheOperationContext;
import org.apache.ignite.internal.processors.cache.CacheStoppedException;
import org.apache.ignite.internal.processors.cache.CacheStorePartialUpdateException;
@@ -90,7 +91,9 @@
import org.apache.ignite.internal.processors.cache.dr.GridCacheDrExpirationInfo;
import org.apache.ignite.internal.processors.cache.dr.GridCacheDrInfo;
import org.apache.ignite.internal.processors.cache.persistence.CacheDataRow;
+import org.apache.ignite.internal.processors.cache.persistence.DataRegion;
import org.apache.ignite.internal.processors.cache.persistence.StorageException;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.CacheVersionIO;
import org.apache.ignite.internal.processors.cache.transactions.IgniteTxLocalEx;
import org.apache.ignite.internal.processors.cache.version.GridCacheVersion;
import org.apache.ignite.internal.processors.cache.version.GridCacheVersionConflictContext;
@@ -135,6 +138,7 @@
import static org.apache.ignite.internal.processors.cache.GridCacheOperation.TRANSFORM;
import static org.apache.ignite.internal.processors.cache.GridCacheOperation.UPDATE;
import static org.apache.ignite.internal.processors.cache.GridCacheUtils.isNearEnabled;
+import static org.apache.ignite.internal.processors.cache.persistence.CacheDataRowAdapter.rowSize;
import static org.apache.ignite.internal.processors.dr.GridDrType.DR_BACKUP;
import static org.apache.ignite.internal.processors.dr.GridDrType.DR_NONE;
import static org.apache.ignite.internal.processors.dr.GridDrType.DR_PRIMARY;
@@ -1806,8 +1810,10 @@ private void updateAllAsyncInternal0(
ctx.shared().database().checkpointReadLock();
+ long batchReserved = 0;
+
try {
- ctx.shared().database().ensureFreeSpace(ctx.dataRegion());
+ batchReserved = reserveBatchSpace(req);
// If batch store update is enabled, we need to lock all entries.
// First, need to acquire locks on cache entries, then check filter.
@@ -1949,6 +1955,8 @@ private void updateAllAsyncInternal0(
return;
}
finally {
+ ctx.shared().database().releaseInRegion(ctx.dataRegion(), batchReserved);
+
ctx.shared().database().checkpointReadUnlock();
}
@@ -2978,6 +2986,53 @@ else if (GridDhtCacheEntry.ReaderId.contains(readers, nearNode.id())) {
}
}
+ /**
+ * Reserves free-list pages for the total size of all rows in the batch: size-aware admission control for
+ * the primary atomic path, which acquires entry locks via {@link #lockEntries} before {@code innerUpdate} and so
+ * cannot reserve (or evict) per entry there. Reserving before {@link #lockEntries} makes room for the data pages
+ * of all rows of a batch with known value sizes, so their inserts are not expected to hit an out-of-memory in
+ * {@code FreeList.insertDataRow} (pages outside the reservation, e.g. B+tree pages, are covered by the empty pages
+ * pool).
+ *
+ * The full row size is reserved for updates too: an update inserts the new row before removing the old one, unless
+ * it is done in place (same row size, no SQL indexes, unchanged expire time), which can't be known before the entry
+ * lock. For in-place updates near the eviction threshold this keeps extra empty pages free (up to the size of the
+ * rows being written concurrently) instead of using them for data; it is not a correctness issue.
+ *
+ * Nothing is reserved for a remove: it inserts no rows. For a transform the result is unknown before the entry
+ * processor runs under the entry lock, so only a row without a value (key, version and row header) is reserved.
+ *
+ * @param req Update request.
+ * @return Reserved pages to release via {@code releaseInRegion}, or {@code 0} if reservation is not engaged.
+ * @throws IgniteCheckedException If failed.
+ */
+ private long reserveBatchSpace(GridNearAtomicAbstractUpdateRequest req) throws IgniteCheckedException {
+ DataRegion dataRegion = ctx.dataRegion();
+
+ // Skip computing the row sizes (it may require marshalling) if the reservation is not engaged for the region.
+ if (req.operation() == GridCacheOperation.DELETE || !ctx.shared().database().isReserveEnabled(dataRegion))
+ return 0;
+
+ CacheObjectValueContext coctx = ctx.cacheObjectContext();
+
+ boolean storeCacheId = ctx.group().storeCacheIdInDataPage();
+
+ long sumRowSize = 0;
+
+ for (int i = 0; i < req.size(); i++) {
+ KeyCacheObject key = req.key(i);
+
+ // The value of a transform is unknown before the entry processor runs, a row without a value is reserved.
+ CacheObject val = req.operation() == GridCacheOperation.UPDATE ? req.value(i) : null;
+
+ // CacheVersionIO.SIZE_V2 is the maximum of both version protocols (V1 is smaller), so this is an upper
+ // bound: the actual rows can not be larger than estimated.
+ sumRowSize += rowSize(key, val, CacheVersionIO.SIZE_V2, coctx, storeCacheId);
+ }
+
+ return ctx.shared().database().reserveInRegion(dataRegion, sumRowSize, false);
+ }
+
/**
* Acquires java-level locks on cache entries. Returns collection of locked entries.
*
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/CacheDataRowAdapter.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/CacheDataRowAdapter.java
index 66059d557c9cc..960d5b1820f8f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/CacheDataRowAdapter.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/CacheDataRowAdapter.java
@@ -28,6 +28,7 @@
import org.apache.ignite.internal.processors.cache.CacheGroupContext;
import org.apache.ignite.internal.processors.cache.CacheObject;
import org.apache.ignite.internal.processors.cache.CacheObjectContext;
+import org.apache.ignite.internal.processors.cache.CacheObjectValueContext;
import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
import org.apache.ignite.internal.processors.cache.IncompleteCacheObject;
import org.apache.ignite.internal.processors.cache.IncompleteObject;
@@ -957,11 +958,32 @@ public boolean isReady() {
/** {@inheritDoc} */
@Override public int size() throws IgniteCheckedException {
- int len = key().valueBytesLength(null);
+ return rowSize(key(), value(), CacheVersionIO.size(version(), false), null, storeCacheId());
+ }
+
+ /**
+ * Computes the size a row with the given key and value occupies in page memory.
+ *
+ * @param key Row key.
+ * @param val Row value ({@code null} if unknown — under-reserves safely).
+ * @param verSize Serialized version size (see {@link CacheVersionIO#size(GridCacheVersion, boolean)}).
+ * @param coctx Cache object context to compute key/value bytes lengths.
+ * @param storeCacheId {@code True} if cache ID stored in the data page.
+ * @return Row size in bytes.
+ * @throws IgniteCheckedException If failed to compute key or value bytes length.
+ */
+ public static int rowSize(
+ KeyCacheObject key,
+ @Nullable CacheObject val,
+ int verSize,
+ CacheObjectValueContext coctx,
+ boolean storeCacheId
+ ) throws IgniteCheckedException {
+ int len = key.valueBytesLength(coctx);
- len += value().valueBytesLength(null) + CacheVersionIO.size(version(), false) + 8;
+ len += (val != null ? val.valueBytesLength(coctx) : 0) + verSize + 8;
- return len + (storeCacheId() ? 4 : 0);
+ return len + (storeCacheId ? 4 : 0);
}
/**
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 e39607e65f29b..89ec2f352fae1 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
@@ -1223,23 +1223,50 @@ public void ensureFreeSpaceForInsert(DataRegion region, int dataRowSize) throws
}
/**
- * See {@code GridCacheMapEntry#ensureFreeSpace()}
+ * Checks whether the size-aware page reservation is engaged for the given data region: only in-memory regions with
+ * enabled page eviction. Callers use it to skip computing the row size when no reservation will be made.
*
- * @param memPlc data region.
+ * @param memPlc Data region.
+ * @return {@code True} if {@link #reserveInRegion(DataRegion, long, boolean)} may reserve pages in the region.
*/
- public void ensureFreeSpace(DataRegion memPlc) throws IgniteCheckedException {
- if (memPlc == null)
- return;
+ public boolean isReserveEnabled(@Nullable DataRegion memPlc) {
+ return memPlc != null && !memPlc.config().isPersistenceEnabled() &&
+ memPlc.config().getPageEvictionMode() != DataPageEvictionMode.DISABLED;
+ }
- while (memPlc.evictionTracker().evictionRequired()) {
- if (memPlc.metrics().onPageEvictionsStarted()) {
- U.warn(log, "Page-based evictions started." +
- " Consider increasing 'maxSize' on Data Region configuration: " + memPlc.config().getName());
- }
+ /**
+ * Reserves free-list pages in the given data region for a row (or a batch of rows totaling {@code rowSize} bytes)
+ * about to be inserted.
+ *
+ * @param memPlc Data region.
+ * @param rowSize Size of the row, or the total size of all rows of a batch.
+ * @param bestEffort {@code True} if the insert can not be safely refused (see {@link FreeList#reserve(long, boolean)}).
+ * @return Number of reserved pages to be released via {@link #releaseInRegion(DataRegion, long)}, or {@code 0} if
+ * nothing is reserved: the reservation is not engaged for this region (see {@link #isReserveEnabled(DataRegion)})
+ * or a best-effort insert is admitted without a reservation.
+ * @throws IgniteCheckedException If failed.
+ */
+ public long reserveInRegion(@Nullable DataRegion memPlc, long rowSize, boolean bestEffort) throws IgniteCheckedException {
+ if (!isReserveEnabled(memPlc))
+ return 0;
- memPlc.evictionTracker().evictDataPage();
+ FreeList> freeList = freeList(memPlc.config().getName());
+
+ return freeList == null ? 0 : freeList.reserve(rowSize, bestEffort);
+ }
+
+ /**
+ * Releases a reservation made by {@link #reserveInRegion(DataRegion, long, boolean)}.
+ *
+ * @param memPlc Data region the reservation was made in.
+ * @param pages Number of pages to release.
+ */
+ public void releaseInRegion(@Nullable DataRegion memPlc, long pages) {
+ if (memPlc != null && pages != 0) {
+ FreeList> freeList = freeList(memPlc.config().getName());
- memPlc.metrics().updateEvictionRate();
+ if (freeList != null)
+ freeList.releaseReserved(pages);
}
}
@@ -1321,7 +1348,7 @@ protected PageEvictionTracker createPageEvictionTracker(DataRegionConfiguration
PageMemoryNoStoreImpl pageMem0 = (PageMemoryNoStoreImpl)pageMem;
if (Boolean.getBoolean("override.fair.fifo.page.eviction.tracker"))
- return new FairFifoPageEvictionTracker(pageMem0, plc, cctx);
+ return new FairFifoPageEvictionTracker(pageMem0, cctx);
switch (plc.getPageEvictionMode()) {
case RANDOM_LRU:
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..4332a1f0bbedb 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
@@ -20,7 +20,6 @@
import java.util.LinkedList;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
-import org.apache.ignite.configuration.DataRegionConfiguration;
import org.apache.ignite.internal.pagemem.PageIdUtils;
import org.apache.ignite.internal.pagemem.impl.PageMemoryNoStoreImpl;
import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
@@ -34,14 +33,10 @@ public class FairFifoPageEvictionTracker extends PageAbstractEvictionTracker {
/**
* @param pageMem Page memory.
- * @param plcCfg Data region configuration.
* @param sharedCtx Shared context.
*/
- public FairFifoPageEvictionTracker(
- PageMemoryNoStoreImpl pageMem,
- DataRegionConfiguration plcCfg,
- GridCacheSharedContext sharedCtx) {
- super(pageMem, plcCfg, sharedCtx);
+ public FairFifoPageEvictionTracker(PageMemoryNoStoreImpl pageMem, GridCacheSharedContext sharedCtx) {
+ super(pageMem, sharedCtx);
}
/** {@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..d27d9e1c9eb85 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
@@ -47,11 +47,6 @@ public class NoOpPageEvictionTracker implements PageEvictionTracker {
// No-op.
}
- /** {@inheritDoc} */
- @Override public boolean evictionRequired() {
- return false;
- }
-
/** {@inheritDoc} */
@Override public void trackFragmentPage(long pageId, long prevPageId, boolean isHeadPage) {
// No-op.
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..082380c3ca597 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
@@ -18,14 +18,12 @@
import java.util.List;
import org.apache.ignite.IgniteCheckedException;
-import org.apache.ignite.configuration.DataRegionConfiguration;
import org.apache.ignite.internal.pagemem.PageIdUtils;
import org.apache.ignite.internal.pagemem.impl.PageMemoryNoStoreImpl;
import org.apache.ignite.internal.processors.cache.GridCacheContext;
import org.apache.ignite.internal.processors.cache.GridCacheEntryEx;
import org.apache.ignite.internal.processors.cache.GridCacheSharedContext;
import org.apache.ignite.internal.processors.cache.persistence.CacheDataRowAdapter;
-import org.apache.ignite.internal.processors.cache.persistence.freelist.AbstractFreeList;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.DataPageIO;
import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIO;
import org.apache.ignite.internal.processors.cache.version.GridCacheVersionManager;
@@ -53,40 +51,21 @@ public abstract class PageAbstractEvictionTracker implements PageEvictionTracker
/** Shared context. */
private final GridCacheSharedContext sharedCtx;
- /** Data region configuration. */
- private final DataRegionConfiguration regCfg;
-
/**
* @param pageMem Page memory.
- * @param plcCfg Data region configuration.
* @param sharedCtx Shared context.
*/
- PageAbstractEvictionTracker(
- PageMemoryNoStoreImpl pageMem,
- DataRegionConfiguration plcCfg,
- GridCacheSharedContext sharedCtx
- ) {
+ PageAbstractEvictionTracker(PageMemoryNoStoreImpl pageMem, GridCacheSharedContext sharedCtx) {
this.pageMem = pageMem;
this.sharedCtx = sharedCtx;
- regCfg = plcCfg;
-
trackingSize = pageMem.totalPages();
baseCompactTs = (U.currentTimeMillis() - DAY) >> COMPACT_TS_SHIFT;
// We subtract day to avoid fail in case of daylight shift or timezone change.
}
- /** {@inheritDoc} */
- @Override public boolean evictionRequired() {
- AbstractFreeList freeList = (AbstractFreeList)sharedCtx.database().freeList(regCfg.getName());
-
- double pagesThreshold = regCfg.getEvictionThreshold() * regCfg.getMaxSize() / pageMem.systemPageSize();
-
- return pageMem.loadedPages() > pagesThreshold && freeList.emptyDataPages() < regCfg.getEmptyPagesPoolSize();
- }
-
/**
* @param pageIdx Page index.
* @return true if at least one data row has been evicted
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..cb3d7df45fe03 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
@@ -31,13 +31,6 @@ public interface PageEvictionTracker extends LifecycleAware {
*/
public void touchPage(long pageId);
- /**
- * Check if page eviction is required according to the configured policy.
- *
- * @return {@code True} if eviction required.
- */
- public boolean evictionRequired();
-
/**
* Evicts one data page.
* In most cases, all entries will be removed from the 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..0f337f01ec7c5 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
@@ -58,7 +58,7 @@ public Random2LruPageEvictionTracker(
DataRegionConfiguration plcCfg,
GridCacheSharedContext, ?> sharedCtx
) {
- super(pageMem, plcCfg, sharedCtx);
+ super(pageMem, sharedCtx);
DataStorageConfiguration memCfg = sharedCtx.kernalContext().config().getDataStorageConfiguration();
@@ -161,7 +161,19 @@ public Random2LruPageEvictionTracker(
firstTs = first(trackingData);
- assert firstTs >= 0 : "[firstTs=" + firstTs + ", secondTs=" + second(trackingData) + "]";
+ // The link read above may be stale: a concurrently deleted row frees the head page, which can be
+ // re-used as an intermediate fragment holding a link again. Legal state: skip and resample.
+ if (firstTs < 0) {
+ sampleSpinCnt++;
+
+ if (sampleSpinCnt > SAMPLE_SPIN_LIMIT) {
+ LT.warn(log, "Too many attempts to choose data page: " + SAMPLE_SPIN_LIMIT);
+
+ return;
+ }
+
+ continue;
+ }
}
}
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..78b49278de5d0 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
@@ -60,7 +60,7 @@ public RandomLruPageEvictionTracker(
DataRegionConfiguration plcCfg,
GridCacheSharedContext, ?> sharedCtx
) {
- super((PageMemoryNoStoreImpl)pageMem, plcCfg, sharedCtx);
+ super((PageMemoryNoStoreImpl)pageMem, sharedCtx);
DataStorageConfiguration memCfg = sharedCtx.kernalContext().config().getDataStorageConfiguration();
@@ -141,7 +141,19 @@ public RandomLruPageEvictionTracker(
compactTs = GridUnsafe.getIntVolatile(null, trackingArrPtr + sampleTrackingIdx * 4L);
- assert compactTs >= 0 : "[compactTs=" + compactTs + "]";
+ // The link read above may be stale: a concurrently deleted row frees the head page, which can be
+ // re-used as an intermediate fragment holding a link again. Legal state: skip and resample.
+ if (compactTs < 0) {
+ sampleSpinCnt++;
+
+ if (sampleSpinCnt > SAMPLE_SPIN_LIMIT) {
+ LT.warn(log, "Too many attempts to choose data page: " + SAMPLE_SPIN_LIMIT);
+
+ return;
+ }
+
+ continue;
+ }
}
}
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 48055a157a270..3212628bd86ae 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
@@ -21,9 +21,13 @@
import java.util.Collection;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReferenceArray;
+import java.util.concurrent.locks.LockSupport;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.IgniteSystemProperties;
+import org.apache.ignite.configuration.DataRegionConfiguration;
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;
@@ -50,10 +54,13 @@
import org.apache.ignite.internal.processors.cache.persistence.tree.util.PageHandler;
import org.apache.ignite.internal.util.GridCursorIteratorWrapper;
import org.apache.ignite.internal.util.lang.GridCursor;
+import org.apache.ignite.internal.util.typedef.internal.LT;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.jetbrains.annotations.Nullable;
+import static org.apache.ignite.IgniteSystemProperties.IGNITE_PAGE_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS;
import static org.apache.ignite.internal.pagemem.PageIdAllocator.FLAG_DATA;
+import static org.apache.ignite.internal.pagemem.impl.PageMemoryNoStoreImpl.SEG_CNT;
/**
*/
@@ -76,6 +83,30 @@ public abstract class AbstractFreeList extends PagesList imp
/** */
private static final int MIN_PAGE_FREE_SPACE = 8;
+ /** 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;
+
+ /**
+ * Default maximum time (milliseconds) the size-aware eviction guard is willing to wait without the region's free
+ * page count increasing (the deficit shrinking) before giving up (see {@link #reserve(long, boolean)}). Configurable via
+ * {@link IgniteSystemProperties#IGNITE_PAGE_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS}.
+ */
+ public static final long DFLT_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS = 5_000L;
+
+ /**
+ * Maximum time (milliseconds) the size-aware eviction guard is willing to wait without the region's free page
+ * count increasing (the deficit shrinking) before giving up. This is a liveness guard: a
+ * {@code evictDataPage()} call can free no page (for example when the sampled rows are not actually reclaimable),
+ * so progress is measured by a real deficit decrease.
+ * An eviction that is merely slow but still frees pages keeps shrinking the deficit and thus keeps resetting the
+ * timer; a stall that frees nothing for the whole window gives up instead of spinning.
+ */
+ private static final long EVICTION_NO_PROGRESS_TIMEOUT_MILLIS = IgniteSystemProperties.getLong(
+ IGNITE_PAGE_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS, DFLT_EVICTION_NO_PROGRESS_TIMEOUT_MILLIS);
+
/**
* 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.
@@ -100,6 +131,26 @@ public abstract class AbstractFreeList extends PagesList imp
/** */
private final PageEvictionTracker evictionTracker;
+ /** Whether the page reservation is engaged for this free list: only for in-memory regions with enabled page eviction. */
+ private final boolean reserveEnabled;
+
+ /** Data region this free list belongs to. */
+ private final DataRegion dataRegion;
+
+ /**
+ * Pages reserved by in-flight inserts (see {@link #reserve(long, boolean)}): only the rows' own pages, the empty pages pool
+ * is a separate region-wide margin. A reservation is released as a whole once its row (or batch of rows) is written.
+ */
+ private final AtomicLong reservedPages = new AtomicLong();
+
+ /**
+ * Whether the eviction is stalled: set by a best-effort reservation that gave up because the eviction made no
+ * progress within {@link #EVICTION_NO_PROGRESS_TIMEOUT_MILLIS}, cleared by any reservation that observes progress or
+ * reserves pages. While set, best-effort reservations (including the already waiting ones) give up after an
+ * eviction attempt without progress, so a series of inserts does not wait for the timeout one by one.
+ */
+ private volatile boolean stalled;
+
/** Page list cache limit. */
private final AtomicLong pageListCacheLimit;
@@ -367,7 +418,7 @@ private final class WriteRowsHandler extends PageHandler, Integer>
AbstractDataPageIO io = (AbstractDataPageIO)iox;
// Fill the page up to the end.
- while (written != COMPLETE || (!evictionTracker.evictionRequired() && cur.next())) {
+ while (written != COMPLETE || cur.next()) {
T row = cur.get();
if (written == COMPLETE) {
@@ -493,6 +544,10 @@ public AbstractFreeList(
rmvRow = new RemoveRowHandler(cacheGrpId == 0);
this.evictionTracker = dataRegion.evictionTracker();
+
+ reserveEnabled = ctx.cache().context().database().isReserveEnabled(dataRegion);
+
+ this.dataRegion = dataRegion;
this.reuseList = reuseList == null ? this : reuseList;
int pageSize = pageMem.pageSize();
@@ -649,6 +704,9 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
* Rows are sequentially added to the page as long as there is enough free space on it. If the row is large then
* those fragments that occupy the whole memory page are written to other pages, and the remainder is added to the
* current one.
+ *
+ * The space of the whole batch is reserved upfront, before any page lock is taken: the eviction is not allowed
+ * under a page write lock, and a batch (e.g. of rebalancing) is small, so a pessimistic reservation is cheap.
*
* @param rows Rows.
* @param statHolder Statistics holder to track IO operations.
@@ -657,39 +715,40 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
@Override public void insertDataRows(Collection rows,
IoStatisticsHolder statHolder) throws IgniteCheckedException {
try {
- GridCursor cur = new GridCursorIteratorWrapper<>(rows.iterator());
-
- int written = COMPLETE;
+ // Best-effort: rows of a batch (e.g. of rebalancing) can not be safely refused.
+ long reserved = reserveEnabled ? reserve(batchSize(rows), true) : 0;
- while (written != COMPLETE || cur.next()) {
- T row = cur.get();
+ try {
+ GridCursor cur = new GridCursorIteratorWrapper<>(rows.iterator());
- // If eviction is required - free up memory before locking the next page.
- while (evictionTracker.evictionRequired()) {
- evictionTracker.evictDataPage();
+ int written = COMPLETE;
- memMetrics.updateEvictionRate();
- }
+ while (written != COMPLETE || cur.next()) {
+ T row = cur.get();
- if (written == COMPLETE) {
- written = writeWholePages(row, statHolder);
+ if (written == COMPLETE) {
+ written = writeWholePages(row, statHolder);
- continue;
- }
+ continue;
+ }
- AbstractDataPageIO initIo = null;
+ AbstractDataPageIO initIo = null;
- long pageId = takePage(row.size() - written, row, statHolder);
+ long pageId = takePage(row.size() - written, row, statHolder);
- if (pageId == 0L) {
- pageId = allocateDataPage(row.partition());
+ if (pageId == 0L) {
+ pageId = allocateDataPage(row.partition());
- initIo = row.ioVersions().latest();
- }
+ initIo = row.ioVersions().latest();
+ }
- written = write(pageId, writeRowsHnd, initIo, cur, written, FAIL_I, statHolder);
+ written = write(pageId, writeRowsHnd, initIo, cur, written, FAIL_I, statHolder);
- assert written != FAIL_I; // We can't fail here.
+ assert written != FAIL_I; // We can't fail here.
+ }
+ }
+ finally {
+ releaseReserved(reserved);
}
}
catch (RuntimeException e) {
@@ -697,6 +756,23 @@ private long allocateDataPage(int part) throws IgniteCheckedException {
}
}
+ /**
+ * Computes a pessimistic size of a batch of rows: every row is rounded up to whole data pages, as if no rows
+ * shared a page.
+ *
+ * @param rows Rows.
+ * @return Size of the batch in bytes.
+ * @throws IgniteCheckedException If failed.
+ */
+ private long batchSize(Collection rows) throws IgniteCheckedException {
+ long size = 0;
+
+ for (T row : rows)
+ size += requiredPages(row.size()) * MIN_SIZE_FOR_DATA_PAGE;
+
+ return size;
+ }
+
/**
* Write fragments of the row, which occupy the whole memory page. A data row is ignored if it is less than the max
* payload of an empty data page.
@@ -1134,8 +1210,184 @@ public int emptyDataPages() {
}
}
+ /** {@inheritDoc} */
+ @Override public long reserve(long rowSize, boolean bestEffort) throws IgniteOutOfMemoryException, IgniteCheckedException {
+ // Nothing to insert (e.g. an empty batch): no eviction is needed.
+ if (!reserveEnabled || rowSize <= 0)
+ return 0;
+
+ long claimPages = requiredPages(rowSize);
+
+ if (!fitsRegion(claimPages)) {
+ if (bestEffort)
+ return 0; // Let the page memory decide.
+
+ throw outOfMemory();
+ }
+
+ long limitPages = limitPages(claimPages);
+
+ long backoffNanos = EVICTION_BACKOFF_START_NANOS;
+
+ long lastProgressMillis = U.currentTimeMillis();
+
+ // Deficit before the last eviction attempt, 0 until the first one.
+ long prevDeficit = 0;
+
+ while (true) {
+ long reserved = reservedPages.get();
+ long deficit = deficit(claimPages, limitPages, reserved);
+
+ if (deficit <= 0) {
+ if (reservedPages.compareAndSet(reserved, reserved + claimPages)) {
+ clearStalled();
+
+ return claimPages;
+ }
+
+ continue;
+ }
+
+ if (prevDeficit > 0) {
+ if (deficit < prevDeficit) {
+ clearStalled();
+
+ lastProgressMillis = U.currentTimeMillis();
+
+ backoffNanos = EVICTION_BACKOFF_START_NANOS;
+ }
+ else if (bestEffort && stalled)
+ return 0;
+ else if (U.currentTimeMillis() - lastProgressMillis > EVICTION_NO_PROGRESS_TIMEOUT_MILLIS) {
+ if (!bestEffort)
+ throw outOfMemory();
+
+ stalled = true;
+
+ LT.warn(log, "Page eviction made no progress, the row is inserted without a page reservation " +
+ "[region=" + dataRegion.config().getName() + ", timeout=" + EVICTION_NO_PROGRESS_TIMEOUT_MILLIS + "ms]");
+
+ return 0;
+ }
+ else {
+ LockSupport.parkNanos(backoffNanos);
+
+ backoffNanos = Math.min(backoffNanos << 1, EVICTION_BACKOFF_MAX_NANOS);
+ }
+ }
+
+ prevDeficit = deficit;
+
+ if (memMetrics.onPageEvictionsStarted()) {
+ U.warn(log, "Page-based evictions started." +
+ " Consider increasing 'maxSize' on Data Region configuration: " + dataRegion.config().getName());
+ }
+
+ evictionTracker.evictDataPage();
+
+ memMetrics.updateEvictionRate();
+ }
+ }
+
+ /**
+ * Clears the {@link #stalled} flag once a reservation observes progress or reserves pages. The flag is read first,
+ * so the common path does not write to the shared field.
+ */
+ private void clearStalled() {
+ if (stalled)
+ stalled = false;
+ }
+
+ /** {@inheritDoc} */
+ @Override public void releaseReserved(long pages) {
+ assert pages >= 0;
+
+ if (reserveEnabled && pages != 0)
+ reservedPages.addAndGet(-pages);
+ }
+
/** {@inheritDoc} */
@Override public String toString() {
return "FreeList [name=" + name() + ']';
}
+
+ /**
+ * Computes the number of pages to claim for a row. The empty pages pool is not a part of the claim: it is a single
+ * region-wide margin that every reservation must leave free (see {@link #deficit}), so concurrent writers do not
+ * multiply it.
+ *
+ * @param rowSize Row size in bytes.
+ * @return Number of pages required for the row.
+ */
+ private long requiredPages(long rowSize) {
+ return (rowSize + MIN_SIZE_FOR_DATA_PAGE - 1) / MIN_SIZE_FOR_DATA_PAGE;
+ }
+
+ /**
+ * @param requiredPages Pages required for a row.
+ * @return {@code True} if the row together with the empty pages pool fits into the whole data region.
+ */
+ private boolean fitsRegion(long requiredPages) {
+ return requiredPages + dataRegion.config().getEmptyPagesPoolSize() <= totalPages();
+ }
+
+ /** @return Total number of pages in the data region. */
+ private long totalPages() {
+ return dataRegion.config().getMaxSize() / pageMem.systemPageSize();
+ }
+
+ /**
+ * Computes the number of loaded pages up to which the free space of the region is counted for a row. Normally it
+ * is the eviction threshold ({@link DataRegionConfiguration#getEvictionThreshold()}), so every insert path starts
+ * evicting at the same fill level. A row that does not fit below the threshold together with the empty pages pool
+ * may use the space above it, up to the whole region.
+ *
+ * @param requiredPages Pages required for the row.
+ * @return Limit of loaded pages for the row.
+ */
+ private long limitPages(long requiredPages) {
+ long totalPages = totalPages();
+
+ long hardPages = totalPages > SEG_CNT ? totalPages - SEG_CNT : 0;
+
+ long thresholdPages = Math.min((long)(dataRegion.config().getEvictionThreshold() * totalPages), hardPages);
+
+ return requiredPages + dataRegion.config().getEmptyPagesPoolSize() > thresholdPages ? hardPages : thresholdPages;
+ }
+
+ /**
+ * Computes how many pages are missing to admit a claim: the claim, the empty pages pool and the reservations of
+ * other in-flight inserts must fit into the free pages of the region (empty pages in this free list plus the
+ * headroom up to the limit).
+ *
+ * @param claimPages Pages to claim.
+ * @param limitPages Limit of loaded pages (see {@link #limitPages(long)}).
+ * @param reserved Pages reserved by other in-flight inserts.
+ * @return Number of missing pages, a non-positive value if the claim can be admitted.
+ */
+ private long deficit(long claimPages, long limitPages, long reserved) {
+ long freePages = emptyDataPages() + Math.max(0, limitPages - pageMem.loadedPages());
+
+ return claimPages + dataRegion.config().getEmptyPagesPoolSize() + reserved - freePages;
+ }
+
+ /**
+ * Creates an out-of-memory exception for a refused reservation. It is thrown only for inserts that can be refused
+ * (see {@link #reserve(long, boolean)}) and is not reported to the failure handler: nothing has been changed yet.
+ *
+ * @return New {@link IgniteOutOfMemoryException} for the data region of this free list.
+ */
+ private IgniteOutOfMemoryException outOfMemory() {
+ DataRegionConfiguration cfg = dataRegion.config();
+
+ return new IgniteOutOfMemoryException("Out of memory in data region [" +
+ "name=" + cfg.getName() +
+ ", initSize=" + U.readableSize(cfg.getInitialSize(), false) +
+ ", maxSize=" + U.readableSize(cfg.getMaxSize(), false) +
+ ", persistenceEnabled=" + cfg.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"
+ );
+ }
}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeList.java
index 3db46ed6a300e..daa2c2abe8f0c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/FreeList.java
@@ -20,6 +20,7 @@
import java.util.Collection;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.internal.mem.IgniteOutOfMemoryException;
import org.apache.ignite.internal.metric.IoStatisticsHolder;
import org.apache.ignite.internal.processors.cache.persistence.Storable;
@@ -63,4 +64,30 @@ public void updateDataRow(
* @param log Logger.
*/
public void dumpStatistics(IgniteLogger log);
+
+ /**
+ * Reserves pages for a row (or a batch of rows) of the given size before it is inserted. Runs blocking eviction
+ * when necessary (the caller must not hold any entry or page locks).
+ *
+ * @param rowSize Size of the row (or the total size of a batch of rows) to be inserted.
+ * @param bestEffort {@code True} if the insert can not be safely refused (e.g. a backup update or a transaction
+ * commit): if the row does not fit into the region or the eviction makes no progress, the row is admitted
+ * without a reservation ({@code 0} is returned) and the page memory remains the hard limit. {@code False} if
+ * the insert can be refused: an out-of-memory is thrown instead.
+ * @return Number of reserved pages to be released via {@link #releaseReserved(long)} once the row insert
+ * completes, or {@code 0} if nothing is reserved (persistent data region, disabled page eviction or a best-effort
+ * admission without a reservation).
+ * @throws IgniteOutOfMemoryException If the insert is not best-effort and the row cannot fit into the region or
+ * eviction makes no progress. The exception is not reported to the failure handler: nothing has been changed
+ * yet, so the caller decides how to handle the refusal.
+ * @throws IgniteCheckedException If failed to evict data pages.
+ */
+ public long reserve(long rowSize, boolean bestEffort) throws IgniteOutOfMemoryException, IgniteCheckedException;
+
+ /**
+ * Releases a reservation made by {@link #reserve(long, boolean)}.
+ *
+ * @param pages Number of pages to release (must not be negative).
+ */
+ public void releaseReserved(long pages);
}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/CacheVersionIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/CacheVersionIO.java
index fd7f2f5db8691..c2ba4e0c998ed 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/CacheVersionIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/CacheVersionIO.java
@@ -40,7 +40,7 @@ public class CacheVersionIO {
private static final int SIZE_V1 = 17;
/** Serialized size in bytes. */
- private static final int SIZE_V2 = 33;
+ public static final int SIZE_V2 = 33;
/**
* @param ver Version.
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionAbstractTest.java
index 18622516a2030..2bd40e87b37d0 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionAbstractTest.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/eviction/paged/PageEvictionAbstractTest.java
@@ -16,19 +16,31 @@
*/
package org.apache.ignite.internal.processors.cache.eviction.paged;
+import javax.cache.Cache;
+import javax.cache.configuration.Factory;
+import javax.cache.expiry.Duration;
+import javax.cache.integration.CacheLoaderException;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.CacheWriteSynchronizationMode;
import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
+import org.apache.ignite.cache.store.CacheStoreAdapter;
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.configuration.NearCacheConfiguration;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.cache.persistence.DataRegion;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.jetbrains.annotations.NotNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static javax.cache.expiry.CreatedExpiryPolicy.factoryOf;
+
/**
*
*/
@@ -46,7 +58,7 @@ public class PageEvictionAbstractTest extends GridCommonAbstractTest {
private static final int EMPTY_PAGES_POOL_SIZE = 100;
/** Eviction threshold. */
- private static final double EVICTION_THRESHOLD = 0.9;
+ static final double EVICTION_THRESHOLD = 0.9;
/** Default policy name. */
private static final String DEFAULT_POLICY_NAME = "dfltPlc";
@@ -130,4 +142,111 @@ protected CacheConfiguration