Skip to content

Commit

Permalink
GH-15754 remove nanohttpd
Browse files Browse the repository at this point in the history
  • Loading branch information
krasinski committed Dec 11, 2023
1 parent b3b6cb4 commit c98cfd3
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 198 deletions.
7 changes: 2 additions & 5 deletions h2o-clustering/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,13 @@ repositories {
}

dependencies {
api 'org.nanohttpd:nanohttpd:2.3.1'
api 'org.nanohttpd:nanohttpd-webserver:2.3.1'
api 'org.nanohttpd:nanohttpd-nanolets:2.3.1'
compileOnly project(":h2o-core") // This module is intended to be put on H2O's classpath separately
compileOnly "javax.servlet:javax.servlet-api:${servletApiVersion}"

testImplementation group: 'junit', name: 'junit', version: '4.12'
testImplementation group: 'junit', name: 'junit', version: '4.13.1'
testImplementation 'com.github.stefanbirkner:system-rules:1.19.0'
testImplementation project(":h2o-test-support")
testImplementation "commons-io:commons-io:2.4"
testImplementation 'commons-io:commons-io:2.7'
testRuntimeOnly project(":${defaultWebserverModule}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import org.apache.log4j.Logger;
import water.H2O;
import water.clustering.api.AssistedClusteringRestApi;
import water.clustering.api.GracefulAsyncRunner;
import water.init.AbstractEmbeddedH2OConfig;
import water.init.EmbeddedConfigProvider;
import water.util.Log;
Expand Down Expand Up @@ -45,7 +44,6 @@ private Optional<AssistedClusteringRestApi> startAssistedClusteringRestApi(final
Log.info("Starting assisted clustering REST API services");
try {
final AssistedClusteringRestApi assistedClusteringRestApi = new AssistedClusteringRestApi(flatFileCallback);
assistedClusteringRestApi.setAsyncRunner(new GracefulAsyncRunner());
assistedClusteringRestApi.start();
Log.info("Assisted clustering REST API services successfully started.");
return Optional.of(assistedClusteringRestApi);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,92 +1,90 @@
package water.clustering.api;

import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.router.RouterNanoHTTPD;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.apache.log4j.Logger;
import water.init.NetworkInit;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static java.net.HttpURLConnection.*;
import static water.clustering.api.HttpResponses.*;

/**
* A REST Endpoint waiting for external assist to POST a flatifle with H2O nodes.
* Once successfully submitted, this endpoint will no longer accept any new calls.
* A REST Endpoint waiting for external assist to POST a flatfile with H2O nodes.
* Once successfully submitted, this endpoint will no longer accept any new calls.
* It is the caller's responsibility to submit a valid flatfile.
*
* <p>
* There is no parsing or validation done on the flatfile, except for basic emptiness checks.
* The logic for IPv4/IPv6 parsing is hidden in {@link water.init.NetworkInit} class and is therefore hidden
* The logic for IPv4/IPv6 parsing is hidden in {@link NetworkInit} class and is therefore hidden
* from this class. As this module is intended to insertable onto classpath of any H2O, it does not rely on
* specific NetworkInit implementation.
*/
public class AssistedClusteringEndpoint extends RouterNanoHTTPD.DefaultHandler {
public class AssistedClusteringEndpoint implements HttpHandler, AutoCloseable {

private static final Logger LOG = Logger.getLogger(AssistedClusteringEndpoint.class);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final AtomicBoolean flatFileReceived;
private final ExecutorService flatFileConsumerCallbackExecutor = Executors.newSingleThreadExecutor();
private final Consumer<String> flatFileConsumer;

public AssistedClusteringEndpoint() {
flatFileReceived = new AtomicBoolean(false);
public AssistedClusteringEndpoint(Consumer<String> flatFileConsumer) {
this.flatFileConsumer = flatFileConsumer;
this.flatFileReceived = new AtomicBoolean(false);
}

@Override
public String getText() {
throw new IllegalStateException(String.format("Method getText should not be called on '%s'",
getClass().getName()));
}

@Override
public String getMimeType() {
return "text/plain";
}

@Override
public NanoHTTPD.Response.IStatus getStatus() {
return null;
}

public static final String RESPONSE_MIME_TYPE = "text/plain";
public void handle(HttpExchange httpExchange) throws IOException {
if (!POST_METHOD.equals(httpExchange.getRequestMethod())) {
newResponseCodeOnlyResponse(httpExchange, HTTP_BAD_METHOD);
}

@Override
public NanoHTTPD.Response post(final RouterNanoHTTPD.UriResource uriResource, final Map<String, String> urlParams, final NanoHTTPD.IHTTPSession session) {
final Map<String, String> map = new HashMap<>();
try {
session.parseBody(map);
} catch (IOException | NanoHTTPD.ResponseException e) {
String postBody;
try (InputStreamReader isr = new InputStreamReader(httpExchange.getRequestBody(), StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(isr)) {
postBody = br.lines().collect(Collectors.joining("\n"));
if (postBody.isEmpty()) {
newFixedLengthResponse(httpExchange, HTTP_BAD_REQUEST,
MIME_TYPE_TEXT_PLAIN, "Unable to parse IP addresses in body. Only one IPv4/IPv6 address per line is accepted.");
return;
}
} catch (IOException e) {
LOG.error("Received incorrect flatfile request.", e);
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.BAD_REQUEST, RESPONSE_MIME_TYPE, null);
newResponseCodeOnlyResponse(httpExchange, HTTP_BAD_REQUEST);
return;
}
// The text/plain content-type is stored as `postData` by HTTPD in the map.
final String postBody = map.get("postData");

if (postBody != null) {
final Lock writeLock = lock.writeLock();
try {
writeLock.lock();
if (flatFileReceived.get()) {
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.BAD_REQUEST, RESPONSE_MIME_TYPE,
"Flatfile already provided.");
} else {
final Consumer<String> flatFileConsumer = (Consumer<String>) uriResource.initParameter(Consumer.class);
// Do not block response with internal handling
flatFileConsumerCallbackExecutor.submit(() -> flatFileConsumer.accept(postBody));
flatFileReceived.set(true); // Do not accept any new requests once the flatfile has been received.
}
} finally {
writeLock.unlock();
final Lock writeLock = lock.writeLock();
try {
writeLock.lock();
if (flatFileReceived.get()) {
newFixedLengthResponse(httpExchange, HTTP_BAD_REQUEST, MIME_TYPE_TEXT_PLAIN, "Flatfile already provided.");
return;
} else {
// Do not block response with internal handling
flatFileConsumerCallbackExecutor.submit(() -> flatFileConsumer.accept(postBody));
flatFileReceived.set(true); // Do not accept any new requests once the flatfile has been received.
}
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, RESPONSE_MIME_TYPE, null);
} else {
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.BAD_REQUEST, RESPONSE_MIME_TYPE,
"Unable to parse IP addresses in body. Only one IPv4/IPv6 address per line is accepted.");
} finally {
writeLock.unlock();
}
newResponseCodeOnlyResponse(httpExchange, HTTP_OK);
}

@Override
public void close() {
flatFileConsumerCallbackExecutor.shutdown();
}
}

Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
package water.clustering.api;

import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.router.RouterNanoHTTPD;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.function.Consumer;

/**
* Rest API definition for the assisted clustering function.
*/
public class AssistedClusteringRestApi extends RouterNanoHTTPD implements AutoCloseable {
public class AssistedClusteringRestApi implements AutoCloseable {

/**
* Default port to bind to / listen on.
*/
private static final int DEFAULT_PORT = 8080;
public static final String ASSISTED_CLUSTERING_PORT_KEY = "H2O_ASSISTED_CLUSTERING_API_PORT";
private final Consumer<String> flatFileConsumer;
private final AssistedClusteringEndpoint assistedClusteringEndpoint;

private final HttpServer server;

/**
* Creates, but not starts assisted clustering REST API. To start the REST API, please use
Expand All @@ -27,13 +28,13 @@ public class AssistedClusteringRestApi extends RouterNanoHTTPD implements AutoCl
* The REST API is bound to a default port of 8080, unless specified otherwise by the H2O_ASSISTED_CLUSTERING_API_PORT environment
* variable.
*/
public AssistedClusteringRestApi(Consumer<String> flatFileConsumer) {
super(getPort());
public AssistedClusteringRestApi(Consumer<String> flatFileConsumer) throws IOException {
Objects.requireNonNull(flatFileConsumer);
this.flatFileConsumer = flatFileConsumer;
this.assistedClusteringEndpoint = new AssistedClusteringEndpoint(flatFileConsumer);
int port = getPort();
server = HttpServer.create(new InetSocketAddress(port), 0);
addMappings();
}

/**
* @return Either user-defined port via environment variable or default port to bind the REST API to.
*/
Expand All @@ -51,30 +52,21 @@ private static int getPort() {
}
}

@Override
public void addMappings() {
super.addMappings();
addRoute("/clustering/flatfile", AssistedClusteringEndpoint.class, this.flatFileConsumer);
addRoute("/cluster/status", H2OClusterStatusEndpoint.class);
private void addMappings() {
server.createContext("/clustering/flatfile", assistedClusteringEndpoint);
server.createContext("/cluster/status", new H2OClusterStatusEndpoint());
}

/**
* From AutoCloseable - aids usage inside try-with-resources blocks.
*/
@Override
public void close() {
stop();
assistedClusteringEndpoint.close();
server.stop(0);
}

@Override
public void start() throws IOException {
// Make sure the API is never ran as daemon and is properly terminated with the H2O JVM (the latest)
start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}

@Override
public void start(int timeout) throws IOException {
// Make sure the API is never ran as daemon and is properly terminated with the H2O JVM (the latest)
super.start(timeout, false);
server.start();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,42 +1,32 @@
package water.clustering.api;

import fi.iki.elonen.NanoHTTPD;
import fi.iki.elonen.router.RouterNanoHTTPD;
import water.H2O;
import water.H2ONode;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import static water.clustering.api.HttpResponses.*;

public class H2OClusterStatusEndpoint extends RouterNanoHTTPD.DefaultHandler {

@Override
public String getText() {
throw new IllegalStateException(String.format("Method getText should not be called on '%s'",
getClass().getName()));
}
public class H2OClusterStatusEndpoint implements HttpHandler {

@Override
public String getMimeType() {
return "application/json";
}

@Override
public NanoHTTPD.Response.IStatus getStatus() {
throw new IllegalStateException(String.format("Method getMimeType should not be called on '%s'",
getClass().getName()));
}

@Override
public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map<String, String> urlParams, NanoHTTPD.IHTTPSession session) {
public void handle(HttpExchange httpExchange) throws IOException {
if (!GET_METHOD.equals(httpExchange.getRequestMethod())) {
newResponseCodeOnlyResponse(httpExchange, HttpURLConnection.HTTP_BAD_METHOD);
}
// H2O cluster grows in time, even when a flat file is used. The H2O.CLOUD property might be updated with new nodes during
// the clustering process and doesn't necessarily have to contain all the nodes since the very beginning of the clustering process.
// From this endpoint's point of view, H2O is clustered if and only if the H2O cloud members contain all nodes defined in the
// flat file.

if (!H2O.isFlatfileEnabled()) {
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.NO_CONTENT, getMimeType(), null);
newResponseCodeOnlyResponse(httpExchange, HttpURLConnection.HTTP_NO_CONTENT);
return;
}
final Set<H2ONode> flatFile = H2O.getFlatfile();
final H2ONode[] cloudMembers = H2O.CLOUD.members();
Expand All @@ -45,9 +35,9 @@ public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map<Strin

if (!clustered) {
// If there is no cluster, there is no content to report.
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.NO_CONTENT, getMimeType(), null);
newResponseCodeOnlyResponse(httpExchange, HttpURLConnection.HTTP_NO_CONTENT);
} else {
return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.OK, getMimeType(), nodesListJson());
newFixedLengthResponse(httpExchange, HttpURLConnection.HTTP_OK, MIME_TYPE_JSON, nodesListJson());
}
}

Expand Down
Loading

0 comments on commit c98cfd3

Please sign in to comment.