[ 
https://issues.apache.org/jira/browse/QPID-8772?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18130748#comment-18130748
 ] 

ASF GitHub Bot commented on QPID-8772:
--------------------------------------

dakirily commented on code in PR #453:
URL: https://github.com/apache/qpid-broker-j/pull/453#discussion_r4357226513


##########
broker-core/src/test/java/org/apache/qpid/server/transport/TlsTestContext.java:
##########
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
+ *
+ *   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 org.apache.qpid.server.transport;
+
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+
+import org.apache.qpid.test.utils.tls.KeyCertificatePair;
+import org.apache.qpid.test.utils.tls.TlsResourceBuilder;
+
+final class TlsTestContext
+{
+    private TlsTestContext()
+    {

Review Comment:
   Please provide a comment explaining private empty constructor



##########
broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionWriteTest.java:
##########
@@ -0,0 +1,507 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
+ *
+ *   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 org.apache.qpid.server.transport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionWriteTest extends UnitTestBase
+{
+    private static final int NETWORK_BUFFER_SIZE = 1024;
+
+    private final AtomicBoolean _hasWork = new AtomicBoolean();
+    private final AtomicBoolean _blocked = new AtomicBoolean();
+    private final AtomicInteger _writableBytes = new AtomicInteger();
+    private final List<Boolean> _blockedTransitions = new ArrayList<>();
+    private ProtocolEngine _engine;
+    private SocketChannel _channel;
+    private NonBlockingConnection _connection;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        _hasWork.set(false);
+        _blocked.set(false);
+        _writableBytes.set(0);
+        _blockedTransitions.clear();
+        final Socket socket = mock(Socket.class);
+        when(socket.getRemoteSocketAddress()).thenReturn(new 
InetSocketAddress("localhost", 1000));
+        when(socket.getLocalSocketAddress()).thenReturn(new 
InetSocketAddress("localhost", 5672));
+        _channel = mock(SocketChannel.class);
+        when(_channel.socket()).thenReturn(socket);
+        when(_channel.write(any(ByteBuffer[].class))).thenAnswer(invocation ->
+        {
+            final ByteBuffer[] buffers = invocation.getArgument(0);
+            final int offset = 0;
+            final int length = buffers.length;
+            long written = 0;
+            for (int i = offset; i < offset + length; i++)
+            {
+                final int size = Math.min(buffers[i].remaining(), 
_writableBytes.get());
+                buffers[i].position(buffers[i].position() + size);
+                _writableBytes.addAndGet(-size);
+                written += size;
+            }
+            return written;
+        });
+
+        _engine = mock(ProtocolEngine.class);
+        final AggregateTicker ticker = mock(AggregateTicker.class);
+        
when(ticker.getTimeToNextTick(anyLong())).thenReturn(Integer.MAX_VALUE);
+        when(_engine.getAggregateTicker()).thenReturn(ticker);
+        when(_engine.processPendingIterator()).thenAnswer(invocation -> 
Collections.emptyIterator());
+        when(_engine.hasWork()).thenAnswer(invocation -> _hasWork.get());
+        when(_engine.isTransportBlockedForWriting()).thenAnswer(invocation -> 
_blocked.get());
+        doAnswer(invocation ->
+        {
+            _hasWork.set(false);
+            return null;
+        }).when(_engine).clearWork();
+        doAnswer(invocation ->
+        {
+            _hasWork.set(true);
+            return null;
+        }).when(_engine).notifyWork();
+        doAnswer(invocation ->
+        {
+            final boolean blocked = invocation.getArgument(0);
+            if (_blocked.getAndSet(blocked) != blocked)
+            {
+                _blockedTransitions.add(blocked);
+                if (!blocked)
+                {
+                    _engine.notifyWork();
+                }
+            }
+            return null;
+        }).when(_engine).setTransportBlockedForWriting(anyBoolean());
+
+        final AmqpPort<?> port = mock(AmqpPort.class);
+        when(port.getNetworkBufferSize()).thenReturn(NETWORK_BUFFER_SIZE);
+        when(port.getContextValue(Integer.class, 
AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(1);
+        when(port.getContextValue(Long.class, 
AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(0L);
+        when(port.getContextValue(Integer.class, 
AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
+        final Broker<?> broker = mock(Broker.class);
+        when(broker.getEventLogger()).thenReturn(mock(EventLogger.class));
+        doReturn(broker).when(port).getParent();
+        _connection = new NonBlockingConnection(_channel, _engine, 
Set.of(TransportEncryption.NONE),
+                () -> { }, mock(NetworkConnectionScheduler.class), port);
+        _connection.setSelectionTask(mock(SelectorThread.SelectionTask.class));
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        _writableBytes.set(Integer.MAX_VALUE);
+        _connection.close();
+        _connection.doWork();
+    }
+
+    @Test
+    void blockedOutputWaitsForSocketReadiness()
+    {
+        send(128);
+
+        assertFalse(_connection.doWork());
+
+        assertTrue(_connection.wantsWrite());
+        assertTrue(_connection.wantsRead(), "Completed protocol work must 
allow incoming traffic during a blocked write");
+        assertFalse(_connection.isStateChanged(), "Pending output alone must 
not reschedule the connection");
+        assertEquals(List.of(true), _blockedTransitions);
+
+        _engine.notifyWork();
+        _connection.doWork();
+        assertEquals(List.of(true), _blockedTransitions, "A failed retry must 
not briefly resume consumers");
+        assertFalse(_connection.isStateChanged());
+
+        _writableBytes.set(32);
+        _connection.doWork();
+        assertTrue(_connection.wantsWrite(), "Partial progress must retain 
write interest");
+        assertEquals(List.of(true), _blockedTransitions);
+        assertFalse(_connection.isStateChanged());
+
+        _writableBytes.set(96);
+        _connection.doWork();
+        assertEquals(List.of(true, false), _blockedTransitions);
+        assertTrue(_connection.isStateChanged(), "Unblocking must resume 
pending protocol output");
+        assertTrue(_connection.wantsRead());
+        assertFalse(_connection.wantsWrite());
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {0, NETWORK_BUFFER_SIZE / 4})
+    void pendingIteratorIsRescheduledWhenSecondWriteDrainsOutput(final int 
firstWriteBytes) throws Exception
+    {
+        final AtomicInteger completed = new AtomicInteger();
+        final List<Runnable> tasks = List.of(() -> send(2 * 
NETWORK_BUFFER_SIZE), completed::incrementAndGet);
+        when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+        final AtomicInteger writeCalls = new AtomicInteger();
+        doAnswer(invocation ->
+        {
+            final ByteBuffer[] buffers = invocation.getArgument(0);
+            int writableBytes = writeCalls.getAndIncrement() == 0 ? 
firstWriteBytes : Integer.MAX_VALUE;
+            long written = 0;
+            for (final ByteBuffer buffer : buffers)
+            {
+                final int size = Math.min(buffer.remaining(), writableBytes);
+                buffer.position(buffer.position() + size);
+                writableBytes -= size;
+                written += size;
+            }
+            return written;
+        }).when(_channel).write(any(ByteBuffer[].class));
+
+        assertFalse(_connection.doWork());
+
+        assertEquals(2, writeCalls.get(), "The second write must drain output 
after the iterator is suspended");
+        assertEquals(0, completed.get(), "The iterator still has runnable work 
after the second write");
+        assertTrue(_connection.wantsRead());
+        assertFalse(_connection.wantsWrite());
+        assertFalse(_connection.isPartialRead());
+        assertFalse(_blocked.get());
+        assertTrue(_blockedTransitions.isEmpty(), "There is no backpressure 
transition to supply a wake-up");
+        assertTrue(_connection.isStateChanged(),
+                "Pending protocol work must be rescheduled when output drains 
in the same pass");
+
+        assertFalse(_connection.doWork());
+        assertEquals(1, completed.get(), "The retained iterator must resume 
exactly once");
+        assertFalse(_connection.isStateChanged(), "An exhausted iterator must 
not keep rescheduling");
+        verify(_engine).processPendingIterator();
+    }
+
+    @Test
+    void pendingIteratorResumesAfterOutputDrains()
+    {
+        final AtomicInteger completed = new AtomicInteger();
+        final List<Runnable> tasks = List.of(() -> send(NETWORK_BUFFER_SIZE), 
completed::incrementAndGet);
+        when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+
+        _connection.doWork();
+
+        assertEquals(0, completed.get());
+        assertTrue(_connection.wantsWrite());
+        assertFalse(_connection.wantsRead(), "Suspended protocol work must 
continue to apply read backpressure");
+        assertFalse(_connection.isStateChanged(), "A suspended iterator must 
wait for its buffered output to drain");
+
+        _writableBytes.set(NETWORK_BUFFER_SIZE);
+        _connection.doWork();
+
+        assertEquals(1, completed.get(), "Suspended work must resume exactly 
once");
+        assertFalse(_connection.wantsWrite());
+    }
+
+    @ParameterizedTest
+    @CsvSource({"false, 768", "true, 768", "false, 1023", "true, 1023"})
+    void partialWriteBelowBufferLimitResumesWorkAndReads(final boolean 
pendingTask,
+                                                        final int 
remainingOutput) throws Exception

Review Comment:
   Please fix the parameter indentation



##########
broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnection.java:
##########
@@ -278,19 +279,26 @@ public boolean doWork()
                     if (processPendingComplete)
                     {
                         _pendingIterator = null;
-                        _protocolEngine.setTransportBlockedForWriting(false);
+                    }
+
+                    if (processPendingComplete || 
_delegate.needsReadForWrite())
+                    {
                         final boolean dataRead = doRead();
-                        
_protocolEngine.setTransportBlockedForWriting(!doWrite());
+                        doWrite();
 
-                        if (!_fullyWritten || dataRead || 
(_delegate.needsWork() && _delegate.getNetInputBuffer().position() != 0))
+                        if (_fullyWritten &&
+                                (dataRead || (_delegate.needsWork() && 
_delegate.getNetInputBuffer().position() != 0)))

Review Comment:
   if expression on lines 289-290 should be on one line



##########
broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionSchedulingTest.java:
##########
@@ -0,0 +1,455 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
+ *
+ *   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 org.apache.qpid.server.transport;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.server.util.Action;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionSchedulingTest extends UnitTestBase
+{
+    private static final int TIMEOUT_MILLIS = 10000;
+
+    private final List<Thread> _threads = new CopyOnWriteArrayList<>();
+    private final List<TestConnection> _connections = new ArrayList<>();
+    private NetworkConnectionScheduler _scheduler;
+    private SSLContext _sslContext;
+
+    @BeforeAll
+    void createTlsContext() throws Exception
+    {
+        _sslContext = TlsTestContext.create();
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {2, 4})
+    void blockedSocketReturnsWorkersAndRecoversPendingOutput(final int 
workers) throws Exception
+    {
+        startScheduler(workers);
+        final TestConnection blocked = createConnection(1024);
+        final TestConnection independent = createConnection(1024);
+        final byte[] payload = new byte[2 * 1024 * 1024];
+        Arrays.fill(payload, (byte) 42);
+        final byte[] marker = new byte[]{1, 2, 3};
+        for (int i = 0; i < 3; i++)
+        {
+            blocked.submit(List.of(() -> blocked.send(payload), () -> 
blocked.send(marker)));
+            blocked.awaitWriteInterest();
+
+            independent.submit(List.of(() -> independent.send(marker)));
+            assertArrayEquals(marker, 
independent._client.getInputStream().readNBytes(marker.length),
+                    "An independent connection must progress while the first 
socket remains blocked");
+
+            assertArrayEquals(payload, 
blocked._client.getInputStream().readNBytes(payload.length));
+            assertArrayEquals(marker, 
blocked._client.getInputStream().readNBytes(marker.length),
+                    "The suspended protocol iterator must resume after socket 
capacity becomes available");
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({"TLSv1.2, 2", "TLSv1.2, 4", "TLSv1.3, 2", "TLSv1.3, 4"})
+    void blockedTlsSocketReturnsWorkersAndRecoversPendingOutput(final String 
protocol, final int workers) throws Exception
+    {
+        startScheduler(workers);
+        final TestConnection blocked = createTlsConnection(protocol);
+        final TestConnection independent = createTlsConnection(protocol);
+        final byte[] payload = new byte[2 * 1024 * 1024];
+        for (int i = 0; i < payload.length; i++)
+        {
+            payload[i] = (byte) i;
+        }
+        final byte[] marker = new byte[]{11, 22, 33};
+        for (int round = 0; round < 3; round++)
+        {
+            blocked.submit(List.of(() -> blocked.send(payload), () -> 
blocked.send(marker)));
+            blocked.awaitWriteInterest();
+            assertTrue(blocked._blocked.get());
+
+            independent.submit(List.of(() -> independent.send(marker)));
+            assertArrayEquals(marker, 
independent._client.getInputStream().readNBytes(marker.length),
+                    "An independent TLS connection must progress while the 
first socket is blocked");
+
+            blocked._client.getOutputStream().write(42 + round);
+            assertEquals(Integer.valueOf(42 + round), 
blocked._received.poll(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS),
+                    "TLS input must still be decrypted while encrypted output 
is blocked");
+            blocked.awaitWriteInterest();
+            assertTrue(blocked._blocked.get(), "Reading peer input must not 
briefly resume blocked consumers");
+
+            assertArrayEquals(payload, 
blocked._client.getInputStream().readNBytes(payload.length));
+            assertArrayEquals(marker, 
blocked._client.getInputStream().readNBytes(marker.length),
+                    "Pending output must resume without losing or reordering 
application bytes");
+            blocked.awaitSelection(false);
+            assertFalse(blocked._blocked.get(), "Draining encrypted output 
must resume consumers");
+        }
+    }
+
+    @Test
+    void incomingHeartbeatsPreventReadIdleTimeoutWhileOutputIsBlocked() throws 
Exception
+    {
+        startScheduler(2);
+        final TestConnection blocked = createConnection(4 * 1024 * 1024);
+        blocked.submit(List.of(() -> blocked.send(new byte[2 * 1024 * 1024])));
+        blocked.awaitWriteInterest();
+
+        final int readIdleMillis = 2000;
+        final AtomicLong currentTime = new AtomicLong();
+        final AtomicLong lastReadTime = new AtomicLong();
+        final BlockingQueue<Integer> heartbeats = new LinkedBlockingQueue<>();
+        final AtomicBoolean readerIdle = new AtomicBoolean();
+        when(blocked._engine.getLastReadTime()).thenAnswer(invocation -> 
lastReadTime.get());
+        doAnswer(invocation ->
+        {
+            final QpidByteBuffer input = invocation.getArgument(0);
+            lastReadTime.set(currentTime.get());
+            while (input.hasRemaining())
+            {
+                heartbeats.add((int) input.get());
+            }
+            return null;
+        }).when(blocked._engine).received(any());
+        doAnswer(invocation ->
+        {
+            readerIdle.set(true);
+            blocked._connection.close();
+            return null;
+        }).when(blocked._engine).readerIdle();
+
+        // Keep wall-clock scheduling timestamps out of the idle calculation 
so only the test advances time.
+        blocked._ticker.addTicker(new 
ServerIdleReadTimeoutTicker(mock(ServerNetworkConnection.class),
+                blocked._engine, readIdleMillis)
+        {
+            @Override
+            public int getTimeToNextTick(final long ignored)
+            {
+                return super.getTimeToNextTick(currentTime.get());
+            }
+        });
+        blocked.awaitWriteInterest();
+
+        for (int i = 0; i < 8; i++)
+        {
+            currentTime.addAndGet(readIdleMillis / 4);
+            blocked._client.getOutputStream().write(42);
+            assertEquals(Integer.valueOf(42), heartbeats.poll(TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS),
+                    "Incoming heartbeats must be consumed while the peer is 
not reading output");
+            blocked.awaitWriteInterest();
+            assertFalse(readerIdle.get(), "A peer sending heartbeats must not 
be considered idle");
+        }
+
+        assertTrue(blocked._connection.wantsWrite(), "Output must remain 
blocked throughout the heartbeat checks");
+        currentTime.addAndGet(readIdleMillis - 1);
+        blocked.awaitWriteInterest();
+        assertFalse(readerIdle.get(), "The connection must remain open until 
the read deadline");
+
+        currentTime.incrementAndGet();
+        blocked._connection.getSelectionTask().wakeup();
+        blocked._closed.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        assertTrue(readerIdle.get(), "The read timeout must still close the 
connection after heartbeats stop");
+    }
+
+    private void startScheduler(final int workers)
+    {
+        _scheduler = new NetworkConnectionScheduler(getTestName(), 1, workers, 
1L, task ->
+        {
+            final Thread thread = new Thread(task, getTestName() + "-" + 
_threads.size());
+            _threads.add(thread);
+            return thread;
+        });
+        _scheduler.start();
+    }
+
+    private TestConnection createConnection(final int networkBufferSize) 
throws Exception
+    {
+        final TestConnection connection = new TestConnection(_scheduler, 
networkBufferSize);
+        _connections.add(connection);
+        return connection;
+    }
+
+    private TestConnection createTlsConnection(final String protocol) throws 
Exception
+    {
+        final SSLSocket client = (SSLSocket) 
_sslContext.getSocketFactory().createSocket();
+        client.setEnabledProtocols(new String[]{protocol});
+        final TestConnection connection = new TestConnection(_scheduler, 64 * 
1024, client, _sslContext);
+        _connections.add(connection);
+        client.startHandshake();
+        assertEquals(protocol, client.getSession().getProtocol());
+        return connection;
+    }
+
+    @AfterEach
+    void tearDown() throws Exception
+    {
+        if (_scheduler != null)
+        {
+            try
+            {
+                for (final TestConnection connection : _connections)
+                {
+                    connection._connection.close();
+                }
+                for (final TestConnection connection : _connections)
+                {
+                    connection._closed.get(TIMEOUT_MILLIS, 
TimeUnit.MILLISECONDS);
+                }
+            }
+            finally
+            {
+                for (final TestConnection connection : _connections)
+                {
+                    connection._client.close();
+                    connection._channel.close();
+                }
+                _scheduler.close();
+                try
+                {
+                    for (final Thread thread : _threads)
+                    {
+                        thread.join(TIMEOUT_MILLIS);
+                        assertFalse(thread.isAlive(), "Scheduler worker did 
not terminate: " + thread.getName());
+                    }
+                }
+                finally
+                {
+                    for (final Thread thread : _threads)
+                    {
+                        thread.interrupt();
+                    }
+                    for (final Thread thread : _threads)
+                    {
+                        thread.join(TIMEOUT_MILLIS);
+                    }
+                }
+            }
+        }
+    }
+
+    private static final class TestConnection
+    {
+        private final AtomicBoolean _hasWork = new AtomicBoolean();
+        private final AtomicBoolean _blocked = new AtomicBoolean();
+        private final AtomicReference<Iterator<Runnable>> _pending =
+                new AtomicReference<>(Collections.emptyIterator());
+        private final AtomicReference<SelectionInterest> _selectionInterest = 
new AtomicReference<>();
+        private final BlockingQueue<Integer> _received = new 
LinkedBlockingQueue<>();
+        private final AggregateTicker _ticker = new AggregateTicker()
+        {
+            @Override
+            public void resetModified()
+            {
+                super.resetModified();
+                // The selector calls this after examining a registered 
connection's ticker.
+                signalSelection();
+            }
+        };
+        private final CompletableFuture<Void> _closed = new 
CompletableFuture<>();
+        private final Socket _client;
+        private final SocketChannel _channel;
+        private final ProtocolEngine _engine = mock(ProtocolEngine.class);
+        private final NonBlockingConnection _connection;
+
+        private TestConnection(final NetworkConnectionScheduler scheduler, 
final int networkBufferSize) throws Exception
+        {
+            this(scheduler, networkBufferSize, new Socket(), null);
+        }
+
+        private TestConnection(final NetworkConnectionScheduler scheduler, 
final int networkBufferSize,

Review Comment:
   Please place each method parameter on a separate line





> [Broker-J] Excessive CPU usage from continuous rescheduling of blocked 
> TCP/TLS writes
> -------------------------------------------------------------------------------------
>
>                 Key: QPID-8772
>                 URL: https://issues.apache.org/jira/browse/QPID-8772
>             Project: Qpid
>          Issue Type: Bug
>          Components: Broker-J
>    Affects Versions: qpid-java-broker-10.1.1
>            Reporter: Kato Kaito
>            Priority: Minor
>             Fix For: qpid-java-broker-10.2.0
>
>         Attachments: qpid-8772-jmh-main.zip
>
>
> Deployments with many concurrent AMQP connections can experience write 
> backpressure when some clients consume data slowly or communicate over 
> constrained network paths, such as VPNs or TCP proxies. In these conditions, 
> outbound data remains buffered while the broker waits for clients to receive 
> it.
> The native TCP/TLS transport can repeatedly reschedule a connection with 
> pending output without waiting for the socket to become writable. This causes 
> repeated write attempts even when the connection cannot make progress. A 
> stale partial-read indication can also keep a connection scheduled when 
> subsequent processing skips reads.
> This behavior can consume significant CPU despite limited network throughput 
> and occupy I/O workers needed by other connections. The impact is 
> particularly relevant when several slow connections share an I/O pool with 
> otherwise healthy clients.
> TLS requires an additional distinction: pending application output may be 
> waiting for peer handshake data rather than socket write capacity. In this 
> case, write readiness cannot advance the connection; the transport must allow 
> reads and wait for incoming data.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to