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

Default to Epoll if available #3247

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions webserver/webserver/pom.xml
Expand Up @@ -159,6 +159,11 @@
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>io.opentracing</groupId>
<artifactId>opentracing-mock</artifactId>
Expand Down
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2017, 2021 Oracle and/or its affiliates.
*
* 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
*
* 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 io.helidon.webserver;

import java.util.Optional;

import io.netty.channel.ChannelFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ServerChannel;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
* Base {@link Transport} for Netty.
*/
public abstract class NettyTransport implements Transport {

@Override
public boolean isAvailableFor(WebServer webserver) {
return webserver instanceof NettyWebServer;
}

@Override
@SuppressWarnings("unchecked")
public <T> Optional<T> createTransportArtifact(Class<T> artifactType,
String artifactName,
ServerConfiguration config) {
if (EventLoopGroup.class.isAssignableFrom(artifactType)) {
switch (artifactName) {
case "bossGroup":
return Optional.of((T) eventLoopGroup(config.sockets().size()));
case "workerGroup":
return Optional.of((T) eventLoopGroup(Math.max(0, config.workersCount())));
default:
}
} else if (ChannelFactory.class.isAssignableFrom(artifactType)) {
switch (artifactName) {
case "serverChannelFactory":
return Optional.of((T) channelFactory());
default:
}
}
return Optional.empty();
}

/**
* Create a new instance for the given number of threads.
*
* @param nThreads the number of threads.
* @return event loop group instance.
*/
protected abstract EventLoopGroup eventLoopGroup(int nThreads);

/**
* @return channel factory instance.
*/
protected abstract ChannelFactory<? extends ServerChannel> channelFactory();

/**
* Transport that leverages NIO Selector.
*/
static class NioTransport extends NettyTransport {

@Override
protected EventLoopGroup eventLoopGroup(int nThreads) {
return new NioEventLoopGroup(nThreads);
}

@Override
protected ChannelFactory<? extends ServerChannel> channelFactory() {
return NioServerSocketChannel::new;
}
}

/**
* Transport that leverages Netty Epoll which should reap better performance than NIO.
*/
static class EpollTransport extends NettyTransport {

@Override
public boolean isAvailableFor(WebServer webserver) {
return super.isAvailableFor(webserver) && Epoll.isAvailable();
}

@Override
protected EventLoopGroup eventLoopGroup(int nThreads) {
return new EpollEventLoopGroup(nThreads);
}

@Override
protected ChannelFactory<? extends ServerChannel> channelFactory() {
return EpollServerSocketChannel::new;
}
}
}
Expand Up @@ -47,16 +47,15 @@
import io.helidon.common.reactive.Single;
import io.helidon.media.common.MessageBodyReaderContext;
import io.helidon.media.common.MessageBodyWriterContext;

import io.helidon.webserver.NettyTransport.EpollTransport;
import io.helidon.webserver.NettyTransport.NioTransport;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ServerChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
Expand Down Expand Up @@ -252,15 +251,16 @@ public synchronized Single<WebServer> start() {
}

