Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fast compiled route matcher #10131

Draft
wants to merge 24 commits into
base: 4.3.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.profile.AsyncProfiler;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
Expand Down Expand Up @@ -129,7 +128,6 @@ public void setUp() {
//System.out.println(response.content().toString(StandardCharsets.UTF_8));
Assertions.assertEquals(HttpResponseStatus.OK, response.status());
Assertions.assertEquals("application/json", response.headers().get(HttpHeaderNames.CONTENT_TYPE));
Assertions.assertEquals("keep-alive", response.headers().get(HttpHeaderNames.CONNECTION));
String expectedResponseBody = "{\"listIndex\":4,\"stringIndex\":0}";
Assertions.assertEquals(expectedResponseBody, response.content().toString(StandardCharsets.UTF_8));
Assertions.assertEquals(expectedResponseBody.length(), response.headers().getInt(HttpHeaderNames.CONTENT_LENGTH));
Expand Down Expand Up @@ -165,7 +163,8 @@ Stack openChannel() {
ApplicationContext ctx = ApplicationContext.run(Map.of(
"spec.name", "FullHttpStackBenchmark",
//"micronaut.server.netty.server-type", NettyHttpServerConfiguration.HttpServerType.FULL_CONTENT,
"micronaut.server.date-header", false // disabling this makes the response identical each time
"micronaut.server.date-header", false, // disabling this makes the response identical each time
"micronaut.server.netty.fast-routing", true
));
EmbeddedServer server = ctx.getBean(EmbeddedServer.class);
EmbeddedChannel channel = ((NettyHttpServer) server).buildEmbeddedChannel(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void writeTo(Argument<ByteBuf> type, MediaType mediaType, ByteBuf object,
}

@Override
public ByteBuffer<?> writeTo(Argument<ByteBuf> type, MediaType mediaType, ByteBuf object, MutableHeaders outgoingHeaders, ByteBufferFactory<?, ?> bufferFactory) throws CodecException {
public ByteBuffer<?> writeTo(MediaType mediaType, ByteBuf object, ByteBufferFactory<?, ?> bufferFactory) throws CodecException {
return NettyByteBufferFactory.DEFAULT.wrap(object);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@
import io.micronaut.json.JsonMapper;
import io.micronaut.json.body.JsonMessageHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

Expand All @@ -47,7 +54,6 @@
*
* @param <T> The type
*/
@SuppressWarnings("DefaultAnnotationParam")
@Singleton
@Internal
@Replaces(JsonMessageHandler.class)
Expand All @@ -71,7 +77,7 @@
})
@BootstrapContextCompatible
@Requires(beans = JsonMapper.class)
public final class NettyJsonHandler<T> implements MessageBodyHandler<T>, ChunkedMessageBodyReader<T>, CustomizableNettyJsonHandler {
public final class NettyJsonHandler<T> implements MessageBodyHandler<T>, ChunkedMessageBodyReader<T>, CustomizableNettyJsonHandler, ShortCircuitNettyBodyWriter<T> {
private final JsonMessageHandler<T> jsonMessageHandler;

public NettyJsonHandler(JsonMapper jsonMapper) {
Expand Down Expand Up @@ -135,6 +141,19 @@ public ByteBuffer<?> writeTo(Argument<T> type, MediaType mediaType, T object, Mu
return jsonMessageHandler.writeTo(type, mediaType, object, outgoingHeaders, bufferFactory);
}

@Override
public void writeTo(io.micronaut.http.HttpHeaders requestHeaders, HttpResponseStatus status, HttpHeaders responseHeaders, T object, NettyWriteContext nettyContext) {
ByteBuf buffer = nettyContext.alloc().buffer();
JsonMapper jsonMapper = jsonMessageHandler.getJsonMapper();
try {
jsonMapper.writeValue(new ByteBufOutputStream(buffer), object);
} catch (IOException e) {
buffer.release();
throw new CodecException("Error encoding object [" + object + "] to JSON: " + e.getMessage(), e);
}
nettyContext.writeFull(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buffer, responseHeaders, EmptyHttpHeaders.INSTANCE));
}

@Override
public MessageBodyWriter<T> createSpecific(Argument<T> type) {
return new NettyJsonHandler<>((JsonMessageHandler<T>) jsonMessageHandler.createSpecific(type));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,18 @@
import io.micronaut.core.type.Headers;
import io.micronaut.core.type.MutableHeaders;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.body.MessageBodyHandler;
import io.micronaut.http.body.MessageBodyWriter;
import io.micronaut.http.body.TextPlainHandler;
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.netty.NettyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import jakarta.inject.Singleton;
Expand All @@ -50,24 +45,17 @@
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
@Internal
final class NettyTextPlainHandler implements MessageBodyHandler<CharSequence>, NettyBodyWriter<CharSequence> {
final class NettyTextPlainHandler implements MessageBodyHandler<CharSequence>, ShortCircuitNettyBodyWriter<CharSequence> {
private final TextPlainHandler defaultHandler = new TextPlainHandler();

@Override
public void writeTo(HttpRequest<?> request, MutableHttpResponse<CharSequence> outgoingResponse, Argument<CharSequence> type, MediaType mediaType, CharSequence object, NettyWriteContext nettyContext) throws CodecException {
MutableHttpHeaders headers = outgoingResponse.getHeaders();
ByteBuf byteBuf = Unpooled.wrappedBuffer(object.toString().getBytes(MessageBodyWriter.getCharset(headers)));
NettyHttpHeaders nettyHttpHeaders = (NettyHttpHeaders) headers;
io.netty.handler.codec.http.HttpHeaders nettyHeaders = nettyHttpHeaders.getNettyHeaders();
if (!nettyHttpHeaders.contains(HttpHeaders.CONTENT_TYPE)) {
nettyHttpHeaders.set(HttpHeaderNames.CONTENT_TYPE, mediaType);
}
nettyHeaders.set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
public void writeTo(HttpHeaders requestHeaders, HttpResponseStatus status, io.netty.handler.codec.http.HttpHeaders responseHeaders, CharSequence object, NettyWriteContext nettyContext) {
ByteBuf byteBuf = Unpooled.wrappedBuffer(object.toString().getBytes(MessageBodyWriter.getCharset(requestHeaders)));
FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(outgoingResponse.code(), outgoingResponse.reason()),
status,
byteBuf,
nettyHeaders,
responseHeaders,
EmptyHttpHeaders.INSTANCE
);
nettyContext.writeFull(fullHttpResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.io.Writable;
import io.micronaut.core.io.buffer.ByteBuffer;
import io.micronaut.core.io.buffer.ByteBufferFactory;
import io.micronaut.core.type.Argument;
import io.micronaut.core.type.Headers;
import io.micronaut.core.type.MutableHeaders;
Expand Down Expand Up @@ -97,6 +98,11 @@ public void writeTo(Argument<Writable> type, MediaType mediaType, Writable objec
defaultWritable.writeTo(type, mediaType, object, outgoingHeaders, outputStream);
}

@Override
public ByteBuffer<?> writeTo(MediaType mediaType, Writable object, ByteBufferFactory<?, ?> bufferFactory) throws CodecException {
return defaultWritable.writeTo(mediaType, object, bufferFactory);
}

@Override
public Publisher<? extends Writable> readChunked(Argument<Writable> type, MediaType mediaType, Headers httpHeaders, Publisher<ByteBuffer<?>> input) {
return defaultWritable.readChunked(type, mediaType, httpHeaders, input);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2017-2023 original 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
*
* https://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.micronaut.http.netty.body;


import io.micronaut.core.annotation.Experimental;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpHeaders;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.MediaType;
import io.micronaut.http.MutableHttpHeaders;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.codec.CodecException;
import io.micronaut.http.netty.NettyHttpHeaders;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;

/**
* {@link NettyBodyWriter} extension that can do without a {@link HttpRequest}.
*
* @param <T> The body type
* @since 4.3.0
* @author Jonas Konrad
*/
@Internal
@Experimental
public interface ShortCircuitNettyBodyWriter<T> extends NettyBodyWriter<T> {
@Override
default void writeTo(HttpRequest<?> request, MutableHttpResponse<T> outgoingResponse, Argument<T> type, MediaType mediaType, T object, NettyWriteContext writeContext) throws CodecException {
MutableHttpHeaders headers = outgoingResponse.getHeaders();
NettyHttpHeaders nettyHttpHeaders = (NettyHttpHeaders) headers;
io.netty.handler.codec.http.HttpHeaders nettyHeaders = nettyHttpHeaders.getNettyHeaders();
if (!nettyHttpHeaders.contains(HttpHeaders.CONTENT_TYPE)) {
nettyHttpHeaders.set(HttpHeaderNames.CONTENT_TYPE, mediaType);
}
writeTo(request.getHeaders(), HttpResponseStatus.valueOf(outgoingResponse.code(), outgoingResponse.reason()), nettyHeaders, object, writeContext);
}

/**
* Write an object to the given context.
*
* @param requestHeaders The request headers
* @param status The response status
* @param responseHeaders The response headers
* @param object The object to write
* @param nettyContext The netty context
* @throws CodecException If an error occurs decoding
*/
void writeTo(
HttpHeaders requestHeaders,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this mean we need to parse the headers? Or could this be the raw Netty headers

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new NettyHttpHeaders(headers, conversionService) has no logic so this is not a big concern. i tried adding a new header abstraction first to abstract over netty and micronaut http header classes but i saw NettyHttpHeaders is so light that it's pointless.

HttpResponseStatus status,
io.netty.handler.codec.http.HttpHeaders responseHeaders,
T object,
NettyWriteContext nettyContext
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.http.netty.NettyHttpResponseBuilder;
import io.micronaut.http.server.netty.body.ByteBody;
import io.micronaut.http.server.netty.configuration.NettyHttpServerConfiguration;
import io.micronaut.http.server.netty.handler.PipeliningServerHandler;
import io.micronaut.http.server.netty.handler.RequestHandler;
Expand All @@ -30,6 +31,7 @@
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;

/**
* Handler to automatically redirect HTTP to HTTPS request when using dual protocol.
Expand All @@ -50,8 +52,8 @@ record HttpToHttpsRedirectHandler(
) implements RequestHandler {

@Override
public void accept(ChannelHandlerContext ctx, io.netty.handler.codec.http.HttpRequest request, PipeliningServerHandler.OutboundAccess outboundAccess) {
NettyHttpRequest<?> strippedRequest = NettyHttpRequest.createSafe(request, ctx, conversionService, serverConfiguration);
public void accept(ChannelHandlerContext ctx, HttpRequest request, ByteBody body, PipeliningServerHandler.OutboundAccess outboundAccess) {
NettyHttpRequest<?> strippedRequest = NettyHttpRequest.createSafe(request, body, ctx, conversionService, serverConfiguration);

UriBuilder uriBuilder = UriBuilder.of(hostResolver.resolve(strippedRequest));
strippedRequest.release();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public class NettyHttpRequest<T> extends AbstractNettyHttpRequest<T> implements
*/
@SuppressWarnings("MagicNumber")
public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest,
ByteBody body,
ChannelHandlerContext ctx,
ConversionService environment,
HttpServerConfiguration serverConfiguration) throws IllegalArgumentException {
Expand All @@ -199,30 +200,28 @@ public NettyHttpRequest(io.netty.handler.codec.http.HttpRequest nettyRequest,
this.serverConfiguration = serverConfiguration;
this.channelHandlerContext = ctx;
this.headers = new NettyHttpHeaders(nettyRequest.headers(), conversionService);
this.body = ByteBody.of(nettyRequest);
this.body = body;
this.contentLength = headers.contentLength().orElse(-1);
this.contentType = headers.contentType().orElse(null);
this.origin = headers.getOrigin().orElse(null);
}

public static NettyHttpRequest<?> createSafe(io.netty.handler.codec.http.HttpRequest request, ChannelHandlerContext ctx, ConversionService conversionService, NettyHttpServerConfiguration serverConfiguration) {
public static NettyHttpRequest<?> createSafe(io.netty.handler.codec.http.HttpRequest request, ByteBody body, ChannelHandlerContext ctx, ConversionService conversionService, NettyHttpServerConfiguration serverConfiguration) {
try {
return new NettyHttpRequest<>(
request,
body,
ctx,
conversionService,
serverConfiguration
);
} catch (IllegalArgumentException iae) {
// invalid URI
if (request instanceof StreamedHttpRequest streamed) {
streamed.closeIfNoSubscriber();
} else {
((FullHttpRequest) request).release();
}
body.release();

return new NettyHttpRequest<>(
new DefaultFullHttpRequest(request.protocolVersion(), request.method(), "/", Unpooled.EMPTY_BUFFER),
ByteBody.empty(),
ctx,
conversionService,
serverConfiguration
Expand Down
Loading