/*
* Copyright (C) 2015 SoftIndex LLC.
*
* 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 io.datakernel.http;
import io.datakernel.async.AsyncCancellable;
import io.datakernel.async.CompletionCallback;
import io.datakernel.async.ResultCallback;
import io.datakernel.eventloop.AbstractServer;
import io.datakernel.eventloop.AsyncTcpSocket;
import io.datakernel.eventloop.AsyncTcpSocketImpl;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.exception.ParseException;
import io.datakernel.jmx.EventStats;
import io.datakernel.jmx.ExceptionStats;
import io.datakernel.jmx.JmxAttribute;
import io.datakernel.jmx.JmxReducers.JmxReducerSum;
import io.datakernel.jmx.ValueStats;
import io.datakernel.util.MemSize;
import java.net.InetAddress;
import java.net.Socket;
import static io.datakernel.http.AbstractHttpConnection.*;
/**
* A server which works asynchronously. An instance of {@code AsyncHttpServer}
* can be created by calling {@link #create(Eventloop, AsyncServlet)} method
* and providing an {@link Eventloop} instance and an implementation of
* {@link AsyncServlet}.
* <p>
* The creation of asynchronous http server implies few steps:
* <ol>
* <li>Create an {@code eventloop} for a server</li>
* <li>Create a {@code servlet}, which will respond to received request</li>
* <li>Create a {@code server} with these instances</li>
* </ol>
* For example, consider an {@code AsyncHttpServer}:
* <pre><code>final {@link Eventloop Eventloop} eventloop = Eventloop.create();
* final {@link AsyncServlet AsyncServlet} servlet = new AsyncServlet() {
* {@literal @}Override
* public void serve({@link HttpRequest HttpRequest} request, final {@link ResultCallback ResultCallback<HttpResponse>} callback) {
* final HttpResponse response = HttpResponse.ok200().withBody(ByteBufStrings.encodeAscii("Hello, client!"));
* eventloop.post(new Runnable() {
* {@literal @}Override
* public void run() {
* System.out.println("Request body: " + request.getBody().toString());
* callback.setResult(response);
* }
* });
* }
* };
* AsyncHttpServer server = AsyncHttpServer.create(eventloop, servlet).withListenPort(40000);
* server.listen();
* eventloop.run(); //eventloop runs in current thread
* </code>
* </pre>
* Now server is ready for accepting requests and responding to clients with
* <pre>"Hello, client!"</pre> message. It's easy to create a client for this
* example using {@link AsyncHttpClient} or send a request with, for example,
* {@link AsyncTcpSocketImpl}.
*/
public final class AsyncHttpServer extends AbstractServer<AsyncHttpServer> {
public static final long DEFAULT_KEEP_ALIVE_MILLIS = 30 * 1000L;
private static final HttpExceptionFormatter DEFAULT_ERROR_FORMATTER = new HttpExceptionFormatter() {
@Override
public HttpResponse formatException(Exception e) {
if (e instanceof HttpException) {
HttpException httpException = (HttpException) e;
return HttpResponse.ofCode(httpException.getCode()).withNoCache();
}
if (e instanceof ParseException) {
return HttpResponse.ofCode(400).withNoCache();
}
return HttpResponse.ofCode(500).withNoCache();
}
};
private final AsyncServlet servlet;
private HttpExceptionFormatter errorFormatter = DEFAULT_ERROR_FORMATTER;
private int maxHttpMessageSize = Integer.MAX_VALUE;
int keepAliveTimeoutMillis = (int) DEFAULT_KEEP_ALIVE_MILLIS;
private int readTimeoutMillis = 0;
private int writeTimeoutMillis = 0;
private boolean gzipResponses = false;
private int connectionsCount;
final ConnectionsLinkedList poolKeepAlive = new ConnectionsLinkedList();
final ConnectionsLinkedList poolReading = new ConnectionsLinkedList();
final ConnectionsLinkedList poolWriting = new ConnectionsLinkedList();
final ConnectionsLinkedList poolServing = new ConnectionsLinkedList();
private int poolKeepAliveExpired;
private int poolReadingExpired;
private int poolWritingExpired;
private final char[] headerChars = new char[MAX_HEADER_LINE_SIZE];
private AsyncCancellable expiredConnectionsCheck;
Inspector inspector;
public interface Inspector {
void onHttpError(InetAddress remoteAddress, Exception e);
void onHttpRequest(HttpRequest request);
void onHttpResponse(HttpRequest request, HttpResponse httpResponse);
void onServletException(HttpRequest request, Exception e);
}
public static class JmxInspector implements Inspector {
private static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE;
private final EventStats totalRequests = EventStats.create(SMOOTHING_WINDOW);
private final EventStats totalResponses = EventStats.create(SMOOTHING_WINDOW);
private final EventStats httpTimeouts = EventStats.create(SMOOTHING_WINDOW);
private final ExceptionStats httpErrors = ExceptionStats.create();
private final ExceptionStats servletExceptions = ExceptionStats.create();
@Override
public void onHttpError(InetAddress remoteAddress, Exception e) {
if (e == AbstractHttpConnection.READ_TIMEOUT_ERROR || e == AbstractHttpConnection.WRITE_TIMEOUT_ERROR) {
httpTimeouts.recordEvent();
} else {
httpErrors.recordException(e);
}
}
@Override
public void onHttpRequest(HttpRequest request) {
totalRequests.recordEvent();
}
@Override
public void onHttpResponse(HttpRequest request, HttpResponse httpResponse) {
totalResponses.recordEvent();
}
@Override
public void onServletException(HttpRequest request, Exception e) {
servletExceptions.recordException(e, request.toString());
}
@JmxAttribute(extraSubAttributes = "totalCount")
public EventStats getTotalRequests() {
return totalRequests;
}
@JmxAttribute(extraSubAttributes = "totalCount")
public EventStats getTotalResponses() {
return totalResponses;
}
@JmxAttribute
public EventStats getHttpTimeouts() {
return httpTimeouts;
}
@JmxAttribute(description = "Number of requests which were invalid according to http protocol. " +
"Responses were not sent for this requests")
public ExceptionStats getHttpErrors() {
return httpErrors;
}
@JmxAttribute(description = "Number of requests which were valid according to http protocol, " +
"but application produced error during handling this request " +
"(responses with 4xx and 5xx HTTP status codes)")
public ExceptionStats getServletExceptions() {
return servletExceptions;
}
}
// region builders
private AsyncHttpServer(Eventloop eventloop, AsyncServlet servlet) {
super(eventloop);
this.servlet = servlet;
}
public static AsyncHttpServer create(Eventloop eventloop, AsyncServlet servlet) {
return new AsyncHttpServer(eventloop, servlet).withInspector(new JmxInspector());
}
public AsyncHttpServer withKeepAliveTimeout(long keepAliveTimeMillis) {
this.keepAliveTimeoutMillis = (int) keepAliveTimeMillis;
return self();
}
public AsyncHttpServer withNoKeepAlive() {
return withKeepAliveTimeout(0);
}
public AsyncHttpServer withReadTimeout(long readTimeoutMillis) {
this.readTimeoutMillis = (int) readTimeoutMillis;
return self();
}
public AsyncHttpServer withWriteTimeout(long writeTimeoutMillis) {
this.writeTimeoutMillis = (int) writeTimeoutMillis;
return self();
}
public AsyncHttpServer withMaxHttpMessageSize(int maxHttpMessageSize) {
this.maxHttpMessageSize = maxHttpMessageSize;
return self();
}
public AsyncHttpServer withMaxHttpMessageSize(MemSize size) {
return withMaxHttpMessageSize((int) size.get());
}
public AsyncHttpServer withHttpErrorFormatter(HttpExceptionFormatter httpExceptionFormatter) {
this.errorFormatter = httpExceptionFormatter;
return self();
}
public AsyncHttpServer withGzipResponses(boolean gzipResponses) {
this.gzipResponses = gzipResponses;
return self();
}
public AsyncHttpServer withInspector(Inspector inspector) {
this.inspector = inspector;
return self();
}
// endregion
private void scheduleExpiredConnectionsCheck() {
assert expiredConnectionsCheck == null;
expiredConnectionsCheck = eventloop.scheduleBackground(eventloop.currentTimeMillis() + 1000L, new Runnable() {
@Override
public void run() {
expiredConnectionsCheck = null;
poolKeepAliveExpired += poolKeepAlive.closeExpiredConnections(eventloop.currentTimeMillis() - keepAliveTimeoutMillis);
if (readTimeoutMillis != 0)
poolReadingExpired += poolReading.closeExpiredConnections(eventloop.currentTimeMillis() - readTimeoutMillis, READ_TIMEOUT_ERROR);
if (writeTimeoutMillis != 0)
poolWritingExpired += poolWriting.closeExpiredConnections(eventloop.currentTimeMillis() - writeTimeoutMillis, WRITE_TIMEOUT_ERROR);
if (connectionsCount != 0)
scheduleExpiredConnectionsCheck();
}
});
}
@Override
protected AsyncTcpSocket.EventHandler createSocketHandler(AsyncTcpSocket asyncTcpSocket) {
assert eventloop.inEventloopThread();
connectionsCount++;
if (expiredConnectionsCheck == null)
scheduleExpiredConnectionsCheck();
return new HttpServerConnection(eventloop, asyncTcpSocket.getRemoteSocketAddress().getAddress(), asyncTcpSocket, this, servlet,
headerChars, maxHttpMessageSize, gzipResponses);
}
private CompletionCallback closeCallback;
void onConnectionClosed() {
connectionsCount--;
if (connectionsCount == 0 && closeCallback != null) {
closeCallback.postComplete(eventloop);
closeCallback = null;
}
}
@Override
protected void onClose(final CompletionCallback completionCallback) {
poolKeepAlive.closeAllConnections();
keepAliveTimeoutMillis = 0;
if (connectionsCount == 0) {
completionCallback.postComplete(eventloop);
} else {
this.closeCallback = completionCallback;
}
}
@JmxAttribute(description = "current number of connections", reducer = JmxReducerSum.class)
public int getConnectionsCount() {
return connectionsCount;
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsKeepAliveCount() {
return poolKeepAlive.size();
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsReadingCount() {
return poolReading.size();
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsWritingCount() {
return poolWriting.size();
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsServingCount() {
return poolServing.size();
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsKeepAliveExpired() {
return poolKeepAliveExpired;
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsReadingExpired() {
return poolReadingExpired;
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getConnectionsWritingExpired() {
return poolWritingExpired;
}
HttpResponse formatHttpError(Exception e) {
return errorFormatter.formatException(e);
}
@JmxAttribute(name = "")
public JmxInspector getStats() {
return inspector instanceof JmxInspector ? (JmxInspector) inspector : null;
}
}