Repository: netty/netty
Pull request #15150: Kqueue: Fix registration failure when fd is reused
Author: login: normanmaurer
Created: 2025-05-12T08:06:35Z | Merged: 2025-05-14T06:08:07Z

Motivation:

We need to ensure the id we use is unique so we don't end up with problems when the FD is reused.

Modifications:

- Generate a unqiue id during registration
- Store the id as udata and use it to lookup the handle
    
Result:
    
Fixes https://github.com/netty/netty/issues/15041



Diff:
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectionAttemptTest.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectionAttemptTest.java
index 106f704ea17..f5025cc3ce6 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectionAttemptTest.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketConnectionAttemptTest.java
@@ -39,6 +39,7 @@
 
 import static io.netty.testsuite.transport.socket.SocketTestPermutation.BAD_HOST;
 import static io.netty.testsuite.transport.socket.SocketTestPermutation.BAD_PORT;
+import static io.netty.testsuite.transport.socket.SocketTestPermutation.UNASSIGNED_PORT;
 import static org.hamcrest.CoreMatchers.*;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -47,9 +48,6 @@
 
 public class SocketConnectionAttemptTest extends AbstractClientSocketTest {
 
-    // See /etc/services
-    private static final int UNASSIGNED_PORT = 4;
-
     @Test
     @Timeout(value = 30000, unit = TimeUnit.MILLISECONDS)
     public void testConnectTimeout(TestInfo testInfo) throws Throwable {
diff --git a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java
index dfcd036b15f..ae014e08b27 100644
--- a/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java
+++ b/testsuite/src/main/java/io/netty/testsuite/transport/socket/SocketTestPermutation.java
@@ -48,6 +48,9 @@ public class SocketTestPermutation {
     static final String BAD_HOST = SystemPropertyUtil.get("io.netty.testsuite.badHost", "198.51.100.254");
     static final int BAD_PORT = SystemPropertyUtil.getInt("io.netty.testsuite.badPort", 65535);
 
+    // See /etc/services
+    public static final int UNASSIGNED_PORT = 4;
+
     static {
         InternalLogger logger = InternalLoggerFactory.getInstance(SocketConnectionAttemptTest.class);
         logger.debug("-Dio.netty.testsuite.badHost: {}", BAD_HOST);
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventArray.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventArray.java
index 763ae419091..c279bf333c4 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventArray.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueEventArray.java
@@ -23,6 +23,7 @@
 /**
  * Represents an array of kevent structures, backed by offheap memory.
  *
+ * <pre>
  * struct kevent {
  *  uintptr_t ident;
  *  short     keventFilter;
@@ -31,6 +32,7 @@
  *  intptr_t  data;
  *  void      *udata;
  * };
+ * </pre>
  */
 final class KQueueEventArray {
     private static final int KQUEUE_EVENT_SIZE = Native.sizeofKEvent();
@@ -39,6 +41,7 @@ final class KQueueEventArray {
     private static final int KQUEUE_FFLAGS_OFFSET = Native.offsetofKEventFFlags();
     private static final int KQUEUE_FLAGS_OFFSET = Native.offsetofKEventFlags();
     private static final int KQUEUE_DATA_OFFSET = Native.offsetofKeventData();
+    private static final int KQUEUE_UDATA_OFFSET = Native.offsetofKeventUdata();
 
     private ByteBuffer memory;
     private long memoryAddress;
@@ -77,9 +80,9 @@ void clear() {
         size = 0;
     }
 
-    void evSet(int ident, short filter, short flags, int fflags) {
+    void evSet(int ident, short filter, short flags, int fflags, long data, long udata) {
         reallocIfNeeded();
-        evSet(getKEventOffset(size++) + memoryAddress, ident, filter, flags, fflags);
+        evSet(getKEventOffset(size++) + memoryAddress, ident, filter, flags, fflags, data, udata);
     }
 
     private void reallocIfNeeded() {
@@ -159,15 +162,24 @@ int ident(int index) {
     }
 
     long data(int index) {
+        return getLong(index, KQUEUE_DATA_OFFSET);
+    }
+
+    long udata(int index) {
+        return getLong(index, KQUEUE_UDATA_OFFSET);
+    }
+
+    private long getLong(int index, int offset) {
         if (PlatformDependent.hasUnsafe()) {
-            return PlatformDependent.getLong(getKEventOffsetAddress(index) + KQUEUE_DATA_OFFSET);
+            return PlatformDependent.getLong(getKEventOffsetAddress(index) + offset);
         }
-        return memory.getLong(getKEventOffset(index) + KQUEUE_DATA_OFFSET);
+        return memory.getLong(getKEventOffset(index) + offset);
     }
 
     private static int calculateBufferCapacity(int capacity) {
         return capacity * KQUEUE_EVENT_SIZE;
     }
 
-    private static native void evSet(long keventAddress, int ident, short filter, short flags, int fflags);
+    private static native void evSet(
+            long keventAddress, int ident, short filter, short flags, int fflags, long data, long udata);
 }
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoEvent.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoEvent.java
index 8d2f3dd39bc..246a08d6501 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoEvent.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoEvent.java
@@ -26,6 +26,7 @@ public final class KQueueIoEvent implements IoEvent {
     private short flags;
     private int fflags;
     private long data;
+    private long udata;
 
     /**
      * Creates a new {@link KQueueIoEvent}.
@@ -35,30 +36,49 @@ public final class KQueueIoEvent implements IoEvent {
      * @param flags     the general flags.
      * @param fflags    filter-specific flags.
      * @return          {@link KQueueIoEvent}.
+     * @deprecated use {@link #newEvent(int, short, short, int, long, long)}
      */
+    @Deprecated
     public static KQueueIoEvent newEvent(int ident, short filter, short flags, int fflags) {
-        return new KQueueIoEvent(ident, filter, flags, fflags, 0);
+        return new KQueueIoEvent(ident, filter, flags, fflags, 0, 0);
     }
 
-    private KQueueIoEvent(int ident, short filter, short flags, int fflags, long data) {
+    /**
+     * Creates a new {@link KQueueIoEvent}.
+     *
+     * @param ident     the identifier for this event.
+     * @param filter    the filter for this event.
+     * @param flags     the general flags.
+     * @param fflags    filter-specific flags.
+     * @param data      the data
+     * @param udata     the user defined data that is passed through.
+     * @return          {@link KQueueIoEvent}.
+     */
+    public static KQueueIoEvent newEvent(int ident, short filter, short flags, int fflags, long data, long udata) {
+        return new KQueueIoEvent(ident, filter, flags, fflags, data, udata);
+    }
+
+    private KQueueIoEvent(int ident, short filter, short flags, int fflags, long data, long udata) {
         this.ident = ident;
         this.filter = filter;
         this.flags = flags;
         this.fflags = fflags;
         this.data = data;
+        this.udata = udata;
     }
 
     KQueueIoEvent() {
-        this(0, (short) 0, (short) 0, 0, 0);
+        this(0, (short) 0, (short) 0, 0, 0, 0);
     }
 
     // Only used internally for re-use.
-    void update(int ident, short filter, short flags, int fflags, long data) {
+    void update(int ident, short filter, short flags, int fflags, long data, long udata) {
         this.ident = ident;
         this.filter = filter;
         this.flags = flags;
         this.fflags = fflags;
         this.data = data;
+        this.udata = udata;
     }
 
     /**
@@ -106,6 +126,15 @@ public long data() {
         return data;
     }
 
+    /**
+     * Returns user specified data.
+     *
+     * @return udata.
+     */
+    public long udata() {
+        return udata;
+    }
+
     @Override
     public String toString() {
         return "KQueueIoEvent{" +
@@ -114,6 +143,7 @@ public String toString() {
                 ", flags=" + flags +
                 ", fflags=" + fflags +
                 ", data=" + data +
+                ", udata=" + udata +
                 '}';
     }
 }
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoHandler.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoHandler.java
index 5068af29197..c7680f6b884 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoHandler.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueIoHandler.java
@@ -27,8 +27,8 @@
 import io.netty.channel.SelectStrategyFactory;
 import io.netty.channel.unix.FileDescriptor;
 import io.netty.util.IntSupplier;
-import io.netty.util.collection.IntObjectHashMap;
-import io.netty.util.collection.IntObjectMap;
+import io.netty.util.collection.LongObjectHashMap;
+import io.netty.util.collection.LongObjectMap;
 import io.netty.util.concurrent.ThreadAwareExecutor;
 import io.netty.util.internal.ObjectUtil;
 import io.netty.util.internal.StringUtil;
@@ -76,11 +76,31 @@ public int get() throws Exception {
         }
     };
     private final ThreadAwareExecutor executor;
-    private final IntObjectMap<DefaultKqueueIoRegistration> registrations = new IntObjectHashMap<>(4096);
+    private final LongObjectMap<DefaultKqueueIoRegistration> registrations = new LongObjectHashMap<>(4096);
     private int numChannels;
+    private long nextId;
 
     private volatile int wakenUp;
 
+    private long generateNextId() {
+        boolean reset = false;
+        for (;;) {
+            if (nextId == Long.MAX_VALUE) {
+                if (reset) {
+                    throw new IllegalStateException("All possible ids in use");
+                }
+                reset = true;
+            }
+            nextId++;
+            if (nextId == KQUEUE_WAKE_UP_IDENT) {
+                continue;
+            }
+            if (!registrations.containsKey(nextId)) {
+                return nextId;
+            }
+        }
+    }
+
     /**
      * Returns a new {@link IoHandlerFactory} that creates {@link KQueueIoHandler} instances.
      */
@@ -170,15 +190,16 @@ private void processReady(int ready) {
                 continue;
             }
 
-            DefaultKqueueIoRegistration registration = registrations.get(ident);
+            long id = eventList.udata(i);
+            DefaultKqueueIoRegistration registration = registrations.get(id);
             if (registration == null) {
                 // This may happen if the channel has already been closed, and it will be removed from kqueue anyways.
                 // We also handle EV_ERROR above to skip this even early if it is a result of a referencing a closed and
                 // thus removed from kqueue FD.
-                logger.warn("events[{}]=[{}, {}] had no registration!", i, ident, filter);
+                logger.warn("events[{}]=[{}, {}, {}] had no registration!", i, ident, id, filter);
                 continue;
             }
-            registration.handle(ident, filter, flags, eventList.fflags(i), eventList.data(i));
+            registration.handle(ident, filter, flags, eventList.fflags(i), eventList.data(i), id);
         }
     }
 
@@ -254,7 +275,7 @@ int numRegisteredChannels() {
     }
 
     List<Channel> registeredChannelsList() {
-        IntObjectMap<DefaultKqueueIoRegistration> ch = registrations;
+        LongObjectMap<DefaultKqueueIoRegistration> ch = registrations;
         if (ch.isEmpty()) {
             return Collections.emptyList();
         }
@@ -323,11 +344,11 @@ public IoRegistration register(IoHandle handle) {
 
         DefaultKqueueIoRegistration registration = new DefaultKqueueIoRegistration(
                 executor, kqueueHandle);
-        DefaultKqueueIoRegistration old = registrations.put(kqueueHandle.ident(), registration);
+        DefaultKqueueIoRegistration old = registrations.put(registration.id, registration);
         if (old != null) {
-            // restore old mapping and throw exception
-            registrations.put(kqueueHandle.ident(), old);
-            throw new IllegalStateException("registration for the KQueueIoHandle.ident() already exists");
+            // This should never happen but just in case.
+            registrations.put(old.id, old);
+            throw new IllegalStateException();
         }
 
         if (kqueueHandle instanceof AbstractKQueueChannel.AbstractKQueueUnsafe) {
@@ -360,12 +381,13 @@ private final class DefaultKqueueIoRegistration implements IoRegistration {
         private final KQueueIoEvent event = new KQueueIoEvent();
 
         final KQueueIoHandle handle;
-
+        final long id;
         private final ThreadAwareExecutor executor;
 
         DefaultKqueueIoRegistration(ThreadAwareExecutor executor, KQueueIoHandle handle) {
             this.executor = executor;
             this.handle = handle;
+            id = generateNextId();
         }
 
         @SuppressWarnings("unchecked")
@@ -391,13 +413,13 @@ public long submit(IoOps ops) {
             return 0;
         }
 
-        void handle(int ident, short filter, short flags, int fflags, long data) {
-            event.update(ident, filter, flags, fflags, data);
+        void handle(int ident, short filter, short flags, int fflags, long data, long udata) {
+            event.update(ident, filter, flags, fflags, data, udata);
             handle.handle(this, event);
         }
 
         private void evSet(short filter, short flags, int fflags) {
-            changeList.evSet(handle.ident(), filter, flags, fflags);
+            changeList.evSet(handle.ident(), filter, flags, fflags, 0, id);
         }
 
         @Override
@@ -419,12 +441,11 @@ public boolean cancel() {
         }
 
         private void cancel0() {
-            int ident = handle.ident();
-            DefaultKqueueIoRegistration old = registrations.remove(ident);
+            DefaultKqueueIoRegistration old = registrations.remove(id);
             if (old != null) {
                 if (old != this) {
                     // The Channel mapping was already replaced due FD reuse, put back the stored Channel.
-                    registrations.put(ident, old);
+                    registrations.put(id, old);
                 } else if (old.handle instanceof AbstractKQueueChannel.AbstractKQueueUnsafe) {
                     numChannels--;
                 }
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/Native.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/Native.java
index 20a09f49b14..5a831b080a6 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/Native.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/Native.java
@@ -142,6 +142,7 @@ private static native int keventWait(int kqueueFd, long changeListAddress, int c
     static native int offsetofKEventFFlags();
     static native int offsetofKEventFilter();
     static native int offsetofKeventData();
+    static native int offsetofKeventUdata();
 
     private static void loadNativeLibrary() {
         String name = PlatformDependent.normalizedOs();
diff --git a/transport-native-kqueue/src/main/c/netty_kqueue_eventarray.c b/transport-native-kqueue/src/main/c/netty_kqueue_eventarray.c
index e254a961ff5..5ced95bd1da 100644
--- a/transport-native-kqueue/src/main/c/netty_kqueue_eventarray.c
+++ b/transport-native-kqueue/src/main/c/netty_kqueue_eventarray.c
@@ -26,13 +26,13 @@
 
 #define EVENT_ARRAY_CLASSNAME "io/netty/channel/kqueue/KQueueEventArray"
 
-static void netty_kqueue_eventarray_evSet(JNIEnv* env, jclass clzz, jlong keventAddress, jint ident, jshort filter, jshort flags, jint fflags) {
-    EV_SET((struct kevent*) keventAddress, ident, filter, flags, fflags, 0, NULL);
+static void netty_kqueue_eventarray_evSet(JNIEnv* env, jclass clzz, jlong keventAddress, jint ident, jshort filter, jshort flags, jint fflags, jlong data, jlong udata) {
+    EV_SET((struct kevent*) keventAddress, ident, filter, flags, fflags, data, (void *) udata);
 }
 
 // JNI Method Registration Table Begin
 static const JNINativeMethod fixed_method_table[] = {
-  { "evSet", "(JISSI)V", (void *) netty_kqueue_eventarray_evSet }
+  { "evSet", "(JISSIJJ)V", (void *) netty_kqueue_eventarray_evSet }
 };
 static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]);
 
diff --git a/transport-native-kqueue/src/main/c/netty_kqueue_native.c b/transport-native-kqueue/src/main/c/netty_kqueue_native.c
index cd162380a62..cf81d212c81 100644
--- a/transport-native-kqueue/src/main/c/netty_kqueue_native.c
+++ b/transport-native-kqueue/src/main/c/netty_kqueue_native.c
@@ -204,6 +204,10 @@ static jint netty_kqueue_native_offsetofKeventData(JNIEnv* env, jclass clazz) {
     return offsetof(struct kevent, data);
 }
 
+static jint netty_kqueue_native_offsetofKeventUdata(JNIEnv* env, jclass clazz) {
+    return offsetof(struct kevent, udata);
+}
+
 static jshort netty_kqueue_native_evfiltRead(JNIEnv* env, jclass clazz) {
     return EVFILT_READ;
 }
@@ -357,6 +361,7 @@ static const JNINativeMethod fixed_method_table[] = {
   { "offsetofKEventFFlags", "()I", (void *) netty_kqueue_native_offsetofKEventFFlags },
   { "offsetofKEventFilter", "()I", (void *) netty_kqueue_native_offsetofKEventFilter },
   { "offsetofKeventData", "()I", (void *) netty_kqueue_native_offsetofKeventData },
+  { "offsetofKeventUdata", "()I", (void *) netty_kqueue_native_offsetofKeventUdata },
   { "registerUnix", "()I", (void *) netty_kqueue_native_registerUnix }
 };
 static const jint fixed_method_table_size = sizeof(fixed_method_table) / sizeof(fixed_method_table[0]);
diff --git a/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueRegistrationTest.java b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueRegistrationTest.java
new file mode 100644
index 00000000000..32a5a05baa6
--- /dev/null
+++ b/transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueRegistrationTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2025 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ *   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.netty.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.testsuite.transport.socket.SocketTestPermutation;
+import io.netty.util.NetUtil;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class KqueueRegistrationTest {
+
+    @BeforeEach
+    public void setUp() {
+        KQueue.ensureAvailability();
+    }
+
+    @Test
+    void testFdReuse() throws Exception {
+        Bootstrap bootstrap = new Bootstrap();
+        EventLoopGroup group = new MultiThreadIoEventLoopGroup(1, KQueueIoHandler.newFactory());
+        Promise<Throwable> promise = ImmediateEventExecutor.INSTANCE.newPromise();
+        try {
+            bootstrap.group(group)
+                    .channel(KQueueSocketChannel.class)
+                    .handler(new LoggingHandler());
+            bootstrap.connect(new InetSocketAddress(NetUtil.LOCALHOST, SocketTestPermutation.UNASSIGNED_PORT))
+                    .addListener((ChannelFutureListener) f1 -> {
+                        if (f1.cause() instanceof ConnectException) {
+                            f1.channel().close();
+                            bootstrap.connect(new InetSocketAddress(NetUtil.LOCALHOST,
+                                            SocketTestPermutation.UNASSIGNED_PORT))
+                                    .addListener((ChannelFutureListener) f2 -> {
+                                        f2.channel().close();
+                                        promise.setSuccess(f2.cause());
+                                    });
+                        } else {
+                            promise.setFailure(new IllegalStateException());
+                        }
+                    });
+            Throwable cause = promise.sync().getNow();
+            assertInstanceOf(ConnectException.class, cause);
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+}


Reviews:
user: login: chrisvest
body: Two nits in the test, but otherwise looks good.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T16:11:33Z

Review comments:
diff_hunk: @@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ *   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.netty.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.testsuite.transport.socket.SocketTestPermutation;
+import io.netty.util.NetUtil;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class KqueueRegistrationTest {
+
+    @BeforeEach
+    public void setUp() {
+        KQueue.ensureAvailability();
+    }
+
+    @Test
+    void testFdReuse() throws Exception {
+        Bootstrap bootstrap = new Bootstrap();
+        EventLoopGroup group = new MultiThreadIoEventLoopGroup(1, KQueueIoHandler.newFactory());
+        Promise<Throwable> promise = ImmediateEventExecutor.INSTANCE.newPromise();
+        try {
+            bootstrap.group(group)
+                    .channel(KQueueSocketChannel.class)
+                    //.channel(NioSocketChannel.class)
+                    .handler(new LoggingHandler());
path: transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueRegistrationTest.java
user: login: chrisvest
body: ```
//.channel(NioSocketChannel.class)
.handler(new LoggingHandler());
```

Debug leftover?
created_at: 2025-05-13T16:07:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 The Netty Project
+ *
+ * The Netty Project 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:
+ *
+ *   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.netty.channel.kqueue;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.testsuite.transport.socket.SocketTestPermutation;
+import io.netty.util.NetUtil;
+import io.netty.util.concurrent.ImmediateEventExecutor;
+import io.netty.util.concurrent.Promise;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.net.ConnectException;
+import java.net.InetSocketAddress;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class KqueueRegistrationTest {
+
+    @BeforeEach
+    public void setUp() {
+        KQueue.ensureAvailability();
+    }
+
+    @Test
+    void testFdReuse() throws Exception {
+        Bootstrap bootstrap = new Bootstrap();
+        EventLoopGroup group = new MultiThreadIoEventLoopGroup(1, KQueueIoHandler.newFactory());
+        Promise<Throwable> promise = ImmediateEventExecutor.INSTANCE.newPromise();
+        try {
+            bootstrap.group(group)
+                    .channel(KQueueSocketChannel.class)
+                    //.channel(NioSocketChannel.class)
+                    .handler(new LoggingHandler());
+            bootstrap.connect(new InetSocketAddress(NetUtil.LOCALHOST, SocketTestPermutation.UNASSIGNED_PORT))
+                    .addListener((ChannelFutureListener) f1 -> {
+                        if (f1.cause() instanceof ConnectException) {
+                            f1.channel().close();
+                            bootstrap.connect(new InetSocketAddress(NetUtil.LOCALHOST,
+                                            SocketTestPermutation.UNASSIGNED_PORT))
+                                    .addListener((ChannelFutureListener) f2 -> {
+                                        f2.channel().close();
+                                        promise.setSuccess(f2.cause());
+                                    });
+                        } else {
+                            promise.setFailure(new IllegalStateException());
+                        }
+                    });
+            Throwable cause = promise.sync().getNow();
+            assertThat(cause, is(instanceOf(ConnectException.class)));
path: transport-native-kqueue/src/test/java/io/netty/channel/kqueue/KqueueRegistrationTest.java
user: login: chrisvest
body: Nit: I suggest we prefer assertj over hamcrest
created_at: 2025-05-13T16:11:03Z
author_association: MEMBER

PR comments:
user: login: normanmaurer
created_at: 2025-05-12T08:06:46Z
body: /cc @violetagg 
author_association: MEMBER
user: login: violetagg
created_at: 2025-05-12T12:32:06Z
body: @normanmaurer Something is not ok. Check this example with/without this fix. With this fix, the server doesn't receive anything.

```
diff --git a/example/pom.xml b/example/pom.xml
index 684691d212..d2c31d9ac4 100644
--- a/example/pom.xml
+++ b/example/pom.xml
@@ -45,6 +45,11 @@
       <artifactId>netty-transport</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-transport-native-kqueue</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>netty-codec</artifactId>
diff --git a/example/src/main/java/io/netty/example/echo/EchoClient.java b/example/src/main/java/io/netty/example/echo/EchoClient.java
index 987649a0ce..27bddd12c2 100644
--- a/example/src/main/java/io/netty/example/echo/EchoClient.java
+++ b/example/src/main/java/io/netty/example/echo/EchoClient.java
@@ -22,10 +22,14 @@ import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.kqueue.KQueueIoHandler;
+import io.netty.channel.kqueue.KQueueSocketChannel;
 import io.netty.channel.nio.NioIoHandler;
 import io.netty.channel.socket.SocketChannel;
 import io.netty.channel.socket.nio.NioSocketChannel;
 import io.netty.example.util.ServerUtil;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
 import io.netty.handler.ssl.SslContext;
 
 /**
@@ -45,11 +49,11 @@ public final class EchoClient {
         final SslContext sslCtx = ServerUtil.buildSslContext();
 
         // Configure the client.
-        EventLoopGroup group = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+        EventLoopGroup group = new MultiThreadIoEventLoopGroup(KQueueIoHandler.newFactory());
         try {
             Bootstrap b = new Bootstrap();
             b.group(group)
-             .channel(NioSocketChannel.class)
+             .channel(KQueueSocketChannel.class)
              .option(ChannelOption.TCP_NODELAY, true)
              .handler(new ChannelInitializer<SocketChannel>() {
                  @Override
@@ -58,7 +62,7 @@ public final class EchoClient {
                      if (sslCtx != null) {
                          p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                      }
-                     //p.addLast(new LoggingHandler(LogLevel.INFO));
+                     p.addLast(new LoggingHandler(LogLevel.INFO));
                      p.addLast(new EchoClientHandler());
                  }
              });
diff --git a/example/src/main/java/io/netty/example/echo/EchoServer.java b/example/src/main/java/io/netty/example/echo/EchoServer.java
index db113f96bf..63e9ec20a5 100644
--- a/example/src/main/java/io/netty/example/echo/EchoServer.java
+++ b/example/src/main/java/io/netty/example/echo/EchoServer.java
@@ -22,6 +22,8 @@ import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.EventLoopGroup;
 import io.netty.channel.MultiThreadIoEventLoopGroup;
+import io.netty.channel.kqueue.KQueueIoHandler;
+import io.netty.channel.kqueue.KQueueServerSocketChannel;
 import io.netty.channel.nio.NioIoHandler;
 import io.netty.channel.socket.SocketChannel;
 import io.netty.channel.socket.nio.NioServerSocketChannel;
@@ -42,13 +44,13 @@ public final class EchoServer {
         final SslContext sslCtx = ServerUtil.buildSslContext();
 
         // Configure the server.
-        EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
-        EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
+        EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(1, KQueueIoHandler.newFactory());
+        EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(KQueueIoHandler.newFactory());
         final EchoServerHandler serverHandler = new EchoServerHandler();
         try {
             ServerBootstrap b = new ServerBootstrap();
             b.group(bossGroup, workerGroup)
-             .channel(NioServerSocketChannel.class)
+             .channel(KQueueServerSocketChannel.class)
              .option(ChannelOption.SO_BACKLOG, 100)
              .handler(new LoggingHandler(LogLevel.INFO))
              .childHandler(new ChannelInitializer<SocketChannel>() {
```
author_association: MEMBER
user: login: normanmaurer
created_at: 2025-05-12T14:22:15Z
body: will check... thanks 
author_association: MEMBER
user: login: normanmaurer
created_at: 2025-05-12T18:27:14Z
body: @violetagg I had a brainfart... this of course does not work. Will do a proper fix tomorrow 
author_association: MEMBER
user: login: normanmaurer
created_at: 2025-05-13T08:08:35Z
body: @violetagg ok I think this is the correct fix. 
author_association: MEMBER
user: login: violetagg
created_at: 2025-05-13T09:12:14Z
body: @normanmaurer my tests are ok with this change :tada:
author_association: MEMBER
user: login: normanmaurer
created_at: 2025-05-13T09:39:06Z
body: A variant of this should also be ported to 4.1 
author_association: MEMBER
user: login: normanmaurer
created_at: 2025-05-13T13:53:19Z
body: Here is the back port https://github.com/netty/netty/pull/15162
author_association: MEMBER