/*
* 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.client.solrj.embedded;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.core.CoreContainer;
import org.apache.solr.servlet.SolrDispatchFilter;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.server.session.HashSessionIdManager;
import org.eclipse.jetty.servlet.BaseHolder;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
/**
* Run solr using jetty
*
* @since solr 1.3
*/
public class JettySolrRunner {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int THREAD_POOL_MAX_THREADS = 10000;
// NOTE: needs to be larger than SolrHttpClient.threadPoolSweeperMaxIdleTime
private static final int THREAD_POOL_MAX_IDLE_TIME_MS = 120000;
Server server;
FilterHolder dispatchFilter;
FilterHolder debugFilter;
private boolean waitOnSolr = false;
private int lastPort = -1;
private final JettyConfig config;
private final String solrHome;
private final Properties nodeProperties;
private volatile boolean startedBefore = false;
private LinkedList<FilterHolder> extraFilters;
private static final String excludePatterns = "/css/.+,/js/.+,/img/.+,/tpl/.+";
private int proxyPort = -1;
public static class DebugFilter implements Filter {
public final static Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private AtomicLong nRequests = new AtomicLong();
List<Delay> delays = new ArrayList<>();
public long getTotalRequests() {
return nRequests.get();
}
/**
* Introduce a delay of specified milliseconds for the specified request.
*
* @param reason Info message logged when delay occurs
* @param count The count-th request will experience a delay
* @param delay There will be a delay of this many milliseconds
*/
public void addDelay(String reason, int count, int delay) {
delays.add(new Delay(reason, count, delay));
}
/**
* Remove any delay introduced before.
*/
public void unsetDelay() {
delays.clear();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException { }
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
nRequests.incrementAndGet();
executeDelay();
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() { }
private void executeDelay() {
int delayMs = 0;
for (Delay delay: delays) {
log.info("Delaying "+delay.delayValue+", for reason: "+delay.reason);
if (delay.counter.decrementAndGet() == 0) {
delayMs += delay.delayValue;
}
}
if (delayMs > 0) {
log.info("Pausing this socket connection for " + delayMs + "ms...");
try {
Thread.sleep(delayMs);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("Waking up after the delay of " + delayMs + "ms...");
}
}
}
/**
* Create a new JettySolrRunner.
*
* After construction, you must start the jetty with {@link #start()}
*
* @param solrHome the solr home directory to use
* @param context the context to run in
* @param port the port to run on
*/
public JettySolrRunner(String solrHome, String context, int port) {
this(solrHome, JettyConfig.builder().setContext(context).setPort(port).build());
}
/**
* Construct a JettySolrRunner
*
* After construction, you must start the jetty with {@link #start()}
*
* @param solrHome the base path to run from
* @param config the configuration
*/
public JettySolrRunner(String solrHome, JettyConfig config) {
this(solrHome, new Properties(), config);
}
/**
* Construct a JettySolrRunner
*
* After construction, you must start the jetty with {@link #start()}
*
* @param solrHome the solrHome to use
* @param nodeProperties the container properties
* @param config the configuration
*/
public JettySolrRunner(String solrHome, Properties nodeProperties, JettyConfig config) {
this.solrHome = solrHome;
this.config = config;
this.nodeProperties = nodeProperties;
this.init(this.config.port);
}
private void init(int port) {
QueuedThreadPool qtp = new QueuedThreadPool();
qtp.setMaxThreads(THREAD_POOL_MAX_THREADS);
qtp.setIdleTimeout(THREAD_POOL_MAX_IDLE_TIME_MS);
qtp.setStopTimeout((int) TimeUnit.MINUTES.toMillis(1));
server = new Server(qtp);
server.manage(qtp);
server.setStopAtShutdown(config.stopAtShutdown);
if (System.getProperty("jetty.testMode") != null) {
// if this property is true, then jetty will be configured to use SSL
// leveraging the same system properties as java to specify
// the keystore/truststore if they are set unless specific config
// is passed via the constructor.
//
// This means we will use the same truststore, keystore (and keys) for
// the server as well as any client actions taken by this JVM in
// talking to that server, but for the purposes of testing that should
// be good enough
final SslContextFactory sslcontext = SSLConfig.createContextFactory(config.sslConfig);
ServerConnector connector;
if (sslcontext != null) {
HttpConfiguration configuration = new HttpConfiguration();
configuration.setSecureScheme("https");
configuration.addCustomizer(new SecureRequestCustomizer());
connector = new ServerConnector(server, new SslConnectionFactory(sslcontext, "http/1.1"),
new HttpConnectionFactory(configuration));
} else {
connector = new ServerConnector(server, new HttpConnectionFactory());
}
connector.setReuseAddress(true);
connector.setSoLingerTime(-1);
connector.setPort(port);
connector.setHost("127.0.0.1");
connector.setIdleTimeout(THREAD_POOL_MAX_IDLE_TIME_MS);
server.setConnectors(new Connector[] {connector});
server.setSessionIdManager(new HashSessionIdManager(new Random()));
} else {
ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory());
connector.setPort(port);
connector.setSoLingerTime(-1);
connector.setIdleTimeout(THREAD_POOL_MAX_IDLE_TIME_MS);
server.setConnectors(new Connector[] {connector});
}
// Initialize the servlets
final ServletContextHandler root = new ServletContextHandler(server, config.context, ServletContextHandler.SESSIONS);
server.addLifeCycleListener(new LifeCycle.Listener() {
@Override
public void lifeCycleStopping(LifeCycle arg0) {
}
@Override
public void lifeCycleStopped(LifeCycle arg0) {}
@Override
public void lifeCycleStarting(LifeCycle arg0) {
synchronized (JettySolrRunner.this) {
waitOnSolr = true;
JettySolrRunner.this.notify();
}
}
@Override
public void lifeCycleStarted(LifeCycle arg0) {
lastPort = getFirstConnectorPort();
nodeProperties.setProperty("hostPort", Integer.toString(lastPort));
nodeProperties.setProperty("hostContext", config.context);
root.getServletContext().setAttribute(SolrDispatchFilter.PROPERTIES_ATTRIBUTE, nodeProperties);
root.getServletContext().setAttribute(SolrDispatchFilter.SOLRHOME_ATTRIBUTE, solrHome);
logger.info("Jetty properties: {}", nodeProperties);
debugFilter = root.addFilter(DebugFilter.class, "*", EnumSet.of(DispatcherType.REQUEST) );
extraFilters = new LinkedList<>();
for (Class<? extends Filter> filterClass : config.extraFilters.keySet()) {
extraFilters.add(root.addFilter(filterClass, config.extraFilters.get(filterClass),
EnumSet.of(DispatcherType.REQUEST)));
}
for (ServletHolder servletHolder : config.extraServlets.keySet()) {
String pathSpec = config.extraServlets.get(servletHolder);
root.addServlet(servletHolder, pathSpec);
}
dispatchFilter = root.getServletHandler().newFilterHolder(BaseHolder.Source.EMBEDDED);
dispatchFilter.setHeldClass(SolrDispatchFilter.class);
dispatchFilter.setInitParameter("excludePatterns", excludePatterns);
root.addFilter(dispatchFilter, "*", EnumSet.of(DispatcherType.REQUEST));
}
@Override
public void lifeCycleFailure(LifeCycle arg0, Throwable arg1) {
System.clearProperty("hostPort");
}
});
// for some reason, there must be a servlet for this to get applied
root.addServlet(Servlet404.class, "/*");
GzipHandler gzipHandler = new GzipHandler();
gzipHandler.setHandler(root);
gzipHandler.setMinGzipSize(0);
gzipHandler.setCheckGzExists(false);
gzipHandler.setCompressionLevel(-1);
gzipHandler.setExcludedAgentPatterns(".*MSIE.6\\.0.*");
gzipHandler.setIncludedMethods("GET");
server.setHandler(gzipHandler);
}
/**
* @return the {@link SolrDispatchFilter} for this node
*/
public SolrDispatchFilter getSolrDispatchFilter() { return (SolrDispatchFilter) dispatchFilter.getFilter(); }
/**
* @return the {@link CoreContainer} for this node
*/
public CoreContainer getCoreContainer() {
if (getSolrDispatchFilter() == null || getSolrDispatchFilter().getCores() == null) {
return null;
}
return getSolrDispatchFilter().getCores();
}
public String getNodeName() {
return getCoreContainer().getZkController().getNodeName();
}
public boolean isRunning() {
return server.isRunning();
}
public boolean isStopped() {
return server.isStopped();
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
/**
* Start the Jetty server
*
* If the server has been started before, it will restart using the same port
*
* @throws Exception if an error occurs on startup
*/
public void start() throws Exception {
start(true);
}
/**
* Start the Jetty server
*
* @param reusePort when true, will start up on the same port as used by any
* previous runs of this JettySolrRunner. If false, will use
* the port specified by the server's JettyConfig.
*
* @throws Exception if an error occurs on startup
*/
public void start(boolean reusePort) throws Exception {
// Do not let Jetty/Solr pollute the MDC for this thread
Map<String, String> prevContext = MDC.getCopyOfContextMap();
MDC.clear();
try {
// if started before, make a new server
if (startedBefore) {
waitOnSolr = false;
int port = reusePort ? lastPort : this.config.port;
init(port);
} else {
startedBefore = true;
}
if (!server.isRunning()) {
server.start();
}
synchronized (JettySolrRunner.this) {
int cnt = 0;
while (!waitOnSolr) {
this.wait(100);
if (cnt++ == 5) {
throw new RuntimeException("Jetty/Solr unresponsive");
}
}
}
if (config.waitForLoadingCoresToFinishMs != null && config.waitForLoadingCoresToFinishMs > 0L) waitForLoadingCoresToFinish(config.waitForLoadingCoresToFinishMs);
} finally {
if (prevContext != null) {
MDC.setContextMap(prevContext);
} else {
MDC.clear();
}
}
}
/**
* Stop the Jetty server
*
* @throws Exception if an error occurs on shutdown
*/
public void stop() throws Exception {
// Do not let Jetty/Solr pollute the MDC for this thread
Map<String, String> prevContext = MDC.getCopyOfContextMap();
MDC.clear();
try {
Filter filter = dispatchFilter.getFilter();
server.stop();
if (server.getState().equals(Server.FAILED)) {
filter.destroy();
if (extraFilters != null) {
for (FilterHolder f : extraFilters) {
f.getFilter().destroy();
}
}
}
server.join();
} finally {
if (prevContext != null) {
MDC.setContextMap(prevContext);
} else {
MDC.clear();
}
}
}
/**
* Returns the Local Port of the jetty Server.
*
* @exception RuntimeException if there is no Connector
*/
private int getFirstConnectorPort() {
Connector[] conns = server.getConnectors();
if (0 == conns.length) {
throw new RuntimeException("Jetty Server has no Connectors");
}
return (proxyPort != -1) ? proxyPort : ((ServerConnector) conns[0]).getLocalPort();
}
/**
* Returns the Local Port of the jetty Server.
*
* @exception RuntimeException if there is no Connector
*/
public int getLocalPort() {
if (lastPort == -1) {
throw new IllegalStateException("You cannot get the port until this instance has started");
}
return (proxyPort != -1) ? proxyPort : lastPort;
}
/**
* Sets the port of a local socket proxy that sits infront of this server; if set
* then all client traffic will flow through the proxy, giving us the ability to
* simulate network partitions very easily.
*/
public void setProxyPort(int proxyPort) {
this.proxyPort = proxyPort;
}
/**
* Returns a base URL consisting of the protocol, host, and port for a
* Connector in use by the Jetty Server contained in this runner.
*/
public URL getBaseUrl() {
String protocol = null;
try {
Connector[] conns = server.getConnectors();
if (0 == conns.length) {
throw new IllegalStateException("Jetty Server has no Connectors");
}
ServerConnector c = (ServerConnector) conns[0];
if (c.getLocalPort() < 0) {
throw new IllegalStateException("Jetty Connector is not open: " +
c.getLocalPort());
}
protocol = c.getDefaultProtocol().startsWith("SSL") ? "https" : "http";
return new URL(protocol, c.getHost(), c.getLocalPort(), config.context);
} catch (MalformedURLException e) {
throw new IllegalStateException
("Java could not make sense of protocol: " + protocol, e);
}
}
public SolrClient newClient() {
return new HttpSolrClient.Builder(getBaseUrl().toString()).build();
}
public DebugFilter getDebugFilter() {
return (DebugFilter)debugFilter.getFilter();
}
// --------------------------------------------------------------
// --------------------------------------------------------------
/**
* This is a stupid hack to give jetty something to attach to
*/
public static class Servlet404 extends HttpServlet {
@Override
public void service(HttpServletRequest req, HttpServletResponse res)
throws IOException {
res.sendError(404, "Can not find: " + req.getRequestURI());
}
}
/**
* A main class that starts jetty+solr This is useful for debugging
*/
public static void main(String[] args) {
try {
JettySolrRunner jetty = new JettySolrRunner(".", "/solr", 8983);
jetty.start();
} catch (Exception ex) {
ex.printStackTrace();
}
}
/**
* @return the Solr home directory of this JettySolrRunner
*/
public String getSolrHome() {
return solrHome;
}
/**
* @return this node's properties
*/
public Properties getNodeProperties() {
return nodeProperties;
}
private void waitForLoadingCoresToFinish(long timeoutMs) {
if (dispatchFilter != null) {
SolrDispatchFilter solrFilter = (SolrDispatchFilter) dispatchFilter.getFilter();
CoreContainer cores = solrFilter.getCores();
if (cores != null) {
cores.waitForLoadingCoresToFinish(timeoutMs);
}
}
}
static class Delay {
final AtomicInteger counter;
final int delayValue;
final String reason;
public Delay(String reason, int counter, int delay) {
this.reason = reason;
this.counter = new AtomicInteger(counter);
this.delayValue = delay;
}
}
}