try {
bootstrap.bind(bindAddress, port).addListener(channelFuture -> {
SocketAddress address = new InetSocketAddress(bindAddress, port);
bootstrap.bind(address).addListener(channelFuture -> {
if (!channelFuture.isSuccess()) {
LOGGER.info(() -> "Channel '" + name + "' startup failed with message '"
+ channelFuture.cause().getMessage() + "'.");
Throwable cause = channelFuture.cause();

String message = "Channel startup failed: " + name;
if (cause instanceof BindException) {
message = message + ", failed to listen on " + configuration.bindAddress() + ":" + port;
message = message + ", failed to listen on " + address;
}

channelsUpFuture.completeExceptionally(new IllegalStateException(message,
Expand Down Expand Up @@ -457,24 +457,22 @@ public void updateTls(WebServerTls tls, String socketName) {
}

private Transport acquireTransport() {
Transport transport = configuration.transport().orElse(new NioTransport());
// (Note that an NioTransport's isAvailableFor() method will
// always return true when passed this.)
return transport.isAvailableFor(this) ? transport : new NioTransport();
return configuration.transport()
.or(() -> Optional.of(new EpollTransport()))
.filter(t -> t.isAvailableFor(this))
.orElseGet(NioTransport::new);
}

private Transport transport() {
Transport transport() {
return transport;
}

@SuppressWarnings("unchecked")
private EventLoopGroup bossGroup() {
return transport()
.createTransportArtifact(EventLoopGroup.class, "bossGroup", configuration)
.orElseThrow(() -> noSuchTransportArtifact("bossGroup"));
}

@SuppressWarnings("unchecked")
private EventLoopGroup workerGroup() {
return transport()
.createTransportArtifact(EventLoopGroup.class, "workerGroup", configuration)
Expand All @@ -495,46 +493,6 @@ private NoSuchElementException noSuchTransportArtifact(String name) {
+ name + "\"");
}

private static final class NioTransport implements Transport {

private NioTransport() {
super();
}

@Override
public boolean isAvailableFor(WebServer webserver) {
return webserver instanceof NettyWebServer;
}

@Override
@SuppressWarnings("unchecked")
public <T> Optional<T> createTransportArtifact(Class<T> artifactType,
String artifactName,
ServerConfiguration config) {
if (EventLoopGroup.class.isAssignableFrom(artifactType)) {
switch (artifactName) {
case "bossGroup":
return Optional.of((T) new NioEventLoopGroup(config.sockets().size()));
case "workerGroup":
return Optional.of((T) new NioEventLoopGroup(Math.max(0, config.workersCount())));
default:
return Optional.empty();
}
} else if (ChannelFactory.class.isAssignableFrom(artifactType)) {
switch (artifactName) {
case "serverChannelFactory":
ChannelFactory<? extends ServerChannel> cf = NioServerSocketChannel::new;
return Optional.of((T) cf);
default:
return Optional.empty();
}
} else {
return Optional.empty();
}
}

}

// this class is only used to create a log handler in NettyLogHandler, to distinguish from webclient
private static final class NettyLog {
}
Expand Down
Expand Up @@ -353,7 +353,6 @@ final class Builder implements io.helidon.common.Builder<WebServer>,
// for backward compatibility
@SuppressWarnings("deprecation")
private ServerConfiguration explicitConfig;
private Transport transport;
private MessageBodyReaderContext readerContext;
private MessageBodyWriterContext writerContext;

Expand Down
1 change: 1 addition & 0 deletions webserver/webserver/src/main/java/module-info.java
Expand Up @@ -39,6 +39,7 @@
requires io.netty.common;
requires io.netty.buffer;
requires io.netty.codec.http2;
requires io.netty.transport.epoll;

exports io.helidon.webserver;
}
Expand Up @@ -16,7 +16,7 @@

package io.helidon.webserver;

import java.util.Arrays;;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
Expand Down
Expand Up @@ -16,6 +16,17 @@

package io.helidon.webserver;

import static io.helidon.config.testing.OptionalMatcher.present;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.core.AllOf.allOf;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.fail;

import java.net.InetAddress;
import java.util.Collections;
import java.util.concurrent.CompletableFuture;
Expand All @@ -30,23 +41,15 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import io.helidon.common.http.DataChunk;
import io.helidon.common.http.Http;
import io.helidon.common.reactive.Multi;

import org.hamcrest.collection.IsCollectionWithSize;
import org.hamcrest.core.Is;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;

import static io.helidon.config.testing.OptionalMatcher.present;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.core.AllOf.allOf;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.fail;
import io.helidon.common.http.DataChunk;
import io.helidon.common.http.Http;
import io.helidon.common.reactive.Multi;
import io.helidon.webserver.NettyTransport.EpollTransport;

/**
* The NettyWebServerTest.
Expand Down Expand Up @@ -268,4 +271,13 @@ public void additionalCoupledPairedRoutingsDoWork() {

assertThat(webServer.configuration().namedSocket("matched"), present());
}

@Test
@EnabledIf("io.netty.channel.epoll.Epoll#isAvailable")
void epoll() {
var webServer = (NettyWebServer) WebServer.create(
routing((bareRequest, bareResponse) -> { }));

assertThat(webServer.transport(), instanceOf(EpollTransport.class));
}
}