diff --git a/solr/core/src/java/org/apache/solr/client/solrj/embedded/JettySolrRunner.java b/solr/core/src/java/org/apache/solr/client/solrj/embedded/JettySolrRunner.java index 5f481c48e92..ec8d3c77f0a 100644 --- a/solr/core/src/java/org/apache/solr/client/solrj/embedded/JettySolrRunner.java +++ b/solr/core/src/java/org/apache/solr/client/solrj/embedded/JettySolrRunner.java @@ -23,6 +23,7 @@ import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -69,6 +70,7 @@ import org.apache.solr.handler.admin.CoreAdminOperation; import org.apache.solr.handler.admin.LukeRequestHandler; import org.apache.solr.metrics.SolrMetricManager; +import org.apache.solr.servlet.CoreContainerProvider; import org.apache.solr.servlet.SolrDispatchFilter; import org.apache.solr.util.TimeOut; import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; @@ -113,7 +115,7 @@ public class JettySolrRunner { // NOTE: needs to be larger than SolrHttpClient.threadPoolSweeperMaxIdleTime private static final int THREAD_POOL_MAX_IDLE_TIME_MS = 260000; - Server server; + private Server server; volatile FilterHolder dispatchFilter; volatile FilterHolder debugFilter; @@ -143,6 +145,8 @@ public class JettySolrRunner { private volatile boolean started = false; + private CoreContainerProvider coreContainerProvider; + public static class DebugFilter implements Filter { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -364,7 +368,9 @@ public void lifeCycleStopping(LifeCycle arg0) { } @Override - public void lifeCycleStopped(LifeCycle arg0) {} + public synchronized void lifeCycleStopped(LifeCycle arg0) { + coreContainerProvider.close(); + } @Override public void lifeCycleStarting(LifeCycle arg0) { @@ -372,8 +378,7 @@ public void lifeCycleStarting(LifeCycle arg0) { } @Override - public void lifeCycleStarted(LifeCycle arg0) { - + public synchronized void lifeCycleStarted(LifeCycle arg0) { jettyPort = getFirstConnectorPort(); int port = jettyPort; if (proxyPort != -1) port = proxyPort; @@ -382,7 +387,8 @@ public void lifeCycleStarted(LifeCycle arg0) { root.getServletContext().setAttribute(SolrDispatchFilter.PROPERTIES_ATTRIBUTE, nodeProperties); root.getServletContext().setAttribute(SolrDispatchFilter.SOLRHOME_ATTRIBUTE, solrHome); - + coreContainerProvider = new CoreContainerProvider(); + coreContainerProvider.init(root.getServletContext()); log.info("Jetty properties: {}", nodeProperties); debugFilter = root.addFilter(DebugFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST) ); @@ -455,10 +461,16 @@ protected HandlerWrapper injectJettyHandlers(HandlerWrapper chain) { * @return the {@link CoreContainer} for this node */ public CoreContainer getCoreContainer() { - if (getSolrDispatchFilter() == null || getSolrDispatchFilter().getCores() == null) { - return null; + try { + if (getSolrDispatchFilter() == null || getSolrDispatchFilter().getCores() == null) { + return null; + } + return getSolrDispatchFilter().getCores(); + } catch (UnavailableException e) { + // Since this is only used in tests, this is just a straight-up failure + // If this is converted for other use something else might be better here + throw new RuntimeException(e); } - return getSolrDispatchFilter().getCores(); } public String getNodeName() { @@ -500,7 +512,7 @@ public void start() throws Exception { * * @throws Exception if an error occurs on startup */ - public void start(boolean reusePort) throws Exception { + public synchronized void start(boolean reusePort) throws Exception { // Do not let Jetty/Solr pollute the MDC for this thread Map prevContext = MDC.getCopyOfContextMap(); MDC.clear(); @@ -527,7 +539,7 @@ public void start(boolean reusePort) throws Exception { } synchronized (JettySolrRunner.this) { int cnt = 0; - while (!waitOnSolr || !dispatchFilter.isRunning() || getCoreContainer() == null) { + while (!waitOnSolr || !dispatchFilter.isRunning() ) { this.wait(100); if (cnt++ == 15) { throw new RuntimeException("Jetty/Solr unresponsive"); @@ -561,7 +573,7 @@ public void start(boolean reusePort) throws Exception { private void setProtocolAndHost() { - String protocol = null; + String protocol; Connector[] conns = server.getConnectors(); if (0 == conns.length) { @@ -575,7 +587,7 @@ private void setProtocolAndHost() { this.host = c.getHost(); } - private void retryOnPortBindFailure(int portRetryTime, int port) throws Exception, InterruptedException { + private void retryOnPortBindFailure(int portRetryTime, int port) throws Exception { TimeOut timeout = new TimeOut(portRetryTime, TimeUnit.SECONDS, TimeSource.NANO_TIME); int tryCnt = 1; while (true) { @@ -625,34 +637,12 @@ Exception lookForBindException(IOException ioe) { * * @throws Exception if an error occurs on shutdown */ - public void stop() throws Exception { + public synchronized void stop() throws Exception { // Do not let Jetty/Solr pollute the MDC for this thread Map prevContext = MDC.getCopyOfContextMap(); MDC.clear(); try { Filter filter = dispatchFilter.getFilter(); - - // we want to shutdown outside of jetty cutting us off - SolrDispatchFilter sdf = getSolrDispatchFilter(); - ExecutorService customThreadPool = null; - if (sdf != null) { - customThreadPool = ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("jettyShutDown")); - - sdf.closeOnDestroy(false); -// customThreadPool.submit(() -> { -// try { -// sdf.close(); -// } catch (Throwable t) { -// log.error("Error shutting down Solr", t); -// } -// }); - try { - sdf.close(); - } catch (Throwable t) { - log.error("Error shutting down Solr", t); - } - } - QueuedThreadPool qtp = (QueuedThreadPool) server.getThreadPool(); ReservedThreadExecutor rte = qtp.getBean(ReservedThreadExecutor.class); @@ -687,12 +677,13 @@ public void stop() throws Exception { rte.stop(); TimeOut timeout = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME); - timeout.waitFor("Timeout waiting for reserved executor to stop.", () - -> rte.isStopped()); + timeout.waitFor("Timeout waiting for reserved executor to stop.", rte::isStopped); } - if (customThreadPool != null) { - ExecutorUtil.shutdownAndAwaitTermination(customThreadPool); + // we want to shutdown outside of jetty cutting us off + SolrDispatchFilter sdf = getSolrDispatchFilter(); + if (sdf != null) { + ExecutorUtil.shutdownAndAwaitTermination(getJettyShutDownThreadPool()); } do { @@ -716,6 +707,10 @@ public void stop() throws Exception { } } + private ExecutorService getJettyShutDownThreadPool() { + return ExecutorUtil.newMDCAwareCachedThreadPool(new SolrNamedThreadFactory("jettyShutDown")); + } + public void outputMetrics(File outputDirectory, String fileName) throws IOException { if (getCoreContainer() != null) { @@ -750,12 +745,11 @@ public void dumpCoresInfo(PrintStream pw) throws IOException { NamedList coreStatus = CoreAdminOperation.getCoreStatus(getCoreContainer(), core.getName(), false); core.withSearcher(solrIndexSearcher -> { SimpleOrderedMap lukeIndexInfo = LukeRequestHandler.getIndexInfo(solrIndexSearcher.getIndexReader()); - @SuppressWarnings({"unchecked", "rawtypes"}) Map indexInfoMap = coreStatus.toMap(new LinkedHashMap<>()); indexInfoMap.putAll(lukeIndexInfo.toMap(new LinkedHashMap<>())); pw.println(JSONUtil.toJSON(indexInfoMap, 2)); - pw.println(""); + pw.println(); return null; }); } @@ -897,7 +891,12 @@ public Properties getNodeProperties() { private void waitForLoadingCoresToFinish(long timeoutMs) { if (dispatchFilter != null) { SolrDispatchFilter solrFilter = (SolrDispatchFilter) dispatchFilter.getFilter(); - CoreContainer cores = solrFilter.getCores(); + CoreContainer cores; + try { + cores = solrFilter.getCores(); + } catch (UnavailableException e) { + throw new IllegalStateException("The CoreContainer is unavailable!"); + } if (cores != null) { cores.waitForLoadingCoresToFinish(timeoutMs); } else { diff --git a/solr/core/src/java/org/apache/solr/core/NodeConfig.java b/solr/core/src/java/org/apache/solr/core/NodeConfig.java index cd8bca76740..03195c73392 100644 --- a/solr/core/src/java/org/apache/solr/core/NodeConfig.java +++ b/solr/core/src/java/org/apache/solr/core/NodeConfig.java @@ -16,11 +16,19 @@ */ package org.apache.solr.core; +import org.apache.commons.lang3.StringUtils; import org.apache.lucene.search.IndexSearcher; import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.logging.LogWatcherConfig; + import org.apache.solr.update.UpdateShardHandlerConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; +import java.lang.invoke.MethodHandles; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; @@ -31,6 +39,8 @@ public class NodeConfig { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + // all Path fields here are absolute and normalized. private final String nodeName; @@ -149,6 +159,35 @@ private NodeConfig(String nodeName, Path coreRootDirectory, Path solrDataHome, I if (null == this.loader) throw new NullPointerException("loader"); } + /** + * Get the NodeConfig whether stored on disk, in ZooKeeper, etc. + * This may also be used by custom filters to load relevant configuration. + * @return the NodeConfig + */ + public static NodeConfig loadNodeConfig(Path solrHome, Properties nodeProperties) { + if (!StringUtils.isEmpty(System.getProperty("solr.solrxml.location"))) { + log.warn("Solr property solr.solrxml.location is no longer supported. Will automatically load solr.xml from ZooKeeper if it exists"); + } + nodeProperties = SolrXmlConfig.wrapAndSetZkHostFromSysPropIfNeeded(nodeProperties); + String zkHost = nodeProperties.getProperty(SolrXmlConfig.ZK_HOST); + if (!StringUtils.isEmpty(zkHost)) { + int startUpZkTimeOut = Integer.getInteger("waitForZk", 30); + startUpZkTimeOut *= 1000; + try (SolrZkClient zkClient = new SolrZkClient(zkHost, startUpZkTimeOut, startUpZkTimeOut)) { + if (zkClient.exists("/solr.xml", true)) { + log.info("solr.xml found in ZooKeeper. Loading..."); + byte[] data = zkClient.getData("/solr.xml", null, null, true); + return SolrXmlConfig.fromInputStream(solrHome, new ByteArrayInputStream(data), nodeProperties, true); + } + } catch (Exception e) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Error occurred while loading solr.xml from zookeeper", e); + } + log.info("Loading solr.xml from SolrHome (not found in ZooKeeper)"); + } + + return SolrXmlConfig.fromSolrHome(solrHome, nodeProperties); + } + public String getConfigSetServiceClass() { return this.configSetServiceClass; } diff --git a/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java b/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java new file mode 100644 index 00000000000..08c05a3a6c8 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/servlet/CoreContainerProvider.java @@ -0,0 +1,476 @@ +/* + * 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.servlet; + +import com.codahale.metrics.jvm.ClassLoadingGaugeSet; +import com.codahale.metrics.jvm.GarbageCollectorMetricSet; +import com.codahale.metrics.jvm.MemoryUsageGaugeSet; +import com.codahale.metrics.jvm.ThreadStatesGaugeSet; +import com.google.common.annotations.VisibleForTesting; +import org.apache.http.client.HttpClient; +import org.apache.lucene.util.Version; +import org.apache.solr.cloud.ZkController; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.common.cloud.SolrZkClient; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.core.NodeConfig; +import org.apache.solr.core.SolrCore; +import org.apache.solr.core.SolrInfoBean.Group; +import org.apache.solr.core.SolrXmlConfig; +import org.apache.solr.metrics.AltBufferPoolMetricSet; +import org.apache.solr.metrics.MetricsMap; +import org.apache.solr.metrics.OperatingSystemMetricSet; +import org.apache.solr.metrics.SolrMetricManager; +import org.apache.solr.metrics.SolrMetricManager.ResolutionStrategy; +import org.apache.solr.metrics.SolrMetricProducer; +import org.apache.solr.servlet.RateLimitManager.Builder; +import org.apache.solr.util.StartupLoggingUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.naming.NoInitialContextException; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.UnavailableException; +import java.lang.invoke.MethodHandles; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static org.apache.solr.core.NodeConfig.loadNodeConfig; +import static org.apache.solr.servlet.SolrDispatchFilter.PROPERTIES_ATTRIBUTE; +import static org.apache.solr.servlet.SolrDispatchFilter.SOLRHOME_ATTRIBUTE; +import static org.apache.solr.servlet.SolrDispatchFilter.SOLR_INSTALL_DIR_ATTRIBUTE; +import static org.apache.solr.servlet.SolrDispatchFilter.SOLR_LOG_LEVEL; +import static org.apache.solr.servlet.SolrDispatchFilter.SOLR_LOG_MUTECONSOLE; + +/** + * A service that can provide access to solr cores. This allows us to have multiple filters and + * servlets that depend on SolrCore and CoreContainer, while still only having one CoreContainer per + * instance of solr. + */ +public class CoreContainerProvider implements ServletContextListener { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private final String metricTag = SolrMetricProducer.getUniqueMetricTag(this, null); + private CoreContainer cores; + private Properties extraProperties; + private HttpClient httpClient; + private SolrMetricManager metricManager; + private RateLimitManager rateLimitManager; + private final CountDownLatch init = new CountDownLatch(1); + private String registryName; + private final boolean isV2Enabled = !"true".equals(System.getProperty("disable.v2.api", "false")); + // AFAIK the only reason we need this is to support JettySolrRunner for tests. In tests we might have + // multiple CoreContainers in the same JVM, but I *think* that doesn't happen in a real server. + private static final Map services = + Collections.synchronizedMap(new WeakHashMap<>()); + + // todo: dependency injection instead, but for now this method and the associated map will have to suffice. + // Note that this relies on ServletContext.equals() not implementing anything significantly different + // than Object.equals for its .equals method (I've found no implementation that even implements it). + public static ServiceHolder serviceForContext(ServletContext ctx) throws InterruptedException { + ContextInitializationKey key = new ContextInitializationKey(ctx); + return services.computeIfAbsent(key, ServiceHolder::new); + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + init(sce.getServletContext()); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + close(); + } + + CoreContainer getCoreContainer() throws UnavailableException { + waitForCoreContainer(() -> cores,init); + return cores; + } + HttpClient getHttpClient() throws UnavailableException { + waitForCoreContainer(() -> cores,init); + return httpClient; + } + + private static void waitForCoreContainer(Supplier provider, CountDownLatch latch) throws UnavailableException { + CoreContainer cores = provider.get(); + if (cores == null || cores.isShutDown()) { + long startWait = System.nanoTime(); + try { + while (!latch.await(10, TimeUnit.SECONDS)) { + long now = System.nanoTime(); + if (log.isInfoEnabled()) { + log.info("Still waiting for CoreContainerStartup ({} seconds elapsed)", (now - startWait) / 1_000_000_000); + } + } + } catch (InterruptedException e) { //well, no wait then + Thread.currentThread().interrupt(); + } + cores = provider.get(); + if (cores == null || cores.isShutDown()) { + final String msg = "Error processing the request. CoreContainer is either not initialized or shutting down."; + log.error(msg); + throw new UnavailableException(msg); + } + } + } + + public void close() { + CoreContainer cc = cores; +// if (cc != null) { +// ZkController zkController = cc.getZkController(); +// if (zkController != null) { +// +// // Mark Miller suggested that we should be publishing that we are down before anything else which makes +// // good sense, but the following causes test failures, so that improvement can be the subject of another +// // PR/issue. Also, jetty might already be refusing requests by this point so that's a potential issue too. +// // Digging slightly I see that there's a whole mess of code looking up collections and calculating state +// // changes associated with this call, which smells a lot like we're duplicating node state in collection +// // stuff, but it will take a lot of code reading to figure out if that's really what it is, why we +// // did it and if there's room for improvement. +// +// zkController.publishNodeAsDown(zkController.getNodeName()); +// } +// } + cores = null; + try { + if (metricManager != null) { + try { + metricManager.unregisterGauges(registryName, metricTag); + } catch (NullPointerException e) { + // okay + } catch (Exception e) { + log.warn("Exception closing FileCleaningTracker", e); + } finally { + metricManager = null; + } + } + } finally { + if (cc != null) { + httpClient = null; + cc.shutdown(); + } + } + } + + public void init(ServletContext servletContext) { + if (log.isTraceEnabled()) { + log.trace("CoreService.init(): {}", this.getClass().getClassLoader()); + } + CoreContainer coresInit = null; + try { + // "extra" properties must be initialized first, so we know things like "do we have a zkHost" + // wrap as defaults (if set) so we can modify w/o polluting the Properties provided by our caller + this.extraProperties = SolrXmlConfig.wrapAndSetZkHostFromSysPropIfNeeded + ((Properties) servletContext.getAttribute(PROPERTIES_ATTRIBUTE)); + + StartupLoggingUtils.checkLogDir(); + if (log.isInfoEnabled()) { + log.info("Using logger factory {}", StartupLoggingUtils.getLoggerImplStr()); + } + + logWelcomeBanner(); + + String muteConsole = System.getProperty(SOLR_LOG_MUTECONSOLE); + if (muteConsole != null && !Arrays.asList("false","0","off","no").contains(muteConsole.toLowerCase(Locale.ROOT))) { + StartupLoggingUtils.muteConsole(); + } + String logLevel = System.getProperty(SOLR_LOG_LEVEL); + if (logLevel != null) { + log.info("Log level override, property solr.log.level={}", logLevel); + StartupLoggingUtils.changeLogLevel(logLevel); + } + + coresInit = createCoreContainer(computeSolrHome(servletContext), extraProperties); + this.httpClient = coresInit.getUpdateShardHandler().getDefaultHttpClient(); + setupJvmMetrics(coresInit); + + SolrZkClient zkClient = null; + ZkController zkController = coresInit.getZkController(); + + if (zkController != null) { + zkClient = zkController.getZkClient(); + } + + Builder builder = new Builder(zkClient); + + this.rateLimitManager = builder.build(); + + if (zkController != null) { + zkController.zkStateReader.registerClusterPropertiesListener(this.rateLimitManager); + } + + if (log.isDebugEnabled()) { + log.debug("user.dir={}", System.getProperty("user.dir")); + } + } catch( Throwable t ) { + // catch this so our filter still works + log.error( "Could not start Solr. Check solr/home property and the logs"); + SolrCore.log( t ); + if (t instanceof Error) { + throw (Error) t; + } + } finally{ + log.trace("SolrDispatchFilter.init() done"); + this.cores = coresInit; // crucially final assignment + services.computeIfAbsent(new ContextInitializationKey(servletContext), ServiceHolder::new) + .setService(this); + init.countDown(); + } + } + + + private void logWelcomeBanner() { + // _Really_ sorry about how clumsy this is as a result of the logging call checker, but this is the only one + // that's so ugly so far. + if (log.isInfoEnabled()) { + log.info(" ___ _ Welcome to Apache Solr™ version {}", solrVersion()); + } + if (log.isInfoEnabled()) { + log.info("/ __| ___| |_ _ Starting in {} mode on port {}", isCloudMode() ? "cloud" : "standalone", getSolrPort()); + } + if (log.isInfoEnabled()) { + log.info("\\__ \\/ _ \\ | '_| Install dir: {}", System.getProperty(SOLR_INSTALL_DIR_ATTRIBUTE)); + } + if (log.isInfoEnabled()) { + log.info("|___/\\___/_|_| Start time: {}", Instant.now()); + } + } + private String solrVersion() { + String specVer = Version.LATEST.toString(); + try { + String implVer = SolrCore.class.getPackage().getImplementationVersion(); + return (specVer.equals(implVer.split(" ")[0])) ? specVer : implVer; + } catch (Exception e) { + return specVer; + } + } + + private String getSolrPort() { + return System.getProperty("jetty.port"); + } + + /** + * We are in cloud mode if Java option zkRun exists OR zkHost exists and is non-empty + * @see SolrXmlConfig#wrapAndSetZkHostFromSysPropIfNeeded + * @see #extraProperties + * @see #init + */ + private boolean isCloudMode() { + assert null != extraProperties; // we should never be called w/o this being initialized + return (null != extraProperties.getProperty(SolrXmlConfig.ZK_HOST)) || (null != System.getProperty("zkRun")); + } + + /** + * Returns the effective Solr Home to use for this node, based on looking up the value in this order: + *
    + *
  1. attribute in the FilterConfig
  2. + *
  3. JNDI: via java:comp/env/solr/home
  4. + *
  5. The system property solr.solr.home
  6. + *
  7. Look in the current working directory for a solr/ directory
  8. + *
+ *

+ * + * @return the Solr home, absolute and normalized. + */ + private static Path computeSolrHome(ServletContext servletContext) { + + // start with explicit check of servlet config... + String source = "servlet config: " + SOLRHOME_ATTRIBUTE; + String home = (String) servletContext.getAttribute(SOLRHOME_ATTRIBUTE); + + if (null == home) { + final String lookup = "java:comp/env/solr/home"; + // Try JNDI + source = "JNDI: " + lookup; + try { + Context c = new InitialContext(); + home = (String) c.lookup(lookup); + } catch (NoInitialContextException e) { + log.debug("JNDI not configured for solr (NoInitialContextEx)"); + } catch (NamingException e) { + log.debug("No /solr/home in JNDI"); + } catch (RuntimeException ex) { + log.warn("Odd RuntimeException while testing for JNDI: ", ex); + } + } + + if (null == home) { + // Now try system property + final String prop = "solr.solr.home"; + source = "system property: " + prop; + home = System.getProperty(prop); + } + + if (null == home) { + // if all else fails, assume default dir + home = "solr/"; + source = "defaulted to '" + home + "' ... could not find system property or JNDI"; + } + final Path solrHome = Paths.get(home).toAbsolutePath().normalize(); + log.info("Solr Home: {} (source: {})", solrHome, source); + + return solrHome; + } + + /** + * CoreContainer initialization + * @return a CoreContainer to hold this server's cores + */ + protected CoreContainer createCoreContainer(Path solrHome, Properties nodeProps) { + NodeConfig nodeConfig = loadNodeConfig(solrHome, nodeProps); + final CoreContainer coreContainer = new CoreContainer(nodeConfig, true); + coreContainer.load(); + return coreContainer; + } + + + + private void setupJvmMetrics(CoreContainer coresInit) { + metricManager = coresInit.getMetricManager(); + registryName = SolrMetricManager.getRegistryName(Group.jvm); + final Set hiddenSysProps = coresInit.getConfig().getMetricsConfig().getHiddenSysProps(); + try { + metricManager.registerAll(registryName, new AltBufferPoolMetricSet(), ResolutionStrategy.IGNORE, "buffers"); + metricManager.registerAll(registryName, new ClassLoadingGaugeSet(), ResolutionStrategy.IGNORE, "classes"); + metricManager.registerAll(registryName, new OperatingSystemMetricSet(), ResolutionStrategy.IGNORE, "os"); + metricManager.registerAll(registryName, new GarbageCollectorMetricSet(), ResolutionStrategy.IGNORE, "gc"); + metricManager.registerAll(registryName, new MemoryUsageGaugeSet(), ResolutionStrategy.IGNORE, "memory"); + metricManager.registerAll(registryName, new ThreadStatesGaugeSet(), ResolutionStrategy.IGNORE, "threads"); // todo should we use CachedThreadStatesGaugeSet instead? + MetricsMap sysprops = new MetricsMap(map -> System.getProperties().forEach((k, v) -> { + //noinspection SuspiciousMethodCalls + if (!hiddenSysProps.contains(k)) { + map.putNoEx(String.valueOf(k), v); + } + })); + metricManager.registerGauge(null, registryName, sysprops, metricTag, ResolutionStrategy.IGNORE, "properties", "system"); + MetricsMap sysenv = new MetricsMap(map -> System.getenv().forEach((k, v) -> { + if (!hiddenSysProps.contains(k)) { + map.putNoEx(String.valueOf(k), v); + } + })); + metricManager.registerGauge(null, registryName, sysenv, metricTag, ResolutionStrategy.IGNORE, "env", "system"); + } catch (Exception e) { + log.warn("Error registering JVM metrics", e); + } + } + + public RateLimitManager getRateLimitManager() { + return rateLimitManager; + } + + @VisibleForTesting + void setRateLimitManager(RateLimitManager rateLimitManager) { + this.rateLimitManager = rateLimitManager; + } + + public boolean isV2Enabled() { + return isV2Enabled; + } + + private static class ContextInitializationKey { + private final ServletContext ctx; + private final CountDownLatch initializing = new CountDownLatch(1); + + private ContextInitializationKey(ServletContext ctx) { + if (ctx == null) { + throw new IllegalArgumentException("Context must not be null"); + } + // if one of these is reachable both must be to avoid collection from weak hashmap, so + // set an attribute holding this object to ensure we never get collected until the ServletContext + // is eligible for collection too. + ctx.setAttribute(this.getClass().getName(), this); + this.ctx = ctx; + } + + public synchronized ServletContext getCtx() { + return ctx; + } + + synchronized void makeReady() { + this.initializing.countDown(); + } + + // NOT synchronized :) + public void waitForReadyService() throws InterruptedException { + initializing.await(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ContextInitializationKey that = (ContextInitializationKey) o; + return ctx.equals(that.ctx); + } + + @Override + public int hashCode() { + return Objects.hash(ctx); + } + } + + + static class ServiceHolder { + private volatile CoreContainerProvider service; + private volatile ContextInitializationKey key; + + private ServiceHolder(ContextInitializationKey key) { + if (key == null) { + throw new IllegalArgumentException("Key for accessing this service holder must be supplied"); + } + this.key = key; + } + + public void setService(CoreContainerProvider service) { + this.service = service; + key.makeReady(); + key = null; // be sure not to hold a reference to the context via the key + } + + public CoreContainerProvider getService() { + try { + if (key != null) { + try { + key.waitForReadyService(); + } catch (NullPointerException e) { + // ignore, means we raced with set service and lost, but that's fine since null implies we are ready. + } + } + } catch (InterruptedException e) { + throw new SolrException(ErrorCode.SERVER_ERROR,"Interrupted while obtaining reference to CoreService"); + } + return service; + } + } +} diff --git a/solr/core/src/java/org/apache/solr/servlet/ExceptionWhileTracing.java b/solr/core/src/java/org/apache/solr/servlet/ExceptionWhileTracing.java new file mode 100644 index 00000000000..c01d3d0c3f8 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/servlet/ExceptionWhileTracing.java @@ -0,0 +1,28 @@ +/* + * 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.servlet; + +/* + * This is not pretty, hope to remove it when tracing becomes a filter. + */ +public class ExceptionWhileTracing extends RuntimeException { + public Exception e; + + public ExceptionWhileTracing(Exception e) { + this.e = e; + } +} 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 a4141022806..4379d6bd48d 100644 --- a/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java +++ b/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java @@ -459,7 +459,7 @@ Action authorize() throws IOException { if (statusCode == AuthorizationResponse.PROMPT.statusCode) { @SuppressWarnings({"unchecked"}) - Map headers = (Map) getReq().getAttribute(AuthenticationPlugin.class.getName()); + Map headers = (Map) getReq().getAttribute(AuthenticationPlugin.class.getName()); if (headers != null) { for (Map.Entry e : headers.entrySet()) response.setHeader(e.getKey(), e.getValue()); } @@ -726,7 +726,7 @@ private void remoteQuery(String coreUrl, HttpServletResponse resp) throws IOExce } final HttpResponse response - = solrDispatchFilter.httpClient.execute(method, HttpClientUtil.createNewHttpClientRequestContext()); + = solrDispatchFilter.getHttpClient().execute(method, HttpClientUtil.createNewHttpClientRequestContext()); int httpStatus = response.getStatusLine().getStatusCode(); httpEntity = response.getEntity(); diff --git a/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java b/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java index 54d592435d6..42103f2f4b1 100644 --- a/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java +++ b/solr/core/src/java/org/apache/solr/servlet/LoadAdminUiServlet.java @@ -48,8 +48,8 @@ public void doGet(HttpServletRequest _request, HttpServletResponse _response) th "ENABLED in bin/solr.in.sh or solr.in.cmd."); return; } - HttpServletRequest request = SolrDispatchFilter.closeShield(_request, false); - HttpServletResponse response = SolrDispatchFilter.closeShield(_response, false); + HttpServletRequest request = ServletUtils.closeShield(_request, false); + HttpServletResponse response = ServletUtils.closeShield(_response, false); response.addHeader("X-Frame-Options", "DENY"); // security: SOLR-7966 - avoid clickjacking for admin interface diff --git a/solr/core/src/java/org/apache/solr/servlet/PathExcluder.java b/solr/core/src/java/org/apache/solr/servlet/PathExcluder.java new file mode 100644 index 00000000000..fef696da490 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/servlet/PathExcluder.java @@ -0,0 +1,28 @@ +/* + * 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.servlet; + +import java.util.List; +import java.util.regex.Pattern; + +/** + * Denotes an object, usually a servlet that denies access to some paths based on the supplied patterns. + * Typically, this would be implemented via compiled regular expressions. + */ +public interface PathExcluder { + void setExcludePatterns(List excludePatterns); +} diff --git a/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java b/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java index e61831ac4e7..673cd86d7a0 100644 --- a/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java +++ b/solr/core/src/java/org/apache/solr/servlet/ServletUtils.java @@ -17,12 +17,52 @@ package org.apache.solr.servlet; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.noop.NoopSpan; +import io.opentracing.noop.NoopTracer; +import io.opentracing.propagation.Format; +import io.opentracing.tag.Tags; +import org.apache.http.HttpHeaders; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.logging.MDCLoggingContext; +import org.apache.solr.request.SolrRequestInfo; +import org.apache.solr.util.tracing.HttpServletCarrier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.FilterChain; +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Various Util methods for interaction on servlet level, i.e. HttpServletRequest */ public abstract class ServletUtils { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + static String CLOSE_STREAM_MSG = "Attempted close of http request or response stream - in general you should not do this, " + + "you may spoil connection reuse and possibly disrupt a client. If you must close without actually needing to close, " + + "use a CloseShield*Stream. Closing or flushing the response stream commits the response and prevents us from modifying it. " + + "Closing the request stream prevents us from guaranteeing ourselves that streams are fully read for proper connection reuse." + + "Let the container manage the lifecycle of these streams when possible."; + private ServletUtils() { /* only static methods in this class */ } /** @@ -34,4 +74,285 @@ private ServletUtils() { /* only static methods in this class */ } public static String getPathAfterContext(HttpServletRequest request) { return request.getServletPath() + (request.getPathInfo() != null ? request.getPathInfo() : ""); } + + /** + * Wrap the request's input stream with a close shield. If this is a + * retry, we will assume that the stream has already been wrapped and do nothing. + * + * Only the container should ever actually close the servlet output stream. This method possibly + * should be turned into a servlet filter + * + * @param request The request to wrap. + * @param retry If this is an original request or a retry. + * @return A request object with an {@link InputStream} that will ignore calls to close. + */ + public static HttpServletRequest closeShield(HttpServletRequest request, boolean retry) { + if (!retry) { + return new HttpServletRequestWrapper(request) { + + @Override + public ServletInputStream getInputStream() throws IOException { + + return new ServletInputStreamWrapper(super.getInputStream()) { + @Override + public void close() { + // even though we skip closes, we let local tests know not to close so that a full understanding can take + // place + assert !Thread.currentThread().getStackTrace()[2].getClassName().matches( + "org\\.apache\\.(?:solr|lucene).*") : CLOSE_STREAM_MSG; + this.stream = ClosedServletInputStream.CLOSED_SERVLET_INPUT_STREAM; + } + }; + + } + }; + } else { + return request; + } + } + + /** + * Wrap the response's output stream with a close shield. If this is a + * retry, we will assume that the stream has already been wrapped and do nothing. + * + * Only the container should ever actually close the servlet request stream. + * + * @param response The response to wrap. + * @param retry If this response corresponds to an original request or a retry. + * @return A response object with an {@link OutputStream} that will ignore calls to close. + */ + public static HttpServletResponse closeShield(HttpServletResponse response, boolean retry) { + if (!retry) { + return new HttpServletResponseWrapper(response) { + + @Override + public ServletOutputStream getOutputStream() throws IOException { + + return new ServletOutputStreamWrapper(super.getOutputStream()) { + @Override + public void close() { + // even though we skip closes, we let local tests know not to close so that a full understanding can take + // place + assert !Thread.currentThread().getStackTrace()[2].getClassName().matches( + "org\\.apache\\.(?:solr|lucene).*") : CLOSE_STREAM_MSG; + stream = ClosedServletOutputStream.CLOSED_SERVLET_OUTPUT_STREAM; + } + }; + } + + }; + } else { + return response; + } + } + + static boolean excludedPath(List excludePatterns, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + String requestPath = getPathAfterContext(request); + // No need to even create the HttpSolrCall object if this path is excluded. + if (excludePatterns != null) { + for (Pattern p : excludePatterns) { + Matcher matcher = p.matcher(requestPath); + if (matcher.lookingAt()) { + if (chain != null) { + chain.doFilter(request, response); + } + return true; + } + } + } + return false; + } + + static boolean excludedPath(List excludePatterns, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + return excludedPath(excludePatterns,request,response, null); + } + + static void configExcludes(PathExcluder excluder, String patternConfig) { + if(patternConfig != null) { + String[] excludeArray = patternConfig.split(","); + List patterns = new ArrayList<>(); + excluder.setExcludePatterns(patterns); + for (String element : excludeArray) { + patterns.add(Pattern.compile(element)); + } + } + } + + /** + * Enforces rate limiting for a request. Should be converted to a servlet filter at some point. Currently, + * this is tightly coupled with request tracing which is not ideal either. + * + * @param request The request to limit + * @param response The associated response + * @param limitedExecution code that will be traced + * @param trace a boolean that turns tracing on or off + */ + static void rateLimitRequest(HttpServletRequest request, HttpServletResponse response, Runnable limitedExecution, boolean trace) throws ServletException, IOException { + boolean accepted = false; + RateLimitManager rateLimitManager = getRateLimitManager(request); + try { + try { + accepted = rateLimitManager.handleRequest(request); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new SolrException(ErrorCode.SERVER_ERROR, e.getMessage()); + } + + if (!accepted) { + String errorMessage = "Too many requests for this request type." + + "Please try after some time or increase the quota for this request type"; + + response.sendError(429, errorMessage); + } + // todo: this shouldn't be required, tracing and rate limiting should be independently composable + traceHttpRequestExecution2(request, response, limitedExecution, trace); + } finally { + if (accepted) { + rateLimitManager.decrementActiveRequests(request); + } + } + } + + /** + * Sets up tracing for an HTTP request. Perhaps should be converted to a servlet filter at some point. + * + * @param tracedExecution the executed code + */ + private static void traceHttpRequestExecution2(HttpServletRequest request, HttpServletResponse response, Runnable tracedExecution, boolean required) throws ServletException, IOException { + Tracer tracer = getTracer(request); + if (tracer != null) { + Span span = buildSpan(tracer, request); + + request.setAttribute(Span.class.getName(), span); + try (var scope = tracer.scopeManager().activate(span)) { + + assert scope != null; // prevent javac warning about scope being unused + MDCLoggingContext.setTracerId(span.context().toTraceId()); // handles empty string + + try { + tracedExecution.run(); + } catch (ExceptionWhileTracing e) { + if (e.e instanceof SolrAuthenticationException) { + throw (SolrAuthenticationException) e.e; + } + if (e.e instanceof ServletException) { + throw (ServletException) e.e; + } + if (e.e instanceof IOException) { + throw (IOException) e.e; + } + if (e.e instanceof RuntimeException) { + throw (RuntimeException) e.e; + } else { + throw new RuntimeException(e.e); + } + } + } catch (SolrAuthenticationException e) { + // done, the response and status code have already been sent + } finally { + consumeInputFully(request, response); + SolrRequestInfo.reset(); + SolrRequestParsers.cleanupMultipartFiles(request); + + + span.setTag(Tags.HTTP_STATUS, response.getStatus()); + span.finish(); + } + } else { + if (required) { + throw new IllegalStateException("Tracing required, but could not find Tracer in request attribute:" + SolrDispatchFilter.ATTR_TRACING_TRACER); + } else { + tracedExecution.run(); + } + } + } + + private static Tracer getTracer(HttpServletRequest req) { + return (Tracer) req.getAttribute(SolrDispatchFilter.ATTR_TRACING_TRACER); + } + + private static RateLimitManager getRateLimitManager(HttpServletRequest req) { + return (RateLimitManager) req.getAttribute(SolrDispatchFilter.ATTR_RATELIMIT_MANAGER); + } + + protected static Span buildSpan(Tracer tracer, HttpServletRequest request) { + if (tracer instanceof NoopTracer) { + return NoopSpan.INSTANCE; + } + Tracer.SpanBuilder spanBuilder = tracer.buildSpan("http.request") // will be changed later + .asChildOf(tracer.extract(Format.Builtin.HTTP_HEADERS, new HttpServletCarrier(request))) + .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER) + .withTag(Tags.HTTP_METHOD, request.getMethod()) + .withTag(Tags.HTTP_URL, request.getRequestURL().toString()); + if (request.getQueryString() != null) { + spanBuilder.withTag("http.params", request.getQueryString()); + } + spanBuilder.withTag(Tags.DB_TYPE, "solr"); + return spanBuilder.start(); + } + + // we make sure we read the full client request so that the client does + // not hit a connection reset and we can reuse the + // connection - see SOLR-8453 and SOLR-8683 + private static void consumeInputFully(HttpServletRequest req, HttpServletResponse response) { + try { + ServletInputStream is = req.getInputStream(); + //noinspection StatementWithEmptyBody + while (!is.isFinished() && is.read() != -1) {} + } catch (IOException e) { + if (req.getHeader(HttpHeaders.EXPECT) != null && response.isCommitted()) { + log.debug("No input stream to consume from client"); + } else { + log.info("Could not consume full client request", e); + } + } + } + + public static class ClosedServletInputStream extends ServletInputStream { + + public static final ClosedServletInputStream CLOSED_SERVLET_INPUT_STREAM = new ClosedServletInputStream(); + + @Override + public int read() { + return -1; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener arg0) {} + } + + public static class ClosedServletOutputStream extends ServletOutputStream { + + public static final ClosedServletOutputStream CLOSED_SERVLET_OUTPUT_STREAM = new ClosedServletOutputStream(); + + @Override + public void write(final int b) throws IOException { + throw new IOException("write(" + b + ") failed: stream is closed"); + } + + @Override + public void flush() throws IOException { + throw new IOException("flush() failed: stream is closed"); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setWriteListener(WriteListener arg0) { + throw new RuntimeException("setWriteListener() failed: stream is closed"); + } + } } diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrAuthenticationException.java b/solr/core/src/java/org/apache/solr/servlet/SolrAuthenticationException.java new file mode 100644 index 00000000000..d59f84b43e3 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/servlet/SolrAuthenticationException.java @@ -0,0 +1,20 @@ +/* + * 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.servlet; + +public class SolrAuthenticationException extends Exception{ +} diff --git a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java index 93a58974c3c..632e0db65f4 100644 --- a/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java +++ b/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java @@ -16,115 +16,89 @@ */ package org.apache.solr.servlet; -import javax.naming.Context; -import javax.naming.InitialContext; -import javax.naming.NamingException; -import javax.naming.NoInitialContextException; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ReadListener; -import javax.servlet.ServletException; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.UnavailableException; -import javax.servlet.WriteListener; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.invoke.MethodHandles; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Locale; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import com.codahale.metrics.jvm.ClassLoadingGaugeSet; -import com.codahale.metrics.jvm.GarbageCollectorMetricSet; -import com.codahale.metrics.jvm.MemoryUsageGaugeSet; -import com.codahale.metrics.jvm.ThreadStatesGaugeSet; import com.google.common.annotations.VisibleForTesting; import io.opentracing.Span; import io.opentracing.Tracer; -import io.opentracing.noop.NoopSpan; -import io.opentracing.noop.NoopTracer; -import io.opentracing.propagation.Format; import io.opentracing.tag.Tags; import io.opentracing.util.GlobalTracer; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpHeaders; import org.apache.http.client.HttpClient; -import org.apache.lucene.util.Version; import org.apache.solr.api.V2HttpCall; -import org.apache.solr.cloud.ZkController; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; -import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.util.ExecutorUtil; import org.apache.solr.core.CoreContainer; -import org.apache.solr.core.NodeConfig; import org.apache.solr.core.SolrCore; -import org.apache.solr.core.SolrInfoBean; -import org.apache.solr.core.SolrXmlConfig; import org.apache.solr.logging.MDCLoggingContext; import org.apache.solr.logging.MDCSnapshot; -import org.apache.solr.metrics.AltBufferPoolMetricSet; -import org.apache.solr.metrics.MetricsMap; -import org.apache.solr.metrics.OperatingSystemMetricSet; -import org.apache.solr.metrics.SolrMetricManager; -import org.apache.solr.metrics.SolrMetricProducer; -import org.apache.solr.request.SolrRequestInfo; import org.apache.solr.security.AuditEvent; import org.apache.solr.security.AuthenticationPlugin; import org.apache.solr.security.PKIAuthenticationPlugin; import org.apache.solr.security.PublicKeyHandler; -import org.apache.solr.util.StartupLoggingUtils; +import org.apache.solr.servlet.CoreContainerProvider.ServiceHolder; import org.apache.solr.util.configuration.SSLConfigurationsFactory; -import org.apache.solr.util.tracing.HttpServletCarrier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.UnavailableException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + import static org.apache.solr.security.AuditEvent.EventType; +import static org.apache.solr.servlet.ServletUtils.closeShield; +import static org.apache.solr.servlet.ServletUtils.configExcludes; +import static org.apache.solr.servlet.ServletUtils.excludedPath; /** * This filter looks at the incoming URL maps them to handlers defined in solrconfig.xml * * @since solr 1.2 */ -public class SolrDispatchFilter extends BaseSolrFilter { +// todo: get rid of this class entirely! Request dispatch is the container's responsibility. Much of what we have here +// should be several separate but composable servlet Filters, wrapping multiple servlets that are more focused in +// scope. This should become possible now that we have a ServletContextListener for startup/shutdown of CoreContainer +// that sets up a service from which things like CoreContainer can be requested. (or better yet injected) +public class SolrDispatchFilter extends BaseSolrFilter implements PathExcluder { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + public static final String ATTR_TRACING_SPAN = Span.class.getName(); + public static final String ATTR_TRACING_TRACER = Tracer.class.getName(); + public static final String ATTR_RATELIMIT_MANAGER = RateLimitManager.class.getName(); + + // TODO: see if we can get rid of the holder here (Servlet spec actually guarantees ContextListeners run + // before filter init, but JettySolrRunner that we use for tests is complicated) + private ServiceHolder coreService; - protected volatile CoreContainer cores; protected final CountDownLatch init = new CountDownLatch(1); protected String abortErrorMessage = null; - //TODO using Http2Client - protected HttpClient httpClient; - private ArrayList excludePatterns; - - private boolean isV2Enabled = !"true".equals(System.getProperty("disable.v2.api", "false")); - private final String metricTag = SolrMetricProducer.getUniqueMetricTag(this, null); - private SolrMetricManager metricManager; - private String registryName; - private volatile boolean closeOnDestroy = true; - private Properties extraProperties; + @Override + public void setExcludePatterns(List excludePatterns) { + this.excludePatterns = excludePatterns; + } + + private List excludePatterns; - private RateLimitManager rateLimitManager; + private final boolean isV2Enabled = !"true".equals(System.getProperty("disable.v2.api", "false")); + + public HttpClient getHttpClient() { + try { + return coreService.getService().getHttpClient(); + } catch (UnavailableException e) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Internal Http Client Unavailable, startup may have failed"); + } + } /** * Enum to define action that needs to be processed. @@ -155,288 +129,46 @@ public SolrDispatchFilter() { @Override public void init(FilterConfig config) throws ServletException { - SSLConfigurationsFactory.current().init(); - if (log.isTraceEnabled()) { - log.trace("SolrDispatchFilter.init(): {}", this.getClass().getClassLoader()); - } - CoreContainer coresInit = null; try { - // "extra" properties must be init'ed first so we know things like "do we have a zkHost" - // wrap as defaults (if set) so we can modify w/o polluting the Properties provided by our caller - this.extraProperties = SolrXmlConfig.wrapAndSetZkHostFromSysPropIfNeeded - ((Properties) config.getServletContext().getAttribute(PROPERTIES_ATTRIBUTE)); - - StartupLoggingUtils.checkLogDir(); - if (log.isInfoEnabled()) { - log.info("Using logger factory {}", StartupLoggingUtils.getLoggerImplStr()); - } - - logWelcomeBanner(); - - String muteConsole = System.getProperty(SOLR_LOG_MUTECONSOLE); - if (muteConsole != null && !Arrays.asList("false","0","off","no").contains(muteConsole.toLowerCase(Locale.ROOT))) { - StartupLoggingUtils.muteConsole(); - } - String logLevel = System.getProperty(SOLR_LOG_LEVEL); - if (logLevel != null) { - log.info("Log level override, property solr.log.level={}", logLevel); - StartupLoggingUtils.changeLogLevel(logLevel); - } - - String exclude = config.getInitParameter("excludePatterns"); - if(exclude != null) { - String[] excludeArray = exclude.split(","); - excludePatterns = new ArrayList<>(); - for (String element : excludeArray) { - excludePatterns.add(Pattern.compile(element)); - } + coreService = CoreContainerProvider.serviceForContext(config.getServletContext()); + SSLConfigurationsFactory.current().init(); + if (log.isTraceEnabled()) { + log.trace("SolrDispatchFilter.init(): {}", this.getClass().getClassLoader()); } - coresInit = createCoreContainer(computeSolrHome(config), extraProperties); - this.httpClient = coresInit.getUpdateShardHandler().getDefaultHttpClient(); - setupJvmMetrics(coresInit); - - SolrZkClient zkClient = null; - ZkController zkController = coresInit.getZkController(); - - if (zkController != null) { - zkClient = zkController.getZkClient(); - } - - RateLimitManager.Builder builder = new RateLimitManager.Builder(zkClient); - - this.rateLimitManager = builder.build(); - - if (zkController != null) { - zkController.zkStateReader.registerClusterPropertiesListener(this.rateLimitManager); - } - - if (log.isDebugEnabled()) { - log.debug("user.dir={}", System.getProperty("user.dir")); - } - } catch( Throwable t ) { + configExcludes(this, config.getInitParameter("excludePatterns")); + } catch (InterruptedException e) { + throw new ServletException("Interrupted while fetching core service"); + + } catch (Throwable t) { // catch this so our filter still works - log.error( "Could not start Solr. Check solr/home property and the logs"); - SolrCore.log( t ); + log.error("Could not start Dispatch Filter."); + SolrCore.log(t); if (t instanceof Error) { throw (Error) t; } - - } finally{ + } finally { log.trace("SolrDispatchFilter.init() done"); - this.cores = coresInit; // crucially final assignment init.countDown(); } } - private void setupJvmMetrics(CoreContainer coresInit) { - metricManager = coresInit.getMetricManager(); - registryName = SolrMetricManager.getRegistryName(SolrInfoBean.Group.jvm); - final Set hiddenSysProps = coresInit.getConfig().getMetricsConfig().getHiddenSysProps(); - try { - metricManager.registerAll(registryName, new AltBufferPoolMetricSet(), SolrMetricManager.ResolutionStrategy.IGNORE, "buffers"); - metricManager.registerAll(registryName, new ClassLoadingGaugeSet(), SolrMetricManager.ResolutionStrategy.IGNORE, "classes"); - metricManager.registerAll(registryName, new OperatingSystemMetricSet(), SolrMetricManager.ResolutionStrategy.IGNORE, "os"); - metricManager.registerAll(registryName, new GarbageCollectorMetricSet(), SolrMetricManager.ResolutionStrategy.IGNORE, "gc"); - metricManager.registerAll(registryName, new MemoryUsageGaugeSet(), SolrMetricManager.ResolutionStrategy.IGNORE, "memory"); - metricManager.registerAll(registryName, new ThreadStatesGaugeSet(), SolrMetricManager.ResolutionStrategy.IGNORE, "threads"); // todo should we use CachedThreadStatesGaugeSet instead? - MetricsMap sysprops = new MetricsMap(map -> { - System.getProperties().forEach((k, v) -> { - if (!hiddenSysProps.contains(k)) { - map.putNoEx(String.valueOf(k), v); - } - }); - }); - metricManager.registerGauge(null, registryName, sysprops, metricTag, SolrMetricManager.ResolutionStrategy.IGNORE, "properties", "system"); - MetricsMap sysenv = new MetricsMap(map -> { - System.getenv().forEach((k, v) -> { - if (!hiddenSysProps.contains(k)) { - map.putNoEx(String.valueOf(k), v); - } - }); - }); - metricManager.registerGauge(null, registryName, sysenv, metricTag, SolrMetricManager.ResolutionStrategy.IGNORE, "env", "system"); - } catch (Exception e) { - log.warn("Error registering JVM metrics", e); - } - } - - private void logWelcomeBanner() { - // _Really_ sorry about how clumsy this is as a result of the logging call checker, but this is the only one - // that's so ugly so far. - if (log.isInfoEnabled()) { - log.info(" ___ _ Welcome to Apache Solr™ version {}", solrVersion()); - } - if (log.isInfoEnabled()) { - log.info("/ __| ___| |_ _ Starting in {} mode on port {}", isCloudMode() ? "cloud" : "standalone", getSolrPort()); - } - if (log.isInfoEnabled()) { - log.info("\\__ \\/ _ \\ | '_| Install dir: {}", System.getProperty(SOLR_INSTALL_DIR_ATTRIBUTE)); - } - if (log.isInfoEnabled()) { - log.info("|___/\\___/_|_| Start time: {}", Instant.now()); - } - } - - private String solrVersion() { - String specVer = Version.LATEST.toString(); - try { - String implVer = SolrCore.class.getPackage().getImplementationVersion(); - return (specVer.equals(implVer.split(" ")[0])) ? specVer : implVer; - } catch (Exception e) { - return specVer; - } - } - - private String getSolrPort() { - return System.getProperty("jetty.port"); - } - - /** - * We are in cloud mode if Java option zkRun exists OR zkHost exists and is non-empty - * @see SolrXmlConfig#wrapAndSetZkHostFromSysPropIfNeeded - * @see #extraProperties - * @see #init - */ - private boolean isCloudMode() { - assert null != extraProperties; // we should never be called w/o this being initialized - return (null != extraProperties.getProperty(SolrXmlConfig.ZK_HOST)) || (null != System.getProperty("zkRun")); + public CoreContainer getCores() throws UnavailableException { + return coreService.getService().getCoreContainer(); } - /** - * Returns the effective Solr Home to use for this node, based on looking up the value in this order: - *

    - *
  1. attribute in the FilterConfig
  2. - *
  3. JNDI: via java:comp/env/solr/home
  4. - *
  5. The system property solr.solr.home
  6. - *
  7. Look in the current working directory for a solr/ directory
  8. - *
- *

- * - * @return the Solr home, absolute and normalized. - * @see #SOLRHOME_ATTRIBUTE - */ - private static Path computeSolrHome(FilterConfig config) { - - // start with explicit check of servlet config... - String source = "servlet config: " + SOLRHOME_ATTRIBUTE; - String home = (String) config.getServletContext().getAttribute(SOLRHOME_ATTRIBUTE); - - if (null == home) { - final String lookup = "java:comp/env/solr/home"; - // Try JNDI - source = "JNDI: " + lookup; - try { - Context c = new InitialContext(); - home = (String) c.lookup(lookup); - } catch (NoInitialContextException e) { - log.debug("JNDI not configured for solr (NoInitialContextEx)"); - } catch (NamingException e) { - log.debug("No /solr/home in JNDI"); - } catch (RuntimeException ex) { - log.warn("Odd RuntimeException while testing for JNDI: ", ex); - } - } - - if (null == home) { - // Now try system property - final String prop = "solr.solr.home"; - source = "system property: " + prop; - home = System.getProperty(prop); - } - - if (null == home) { - // if all else fails, assume default dir - home = "solr/"; - source = "defaulted to '" + home + "' ... could not find system property or JNDI"; - } - final Path solrHome = Paths.get(home).toAbsolutePath().normalize(); - log.info("Solr Home: {} (source: {})", solrHome, source); - - return solrHome; - } - - /** - * Override this to change CoreContainer initialization - * @return a CoreContainer to hold this server's cores - */ - protected CoreContainer createCoreContainer(Path solrHome, Properties nodeProps) { - NodeConfig nodeConfig = loadNodeConfig(solrHome, nodeProps); - final CoreContainer coreContainer = new CoreContainer(nodeConfig, true); - coreContainer.load(); - return coreContainer; - } - - /** - * Get the NodeConfig whether stored on disk, in ZooKeeper, etc. - * This may also be used by custom filters to load relevant configuration. - * @return the NodeConfig - */ - public static NodeConfig loadNodeConfig(Path solrHome, Properties nodeProperties) { - if (!StringUtils.isEmpty(System.getProperty("solr.solrxml.location"))) { - log.warn("Solr property solr.solrxml.location is no longer supported. Will automatically load solr.xml from ZooKeeper if it exists"); - } - nodeProperties = SolrXmlConfig.wrapAndSetZkHostFromSysPropIfNeeded(nodeProperties); - String zkHost = nodeProperties.getProperty(SolrXmlConfig.ZK_HOST); - if (!StringUtils.isEmpty(zkHost)) { - int startUpZkTimeOut = Integer.getInteger("waitForZk", 30); - startUpZkTimeOut *= 1000; - try (SolrZkClient zkClient = new SolrZkClient(zkHost, startUpZkTimeOut, startUpZkTimeOut)) { - if (zkClient.exists("/solr.xml", true)) { - log.info("solr.xml found in ZooKeeper. Loading..."); - byte[] data = zkClient.getData("/solr.xml", null, null, true); - return SolrXmlConfig.fromInputStream(solrHome, new ByteArrayInputStream(data), nodeProperties, true); - } - } catch (Exception e) { - throw new SolrException(ErrorCode.SERVER_ERROR, "Error occurred while loading solr.xml from zookeeper", e); - } - log.info("Loading solr.xml from SolrHome (not found in ZooKeeper)"); - } - - return SolrXmlConfig.fromSolrHome(solrHome, nodeProperties); - } - - public CoreContainer getCores() { - return cores; - } - @Override public void destroy() { - if (closeOnDestroy) { - close(); - } - } - - public void close() { - CoreContainer cc = cores; - cores = null; - try { - if (metricManager != null) { - try { - metricManager.unregisterGauges(registryName, metricTag); - } catch (NullPointerException e) { - // okay - } catch (Exception e) { - log.warn("Exception closing FileCleaningTracker", e); - } finally { - metricManager = null; - } - } - } finally { - if (cc != null) { - httpClient = null; - cc.shutdown(); - } - } + // CoreService shuts itself down as a ContextListener. The filter does not own anything with a lifecycle anymore! Yay! } - + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { try (var mdcSnapshot = MDCSnapshot.create()) { assert null != mdcSnapshot; // prevent compiler warning MDCLoggingContext.reset(); - MDCLoggingContext.setNode(cores); - + MDCLoggingContext.setNode(getCores()); + doFilter(request, response, chain, false); } } @@ -446,141 +178,70 @@ public void doFilter(ServletRequest _request, ServletResponse _response, FilterC HttpServletRequest request = closeShield((HttpServletRequest)_request, retry); HttpServletResponse response = closeShield((HttpServletResponse)_response, retry); - String requestPath = ServletUtils.getPathAfterContext(request); - // No need to even create the HttpSolrCall object if this path is excluded. - if (excludePatterns != null) { - for (Pattern p : excludePatterns) { - Matcher matcher = p.matcher(requestPath); - if (matcher.lookingAt()) { - chain.doFilter(request, response); - return; - } - } + if (excludedPath(excludePatterns, request, response, chain)) { + return; } - - Tracer tracer = cores == null ? GlobalTracer.get() : cores.getTracer(); - Span span = buildSpan(tracer, request); - request.setAttribute(Tracer.class.getName(), tracer); - request.setAttribute(Span.class.getName(), span); - boolean accepted = false; - try (var scope = tracer.scopeManager().activate(span)) { - assert scope != null; // prevent javac warning about scope being unused - MDCLoggingContext.setTracerId(span.context().toTraceId()); // handles empty string - - if (cores == null || cores.isShutDown()) { - try { - init.await(); - } catch (InterruptedException e) { //well, no wait then - } - final String msg = "Error processing the request. CoreContainer is either not initialized or shutting down."; - if (cores == null || cores.isShutDown()) { - log.error(msg); - throw new UnavailableException(msg); - } - } - + Tracer t = getCores() == null ? GlobalTracer.get() : getCores().getTracer(); + request.setAttribute(Tracer.class.getName(), t); + RateLimitManager rateLimitManager = coreService.getService().getRateLimitManager(); + request.setAttribute(RateLimitManager.class.getName(), rateLimitManager); + ServletUtils.rateLimitRequest(request, response, () -> { try { - accepted = rateLimitManager.handleRequest(request); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new SolrException(ErrorCode.SERVER_ERROR, e.getMessage()); - } - - if (!accepted) { - String errorMessage = "Too many requests for this request type." + - "Please try after some time or increase the quota for this request type"; - - response.sendError(429, errorMessage); - } - - AtomicReference wrappedRequest = new AtomicReference<>(); - if (!authenticateRequest(request, response, wrappedRequest)) { // the response and status code have already been sent - return; - } - - if (wrappedRequest.get() != null) { - request = wrappedRequest.get(); + dispatch(chain, request, response, retry); + } catch (IOException | ServletException | SolrAuthenticationException e) { + throw new ExceptionWhileTracing( e); } + }, true); + } - if (cores.getAuthenticationPlugin() != null) { - if (log.isDebugEnabled()) { - log.debug("User principal: {}", request.getUserPrincipal()); - } - span.setTag(Tags.DB_USER, String.valueOf(request.getUserPrincipal())); - } + private static Span getSpan(HttpServletRequest req) { + return (Span) req.getAttribute(ATTR_TRACING_SPAN); + } - HttpSolrCall call = getHttpSolrCall(request, response, retry); - ExecutorUtil.setServerThreadFlag(Boolean.TRUE); - try { - Action result = call.call(); - switch (result) { - case PASSTHROUGH: - span.log("SolrDispatchFilter PASSTHROUGH"); - chain.doFilter(request, response); - break; - case RETRY: - span.log("SolrDispatchFilter RETRY"); - doFilter(request, response, chain, true); // RECURSION - break; - case FORWARD: - span.log("SolrDispatchFilter FORWARD"); - request.getRequestDispatcher(call.getPath()).forward(request, response); - break; - case ADMIN: - case PROCESS: - case REMOTEQUERY: - case RETURN: - break; - } - } finally { - call.destroy(); - ExecutorUtil.setServerThreadFlag(null); - } - } finally { - consumeInputFully(request, response); - SolrRequestInfo.reset(); - SolrRequestParsers.cleanupMultipartFiles(request); + private void dispatch(FilterChain chain, HttpServletRequest request, HttpServletResponse response, boolean retry) throws IOException, ServletException, SolrAuthenticationException { - if (accepted) { - rateLimitManager.decrementActiveRequests(request); - } - span.setTag(Tags.HTTP_STATUS, response.getStatus()); - span.finish(); + AtomicReference wrappedRequest = new AtomicReference<>(); + authenticateRequest(request, response, wrappedRequest); + if (wrappedRequest.get() != null) { + request = wrappedRequest.get(); } - } - protected Span buildSpan(Tracer tracer, HttpServletRequest request) { - if (tracer instanceof NoopTracer) { - return NoopSpan.INSTANCE; - } - Tracer.SpanBuilder spanBuilder = tracer.buildSpan("http.request") // will be changed later - .asChildOf(tracer.extract(Format.Builtin.HTTP_HEADERS, new HttpServletCarrier(request))) - .withTag(Tags.SPAN_KIND, Tags.SPAN_KIND_SERVER) - .withTag(Tags.HTTP_METHOD, request.getMethod()) - .withTag(Tags.HTTP_URL, request.getRequestURL().toString()); - if (request.getQueryString() != null) { - spanBuilder.withTag("http.params", request.getQueryString()); + if (getCores().getAuthenticationPlugin() != null) { + if (log.isDebugEnabled()) { + log.debug("User principal: {}", request.getUserPrincipal()); + } + getSpan(request).setTag(Tags.DB_USER, String.valueOf(request.getUserPrincipal())); } - spanBuilder.withTag(Tags.DB_TYPE, "solr"); - return spanBuilder.start(); - } - // we make sure we read the full client request so that the client does - // not hit a connection reset and we can reuse the - // connection - see SOLR-8453 and SOLR-8683 - private void consumeInputFully(HttpServletRequest req, HttpServletResponse response) { + HttpSolrCall call = getHttpSolrCall(request, response, retry); + ExecutorUtil.setServerThreadFlag(Boolean.TRUE); try { - ServletInputStream is = req.getInputStream(); - while (!is.isFinished() && is.read() != -1) {} - } catch (IOException e) { - if (req.getHeader(HttpHeaders.EXPECT) != null && response.isCommitted()) { - log.debug("No input stream to consume from client"); - } else { - log.info("Could not consume full client request", e); + Action result = call.call(); + switch (result) { + case PASSTHROUGH: + getSpan(request).log("SolrDispatchFilter PASSTHROUGH"); + chain.doFilter(request, response); + break; + case RETRY: + getSpan(request).log("SolrDispatchFilter RETRY"); + doFilter(request, response, chain, true); // RECURSION + break; + case FORWARD: + getSpan(request).log("SolrDispatchFilter FORWARD"); + request.getRequestDispatcher(call.getPath()).forward(request, response); + break; + case ADMIN: + case PROCESS: + case REMOTEQUERY: + case RETURN: + break; } + } finally { + call.destroy(); + ExecutorUtil.setServerThreadFlag(null); } } - + /** * Allow a subclass to modify the HttpSolrCall. In particular, subclasses may * want to add attributes to the request and send errors differently @@ -588,6 +249,12 @@ private void consumeInputFully(HttpServletRequest req, HttpServletResponse respo protected HttpSolrCall getHttpSolrCall(HttpServletRequest request, HttpServletResponse response, boolean retry) { String path = ServletUtils.getPathAfterContext(request); + CoreContainer cores; + try { + cores = getCores(); + } catch (UnavailableException e) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container Unavailable"); + } if (isV2Enabled && (path.startsWith("/____v2/") || path.equals("/____v2"))) { return new V2HttpCall(this, cores, request, response, false); } else { @@ -595,26 +262,33 @@ protected HttpSolrCall getHttpSolrCall(HttpServletRequest request, HttpServletRe } } - private boolean authenticateRequest(HttpServletRequest request, HttpServletResponse response, final AtomicReference wrappedRequest) throws IOException { - boolean requestContinues = false; + // TODO: make this a servlet filter + private void authenticateRequest(HttpServletRequest request, HttpServletResponse response, final AtomicReference wrappedRequest) throws IOException,SolrAuthenticationException { + boolean requestContinues; final AtomicBoolean isAuthenticated = new AtomicBoolean(false); + CoreContainer cores; + try { + cores = getCores(); + } catch (UnavailableException e) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container Unavailable"); + } AuthenticationPlugin authenticationPlugin = cores.getAuthenticationPlugin(); if (authenticationPlugin == null) { if (shouldAudit(EventType.ANONYMOUS)) { cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.ANONYMOUS, request)); } - return true; + return; } else { // /admin/info/key must be always open. see SOLR-9188 String requestPath = ServletUtils.getPathAfterContext(request); if (PublicKeyHandler.PATH.equals(requestPath)) { log.debug("Pass through PKI authentication endpoint"); - return true; + return; } // /solr/ (Admin UI) must be always open to allow displaying Admin UI with login page if ("/solr/".equals(requestPath) || "/".equals(requestPath)) { log.debug("Pass through Admin UI entry point"); - return true; + return; } String header = request.getHeader(PKIAuthenticationPlugin.HEADER); if (header != null && cores.getPkiAuthenticationPlugin() != null) @@ -623,7 +297,17 @@ private boolean authenticateRequest(HttpServletRequest request, HttpServletRespo if (log.isDebugEnabled()) { log.debug("Request to authenticate: {}, domain: {}, port: {}", request, request.getLocalName(), request.getLocalPort()); } - // upon successful authentication, this should call the chain's next filter. + // For legacy reasons, upon successful authentication this wants to call the chain's next filter, which + // obfuscates the layout of the code since one usually expects to be able to find the call to doFilter() + // in the implementation of javax.servlet.Filter. Supplying a trivial impl here to keep existing code happy + // while making the flow clearer. Chain will be called after this method completes. Eventually auth all + // moves to its own filter (hopefully). Most auth plugins simply return true after calling this anyway, + // so they obviously don't care. Kerberos plugins seem to mostly use it to satisfy the api of a wrapped + // instance of javax.servlet.Filter and neither of those seem to be doing anything fancy with the filter chain, + // so this would seem to be a hack brought on by the fact that our auth code has been forced to be code + // within dispatch filter, rather than being a filter itself. The HadoopAuthPlugin has a suspicious amount + // of code after the call to doFilter() which seems to imply that anything in this chain can get executed before + // authentication completes, and I can't figure out how that's a good idea in the first place. requestContinues = authenticationPlugin.authenticate(request, response, (req, rsp) -> { isAuthenticated.set(true); wrappedRequest.set((HttpServletRequest) req); @@ -644,154 +328,31 @@ private boolean authenticateRequest(HttpServletRequest request, HttpServletRespo if (shouldAudit(EventType.REJECTED)) { cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.REJECTED, request)); } - return false; + throw new SolrAuthenticationException(); } if (shouldAudit(EventType.AUTHENTICATED)) { cores.getAuditLoggerPlugin().doAudit(new AuditEvent(EventType.AUTHENTICATED, request)); } - return true; - } - - public static class ClosedServletInputStream extends ServletInputStream { - - public static final ClosedServletInputStream CLOSED_SERVLET_INPUT_STREAM = new ClosedServletInputStream(); - - @Override - public int read() { - return -1; - } - - @Override - public boolean isFinished() { - return false; - } - - @Override - public boolean isReady() { - return false; - } - - @Override - public void setReadListener(ReadListener arg0) {} + // Auth Success } - - public static class ClosedServletOutputStream extends ServletOutputStream { - - public static final ClosedServletOutputStream CLOSED_SERVLET_OUTPUT_STREAM = new ClosedServletOutputStream(); - - @Override - public void write(final int b) throws IOException { - throw new IOException("write(" + b + ") failed: stream is closed"); - } - - @Override - public void flush() throws IOException { - throw new IOException("flush() failed: stream is closed"); - } - @Override - public boolean isReady() { - return false; - } - - @Override - public void setWriteListener(WriteListener arg0) { - throw new RuntimeException("setWriteListener() failed: stream is closed"); - } - } - - private static String CLOSE_STREAM_MSG = "Attempted close of http request or response stream - in general you should not do this, " - + "you may spoil connection reuse and possibly disrupt a client. If you must close without actually needing to close, " - + "use a CloseShield*Stream. Closing or flushing the response stream commits the response and prevents us from modifying it. " - + "Closing the request stream prevents us from gauranteeing ourselves that streams are fully read for proper connection reuse." - + "Let the container manage the lifecycle of these streams when possible."; - /** * Check if audit logging is enabled and should happen for given event type * @param eventType the audit event */ private boolean shouldAudit(AuditEvent.EventType eventType) { - return cores.getAuditLoggerPlugin() != null && cores.getAuditLoggerPlugin().shouldLog(eventType); - } - - /** - * Wrap the request's input stream with a close shield. If this is a - * retry, we will assume that the stream has already been wrapped and do nothing. - * - * Only the container should ever actually close the servlet output stream. - * - * @param request The request to wrap. - * @param retry If this is an original request or a retry. - * @return A request object with an {@link InputStream} that will ignore calls to close. - */ - public static HttpServletRequest closeShield(HttpServletRequest request, boolean retry) { - if (!retry) { - return new HttpServletRequestWrapper(request) { - - @Override - public ServletInputStream getInputStream() throws IOException { - - return new ServletInputStreamWrapper(super.getInputStream()) { - @Override - public void close() { - // even though we skip closes, we let local tests know not to close so that a full understanding can take - // place - assert Thread.currentThread().getStackTrace()[2].getClassName().matches( - "org\\.apache\\.(?:solr|lucene).*") ? false : true : CLOSE_STREAM_MSG; - this.stream = ClosedServletInputStream.CLOSED_SERVLET_INPUT_STREAM; - } - }; - - } - }; - } else { - return request; - } - } - - /** - * Wrap the response's output stream with a close shield. If this is a - * retry, we will assume that the stream has already been wrapped and do nothing. - * - * Only the container should ever actually close the servlet request stream. - * - * @param response The response to wrap. - * @param retry If this response corresponds to an original request or a retry. - * @return A response object with an {@link OutputStream} that will ignore calls to close. - */ - public static HttpServletResponse closeShield(HttpServletResponse response, boolean retry) { - if (!retry) { - return new HttpServletResponseWrapper(response) { - - @Override - public ServletOutputStream getOutputStream() throws IOException { - - return new ServletOutputStreamWrapper(super.getOutputStream()) { - @Override - public void close() { - // even though we skip closes, we let local tests know not to close so that a full understanding can take - // place - assert Thread.currentThread().getStackTrace()[2].getClassName().matches( - "org\\.apache\\.(?:solr|lucene).*") ? false - : true : CLOSE_STREAM_MSG; - stream = ClosedServletOutputStream.CLOSED_SERVLET_OUTPUT_STREAM; - } - }; - } - - }; - } else { - return response; + CoreContainer cores; + try { + cores = getCores(); + } catch (UnavailableException e) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Core Container Unavailable"); } - } - - public void closeOnDestroy(boolean closeOnDestroy) { - this.closeOnDestroy = closeOnDestroy; + return cores.getAuditLoggerPlugin() != null && cores.getAuditLoggerPlugin().shouldLog(eventType); } @VisibleForTesting void replaceRateLimitManager(RateLimitManager rateLimitManager) { - this.rateLimitManager = rateLimitManager; + coreService.getService().setRateLimitManager(rateLimitManager); } } diff --git a/solr/core/src/test/org/apache/solr/cloud/SolrXmlInZkTest.java b/solr/core/src/test/org/apache/solr/cloud/SolrXmlInZkTest.java index 8e1358c9fb8..55fcab8acab 100644 --- a/solr/core/src/test/org/apache/solr/cloud/SolrXmlInZkTest.java +++ b/solr/core/src/test/org/apache/solr/cloud/SolrXmlInZkTest.java @@ -29,7 +29,6 @@ import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkStateReader; import org.apache.solr.core.NodeConfig; -import org.apache.solr.servlet.SolrDispatchFilter; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; @@ -90,7 +89,7 @@ private void setUpZkAndDiskXml(boolean toZk, boolean leaveOnLocal) throws Except props.setProperty("solr.test.sys.prop1", "propone"); props.setProperty("solr.test.sys.prop2", "proptwo"); - cfg = SolrDispatchFilter.loadNodeConfig(solrHome, props); + cfg = NodeConfig.loadNodeConfig(solrHome, props); if (log.isInfoEnabled()) { log.info("####SETUP_END {}", getTestName()); } diff --git a/solr/core/src/test/org/apache/solr/core/TestLazyCores.java b/solr/core/src/test/org/apache/solr/core/TestLazyCores.java index 11aa24694bc..81889a8b310 100644 --- a/solr/core/src/test/org/apache/solr/core/TestLazyCores.java +++ b/solr/core/src/test/org/apache/solr/core/TestLazyCores.java @@ -40,7 +40,6 @@ import org.apache.solr.request.LocalSolrQueryRequest; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.SolrQueryResponse; -import org.apache.solr.servlet.SolrDispatchFilter; import org.apache.solr.update.AddUpdateCommand; import org.apache.solr.update.CommitUpdateCommand; import org.apache.solr.update.UpdateHandler; @@ -94,7 +93,7 @@ private CoreContainer init() throws Exception { copyMinConf(new File(solrHomeDirectory, "collection" + idx)); } - NodeConfig cfg = SolrDispatchFilter.loadNodeConfig(solrHomeDirectory.toPath(), null); + NodeConfig cfg = NodeConfig.loadNodeConfig(solrHomeDirectory.toPath(), null); return createCoreContainer(cfg, testCores); } diff --git a/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallGetCoreTest.java b/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallGetCoreTest.java index 34eb34471fc..7c06028e6ed 100644 --- a/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallGetCoreTest.java +++ b/solr/core/src/test/org/apache/solr/servlet/HttpSolrCallGetCoreTest.java @@ -20,6 +20,7 @@ import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.ServletOutputStream; +import javax.servlet.UnavailableException; import javax.servlet.WriteListener; import java.io.IOException; import java.util.HashSet; @@ -60,7 +61,7 @@ public void test() throws Exception { assertCoreChosen(NUM_SHARD * REPLICA_FACTOR, new TestRequest("/collection1/select")); } - private void assertCoreChosen(int numCores, TestRequest testRequest) { + private void assertCoreChosen(int numCores, TestRequest testRequest) throws UnavailableException { JettySolrRunner jettySolrRunner = cluster.getJettySolrRunner(0); Set coreNames = new HashSet<>(); SolrDispatchFilter dispatchFilter = jettySolrRunner.getSolrDispatchFilter(); diff --git a/solr/test-framework/src/java/org/apache/solr/util/BaseTestHarness.java b/solr/test-framework/src/java/org/apache/solr/util/BaseTestHarness.java index ad1d38e4ddc..2f825e33e99 100644 --- a/solr/test-framework/src/java/org/apache/solr/util/BaseTestHarness.java +++ b/solr/test-framework/src/java/org/apache/solr/util/BaseTestHarness.java @@ -77,7 +77,7 @@ public static String validateXPath(String xml, String... tests) if (tests==null || tests.length == 0) return null; - Document document = null; + Document document; try { document = getXmlDocumentBuilder().parse(new ByteArrayInputStream (xml.getBytes(StandardCharsets.UTF_8))); @@ -102,7 +102,7 @@ public static Object evaluateXPath(String xml, String xpath, QName returnType) throws XPathExpressionException, SAXException { if (null == xpath) return null; - Document document = null; + Document document; try { document = getXmlDocumentBuilder().parse(new ByteArrayInputStream (xml.getBytes(StandardCharsets.UTF_8))); diff --git a/solr/webapp/web/WEB-INF/web.xml b/solr/webapp/web/WEB-INF/web.xml index 2599420ddb1..33cad1f47e2 100644 --- a/solr/webapp/web/WEB-INF/web.xml +++ b/solr/webapp/web/WEB-INF/web.xml @@ -21,7 +21,9 @@ version="2.5" metadata-complete="true" > - + + org.apache.solr.servlet.CoreContainerProvider + SolrRequestFilter