This is an automated email from the ASF dual-hosted git repository. epugh pushed a commit to branch branch_10x in repository https://gitbox.apache.org/repos/asf/solr.git
commit a7f40683e60f8d3609a6c86b84442ccf9489785f Author: Eric Pugh <[email protected]> AuthorDate: Tue Feb 10 11:08:55 2026 -0500 SOLR-18085: Remove use of "wt=standard" internally to Solr. (#4080) Updates Solr’s response-writer (wt) handling to remove reliance on the legacy "standard" writer and to fail fast when an unknown response writer is requested. Additionally we migrate from throwing a 500 error to a more appropriate 400 error when a non existing response-write is specified. (cherry picked from commit 4f40e977a337d440c598b6f37156045e5bd8a926) --- changelog/unreleased/SOLR-18085.yml | 8 + .../src/java/org/apache/solr/core/PluginBag.java | 2 +- .../java/org/apache/solr/core/RequestHandlers.java | 2 +- .../src/java/org/apache/solr/core/SolrCore.java | 7 +- .../org/apache/solr/request/SolrQueryRequest.java | 9 +- .../apache/solr/response/RawResponseWriter.java | 9 +- .../solr/response/ResponseWritersRegistry.java | 88 ++++++++--- .../java/org/apache/solr/servlet/HttpSolrCall.java | 2 +- .../apache/solr/servlet/SolrRequestParsers.java | 56 ++----- .../src/test/org/apache/solr/OutputWriterTest.java | 21 +-- .../apache/solr/handler/V2ApiIntegrationTest.java | 35 ++--- .../response/TestPrometheusResponseWriter.java | 1 + .../solr/response/TestRawResponseWriter.java | 13 +- .../solr/response/TestResponseWritersRegistry.java | 23 +-- solr/packaging/test/test_rolling_upgrade.bats | 172 +++++++++++++++++++++ .../src/java/org/apache/solr/util/TestHarness.java | 1 + 16 files changed, 305 insertions(+), 144 deletions(-) diff --git a/changelog/unreleased/SOLR-18085.yml b/changelog/unreleased/SOLR-18085.yml new file mode 100644 index 00000000000..d6ca801ddc8 --- /dev/null +++ b/changelog/unreleased/SOLR-18085.yml @@ -0,0 +1,8 @@ +# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc +title: Removed the wt=standard concept that was used internally by Solr. +type: removed # added, changed, fixed, deprecated, removed, dependency_update, security, other +authors: + - name: Eric Pugh +links: + - name: SOLR-18085 + url: https://issues.apache.org/jira/browse/SOLR-18085 diff --git a/solr/core/src/java/org/apache/solr/core/PluginBag.java b/solr/core/src/java/org/apache/solr/core/PluginBag.java index 7624a4c13dd..1af3445e7aa 100644 --- a/solr/core/src/java/org/apache/solr/core/PluginBag.java +++ b/solr/core/src/java/org/apache/solr/core/PluginBag.java @@ -202,7 +202,7 @@ public class PluginBag<T> implements AutoCloseable { * Fetches a plugin by name , or the default * * @param name name using which it is registered - * @param useDefault Return the default , if a plugin by that name does not exist + * @param useDefault Return the default, if a plugin by that name does not exist */ public T get(String name, boolean useDefault) { T result = get(name); diff --git a/solr/core/src/java/org/apache/solr/core/RequestHandlers.java b/solr/core/src/java/org/apache/solr/core/RequestHandlers.java index dca7c832e3f..39f043d95f9 100644 --- a/solr/core/src/java/org/apache/solr/core/RequestHandlers.java +++ b/solr/core/src/java/org/apache/solr/core/RequestHandlers.java @@ -117,7 +117,7 @@ public final class RequestHandlers { modifiedInfos.add(applyInitParams(config, info)); } handlers.init(Collections.emptyMap(), core, modifiedInfos); - handlers.alias(handlers.getDefault(), ""); + if (log.isDebugEnabled()) { log.debug("Registered paths: {}", StrUtils.join(new ArrayList<>(handlers.keySet()), ',')); } diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java index fc50c62cb1f..264643ff2bf 100644 --- a/solr/core/src/java/org/apache/solr/core/SolrCore.java +++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java @@ -3128,14 +3128,11 @@ public class SolrCore implements SolrInfoBean, Closeable { // Initialize with the built defaults responseWriters.init(defaultWriters, this); - - // configure the default response writer; this one should never be null - if (responseWriters.getDefault() == null) responseWriters.setDefault("standard"); } - /** Finds a writer by name, or returns the default writer if not found. */ + /** Finds a writer by name, or null if not found. */ public final QueryResponseWriter getQueryResponseWriter(String writerName) { - return responseWriters.get(writerName, true); + return responseWriters.get(writerName, false); } /** diff --git a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java index 0ce4d82e551..c4aecf0c47e 100644 --- a/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java +++ b/solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java @@ -204,14 +204,7 @@ public interface SolrQueryRequest extends AutoCloseable { */ default QueryResponseWriter getResponseWriter() { // it's weird this method is here instead of SolrQueryResponse, but it's practical/convenient - SolrCore core = getCore(); - String wt = getParams().get(CommonParams.WT); - // Use core writers if available, otherwise fall back to built-in writers - if (core != null) { - return core.getQueryResponseWriter(wt); - } else { - return ResponseWritersRegistry.getWriter(wt); - } + return ResponseWritersRegistry.getWriter(getParams().get(CommonParams.WT), getCore()); } /** diff --git a/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java b/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java index 922b4932952..fe5269497ff 100644 --- a/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java +++ b/solr/core/src/java/org/apache/solr/response/RawResponseWriter.java @@ -47,7 +47,7 @@ public class RawResponseWriter implements QueryResponseWriter { */ public static final String CONTENT = "content"; - private String _baseWriter = null; + private String baseWriter = null; /** * A fallback writer used for requests that don't return raw content and that aren't associated @@ -62,14 +62,17 @@ public class RawResponseWriter implements QueryResponseWriter { if (n != null) { Object base = n.get("base"); if (base != null) { - _baseWriter = base.toString(); + baseWriter = base.toString(); } } } protected QueryResponseWriter getBaseWriter(SolrQueryRequest request) { if (request.getCore() != null) { - return request.getCore().getQueryResponseWriter(_baseWriter); + // When baseWriter is null, use the core's default writer (useDefault=true) + // Otherwise, look up the specific writer by name (useDefault=false for explicit lookups) + boolean useDefault = (baseWriter == null); + return request.getCore().getResponseWriters().get(baseWriter, useDefault); } // Requests to a specific core already have writers, but we still need a 'default writer' for diff --git a/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java index 67ec648cbf6..230b8caf99c 100644 --- a/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java +++ b/solr/core/src/java/org/apache/solr/response/ResponseWritersRegistry.java @@ -20,7 +20,9 @@ import static org.apache.solr.util.stats.MetricUtils.OPEN_METRICS_WT; import static org.apache.solr.util.stats.MetricUtils.PROMETHEUS_METRICS_WT; import java.util.Map; +import org.apache.solr.common.SolrException; import org.apache.solr.common.params.CommonParams; +import org.apache.solr.core.SolrCore; import org.apache.solr.handler.admin.api.ReplicationAPIBase; /** @@ -46,23 +48,14 @@ public class ResponseWritersRegistry { PrometheusResponseWriter prometheusWriter = new PrometheusResponseWriter(); BUILTIN_WRITERS = - Map.of( - CommonParams.JAVABIN, - new JavaBinResponseWriter(), - CommonParams.JSON, - jsonWriter, - "standard", - jsonWriter, // Alias for JSON - "xml", - new XMLResponseWriter(), - "raw", - new RawResponseWriter(), - PROMETHEUS_METRICS_WT, - prometheusWriter, - OPEN_METRICS_WT, - prometheusWriter, - ReplicationAPIBase.FILE_STREAM, - new FileStreamResponseWriter()); + Map.ofEntries( + Map.entry(CommonParams.JAVABIN, new JavaBinResponseWriter()), + Map.entry(CommonParams.JSON, jsonWriter), + Map.entry("xml", new XMLResponseWriter()), + Map.entry("raw", new RawResponseWriter()), + Map.entry(PROMETHEUS_METRICS_WT, prometheusWriter), + Map.entry(OPEN_METRICS_WT, prometheusWriter), + Map.entry(ReplicationAPIBase.FILE_STREAM, new FileStreamResponseWriter())); } /** @@ -71,17 +64,68 @@ public class ResponseWritersRegistry { * <p>Built-in writers are always available and provide essential formats needed by admin APIs and * core functionality. They do not depend on core configuration or ImplicitPlugins.json settings. * - * <p>If the requested writer is not available, returns the "standard" (JSON) writer as a - * fallback. This ensures requests always get a valid response format. + * <p>If the requested writer is not available, returns the JSON writer as a fallback. This + * ensures requests always get a valid response format. * * @param writerName the writer name (e.g., "json", "xml", "javabin"), or null for default - * @return the response writer, never null (returns "standard"/JSON if not found) + * @return the response writer, never null (returns JSON if not found) */ public static QueryResponseWriter getWriter(String writerName) { if (writerName == null || writerName.isEmpty()) { - return BUILTIN_WRITERS.get("standard"); + writerName = CommonParams.JSON; } - return BUILTIN_WRITERS.getOrDefault(writerName, BUILTIN_WRITERS.get("standard")); + return BUILTIN_WRITERS.get(writerName); + } + + /** + * Gets a response writer, trying the core's registry first, then falling back to built-in + * writers. This is the unified entry point for all writer resolution. + * + * <p>Resolution order: + * + * <ol> + * <li>If core is provided, check core's writer registry + * <li>If not found in core (or no core), check built-in writers + * <li>If writer name is explicitly specified but not found anywhere, throw exception + * <li>If writer name is null/empty, return default (JSON) + * </ol> + * + * @param writerName the writer name (e.g., "json", "xml", "javabin"), or null for default + * @param core the SolrCore to check first, or null for node-level requests + * @return the response writer, never null + * @throws SolrException if an explicitly requested writer type is not found + */ + public static QueryResponseWriter getWriter(String writerName, SolrCore core) { + QueryResponseWriter writer = null; + + // Try core registry first if available + if (core != null) { + writer = core.getQueryResponseWriter(writerName); + } + + // If not found and writer is explicitly requested, validate it exists in built-in + if (writer == null) { + if (!hasWriter(writerName)) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, "Unknown response writer type: " + writerName); + } else { + writer = getWriter(writerName); + } + } + return writer; + } + + /** + * Checks if a writer with the given name exists in the built-in writers. + * + * @param writerName the writer name to check + * @return true if the writer exists, false otherwise + */ + public static boolean hasWriter(String writerName) { + if (writerName == null || writerName.isEmpty()) { + return true; // null/empty is valid, will use default + } + return BUILTIN_WRITERS.containsKey(writerName); } /** diff --git a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java index a77541be552..c289ca6a3a9 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -727,7 +727,7 @@ public class HttpSolrCall { protected void logAndFlushAdminRequest(SolrQueryResponse solrResp) throws IOException { if (solrResp.getToLog().size() > 0) { - // has to come second and in it's own if to keep ./gradlew check happy. + // has to come second and in its own "if" to keep ./gradlew check happy. if (log.isInfoEnabled()) { log.info( handler != null diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java b/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java index 3cf12aa7982..e835d844db8 100644 --- a/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java +++ b/solr/core/src/java/org/apache/solr/servlet/SolrRequestParsers.java @@ -67,13 +67,6 @@ public class SolrRequestParsers { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - // Should these constants be in a more public place? - public static final String MULTIPART = "multipart"; - public static final String FORMDATA = "formdata"; - public static final String RAW = "raw"; - public static final String SIMPLE = "simple"; - public static final String STANDARD = "standard"; - private static final Charset CHARSET_US_ASCII = StandardCharsets.US_ASCII; public static final String INPUT_ENCODING_KEY = "ie"; @@ -81,8 +74,7 @@ public class SolrRequestParsers { public static final String REQUEST_TIMER_SERVLET_ATTRIBUTE = "org.apache.solr.RequestTimer"; - private final HashMap<String, SolrRequestParser> parsers = new HashMap<>(); - private StandardRequestParser standard; + private StandardRequestParser parser; /** * Default instance for e.g. admin requests. Limits to 2 MB uploads and does not allow remote @@ -91,7 +83,7 @@ public class SolrRequestParsers { public static final SolrRequestParsers DEFAULT = new SolrRequestParsers(); /** - * Pass in an xml configuration. A null configuration will enable everything with maximum values. + * Pass in a xml configuration. A null configuration will enable everything with maximum values. */ public SolrRequestParsers(SolrConfig globalConfig) { final int multipartUploadLimitKB, formUploadLimitKB; @@ -116,21 +108,12 @@ public class SolrRequestParsers { MultipartRequestParser multi = new MultipartRequestParser(multipartUploadLimitKB); RawRequestParser raw = new RawRequestParser(); FormDataRequestParser formdata = new FormDataRequestParser(formUploadLimitKB); - standard = new StandardRequestParser(multi, raw, formdata); - - // I don't see a need to have this publicly configured just yet - // adding it is trivial - parsers.put(MULTIPART, multi); - parsers.put(FORMDATA, formdata); - parsers.put(RAW, raw); - parsers.put(SIMPLE, new SimpleRequestParser()); - parsers.put(STANDARD, standard); - parsers.put("", standard); + parser = new StandardRequestParser(multi, raw, formdata); } private static RTimerTree getRequestTimer(HttpServletRequest req) { final Object reqTimer = req.getAttribute(REQUEST_TIMER_SERVLET_ATTRIBUTE); - if (reqTimer != null && reqTimer instanceof RTimerTree) { + if (reqTimer instanceof RTimerTree) { return ((RTimerTree) reqTimer); } @@ -139,7 +122,6 @@ public class SolrRequestParsers { public SolrQueryRequest parse(SolrCore core, String path, HttpServletRequest req) throws Exception { - SolrRequestParser parser = standard; // TODO -- in the future, we could pick a different parser based on the request @@ -164,13 +146,12 @@ public class SolrRequestParsers { /** For embedded Solr use; not related to HTTP. */ public SolrQueryRequest buildRequestFrom( - SolrCore core, SolrParams params, Collection<ContentStream> streams) throws Exception { + SolrCore core, SolrParams params, Collection<ContentStream> streams) { return buildRequestFrom(core, params, streams, new RTimerTree(), null, null); } public SolrQueryRequest buildRequestFrom( - SolrCore core, SolrParams params, Collection<ContentStream> streams, Principal principal) - throws Exception { + SolrCore core, SolrParams params, Collection<ContentStream> streams, Principal principal) { return buildRequestFrom(core, params, streams, new RTimerTree(), null, principal); } @@ -181,7 +162,7 @@ public class SolrRequestParsers { RTimerTree requestTimer, final HttpServletRequest req, final Principal principal) // from req, if req was provided, otherwise from elsewhere - throws Exception { + { // ensure streams is non-null and mutable so we can easily add to it if (streams == null) { streams = new ArrayList<>(); @@ -213,7 +194,7 @@ public class SolrRequestParsers { @Override public Map<String, String> getPathTemplateValues() { - if (httpSolrCall != null && httpSolrCall instanceof V2HttpCall) { + if (httpSolrCall instanceof V2HttpCall) { return ((V2HttpCall) httpSolrCall).getUrlParts(); } return super.getPathTemplateValues(); @@ -337,9 +318,9 @@ public class SolrRequestParsers { // we have no charset decoder until now, buffer the keys / values for later // processing: buffer.add(keyBytes); - buffer.add(Long.valueOf(keyPos)); + buffer.add(keyPos); buffer.add(valueBytes); - buffer.add(Long.valueOf(valuePos)); + buffer.add(valuePos); } else { // we already have a charsetDecoder, so we can directly decode without buffering: final String key = decodeChars(keyBytes, keyPos, charsetDecoder), @@ -457,7 +438,7 @@ public class SolrRequestParsers { // ----------------------------------------------------------------- // ----------------------------------------------------------------- - // I guess we don't really even need the interface, but i'll keep it here just for kicks + // I guess we don't really even need the interface, but I'll keep it here just for kicks interface SolrRequestParser { public SolrParams parseParamsAndFillStreams( final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception; @@ -466,15 +447,6 @@ public class SolrRequestParsers { // ----------------------------------------------------------------- // ----------------------------------------------------------------- - /** The simple parser just uses the params directly, does not support POST URL-encoded forms */ - static class SimpleRequestParser implements SolrRequestParser { - @Override - public SolrParams parseParamsAndFillStreams( - final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { - return parseQueryString(req.getQueryString()); - } - } - /** Wrap an HttpServletRequest as a ContentStream */ static class HttpRequestContentStream extends ContentStreamBase { private final InputStream inputStream; @@ -515,7 +487,7 @@ public class SolrRequestParsers { || req.getHeader("Transfer-Encoding") != null || !NO_BODY_METHODS.contains(req.getMethod())) { // If Content-Length > 0 OR Transfer-Encoding exists OR - // it's a method that can have a body (POST/PUT/PATCH etc) + // it's a method that can have a body (POST/PUT/PATCH etc.) streams.add(new HttpRequestContentStream(req, req.getInputStream())); } @@ -543,7 +515,7 @@ public class SolrRequestParsers { throw new SolrException( ErrorCode.BAD_REQUEST, "Not multipart content! " + req.getContentType()); } - // Magic way to tell Jetty dynamically we want multi-part processing. + // Magic way to tell Jetty dynamically we want multipart processing. // This is taken from: // https://github.com/eclipse/jetty.project/blob/jetty-10.0.12/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java#L144 req.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfigElement); @@ -735,7 +707,7 @@ public class SolrRequestParsers { public SolrParams parseParamsAndFillStreams( final HttpServletRequest req, ArrayList<ContentStream> streams) throws Exception { String contentType = req.getContentType(); - String method = req.getMethod(); // No need to uppercase... HTTP verbs are case sensitive + String method = req.getMethod(); // No need to uppercase... HTTP verbs are case-sensitive String uri = req.getRequestURI(); boolean isV2 = getHttpSolrCall(req) instanceof V2HttpCall; boolean isPost = "POST".equals(method); diff --git a/solr/core/src/test/org/apache/solr/OutputWriterTest.java b/solr/core/src/test/org/apache/solr/OutputWriterTest.java index 30df9ed98fa..7f711631ce1 100644 --- a/solr/core/src/test/org/apache/solr/OutputWriterTest.java +++ b/solr/core/src/test/org/apache/solr/OutputWriterTest.java @@ -29,32 +29,16 @@ import org.junit.Test; /** Tests the ability to configure multiple query output writers, and select those at query time. */ public class OutputWriterTest extends SolrTestCaseJ4 { - /** The XML string that's output for testing purposes. */ - public static final String USELESS_OUTPUT = "useless output"; - @BeforeClass public static void beforeClass() throws Exception { initCore("solr/crazy-path-to-config.xml", "solr/crazy-path-to-schema.xml"); } - /** - * responseHeader has changed in SOLR-59, check old and new variants, In SOLR-2413, we removed - * support for the deprecated versions - */ - @Test - public void testSOLR59responseHeaderVersions() { - // default results in "new" responseHeader - lrf.args.put("wt", "standard"); - assertQ(req("foo"), "/response/lst[@name='responseHeader']/int[@name='status'][.='0']"); - lrf.args.remove("wt"); - assertQ(req("foo"), "/response/lst[@name='responseHeader']/int[@name='QTime']"); - } - @Test public void testUselessWriter() throws Exception { lrf.args.put("wt", "useless"); String out = h.query(req("foo")); - assertEquals(USELESS_OUTPUT, out); + assertEquals(UselessOutputWriter.USELESS_OUTPUT, out); } public void testLazy() { @@ -71,6 +55,9 @@ public class OutputWriterTest extends SolrTestCaseJ4 { /** An output writer that doesn't do anything useful. */ public static class UselessOutputWriter implements TextQueryResponseWriter { + /** The XML string that's output for testing purposes. */ + public static final String USELESS_OUTPUT = "useless output"; + public UselessOutputWriter() {} @Override diff --git a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java index 3b7a85eb096..4ef3c7f48df 100644 --- a/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java +++ b/solr/core/src/test/org/apache/solr/handler/V2ApiIntegrationTest.java @@ -48,7 +48,7 @@ import org.junit.BeforeClass; import org.junit.Test; public class V2ApiIntegrationTest extends SolrCloudTestCase { - private static String COLL_NAME = "collection1"; + private static final String COLL_NAME = "collection1"; @BeforeClass public static void createCluster() throws Exception { @@ -80,11 +80,7 @@ public class V2ApiIntegrationTest extends SolrCloudTestCase { .build(); v2Request.setResponseParser(responseParser); RemoteSolrException ex = - expectThrows( - RemoteSolrException.class, - () -> { - v2Request.process(cluster.getSolrClient()); - }); + expectThrows(RemoteSolrException.class, () -> v2Request.process(cluster.getSolrClient())); assertEquals(expectedCode, ex.code()); } @@ -114,28 +110,29 @@ public class V2ApiIntegrationTest extends SolrCloudTestCase { } @Test - public void testWTParam() throws Exception { + public void testInvalidWTParamReturnsError() throws Exception { V2Request request = new V2Request.Builder("/c/" + COLL_NAME + "/get/_introspect").build(); - // TODO: If possible do this in a better way + // Using an invalid wt parameter should return a 400 error request.setResponseParser(new InputStreamResponseParser("bleh")); NamedList<Object> res = cluster.getSolrClient().request(request); String respString = InputStreamResponseParser.consumeResponseToString(res); - assertFalse(respString.contains("<body><h2>HTTP ERROR 500</h2>")); - assertFalse(respString.contains("500")); - assertFalse(respString.contains("NullPointerException")); - assertFalse( - respString.contains( - "<p>Problem accessing /solr/____v2/c/collection1/get/_introspect. Reason:")); - // since no-op response writer is used, doing contains match - assertTrue(respString.contains("/c/collection1/get")); + // Should get a 400 Bad Request error for unknown writer type + assertTrue( + "Expected error message about unknown writer type", + respString.contains("Unknown response writer type")); + assertTrue("Expected 400 error code", respString.contains("400")); + } - // no response parser + @Test + public void testWTParam() throws Exception { + // When no response parser is set, the default JSON writer should be used + V2Request request = new V2Request.Builder("/c/" + COLL_NAME + "/get/_introspect").build(); request.setResponseParser(null); Map<?, ?> resp = resAsMap(cluster.getSolrClient(), request); - respString = resp.toString(); + String respString = resp.toString(); - assertFalse(respString.contains("<body><h2>HTTP ERROR 500</h2>")); + assertFalse(respString.contains("400")); assertFalse( respString.contains( "<p>Problem accessing /solr/____v2/c/collection1/get/_introspect. Reason:")); diff --git a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java index 530a2ba6009..a8250a1ba3a 100644 --- a/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java +++ b/solr/core/src/test/org/apache/solr/response/TestPrometheusResponseWriter.java @@ -194,6 +194,7 @@ public class TestPrometheusResponseWriter extends SolrTestCaseJ4 { try (SolrClient adminClient = getHttpSolrClient(solrTestRule.getBaseUrl())) { NamedList<Object> res = adminClient.request(req); + // Unknown wt parameter should return a 400 error assertEquals(400, res.get("responseStatus")); } } diff --git a/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java b/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java index 7b822b18848..34e006b727b 100644 --- a/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java +++ b/solr/core/src/test/org/apache/solr/response/TestRawResponseWriter.java @@ -56,7 +56,9 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 { // we spin up. initCore("solrconfig.xml", "schema.xml"); - writerNoBase = newRawResponseWriter(null); /* defaults to standard writer as base */ + writerNoBase = + newRawResponseWriter( + null); /* null base uses core's default writer (XML for this core), or JSON if no core */ writerXmlBase = newRawResponseWriter("xml"); writerJsonBase = newRawResponseWriter("json"); writerBinBase = newRawResponseWriter("javabin"); @@ -120,7 +122,7 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 { // we should have UTF-8 Bytes if we use an OutputStream ByteArrayOutputStream bout = new ByteArrayOutputStream(); writer.write(bout, req(), rsp); - assertEquals(data, bout.toString(StandardCharsets.UTF_8.toString())); + assertEquals(data, bout.toString(StandardCharsets.UTF_8)); } } @@ -153,13 +155,12 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 { assertEquals(xml, writerXmlBase.writeToString(req(), rsp)); ByteArrayOutputStream xmlBout = new ByteArrayOutputStream(); writerXmlBase.write(xmlBout, req(), rsp); - assertEquals(xml, xmlBout.toString(StandardCharsets.UTF_8.toString())); + assertEquals(xml, xmlBout.toString(StandardCharsets.UTF_8)); - // assertEquals(xml, writerNoBase.writeToString(req(), rsp)); ByteArrayOutputStream noneBout = new ByteArrayOutputStream(); writerNoBase.write(noneBout, req(), rsp); - assertEquals(xml, noneBout.toString(StandardCharsets.UTF_8.toString())); + assertEquals(xml, noneBout.toString(StandardCharsets.UTF_8)); // json String json = "{\n" + " \"content\":\"test\",\n" + " \"foo\":\"bar\"}\n"; @@ -206,7 +207,7 @@ public class TestRawResponseWriter extends SolrTestCaseJ4 { } /** - * Generates a new {@link RawResponseWriter} wrapping the specified baseWriter name (which much + * Generates a new {@link RawResponseWriter} wrapping the specified baseWriter name (which must * either be an implicitly defined response writer, or one explicitly configured in * solrconfig.xml) * diff --git a/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java index 695ad0e1278..0ab46734b38 100644 --- a/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java +++ b/solr/core/src/test/org/apache/solr/response/TestResponseWritersRegistry.java @@ -31,34 +31,19 @@ public class TestResponseWritersRegistry extends SolrTestCaseJ4 { @Test public void testBuiltInWriterFallbackBehavior() { - QueryResponseWriter standardWriter = ResponseWritersRegistry.getWriter("standard"); + QueryResponseWriter defaultWriter = ResponseWritersRegistry.getWriter("json"); // Test null fallback QueryResponseWriter nullWriter = ResponseWritersRegistry.getWriter(null); - assertThat("null writer should not be null", nullWriter, is(not(nullValue()))); - assertThat("null writer should be same as standard", nullWriter, is(standardWriter)); + assertThat("null writer should be same as default", nullWriter, is(defaultWriter)); // Test empty string fallback QueryResponseWriter emptyWriter = ResponseWritersRegistry.getWriter(""); assertThat("empty writer should not be null", emptyWriter, is(not(nullValue()))); - assertThat("empty writer should be same as standard", emptyWriter, is(standardWriter)); + assertThat("empty writer should be same as default", emptyWriter, is(defaultWriter)); // Test unknown format fallback QueryResponseWriter unknownWriter = ResponseWritersRegistry.getWriter("nonexistent"); - assertThat("unknown writer should not be null", unknownWriter, is(not(nullValue()))); - assertThat("unknown writer should be same as standard", unknownWriter, is(standardWriter)); - } - - @Test - public void testBuiltInWriterLimitedSet() { - QueryResponseWriter standardWriter = ResponseWritersRegistry.getWriter("standard"); - - // Built-in writers should NOT include extended format writers (csv, geojson, etc.) - // These should all fall back to standard - // I think this standard thing is weird... I think it should throw an exception! - assertThat( - "geojson should fall back to standard", - ResponseWritersRegistry.getWriter("geojson"), - is(standardWriter)); + assertThat("unknown writer should be null", unknownWriter, is(nullValue())); } } diff --git a/solr/packaging/test/test_rolling_upgrade.bats b/solr/packaging/test/test_rolling_upgrade.bats new file mode 100644 index 00000000000..ae956a868ba --- /dev/null +++ b/solr/packaging/test/test_rolling_upgrade.bats @@ -0,0 +1,172 @@ +#!/usr/bin/env bats + +# 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. + +load bats_helper + +# You can test alternative images via +# export SOLR_BEGIN_IMAGE="apache/solr-nightly:9.9.0-slim" and then running +# ./gradlew iTest --tests test_rolling_upgrade.bats +SOLR_BEGIN_IMAGE="${SOLR_BEGIN_IMAGE:-apache/solr-nightly:9.10.0-SNAPSHOT-slim}" +SOLR_END_IMAGE="${SOLR_END_IMAGE:-apache/solr-nightly:10.0.0-SNAPSHOT-slim}" + +setup() { + common_clean_setup + + # Pre-checks + if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then + skip "Docker is not available" + fi + docker pull "$SOLR_BEGIN_IMAGE" || skip "Docker image $SOLR_BEGIN_IMAGE is not available" + docker pull "$SOLR_END_IMAGE" || skip "Docker image $SOLR_END_IMAGE is not available" + + # Record test start time for scoping logs on failure + TEST_STARTED_AT_ISO=$(date -Iseconds) + export TEST_STARTED_AT_ISO + + # Persist artifacts under Gradle’s test-output + ARTIFACT_DIR="${TEST_OUTPUT_DIR}/docker" + mkdir -p "$ARTIFACT_DIR" + export ARTIFACT_DIR +} + +teardown() { + failed=$([[ -z "${BATS_TEST_COMPLETED:-}" ]] && [[ -z "${BATS_TEST_SKIPPED:-}" ]] && echo 1 || echo 0) + if [[ "$failed" -eq 1 ]]; then + echo "# Test failed - capturing Docker diagnostics" >&3 + echo "# === docker ps (summary) ===" >&3 + docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}\t{{.Ports}}' >&3 2>&3 || true + fi + + for container in solr-node1 solr-node2 solr-node3; do + if docker ps -a --format '{{.Names}}' | grep -q "^${container}$" 2>/dev/null; then + if [[ "$failed" -eq 1 ]]; then + echo "# === Docker logs for $container ===" >&3 + docker logs --timestamps --since "$TEST_STARTED_AT_ISO" "$container" >&3 2>&3 || echo "# Failed to get logs for $container" >&3 + echo "# === Docker inspect for $container ===" >&3 + docker inspect "$container" | jq '.[] | {Name: .Name, State: .State, Ports: .NetworkSettings.Ports}' >&3 2>&3 || true + fi + # Persist artifacts + docker logs --timestamps "$container" >"$ARTIFACT_DIR/${container}.log" 2>&1 || true + docker inspect "$container" >"$ARTIFACT_DIR/${container}.inspect.json" 2>&1 || true + docker exec "$container" ps aux >"$ARTIFACT_DIR/${container}.ps.txt" 2>&1 || true + fi + done + + echo "# Docker artifacts saved to: $ARTIFACT_DIR" >&3 + + docker stop solr-node1 solr-node2 solr-node3 2>/dev/null || true + docker rm solr-node1 solr-node2 solr-node3 2>/dev/null || true + docker volume rm solr-data1 solr-data2 solr-data3 2>/dev/null || true + docker network rm solrcloud-test 2>/dev/null || true +} + +@test "Docker SolrCloud rolling upgrade" { + # Networking & volumes + docker network create solrcloud-test + docker volume create solr-data1 + docker volume create solr-data2 + docker volume create solr-data3 + + echo "Starting solr-node1 with embedded ZooKeeper" + docker run --name solr-node1 -d \ + --network solrcloud-test \ + --memory=400m \ + --platform linux/amd64 \ + -v solr-data1:/var/solr \ + "$SOLR_BEGIN_IMAGE" solr start -f -c -m 200m --host solr-node1 -p 8983 + docker exec solr-node1 solr assert --started http://solr-node1:8983 --timeout 10000 + + # start next 2 in parallel + + echo "Starting solr-node2 connected to first node's ZooKeeper" + docker run --name solr-node2 -d \ + --network solrcloud-test \ + --memory=400m \ + --platform linux/amd64 \ + -v solr-data2:/var/solr \ + "$SOLR_BEGIN_IMAGE" solr start -f -c -m 200m --host solr-node2 -p 8984 -z solr-node1:9983 + + echo "Started solr-node3 connected to first node's ZooKeeper" + docker run --name solr-node3 -d \ + --network solrcloud-test \ + --memory=400m \ + --platform linux/amd64 \ + -v solr-data3:/var/solr \ + "$SOLR_BEGIN_IMAGE" solr start -f -c -m 200m --host solr-node3 -p 8985 -z solr-node1:9983 + + docker exec solr-node2 solr assert --started http://solr-node2:8984 --timeout 30000 + docker exec solr-node3 solr assert --started http://solr-node3:8985 --timeout 30000 + + echo "Creating a Collection" + docker exec --user=solr solr-node1 solr create -c test-collection -n techproducts --shards 3 + + echo "Checking collection health" + wait_for 30 1 docker exec solr-node1 solr healthcheck -c test-collection + + echo "Add some sample data" + docker exec --user=solr solr-node1 solr post -c test-collection example/exampledocs/mem.xml + assert_success + + # Begin rolling upgrade - upgrade node 3 first (reverse order: 3, 2, 1) + echo "Starting rolling upgrade - upgrading node 3" + docker stop solr-node3 + docker rm solr-node3 + docker run --name solr-node3 -d \ + --network solrcloud-test \ + --memory=400m \ + --platform linux/amd64 \ + -v solr-data3:/var/solr \ + "$SOLR_END_IMAGE" solr start -f -m 200m --host solr-node3 -p 8985 -z solr-node1:9983 + docker exec solr-node3 solr assert --started http://solr-node3:8985 --timeout 30000 + assert_success + + # Upgrade node 2 second + echo "Upgrading node 2" + docker stop solr-node2 + docker rm solr-node2 + docker run --name solr-node2 -d \ + --network solrcloud-test \ + --memory=400m \ + --platform linux/amd64 \ + -v solr-data2:/var/solr \ + "$SOLR_END_IMAGE" solr start -f -m 200m --host solr-node2 -p 8984 -z solr-node1:9983 + docker exec solr-node2 solr assert --started http://solr-node2:8984 --timeout 30000 + assert_success + + echo "Upgrading node 1 (ZK node)" + docker stop solr-node1 + docker rm solr-node1 + docker run --name solr-node1 -d \ + --network solrcloud-test \ + --memory=400m \ + --platform linux/amd64 \ + -v solr-data1:/var/solr \ + "$SOLR_END_IMAGE" solr start -f -m 200m --host solr-node1 -p 8983 + docker exec solr-node1 solr assert --started http://solr-node1:8983 --timeout 30000 + assert_success + + # Final collection health check + wait_for 30 1 docker exec solr-node1 solr healthcheck -c test-collection + + echo "checking cluster has exactly 3 live nodes" + run docker exec solr-node1 curl -s "http://solr-node1:8983/solr/admin/collections?action=CLUSTERSTATUS" + assert_success + + local live_nodes_count=$(echo "$output" | jq -r '.cluster.live_nodes | length') + assert_equal "$live_nodes_count" "3" + +} diff --git a/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java b/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java index 29caa318a04..a933605466f 100644 --- a/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java +++ b/solr/test-framework/src/java/org/apache/solr/util/TestHarness.java @@ -419,6 +419,7 @@ public class TestHarness extends BaseTestHarness { @SuppressWarnings({"unchecked"}) public LocalSolrQueryRequest makeRequest(String... q) { if (q.length == 1) { + args.computeIfAbsent("wt", k -> "xml"); return new LocalSolrQueryRequest( TestHarness.this.getCore(), q[0], qtype, start, limit, args); }
