-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Promote PerThreadPKLookup to core as PrimaryKeyLookup #16128
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
Open
shubhamsrkdev
wants to merge
13
commits into
apache:main
Choose a base branch
from
shubhamsrkdev:PKLookup
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+398
−103
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
6830ef4
Promoted PrimaryKeyLookup and added tests
shubhamsrkdev a04a4a5
Tidy
shubhamsrkdev dee58de
Tidy and javadoc
shubhamsrkdev 5c44c5d
Merge branch 'main' into PKLookup
shubhamsrkdev 31fb109
Javadoc
shubhamsrkdev 005eaae
Added fast and slow paths
shubhamsrkdev af7eb9f
Changed extending class
shubhamsrkdev bcf8297
gradle tidy
shubhamsrkdev 838e1d3
Merge branch 'main' into PKLookup
shubhamsrkdev 511f2b1
Merge branch 'main' into PKLookup
shubhamsrkdev caf337d
Merge branch 'main' into PKLookup
shubhamsrkdev 984746d
Merge branch 'main' into PKLookup
shubhamsrkdev 1c001e8
Merge branch 'main' into PKLookup
shubhamsrkdev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
lucene/core/src/java/org/apache/lucene/index/PrimaryKeyLookup.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| /* | ||
| * 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.lucene.index; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import org.apache.lucene.index.IndexReader.CacheHelper; | ||
| import org.apache.lucene.util.Bits; | ||
| import org.apache.lucene.util.BytesRef; | ||
|
|
||
| /** | ||
| * Class to do efficient primary-key lookups. Instances are not thread-safe. Each calling thread | ||
| * must own its own instance, and an instance must only ever be used from the thread that | ||
| * constructed it. When assertions are enabled this invariant is checked on every lookup and reopen | ||
| * call. Do not use this if a term may appear in more than one document! It will only return the | ||
| * first one it finds. | ||
| * | ||
| * @lucene.experimental | ||
| */ | ||
| public class PrimaryKeyLookup { | ||
|
shubhamsrkdev marked this conversation as resolved.
|
||
|
|
||
| /** Name of the primary-key field. */ | ||
| protected final String idFieldName; | ||
|
|
||
| /** Per-segment {@link TermsEnum}. */ | ||
| protected final TermsEnum[] termsEnums; | ||
|
|
||
| /** Per-segment {@link PostingsEnum}. */ | ||
| protected final PostingsEnum[] postingsEnums; | ||
|
|
||
| /** Per-segment live-docs bitset. */ | ||
| protected final Bits[] liveDocs; | ||
|
|
||
| /** Per-segment {@code docBase} used to translate leaf docIDs to absolute docIDs. */ | ||
| protected final int[] docBases; | ||
|
|
||
| /** Number of populated entries in termsEnums/postingsEnums. */ | ||
| protected final int numEnums; | ||
|
|
||
| /** True if any segment in this lookup has deletions. */ | ||
| protected final boolean hasDeletions; | ||
|
|
||
| /** | ||
| * Maps a segment core cache key to its index in termsEnums/postingsEnums, so that reopen can | ||
| * transfer per-segment enum state to the next generation. | ||
| */ | ||
| protected final Map<IndexReader.CacheKey, Integer> enumIndexes; | ||
|
|
||
| // Thread-stickiness guard. | ||
| private final long ownerThreadId; | ||
|
|
||
| /** Construct a {@code PrimaryKeyLookup} bound to {@code reader}. */ | ||
| public PrimaryKeyLookup(IndexReader reader, String idFieldName) throws IOException { | ||
| this(reader, idFieldName, Collections.emptyMap(), null, null); | ||
| } | ||
|
|
||
| /** Construct a {@code PrimaryKeyLookup} instance. */ | ||
| protected PrimaryKeyLookup( | ||
| IndexReader reader, | ||
| String idFieldName, | ||
| Map<IndexReader.CacheKey, Integer> prevEnumIndexes, | ||
| TermsEnum[] reusableTermsEnums, | ||
| PostingsEnum[] reusablePostingsEnums) | ||
| throws IOException { | ||
| this.idFieldName = idFieldName; | ||
| this.ownerThreadId = Thread.currentThread().threadId(); | ||
|
|
||
| List<LeafReaderContext> leaves = new ArrayList<>(reader.leaves()); | ||
| // Larger segments are more likely to have the id, so we sort largest to smallest by numDocs: | ||
| leaves.sort((c1, c2) -> c2.reader().numDocs() - c1.reader().numDocs()); | ||
|
|
||
| termsEnums = new TermsEnum[leaves.size()]; | ||
| postingsEnums = new PostingsEnum[leaves.size()]; | ||
| liveDocs = new Bits[leaves.size()]; | ||
| docBases = new int[leaves.size()]; | ||
| enumIndexes = new HashMap<>(); | ||
| int numEnums = 0; | ||
| boolean hasDeletions = false; | ||
|
|
||
| for (LeafReaderContext context : leaves) { | ||
| LeafReader leafReader = context.reader(); | ||
| CacheHelper cacheHelper = leafReader.getCoreCacheHelper(); | ||
| IndexReader.CacheKey cacheKey = cacheHelper == null ? null : cacheHelper.getKey(); | ||
|
|
||
| if (cacheKey != null && prevEnumIndexes.containsKey(cacheKey)) { | ||
| // Reuse termsEnum, postingsEnum. | ||
| int seg = prevEnumIndexes.get(cacheKey); | ||
| termsEnums[numEnums] = reusableTermsEnums[seg]; | ||
| postingsEnums[numEnums] = reusablePostingsEnums[seg]; | ||
| } else { | ||
| // New or empty segment. | ||
| Terms terms = leafReader.terms(idFieldName); | ||
| if (terms != null) { | ||
| termsEnums[numEnums] = terms.iterator(); | ||
| assert termsEnums[numEnums] != null; | ||
| } | ||
| } | ||
|
|
||
| if (termsEnums[numEnums] != null) { | ||
| if (cacheKey != null) { | ||
| enumIndexes.put(cacheKey, numEnums); | ||
| } | ||
|
|
||
| docBases[numEnums] = context.docBase; | ||
| liveDocs[numEnums] = leafReader.getLiveDocs(); | ||
| hasDeletions |= leafReader.hasDeletions(); | ||
|
|
||
| numEnums++; | ||
| } | ||
| } | ||
|
|
||
| this.numEnums = numEnums; | ||
| this.hasDeletions = hasDeletions; | ||
| } | ||
|
|
||
| /** Returns docID if found, else -1. */ | ||
| public int lookup(BytesRef id) throws IOException { | ||
| assert assertOwnerThread(); | ||
| for (int seg = 0; seg < numEnums; seg++) { | ||
| if (termsEnums[seg].seekExact(id) == false) { | ||
| continue; | ||
| } | ||
| postingsEnums[seg] = termsEnums[seg].postings(postingsEnums[seg], 0); | ||
|
|
||
| if (liveDocs[seg] == null) { | ||
| // Fast path: segment has no deletions. | ||
| int docID = postingsEnums[seg].nextDoc(); | ||
| assert docID != PostingsEnum.NO_MORE_DOCS; | ||
| return docBases[seg] + docID; | ||
| } | ||
|
|
||
| // Slow path: segment has deletions; scan until we find a live doc or exhaust. | ||
| int docID; | ||
| while ((docID = postingsEnums[seg].nextDoc()) != PostingsEnum.NO_MORE_DOCS) { | ||
| if (liveDocs[seg].get(docID)) { | ||
| return docBases[seg] + docID; | ||
| } | ||
| } | ||
|
|
||
| assert hasDeletions; | ||
| } | ||
| return -1; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a new PrimaryKeyLookup instance bound reusing this instance per-segment | ||
| * TermsEnum/PostingsEnum etc. | ||
| */ | ||
| public PrimaryKeyLookup reopen(IndexReader reader) throws IOException { | ||
| assert assertOwnerThread(); | ||
| if (reader == null) { | ||
| return null; | ||
| } | ||
| return new PrimaryKeyLookup( | ||
| reader, this.idFieldName, this.enumIndexes, this.termsEnums, this.postingsEnums); | ||
| } | ||
|
|
||
| private boolean assertOwnerThread() { | ||
| long current = Thread.currentThread().threadId(); | ||
| assert current == ownerThreadId | ||
| : "PrimaryKeyLookup is not thread-safe: it was created by thread " | ||
| + ownerThreadId | ||
| + " but accessed from thread " | ||
| + current; | ||
| return true; | ||
| } | ||
| } | ||
196 changes: 196 additions & 0 deletions
196
lucene/core/src/test/org/apache/lucene/index/TestPrimaryKeyLookup.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /* | ||
| * 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.lucene.index; | ||
|
|
||
| import org.apache.lucene.document.Document; | ||
| import org.apache.lucene.document.Field; | ||
| import org.apache.lucene.document.KeywordField; | ||
| import org.apache.lucene.store.Directory; | ||
| import org.apache.lucene.tests.analysis.MockAnalyzer; | ||
| import org.apache.lucene.tests.util.LuceneTestCase; | ||
|
|
||
| public class TestPrimaryKeyLookup extends LuceneTestCase { | ||
|
|
||
| public void testReopen() throws Exception { | ||
| Directory dir = newDirectory(); | ||
| IndexWriter writer = | ||
| new IndexWriter( | ||
| dir, | ||
| new IndexWriterConfig(new MockAnalyzer(random())) | ||
| .setMergePolicy(NoMergePolicy.INSTANCE)); | ||
|
|
||
| Document doc; | ||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "1", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "2", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
| writer.flush(); | ||
|
|
||
| // Segment with no PK terms. | ||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK2", "3", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK2", "4", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
| writer.flush(); | ||
|
|
||
| DirectoryReader reader1 = DirectoryReader.open(writer); | ||
| PrimaryKeyLookup pk1 = new PrimaryKeyLookup(reader1, "PK"); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "5", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "6", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
| writer.deleteDocuments(new Term("PK", "1")); | ||
| writer.flush(); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK2", "7", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK2", "8", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
| writer.flush(); | ||
|
|
||
| assertEquals(0, pk1.lookup(newBytesRef("1"))); | ||
| assertEquals(1, pk1.lookup(newBytesRef("2"))); | ||
| assertEquals(-1, pk1.lookup(newBytesRef("5"))); | ||
| assertEquals(-1, pk1.lookup(newBytesRef("8"))); | ||
|
|
||
| DirectoryReader reader2 = DirectoryReader.openIfChanged(reader1); | ||
| PrimaryKeyLookup pk2 = pk1.reopen(reader2); | ||
|
|
||
| assertEquals(-1, pk2.lookup(newBytesRef("1"))); | ||
| assertEquals(1, pk2.lookup(newBytesRef("2"))); | ||
| assertEquals(4, pk2.lookup(newBytesRef("5"))); | ||
| assertEquals(-1, pk2.lookup(newBytesRef("8"))); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "9", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "10", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
| writer.flush(); | ||
|
|
||
| assertEquals(-1, pk2.lookup(newBytesRef("9"))); | ||
| DirectoryReader reader3 = DirectoryReader.openIfChanged(reader2); | ||
| PrimaryKeyLookup pk3 = pk2.reopen(reader3); | ||
| assertEquals(8, pk3.lookup(newBytesRef("9"))); | ||
|
|
||
| DirectoryReader reader4 = DirectoryReader.openIfChanged(reader3); | ||
| assertNull(pk3.reopen(reader4)); | ||
|
|
||
| writer.close(); | ||
| reader1.close(); | ||
| reader2.close(); | ||
| reader3.close(); | ||
| dir.close(); | ||
| } | ||
|
|
||
| public void testPKLookupWithUpdate() throws Exception { | ||
| Directory dir = newDirectory(); | ||
| IndexWriter writer = | ||
| new IndexWriter( | ||
| dir, | ||
| new IndexWriterConfig(new MockAnalyzer(random())) | ||
| .setMergePolicy(NoMergePolicy.INSTANCE)); | ||
|
|
||
| Document doc; | ||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "1", Field.Store.NO)); | ||
| doc.add(new KeywordField("version", "1", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "1", Field.Store.NO)); | ||
| doc.add(new KeywordField("version", "2", Field.Store.NO)); | ||
| writer.updateDocument(new Term("PK", "1"), doc); | ||
|
|
||
| doc = new Document(); | ||
| doc.add(new KeywordField("PK", "1", Field.Store.NO)); | ||
| doc.add(new KeywordField("version", "3", Field.Store.NO)); | ||
| writer.updateDocument(new Term("PK", "1"), doc); | ||
| writer.flush(); | ||
| writer.close(); | ||
|
|
||
| DirectoryReader reader = DirectoryReader.open(dir); | ||
| PrimaryKeyLookup pk = new PrimaryKeyLookup(reader, "PK"); | ||
| assertEquals(2, pk.lookup(newBytesRef("1"))); | ||
|
|
||
| reader.close(); | ||
| dir.close(); | ||
| } | ||
|
|
||
| /** | ||
| * Constructing on one thread and calling {@link PrimaryKeyLookup#lookup} from another must fire | ||
| * the thread-stickiness assertion. | ||
| */ | ||
| public void testThreadStickinessAssertion() throws Exception { | ||
| Directory dir = newDirectory(); | ||
| IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new MockAnalyzer(random()))); | ||
| Document doc = new Document(); | ||
| doc.add(new KeywordField("PK", "1", Field.Store.NO)); | ||
| writer.addDocument(doc); | ||
| writer.commit(); | ||
| writer.close(); | ||
|
|
||
| DirectoryReader reader = DirectoryReader.open(dir); | ||
| PrimaryKeyLookup pk = new PrimaryKeyLookup(reader, "PK"); | ||
|
|
||
| if (TEST_ASSERTS_ENABLED) { | ||
| // pk was constructed on this thread; calling lookup from any other thread must trip | ||
| // the thread-stickiness assertion in PrimaryKeyLookup. | ||
| class CrossThreadLookup extends Thread { | ||
| Throwable thrown; | ||
|
|
||
| @Override | ||
| public void run() { | ||
| try { | ||
| pk.lookup(newBytesRef("1")); | ||
| } catch (Throwable t) { | ||
| thrown = t; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| CrossThreadLookup other = new CrossThreadLookup(); | ||
| other.setName("pk-cross-thread-lookup"); | ||
| other.start(); | ||
| other.join(); | ||
|
|
||
| assertTrue( | ||
| "expected AssertionError but got: " + other.thrown, | ||
| other.thrown instanceof AssertionError); | ||
| assertTrue( | ||
| "unexpected message: " + other.thrown.getMessage(), | ||
| other.thrown.getMessage().contains("not thread-safe")); | ||
| } | ||
| reader.close(); | ||
| dir.close(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.