From 32dbb7a4bb2b566b52b68b9684bb33b2a315b499 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 02:20:13 -0400 Subject: [PATCH 01/23] 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 888cba4c4ffc..8feb8ecb158b 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/23] 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 000000000000..19ebd334fb47 --- /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/23] 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 6b3ddf6b9d20..0db706c13f7d 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 8feb8ecb158b..2538b99d13bf 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/23] 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 19ebd334fb47..000000000000 --- 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/23] 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 2538b99d13bf..afb26694a28d 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/23] 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 ac054f118b77..3730aa9f681f 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 d44152becaa7..4a20b0778c88 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/23] 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 4df16b611d20..ddc41b41b915 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/23] 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 e7166b159739..7fda446b2b12 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/23] 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 7fda446b2b12..26000cec1fdc 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 dd83fdcccb6132c437f1dbb323366e1601b6e109 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 02:56:12 -0400 Subject: [PATCH 10/23] SOLR-13943: SOLR-13943: testDateMathInStart polls the cluster state provider for the rewritten router start Hypothetical, unrun regression test; see SOLR-13943-TESTING.md. --- .../TimeRoutedAliasUpdateProcessorTest.java | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 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 b0d0097b6d3d..434a715ac283 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,6 +40,7 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.BaseHttpClusterStateProvider; @@ -59,10 +60,12 @@ import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.ExecutorUtil; +import org.apache.solr.common.util.TimeSource; import org.apache.solr.common.util.Utils; import org.apache.solr.update.UpdateCommand; import org.apache.solr.util.DateMathParser; import org.apache.solr.util.LogLevel; +import org.apache.solr.util.TimeOut; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; @@ -963,7 +966,6 @@ public void testMaxFutureMs() throws Exception { } } - @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/SOLR-13943") @Test public void testDateMathInStart() throws Exception { ClusterStateProvider clusterStateProvider = solrClient.getClusterStateProvider(); @@ -993,8 +995,6 @@ public void testDateMathInStart() throws Exception { if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); } - aliasUpdate = new CountDownLatch(1); - monitorAlias(aliasUpdate); ModifiableSolrParams params = params(); String nowDay = @@ -1014,23 +1014,33 @@ public void testDateMathInStart() throws Exception { // this process should have lead to the modification of the start time for the alias, converting // it into a parsable date, removing the DateMath - // what we test next happens in a separate thread, so we have to give it some time to happen - aliasUpdate.await(); - if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { - ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); - } - - String hopeFullyModified = - clusterStateProvider.getAliasProperties(getAlias()).get(ROUTER_START); - try { - Instant.parse(hopeFullyModified); - } catch (DateTimeParseException e) { - fail( - ROUTER_START - + " should not have any date math by this point and parse as an instant. Using " - + aClass - + " Found:" - + hopeFullyModified); + // what we test next happens in a separate thread, so we have to give it some time to happen. + // Our own watcher on /aliases.json may fire before the provider's ZkStateReader has refreshed + // its aliases, so poll the provider itself instead of waiting on a latch. + final TimeOut timeOut = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME); + String hopeFullyModified; + while (true) { + if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { + ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); + } + hopeFullyModified = clusterStateProvider.getAliasProperties(getAlias()).get(ROUTER_START); + if (hopeFullyModified != null) { + try { + Instant.parse(hopeFullyModified); + break; + } catch (DateTimeParseException e) { + // still has date math, try again + } + } + if (timeOut.hasTimedOut()) { + fail( + ROUTER_START + + " should not have any date math by this point and parse as an instant. Using " + + aClass + + " Found:" + + hopeFullyModified); + } + timeOut.sleep(100); } } From 7e4df21a2e636339616b80b277589d3a52b7e0fd Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 02:56:14 -0400 Subject: [PATCH 11/23] SOLR-13943: add hypothetical-reproduction handoff doc --- SOLR-13943-TESTING.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 SOLR-13943-TESTING.md diff --git a/SOLR-13943-TESTING.md b/SOLR-13943-TESTING.md new file mode 100644 index 000000000000..d673ee686a7c --- /dev/null +++ b/SOLR-13943-TESTING.md @@ -0,0 +1,31 @@ +# SOLR-13943 - test-only fix (not run) + +Nothing here was compiled or executed. The change was written by reading `upstream/main`; +treat every claim below as a guess to verify first. + +## JIRA context +`TimeRoutedAliasUpdateProcessorTest.testDateMathInStart` "multi-threaded race condition due to ZK +assumptions" (Open, assigned to Gus Heck 2019, no follow-up). Hoss's analysis in the ticket: the +test waits on its own watcher on `/aliases.json`, but then reads `router.start` through the +`ClusterStateProvider`, whose `ZkStateReader`/`AliasesManager` may not have refreshed yet, so it +sometimes still sees `2019-09-14T03:00:00Z/DAY`. He proposed either using the watcher's data or +only proceeding once the reader knows the new aliases. The method has been `@AwaitsFix` since. + +## Change +- After the document is added, the test polls the provider (re-resolving the alias for the HTTP + provider) for up to 30 seconds until `router.start` parses as an `Instant`, instead of waiting + on a second latch and reading once. +- The second `monitorAlias` latch is gone (it is what raced); the first one, which waits for the + alias creation, is unchanged. +- The method level `@AwaitsFix(SOLR-13943)` is removed. + +## What was guessed / verify first +- The whole class is still `@AwaitsFix(SOLR-13059)` ("TBD"), so this test will not run in CI until + that is lifted. Verify with `-Dtests.awaitsfix=true`. +- That the start rewrite always happens: if no rewrite ever occurs (a real bug rather than a race) + the loop times out after 30 s with the old failure message. +- `getAliasProperties` may have no `router.start` entry (null) before the alias is visible; that + case is retried like a start that still has date math. +- Fail-before is not meaningful for a race fix; use repeated runs of this method with the old + and new test body and a fixed seed such as the one in the ticket (`8879E35521A4B9EA`). +- Spotless formatting. From 12e660da33343046912ee24a1124d67193e07363 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 17:41:16 +0000 Subject: [PATCH 12/23] SOLR-13943: remove hypothetical-reproduction handoff doc --- SOLR-13943-TESTING.md | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 SOLR-13943-TESTING.md diff --git a/SOLR-13943-TESTING.md b/SOLR-13943-TESTING.md deleted file mode 100644 index d673ee686a7c..000000000000 --- a/SOLR-13943-TESTING.md +++ /dev/null @@ -1,31 +0,0 @@ -# SOLR-13943 - test-only fix (not run) - -Nothing here was compiled or executed. The change was written by reading `upstream/main`; -treat every claim below as a guess to verify first. - -## JIRA context -`TimeRoutedAliasUpdateProcessorTest.testDateMathInStart` "multi-threaded race condition due to ZK -assumptions" (Open, assigned to Gus Heck 2019, no follow-up). Hoss's analysis in the ticket: the -test waits on its own watcher on `/aliases.json`, but then reads `router.start` through the -`ClusterStateProvider`, whose `ZkStateReader`/`AliasesManager` may not have refreshed yet, so it -sometimes still sees `2019-09-14T03:00:00Z/DAY`. He proposed either using the watcher's data or -only proceeding once the reader knows the new aliases. The method has been `@AwaitsFix` since. - -## Change -- After the document is added, the test polls the provider (re-resolving the alias for the HTTP - provider) for up to 30 seconds until `router.start` parses as an `Instant`, instead of waiting - on a second latch and reading once. -- The second `monitorAlias` latch is gone (it is what raced); the first one, which waits for the - alias creation, is unchanged. -- The method level `@AwaitsFix(SOLR-13943)` is removed. - -## What was guessed / verify first -- The whole class is still `@AwaitsFix(SOLR-13059)` ("TBD"), so this test will not run in CI until - that is lifted. Verify with `-Dtests.awaitsfix=true`. -- That the start rewrite always happens: if no rewrite ever occurs (a real bug rather than a race) - the loop times out after 30 s with the old failure message. -- `getAliasProperties` may have no `router.start` entry (null) before the alias is visible; that - case is retried like a start that still has date math. -- Fail-before is not meaningful for a race fix; use repeated runs of this method with the old - and new test body and a fixed seed such as the one in the ticket (`8879E35521A4B9EA`). -- Spotless formatting. From ae1d96e69290d5a406bb74168d0c10cc0299c829 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Mon, 5 Oct 2026 17:41:16 +0000 Subject: [PATCH 13/23] SOLR-13943: add changelog entry --- changelog/unreleased/SOLR-13943.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/SOLR-13943.yml diff --git a/changelog/unreleased/SOLR-13943.yml b/changelog/unreleased/SOLR-13943.yml new file mode 100644 index 000000000000..17e1e2f0e17d --- /dev/null +++ b/changelog/unreleased/SOLR-13943.yml @@ -0,0 +1,7 @@ +title: "Fix the race in TimeRoutedAliasUpdateProcessorTest.testDateMathInStart: poll the cluster state provider for the rewritten router.start instead of reading it once after the test's own ZooKeeper watcher fires" +type: fixed +authors: + - name: Nick Shanin +links: + - name: SOLR-13943 + url: https://issues.apache.org/jira/browse/SOLR-13943 From dd8f96703cb43ec9aa976c921e2f00eaf38cfe6a Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Thu, 8 Oct 2026 13:21:51 +0000 Subject: [PATCH 14/23] SOLR-13943: remove changelog entry (test-only change) --- changelog/unreleased/SOLR-13943.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 changelog/unreleased/SOLR-13943.yml diff --git a/changelog/unreleased/SOLR-13943.yml b/changelog/unreleased/SOLR-13943.yml deleted file mode 100644 index 17e1e2f0e17d..000000000000 --- a/changelog/unreleased/SOLR-13943.yml +++ /dev/null @@ -1,7 +0,0 @@ -title: "Fix the race in TimeRoutedAliasUpdateProcessorTest.testDateMathInStart: poll the cluster state provider for the rewritten router.start instead of reading it once after the test's own ZooKeeper watcher fires" -type: fixed -authors: - - name: Nick Shanin -links: - - name: SOLR-13943 - url: https://issues.apache.org/jira/browse/SOLR-13943 From cc155cf68e1d8e79f1bcecd2e4c25ada864d8f53 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 15:51:07 +0000 Subject: [PATCH 15/23] SOLR-13943: move testDateMathInStart into TimeRoutedAliasDateMathInStartTest so it runs in normal mode --- .../TimeRoutedAliasDateMathInStartTest.java | 176 ++++++++++++++++++ .../TimeRoutedAliasUpdateProcessorTest.java | 102 ---------- 2 files changed, 176 insertions(+), 102 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasDateMathInStartTest.java diff --git a/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasDateMathInStartTest.java b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasDateMathInStartTest.java new file mode 100644 index 000000000000..60008cff3ae8 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/update/processor/TimeRoutedAliasDateMathInStartTest.java @@ -0,0 +1,176 @@ +/* + * 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.solr.update.processor; + +import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.ROUTER_START; + +import java.lang.invoke.MethodHandles; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.apache.solr.client.solrj.impl.BaseHttpClusterStateProvider; +import org.apache.solr.client.solrj.impl.CloudSolrClient; +import org.apache.solr.client.solrj.impl.ClusterStateProvider; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.TimeSource; +import org.apache.solr.util.DateMathParser; +import org.apache.solr.util.TimeOut; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.data.Stat; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests date math in the start parameter of a time routed alias: after a document is added, the + * stored start is rewritten without date math so it parses as an instant. + */ +public class TimeRoutedAliasDateMathInStartTest extends RoutedAliasUpdateProcessorTest { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final String alias = "myalias"; + private static final String timeField = "timestamp_dt"; + + private CloudSolrClient solrClient; + + @Before + public void doBefore() throws Exception { + configureCluster(4).configure(); + solrClient = cluster.getSolrClient(); + // log this to help debug potential causes of problems + if (log.isInfoEnabled()) { + log.info("SolrClient: {}", solrClient); + log.info("ClusterStateProvider {}", solrClient.getClusterStateProvider()); // nowarn + } + } + + @Override + public String getAlias() { + return alias; + } + + @Override + public CloudSolrClient getSolrClient() { + return solrClient; + } + + private String getTimeField() { + return timeField; + } + + @Test + public void testDateMathInStart() throws Exception { + ClusterStateProvider clusterStateProvider = solrClient.getClusterStateProvider(); + Class aClass = clusterStateProvider.getClass(); + System.out.println("CSPROVIDER:" + aClass); + + // This test prevents recurrence of SOLR-13760 + + String configName = getSaferTestName(); + createConfigSet(configName); + CountDownLatch aliasUpdate = new CountDownLatch(1); + monitorAlias(aliasUpdate); + + // each collection has 4 shards with 3 replicas for 12 possible destinations + // 4 of which are leaders, and 8 of which should fail this test. + final int numShards = 1 + random().nextInt(4); + final int numReplicas = 1 + random().nextInt(3); + CollectionAdminRequest.createTimeRoutedAlias( + alias, + "2019-09-14T03:00:00Z/DAY", + "+1DAY", + getTimeField(), + CollectionAdminRequest.createCollection("_unused_", configName, numShards, numReplicas)) + .process(solrClient); + + aliasUpdate.await(); + if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { + ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); + } + + ModifiableSolrParams params = params(); + String nowDay = + DateTimeFormatter.ISO_INSTANT.format( + DateMathParser.parseMath(new Date(), "2019-09-14T01:00:00Z").toInstant()); + assertUpdateResponse( + add( + alias, + Arrays.asList( + sdoc( + "id", + "1", + "timestamp_dt", + nowDay)), // should not cause preemptive creation of 10-28 now + params)); + + // this process should have lead to the modification of the start time for the alias, converting + // it into a parsable date, removing the DateMath + + // what we test next happens in a separate thread, so we have to give it some time to happen. + // Our own watcher on /aliases.json may fire before the provider's ZkStateReader has refreshed + // its aliases, so poll the provider itself instead of waiting on a latch. + final TimeOut timeOut = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME); + String hopeFullyModified; + while (true) { + if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { + ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); + } + hopeFullyModified = clusterStateProvider.getAliasProperties(getAlias()).get(ROUTER_START); + if (hopeFullyModified != null) { + try { + Instant.parse(hopeFullyModified); + break; + } catch (DateTimeParseException e) { + // still has date math, try again + } + } + if (timeOut.hasTimedOut()) { + fail( + ROUTER_START + + " should not have any date math by this point and parse as an instant. Using " + + aClass + + " Found:" + + hopeFullyModified); + } + timeOut.sleep(100); + } + } + + private void monitorAlias(CountDownLatch aliasUpdate) + throws KeeperException, InterruptedException { + Stat stat = new Stat(); + zkClient() + .getData( + "/aliases.json", + new Watcher() { + @Override + public void process(WatchedEvent watchedEvent) { + aliasUpdate.countDown(); + } + }, + stat); + } +} 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 434a715ac283..3c05ce9a20a1 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 @@ -19,7 +19,6 @@ import static org.apache.solr.client.solrj.request.RoutedAliasTypes.TIME; import static org.apache.solr.cloud.api.collections.RoutedAlias.ROUTED_ALIAS_NAME_CORE_PROP; -import static org.apache.solr.cloud.api.collections.TimeRoutedAlias.ROUTER_START; import static org.apache.solr.common.cloud.ZkStateReader.COLLECTIONS_ZKNODE; import static org.apache.solr.common.cloud.ZkStateReader.COLLECTION_PROPS_ZKNODE; @@ -30,7 +29,6 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; @@ -40,7 +38,6 @@ import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.impl.BaseHttpClusterStateProvider; @@ -60,16 +57,10 @@ import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.ExecutorUtil; -import org.apache.solr.common.util.TimeSource; import org.apache.solr.common.util.Utils; import org.apache.solr.update.UpdateCommand; import org.apache.solr.util.DateMathParser; import org.apache.solr.util.LogLevel; -import org.apache.solr.util.TimeOut; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.WatchedEvent; -import org.apache.zookeeper.Watcher; -import org.apache.zookeeper.data.Stat; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; @@ -966,99 +957,6 @@ public void testMaxFutureMs() throws Exception { } } - @Test - public void testDateMathInStart() throws Exception { - ClusterStateProvider clusterStateProvider = solrClient.getClusterStateProvider(); - Class aClass = clusterStateProvider.getClass(); - System.out.println("CSPROVIDER:" + aClass); - - // This test prevents recurrence of SOLR-13760 - - String configName = getSaferTestName(); - createConfigSet(configName); - CountDownLatch aliasUpdate = new CountDownLatch(1); - monitorAlias(aliasUpdate); - - // each collection has 4 shards with 3 replicas for 12 possible destinations - // 4 of which are leaders, and 8 of which should fail this test. - final int numShards = 1 + random().nextInt(4); - final int numReplicas = 1 + random().nextInt(3); - CollectionAdminRequest.createTimeRoutedAlias( - alias, - "2019-09-14T03:00:00Z/DAY", - "+1DAY", - getTimeField(), - CollectionAdminRequest.createCollection("_unused_", configName, numShards, numReplicas)) - .process(solrClient); - - aliasUpdate.await(); - if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { - ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); - } - - ModifiableSolrParams params = params(); - String nowDay = - DateTimeFormatter.ISO_INSTANT.format( - DateMathParser.parseMath(new Date(), "2019-09-14T01:00:00Z").toInstant()); - assertUpdateResponse( - add( - alias, - Arrays.asList( - sdoc( - "id", - "1", - "timestamp_dt", - nowDay)), // should not cause preemptive creation of 10-28 now - params)); - - // this process should have lead to the modification of the start time for the alias, converting - // it into a parsable date, removing the DateMath - - // what we test next happens in a separate thread, so we have to give it some time to happen. - // Our own watcher on /aliases.json may fire before the provider's ZkStateReader has refreshed - // its aliases, so poll the provider itself instead of waiting on a latch. - final TimeOut timeOut = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME); - String hopeFullyModified; - while (true) { - if (BaseHttpClusterStateProvider.class.isAssignableFrom(aClass)) { - ((BaseHttpClusterStateProvider) clusterStateProvider).resolveAlias(getAlias(), true); - } - hopeFullyModified = clusterStateProvider.getAliasProperties(getAlias()).get(ROUTER_START); - if (hopeFullyModified != null) { - try { - Instant.parse(hopeFullyModified); - break; - } catch (DateTimeParseException e) { - // still has date math, try again - } - } - if (timeOut.hasTimedOut()) { - fail( - ROUTER_START - + " should not have any date math by this point and parse as an instant. Using " - + aClass - + " Found:" - + hopeFullyModified); - } - timeOut.sleep(100); - } - } - - private void monitorAlias(CountDownLatch aliasUpdate) - throws KeeperException, InterruptedException { - Stat stat = new Stat(); - zkClient() - .getData( - "/aliases.json", - new Watcher() { - @Override - public void process(WatchedEvent watchedEvent) { - aliasUpdate.countDown(); - } - }, - stat); - } - /** * Need to ensure that the existing TRA's gracefully handle, old, new and mixtures thereof. TRA's * with an autoDeleteAge setting will gracefully convert to the new format over time. From da4fa6df11784ce5a83bc7e74ec7b6aa78f689b9 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Fri, 9 Oct 2026 16:10:11 +0000 Subject: [PATCH 16/23] 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 000000000000..c3b42b1f6699 --- /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 ca443f6f680606b6c995ae86b88b4841b53addbf Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sat, 10 Oct 2026 14:34:04 +0000 Subject: [PATCH 17/23] SOLR-13943: restore changelog entry --- changelog/unreleased/SOLR-13943.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog/unreleased/SOLR-13943.yml diff --git a/changelog/unreleased/SOLR-13943.yml b/changelog/unreleased/SOLR-13943.yml new file mode 100644 index 000000000000..17e1e2f0e17d --- /dev/null +++ b/changelog/unreleased/SOLR-13943.yml @@ -0,0 +1,7 @@ +title: "Fix the race in TimeRoutedAliasUpdateProcessorTest.testDateMathInStart: poll the cluster state provider for the rewritten router.start instead of reading it once after the test's own ZooKeeper watcher fires" +type: fixed +authors: + - name: Nick Shanin +links: + - name: SOLR-13943 + url: https://issues.apache.org/jira/browse/SOLR-13943 From 21e4407d79fcd684e78de32ddf3118a91a506221 Mon Sep 17 00:00:00 2001 From: Nick Shanin Date: Sun, 18 Oct 2026 02:56:00 +0000 Subject: [PATCH 18/23] 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 b0d0097b6d3d..0587a305ad36 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 19/23] 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 0587a305ad36..2020c3515699 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 20/23] 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 2020c3515699..c63786fdceb7 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 21/23] 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 c63786fdceb7..ede7919562a6 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 22/23] 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 ede7919562a6..587d76e23df2 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 23/23] 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 4e2ab6f591b3..49cbb049fe99 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 3ca9e5d78a6e..9446b74cc80d 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 587d76e23df2..9cbc272ead0e 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);