From d3b1cad2c3828bf0b6a677747defbe2306d85b43 Mon Sep 17 00:00:00 2001 From: Yury Gribkov Date: Thu, 27 Aug 2026 15:26:56 -0700 Subject: [PATCH] fix(netty-4.1): Treat Netty native client aborts as non-error responses (#12295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(netty-4.1): Reproduce Netty's `writevAddresses(..) failed with error(-32): Broken pipe` resulting in an error span with 500 status fix(netty-4.1): Stop treating cancelled Netty responses as errors Move client abort detection into the Netty-specific decorator Merge branch 'master' into ygree/treat-netty-broken-pipe-as-no-err Fix Netty pipelining test trace isolation Assert and drain expected traces in AppSec pipelining scenarios so spans are consumed by the tests that create them. The failure appeared now because this branch adds native Netty latest-dep dependencies/tests, changing the `latestDepTest` classpath and execution order enough for previously unasserted AppSec traces to be written after the next test’s writer reset. Add method-aware server trace matching for the HEAD response case. Removed the two stale `DDSpan.addThrowable` null-message hardening tests from DDSpanTest.java Make Netty native client-abort matching flexible but scoped Replace brittle exact-message matching with a scoped matcher for Netty NativeIoException writev failures ending in known client-abort errno messages. This covers another observed Linux ECONNRESET format: writevAddresses(..) failed with error(-104): Connection reset by peer while still limiting the non-error treatment to native Netty write-side client aborts, such as Broken pipe and Connection reset by peer, instead of broadly suppressing unrelated native I/O failures. Merge branch 'master' into ygree/treat-netty-broken-pipe-as-no-err Co-authored-by: yury.gribkov (cherry picked from commit 042bde810585065eaef8c16333fdab50127c15d9) --- .../netty/netty-4.1/build.gradle | 11 + .../HttpServerResponseTracingHandler.java | 1 - .../server/NettyHttpServerDecorator.java | 41 +++ .../server/NettyChunkedResponseSpanTest.java | 2 +- .../server/NettyHttp11PipeliningTest.java | 23 +- .../NettyNativeClientAbortSpanTest.java | 241 ++++++++++++++++++ 6 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyNativeClientAbortSpanTest.java diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/build.gradle b/dd-java-agent/instrumentation/netty/netty-4.1/build.gradle index d894c6cebbd..a7572556795 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/build.gradle +++ b/dd-java-agent/instrumentation/netty/netty-4.1/build.gradle @@ -53,6 +53,17 @@ dependencies { latestDepTestImplementation group: 'io.netty', name: 'netty-codec-http', version: '4.+' latestDepTestImplementation group: 'io.netty', name: 'netty-codec-http2', version: '4.+' latestDepTestImplementation group: 'io.netty', name: 'netty-codec-socks', version: '4.+' + // Next native dependencies are required by NettyNativeClientAbortSpanTest + latestDepTestImplementation group: 'io.netty', name: 'netty-transport-classes-epoll', version: '4.+' + latestDepTestImplementation group: 'io.netty', name: 'netty-transport-classes-kqueue', version: '4.+' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.+', classifier: 'linux-aarch_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-epoll', version: '4.+', classifier: 'linux-x86_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.+', classifier: 'osx-aarch_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-kqueue', version: '4.+', classifier: 'osx-x86_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-unix-common', version: '4.+', classifier: 'linux-aarch_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-unix-common', version: '4.+', classifier: 'linux-x86_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-unix-common', version: '4.+', classifier: 'osx-aarch_64' + latestDepTestRuntimeOnly group: 'io.netty', name: 'netty-transport-native-unix-common', version: '4.+', classifier: 'osx-x86_64' latestDepTestImplementation group: 'org.asynchttpclient', name: 'async-http-client', version: '2.+' } diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java index 9f330301f28..10aa1368008 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerResponseTracingHandler.java @@ -126,7 +126,6 @@ private static void finishSpan( final Future future) { if (!future.isSuccess()) { DECORATE.onError(span, future.cause()); - span.setHttpStatusCode(500); } finishSpan(serverContext, storedContext, span); } diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/NettyHttpServerDecorator.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/NettyHttpServerDecorator.java index 81d94995d05..5794c75f9bf 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/NettyHttpServerDecorator.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/NettyHttpServerDecorator.java @@ -1,9 +1,11 @@ package datadog.trace.instrumentation.netty41.server; import datadog.appsec.api.blocking.BlockingContentType; +import datadog.trace.api.DDTags; import datadog.trace.api.gateway.BlockResponseFunction; import datadog.trace.api.internal.TraceSegment; import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.ContextVisitors; import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter; import datadog.trace.bootstrap.instrumentation.api.URIDataAdapterBase; @@ -34,6 +36,14 @@ public class NettyHttpServerDecorator public static final NettyHttpServerDecorator DECORATE = new NettyHttpServerDecorator(); private static final CharSequence NETTY_REQUEST = UTF8BytesString.create(DECORATE.operationName()); + private static final String NETTY_NATIVE_IO_EXCEPTION_CLASS_NAME = + "io.netty.channel.unix.Errors$NativeIoException"; + private static final String NETTY_NATIVE_WRITEV_ADDRESSES_FAILURE_PREFIX = + "writevAddresses(..) failed"; + private static final String NETTY_NATIVE_WRITEV_SYSCALL_FAILURE_PREFIX = + "syscall:writev(..) failed"; + private static final String BROKEN_PIPE_MESSAGE_SUFFIX = ": Broken pipe"; + private static final String CONNECTION_RESET_MESSAGE_SUFFIX = ": Connection reset by peer"; @Override protected String[] instrumentationNames() { @@ -108,6 +118,37 @@ protected boolean isAppSecOnResponseSeparate() { return true; } + @Override + protected void doOnError(final AgentSpan span, final Throwable throwable, byte errorPriority) { + if (isNettyNativeClientAbort(throwable)) { + span.setTag(DDTags.ERROR_MSG, safeMessage(throwable)); + span.setTag(DDTags.ERROR_TYPE, throwable.getClass().getName()); + return; + } + super.doOnError(span, throwable, errorPriority); + } + + private static boolean isNettyNativeClientAbort(final Throwable throwable) { + if (throwable == null + || !NETTY_NATIVE_IO_EXCEPTION_CLASS_NAME.equals(throwable.getClass().getName())) { + return false; + } + final String message = safeMessage(throwable); + return message != null + && (message.startsWith(NETTY_NATIVE_WRITEV_ADDRESSES_FAILURE_PREFIX) + || message.startsWith(NETTY_NATIVE_WRITEV_SYSCALL_FAILURE_PREFIX)) + && (message.endsWith(BROKEN_PIPE_MESSAGE_SUFFIX) + || message.endsWith(CONNECTION_RESET_MESSAGE_SUFFIX)); + } + + private static String safeMessage(final Throwable throwable) { + try { + return throwable.getMessage(); + } catch (Throwable ignored) { + return null; + } + } + @Override protected BlockResponseFunction createBlockResponseFunction( HttpRequest httpRequest, Channel channel) { diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyChunkedResponseSpanTest.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyChunkedResponseSpanTest.java index a68de06d005..1fae3626f0e 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyChunkedResponseSpanTest.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyChunkedResponseSpanTest.java @@ -376,7 +376,7 @@ public Flow apply(RequestContext context, IGSpanInfo span) { assertThrows(IOException.class, channel::checkException); assertTrue(requestSpanActive.get(), "request span was not active during requestEnded"); - assertEquals(500, span.getTag(Tags.HTTP_STATUS)); + assertEquals(200, span.getTag(Tags.HTTP_STATUS)); assertTraces(trace(span().root().operationName("netty.request").error())); } finally { holdingHandler.failWrite(writeFailure); diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyHttp11PipeliningTest.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyHttp11PipeliningTest.java index a611fbb8cb8..e10d032d965 100644 --- a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyHttp11PipeliningTest.java +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyHttp11PipeliningTest.java @@ -186,6 +186,8 @@ void requestBlockOnLaterPipelinedRequestDoesNotOvertakeEarlierResponse() throws readHeaders(socket.getInputStream()).startsWith("HTTP/1.1 403 "), "second response should be the deferred blocking response"); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH), serverTrace(SECOND_PATH)); } @Test @@ -221,6 +223,8 @@ void additionalPipelinedRequestsBehindDeferredBlockAreIgnored() throws Exception handler.inboundException, "additional pipelined requests should be swallowed by the existing blocking handler"); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH), serverTrace(SECOND_PATH)); } @Test @@ -248,6 +252,8 @@ void requestBlockOnLaterPipelinedRequestWaitsForEarlierChunkedResponseCompletion readHeaders(socket.getInputStream()).startsWith("HTTP/1.1 403 "), "second response should be the deferred blocking response"); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH), serverTrace(SECOND_PATH)); } @Test @@ -282,6 +288,8 @@ void requestBlockOnLaterPipelinedRequestFollowsEarlierHeaderOnlyResponse() throw readHeaders(socket.getInputStream()).startsWith("HTTP/1.1 403 "), "second response should be the deferred blocking response"); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH), serverTrace(SECOND_PATH)); } @Test @@ -310,6 +318,9 @@ void requestBlockOnLaterPipelinedRequestFollowsEarlierHeadResponse() throws Exce readHeaders(socket.getInputStream()).startsWith("HTTP/1.1 403 "), "second response should be the deferred blocking response"); } + + assertTraces( + SORT_BY_START_TIME, serverTrace("HEAD", FIRST_PATH), serverTrace("GET", SECOND_PATH)); } @Test @@ -329,6 +340,8 @@ void lastContentAfterInterimResponseDoesNotCompleteServerSpan() throws Exception "first response should be the interim response"); assertEquals("response " + FIRST_PATH, readHttpResponseBody(socket.getInputStream())); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH)); } @Test @@ -359,6 +372,8 @@ void requestBlockOnLaterPipelinedRequestWaitsForEarlierEarlyHintsResponseComplet readHeaders(socket.getInputStream()).startsWith("HTTP/1.1 403 "), "second response should be the deferred blocking response"); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH), serverTrace(SECOND_PATH)); } @Test @@ -387,6 +402,8 @@ void blockResponseFunctionOnLaterPipelinedRequestDoesNotOvertakeEarlierResponse( readHeaders(socket.getInputStream()).startsWith("HTTP/1.1 403 "), "second response should be the deferred blocking response"); } + + assertTraces(SORT_BY_START_TIME, serverTrace(FIRST_PATH), serverTrace(SECOND_PATH)); } private static String pipelinedRequests() { @@ -453,11 +470,15 @@ public Flow get() { } private static TraceMatcher serverTrace(String path) { + return serverTrace("GET", path); + } + + private static TraceMatcher serverTrace(String method, String path) { return trace( span() .root() .operationName(Pattern.compile("netty\\.request")) - .resourceName(Pattern.compile("GET " + Pattern.quote(path))) + .resourceName(Pattern.compile(Pattern.quote(method + " " + path))) .type("web")); } diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyNativeClientAbortSpanTest.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyNativeClientAbortSpanTest.java new file mode 100644 index 00000000000..28ed7e1d926 --- /dev/null +++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/test/java/datadog/trace/instrumentation/netty41/server/NettyNativeClientAbortSpanTest.java @@ -0,0 +1,241 @@ +package datadog.trace.instrumentation.netty41.server; + +import static io.netty.handler.codec.http.HttpHeaderNames.TRANSFER_ENCODING; +import static io.netty.handler.codec.http.HttpHeaderValues.CHUNKED; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.api.DDTags; +import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.core.DDSpan; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ServerChannel; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.LastHttpContent; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Locale; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; + +public class NettyNativeClientAbortSpanTest extends AbstractInstrumentationTest { + + private static final String PATH = "/native-broken-pipe"; + private static final String NATIVE_IO_EXCEPTION = + "io.netty.channel.unix.Errors$NativeIoException"; + private static final String WRITEV_ADDRESSES_FAILURE_PREFIX = "writevAddresses(..) failed"; + private static final String WRITEV_SYSCALL_FAILURE_PREFIX = "syscall:writev(..) failed"; + private static final String BROKEN_PIPE_MESSAGE_SUFFIX = ": Broken pipe"; + private static final String CONNECTION_RESET_MESSAGE_SUFFIX = ": Connection reset by peer"; + + @Test + void nativeBrokenPipeFromCancelledResponseDoesNotMarkServerSpanError() throws Exception { + NativeTransport transport = NativeTransport.current(); + assumeTrue(transport.available, transport.unavailableReason); + + NativeBrokenPipeHandler handler = new NativeBrokenPipeHandler(); + EventLoopGroup boss = transport.newEventLoopGroup(1); + EventLoopGroup worker = transport.newEventLoopGroup(1); + Channel server = null; + try { + server = + new ServerBootstrap() + .group(boss, worker) + .channel(transport.serverSocketChannelClass) + .childOption( + ChannelOption.WRITE_BUFFER_WATER_MARK, + new WriteBufferWaterMark(32 * 1024, 64 * 1024)) + .childHandler( + new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline().addLast(new HttpServerCodec()); + ch.pipeline().addLast(handler); + } + }) + .bind("127.0.0.1", 0) + .sync() + .channel(); + + int port = ((InetSocketAddress) server.localAddress()).getPort(); + try (Socket socket = new Socket("127.0.0.1", port)) { + socket.setReceiveBufferSize(1024); + socket.getOutputStream().write(request().getBytes(US_ASCII)); + socket.getOutputStream().flush(); + + InputStream response = socket.getInputStream(); + assertTrue(response.read() >= 0, "server did not write any response bytes"); + } + + Throwable failure = handler.awaitFailure(); + assertEquals(NATIVE_IO_EXCEPTION, failure.getClass().getName()); + assertTrue( + isExpectedClientAbortMessage(failure.getMessage()), + () -> "unexpected native write failure message: " + failure.getMessage()); + + writer.waitForTraces(1); + DDSpan span = writer.firstTrace().get(0); + assertFalse(span.isError(), "client abort should result in a non-error span"); + assertEquals(200, span.getTag(Tags.HTTP_STATUS)); + assertEquals(NATIVE_IO_EXCEPTION, span.getTag(DDTags.ERROR_TYPE)); + assertEquals(failure.getMessage(), span.getTag(DDTags.ERROR_MSG)); + assertNull(span.getTag(DDTags.ERROR_STACK)); + } finally { + if (server != null) { + server.close().syncUninterruptibly(); + } + boss.shutdownGracefully().syncUninterruptibly(); + worker.shutdownGracefully().syncUninterruptibly(); + } + } + + private static String request() { + return "GET " + PATH + " HTTP/1.1\r\nHost: localhost\r\n\r\n"; + } + + private static boolean isExpectedClientAbortMessage(String message) { + return message != null + && (message.startsWith(WRITEV_ADDRESSES_FAILURE_PREFIX) + || message.startsWith(WRITEV_SYSCALL_FAILURE_PREFIX)) + && (message.endsWith(BROKEN_PIPE_MESSAGE_SUFFIX) + || message.endsWith(CONNECTION_RESET_MESSAGE_SUFFIX)); + } + + @ChannelHandler.Sharable + private static final class NativeBrokenPipeHandler + extends SimpleChannelInboundHandler { + private final AtomicBoolean failureRecorded = new AtomicBoolean(); + private final BlockingQueue failures = new LinkedBlockingQueue<>(); + + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) { + if (!PATH.equals(request.uri())) { + ctx.close(); + return; + } + + DefaultHttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); + response.headers().set(TRANSFER_ENCODING, CHUNKED); + ctx.write(response); + ctx.writeAndFlush(new DefaultHttpContent(ctx.alloc().buffer(1).writeByte(1))) + .addListener(future -> writeCancelledResponseTail(ctx)); + } + + private void writeCancelledResponseTail(ChannelHandlerContext ctx) { + for (int i = 0; i < 512; i++) { + ByteBuf content = ctx.alloc().directBuffer(16 * 1024); + content.writeZero(content.writableBytes()); + ctx.write(new DefaultHttpContent(content)); + } + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + .addListener( + future -> { + if (future.isSuccess()) { + failures.offer( + new AssertionError("cancelled response tail write unexpectedly succeeded")); + } else if (failureRecorded.compareAndSet(false, true)) { + failures.offer(future.cause()); + } + }); + } + + private Throwable awaitFailure() throws InterruptedException, TimeoutException { + Throwable failure = failures.poll(5, SECONDS); + if (failure == null) { + throw new TimeoutException("server did not observe a failed native response write"); + } + return failure; + } + } + + private static final class NativeTransport { + private final boolean available; + private final String unavailableReason; + private final Constructor eventLoopGroupConstructor; + private final Class serverSocketChannelClass; + + private NativeTransport(String unavailableReason) { + this.available = false; + this.unavailableReason = unavailableReason; + this.eventLoopGroupConstructor = null; + this.serverSocketChannelClass = null; + } + + private NativeTransport( + Constructor eventLoopGroupConstructor, + Class serverSocketChannelClass) { + this.available = true; + this.unavailableReason = null; + this.eventLoopGroupConstructor = eventLoopGroupConstructor; + this.serverSocketChannelClass = serverSocketChannelClass; + } + + private static NativeTransport current() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + if (osName.contains("mac")) { + return load( + "kqueue", + "io.netty.channel.kqueue.KQueue", + "io.netty.channel.kqueue.KQueueEventLoopGroup", + "io.netty.channel.kqueue.KQueueServerSocketChannel"); + } else if (osName.contains("linux")) { + return load( + "epoll", + "io.netty.channel.epoll.Epoll", + "io.netty.channel.epoll.EpollEventLoopGroup", + "io.netty.channel.epoll.EpollServerSocketChannel"); + } + return new NativeTransport("Netty native transport is not supported on " + osName); + } + + private static NativeTransport load( + String name, String availabilityClass, String eventLoopGroupClass, String channelClass) { + try { + Class availability = Class.forName(availabilityClass); + Method isAvailable = availability.getMethod("isAvailable"); + if (!Boolean.TRUE.equals(isAvailable.invoke(null))) { + return new NativeTransport(name + " is not available"); + } + return new NativeTransport( + Class.forName(eventLoopGroupClass) + .asSubclass(EventLoopGroup.class) + .getConstructor(int.class), + Class.forName(channelClass).asSubclass(ServerChannel.class)); + } catch (Throwable error) { + return new NativeTransport(name + " could not be loaded: " + error); + } + } + + private EventLoopGroup newEventLoopGroup(int threads) throws Exception { + assertNotNull(eventLoopGroupConstructor); + return eventLoopGroupConstructor.newInstance(threads); + } + } +}