This is an automated email from the ASF dual-hosted git repository. kenhuuu pushed a commit to branch graphsonv4-driver in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 08535f03059f5b7c3ae53cf43205b9ea9e9d6869 Author: Ken Hu <[email protected]> AuthorDate: Thu Oct 10 22:35:50 2024 -0700 Re-enable GraphSON message serializer for debugging. The GraphSONv4 MessageSerializer doesn't support chunking so the HttpObjectAggregator is added back and the HttpGremlinResponseStreamDecoder is replaced with the original, non-streaming version. This is mainly for debugging and testing purposes so lowered performance is expected. --- .../tinkerpop/gremlin/driver/Channelizer.java | 10 ++- .../apache/tinkerpop/gremlin/driver/Cluster.java | 17 ++++- .../driver/handler/HttpGremlinResponseDecoder.java | 87 ++++++++++++++++++++++ ...tor.java => PayloadSerializingInterceptor.java} | 17 +++-- .../gremlin/driver/simple/SimpleHttpClient.java | 5 +- .../tinkerpop/gremlin/driver/ClusterTest.java | 4 +- .../gremlin/server/GremlinDriverIntegrateTest.java | 47 +++++++++++- .../gremlin/server/GremlinServerIntegrateTest.java | 6 +- .../gremlin/server/TestClientFactory.java | 5 ++ 9 files changed, 177 insertions(+), 21 deletions(-) diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java index c43b80c7d2..b64f969838 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java @@ -24,6 +24,7 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; @@ -31,7 +32,7 @@ import org.apache.tinkerpop.gremlin.driver.exception.ConnectionException; import org.apache.tinkerpop.gremlin.driver.handler.GremlinResponseHandler; import org.apache.tinkerpop.gremlin.driver.handler.HttpContentDecompressionHandler; import org.apache.tinkerpop.gremlin.driver.handler.HttpGremlinRequestEncoder; -import org.apache.tinkerpop.gremlin.driver.handler.HttpGremlinResponseStreamDecoder; +import org.apache.tinkerpop.gremlin.driver.handler.HttpGremlinResponseDecoder; import org.apache.tinkerpop.gremlin.driver.handler.SslCheckHandler; import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; @@ -95,6 +96,7 @@ public interface Channelizer extends ChannelHandler { public static final String PIPELINE_SSL_HANDLER = "gremlin-ssl-handler"; protected static final String PIPELINE_HTTP_CODEC = "http-codec"; + protected static final String PIPELINE_HTTP_AGGREGATOR = "http-aggregator"; protected static final String PIPELINE_HTTP_ENCODER = "gremlin-encoder"; protected static final String PIPELINE_HTTP_DECODER = "gremlin-decoder"; protected static final String PIPELINE_HTTP_DECOMPRESSION_HANDLER = "http-decompression-handler"; @@ -182,7 +184,7 @@ public interface Channelizer extends ChannelHandler { ResponseMessage.build().code(HttpResponseStatus.NO_CONTENT).result(Collections.emptyList()).create(); private HttpGremlinRequestEncoder gremlinRequestEncoder; - private HttpGremlinResponseStreamDecoder gremlinResponseDecoder; + private HttpGremlinResponseDecoder gremlinResponseDecoder; private HttpContentDecompressionHandler httpCompressionDecoder; @@ -193,7 +195,7 @@ public interface Channelizer extends ChannelHandler { httpCompressionDecoder = new HttpContentDecompressionHandler(); gremlinRequestEncoder = new HttpGremlinRequestEncoder(cluster.getSerializer(), cluster.getRequestInterceptors(), cluster.isUserAgentOnConnectEnabled(), cluster.isBulkingEnabled(), connection.getUri()); - gremlinResponseDecoder = new HttpGremlinResponseStreamDecoder(cluster.getSerializer(), cluster.getMaxResponseContentLength()); + gremlinResponseDecoder = new HttpGremlinResponseDecoder(cluster.getSerializer()); } @Override @@ -222,6 +224,8 @@ public interface Channelizer extends ChannelHandler { DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS, false); pipeline.addLast(PIPELINE_HTTP_CODEC, handler); + pipeline.addLast(PIPELINE_HTTP_AGGREGATOR, new HttpObjectAggregator(cluster.getMaxResponseContentLength() > 0 + ? (int) cluster.getMaxResponseContentLength() : Integer.MAX_VALUE)); pipeline.addLast(PIPELINE_HTTP_ENCODER, gremlinRequestEncoder); pipeline.addLast(PIPELINE_HTTP_DECOMPRESSION_HANDLER, httpCompressionDecoder); pipeline.addLast(PIPELINE_HTTP_DECODER, gremlinResponseDecoder); diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java index 6f18c394e1..8d14d9cf53 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Cluster.java @@ -31,9 +31,10 @@ import org.apache.commons.configuration2.Configuration; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.apache.commons.lang3.tuple.Pair; import org.apache.tinkerpop.gremlin.driver.auth.Auth; -import org.apache.tinkerpop.gremlin.driver.interceptor.GraphBinarySerializationInterceptor; +import org.apache.tinkerpop.gremlin.driver.interceptor.PayloadSerializingInterceptor; import org.apache.tinkerpop.gremlin.util.MessageSerializer; import org.apache.tinkerpop.gremlin.util.message.RequestMessage; +import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4; import org.apache.tinkerpop.gremlin.util.ser.Serializers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -151,6 +152,10 @@ public final class Cluster { return new Builder(address); } + public static Builder build(final RequestInterceptor serializingInterceptor) { + return new Builder(serializingInterceptor); + } + public static Builder build(final File configurationFile) throws FileNotFoundException { final Settings settings = Settings.read(new FileInputStream(configurationFile)); return getBuilderFromSettings(settings); @@ -511,12 +516,18 @@ public final class Cluster { private boolean enableBulkedResult = false; private Builder() { - addInterceptor(SERIALIZER_INTERCEPTOR_NAME, new GraphBinarySerializationInterceptor()); + addInterceptor(SERIALIZER_INTERCEPTOR_NAME, + new PayloadSerializingInterceptor(new GraphBinaryMessageSerializerV4())); } private Builder(final String address) { addContactPoint(address); - addInterceptor(SERIALIZER_INTERCEPTOR_NAME, new GraphBinarySerializationInterceptor()); + addInterceptor(SERIALIZER_INTERCEPTOR_NAME, + new PayloadSerializingInterceptor(new GraphBinaryMessageSerializerV4())); + } + + private Builder(final RequestInterceptor bodySerializer) { + addInterceptor(SERIALIZER_INTERCEPTOR_NAME, bodySerializer); } /** diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java new file mode 100644 index 0000000000..3bd1719342 --- /dev/null +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/handler/HttpGremlinResponseDecoder.java @@ -0,0 +1,87 @@ +/* + * 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.tinkerpop.gremlin.driver.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.handler.codec.http.DefaultHttpObject; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; +import io.netty.util.AttributeMap; +import io.netty.util.CharsetUtil; +import org.apache.tinkerpop.gremlin.util.MessageSerializer; +import org.apache.tinkerpop.gremlin.util.message.ResponseMessage; +import org.apache.tinkerpop.gremlin.util.ser.SerTokens; +import org.apache.tinkerpop.gremlin.util.ser.SerializationException; +import org.apache.tinkerpop.shaded.jackson.databind.JsonNode; +import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper; + +import java.util.List; + +import static org.apache.tinkerpop.gremlin.driver.Channelizer.HttpChannelizer.LAST_CONTENT_READ_RESPONSE; +import static org.apache.tinkerpop.gremlin.driver.handler.HttpGremlinResponseStreamDecoder.IS_BULKED; + +public class HttpGremlinResponseDecoder extends MessageToMessageDecoder<FullHttpResponse> { + private static final String MESSAGE_NAME = "message"; + private final MessageSerializer<?> serializer; + private final ObjectMapper mapper = new ObjectMapper(); + + public HttpGremlinResponseDecoder(final MessageSerializer<?> serializer) { + this.serializer = serializer; + } + + @Override + protected void decode(ChannelHandlerContext ctx, FullHttpResponse msg, List<Object> out) throws Exception { + final ByteBuf content = msg.content(); + ResponseMessage response; + + try { + // no more chunks expected + if (isError(msg.status()) && !serializer.mimeTypesSupported()[0].equals(msg.headers().get(HttpHeaderNames.CONTENT_TYPE))) { + final String json = content.toString(CharsetUtil.UTF_8); + final JsonNode node = mapper.readTree(json); + final String message = node.has(MESSAGE_NAME) ? node.get(MESSAGE_NAME).asText() : ""; + response = ResponseMessage.build() + .code(msg.status()) + .statusMessage(message.isEmpty() ? msg.status().reasonPhrase() : message) + .create(); + } else { + response = serializer.deserializeBinaryResponse(content); + } + + ctx.channel().attr(IS_BULKED).set(response.getResult().isBulked()); + out.add(response); + out.add(LAST_CONTENT_READ_RESPONSE); + } catch (SerializationException e) { + throw new RuntimeException(e); + } + } + + private static boolean isError(final HttpResponseStatus status) { + return status != HttpResponseStatus.OK; + } +} \ No newline at end of file diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/interceptor/GraphBinarySerializationInterceptor.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/interceptor/PayloadSerializingInterceptor.java similarity index 82% rename from gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/interceptor/GraphBinarySerializationInterceptor.java rename to gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/interceptor/PayloadSerializingInterceptor.java index a6e656eea8..13616177c1 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/interceptor/GraphBinarySerializationInterceptor.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/interceptor/PayloadSerializingInterceptor.java @@ -29,14 +29,21 @@ import org.apache.tinkerpop.gremlin.util.MessageSerializer; import org.apache.tinkerpop.gremlin.util.message.RequestMessage; import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4; import org.apache.tinkerpop.gremlin.util.ser.SerializationException; +import org.apache.tinkerpop.gremlin.util.ser.Serializers; + +import java.util.Map; /** - * A {@link RequestInterceptor} that serializes the request body to the {@code GraphBinary} format. This interceptor - * should be run before other interceptors that need to calculate values based on the request body. + * A {@link RequestInterceptor} that serializes the request body usng the provided {@link MessageSerializer}. This + * interceptor should be run before other interceptors that need to calculate values based on the request body. */ -public class GraphBinarySerializationInterceptor implements RequestInterceptor { - // Should be thread-safe as the GraphBinaryWriter doesn't maintain state. - private static final MessageSerializer serializer = new GraphBinaryMessageSerializerV4(); +public class PayloadSerializingInterceptor implements RequestInterceptor { + // Should be thread-safe as the GraphBinaryWriter/GraphSONMessageSerializer doesn't maintain state. + private final MessageSerializer serializer; + + public PayloadSerializingInterceptor(final MessageSerializer serializer) { + this.serializer = serializer; + } @Override public HttpRequest apply(HttpRequest httpRequest) { diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/SimpleHttpClient.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/SimpleHttpClient.java index b2d932b2e2..232f94d1f4 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/SimpleHttpClient.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/simple/SimpleHttpClient.java @@ -28,7 +28,7 @@ import org.apache.tinkerpop.gremlin.driver.Channelizer; import org.apache.tinkerpop.gremlin.driver.handler.HttpContentDecompressionHandler; import org.apache.tinkerpop.gremlin.driver.handler.HttpGremlinResponseStreamDecoder; import org.apache.tinkerpop.gremlin.driver.handler.HttpGremlinRequestEncoder; -import org.apache.tinkerpop.gremlin.driver.interceptor.GraphBinarySerializationInterceptor; +import org.apache.tinkerpop.gremlin.driver.interceptor.PayloadSerializingInterceptor; import org.apache.tinkerpop.gremlin.util.MessageSerializer; import org.apache.tinkerpop.gremlin.util.message.RequestMessage; import io.netty.bootstrap.Bootstrap; @@ -110,7 +110,8 @@ public class SimpleHttpClient extends AbstractClient { new HttpGremlinResponseStreamDecoder(serializer, Integer.MAX_VALUE), new HttpGremlinRequestEncoder(serializer, Collections.singletonList( - Pair.of("serializer", new GraphBinarySerializationInterceptor())), + Pair.of("serializer", new PayloadSerializingInterceptor( + new GraphBinaryMessageSerializerV4()))), false, false, uri), callbackResponseHandler); } diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ClusterTest.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ClusterTest.java index fff9485a2a..4a56ed7f06 100644 --- a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ClusterTest.java +++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ClusterTest.java @@ -19,7 +19,7 @@ package org.apache.tinkerpop.gremlin.driver; import org.apache.commons.lang3.tuple.Pair; -import org.apache.tinkerpop.gremlin.driver.interceptor.GraphBinarySerializationInterceptor; +import org.apache.tinkerpop.gremlin.driver.interceptor.PayloadSerializingInterceptor; import org.junit.Test; import java.util.List; @@ -132,7 +132,7 @@ public class ClusterTest { public void shouldContainBodySerializerByDefault() { final List<Pair<String, ? extends RequestInterceptor>> interceptors = Cluster.build().create().getRequestInterceptors(); assertEquals(1, interceptors.size()); - assertTrue(interceptors.get(0).getRight() instanceof GraphBinarySerializationInterceptor); + assertTrue(interceptors.get(0).getRight() instanceof PayloadSerializingInterceptor); } @Test diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java index b14b49959a..6dc11483f1 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinDriverIntegrateTest.java @@ -33,6 +33,7 @@ import org.apache.tinkerpop.gremlin.driver.Result; import org.apache.tinkerpop.gremlin.driver.ResultSet; import org.apache.tinkerpop.gremlin.driver.exception.NoHostAvailableException; import org.apache.tinkerpop.gremlin.driver.exception.ResponseException; +import org.apache.tinkerpop.gremlin.driver.interceptor.PayloadSerializingInterceptor; import org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin; import org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer; import org.apache.tinkerpop.gremlin.structure.Vertex; @@ -44,6 +45,7 @@ import org.apache.tinkerpop.gremlin.util.TimeUtil; import org.apache.tinkerpop.gremlin.util.function.FunctionUtils; import org.apache.tinkerpop.gremlin.util.message.RequestMessage; import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4; +import org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4; import org.apache.tinkerpop.gremlin.util.ser.Serializers; import org.junit.AfterClass; import org.junit.Before; @@ -208,6 +210,45 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration } } + @Test + public void shouldWorkWithGraphSONSerializer() throws Exception { + final Cluster cluster = TestClientFactory.build(new PayloadSerializingInterceptor(new GraphSONMessageSerializerV4())) + .serializer(Serializers.GRAPHSON_V4.simpleInstance()).create(); + + try { + final Client client = cluster.connect(); + assertEquals(2, client.submit("g.inject(2)").all().get().get(0).getInt()); + } finally { + cluster.close(); + } + } + + @Test + public void shouldWorkWithGraphSONRequestAndGraphBinaryResponse() throws Exception { + final Cluster cluster = TestClientFactory.build(new PayloadSerializingInterceptor(new GraphSONMessageSerializerV4())) + .serializer(Serializers.GRAPHBINARY_V4.simpleInstance()).create(); + + try { + final Client client = cluster.connect(); + assertEquals(3, client.submit("g.inject(3)").all().get().get(0).getInt()); + } finally { + cluster.close(); + } + } + + @Test + public void shouldWorkWithGraphBinaryRequestAndGraphSONResponse() throws Exception { + final Cluster cluster = TestClientFactory.build(new PayloadSerializingInterceptor(new GraphBinaryMessageSerializerV4())) + .serializer(Serializers.GRAPHSON_V4.simpleInstance()).create(); + + try { + final Client client = cluster.connect(); + assertEquals(5, client.submit("g.inject(5)").all().get().get(0).getInt()); + } finally { + cluster.close(); + } + } + @Test public void shouldInterceptRequestsWithHandshake() throws Exception { final int requestsToMake = 32; @@ -296,7 +337,7 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration public void shouldEventuallySucceedAfterChannelLevelError() { final Cluster cluster = TestClientFactory.build() .reconnectInterval(500) - .maxResponseContentLength(64).create(); + .maxResponseContentLength(32).create(); // Warning: compression can change the content length. Adjust as needed. final Client client = cluster.connect(); try { @@ -305,7 +346,7 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration fail("Request should have failed because it exceeded the max content length allowed"); } catch (Exception ex) { final Throwable root = ExceptionHelper.getRootCause(ex); - assertThat(root.getMessage(), containsString("Response exceeded 64 bytes.")); + assertThat(root.getMessage(), containsString("Response entity too large")); } assertEquals(2, client.submit("1+1").all().join().get(0).getInt()); @@ -786,7 +827,7 @@ public class GremlinDriverIntegrateTest extends AbstractGremlinServerIntegration fail("Should throw an exception."); } catch (Exception re) { final Throwable root = ExceptionHelper.getRootCause(re); - assertTrue(root.getMessage().equals("Response exceeded 1 bytes.")); + assertTrue(root.getMessage().contains("Response entity too large")); } finally { cluster.close(); } diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java index 9ee754ce8e..d6bd151956 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/GremlinServerIntegrateTest.java @@ -743,9 +743,9 @@ public class GremlinServerIntegrateTest extends AbstractGremlinServerIntegration } catch (Exception re) { final Throwable root = ExceptionHelper.getRootCause(re); - // went with two possible error messages here as i think that there is some either non-deterministic - // behavior around the error message or it's environmentally dependent (e.g. different jdk, versions, etc) - assertThat(root.getMessage(), Matchers.anyOf(is("Connection to server is no longer active"), is("Connection reset by peer"))); + // the server sends back a 413 Request Entity Too Large now in HTTP so detect that rather than the + // underlying connection closing due to error. + assertThat(root.getMessage(), Matchers.anyOf(is("Request Entity Too Large"))); // validate that we can still send messages to the server assertEquals(2, client.submit("1+1").all().join().get(0).getInt()); diff --git a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/TestClientFactory.java b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/TestClientFactory.java index c8f1f4c6f1..ff7de898ca 100644 --- a/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/TestClientFactory.java +++ b/gremlin-server/src/test/java/org/apache/tinkerpop/gremlin/server/TestClientFactory.java @@ -19,6 +19,7 @@ package org.apache.tinkerpop.gremlin.server; import org.apache.tinkerpop.gremlin.driver.Cluster; +import org.apache.tinkerpop.gremlin.driver.RequestInterceptor; import org.apache.tinkerpop.gremlin.driver.simple.SimpleHttpClient; import java.net.URI; @@ -42,6 +43,10 @@ public final class TestClientFactory { return Cluster.build(address).port(PORT); } + public static Cluster.Builder build(final RequestInterceptor serializingInterceptor) { + return Cluster.build(serializingInterceptor).port(PORT); + } + public static Cluster open() { return build().create(); }
