-
Notifications
You must be signed in to change notification settings - Fork 4.1k
CASSANDRA-21644 Fix concurrent SAI vector inserts failing once jvector's per-graph pool limit is exceeded #5102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: cassandra-5.0
Are you sure you want to change the base?
Changes from all commits
01c138e
ca75c5a
0b11c79
ed31aa3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,10 @@ | |
| import java.util.function.Function; | ||
| import java.util.stream.IntStream; | ||
|
|
||
| import javax.annotation.Nullable; | ||
|
|
||
| import com.google.common.annotations.VisibleForTesting; | ||
|
|
||
| import org.cliffc.high_scale_lib.NonBlockingHashMap; | ||
| import org.cliffc.high_scale_lib.NonBlockingHashMapLong; | ||
| import org.slf4j.Logger; | ||
|
|
@@ -63,6 +67,7 @@ | |
| import org.apache.cassandra.io.util.SequentialWriter; | ||
| import org.apache.cassandra.utils.ByteBufferUtil; | ||
| import org.apache.cassandra.utils.CloseableIterator; | ||
| import org.apache.cassandra.utils.concurrent.Semaphore; | ||
| import org.apache.lucene.util.StringHelper; | ||
|
|
||
| public class OnHeapGraph<T> | ||
|
|
@@ -71,6 +76,13 @@ public class OnHeapGraph<T> | |
|
|
||
| public static final int MIN_PQ_ROWS = 1024; | ||
|
|
||
| /** | ||
| * jvector's {@link GraphIndexBuilder#addGraphNode} throws if more than this many threads are inside it at once | ||
| * (see {@code PoolingSupport}). Uses {@link Runtime} rather than {@code DatabaseDescriptor} because that is what jvector reads. | ||
| */ | ||
| @VisibleForTesting | ||
| public static final int MAX_CONCURRENT_GRAPH_INSERTS = Runtime.getRuntime().availableProcessors() + 1; | ||
|
|
||
| private final RamAwareVectorValues vectorValues; | ||
| private final GraphIndexBuilder<float[]> builder; | ||
| private final VectorType<?> vectorType; | ||
|
|
@@ -79,6 +91,10 @@ public class OnHeapGraph<T> | |
| private final NonBlockingHashMapLong<VectorPostings<T>> postingsByOrdinal; | ||
| private final NonBlockingHashMap<T, float[]> vectorsByKey; | ||
| private final AtomicInteger nextOrdinal = new AtomicInteger(); | ||
| // memtable inserts are concurrent (CASSANDRA-21160); excess writers wait here rather than fail in jvector. | ||
| // Null when the memtable itself already limits writers to that many, e.g. a TrieMemtable with few enough shards. | ||
| @Nullable | ||
| private final Semaphore graphInsertPermits; | ||
| private volatile boolean hasDeletions; | ||
| private String source; | ||
|
|
||
|
|
@@ -107,6 +123,9 @@ public OnHeapGraph(AbstractType<?> termComparator, IndexWriterConfig indexWriter | |
| postingsMap = new ConcurrentSkipListMap<>(Arrays::compare); | ||
| postingsByOrdinal = new NonBlockingHashMapLong<>(); | ||
| vectorsByKey = memtable != null ? new NonBlockingHashMap<>() : null; | ||
| graphInsertPermits = memtable != null && memtable.limitsConcurrentWritesTo(MAX_CONCURRENT_GRAPH_INSERTS) | ||
| ? null | ||
| : Semaphore.newSemaphore(MAX_CONCURRENT_GRAPH_INSERTS); | ||
|
|
||
| builder = new GraphIndexBuilder<>(vectorValues, | ||
| VectorEncoding.FLOAT32, | ||
|
|
@@ -187,7 +206,17 @@ public long add(ByteBuffer term, T key, InvalidVectorBehavior behavior) | |
| : ((CompactionVectorValues) vectorValues).add(ordinal, term); | ||
| bytesUsed += VectorPostings.emptyBytesUsed() + VectorPostings.bytesPerPosting(); | ||
| postingsByOrdinal.put(ordinal, postings); | ||
| bytesUsed += builder.addGraphNode(ordinal, vectorValues); | ||
| if (graphInsertPermits != null) | ||
| graphInsertPermits.acquireThrowUncheckedOnInterrupt(1); | ||
| try | ||
| { | ||
| bytesUsed += builder.addGraphNode(ordinal, vectorValues); | ||
| } | ||
| finally | ||
| { | ||
| if (graphInsertPermits != null) | ||
| graphInsertPermits.release(1); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: The alternative to this null-checking stuff would be to have some kind of no-op
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes. I think that would be overkill and I did not see a reason to replace the null check with a boolean check. |
||
| return bytesUsed; | ||
| } | ||
| else | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /* | ||
| * 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.cassandra.db.memtable; | ||
|
|
||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
|
|
||
| import org.junit.BeforeClass; | ||
| import org.junit.Test; | ||
|
|
||
| import org.apache.cassandra.config.Config; | ||
| import org.apache.cassandra.config.DatabaseDescriptor; | ||
| import org.apache.cassandra.config.InheritingClass; | ||
| import org.apache.cassandra.cql3.CQLTester; | ||
| import org.apache.cassandra.db.ColumnFamilyStore; | ||
|
|
||
| import static org.apache.cassandra.db.memtable.AbstractShardedMemtable.SHARDS_OPTION; | ||
| import static org.apache.cassandra.db.memtable.ShardedSkipListMemtable.LOCKING_OPTION; | ||
| import static org.junit.Assert.assertFalse; | ||
| import static org.junit.Assert.assertTrue; | ||
|
|
||
| /** | ||
| * {@link Memtable#limitsConcurrentWritesTo} must be true only for memtables that serialize writes per shard, and | ||
| * only when the shard count is within the limit asked about. | ||
| */ | ||
| public class MemtableConcurrentWriteLimitTest extends CQLTester | ||
| { | ||
| private static final int SHARDS = 4; | ||
|
|
||
| // Overrides CQLTester.setUpClass so the memtable configurations are registered before the server is prepared | ||
| @BeforeClass | ||
| public static void setUpClass() | ||
| { | ||
| prePrepareServer(); | ||
|
|
||
| LinkedHashMap<String, InheritingClass> memtableConfig = new LinkedHashMap<>(); | ||
| memtableConfig.put("skiplist", new InheritingClass(null, SkipListMemtable.class.getName(), Map.of())); | ||
| memtableConfig.put("trie", new InheritingClass(null, TrieMemtable.class.getName(), Map.of(SHARDS_OPTION, String.valueOf(SHARDS)))); | ||
| memtableConfig.put("sharded", new InheritingClass(null, ShardedSkipListMemtable.class.getName(), Map.of(SHARDS_OPTION, String.valueOf(SHARDS)))); | ||
| memtableConfig.put("sharded_locking", new InheritingClass(null, ShardedSkipListMemtable.class.getName(), Map.of(SHARDS_OPTION, String.valueOf(SHARDS), LOCKING_OPTION, "true"))); | ||
| DatabaseDescriptor.getRawConfig().memtable = new Config.MemtableOptions(); | ||
| DatabaseDescriptor.getRawConfig().memtable.configurations = memtableConfig; | ||
|
|
||
| prepareServer(); | ||
| } | ||
|
|
||
| @Test | ||
| public void unshardedMemtableDoesNotLimitWriters() | ||
| { | ||
| assertFalse(memtableFor("skiplist").limitsConcurrentWritesTo(Integer.MAX_VALUE)); | ||
| } | ||
|
|
||
| @Test | ||
| public void shardedSkipListWithoutLockingDoesNotLimitWriters() | ||
| { | ||
| assertFalse(memtableFor("sharded").limitsConcurrentWritesTo(Integer.MAX_VALUE)); | ||
| } | ||
|
|
||
| @Test | ||
| public void trieMemtableLimitsWritersToShardCount() | ||
| { | ||
| assertLimitsToShardCount(memtableFor("trie")); | ||
| } | ||
|
|
||
| @Test | ||
| public void lockingShardedSkipListLimitsWritersToShardCount() | ||
| { | ||
| assertLimitsToShardCount(memtableFor("sharded_locking")); | ||
| } | ||
|
|
||
| private void assertLimitsToShardCount(Memtable memtable) | ||
| { | ||
| // the memtable may get fewer shards than requested, e.g. if local ranges cannot be split that finely | ||
| int shards = getCurrentColumnFamilyStore().localRangeSplits(SHARDS).shardCount(); | ||
| assertTrue(memtable.limitsConcurrentWritesTo(shards)); | ||
| assertTrue(memtable.limitsConcurrentWritesTo(shards + 1)); | ||
| assertFalse(memtable.limitsConcurrentWritesTo(shards - 1)); | ||
| } | ||
|
|
||
| private Memtable memtableFor(String memtableConfig) | ||
| { | ||
| createTable("CREATE TABLE %s (pk int PRIMARY KEY, v int) WITH memtable = '" + memtableConfig + '\''); | ||
| ColumnFamilyStore cfs = getCurrentColumnFamilyStore(); | ||
| return cfs.getTracker().getView().getCurrentMemtable(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't put this in
AbstractShardedMemtablebecause ofLocking?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. The base ShardedSkipListMemtable does not lock, so is unbounded. Only the Locking one limits it, so can return true of the shard count is low enough.