-
Notifications
You must be signed in to change notification settings - Fork 822
Separate core specific Request Writers from node specific "built in" ones. Move core specific to using ImplicitPlugins.json. #4073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
2c89423
ec3b79e
509e490
9735afc
ac54910
de08d31
a0a9da4
c7e4693
e4f3370
4a9745d
95b9fea
513ad20
6741fe6
816c92b
f2364d3
45704e1
1ed1885
f88abea
fe51a67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc | ||
| title: Introduce minimal ADMIN_RESPONSE_WRITERS for admin/container-level requests. Admin requests now use a minimal set of 6 response writers (javabin, json, xml, prometheus, openmetrics, standard) instead of the full 14-writer DEFAULT_RESPONSE_WRITERS map. Core-specific requests continue to use ImplicitPlugins.json with ConfigOverlay support. | ||
| type: changed | ||
|
epugh marked this conversation as resolved.
Outdated
|
||
| authors: | ||
| - name: Eric Pugh | ||
| links: | ||
| - name: GitHub Discussion | ||
| url: https://github.com/apache/solr/discussions | ||
| notes: | | ||
| Admin/container-level requests (e.g., /admin/cores, /admin/collections) have no associated SolrCore | ||
| and cannot use the core's response writer registry loaded from ImplicitPlugins.json. Previously, | ||
| these requests used DEFAULT_RESPONSE_WRITERS (14 entries). Now they use ADMIN_RESPONSE_WRITERS (6 entries) | ||
| containing only the formats actually needed by admin APIs: | ||
| - javabin (SolrJ clients) | ||
| - json (Admin UI) | ||
| - xml (backward compatibility) | ||
| - prometheus/openmetrics (metrics endpoint) | ||
| - standard (alias for json) | ||
|
|
||
| This provides clearer separation of concerns and better documentation of requirements. Core-specific | ||
| requests are unaffected and continue to use the full ImplicitPlugins.json system with ConfigOverlay support. | ||
| DEFAULT_RESPONSE_WRITERS remains available but is now deprecated for internal use. | ||
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||
|
|
@@ -129,19 +127,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.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; | ||||||||
|
|
@@ -3088,26 +3076,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() { | ||||||||
|
|
@@ -3148,11 +3116,49 @@ 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 admin/container-level requests. | ||||||||
|
epugh marked this conversation as resolved.
Outdated
|
||||||||
| * | ||||||||
| * @param writerName the writer name, or null for default | ||||||||
| * @return the response writer, never null | ||||||||
| * @deprecated Use {@link | ||||||||
| * org.apache.solr.response.BuiltInResponseWriterRegistry#getWriter(String)} instead. | ||||||||
| */ | ||||||||
| @Deprecated | ||||||||
| public static QueryResponseWriter getAdminResponseWriter(String writerName) { | ||||||||
| return org.apache.solr.response.BuiltInResponseWriterRegistry.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<>(); | ||||||||
|
|
||||||||
| // Load writers from ImplicitPlugins.json | ||||||||
| List<PluginInfo> implicitWriters = getImplicitResponseWriters(); | ||||||||
| for (PluginInfo info : implicitWriters) { | ||||||||
| try { | ||||||||
| QueryResponseWriter writer = | ||||||||
| createInstance( | ||||||||
| info.className, | ||||||||
| QueryResponseWriter.class, | ||||||||
| "queryResponseWriter", | ||||||||
| null, | ||||||||
|
||||||||
| null, | |
| this, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| getResourceLoader()); | |
| getResourceLoader()); | |
| initPlugin(info, writer); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, I don't know why this is "special". I looked, and I think we could just create a class called FileStreamWriter instead of this special magic. Claude agreed with me and even cranked out a class. I think that would simplify the code flow. Maybe we call it REPLICATION_FILE_STREAM or REPLICATION_STREAM. We don't have to fix in this, but it's a candiate for an improvement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad you looked into this. Seems in-scope to include.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great... Going to roll that into this PR.
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SolrCore.initWriters() swallows exceptions when loading implicit response writers (logs warn and continues). If the "standard" writer (or any required default) fails to load, responseWriters may end up with no default (PluginBag#setDefault is a no-op when the name is missing), violating the “Never null” contract of getQueryResponseWriter(..) and potentially causing NPEs later. Consider failing fast when required writers can’t be loaded, or explicitly ensure a default writer is present (e.g., fall back to a built-in JSON writer) before continuing startup.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ | |
| import org.apache.solr.common.util.EnvUtils; | ||
| import org.apache.solr.core.CoreContainer; | ||
| import org.apache.solr.core.SolrCore; | ||
| import org.apache.solr.response.BuiltInResponseWriterRegistry; | ||
| import org.apache.solr.response.QueryResponseWriter; | ||
| import org.apache.solr.schema.IndexSchema; | ||
| import org.apache.solr.search.SolrIndexSearcher; | ||
|
|
@@ -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. */ | ||
|
|
@@ -195,16 +196,22 @@ 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. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think this is saying that the |
||
| * | ||
| * <p>If a core is available, uses the core's response writer registry (loaded from | ||
| * ImplicitPlugins.json with ConfigOverlay support). If no core is available (e.g., for | ||
| * admin/container requests), uses a minimal set of admin-appropriate writers. | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. admin thing again.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe in javadocs over-specifying implementation details (like added here). Let us be free to change the implementation details without maintaining these docs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i pruned it back some... all the javadocs in this class are fairly extensive... |
||
| */ | ||
| 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 BuiltInResponseWriterRegistry.getWriter(wt); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| /* | ||
| * 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 static org.apache.solr.handler.admin.MetricsHandler.OPEN_METRICS_WT; | ||
| import static org.apache.solr.handler.admin.MetricsHandler.PROMETHEUS_METRICS_WT; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
| import org.apache.solr.common.params.CommonParams; | ||
|
|
||
| /** | ||
| * Registry for built-in response writers in Solr. | ||
| * | ||
| * <p>Manages a minimal set of essential response writers that are always available, regardless of | ||
| * core configuration or ImplicitPlugins.json settings. These writers are primarily used by | ||
| * admin/container-level requests that have no associated SolrCore. | ||
| * | ||
| * <p>Built-in writers include essential formats needed by admin APIs and core functionality: | ||
| * javabin, json, xml, prometheus, and openmetrics. | ||
| * | ||
| * <p>For core-specific requests that need access to the full set of response writers (including | ||
| * csv, geojson, graphml, smile, etc.), use the SolrCore's response writer registry which is loaded | ||
| * from ImplicitPlugins.json and supports ConfigOverlay customizations. | ||
| * | ||
| * @since 11.0 | ||
| */ | ||
| public class BuiltInResponseWriterRegistry { | ||
|
|
||
| /** | ||
| * Built-in response writers that are always available. | ||
| * | ||
| * <p>Contains only essential formats needed by admin APIs and core functionality: | ||
| * | ||
| * <ul> | ||
| * <li><b>javabin</b> - Binary format, efficient for SolrJ clients | ||
| * <li><b>json/standard</b> - JSON format, default for most requests | ||
| * <li><b>xml</b> - XML format, provides backward compatibility | ||
| * <li><b>prometheus/openmetrics</b> - Required by metrics endpoints | ||
| * </ul> | ||
| */ | ||
| private static final Map<String, QueryResponseWriter> BUILTIN_WRITERS; | ||
|
|
||
| static { | ||
| // Initialize built-in writers that are always available | ||
| Map<String, QueryResponseWriter> builtinWriters = new HashMap<>(6, 1); | ||
| builtinWriters.put(CommonParams.JAVABIN, new JavaBinResponseWriter()); | ||
| builtinWriters.put(CommonParams.JSON, new JacksonJsonWriter()); | ||
| builtinWriters.put("standard", builtinWriters.get(CommonParams.JSON)); // Alias for JSON | ||
| builtinWriters.put("xml", new XMLResponseWriter()); | ||
| builtinWriters.put(PROMETHEUS_METRICS_WT, new PrometheusResponseWriter()); | ||
| builtinWriters.put(OPEN_METRICS_WT, new PrometheusResponseWriter()); | ||
| BUILTIN_WRITERS = Collections.unmodifiableMap(builtinWriters); | ||
| } | ||
|
|
||
| /** | ||
| * Gets a built-in response writer. | ||
| * | ||
| * <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. | ||
| * | ||
| * @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) | ||
| */ | ||
| public static QueryResponseWriter getWriter(String writerName) { | ||
| // carrying over this "standard" thing from original code, but do we want this? null/blank | ||
| // means standard? | ||
| // feels like null or blank should throw an exception. And a method should be added that is | ||
| // getWriter() that | ||
| // returns the json guy. I hate passing around nulls and ""... | ||
| if (writerName == null || writerName.isEmpty()) { | ||
| return BUILTIN_WRITERS.get("standard"); | ||
| } | ||
| return BUILTIN_WRITERS.getOrDefault(writerName, BUILTIN_WRITERS.get("standard")); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the set of built-in writer names. | ||
| * | ||
| * <p>This is useful for debugging, testing, and API documentation. The returned set is immutable. | ||
| * | ||
| * @return unmodifiable set of built-in writer names | ||
| */ | ||
| static java.util.Set<String> getWriterNames() { | ||
| return BUILTIN_WRITERS.keySet(); | ||
| } | ||
|
|
||
| /** | ||
| * Checks if a writer is available as a built-in writer. | ||
| * | ||
| * @param writerName the writer name to check | ||
| * @return true if the writer is available as a built-in writer | ||
| */ | ||
| static boolean hasWriter(String writerName) { | ||
| return BUILTIN_WRITERS.containsKey(writerName); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -85,6 +85,7 @@ | |||
| import org.apache.solr.request.SolrQueryRequestBase; | ||||
| import org.apache.solr.request.SolrRequestHandler; | ||||
| import org.apache.solr.request.SolrRequestInfo; | ||||
| import org.apache.solr.response.BuiltInResponseWriterRegistry; | ||||
| import org.apache.solr.response.QueryResponseWriter; | ||||
| import org.apache.solr.response.SolrQueryResponse; | ||||
| import org.apache.solr.security.AuditEvent; | ||||
|
|
@@ -735,8 +736,9 @@ protected void logAndFlushAdminRequest(SolrQueryResponse solrResp) throws IOExce | |||
| solrResp.getToLogAsString("[admin]")); | ||||
| } | ||||
| } | ||||
| // Admin requests have no core, use built-in writers | ||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be calling these "Cluster requests" or "Builtin requests"? Admin isn't really a very precise term since we would call "Add a shard" a "admin request"....? Or make sure that when we say "Admin" we mean "Solr Cluster Administration"???
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "node/container" |
||||
| QueryResponseWriter respWriter = | ||||
| SolrCore.DEFAULT_RESPONSE_WRITERS.get(solrReq.getParams().get(CommonParams.WT)); | ||||
| BuiltInResponseWriterRegistry.getWriter(solrReq.getParams().get(CommonParams.WT)); | ||||
| if (respWriter == null) respWriter = getResponseWriter(); | ||||
|
||||
| if (respWriter == null) respWriter = getResponseWriter(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a fair comment. It's that werid "standard" thing. We should either solve the null or delete this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to redo this... Claude generated (and committed! bad claude!) this...