You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@solr.apache.org by ge...@apache.org on 2023/01/30 10:27:46 UTC
[solr] branch main updated: SOLR-16615: Reinstate Jersey app-per-configset (#1314)
This is an automated email from the ASF dual-hosted git repository.
gerlowskija pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new 73c938ad2ce SOLR-16615: Reinstate Jersey app-per-configset (#1314)
73c938ad2ce is described below
commit 73c938ad2ce70a91e81848f2e810315a273bb7d4
Author: Jason Gerlowski <ge...@apache.org>
AuthorDate: Mon Jan 30 05:27:36 2023 -0500
SOLR-16615: Reinstate Jersey app-per-configset (#1314)
Previously reverted due to a large bug that crept in while resolving
some merge conflicts, this PR adds the app-per-configset code back.
Jersey 'ApplicationHandlers' are now shared by cores in the same JVM
with the same configset.
---
solr/CHANGES.txt | 2 +
.../src/java/org/apache/solr/api/V2HttpCall.java | 31 ++++++--
.../java/org/apache/solr/core/ConfigOverlay.java | 24 +++---
.../java/org/apache/solr/core/CoreContainer.java | 7 ++
.../src/java/org/apache/solr/core/PluginBag.java | 29 +++++--
.../src/java/org/apache/solr/core/SolrConfig.java | 30 ++++++-
.../src/java/org/apache/solr/core/SolrCore.java | 29 +++++--
.../org/apache/solr/core/SolrResourceLoader.java | 22 +++++-
.../org/apache/solr/handler/SolrConfigHandler.java | 7 +-
.../designer/SchemaDesignerSettingsDAO.java | 2 +-
.../org/apache/solr/jersey/InjectionFactories.java | 22 ++++++
.../apache/solr/jersey/JerseyAppHandlerCache.java | 59 ++++++++++++++
.../org/apache/solr/jersey/JerseyApplications.java | 21 ++---
.../org/apache/solr/jersey/MetricBeanFactory.java | 55 -------------
.../org/apache/solr/jersey/RequestContextKeys.java | 2 +
.../apache/solr/jersey/RequestMetricHandling.java | 34 +++++---
.../solr/jersey/JerseyApplicationSharingTest.java | 92 ++++++++++++++++++++++
17 files changed, 342 insertions(+), 126 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 8f5bcafe43a..7a7660c7082 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -138,6 +138,8 @@ Optimizations
* SOLR-15616: Allow thread metrics to be cached (Ishan Chattopadhyaya, ab)
+* SOLR-16615: Jersey 'ApplicationHandlers' are now shared by compatible cores where possible (Jason Gerlowski, Houston Putman)
+
Bug Fixes
---------------------
diff --git a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
index 18b3c68a271..9595cb08fb8 100644
--- a/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
+++ b/solr/core/src/java/org/apache/solr/api/V2HttpCall.java
@@ -335,14 +335,19 @@ public class V2HttpCall extends HttpSolrCall {
}
private boolean invokeJerseyRequest(
- CoreContainer cores, SolrCore core, ApplicationHandler primary, SolrQueryResponse rsp) {
- return invokeJerseyRequest(cores, core, primary, rsp, Map.of());
+ CoreContainer cores,
+ SolrCore core,
+ ApplicationHandler jerseyHandler,
+ PluginBag<SolrRequestHandler> requestHandlers,
+ SolrQueryResponse rsp) {
+ return invokeJerseyRequest(cores, core, jerseyHandler, requestHandlers, rsp, Map.of());
}
private boolean invokeJerseyRequest(
CoreContainer cores,
SolrCore core,
ApplicationHandler jerseyHandler,
+ PluginBag<SolrRequestHandler> requestHandlers,
SolrQueryResponse rsp,
Map<String, String> additionalProperties) {
final ContainerRequest containerRequest =
@@ -353,6 +358,8 @@ public class V2HttpCall extends HttpSolrCall {
containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_REQUEST, solrReq);
containerRequest.setProperty(RequestContextKeys.SOLR_QUERY_RESPONSE, rsp);
containerRequest.setProperty(RequestContextKeys.CORE_CONTAINER, cores);
+ containerRequest.setProperty(
+ RequestContextKeys.RESOURCE_TO_RH_MAPPING, requestHandlers.getJaxrsRegistry());
containerRequest.setProperty(RequestContextKeys.HTTP_SERVLET_REQ, req);
containerRequest.setProperty(RequestContextKeys.REQUEST_TYPE, requestType);
containerRequest.setProperty(RequestContextKeys.SOLR_PARAMS, queryParams);
@@ -399,7 +406,12 @@ public class V2HttpCall extends HttpSolrCall {
SolrQueryResponse solrResp = new SolrQueryResponse();
final boolean jerseyResourceFound =
invokeJerseyRequest(
- cores, null, cores.getJerseyApplicationHandler(), solrResp, suppressNotFoundProp);
+ cores,
+ null,
+ cores.getJerseyApplicationHandler(),
+ cores.getRequestHandlers(),
+ solrResp,
+ suppressNotFoundProp);
if (jerseyResourceFound) {
logAndFlushAdminRequest(solrResp);
return;
@@ -413,7 +425,8 @@ public class V2HttpCall extends HttpSolrCall {
@Override
protected void handleAdmin(SolrQueryResponse solrResp) {
if (api == null) {
- invokeJerseyRequest(cores, null, cores.getJerseyApplicationHandler(), solrResp);
+ invokeJerseyRequest(
+ cores, null, cores.getJerseyApplicationHandler(), cores.getRequestHandlers(), solrResp);
} else {
SolrCore.preDecorateResponse(solrReq, solrResp);
try {
@@ -447,10 +460,16 @@ public class V2HttpCall extends HttpSolrCall {
Map.of(RequestContextKeys.SUPPRESS_ERROR_ON_NOT_FOUND_EXCEPTION, "true");
final boolean resourceFound =
invokeJerseyRequest(
- cores, core, core.getJerseyApplicationHandler(), rsp, suppressNotFoundProp);
+ cores,
+ core,
+ core.getJerseyApplicationHandler(),
+ core.getRequestHandlers(),
+ rsp,
+ suppressNotFoundProp);
if (!resourceFound) {
response.getHeaderNames().stream().forEach(name -> response.setHeader(name, null));
- invokeJerseyRequest(cores, null, cores.getJerseyApplicationHandler(), rsp);
+ invokeJerseyRequest(
+ cores, null, cores.getJerseyApplicationHandler(), cores.getRequestHandlers(), rsp);
}
} else {
SolrCore.preDecorateResponse(solrReq, rsp);
diff --git a/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java b/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
index acfa64941e1..478ab92f188 100644
--- a/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
+++ b/solr/core/src/java/org/apache/solr/core/ConfigOverlay.java
@@ -34,15 +34,15 @@ import org.apache.solr.common.util.Utils;
* performed on tbhis gives a new copy of the object with the changed value
*/
public class ConfigOverlay implements MapSerializable {
- private final int znodeVersion;
+ private final int version;
private final Map<String, Object> data;
private Map<String, Object> props;
private Map<String, Object> userProps;
@SuppressWarnings({"unchecked"})
- public ConfigOverlay(Map<String, Object> jsonObj, int znodeVersion) {
+ public ConfigOverlay(Map<String, Object> jsonObj, int version) {
if (jsonObj == null) jsonObj = Collections.emptyMap();
- this.znodeVersion = znodeVersion;
+ this.version = version;
data = Collections.unmodifiableMap(jsonObj);
props = (Map<String, Object>) data.get("props");
if (props == null) props = Collections.emptyMap();
@@ -71,7 +71,7 @@ public class ConfigOverlay implements MapSerializable {
copy.put(key, val);
Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
jsonObj.put("userProps", copy);
- return new ConfigOverlay(jsonObj, znodeVersion);
+ return new ConfigOverlay(jsonObj, version);
}
public ConfigOverlay unsetUserProperty(String key) {
@@ -80,7 +80,7 @@ public class ConfigOverlay implements MapSerializable {
copy.remove(key);
Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
jsonObj.put("userProps", copy);
- return new ConfigOverlay(jsonObj, znodeVersion);
+ return new ConfigOverlay(jsonObj, version);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@@ -103,7 +103,7 @@ public class ConfigOverlay implements MapSerializable {
Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
jsonObj.put("props", deepCopy);
- return new ConfigOverlay(jsonObj, znodeVersion);
+ return new ConfigOverlay(jsonObj, version);
}
public static final String NOT_EDITABLE = "''{0}'' is not an editable property";
@@ -139,15 +139,15 @@ public class ConfigOverlay implements MapSerializable {
Map<String, Object> jsonObj = new LinkedHashMap<>(this.data);
jsonObj.put("props", deepCopy);
- return new ConfigOverlay(jsonObj, znodeVersion);
+ return new ConfigOverlay(jsonObj, version);
}
public byte[] toByteArray() {
return Utils.toJSON(data);
}
- public int getZnodeVersion() {
- return znodeVersion;
+ public int getVersion() {
+ return version;
}
@Override
@@ -236,7 +236,7 @@ public class ConfigOverlay implements MapSerializable {
@Override
public Map<String, Object> toMap(Map<String, Object> map) {
- map.put(ZNODEVER, znodeVersion);
+ map.put(ZNODEVER, version);
map.putAll(data);
return map;
}
@@ -258,7 +258,7 @@ public class ConfigOverlay implements MapSerializable {
Map<String, Object> existing = (Map<String, Object>) dataCopy.get(typ);
if (existing == null) dataCopy.put(typ, existing = new LinkedHashMap<>());
existing.put(info.get(CoreAdminParams.NAME).toString(), info);
- return new ConfigOverlay(dataCopy, this.znodeVersion);
+ return new ConfigOverlay(dataCopy, this.version);
}
@SuppressWarnings({"unchecked"})
@@ -267,7 +267,7 @@ public class ConfigOverlay implements MapSerializable {
Map<?, ?> reqHandler = (Map<?, ?>) dataCopy.get(typ);
if (reqHandler == null) return this;
reqHandler.remove(name);
- return new ConfigOverlay(dataCopy, this.znodeVersion);
+ return new ConfigOverlay(dataCopy, this.version);
}
public static final String ZNODEVER = "znodeVersion";
diff --git a/solr/core/src/java/org/apache/solr/core/CoreContainer.java b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
index 666536abc72..21da510c60d 100644
--- a/solr/core/src/java/org/apache/solr/core/CoreContainer.java
+++ b/solr/core/src/java/org/apache/solr/core/CoreContainer.java
@@ -129,6 +129,7 @@ import org.apache.solr.handler.api.V2ApiUtils;
import org.apache.solr.handler.component.ShardHandlerFactory;
import org.apache.solr.handler.designer.SchemaDesignerAPI;
import org.apache.solr.jersey.InjectionFactories;
+import org.apache.solr.jersey.JerseyAppHandlerCache;
import org.apache.solr.logging.LogWatcher;
import org.apache.solr.logging.MDCLoggingContext;
import org.apache.solr.metrics.SolrCoreMetricManager;
@@ -192,11 +193,16 @@ public class CoreContainer {
new PluginBag<>(SolrRequestHandler.class, null);
private volatile ApplicationHandler jerseyAppHandler;
+ private volatile JerseyAppHandlerCache appHandlersByConfigSetId;
public ApplicationHandler getJerseyApplicationHandler() {
return jerseyAppHandler;
}
+ public JerseyAppHandlerCache getJerseyAppHandlerCache() {
+ return appHandlersByConfigSetId;
+ }
+
/** Minimize exposure to CoreContainer. Mostly only ZK interface is required */
public final Supplier<SolrZkClient> zkClientSupplier = () -> getZkController().getZkClient();
@@ -407,6 +413,7 @@ public class CoreContainer {
ExecutorUtil.newMDCAwareCachedThreadPool(
cfg.getReplayUpdatesThreads(),
new SolrNamedThreadFactory("replayUpdatesExecutor")));
+ this.appHandlersByConfigSetId = new JerseyAppHandlerCache();
SolrPaths.AllowPathBuilder allowPathBuilder = new SolrPaths.AllowPathBuilder();
allowPathBuilder.addPath(cfg.getSolrHome());
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 3711e2c0f17..7e06449ca5a 100644
--- a/solr/core/src/java/org/apache/solr/core/PluginBag.java
+++ b/solr/core/src/java/org/apache/solr/core/PluginBag.java
@@ -69,26 +69,38 @@ public class PluginBag<T> implements AutoCloseable {
private final ApiBag apiBag;
private final ResourceConfig jerseyResources;
- public static class JerseyMetricsLookupRegistry
+ /**
+ * Allows JAX-RS 'filters' to find the requestHandler (if any) associated particular JAX-RS
+ * resource classes
+ *
+ * <p>Used primarily by JAX-RS when recording per-request metrics, which requires a {@link
+ * org.apache.solr.handler.RequestHandlerBase.HandlerMetrics} object from the relevant
+ * requestHandler.
+ */
+ public static class JaxrsResourceToHandlerMappings
extends HashMap<Class<? extends JerseyResource>, RequestHandlerBase> {}
- private final JerseyMetricsLookupRegistry infoBeanByResource;
+ private final JaxrsResourceToHandlerMappings jaxrsResourceRegistry;
+
+ public JaxrsResourceToHandlerMappings getJaxrsRegistry() {
+ return jaxrsResourceRegistry;
+ }
/** Pass needThreadSafety=true if plugins can be added and removed concurrently with lookups. */
public PluginBag(Class<T> klass, SolrCore core, boolean needThreadSafety) {
if (klass == SolrRequestHandler.class && V2ApiUtils.isEnabled()) {
this.loadV2ApisIfPresent = true;
this.apiBag = new ApiBag(core != null);
- this.infoBeanByResource = new JerseyMetricsLookupRegistry();
+ this.jaxrsResourceRegistry = new JaxrsResourceToHandlerMappings();
this.jerseyResources =
(core == null)
- ? new JerseyApplications.CoreContainerApp(infoBeanByResource)
- : new JerseyApplications.SolrCoreApp(core, infoBeanByResource);
+ ? new JerseyApplications.CoreContainerApp()
+ : new JerseyApplications.SolrCoreApp();
} else {
this.loadV2ApisIfPresent = false;
this.apiBag = null;
this.jerseyResources = null;
- this.infoBeanByResource = null;
+ this.jaxrsResourceRegistry = null;
}
this.core = core;
this.klass = klass;
@@ -250,10 +262,11 @@ public class PluginBag<T> implements AutoCloseable {
log.debug("Registering jersey resource class: {}", jerseyClazz.getName());
}
jerseyResources.register(jerseyClazz);
- // See MetricsBeanFactory javadocs for a better understanding of this resource->RH
+ // See RequestMetricHandling javadocs for a better understanding of this
+ // resource->RH
// mapping
if (inst instanceof RequestHandlerBase) {
- infoBeanByResource.put(jerseyClazz, (RequestHandlerBase) inst);
+ jaxrsResourceRegistry.put(jerseyClazz, (RequestHandlerBase) inst);
}
}
}
diff --git a/solr/core/src/java/org/apache/solr/core/SolrConfig.java b/solr/core/src/java/org/apache/solr/core/SolrConfig.java
index a3be18156c5..d5f9769ada1 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrConfig.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrConfig.java
@@ -108,6 +108,7 @@ public class SolrConfig implements MapSerializable {
private int znodeVersion;
ConfigNode root;
+ int rootDataHashCode;
private final SolrResourceLoader resourceLoader;
private Properties substituteProperties;
@@ -178,8 +179,12 @@ public class SolrConfig implements MapSerializable {
ZkSolrResourceLoader.ZkByteArrayInputStream zkin =
(ZkSolrResourceLoader.ZkByteArrayInputStream) in;
zkVersion = zkin.getStat().getVersion();
- hash = Objects.hash(zkin.getStat().getCtime(), zkVersion, overlay.getZnodeVersion());
+ hash = Objects.hash(zkin.getStat().getCtime(), zkVersion, overlay.getVersion());
this.fileName = zkin.fileName;
+ } else if (in instanceof SolrResourceLoader.SolrFileInputStream) {
+ SolrResourceLoader.SolrFileInputStream sfin = (SolrResourceLoader.SolrFileInputStream) in;
+ zkVersion = (int) sfin.getLastModified();
+ hash = Objects.hash(sfin.getLastModified(), overlay.getVersion());
}
}
@@ -235,6 +240,9 @@ public class SolrConfig implements MapSerializable {
}
});
try {
+ // This will hash the solrconfig.xml and user properties.
+ rootDataHashCode = this.root.txt().hashCode();
+
getRequestParams();
initLibs(loader, isConfigsetTrusted);
String val =
@@ -576,11 +584,17 @@ public class SolrConfig implements MapSerializable {
return new ConfigOverlay(Collections.emptyMap(), -1);
}
- int version = 0; // will be always 0 for file based resourceLoader
+ int version = 0;
if (in instanceof ZkSolrResourceLoader.ZkByteArrayInputStream) {
version = ((ZkSolrResourceLoader.ZkByteArrayInputStream) in).getStat().getVersion();
log.debug("Config overlay loaded. version : {} ", version);
}
+ if (in instanceof SolrResourceLoader.SolrFileInputStream) {
+ // We should be ok, it is unlikely that a configOverlay is loaded decades apart and has the
+ // same version after casting to an int
+ version = (int) ((SolrResourceLoader.SolrFileInputStream) in).getLastModified();
+ log.debug("Config overlay loaded. version : {} ", version);
+ }
@SuppressWarnings("unchecked")
Map<String, Object> m = (Map<String, Object>) Utils.fromJSON(in);
return new ConfigOverlay(m, version);
@@ -1162,4 +1176,16 @@ public class SolrConfig implements MapSerializable {
public ConfigNode get(String name, Predicate<ConfigNode> test) {
return root.get(name, test);
}
+
+ /**
+ * Generates a String ID to represent the {@link SolrConfig}
+ *
+ * <p>Relies on the name of the SolrConfig, {@link String#hashCode()} to generate a "unique" id
+ * for the solr.xml data (including substitutions), and the version of the overlay. These 3 pieces
+ * of data should combine to make a "unique" identifier for SolrConfigs, since those are
+ * ultimately all inputs to modifying the solr.xml result.
+ */
+ public String effectiveId() {
+ return getName() + "-" + znodeVersion + "-" + rootDataHashCode;
+ }
}
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 75cb384d4fd..a8b6450cb8c 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -1136,10 +1136,23 @@ public class SolrCore implements SolrInfoBean, Closeable {
updateProcessorChains = loadUpdateProcessorChains();
reqHandlers = new RequestHandlers(this);
reqHandlers.initHandlersFromConfig(solrConfig);
- jerseyAppHandler =
- (V2ApiUtils.isEnabled())
- ? new ApplicationHandler(reqHandlers.getRequestHandlers().getJerseyEndpoints())
- : null;
+ if (V2ApiUtils.isEnabled()) {
+ final String effectiveConfigSetId = configSet.getName() + "-" + solrConfig.effectiveId();
+ jerseyAppHandler =
+ coreContainer
+ .getJerseyAppHandlerCache()
+ .computeIfAbsent(
+ effectiveConfigSetId,
+ () -> {
+ log.debug(
+ "Creating Jersey ApplicationHandler for 'effective solrConfig' [{}]",
+ effectiveConfigSetId);
+ return new ApplicationHandler(
+ reqHandlers.getRequestHandlers().getJerseyEndpoints());
+ });
+ } else {
+ jerseyAppHandler = null;
+ }
// cause the executor to stall so firstSearcher events won't fire
// until after inform() has been called for all components.
@@ -3390,8 +3403,8 @@ public class SolrCore implements SolrInfoBean, Closeable {
if (solrCore == null || solrCore.isClosed() || solrCore.getCoreContainer().isShutDown())
return;
cfg = solrCore.getSolrConfig();
- solrConfigversion = solrCore.getSolrConfig().getOverlay().getZnodeVersion();
- overlayVersion = solrCore.getSolrConfig().getZnodeVersion();
+ solrConfigversion = solrCore.getSolrConfig().getZnodeVersion();
+ overlayVersion = solrCore.getSolrConfig().getOverlay().getVersion();
if (managedSchmaResourcePath != null) {
managedSchemaVersion =
((ManagedIndexSchema) solrCore.getLatestSchema()).getSchemaZkVersion();
@@ -3400,8 +3413,8 @@ public class SolrCore implements SolrInfoBean, Closeable {
if (cfg != null) {
cfg.refreshRequestParams();
}
- if (checkStale(zkClient, overlayPath, solrConfigversion)
- || checkStale(zkClient, solrConfigPath, overlayVersion)
+ if (checkStale(zkClient, overlayPath, overlayVersion)
+ || checkStale(zkClient, solrConfigPath, solrConfigversion)
|| checkStale(zkClient, managedSchmaResourcePath, managedSchemaVersion)) {
log.info("core reload {}", coreName);
SolrConfigHandler configHandler = ((SolrConfigHandler) core.getRequestHandler("/config"));
diff --git a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
index 39a8801f746..8e5ea021eac 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrResourceLoader.java
@@ -19,6 +19,7 @@ package org.apache.solr.core;
import com.google.common.annotations.VisibleForTesting;
import java.io.Closeable;
import java.io.File;
+import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
@@ -358,11 +359,11 @@ public class SolrResourceLoader
// The resource is either inside instance dir or we allow unsafe loading, so allow testing if
// file exists
if (Files.exists(inConfigDir) && Files.isReadable(inConfigDir)) {
- return Files.newInputStream(inConfigDir);
+ return new SolrFileInputStream(inConfigDir);
}
if (Files.exists(inInstanceDir) && Files.isReadable(inInstanceDir)) {
- return Files.newInputStream(inInstanceDir);
+ return new SolrFileInputStream(inInstanceDir);
}
}
@@ -984,4 +985,21 @@ public class SolrResourceLoader
// This is to verify if this requires to use the schema classloader for classes loaded from
// packages
private static final ThreadLocal<ResourceLoaderAware> CURRENT_AWARE = new ThreadLocal<>();
+
+ public static class SolrFileInputStream extends FilterInputStream {
+ private final long lastModified;
+
+ public SolrFileInputStream(Path filePath) throws IOException {
+ this(Files.newInputStream(filePath), Files.getLastModifiedTime(filePath).toMillis());
+ }
+
+ public SolrFileInputStream(InputStream delegate, long lastModified) {
+ super(delegate);
+ this.lastModified = lastModified;
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+ }
}
diff --git a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
index b83fdb57839..aeb17522b8d 100644
--- a/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
+++ b/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
@@ -213,13 +213,12 @@ public class SolrConfigHandler extends RequestHandlerBase
resp.add(
ZNODEVER,
Map.of(
- ConfigOverlay.NAME,
- req.getCore().getSolrConfig().getOverlay().getZnodeVersion(),
+ ConfigOverlay.NAME, req.getCore().getSolrConfig().getOverlay().getVersion(),
RequestParams.NAME,
req.getCore().getSolrConfig().getRequestParams().getZnodeVersion()));
boolean isStale = false;
int expectedVersion = req.getParams().getInt(ConfigOverlay.NAME, -1);
- int actualVersion = req.getCore().getSolrConfig().getOverlay().getZnodeVersion();
+ int actualVersion = req.getCore().getSolrConfig().getOverlay().getVersion();
if (expectedVersion > actualVersion) {
log.info(
"expecting overlay version {} but my version is {}",
@@ -589,7 +588,7 @@ public class SolrConfigHandler extends RequestHandlerBase
int latestVersion =
ZkController.persistConfigResourceToZooKeeper(
(ZkSolrResourceLoader) loader,
- overlay.getZnodeVersion(),
+ overlay.getVersion(),
ConfigOverlay.RESOURCE_NAME,
overlay.toByteArray(),
true);
diff --git a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java
index 63e11ea4f5d..eea151dc69d 100644
--- a/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java
+++ b/solr/core/src/java/org/apache/solr/handler/designer/SchemaDesignerSettingsDAO.java
@@ -99,7 +99,7 @@ class SchemaDesignerSettingsDAO implements SchemaDesignerConstants {
if (changed) {
ZkController.persistConfigResourceToZooKeeper(
zkLoaderForConfigSet(configSet),
- overlay.getZnodeVersion(),
+ overlay.getVersion(),
ConfigOverlay.RESOURCE_NAME,
overlay.toByteArray(),
true);
diff --git a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
index ee430c86258..758f32adbe3 100644
--- a/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
+++ b/solr/core/src/java/org/apache/solr/jersey/InjectionFactories.java
@@ -17,8 +17,11 @@
package org.apache.solr.jersey;
+import static org.apache.solr.jersey.RequestContextKeys.SOLR_CORE;
+
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
+import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.glassfish.hk2.api.Factory;
@@ -62,6 +65,25 @@ public class InjectionFactories {
public void dispose(SolrQueryResponse instance) {}
}
+ /** Fetch the (existing) SolrCore from the request context */
+ public static class ReuseFromContextSolrCoreFactory implements Factory<SolrCore> {
+
+ private final ContainerRequestContext containerRequestContext;
+
+ @Inject
+ public ReuseFromContextSolrCoreFactory(ContainerRequestContext containerRequestContext) {
+ this.containerRequestContext = containerRequestContext;
+ }
+
+ @Override
+ public SolrCore provide() {
+ return (SolrCore) containerRequestContext.getProperty(SOLR_CORE);
+ }
+
+ @Override
+ public void dispose(SolrCore instance) {}
+ }
+
public static class SingletonFactory<T> implements Factory<T> {
private final T singletonVal;
diff --git a/solr/core/src/java/org/apache/solr/jersey/JerseyAppHandlerCache.java b/solr/core/src/java/org/apache/solr/jersey/JerseyAppHandlerCache.java
new file mode 100644
index 00000000000..85e1e5098f7
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/jersey/JerseyAppHandlerCache.java
@@ -0,0 +1,59 @@
+/*
+ * 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.jersey;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.apache.solr.core.ConfigSet;
+import org.apache.solr.core.SolrConfig;
+import org.glassfish.jersey.server.ApplicationHandler;
+
+/**
+ * Stores Jersey 'ApplicationHandler' instances by an ID or hash derived from their {@link
+ * ConfigSet}.
+ *
+ * <p>ApplicationHandler creation is expensive; caching these objects allows them to be shared by
+ * multiple cores with the same configuration.
+ */
+public class JerseyAppHandlerCache {
+
+ private final Cache<String, ApplicationHandler> applicationByConfigSetId =
+ Caffeine.newBuilder().weakValues().build();
+
+ /**
+ * Return the 'ApplicationHandler' associated with the provided ID, creating it first if
+ * necessary.
+ *
+ * <p>This method is thread-safe by virtue of its delegation to {@link Cache#get(Object,
+ * Function)} internally.
+ *
+ * @param effectiveSolrConfigId an ID to associate the ApplicationHandler with. Usually created
+ * via {@link SolrConfig#effectiveId()}.
+ * @param createApplicationHandler a Supplier producing an ApplicationHandler
+ */
+ public ApplicationHandler computeIfAbsent(
+ String effectiveSolrConfigId, Supplier<ApplicationHandler> createApplicationHandler) {
+ return applicationByConfigSetId.get(effectiveSolrConfigId, k -> createApplicationHandler.get());
+ }
+
+ public int size() {
+ return applicationByConfigSetId.asMap().size();
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
index 7a4bb484e4b..42e5f713709 100644
--- a/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
+++ b/solr/core/src/java/org/apache/solr/jersey/JerseyApplications.java
@@ -20,8 +20,6 @@ package org.apache.solr.jersey;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
-import javax.inject.Singleton;
-import org.apache.solr.core.PluginBag;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
@@ -44,7 +42,7 @@ import org.glassfish.jersey.server.ResourceConfig;
public class JerseyApplications {
public static class CoreContainerApp extends ResourceConfig {
- public CoreContainerApp(PluginBag.JerseyMetricsLookupRegistry beanRegistry) {
+ public CoreContainerApp() {
super();
// Authentication and authorization
@@ -63,15 +61,6 @@ public class JerseyApplications {
register(RequestMetricHandling.PreRequestMetricsFilter.class);
register(RequestMetricHandling.PostRequestMetricsFilter.class);
register(PostRequestDecorationFilter.class);
- register(
- new AbstractBinder() {
- @Override
- protected void configure() {
- bindFactory(new MetricBeanFactory(beanRegistry))
- .to(PluginBag.JerseyMetricsLookupRegistry.class)
- .in(Singleton.class);
- }
- });
register(
new AbstractBinder() {
@Override
@@ -102,17 +91,17 @@ public class JerseyApplications {
public static class SolrCoreApp extends CoreContainerApp {
- public SolrCoreApp(SolrCore solrCore, PluginBag.JerseyMetricsLookupRegistry beanRegistry) {
- super(beanRegistry);
+ public SolrCoreApp() {
+ super();
// Dependency Injection for Jersey resources
register(
new AbstractBinder() {
@Override
protected void configure() {
- bindFactory(new InjectionFactories.SingletonFactory<>(solrCore))
+ bindFactory(InjectionFactories.ReuseFromContextSolrCoreFactory.class)
.to(SolrCore.class)
- .in(Singleton.class);
+ .in(RequestScoped.class);
}
});
}
diff --git a/solr/core/src/java/org/apache/solr/jersey/MetricBeanFactory.java b/solr/core/src/java/org/apache/solr/jersey/MetricBeanFactory.java
deleted file mode 100644
index c23851359d9..00000000000
--- a/solr/core/src/java/org/apache/solr/jersey/MetricBeanFactory.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.jersey;
-
-import org.apache.solr.core.PluginBag;
-import org.glassfish.hk2.api.Factory;
-
-/**
- * Factory to inject JerseyMetricsLookupRegistry instances into Jersey resources and filters.
- *
- * <p>Currently, Jersey resources that have a corresponding v1 API produce the same metrics as their
- * v1 equivalent and rely on the v1 requestHandler instance to do so. Solr facilitates this by
- * building a map of the Jersey resource to requestHandler mapping (a {@link
- * org.apache.solr.core.PluginBag.JerseyMetricsLookupRegistry}), and injecting it into the pre- and
- * post- Jersey filters that handle metrics.
- *
- * <p>This isn't ideal, as requestHandler's don't really "fit" conceptually here. But it's
- * unavoidable while we want our v2 APIs to exactly match the metrics produced by v1 calls.
- *
- * @see RequestMetricHandling.PreRequestMetricsFilter
- * @see RequestMetricHandling.PostRequestMetricsFilter
- */
-public class MetricBeanFactory implements Factory<PluginBag.JerseyMetricsLookupRegistry> {
-
- private final PluginBag.JerseyMetricsLookupRegistry metricsLookupRegistry;
-
- public MetricBeanFactory(PluginBag.JerseyMetricsLookupRegistry metricsLookupRegistry) {
- this.metricsLookupRegistry = metricsLookupRegistry;
- }
-
- @Override
- public PluginBag.JerseyMetricsLookupRegistry provide() {
- return metricsLookupRegistry;
- }
-
- @Override
- public void dispose(PluginBag.JerseyMetricsLookupRegistry instance) {
- /* No-op */
- }
-}
diff --git a/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java b/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java
index 68b37aa8758..f300ffa4bbb 100644
--- a/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java
+++ b/solr/core/src/java/org/apache/solr/jersey/RequestContextKeys.java
@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.container.ContainerRequestContext;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.CoreContainer;
+import org.apache.solr.core.PluginBag;
import org.apache.solr.core.SolrCore;
import org.apache.solr.handler.RequestHandlerBase;
import org.apache.solr.request.SolrQueryRequest;
@@ -42,6 +43,7 @@ public interface RequestContextKeys {
String SOLR_QUERY_REQUEST = SolrQueryRequest.class.getName();
String SOLR_QUERY_RESPONSE = SolrQueryResponse.class.getName();
String CORE_CONTAINER = CoreContainer.class.getName();
+ String RESOURCE_TO_RH_MAPPING = PluginBag.JaxrsResourceToHandlerMappings.class.getName();
String SOLR_CORE = SolrCore.class.getName();
String REQUEST_TYPE = AuthorizationContext.RequestType.class.getName();
String SOLR_PARAMS = SolrParams.class.getName();
diff --git a/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java b/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java
index 595027005ad..a6e8b03919e 100644
--- a/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java
+++ b/solr/core/src/java/org/apache/solr/jersey/RequestMetricHandling.java
@@ -24,7 +24,6 @@ import static org.apache.solr.jersey.RequestContextKeys.TIMER;
import com.codahale.metrics.Timer;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
-import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
@@ -40,9 +39,18 @@ import org.slf4j.LoggerFactory;
/**
* A request and response filter used to initialize and report per-request metrics.
*
- * <p>Currently, JAX-RS v2 APIs rely on a {@link
- * org.apache.solr.handler.RequestHandlerBase.HandlerMetrics} instance from an associated request
- * handler.
+ * <p>Currently, Jersey resources that have a corresponding v1 API produce the same metrics as their
+ * v1 equivalent and rely on the v1 requestHandler instance to do so. Solr facilitates this by
+ * building a map of the JAX-RS resources to requestHandler mapping (a {@link
+ * org.apache.solr.core.PluginBag.JaxrsResourceToHandlerMappings}), and using that to look up the
+ * associated request handler (if one exists) in pre- and post- filters
+ *
+ * <p>This isn't ideal, as requestHandler's don't really "fit" conceptually here. But it's
+ * unavoidable while we want our v2 APIs to exactly match the metrics produced by v1 calls, and
+ * while metrics are bundled in with requestHandlers as they are currently.
+ *
+ * @see RequestMetricHandling.PreRequestMetricsFilter
+ * @see RequestMetricHandling.PostRequestMetricsFilter
*/
public class RequestMetricHandling {
@@ -58,16 +66,18 @@ public class RequestMetricHandling {
@Context private ResourceInfo resourceInfo;
- private PluginBag.JerseyMetricsLookupRegistry beanRegistry;
-
- @Inject
- public PreRequestMetricsFilter(PluginBag.JerseyMetricsLookupRegistry beanRegistry) {
- this.beanRegistry = beanRegistry;
- }
-
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
- final RequestHandlerBase handlerBase = beanRegistry.get(resourceInfo.getResourceClass());
+ final PluginBag.JaxrsResourceToHandlerMappings requestHandlerByJerseyResource =
+ (PluginBag.JaxrsResourceToHandlerMappings)
+ requestContext.getProperty(RequestContextKeys.RESOURCE_TO_RH_MAPPING);
+ if (requestHandlerByJerseyResource == null) {
+ log.debug("No jax-rs registry found for request {}", requestContext);
+ return;
+ }
+
+ final RequestHandlerBase handlerBase =
+ requestHandlerByJerseyResource.get(resourceInfo.getResourceClass());
if (handlerBase == null) {
log.debug("No handler found for request {}", requestContext);
return;
diff --git a/solr/core/src/test/org/apache/solr/jersey/JerseyApplicationSharingTest.java b/solr/core/src/test/org/apache/solr/jersey/JerseyApplicationSharingTest.java
new file mode 100644
index 00000000000..df21c0cef8b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/jersey/JerseyApplicationSharingTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.jersey;
+
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Tests ensuring that Jersey apps are shared between cores as expected.
+ *
+ * <p>Jersey applications should be shared by any cores on the same node that have the same
+ * "effective configset" (i.e. the same configset content and any overlays or relevant configuration
+ * properties)
+ */
+public class JerseyApplicationSharingTest extends SolrCloudTestCase {
+
+ private static final String collection = "collection1";
+ private static final String confDir = collection + "/conf";
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ configureCluster(1)
+ .addConfig("conf1", configset("cloud-minimal"))
+ .addConfig("conf2", configset("cloud-minimal"))
+ .configure();
+ }
+
+ @Test
+ public void testMultipleCoresWithSameConfigsetShareApplication() throws Exception {
+ final SolrClient solrClient = cluster.getSolrClient();
+
+ // No applications should be in the cache to start
+ assertJerseyAppCacheHasSize(0);
+
+ // All replicas for the created collection should share a single Jersey ApplicationHandler entry
+ // in the cache
+ final CollectionAdminRequest.Create coll1Create =
+ CollectionAdminRequest.createCollection("coll1", "conf1", 2, 2);
+ assertEquals(0, coll1Create.process(solrClient).getStatus());
+ assertJerseyAppCacheHasSize(1);
+
+ // A new collection using the same configset will also share the existing cached Jersey
+ // ApplicationHandler
+ final CollectionAdminRequest.Create coll2Create =
+ CollectionAdminRequest.createCollection("coll2", "conf1", 2, 2);
+ assertEquals(0, coll2Create.process(solrClient).getStatus());
+ assertJerseyAppCacheHasSize(1);
+
+ // Using a different configset WILL cause a new Jersey ApplicationHandler to be used (total
+ // cache-count = 2)
+ final CollectionAdminRequest.Create coll3Create =
+ CollectionAdminRequest.createCollection("coll3", "conf2", 2, 2);
+ assertEquals(0, coll3Create.process(solrClient).getStatus());
+ assertJerseyAppCacheHasSize(2);
+
+ // Modifying properties that affect a configset will also cause a new Jersey ApplicationHandler
+ // to be created (total cache-count = 3)
+ final CollectionAdminRequest.Create coll4Create =
+ CollectionAdminRequest.createCollection("coll4", "conf1", 2, 2);
+ coll4Create.setProperties(
+ Map.of(
+ "solr.commitwithin.softcommit",
+ "false")); // Set any collection property used in the cloud-minimal configset
+ assertEquals(0, coll4Create.process(solrClient).getStatus());
+ assertJerseyAppCacheHasSize(3);
+ }
+
+ private void assertJerseyAppCacheHasSize(int expectedSize) {
+ assertEquals(
+ expectedSize,
+ cluster.getJettySolrRunners().get(0).getCoreContainer().getJerseyAppHandlerCache().size());
+ }
+}