Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog/unreleased/admin-response-writers-minimal-set.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Introduce minimal set of request writers for node/container-level requests. Core-specific request writers now leverage ImplicitPlugins.json for creation.
type: other
authors:
- name: Eric Pugh
- name: David Smiley
links:
- name: PR#4073
url: https://github.com/apache/solr/pull/4073
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.apache.solr.client.solrj.response.InputStreamResponseParser;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.core.SolrCore;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
Expand Down Expand Up @@ -58,7 +57,6 @@ public class QueryResponseWriters {
@State(Scope.Benchmark)
public static class BenchState {

/** See {@link SolrCore#DEFAULT_RESPONSE_WRITERS} */
@Param({CommonParams.JAVABIN, CommonParams.JSON, "cbor", "smile", "xml", "raw"})
String wt;

Expand Down
146 changes: 71 additions & 75 deletions solr/core/src/java/org/apache/solr/core/SolrCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
package org.apache.solr.core;

import static org.apache.solr.common.params.CommonParams.PATH;
import static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT;
import static org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT;
import static org.apache.solr.metrics.SolrCoreMetricManager.COLLECTION_ATTR;
import static org.apache.solr.metrics.SolrCoreMetricManager.CORE_ATTR;
import static org.apache.solr.metrics.SolrCoreMetricManager.REPLICA_TYPE_ATTR;
Expand Down Expand Up @@ -112,7 +110,6 @@
import org.apache.solr.handler.IndexFetcher;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.handler.SolrConfigHandler;
import org.apache.solr.handler.admin.api.ReplicationAPIBase;
import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.handler.component.HighlightComponent;
import org.apache.solr.handler.component.SearchComponent;
Expand All @@ -129,19 +126,9 @@
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.CSVResponseWriter;
import org.apache.solr.response.CborResponseWriter;
import org.apache.solr.response.GeoJSONResponseWriter;
import org.apache.solr.response.GraphMLResponseWriter;
import org.apache.solr.response.JacksonJsonWriter;
import org.apache.solr.response.JavaBinResponseWriter;
import org.apache.solr.response.PrometheusResponseWriter;
import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.response.RawResponseWriter;
import org.apache.solr.response.SchemaXmlResponseWriter;
import org.apache.solr.response.SmileResponseWriter;
import org.apache.solr.response.ResponseWritersRegistry;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.response.XMLResponseWriter;
import org.apache.solr.response.transform.TransformerFactory;
import org.apache.solr.rest.ManagedResourceStorage;
import org.apache.solr.rest.ManagedResourceStorage.StorageIO;
Expand Down Expand Up @@ -3088,51 +3075,6 @@ public PluginBag<QueryResponseWriter> getResponseWriters() {

private final PluginBag<QueryResponseWriter> responseWriters =
new PluginBag<>(QueryResponseWriter.class, this);
public static final Map<String, QueryResponseWriter> DEFAULT_RESPONSE_WRITERS;

static {
HashMap<String, QueryResponseWriter> m = new HashMap<>(15, 1);
m.put("xml", new XMLResponseWriter());
m.put(CommonParams.JSON, new JacksonJsonWriter());
m.put("standard", m.get(CommonParams.JSON));
m.put("geojson", new GeoJSONResponseWriter());
m.put("graphml", new GraphMLResponseWriter());
m.put("raw", new RawResponseWriter());
m.put(CommonParams.JAVABIN, new JavaBinResponseWriter());
m.put("cbor", new CborResponseWriter());
m.put("csv", new CSVResponseWriter());
m.put("schema.xml", new SchemaXmlResponseWriter());
m.put("smile", new SmileResponseWriter());
m.put(PROMETHEUS_METRICS_WT, new PrometheusResponseWriter());
m.put(OPEN_METRICS_WT, new PrometheusResponseWriter());
m.put(ReplicationAPIBase.FILE_STREAM, getFileStreamWriter());
DEFAULT_RESPONSE_WRITERS = Collections.unmodifiableMap(m);
}

private static JavaBinResponseWriter getFileStreamWriter() {
return new JavaBinResponseWriter() {
@Override
public void write(
OutputStream out, SolrQueryRequest req, SolrQueryResponse response, String contentType)
throws IOException {
RawWriter rawWriter = (RawWriter) response.getValues().get(ReplicationAPIBase.FILE_STREAM);
if (rawWriter != null) {
rawWriter.write(out);
if (rawWriter instanceof Closeable) ((Closeable) rawWriter).close();
}
}

@Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
RawWriter rawWriter = (RawWriter) response.getValues().get(ReplicationAPIBase.FILE_STREAM);
if (rawWriter != null) {
return rawWriter.getContentType();
} else {
return JavaBinResponseParser.JAVABIN_CONTENT_TYPE;
}
}
};
}

public void fetchLatestSchema() {
IndexSchema schema = configSet.getIndexSchema(true);
Expand All @@ -3148,11 +3090,48 @@ default String getContentType() {
}

/**
* Configure the query response writers. There will always be a default writer; additional writers
* may also be configured.
* Gets a response writer suitable for node/container-level requests.
*
* @param writerName the writer name, or null for default
* @return the response writer, never null
* @deprecated Use {@link ResponseWritersRegistry#getWriter(String)} instead.
*/
@Deprecated
public static QueryResponseWriter getAdminResponseWriter(String writerName) {
return ResponseWritersRegistry.getWriter(writerName);
}

/**
* Initializes query response writers. Response writers from {@code ImplicitPlugins.json} may also
* be configured.
*/
private void initWriters() {
responseWriters.init(DEFAULT_RESPONSE_WRITERS, this);
// Build default writers map from implicit plugins
Map<String, QueryResponseWriter> defaultWriters = new HashMap<>();

// Start with built-in writers that are always available
defaultWriters.putAll(ResponseWritersRegistry.getAllWriters());

// Load writers from ImplicitPlugins.json (may override built-ins)
List<PluginInfo> implicitWriters = getImplicitResponseWriters();
for (PluginInfo info : implicitWriters) {
try {
QueryResponseWriter writer =
createInstance(
info.className,
QueryResponseWriter.class,
"queryResponseWriter",
null,
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SolrCore.initWriters() passes core=null into createInstance(...) when loading core-level response writers. If any QueryResponseWriter has a SolrCore constructor (or otherwise expects a non-null core), this will either instantiate with a null core or skip the core-aware constructor path. Use this (the current SolrCore) instead of null when creating core-specific writers.

Suggested change
null,
this,

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting, and I think a separte issue. None of the QueryResponseWriter's take a core. RawResponseWriter looks up a core from a specific request, but doesn't take it directly. There are. a few calls to the createInstance and they mostly pass a null value. The only place I see a core being passed in is the call from PackagePluginHolder.initNewInstance. I don't know if there is a refactoring to have two createInstances? Or one that you call that doesn't take a core and then calls this one for a null? Probably for another PR. So, based on this analysis, I am going to leave this null. This is just a bit icky.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to have a version of createInstance that doesn't take a SolrCore just to make it clearer and use that in the places we call createInstance

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a change -- were response writers receiving the core and now, not?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was NOT a change. Only in one instance (that of PackagePluginHolder) do they get a core. I think it's a refactoring opportunity.

getResourceLoader());
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SolrCore.initWriters() instantiates implicit response writers via createInstance(...) but does not run plugin initialization (init args / PluginInfoInitialized / NamedListInitializedPlugin) like other Solr plugins do (see SolrCore.createInitInstance / SolrCore.initPlugin). This can break response writers that expect initialization from PluginInfo. After instantiating, call SolrCore.initPlugin(info, writer) (or use createInitInstance with a PluginInfo) before registering it.

Suggested change
getResourceLoader());
getResourceLoader());
initPlugin(info, writer);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is probably a "good thing" yet none of our QueryResponseWriters need it, so we don't have it. If this plugin creation was centralized more, then it might happen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great feedback from Copilot.
Agreed that we should better normalize/standardize plugin instantiation!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the VelocityResponseWriter needs a SolrResourceLoader (from a core). Any way, we need not concern ourselves with that.

defaultWriters.put(info.name, writer);
} catch (Exception e) {
log.warn("Failed to load implicit response writer: {}", info.name, e);
}
}

// 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");
}
Expand Down Expand Up @@ -3614,32 +3593,49 @@ public void cleanupOldIndexDirectories(boolean reload) {
}
}

