diff --git a/Dockerfile b/Dockerfile index b3feb0b280..0902778f71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,8 @@ # Usage: # - docker build -t jooby . -# - docker run -it jooby -v "$HOME/.m2":/root/.m2 -# - /build # mvn clean package +# - docker run -v "$HOME/.m2":/root/.m2 -it jooby +# - /build # mvn clean package -P '!git-hooks' FROM maven:3-eclipse-temurin-17 as build diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java index 6be4a100c4..b95fce8fdd 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java @@ -144,7 +144,7 @@ public class NettyContext implements DefaultContext, ChannelFutureListener { private String scheme; private int port; - public void init( + public NettyContext( ChannelHandlerContext ctx, HttpRequest req, Router router, @@ -157,32 +157,9 @@ public void init( this.router = router; this.bufferSize = bufferSize; this.method = req.method().name().toUpperCase(); - /** Clear everything else. */ - this.attributes.clear(); - this.setHeaders.clear(); - this.route = null; - this.decoder = null; - this.webSocket = null; - this.listeners = null; - this.cookies = null; - this.responseCookies = null; - this.needsFlush = false; - this.host = null; - this.port = 0; - this.contentLength = -1; - this.scheme = null; - this.responseType = null; - this.responseStarted = false; - this.headers = null; - this.status = HttpResponseStatus.OK; - this.query = null; - this.formdata = null; - this.files = null; - this.pathMap = Collections.EMPTY_MAP; - this.resetHeadersOnError = null; if (http2) { // Save streamId for HTTP/2 - this.streamId = req.headers().get(STREAM_ID); + this.streamId = header(STREAM_ID).valueOrNull(); ifStreamId(this.streamId); } else { this.streamId = null; @@ -352,7 +329,7 @@ public List getClientCertificates() { throw SneakyThrows.propagate(x); } } - return new ArrayList(); + return Collections.emptyList(); } @NonNull @Override @@ -451,7 +428,7 @@ public Context upgrade(WebSocket.Initializer handler) { handler.init(Context.readOnly(this), webSocket); FullHttpRequest webSocketRequest = new DefaultFullHttpRequest( - req.protocolVersion(), + HTTP_1_1, req.method(), req.uri(), Unpooled.EMPTY_BUFFER, @@ -596,7 +573,7 @@ public PrintWriter responseWriter(MediaType type, Charset charset) { @NonNull @Override public Sender responseSender() { prepareChunked(); - ctx.write(new DefaultHttpResponse(req.protocolVersion(), status, setHeaders)); + ctx.write(new DefaultHttpResponse(HTTP_1_1, status, setHeaders)); return new NettySender(this, ctx); } @@ -638,7 +615,7 @@ public final Context send(ByteBuffer data) { private Context send(@NonNull ByteBuf data) { try { responseStarted = true; - setHeaders.set(CONTENT_LENGTH, Long.toString(data.readableBytes())); + setHeaders.set(CONTENT_LENGTH, Integer.toString(data.readableBytes())); DefaultFullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, data, setHeaders, NO_TRAILING); if (ctx.channel().eventLoop().inEventLoop()) { @@ -903,7 +880,7 @@ void destroy(Throwable cause) { private NettyOutputStream newOutputStream() { prepareChunked(); return new NettyOutputStream( - this, ctx, bufferSize, new DefaultHttpResponse(req.protocolVersion(), status, setHeaders)); + this, ctx, bufferSize, new DefaultHttpResponse(HTTP_1_1, status, setHeaders)); } private FileUpload register(FileUpload upload) { diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java index bd036ade13..3144c08722 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyHandler.java @@ -28,7 +28,6 @@ import io.jooby.SneakyThrows; import io.jooby.StatusCode; import io.jooby.WebSocketCloseStatus; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.http.HttpContent; @@ -43,10 +42,8 @@ import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.timeout.IdleStateEvent; import io.netty.util.AsciiString; -import io.netty.util.Attribute; import io.netty.util.AttributeKey; -@ChannelHandler.Sharable public class NettyHandler extends ChannelInboundHandlerAdapter { private static final AtomicReference cachedDateString = new AtomicReference<>(); @@ -65,6 +62,8 @@ public class NettyHandler extends ChannelInboundHandlerAdapter { private long chunkSize; private boolean http2; + private NettyContext context; + public NettyHandler( ScheduledExecutorService scheduler, Router router, @@ -82,16 +81,12 @@ public NettyHandler( this.http2 = http2; } - public boolean isHttp2() { - return http2; - } - @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (isHttpRequest(msg)) { var req = (HttpRequest) msg; - var context = getOrCreateContext(ctx); - context.init(ctx, req, router, pathOnly(req.uri()), bufferSize, http2); + + context = new NettyContext(ctx, req, router, pathOnly(req.uri()), bufferSize, http2); if (defaultHeaders) { context.setHeaders.set(HttpHeaderNames.DATE, date(router.getLog(), scheduler)); @@ -114,7 +109,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { } else if (isHttpContent(msg)) { var chunk = (HttpContent) msg; try { - var context = getContextOrNull(ctx); // when decoder == null, chunk is always a LastHttpContent.EMPTY, ignore it if (context.decoder != null) { chunkSize += chunk.content().readableBytes(); @@ -136,7 +130,6 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { release(chunk); } } else if (msg instanceof WebSocketFrame) { - var context = getContextOrNull(ctx); if (context.webSocket != null) { context.webSocket.handleFrame((WebSocketFrame) msg); } @@ -149,23 +142,8 @@ private void release(HttpContent ref) { } } - private NettyContext getOrCreateContext(ChannelHandlerContext ctx) { - Attribute attr = ctx.channel().attr(CONTEXT); - var context = attr.get(); - if (context == null) { - context = new NettyContext(); - attr.set(context); - } - return context; - } - - private NettyContext getContextOrNull(ChannelHandlerContext ctx) { - return ctx.channel().attr(CONTEXT).get(); - } - @Override public void channelReadComplete(ChannelHandlerContext ctx) { - var context = getContextOrNull(ctx); if (context != null) { context.flush(); } @@ -184,7 +162,6 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { try { - var context = getContextOrNull(ctx); Logger log = router.getLog(); if (Server.connectionLost(cause)) { if (log.isDebugEnabled()) { diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java index bbf2a2aee3..aefc17412f 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyPipeline.java @@ -24,27 +24,29 @@ public class NettyPipeline extends ChannelInitializer { private static final String H2_HANDSHAKE = "h2-handshake"; - private Integer compressionLevel; private int bufferSize; private long maxRequestSize; private SslContext sslContext; private boolean is100ContinueExpected; - private NettyHandler handler; + private boolean http2; + private Supplier handlerFactory; public NettyPipeline( - NettyHandler handler, + Supplier handlerFactory, SslContext sslContext, Integer compressionLevel, int bufferSize, long maxRequestSize, + boolean http2, boolean is100ContinueExpected) { this.sslContext = sslContext; this.compressionLevel = compressionLevel; this.bufferSize = bufferSize; this.maxRequestSize = maxRequestSize; this.is100ContinueExpected = is100ContinueExpected; - this.handler = handler; + this.http2 = http2; + this.handlerFactory = handlerFactory; } @Override @@ -53,7 +55,7 @@ public void initChannel(SocketChannel ch) { if (sslContext != null) { p.addLast("ssl", sslContext.newHandler(ch.alloc())); } - if (handler.isHttp2()) { + if (http2) { Http2Settings settings = new Http2Settings(maxRequestSize, sslContext != null); Http2Extension extension = new Http2Extension( @@ -67,7 +69,7 @@ public void initChannel(SocketChannel ch) { setupCompression(p); - p.addLast("handler", handler); + p.addLast("handler", handlerFactory.get()); } else { http11(p); } @@ -113,7 +115,7 @@ private void http11(ChannelPipeline p) { p.addLast("codec", codec); setupExpectContinue(p); setupCompression(p); - p.addLast("handler", handler); + p.addLast("handler", handlerFactory.get()); } HttpServerCodec createServerCodec() { diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java index f414b4dbc5..2137b3b33e 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java @@ -191,13 +191,13 @@ private ClientAuth toClientAuth(SslOptions.ClientAuth clientAuth) { private NettyPipeline newPipeline(HttpDataFactory factory, SslContext sslContext, boolean http2) { var executor = acceptorloop.next(); var router = applications.get(0); - var handler = createHandler(executor, router, options, factory, http2); return new NettyPipeline( - handler, + () -> createHandler(executor, router, options, factory, http2), sslContext, options.getCompressionLevel(), options.getBufferSize(), options.getMaxRequestSize(), + http2, options.isExpectContinue() == Boolean.TRUE); } diff --git a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java index e7c7d1892e..35e6feda60 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/internal/undertow/UndertowContext.java @@ -23,7 +23,6 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.security.cert.Certificate; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -242,7 +241,7 @@ public List getClientCertificates() { throw SneakyThrows.propagate(x); } } - return new ArrayList(); + return Collections.emptyList(); } @NonNull @Override diff --git a/tests/src/test/java/io/jooby/NettySharedContextTest.java b/tests/src/test/java/io/jooby/NettySharedContextTest.java deleted file mode 100644 index 49596da79f..0000000000 --- a/tests/src/test/java/io/jooby/NettySharedContextTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby; - -import static io.jooby.ExecutionMode.DEFAULT; -import static io.jooby.ExecutionMode.EVENT_LOOP; -import static io.jooby.SneakyThrows.throwingSupplier; -import static java.lang.Integer.toHexString; -import static java.lang.System.identityHashCode; -import static java.lang.Thread.currentThread; -import static java.util.concurrent.CompletableFuture.supplyAsync; -import static org.apache.commons.lang3.Validate.isTrue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.ArrayList; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; - -import io.jooby.junit.ServerTest; -import io.jooby.junit.ServerTestRunner; -import io.jooby.netty.NettyServer; - -public class NettySharedContextTest { - - @ServerTest( - server = NettyServer.class, - executionMode = {EVENT_LOOP, DEFAULT}) - public void shouldCheckNettySharedContext(ServerTestRunner runner) { - var concurrentRequests = 100; - var numberOfRequests = 5000; - var contexts = new ConcurrentHashMap(); - var serverThreads = new ConcurrentHashMap(); - var clientThreads = new ConcurrentHashMap(); - var totalRequests = new AtomicInteger(); - var totalErrors = new AtomicInteger(); - var totalResponses = new AtomicInteger(); - runner - .define( - app -> { - app.use( - next -> - ctx -> { - // Make sure attributes is empty - isTrue(ctx.getAttributes().isEmpty()); - // Now set something: - ctx.setAttribute("foo", "bar"); - return next.apply(ctx); - }); - app.get( - "/rnd", - ctx -> { - var id = ctx.query("id").value(); - contexts - .computeIfAbsent( - toHexString(identityHashCode(ctx)), k -> new AtomicInteger()) - .incrementAndGet(); - serverThreads - .computeIfAbsent(currentThread().getName(), k -> new AtomicInteger()) - .incrementAndGet(); - totalRequests.incrementAndGet(); - return id; - }); - }) - .ready( - conf -> { - var executor = Executors.newFixedThreadPool(concurrentRequests); - var start = System.currentTimeMillis(); - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { - var futures = new ArrayList>(); - for (var r = 0; r < numberOfRequests; r++) { - var future = - supplyAsync( - throwingSupplier( - () -> { - var id = UUID.randomUUID().toString(); - HttpGet request = - new HttpGet( - "http://localhost:" + conf.getPort() + "/rnd?id=" + id); - clientThreads - .computeIfAbsent( - currentThread().getName(), k -> new AtomicInteger()) - .incrementAndGet(); - try (var rsp = client.execute(request)) { - var value = EntityUtils.toString(rsp.getEntity()); - assertEquals(id, value); - return value; - } - }), - executor) - .whenComplete( - (rsp, cause) -> { - totalResponses.incrementAndGet(); - if (cause != null) { - totalErrors.incrementAndGet(); - cause.printStackTrace(); - } - }); - futures.add(future); - } - futures.stream().map(CompletableFuture::join).collect(Collectors.toList()); - assertEquals(concurrentRequests, clientThreads.size()); - assertEquals(numberOfRequests, totalRequests.intValue()); - assertEquals(0, totalErrors.intValue()); - assertEquals(numberOfRequests, totalResponses.intValue()); - assertTrue(contexts.size() <= serverThreads.size()); - } - executor.shutdown(); - }); - } -}