From 17cec747af4fa858d26544e5be386e0498d5eba4 Mon Sep 17 00:00:00 2001 From: Prudhvi Godithi Date: Sat, 24 Jan 2026 15:47:17 -0800 Subject: [PATCH 1/4] Support thread safe Signed-off-by: Prudhvi Godithi --- .../org/apache/lucene/search/TopFieldCollectorManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java b/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java index c62e9dc9f255..1fb3c1fc738a 100644 --- a/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java +++ b/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java @@ -17,9 +17,9 @@ package org.apache.lucene.search; import java.io.IOException; -import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * Create a TopFieldCollectorManager which uses a shared hit counter to maintain number of hits and @@ -111,7 +111,7 @@ public TopFieldCollectorManager(Sort sort, int numHits, FieldDoc after, int tota this.after = after; this.totalHitsThreshold = totalHitsThreshold; this.minScoreAcc = totalHitsThreshold != Integer.MAX_VALUE ? new MaxScoreAccumulator() : null; - this.collectors = new ArrayList<>(); + this.collectors = new CopyOnWriteArrayList<>(); } /** @@ -182,6 +182,7 @@ public TopFieldDocs reduce(Collection collectors) throws IOEx return TopDocs.merge(sort, 0, numHits, topDocs); } + @Deprecated public List getCollectors() { return collectors; } From 5fed6256fe6dc1d6e0a40d5439e4b4fbb6c400db Mon Sep 17 00:00:00 2001 From: Prudhvi Godithi Date: Sun, 22 Feb 2026 11:13:19 -0800 Subject: [PATCH 2/4] Add tests Signed-off-by: Prudhvi Godithi --- lucene/CHANGES.txt | 2 + .../search/TopFieldCollectorManager.java | 1 + .../lucene/search/TestTopFieldCollector.java | 44 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt index e20be387945d..4ddc09d67e8d 100644 --- a/lucene/CHANGES.txt +++ b/lucene/CHANGES.txt @@ -100,6 +100,8 @@ Optimizations Bug Fixes --------------------- +* GITHUB#15605: Make TopFieldCollectorManager thread-safe by using CopyOnWriteArrayList and deprecated getCollectors(). (Prudhvi Godithi) + * GITHUB#14049: Randomize KNN codec params in RandomCodec. Fixes scalar quantization div-by-zero when all values are identical. (Mike Sokolov) diff --git a/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java b/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java index 1fb3c1fc738a..56367811255d 100644 --- a/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java +++ b/lucene/core/src/java/org/apache/lucene/search/TopFieldCollectorManager.java @@ -182,6 +182,7 @@ public TopFieldDocs reduce(Collection collectors) throws IOEx return TopDocs.merge(sort, 0, numHits, topDocs); } + /** Returns the collectors created by this manager. */ @Deprecated public List getCollectors() { return collectors; diff --git a/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java b/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java index bcbc8cac50d2..37f55e8067c9 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java @@ -22,9 +22,16 @@ import static org.hamcrest.Matchers.sameInstance; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.Field.Store; @@ -703,6 +710,43 @@ public void testRandomMinCompetitiveScore() throws Exception { dir.close(); } + /** + * Test that concurrent newCollector() calls are thread-safe. See GitHub issue #15605 + */ + @SuppressWarnings("deprecation") + public void testConcurrentNewCollector() throws Exception { + Sort sort = new Sort(SortField.FIELD_SCORE); + TopFieldCollectorManager manager = new TopFieldCollectorManager(sort, 10, null, 100); + int numThreads = 8; + int callsPerThread = 1000; + int expectedTotal = numThreads * callsPerThread; + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + CountDownLatch startLatch = new CountDownLatch(1); + AtomicReference error = new AtomicReference<>(); + List> futures = new ArrayList<>(); + for (int i = 0; i < numThreads; i++) { + futures.add( + executor.submit( + () -> { + try { + startLatch.await(); + for (int j = 0; j < callsPerThread; j++) { + manager.newCollector(); + } + } catch (Throwable t) { + error.compareAndSet(null, t); + } + })); + } + startLatch.countDown(); + for (Future f : futures) { + f.get(); + } + executor.shutdown(); + assertEquals(expectedTotal, manager.getCollectors().size()); + } + public void testRelationVsTopDocsCount() throws Exception { Sort sort = new Sort(SortField.FIELD_SCORE, SortField.FIELD_DOC); try (Directory dir = newDirectory(); From 4368aa0859695e80df7c03d65a4d42b24484c965 Mon Sep 17 00:00:00 2001 From: Prudhvi Godithi Date: Thu, 26 Feb 2026 14:05:29 -0800 Subject: [PATCH 3/4] Fix forbiddenApis Signed-off-by: Prudhvi Godithi --- .../test/org/apache/lucene/search/TestTopFieldCollector.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java b/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java index 37f55e8067c9..441a81896558 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java @@ -52,6 +52,7 @@ import org.apache.lucene.tests.search.CheckHits; import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.lucene.tests.util.TestUtil; +import org.apache.lucene.util.NamedThreadFactory; public class TestTopFieldCollector extends LuceneTestCase { private IndexSearcher is; @@ -721,7 +722,8 @@ public void testConcurrentNewCollector() throws Exception { int numThreads = 8; int callsPerThread = 1000; int expectedTotal = numThreads * callsPerThread; - ExecutorService executor = Executors.newFixedThreadPool(numThreads); + ExecutorService executor = + Executors.newFixedThreadPool(numThreads, new NamedThreadFactory("testConcurrentNewCollector")); CountDownLatch startLatch = new CountDownLatch(1); AtomicReference error = new AtomicReference<>(); List> futures = new ArrayList<>(); From 7564d86544a73df237510ac16d0fd21aa4ff545c Mon Sep 17 00:00:00 2001 From: Prudhvi Godithi Date: Thu, 26 Feb 2026 14:11:44 -0800 Subject: [PATCH 4/4] Fix tidy Signed-off-by: Prudhvi Godithi --- .../test/org/apache/lucene/search/TestTopFieldCollector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java b/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java index 441a81896558..0ad511a9501c 100644 --- a/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java +++ b/lucene/core/src/test/org/apache/lucene/search/TestTopFieldCollector.java @@ -723,7 +723,8 @@ public void testConcurrentNewCollector() throws Exception { int callsPerThread = 1000; int expectedTotal = numThreads * callsPerThread; ExecutorService executor = - Executors.newFixedThreadPool(numThreads, new NamedThreadFactory("testConcurrentNewCollector")); + Executors.newFixedThreadPool( + numThreads, new NamedThreadFactory("testConcurrentNewCollector")); CountDownLatch startLatch = new CountDownLatch(1); AtomicReference error = new AtomicReference<>(); List> futures = new ArrayList<>();