All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.cometd.benchmark.server.CometDLoadServer Maven / Gradle / Ivy

There is a newer version: 7.0.17
Show newest version
/*
 * Copyright (c) 2008-2016 the original author or authors.
 *
 * Licensed 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.cometd.benchmark.server;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Enumeration;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.HdrHistogram.AtomicHistogram;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.benchmark.Config;
import org.cometd.benchmark.MonitoringQueuedThreadPool;
import org.cometd.server.AbstractServerTransport;
import org.cometd.server.AbstractService;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.CometDServlet;
import org.cometd.server.JacksonJSONContextServer;
import org.cometd.server.ext.AcknowledgedMessagesExtension;
import org.cometd.server.transport.AsyncJSONTransport;
import org.cometd.server.transport.JSONTransport;
import org.cometd.websocket.server.JettyWebSocketTransport;
import org.cometd.websocket.server.WebSocketTransport;
import org.eclipse.jetty.jmx.MBeanContainer;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerWrapper;
import org.eclipse.jetty.server.handler.StatisticsHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.toolchain.perf.HistogramSnapshot;
import org.eclipse.jetty.toolchain.perf.MeasureConverter;
import org.eclipse.jetty.toolchain.perf.PlatformMonitor;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer;

public class CometDLoadServer {
    private final MonitoringQueuedThreadPool jettyThreadPool = new MonitoringQueuedThreadPool(0);
    private final BayeuxServerImpl bayeuxServer = new BayeuxServerImpl();
    private final Server server = new Server(jettyThreadPool);
    private boolean interactive = true;
    private int port = 8080;
    private boolean tls = false;
    private int selectors = Runtime.getRuntime().availableProcessors();
    private int maxThreads = 256;
    private String transports = "jsrws,asynchttp";
    private boolean statistics = true;
    private boolean latencies = true;
    private boolean longRequests = false;
    private RequestLatencyHandler requestLatencyHandler;
    private StatisticsHandler statisticsHandler;

    public static void main(String[] args) throws Exception {
        CometDLoadServer server = new CometDLoadServer();
        parseArguments(args, server);
        server.run();
    }

    private static void parseArguments(String[] args, CometDLoadServer server) {
        for (String arg : args) {
            if (arg.equals("--auto")) {
                server.interactive = false;
            } else if (arg.startsWith("--port=")) {
                server.port = Integer.parseInt(arg.substring("--port=".length()));
            } else if (arg.equals("--tls")) {
                server.tls = true;
            } else if (arg.startsWith("--maxThreads=")) {
                server.maxThreads = Integer.parseInt(arg.substring("--maxThreads=".length()));
            } else if (arg.startsWith("--transports=")) {
                server.transports = arg.substring("--transports=".length());
            } else if (arg.equals("--statistics")) {
                server.statistics = true;
            } else if (arg.equals("--latencies")) {
                server.latencies = true;
            } else if (arg.equals("--longRequests")) {
                server.longRequests = true;
            }
        }
    }

    public void run() throws Exception {
        BufferedReader console = new BufferedReader(new InputStreamReader(System.in));

        int port = this.port;
        if (interactive) {
            System.err.printf("listen port [%d]: ", port);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(port);
            }
            port = Integer.parseInt(value);
        }

        boolean tls = this.tls;
        if (interactive) {
            System.err.printf("use tls [%b]: ", tls);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(tls);
            }
            tls = Boolean.parseBoolean(value);
        }

        int selectors = this.selectors;
        if (interactive) {
            System.err.printf("selectors [%d]: ", selectors);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(selectors);
            }
            selectors = Integer.parseInt(value);
        }

        int maxThreads = this.maxThreads;
        if (interactive) {
            maxThreads = Integer.parseInt(System.getProperty("cometd.threads", String.valueOf(maxThreads)));
            System.err.printf("max threads [%d]: ", maxThreads);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(maxThreads);
            }
            maxThreads = Integer.parseInt(value);
        }

        bayeuxServer.addExtension(new AcknowledgedMessagesExtension());

        String availableTransports = "jsrws,jettyws,http,asynchttp";
        String transports = this.transports;
        if (interactive) {
            System.err.printf("transports (%s) [%s]: ", availableTransports, transports);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = transports;
            }
            transports = value;
        }
        for (String token : transports.split(",")) {
            switch (token.trim()) {
                case "jsrws":
                    bayeuxServer.addTransport(new WebSocketTransport(bayeuxServer));
                    break;
                case "jettyws":
                    bayeuxServer.addTransport(new JettyWebSocketTransport(bayeuxServer));
                    break;
                case "http":
                    bayeuxServer.addTransport(new JSONTransport(bayeuxServer));
                    break;
                case "asynchttp":
                    bayeuxServer.addTransport(new AsyncJSONTransport(bayeuxServer));
                    break;
                default:
                    throw new IllegalArgumentException("Invalid transport: " + token);
            }
        }

        boolean statistics = this.statistics;
        if (interactive) {
            System.err.printf("record statistics [%b]: ", statistics);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(statistics);
            }
            statistics = Boolean.parseBoolean(value);
        }

        boolean latencies = this.latencies;
        if (interactive) {
            System.err.printf("record latencies [%b]: ", latencies);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(latencies);
            }
            latencies = Boolean.parseBoolean(value);
        }

        boolean longRequests = this.longRequests;
        if (interactive) {
            System.err.printf("detect long requests [%b]: ", longRequests);
            String value = console.readLine().trim();
            if (value.length() == 0) {
                value = String.valueOf(longRequests);
            }
            longRequests = Boolean.parseBoolean(value);
        }

        jettyThreadPool.setMaxThreads(maxThreads);

        // Setup JMX
        MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer());
        server.addBean(mbeanContainer);

        SslContextFactory sslContextFactory = null;
        if (tls) {
            Path keyStoreFile = Paths.get("src/main/resources/keystore.jks");
            if (Files.exists(keyStoreFile)) {
                throw new FileNotFoundException(keyStoreFile.toString());
            }
            sslContextFactory = new SslContextFactory();
            sslContextFactory.setKeyStorePath(keyStoreFile.toString());
            sslContextFactory.setKeyStorePassword("storepwd");
            sslContextFactory.setKeyManagerPassword("keypwd");
        }

        HttpConfiguration httpConfiguration = new HttpConfiguration();
        httpConfiguration.setDelayDispatchUntilContent(true);
        HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfiguration);
        ConnectionFactory[] factories = AbstractConnectionFactory.getFactories(sslContextFactory, httpConnectionFactory);
        ServerConnector connector = new ServerConnector(server, null, null, null, 1, selectors, factories);
        // Make sure the OS is configured properly for load testing;
        // see http://cometd.org/documentation/howtos/loadtesting
        connector.setAcceptQueueSize(2048);
        // Make sure the server timeout on a TCP connection is large
        connector.setIdleTimeout(50 * Config.MAX_NETWORK_DELAY);
        connector.setPort(port);
        server.addConnector(connector);

        HandlerWrapper handler = server;

        if (latencies) {
            requestLatencyHandler = new RequestLatencyHandler();
            handler.setHandler(requestLatencyHandler);
            handler = requestLatencyHandler;
        }

        if (longRequests) {
            RequestQoSHandler requestQoSHandler = new RequestQoSHandler();
            handler.setHandler(requestQoSHandler);
            handler = requestQoSHandler;
        }

        if (statistics) {
            statisticsHandler = new StatisticsHandler();
            handler.setHandler(statisticsHandler);
            handler = statisticsHandler;
        }

        // Add more handlers if needed

        ServletContextHandler context = new ServletContextHandler(handler, Config.CONTEXT_PATH, ServletContextHandler.SESSIONS);
        context.setAttribute(BayeuxServer.ATTRIBUTE, bayeuxServer);
        context.setInitParameter(ServletContextHandler.MANAGED_ATTRIBUTES, BayeuxServer.ATTRIBUTE);

        WebSocketServerContainerInitializer.configureContext(context);

        // Setup default servlet to serve static files
        context.addServlet(DefaultServlet.class, "/");

        // Setup comet servlet
        String cometdURLMapping = Config.SERVLET_PATH + "/*";
        CometDServlet cometServlet = new CometDServlet();
        ServletHolder cometdServletHolder = new ServletHolder(cometServlet);
        context.addServlet(cometdServletHolder, cometdURLMapping);

        // Make sure the expiration timeout is large to avoid clients to timeout
        // This value must be several times larger than the client value
        // (e.g. 60 s on server vs 5 s on client) so that it's guaranteed that
        // it will be the client to dispose idle connections.
        bayeuxServer.setOption(AbstractServerTransport.MAX_INTERVAL_OPTION, String.valueOf(10 * Config.MAX_NETWORK_DELAY));
        // Explicitly set the timeout value
        bayeuxServer.setOption(AbstractServerTransport.TIMEOUT_OPTION, String.valueOf(Config.META_CONNECT_TIMEOUT));
        // Use the faster JSON parser/generator
        bayeuxServer.setOption(AbstractServerTransport.JSON_CONTEXT_OPTION, JacksonJSONContextServer.class.getName());
        bayeuxServer.setOption("ws.cometdURLMapping", cometdURLMapping);
        bayeuxServer.setOption(ServletContext.class.getName(), context.getServletContext());

        server.start();

        new StatisticsService(this);
    }

    public static class StatisticsService extends AbstractService {
        private final PlatformMonitor monitor = new PlatformMonitor();
        private final CometDLoadServer server;

        private StatisticsService(CometDLoadServer server) {
            super(server.bayeuxServer, "statistics-service");
            this.server = server;
            addService("/service/statistics/start", "startStatistics");
            addService("/service/statistics/stop", "stopStatistics");
            addService("/service/statistics/exit", "exit");
        }

        public void startStatistics(ServerSession remote, ServerMessage message) {
            // Multiple nodes must wait that initialization is completed
            synchronized (this) {
                PlatformMonitor.Start start = monitor.start();
                if (start != null) {
                    System.err.println();
                    System.err.println(start);

                    if (server.jettyThreadPool != null) {
                        server.jettyThreadPool.reset();
                    }

                    if (server.statisticsHandler != null) {
                        server.statisticsHandler.statsReset();
                    }

                    if (server.requestLatencyHandler != null) {
                        server.requestLatencyHandler.reset();
                        server.requestLatencyHandler.doNotTrackCurrentRequest();
                    }
                }
            }
        }

        public void stopStatistics(ServerSession remote, ServerMessage message) throws Exception {
            synchronized (this) {
                PlatformMonitor.Stop stop = monitor.stop();
                if (stop != null) {
                    System.err.println(stop);

                    if (server.requestLatencyHandler != null) {
                        server.requestLatencyHandler.print();
                        server.requestLatencyHandler.doNotTrackCurrentRequest();
                    }

                    if (server.statisticsHandler != null) {
                        System.err.printf("Requests times (total/avg/max - stddev): %d/%d/%d ms - %d%n",
                                server.statisticsHandler.getDispatchedTimeTotal(),
                                ((Double)server.statisticsHandler.getDispatchedTimeMean()).longValue(),
                                server.statisticsHandler.getDispatchedTimeMax(),
                                ((Double)server.statisticsHandler.getDispatchedTimeStdDev()).longValue());
                        System.err.printf("Requests (total/failed/max - rate): %d/%d/%d - %d requests/s%n",
                                server.statisticsHandler.getDispatched(),
                                server.statisticsHandler.getResponses4xx() + server.statisticsHandler.getResponses5xx(),
                                server.statisticsHandler.getDispatchedActiveMax(),
                                server.statisticsHandler.getStatsOnMs() == 0 ? -1 : server.statisticsHandler.getDispatched() * 1000L / server.statisticsHandler.getStatsOnMs());
                    }

                    if (server.jettyThreadPool != null) {
                        System.err.printf("Jetty Thread Pool - Tasks = %d | Concurrent Threads max = %d | Queue Size max = %d | Queue Latency avg/max = %d/%d ms | Task Latency avg/max = %d/%d ms%n",
                                server.jettyThreadPool.getTasks(),
                                server.jettyThreadPool.getMaxActiveThreads(),
                                server.jettyThreadPool.getMaxQueueSize(),
                                TimeUnit.NANOSECONDS.toMillis(server.jettyThreadPool.getAverageQueueLatency()),
                                TimeUnit.NANOSECONDS.toMillis(server.jettyThreadPool.getMaxQueueLatency()),
                                TimeUnit.NANOSECONDS.toMillis(server.jettyThreadPool.getAverageTaskLatency()),
                                TimeUnit.NANOSECONDS.toMillis(server.jettyThreadPool.getMaxTaskLatency()));
                    }

                    System.err.println();
                }
            }
        }

        public void exit(ServerSession remote, ServerMessage message) throws Exception {
            remote.disconnect();
            // Cannot stop the server from a threadPool thread.
            new Thread() {
                @Override
                public void run() {
                    try {
                        if (!server.interactive) {
                            server.server.stop();
                        }
                    } catch (Throwable x) {
                        _logger.trace("", x);
                    }
                }
            }.start();
        }
    }

    private static class RequestQoSHandler extends HandlerWrapper {
        private final long maxRequestTime = 500;
        private final AtomicLong requestIds = new AtomicLong();
        private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(50);

        @Override
        protected void doStop() throws Exception {
            super.doStop();
            scheduler.shutdown();
        }

        @Override
        public void handle(String target, Request request, final HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException {
            final long requestId = requestIds.incrementAndGet();
            final AtomicBoolean longRequest = new AtomicBoolean(false);
            final Thread thread = Thread.currentThread();
            ScheduledFuture task = scheduler.scheduleWithFixedDelay(new Runnable() {
                public void run() {
                    longRequest.set(true);
                    onLongRequestDetected(requestId, httpRequest, thread);
                }
            }, maxRequestTime, maxRequestTime, TimeUnit.MILLISECONDS);
            long start = System.nanoTime();
            try {
                super.handle(target, request, httpRequest, httpResponse);
            } finally {
                long end = System.nanoTime();
                task.cancel(false);
                if (longRequest.get()) {
                    onLongRequestEnded(requestId, end - start);
                }
            }
        }

        private void onLongRequestDetected(long requestId, HttpServletRequest request, Thread thread) {
            try {
                long begin = System.nanoTime();
                StackTraceElement[] stackFrames = thread.getStackTrace();
                StringBuilder builder = new StringBuilder();
                formatRequest(request, builder);
                builder.append(thread).append("\n");
                formatStackFrames(stackFrames, builder);
                System.err.println("Request #" + requestId + " is too slow (> " + maxRequestTime + " ms)\n" + builder);
                long end = System.nanoTime();
                System.err.println("Request #" + requestId + " printed in " + TimeUnit.NANOSECONDS.toMicros(end - begin) + " \u00B5s");
            } catch (Exception x) {
                x.printStackTrace();
            }
        }

        private void formatRequest(HttpServletRequest request, StringBuilder builder) {
            builder.append(request.getRequestURI()).append("\n");
            for (Enumeration headers = request.getHeaderNames(); headers.hasMoreElements(); ) {
                String name = headers.nextElement();
                builder.append(name).append("=").append(Collections.list(request.getHeaders(name))).append("\n");
            }
            builder.append(request.getRemoteAddr()).append(":").append(request.getRemotePort()).append(" => ");
            builder.append(request.getLocalAddr()).append(":").append(request.getLocalPort()).append("\n");
        }

        private void onLongRequestEnded(long requestId, long time) {
            System.err.println("Request #" + requestId + " lasted " + TimeUnit.NANOSECONDS.toMillis(time) + " ms");
        }

        private void formatStackFrames(StackTraceElement[] stackFrames, StringBuilder builder) {
            for (int i = 0; i < stackFrames.length; ++i) {
                StackTraceElement stackFrame = stackFrames[i];
                for (int j = 0; j < i; ++j) {
                    builder.append(" ");
                }
                builder.append(stackFrame).append("\n");
            }
        }
    }

    private static class RequestLatencyHandler extends HandlerWrapper implements MeasureConverter {
        private final AtomicHistogram histogram = new AtomicHistogram(TimeUnit.MINUTES.toNanos(1), 3);
        private final ThreadLocal currentEnabled = new ThreadLocal() {
            @Override
            protected Boolean initialValue() {
                return Boolean.TRUE;
            }
        };

        @Override
        public void handle(String target, Request request, final HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws IOException, ServletException {
            long begin = System.nanoTime();
            try {
                super.handle(target, request, httpRequest, httpResponse);
            } finally {
                long end = System.nanoTime();
                if (currentEnabled.get()) {
                    updateLatencies(begin, end);
                } else {
                    currentEnabled.set(true);
                }
            }
        }

        @Override
        public long convert(long measure) {
            return TimeUnit.NANOSECONDS.toMicros(measure);
        }

        private void reset() {
            histogram.reset();
        }

        private void updateLatencies(long begin, long end) {
            histogram.recordValue(end - begin);
        }

        private void print() {
            System.err.println(new HistogramSnapshot(histogram.copy(), 20, "Requests - Latency", "\u00B5s", this));
        }

        public void doNotTrackCurrentRequest() {
            currentEnabled.set(false);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy