From 32dbb7a4bb2b566b52b68b9684bb33b2a315b499 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 02:20:13 -0400 Subject: [PATCH 01/16] SOLR-13696: stop using commitWithin in RoutedAliasUpdateProcessorTest.addDocsAndCommit and re-enable the test Hypothetical, unrun regression test; see SOLR-13696-TESTING.md. --- .../RoutedAliasUpdateProcessorTest.java | 47 +++++-------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java index 888cba4c4ff..8feb8ecb158 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java @@ -31,8 +31,6 @@ import java.util.TreeSet; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.stream.Collectors; -import org.apache.lucene.tests.util.LuceneTestCase.AwaitsFix; import org.apache.solr.client.solrj.SolrRequest; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.CloudSolrClient; @@ -64,7 +62,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-13696") @Ignore // don't try to run abstract base class public abstract class RoutedAliasUpdateProcessorTest extends SolrCloudTestCase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -317,8 +314,6 @@ void addDocsAndCommit(boolean aliasOnly, SolrInputDocument... solrInputDocuments .get(getAlias())); } - int commitWithin = random().nextBoolean() ? -1 : 500; // if -1, we commit explicitly instead - if (random().nextBoolean()) { // Send in separate threads. Choose random collection & solrClient ExecutorService exec = null; @@ -330,7 +325,7 @@ void addDocsAndCommit(boolean aliasOnly, SolrInputDocument... solrInputDocuments List> futures = new ArrayList<>(solrInputDocuments.length); for (SolrInputDocument solrInputDocument : solrInputDocuments) { String col = collections.get(random().nextInt(collections.size())); - futures.add(exec.submit(() -> solrClient.add(col, solrInputDocument, commitWithin))); + futures.add(exec.submit(() -> solrClient.add(col, solrInputDocument))); } for (Future future : futures) { assertUpdateResponse(future.get()); @@ -347,33 +342,18 @@ void addDocsAndCommit(boolean aliasOnly, SolrInputDocument... solrInputDocuments // send in a batch. String col = collections.get(random().nextInt(collections.size())); CloudSolrClient solrClient = cluster.getSolrClient(); - assertUpdateResponse(solrClient.add(col, Arrays.asList(solrInputDocuments), commitWithin)); + assertUpdateResponse(solrClient.add(col, Arrays.asList(solrInputDocuments))); } - String col = collections.get(random().nextInt(collections.size())); - if (commitWithin == -1) { + // The docs may have been routed to any collection of the alias, so commit every one of them. + // This is an explicit commit rather than commitWithin: each replica opens its searcher on its + // own timer then, and a query could see the docs on one replica but not on another. + List aliasCollections = + new CollectionAdminRequest.ListAliases() + .process(getSolrClient()) + .getAliasesAsLists() + .get(getAlias()); + for (String col : aliasCollections) { getSolrClient().commit(col); - } else { - // check that it all got committed eventually - String docsQ = - "{!terms f=id}" - + Arrays.stream(solrInputDocuments) - .map(d -> d.getFieldValue("id").toString()) - .collect(Collectors.joining(",")); - int numDocs = queryNumDocs(docsQ); - if (numDocs == solrInputDocuments.length) { - System.err.println("Docs committed sooner than expected. Bug or slow test env?"); - return; - } - // wait until it's committed - Thread.sleep(commitWithin); - // Loop for up to 10 seconds waiting for commit to catch up - for (int idx = 0; idx < 100; ++idx) { - numDocs = queryNumDocs(docsQ); - if (numDocs == solrInputDocuments.length) break; - Thread.sleep(100); - } - - assertEquals("not committed. Bug or a slow test?", solrInputDocuments.length, numDocs); } } @@ -384,11 +364,6 @@ void assertUpdateResponse(UpdateResponse rsp) { assertTrue("Expected no errors: " + errors, errors == null || errors.isEmpty()); } - private int queryNumDocs(String q) throws SolrServerException, IOException { - return (int) - getSolrClient().query(getAlias(), params("q", q, "rows", "0")).getResults().getNumFound(); - } - /** Adds the docs to Solr via {@link #getSolrClient()} with the params */ @SuppressWarnings("SameParameterValue") protected UpdateResponse add( From 05ab4664dace5a8e5a60970bb97546d3ef028dfb Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 02:20:14 -0400 Subject: [PATCH 02/16] SOLR-13696: add hypothetical-reproduction handoff doc --- SOLR-13696-TESTING.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 SOLR-13696-TESTING.md diff --git a/SOLR-13696-TESTING.md b/SOLR-13696-TESTING.md new file mode 100644 index 00000000000..19ebd334fb4 --- /dev/null +++ b/SOLR-13696-TESTING.md @@ -0,0 +1,40 @@ +# SOLR-13696 - RoutedAliasUpdateProcessorTest: commitWithin check is not a commit barrier, test-only fix (not run) + +Nothing here was compiled or executed. Verify the guesses below first. + +## JIRA context +`DimensionalRoutedAliasUpdateProcessorTest.testTimeCat` failed with "expected:<16> but was:<15>" +right after `addDocsAndCommit` returned. The abstract base `RoutedAliasUpdateProcessorTest` (and so +`CategoryRoutedAliasUpdateProcessorTest` and `DimensionalRoutedAliasUpdateProcessorTest`) was marked +`@AwaitsFix`. Hossman's log analysis showed the searchers on the replicas opening at different +times. Gus Heck later wrote that the commitWithin part of the test was overzealous (commitWithin is +orthogonal to routed aliases) and that removing it simplifies the helper. + +## Finding +Half of the `addDocsAndCommit` runs send the docs with `commitWithin=500` and then poll an alias +query until all the docs are visible. Each replica opens its searcher on its own timer, and the +alias query lands on one replica per shard. The poll can succeed on replicas that have already +committed, return, and the very next assertion lands on a replica whose searcher is not open yet +(15 of 16 docs). + +The explicit-commit branch had a second gap: it committed one randomly chosen collection of the +alias, but the docs may have been routed to any collection of the alias. + +## Fix (test only) +`addDocsAndCommit` no longer uses `commitWithin`. After the adds it commits every collection of the +alias explicitly (a commit is distributed to all replicas and opens a new searcher by default). The +now-unused `queryNumDocs` helper and `Collectors` import are removed, and `@AwaitsFix` is removed +from the base class so its subclasses run again. `TimeRoutedAliasUpdateProcessorTest` keeps its own +`@AwaitsFix` for SOLR-13059, which is a separate "no core retrieved" race. + +## What was guessed / verify first +- That `@AwaitsFix` on the abstract base is inherited by the subclasses (the original commit message + says so); `CategoryRoutedAliasUpdateProcessorTest` and `DimensionalRoutedAliasUpdateProcessorTest` + have no annotation of their own. +- That `CollectionAdminRequest.ListAliases` always lists the alias in `getAliasesAsLists()` when + `aliasOnly` is true (it was already used for `aliasOnly=false`). +- That the two subclasses are stable now. Beast them (the ticket reported a steady stream of + failures); other timing issues such as SOLR-13059 may surface once they run again. +- Spotless formatting and unused imports. +- Fail-before is not meaningful: the failure was an intermittent race, so a revert would only + show it under beasting. From 1deddc51958e6230b7d1fe3012ce431af80c0225 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Thu, 8 Oct 2026 23:54:37 -0400 Subject: [PATCH 03/16] SOLR-13696: route the category test on a field the cluster configset defines Category tests used ship_name_en, which the server _default configset never defines, so autoCreateFields=false rejects every document. Use the string dynamic field ship_name_s. The explicit commit now fails with a clear message if the alias is not listed, instead of a null dereference. --- .../processor/CategoryRoutedAliasUpdateProcessorTest.java | 2 +- .../update/processor/RoutedAliasUpdateProcessorTest.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/CategoryRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/CategoryRoutedAliasUpdateProcessorTest.java index 6b3ddf6b9d2..0db706c13f7 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/CategoryRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/CategoryRoutedAliasUpdateProcessorTest.java @@ -50,7 +50,7 @@ public class CategoryRoutedAliasUpdateProcessorTest extends RoutedAliasUpdatePro "Constructor", "Heart of Gold", "Stunt Ship", "B-ark", "Bi$tromath" }; - private static final String categoryField = "ship_name_en"; + private static final String categoryField = "ship_name_s"; private static final String intField = "integer_i"; private int lastDocId = 0; diff --git a/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java index 8feb8ecb158..2538b99d13b 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java @@ -344,14 +344,14 @@ void addDocsAndCommit(boolean aliasOnly, SolrInputDocument... solrInputDocuments CloudSolrClient solrClient = cluster.getSolrClient(); assertUpdateResponse(solrClient.add(col, Arrays.asList(solrInputDocuments))); } - // The docs may have been routed to any collection of the alias, so commit every one of them. - // This is an explicit commit rather than commitWithin: each replica opens its searcher on its - // own timer then, and a query could see the docs on one replica but not on another. + // The docs may have been routed to any collection of the alias, so commit each of them. + // A commit waits for every replica to open a new searcher. commitWithin does not. List aliasCollections = new CollectionAdminRequest.ListAliases() .process(getSolrClient()) .getAliasesAsLists() .get(getAlias()); + assertNotNull("alias " + getAlias() + " is not listed", aliasCollections); for (String col : aliasCollections) { getSolrClient().commit(col); } From 89d05a2b6d9deb88bb8c9404a23e435fca6cf3e4 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 04:04:12 +0000 Subject: [PATCH 04/16] SOLR-13696: remove the handoff note for packaging --- SOLR-13696-TESTING.md | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 SOLR-13696-TESTING.md diff --git a/SOLR-13696-TESTING.md b/SOLR-13696-TESTING.md deleted file mode 100644 index 19ebd334fb4..00000000000 --- a/SOLR-13696-TESTING.md +++ /dev/null @@ -1,40 +0,0 @@ -# SOLR-13696 - RoutedAliasUpdateProcessorTest: commitWithin check is not a commit barrier, test-only fix (not run) - -Nothing here was compiled or executed. Verify the guesses below first. - -## JIRA context -`DimensionalRoutedAliasUpdateProcessorTest.testTimeCat` failed with "expected:<16> but was:<15>" -right after `addDocsAndCommit` returned. The abstract base `RoutedAliasUpdateProcessorTest` (and so -`CategoryRoutedAliasUpdateProcessorTest` and `DimensionalRoutedAliasUpdateProcessorTest`) was marked -`@AwaitsFix`. Hossman's log analysis showed the searchers on the replicas opening at different -times. Gus Heck later wrote that the commitWithin part of the test was overzealous (commitWithin is -orthogonal to routed aliases) and that removing it simplifies the helper. - -## Finding -Half of the `addDocsAndCommit` runs send the docs with `commitWithin=500` and then poll an alias -query until all the docs are visible. Each replica opens its searcher on its own timer, and the -alias query lands on one replica per shard. The poll can succeed on replicas that have already -committed, return, and the very next assertion lands on a replica whose searcher is not open yet -(15 of 16 docs). - -The explicit-commit branch had a second gap: it committed one randomly chosen collection of the -alias, but the docs may have been routed to any collection of the alias. - -## Fix (test only) -`addDocsAndCommit` no longer uses `commitWithin`. After the adds it commits every collection of the -alias explicitly (a commit is distributed to all replicas and opens a new searcher by default). The -now-unused `queryNumDocs` helper and `Collectors` import are removed, and `@AwaitsFix` is removed -from the base class so its subclasses run again. `TimeRoutedAliasUpdateProcessorTest` keeps its own -`@AwaitsFix` for SOLR-13059, which is a separate "no core retrieved" race. - -## What was guessed / verify first -- That `@AwaitsFix` on the abstract base is inherited by the subclasses (the original commit message - says so); `CategoryRoutedAliasUpdateProcessorTest` and `DimensionalRoutedAliasUpdateProcessorTest` - have no annotation of their own. -- That `CollectionAdminRequest.ListAliases` always lists the alias in `getAliasesAsLists()` when - `aliasOnly` is true (it was already used for `aliasOnly=false`). -- That the two subclasses are stable now. Beast them (the ticket reported a steady stream of - failures); other timing issues such as SOLR-13059 may surface once they run again. -- Spotless formatting and unused imports. -- Fail-before is not meaningful: the failure was an intermittent race, so a revert would only - show it under beasting. From f4f1187a5776a5b1f95b9c7ef7762c37f383d9b9 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 05:56:07 +0000 Subject: [PATCH 05/16] SOLR-13696: shut down each routed-alias test cluster after its test method --- .../processor/RoutedAliasUpdateProcessorTest.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java index 2538b99d13b..afb26694a28 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/RoutedAliasUpdateProcessorTest.java @@ -58,6 +58,7 @@ import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.update.UpdateCommand; +import org.junit.After; import org.junit.Ignore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,6 +69,18 @@ public abstract class RoutedAliasUpdateProcessorTest extends SolrCloudTestCase { private static final String intField = "integer_i"; + /** + * The subclasses create a fresh cluster in every test method (their {@code @Before} calls {@code + * configureCluster}), because each class routes to a fixed alias name whose collections cannot be + * shared between methods. The framework only shuts down the cluster still referenced when the + * class ends, so each method's cluster is shut down here; otherwise the earlier clusters stay + * open and their cores, clients and directories fail the suite's resource tracking checks. + */ + @After + public void shutdownMethodCluster() throws Exception { + shutdownCluster(); + } + void waitColAndAlias(String alias, String separator, final String suffix, int slices) throws InterruptedException { // collection to exist From 98ad9d3fcc33326433dbdb5f9f58011255a47069 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 13:33:22 +0000 Subject: [PATCH 06/16] SOLR-13696: put the top level router name and field in the V2 create alias message for dimensional aliases --- .../solr/handler/admin/api/CreateAlias.java | 28 +++++++++++++++++-- .../handler/admin/api/CreateAliasAPITest.java | 4 ++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java index ac054f118b7..3730aa9f681 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/api/CreateAlias.java @@ -20,6 +20,8 @@ import static org.apache.solr.client.solrj.request.beans.V2ApiConstants.COLLECTIONS; import static org.apache.solr.cloud.api.collections.RoutedAlias.CATEGORY; import static org.apache.solr.cloud.api.collections.RoutedAlias.CREATE_COLLECTION_PREFIX; +import static org.apache.solr.cloud.api.collections.RoutedAlias.DIMENSIONAL; +import static org.apache.solr.cloud.api.collections.RoutedAlias.ROUTER_FIELD; import static org.apache.solr.cloud.api.collections.RoutedAlias.ROUTER_TYPE_NAME; import static org.apache.solr.cloud.api.collections.RoutedAlias.TIME; import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.ROUTER_MAX_FUTURE; @@ -35,6 +37,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import org.apache.solr.client.api.endpoint.CreateAliasApi; @@ -123,10 +126,19 @@ public static ZkNodeProps createRemoteMessageForRoutedAlias(CreateAliasRequestBo remoteMessage.put(NAME, requestBody.name); if (requestBody.routers.size() > 1) { // Multi-dimensional alias + final List dimensionTypes = new ArrayList<>(); + final List dimensionFields = new ArrayList<>(); for (int i = 0; i < requestBody.routers.size(); i++) { - createValidationHelper(requestBody.routers.get(i)) - .addRemoteMessageProperties(remoteMessage, "router." + i + "."); + final RoutedAliasProperties router = requestBody.routers.get(i); + final RoutedAliasValidationHelper helper = createValidationHelper(router); + helper.addRemoteMessageProperties(remoteMessage, "router." + i + "."); + dimensionTypes.add(helper.getRouterType().name()); + dimensionFields.add(router.field); } + // The overseer also requires the top level router name and field, in the same shape + // that SolrJ sends for a dimensional alias. + remoteMessage.put(ROUTER_TYPE_NAME, DIMENSIONAL + String.join(",", dimensionTypes) + "]"); + remoteMessage.put(ROUTER_FIELD, String.join(",", dimensionFields)); } else if (requestBody.routers.size() == 1) { // Single dimensional alias createValidationHelper(requestBody.routers.get(0)) .addRemoteMessageProperties(remoteMessage, "router."); @@ -238,6 +250,8 @@ private interface RoutedAliasValidationHelper { void addRemoteMessageProperties(Map remoteMessage, String prefix); + RoutedAliasTypes getRouterType(); + default void ensureRequiredFieldPresent(Object val, String name) { if (val == null) { throw new SolrException( @@ -282,6 +296,11 @@ public void validate() { } } + @Override + public RoutedAliasTypes getRouterType() { + return RoutedAliasTypes.TIME; + } + @Override public void addRemoteMessageProperties(Map remoteMessage, String prefix) { remoteMessage.put(prefix + CoreAdminParams.NAME, TIME); @@ -327,6 +346,11 @@ public void validate() { ensureRequiredFieldPresent(aliasProperties.field, "'field' on category routed alias"); } + @Override + public RoutedAliasTypes getRouterType() { + return RoutedAliasTypes.CATEGORY; + } + @Override public void addRemoteMessageProperties(Map remoteMessage, String prefix) { remoteMessage.put(prefix + CoreAdminParams.NAME, CATEGORY); diff --git a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java index d44152becaa..4a20b0778c8 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/api/CreateAliasAPITest.java @@ -305,8 +305,10 @@ public void testRemoteMessageCreationForMultiDimensionalRoutedAlias() throws Exc validateRunCommand( CollectionParams.CollectionAction.CREATEALIAS, message -> { - assertEquals(10, message.size()); + assertEquals(12, message.size()); assertEquals("someAliasName", message.get("name")); + assertEquals("Dimensional[TIME,CATEGORY]", message.get("router.name")); + assertEquals("someField,someField", message.get("router.field")); assertEquals("time", message.get("router.0.name")); assertEquals("someField", message.get("router.0.field")); assertEquals("NOW/HOUR", message.get("router.0.start")); From a4e0da4223279d541f9ff8fff9cd4fbbcf02976c Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 14:23:47 +0000 Subject: [PATCH 07/16] SOLR-13696: parse the route value in TimeRoutedAlias.formattedRouteValues instead of casting it to Date --- .../apache/solr/cloud/api/collections/TimeRoutedAlias.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java index 4df16b611d2..ddc41b41b91 100644 --- a/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java +++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/TimeRoutedAlias.java @@ -218,8 +218,8 @@ public String computeInitialCollectionName() { @Override String[] formattedRouteValues(SolrInputDocument doc) { String routeField = getRouteField(); - Date fieldValue = (Date) doc.getFieldValue(routeField); - String dest = calcCandidateCollection(fieldValue.toInstant()).getDestinationCollection(); + Instant fieldValue = parseRouteKey(doc.getFieldValue(routeField)); + String dest = calcCandidateCollection(fieldValue).getDestinationCollection(); int nonValuePrefix = getAliasName().length() + getRoutedAliasType().getSeparatorPrefix().length(); return new String[] {dest.substring(nonValuePrefix)}; From 08f9384e47c016df903b1899d4bf136a5b7149e8 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 14:54:31 +0000 Subject: [PATCH 08/16] SOLR-13696: compute the too far in the future test document relative to now in the dimensional routed alias test --- .../DimensionalRoutedAliasUpdateProcessorTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java index e7166b15973..7fda446b2b1 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Date; import java.util.List; @@ -127,7 +128,7 @@ public void testTimeCat() throws Exception { ap("tabby", "calico")); testFailedDocument("shorthair", "2017-10-23T00:00:00Z", "couldn't be routed"); - testFailedDocument("shorthair", "2020-10-23T00:00:00Z", "too far in the future"); + testFailedDocument("shorthair", tooFarInTheFuture(), "too far in the future"); testFailedDocument(null, "2019-07-02T00:00:00Z", "Route value is null"); testFailedDocument("foo__CRA__bar", "2019-07-02T00:00:00Z", "7 character sequence __CRA__"); testFailedDocument("fóóCRAóóbar", "2019-07-02T00:00:00Z", "7 character sequence __CRA__"); @@ -349,7 +350,7 @@ public void testCatTime() throws Exception { ap("tabby", "calico")); testFailedDocument("shorthair", "2017-10-23T00:00:00Z", "couldn't be routed"); - testFailedDocument("shorthair", "2020-10-23T00:00:00Z", "too far in the future"); + testFailedDocument("shorthair", tooFarInTheFuture(), "too far in the future"); testFailedDocument(null, "2019-07-02T00:00:00Z", "Route value is null"); testFailedDocument("foo__CRA__bar", "2019-07-02T00:00:00Z", "7 character sequence __CRA__"); testFailedDocument("fóóCRAóóbar", "2019-07-02T00:00:00Z", "7 character sequence __CRA__"); @@ -603,6 +604,14 @@ private void assertCatTimeInvariants(String[] expectedCols, String[] categories) assertEquals("COLS FOUND:" + cols, expectedCols.length, cols.size()); } + // Computed relative to now so the document is always beyond router.maxFutureMs and is + // rejected by TimeRoutedAlias.validateRouteValue. A fixed "future" date eventually becomes + // the past; the document is then routed instead, and creating the collections between the + // alias start and that date one day at a time stalls the update until it times out. + private static String tooFarInTheFuture() { + return Instant.now().plus(30, ChronoUnit.DAYS).toString(); + } + private void testFailedDocument(String category, String timestamp, String errorMsg) throws SolrServerException, IOException { try { From 1d0b8a0a73cd10f4968f57985af93b450c484aca Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 15:26:57 +0000 Subject: [PATCH 09/16] SOLR-13696: ask for the replica URL form of [shard] in the dimensional routed alias placement check --- ...imensionalRoutedAliasUpdateProcessorTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java index 7fda446b2b1..26000cec1fd 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/DimensionalRoutedAliasUpdateProcessorTest.java @@ -279,18 +279,22 @@ Here we need to be testing that something that should not be created (extra pree ap("shorthair", "tabby", "calico")); // verify that all the documents ended up in the right collections. + // [shard] is requested with style='urls' because the replica core URLs + // contain the collection name, which encodes the category and the day + // each document should have been routed to. The default [shard] style + // returns only the shard id, which names neither. QueryResponse resp = solrClient.query( getAlias(), params( "q", "*:*", "rows", "100", - "fl", "*,[shard]", + "fl", "*,shard:[shard style='urls']", "sort", "id asc")); SolrDocumentList results = resp.getResults(); assertEquals(18, results.getNumFound()); for (SolrDocument result : results) { - String shard = String.valueOf(result.getFieldValue("[shard]")); + String shard = String.valueOf(result.getFieldValue("shard")); String cat = String.valueOf(result.getFieldValue("cat_s")); Date date = (Date) result.getFieldValue("timestamp_dt"); String day = date.toInstant().toString().split("T")[0]; @@ -503,18 +507,22 @@ Here we need to be testing that something that should not be created (extra pree ap("shorthair", "tabby", "calico")); // verify that all the documents ended up in the right collections. + // [shard] is requested with style='urls' because the replica core URLs + // contain the collection name, which encodes the category and the day + // each document should have been routed to. The default [shard] style + // returns only the shard id, which names neither. QueryResponse resp = solrClient.query( getAlias(), params( "q", "*:*", "rows", "100", - "fl", "*,[shard]", + "fl", "*,shard:[shard style='urls']", "sort", "id asc")); SolrDocumentList results = resp.getResults(); assertEquals(18, results.getNumFound()); for (SolrDocument result : results) { - String shard = String.valueOf(result.getFieldValue("[shard]")); + String shard = String.valueOf(result.getFieldValue("shard")); String cat = String.valueOf(result.getFieldValue("cat_s")); Date date = (Date) result.getFieldValue("timestamp_dt"); String day = date.toInstant().toString().split("T")[0]; From da4fa6df11784ce5a83bc7e74ec7b6aa78f689b9 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 16:10:11 +0000 Subject: [PATCH 10/16] SOLR-13696: add changelog fragment for the two routed alias production fixes --- .../unreleased/SOLR-13696-dimensional-routed-alias.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/SOLR-13696-dimensional-routed-alias.yml diff --git a/changelog/unreleased/SOLR-13696-dimensional-routed-alias.yml b/changelog/unreleased/SOLR-13696-dimensional-routed-alias.yml new file mode 100644 index 00000000000..c3b42b1f669 --- /dev/null +++ b/changelog/unreleased/SOLR-13696-dimensional-routed-alias.yml @@ -0,0 +1,7 @@ +title: Creating a dimensional routed alias through the V2 collections API no longer fails for missing router parameters, and time routed alias routing no longer throws a ClassCastException when a document timestamp is still a String +type: fixed +authors: + - name: Nick Shanin +links: + - name: SOLR-13696 + url: https://issues.apache.org/jira/browse/SOLR-13696 From 21e4407d79fcd684e78de32ddf3118a91a506221 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 18 Oct 2026 02:56:00 +0000 Subject: [PATCH 11/16] SOLR-13059: settle the preemptive creation snapshot instead of asserting it mid-creation --- .../TimeRoutedAliasUpdateProcessorTest.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java index b0d0097b6d3..0587a305ad3 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java @@ -343,7 +343,7 @@ public void testPreemptiveCreation() throws Exception { ExecutorUtil.newMDCAwareCachedThreadPool( "TimeRoutedAliasProcessorTestx-testPreemptiveCreation"); - Exception[] threadExceptions = new Exception[2]; + Throwable[] threadExceptions = new Throwable[2]; boolean[] threadStarted = new boolean[2]; boolean[] threadFinished = new boolean[2]; try { @@ -354,8 +354,8 @@ public void testPreemptiveCreation() throws Exception { try { starter.await(); concurrentUpdates(params, alias); - } catch (Exception e) { - threadExceptions[0] = e; + } catch (Throwable t) { + threadExceptions[0] = t; } threadFinished[0] = true; }); @@ -366,8 +366,8 @@ public void testPreemptiveCreation() throws Exception { try { starter.await(); concurrentUpdates(params, alias2); - } catch (Exception e) { - threadExceptions[1] = e; + } catch (Throwable t) { + threadExceptions[1] = t; } threadFinished[1] = true; }); @@ -377,10 +377,13 @@ public void testPreemptiveCreation() throws Exception { } // threads are known to be terminated by now, check for exceptions - for (Exception threadException : threadExceptions) { + for (Throwable threadException : threadExceptions) { if (threadException != null) { Thread.sleep(5000); // avoid spurious fails due to TRA thread not done yet - throw threadException; + if (threadException instanceof Error) { + throw (Error) threadException; + } + throw (Exception) threadException; } } @@ -705,13 +708,6 @@ private void concurrentUpdates(ModifiableSolrParams params, String alias) params)); assertUpdateResponse(solrClient.commit(alias)); - List colsT1; - colsT1 = - new CollectionAdminRequest.ListAliases().process(solrClient).getAliasesAsLists().get(alias); - assertEquals(3, colsT1.size()); - assertFalse( - "Preemptive creation appears to not be asynchronous anymore", - colsT1.contains("myalias" + TRA + "2017-10-26")); assertNumDocs("2017-10-23", 1, alias); assertNumDocs("2017-10-24", 1, alias); assertNumDocs("2017-10-25", 3, alias); From 424c52bb029cbf7545cfc83eb6b63594e4d59fea Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 18 Oct 2026 04:53:17 +0000 Subject: [PATCH 12/16] SOLR-13059: settle the preemptive creation count, bound the racing count, and remove the class-level skip --- .../processor/TimeRoutedAliasUpdateProcessorTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java index 0587a305ad3..2020c351569 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java @@ -40,7 +40,6 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; -import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.BaseHttpClusterStateProvider; import org.apache.solr.client.solrj.impl.CloudSolrClient; @@ -72,7 +71,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -@LuceneTestCase.AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-13059") // TBD public class TimeRoutedAliasUpdateProcessorTest extends RoutedAliasUpdateProcessorTest { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -599,7 +597,11 @@ public void testPreemptiveCreation() throws Exception { cols = new CollectionAdminRequest.ListAliases().process(solrClient).getAliasesAsLists().get(alias); - assertEquals(8, cols.size()); + assertTrue( + "preemptive creation of 2017-10-31 may or may not have finished, but nothing beyond it " + + "may exist yet: " + + cols, + cols.size() == 8 || cols.size() == 9); assertNumDocs("2017-10-23", 1, alias); assertNumDocs("2017-10-24", 1, alias); assertNumDocs("2017-10-25", 7, alias); From 7279285697d9aa15599b75420c80655eeaf7144f Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 18 Oct 2026 05:13:57 +0000 Subject: [PATCH 13/16] SOLR-13059: keep the asynchronous proof as a bounded either-or check instead of dropping it --- .../processor/TimeRoutedAliasUpdateProcessorTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java index 2020c351569..c63786fdceb 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java @@ -710,6 +710,12 @@ private void concurrentUpdates(ModifiableSolrParams params, String alias) params)); assertUpdateResponse(solrClient.commit(alias)); + List colsT1 = + new CollectionAdminRequest.ListAliases().process(solrClient).getAliasesAsLists().get(alias); + assertTrue( + "preemptive creation of 2017-10-26 is either still in flight (3 collections) or finished (4)" + + colsT1, + colsT1.contains("myalias" + TRA + "2017-10-26") ? colsT1.size() == 4 : colsT1.size() == 3); assertNumDocs("2017-10-23", 1, alias); assertNumDocs("2017-10-24", 1, alias); assertNumDocs("2017-10-25", 3, alias); From 6914eae1c2edd8d2ff79dfac6a64282a94e0937b Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 18 Oct 2026 06:00:21 +0000 Subject: [PATCH 14/16] SOLR-13059: the preemptive checks use the alias they were given, not the literal myalias --- .../update/processor/TimeRoutedAliasUpdateProcessorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java index c63786fdceb..ede7919562a 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java @@ -715,7 +715,7 @@ private void concurrentUpdates(ModifiableSolrParams params, String alias) assertTrue( "preemptive creation of 2017-10-26 is either still in flight (3 collections) or finished (4)" + colsT1, - colsT1.contains("myalias" + TRA + "2017-10-26") ? colsT1.size() == 4 : colsT1.size() == 3); + colsT1.contains(alias + TRA + "2017-10-26") ? colsT1.size() == 4 : colsT1.size() == 3); assertNumDocs("2017-10-23", 1, alias); assertNumDocs("2017-10-24", 1, alias); assertNumDocs("2017-10-25", 3, alias); @@ -744,7 +744,7 @@ private void checkPreemptiveCase1(String alias) throws SolrServerException, IOEx assertFalse( "Preemptive creation happened twice and created a collection " + "further in the future than the configured time slice!", - cols.contains("myalias" + TRA + "2017-10-27")); + cols.contains(alias + TRA + "2017-10-27")); validateCollectionCountAndAvailability( alias, 4, "Only 4 cols expected (preemptive create happened" + "twice among threads"); From 7d3fb3b74b15246b559c1236d7da53902c0c473d Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 18 Oct 2026 22:27:56 +0000 Subject: [PATCH 15/16] SOLR-13059: pin the ninth collection to 2017-10-31 and use the alias constant in the 11-03 check --- .../processor/TimeRoutedAliasUpdateProcessorTest.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java index ede7919562a..587d76e23df 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java @@ -602,6 +602,11 @@ public void testPreemptiveCreation() throws Exception { + "may exist yet: " + cols, cols.size() == 8 || cols.size() == 9); + assertEquals( + "the only collection that can follow 2017-10-30 is 2017-10-31", + cols.size() == 9, + cols.contains(alias + TRA + "2017-10-31")); + assertFalse(cols.contains(alias + TRA + "2017-11-01")); assertNumDocs("2017-10-23", 1, alias); assertNumDocs("2017-10-24", 1, alias); assertNumDocs("2017-10-25", 7, alias); @@ -669,7 +674,7 @@ public void testPreemptiveCreation() throws Exception { cols = new CollectionAdminRequest.ListAliases().process(solrClient).getAliasesAsLists().get(alias); - assertFalse(cols.contains("myalias" + TRA + "2017-11-03")); + assertFalse(cols.contains(alias + TRA + "2017-11-03")); assertUpdateResponse( add( From 960355fce36ff254611190b21b8ceaf3fc887a0e Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 19 Oct 2026 00:36:30 +0000 Subject: [PATCH 16/16] SOLR-13059: hold preemptive creation at a test latch so the snapshot is deterministic --- .../solr/cloud/api/collections/RoutedAlias.java | 7 ++++++- .../java/org/apache/solr/util/TestInjection.java | 14 ++++++++++++++ .../TimeRoutedAliasUpdateProcessorTest.java | 12 ++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java b/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java index 4e2ab6f591b..49cbb049fe9 100644 --- a/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java +++ b/solr/core/src/java/org/apache/solr/cloud/api/collections/RoutedAlias.java @@ -44,6 +44,7 @@ import org.apache.solr.handler.admin.CollectionsHandler; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.update.AddUpdateCommand; +import org.apache.solr.util.TestInjection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -433,7 +434,11 @@ private String doPreemptive( private void preemptiveAsync(Runnable r, SolrCore core) { preemptiveCreateOnceAlready = true; - core.runAsync(r); + core.runAsync( + () -> { + TestInjection.injectPreemptiveCreateLatch(); + r.run(); + }); } private SolrException unknownCreateType() { diff --git a/solr/core/src/java/org/apache/solr/util/TestInjection.java b/solr/core/src/java/org/apache/solr/util/TestInjection.java index 3ca9e5d78a6..9446b74cc80 100644 --- a/solr/core/src/java/org/apache/solr/util/TestInjection.java +++ b/solr/core/src/java/org/apache/solr/util/TestInjection.java @@ -143,6 +143,8 @@ static Random random() { // non-private for testing public static volatile CountDownLatch reindexLatch = null; + public static volatile CountDownLatch preemptiveCreateLatch = null; + public static volatile String reindexFailure = null; public static volatile String failIndexFingerprintRequests = null; @@ -213,6 +215,7 @@ public static void reset() { splitLatch = null; directUpdateLatch = null; reindexLatch = null; + preemptiveCreateLatch = null; reindexFailure = null; prepRecoveryOpPauseForever = null; countPrepRecoveryOpPauseForever = new AtomicInteger(0); @@ -631,6 +634,17 @@ public static boolean injectReindexLatch() { return true; } + public static void injectPreemptiveCreateLatch() { + CountDownLatch latch = preemptiveCreateLatch; + if (latch != null) { + try { + latch.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + private static Pair parseValue(final String raw) { if (raw == null) return new Pair<>(false, 0); Matcher m = ENABLED_PERCENT.matcher(raw); diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java index 587d76e23df..9cbc272ead0 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasUpdateProcessorTest.java @@ -62,6 +62,7 @@ import org.apache.solr.update.UpdateCommand; import org.apache.solr.util.DateMathParser; import org.apache.solr.util.LogLevel; +import org.apache.solr.util.TestInjection; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; @@ -341,6 +342,7 @@ public void testPreemptiveCreation() throws Exception { ExecutorUtil.newMDCAwareCachedThreadPool( "TimeRoutedAliasProcessorTestx-testPreemptiveCreation"); + TestInjection.preemptiveCreateLatch = new CountDownLatch(2); Throwable[] threadExceptions = new Throwable[2]; boolean[] threadStarted = new boolean[2]; boolean[] threadFinished = new boolean[2]; @@ -372,6 +374,7 @@ public void testPreemptiveCreation() throws Exception { starter.countDown(); } finally { ExecutorUtil.shutdownAndAwaitTermination(executorService); + TestInjection.preemptiveCreateLatch = null; } // threads are known to be terminated by now, check for exceptions @@ -717,10 +720,11 @@ private void concurrentUpdates(ModifiableSolrParams params, String alias) List colsT1 = new CollectionAdminRequest.ListAliases().process(solrClient).getAliasesAsLists().get(alias); - assertTrue( - "preemptive creation of 2017-10-26 is either still in flight (3 collections) or finished (4)" - + colsT1, - colsT1.contains(alias + TRA + "2017-10-26") ? colsT1.size() == 4 : colsT1.size() == 3); + assertFalse( + "preemptive creation of 2017-10-26 must still be held by the test latch" + colsT1, + colsT1.contains(alias + TRA + "2017-10-26")); + assertEquals(3, colsT1.size()); + TestInjection.preemptiveCreateLatch.countDown(); assertNumDocs("2017-10-23", 1, alias); assertNumDocs("2017-10-24", 1, alias); assertNumDocs("2017-10-25", 3, alias);