private static final class ImplicitHolder {
private ImplicitHolder() {}
private static final class ImplicitPluginsHolder {
private ImplicitPluginsHolder() {}

private static final List<PluginInfo> INSTANCE;
private static final Map<String, List<PluginInfo>> ALL_IMPLICIT_PLUGINS;

static {
@SuppressWarnings("unchecked")
Map<String, ?> implicitPluginsInfo =
(Map<String, ?>)
Utils.fromJSONResource(SolrCore.class.getClassLoader(), "ImplicitPlugins.json");
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> requestHandlers =
(Map<String, Map<String, Object>>) implicitPluginsInfo.get(SolrRequestHandler.TYPE);

List<PluginInfo> implicits = new ArrayList<>(requestHandlers.size());
for (Map.Entry<String, Map<String, Object>> entry : requestHandlers.entrySet()) {
Map<String, Object> info = entry.getValue();
info.put(CommonParams.NAME, entry.getKey());
implicits.add(new PluginInfo(SolrRequestHandler.TYPE, info));
Map<String, List<PluginInfo>> plugins = new HashMap<>();

// Load all plugin types from the JSON
for (Map.Entry<String, ?> entry : implicitPluginsInfo.entrySet()) {
String pluginType = entry.getKey();
@SuppressWarnings("unchecked")
Map<String, Map<String, Object>> pluginConfigs =
(Map<String, Map<String, Object>>) entry.getValue();

List<PluginInfo> pluginInfos = new ArrayList<>(pluginConfigs.size());
for (Map.Entry<String, Map<String, Object>> plugin : pluginConfigs.entrySet()) {
Map<String, Object> info = plugin.getValue();
info.put(CommonParams.NAME, plugin.getKey());
pluginInfos.add(new PluginInfo(pluginType, info));
}
plugins.put(pluginType, Collections.unmodifiableList(pluginInfos));
}
INSTANCE = Collections.unmodifiableList(implicits);

ALL_IMPLICIT_PLUGINS = Collections.unmodifiableMap(plugins);
}

public static List<PluginInfo> getImplicitPlugins(String type) {
return ALL_IMPLICIT_PLUGINS.getOrDefault(type, Collections.emptyList());
}
}

public List<PluginInfo> getImplicitHandlers() {
return ImplicitHolder.INSTANCE;
return ImplicitPluginsHolder.getImplicitPlugins(SolrRequestHandler.TYPE);
}

public List<PluginInfo> getImplicitResponseWriters() {
return ImplicitPluginsHolder.getImplicitPlugins("queryResponseWriter");
}

public CancellableQueryTracker getCancellableQueryTracker() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,8 @@ public class SystemInfoHandler extends RequestHandlerBase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

/**
* Undocumented expert level system property to prevent doing a reverse lookup of our hostname.
* This property will be logged as a suggested workaround if any problems are noticed when doing
* reverse lookup.
* Expert level system property to prevent doing a reverse lookup of our hostname. This property
* will be logged as a suggested workaround if any problems are noticed when doing reverse lookup.
*
* <p>TODO: should we refactor this (and the associated logic) into a helper method for any other
* places where DNS is used?
Expand All @@ -97,7 +96,7 @@ public class SystemInfoHandler extends RequestHandlerBase {
private static final ConcurrentMap<Class<?>, BeanInfo> beanInfos = new ConcurrentHashMap<>();

// on some platforms, resolving canonical hostname can cause the thread
// to block for several seconds if nameservices aren't available
// to block for several seconds if name services aren't available
// so resolve this once per handler instance
// (ie: not static, so core reload will refresh)
private String hostname = null;
Expand Down
16 changes: 11 additions & 5 deletions solr/core/src/java/org/apache/solr/request/SolrQueryRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.apache.solr.core.CoreContainer;
import org.apache.solr.core.SolrCore;
import org.apache.solr.response.QueryResponseWriter;
import org.apache.solr.response.ResponseWritersRegistry;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.search.SolrIndexSearcher;
import org.apache.solr.servlet.HttpSolrCall;
Expand Down Expand Up @@ -117,7 +118,7 @@ static boolean disallowPartialResults(SolrParams params) {
/** The index searcher associated with this request */
SolrIndexSearcher getSearcher();

/** The solr core (coordinator, etc) associated with this request */
/** The solr core (coordinator, etc.) associated with this request */
SolrCore getCore();

/** The schema snapshot from core.getLatestSchema() at request creation. */
Expand Down Expand Up @@ -145,7 +146,7 @@ default String getPath() {

/**
* Only for V2 API. Returns a map of path segments and their values. For example, if the path is
* configured as /path/{segment1}/{segment2} and a reguest is made as /path/x/y the returned map
* configured as /path/{segment1}/{segment2} and a request is made as /path/x/y the returned map
* would contain {segment1:x ,segment2:y}
*/
default Map<String, String> getPathTemplateValues() {
Expand Down Expand Up @@ -195,16 +196,21 @@ default CloudDescriptor getCloudDescriptor() {
return getCore().getCoreDescriptor().getCloudDescriptor();
}

/** The writer to use for this request, considering {@link CommonParams#WT}. Never null. */
/**
* The writer to use for this request, considering {@link CommonParams#WT}. Never null.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is saying that the wt is always there, which means we have some extra "if no wt, than do this" which wouldn't match these docs?

*
* <p>If a core is available, uses the core's response writer registry. If no core is available
* (e.g., for node/container requests), uses a minimal set of node/container-appropriate writers.
*/
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 SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(
wt, SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard"));
return ResponseWritersRegistry.getWriter(wt);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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.response;

import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.solr.client.solrj.response.JavaBinResponseParser;
import org.apache.solr.core.SolrCore;
import org.apache.solr.handler.admin.api.ReplicationAPIBase;
import org.apache.solr.request.SolrQueryRequest;

/**
* Response writer for file streaming operations, used for replication, exports, and other core Solr
* operations.
*
* <p>This writer handles streaming of large files (such as index files) by looking for a {@link
* org.apache.solr.core.SolrCore.RawWriter} object in the response under the {@link
* ReplicationAPIBase#FILE_STREAM} key. When found, it delegates directly to the raw writer to
* stream the file content efficiently.
*
* <p>This writer is specifically designed for replication file transfers and provides no fallback
* behavior - it only works when a proper RawWriter is present in the response.
*/
public class FileStreamResponseWriter implements QueryResponseWriter {

@Override
public void write(
OutputStream out, SolrQueryRequest request, SolrQueryResponse response, String contentType)
throws IOException {
SolrCore.RawWriter rawWriter =
(SolrCore.RawWriter) response.getValues().get(ReplicationAPIBase.FILE_STREAM);
if (rawWriter != null) {
rawWriter.write(out);
if (rawWriter instanceof Closeable closeable) {
closeable.close();
}
}
}

@Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
SolrCore.RawWriter rawWriter =
(SolrCore.RawWriter) response.getValues().get(ReplicationAPIBase.FILE_STREAM);
if (rawWriter != null) {
String contentType = rawWriter.getContentType();
if (contentType != null) {
return contentType;
}
}
return JavaBinResponseParser.JAVABIN_CONTENT_TYPE;
}
}
Loading