ch.squaredesk.nova.comm.http.RpcServer Maven / Gradle / Ivy
package ch.squaredesk.nova.comm.http;
import ch.squaredesk.nova.comm.retrieving.MessageUnmarshaller;
import ch.squaredesk.nova.comm.sending.MessageMarshaller;
import ch.squaredesk.nova.metrics.Metrics;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.subjects.PublishSubject;
import io.reactivex.subjects.Subject;
import org.glassfish.grizzly.ReadHandler;
import org.glassfish.grizzly.http.Method;
import org.glassfish.grizzly.http.io.NIOReader;
import org.glassfish.grizzly.http.io.NIOWriter;
import org.glassfish.grizzly.http.server.HttpHandler;
import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.grizzly.http.server.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class RpcServer extends ch.squaredesk.nova.comm.rpc.RpcServer> {
private static final Logger logger = LoggerFactory.getLogger(RpcServer.class);
private final MessageMarshaller messageMarshaller;
private final MessageUnmarshaller messageUnmarshaller;
private final Map>> mapDestinationToIncomingMessages = new ConcurrentHashMap<>();
private final HttpServer httpServer;
public RpcServer(HttpServer httpServer,
MessageMarshaller messageMarshaller,
MessageUnmarshaller messageUnmarshaller,
Metrics metrics) {
this(null, httpServer, messageMarshaller, messageUnmarshaller, metrics);
}
RpcServer(String identifier,
HttpServer httpServer,
MessageMarshaller messageMarshaller,
MessageUnmarshaller messageUnmarshaller,
Metrics metrics) {
super(identifier, metrics);
Objects.requireNonNull(httpServer, "httpServer must not be null");
Objects.requireNonNull(messageMarshaller, "messageMarshaller must not be null");
Objects.requireNonNull(messageUnmarshaller, "messageUnmarshaller must not be null");
this.httpServer = httpServer;
this.messageUnmarshaller = messageUnmarshaller;
this.messageMarshaller = messageMarshaller;
}
@Override
public Flowable> requests(String destination) {
Flowable retVal = mapDestinationToIncomingMessages
.computeIfAbsent(destination, key -> {
logger.info("Listening to requests on " + destination);
Subject> stream = PublishSubject.create();
stream = stream.toSerialized();
NonBlockingHttpHandler httpHandler = new NonBlockingHttpHandler(stream);
httpServer.getServerConfiguration().addHttpHandler(httpHandler, destination);
return stream.toFlowable(BackpressureStrategy.BUFFER)
.doFinally(() -> {
mapDestinationToIncomingMessages.remove(destination);
httpServer.getServerConfiguration().removeHttpHandler(httpHandler);
logger.info("Stopped listening to requests on " + destination);
})
.share();
});
return retVal;
}
private static HttpSpecificInfo httpSpecificInfoFrom (Request request) throws Exception {
Map parameters = new HashMap<>();
for (Map.Entry entry: request.getParameterMap().entrySet()) {
String[] valueList = entry.getValue();
String valueToPass = null;
if (valueList != null && valueList.length>0) {
valueToPass = valueList[0];
}
parameters.put(entry.getKey(), valueToPass);
}
return new HttpSpecificInfo(
convert(request.getMethod()), parameters);
}
private static HttpRequestMethod convert (Method method) {
if (method==Method.POST) {
return HttpRequestMethod.POST;
} else if (method == Method.DELETE) {
return HttpRequestMethod.DELETE;
} else if (method == Method.PUT) {
return HttpRequestMethod.PUT;
} else {
// TODO: do we want to add all other ones known to grizzly?
return HttpRequestMethod.GET;
}
}
private static T convertRequestData (String objectAsString, MessageUnmarshaller unmarshaller) throws Exception {
return unmarshaller.unmarshal(objectAsString);
}
private static String convertResponseData (T replyObject, MessageMarshaller marshaller) throws Exception {
return marshaller.marshal(replyObject);
}
/**
* writes reply Object to response body. Assumes that the marshaller creates a String that is a JSON
* representation of the reply object
*/
private static void writeResponse (String reply, NIOWriter out) throws Exception {
out.write(reply);
out.flush();
out.close();
}
/**
* Non-blockingly reads all (non blockingly) available characters of the passed InputReader and
* concats those characters to the passed , returning a new character array
*/
private static char[] appendAvailableDataToBuffer(NIOReader in, char currentBuffer[]) throws IOException {
// we are not synchronizing here, since we assume that onDataAvailable() is called sequentially
char[] readBuffer = new char[in.readyData()];
int numRead = in.read(readBuffer);
if (numRead<=0) {
return currentBuffer;
} else {
char[] retVal = new char[currentBuffer.length + numRead];
System.arraycopy(currentBuffer, 0, retVal, 0, currentBuffer.length);
System.arraycopy(readBuffer, 0, retVal, currentBuffer.length, numRead);
return retVal;
}
}
void start() throws IOException {
httpServer.start();
}
void shutdown() {
try {
httpServer.shutdown(2, TimeUnit.SECONDS).get();
} catch (Exception e) {
logger.info("An error occurred, trying to shutdown REST HTTP server", e);
}
}
/**
* This class implements the non-blocking http handler. It takes the incoming requests and puts them into
* a blocking(!) queue to be processed by interested consumers.
*
* Blocking? Yes, because this way we apply backpressure and only read those messages from the wire that we are
* able to process. The size of the queue defines, how many requests we are willing to lose in the worst case
*/
private class NonBlockingHttpHandler extends HttpHandler {
private final Subject> stream;
private NonBlockingHttpHandler(
Subject> stream) {
this.stream = stream;
}
public void service(Request request, Response response) throws Exception {
response.suspend();
NIOWriter out = response.getNIOWriter();
NIOReader in = request.getNIOReader();
in.notifyAvailable(new ReadHandler() {
private char[] inputBuffer = new char[0];
@Override
public void onDataAvailable() throws Exception {
inputBuffer = appendAvailableDataToBuffer(in, inputBuffer);
in.notifyAvailable(this);
}
@Override
public void onError(Throwable t) {
logger.error("Error parsing request data", t);
response.setStatus(400, "Bad request");
response.resume();
}
@Override
public void onAllDataRead() throws Exception {
inputBuffer = appendAvailableDataToBuffer(in, inputBuffer);
String requestAsString = new String(inputBuffer);
try {
in.close();
} catch (Exception ignored) {
// TODO - this is from the example. Do we want to do something here?
}
InternalMessageType requestObject = convertRequestData(new String(inputBuffer), messageUnmarshaller);
HttpRpcInvocation extends InternalMessageType> rpci =
new HttpRpcInvocation<>(
requestObject,
httpSpecificInfoFrom(request),
replyInfo -> {
try {
String responseAsString = convertResponseData(replyInfo._1, messageMarshaller);
response.setContentType("application/json");
response.setContentLength(responseAsString.length());
int statusCode = replyInfo._2 == null ? 200 : replyInfo._2.statusCode;
response.setStatus(statusCode);
writeResponse(responseAsString, out);
metricsCollector.requestCompleted(requestObject, responseAsString);
} catch (Exception e) {
metricsCollector.requestCompletedExceptionally(requestObject, e);
logger.error("An error occurred trying to send HTTP response " + replyInfo, e);
try {
response.sendError(500, "Internal server error");
} catch (Exception any) {
logger.error("Failed to send error 500 back to client", any);
}
} finally {
try {
out.close();
} catch (Exception ignored) {
// TODO - this is from the example. Do we want to do something here?
}
response.resume();
}
},
error -> {
logger.error("An error occurred trying to process HTTP request " + requestAsString, error);
try {
response.sendError(500, "Internal server error");
} catch (Exception any) {
logger.error("Failed to send error 500 back to client", any);
}
}
);
metricsCollector.requestReceived(rpci.request);
stream.onNext(rpci);
}
});
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy