This is an automated email from the ASF dual-hosted git repository.
dakirily pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git
The following commit(s) were added to refs/heads/main by this push:
new fff3dbeba0 QPID-8772: [Broker-J] Excessive CPU usage from continuous
rescheduling of blocked TCP/TLS writes (#453)
fff3dbeba0 is described below
commit fff3dbeba0728ead7ce4f6dab62deff6d85d0bbb
Author: coolkaito <[email protected]>
AuthorDate: Mon Oct 26 10:22:24 2026 +0100
QPID-8772: [Broker-J] Excessive CPU usage from continuous rescheduling of
blocked TCP/TLS writes (#453)
---
.../server/transport/NonBlockingConnection.java | 20 +-
.../transport/NonBlockingConnectionDelegate.java | 6 +
.../NonBlockingConnectionTLSDelegate.java | 12 +-
.../NonBlockingConnectionSchedulingTest.java | 459 +++++++++++++++++++
.../NonBlockingConnectionTLSDelegateTest.java | 106 +++++
.../transport/NonBlockingConnectionTLSTest.java | 370 +++++++++++++++
.../transport/NonBlockingConnectionWriteTest.java | 508 +++++++++++++++++++++
.../qpid/server/transport/TlsTestContext.java | 54 +++
.../server/protocol/v0_10/ConsumerTarget_0_10.java | 21 +-
.../v0_10/ConsumerTarget_0_10FlushTest.java | 246 ++++++++++
.../server/protocol/v0_8/ConsumerTarget_0_8.java | 47 +-
.../qpid/server/protocol/v0_8/AMQChannelTest.java | 132 ++++++
12 files changed, 1959 insertions(+), 22 deletions(-)
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnection.java
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnection.java
index 343e2b3343..1d0cc188fe 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnection.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnection.java
@@ -225,12 +225,12 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
boolean wantsRead()
{
- return _fullyWritten;
+ return (_fullyWritten || _pendingIterator == null ||
_delegate.needsReadForWrite()) && _delegate.readyForRead();
}
boolean wantsWrite()
{
- return !_fullyWritten;
+ return !_fullyWritten && !_delegate.needsReadForWrite();
}
public boolean isStateChanged()
@@ -257,6 +257,7 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
public boolean doWork()
{
_protocolEngine.clearWork();
+ _partialRead = false;
if (!_closed.get())
{
try
@@ -278,19 +279,26 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
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)))
{
_protocolEngine.notifyWork();
}
}
- else
+
+ if (!processPendingComplete && (_fullyWritten ||
getBufferedSize() < _port.getNetworkBufferSize()))
{
_protocolEngine.notifyWork();
}
+
+
_protocolEngine.setTransportBlockedForWriting(!_fullyWritten);
}
}
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionDelegate.java
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionDelegate.java
index f827768b05..873e7d0e37 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionDelegate.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionDelegate.java
@@ -54,6 +54,12 @@ interface NonBlockingConnectionDelegate
boolean readyForRead();
+ /** Whether pending output requires peer input before another write can
make progress. */
+ default boolean needsReadForWrite()
+ {
+ return false;
+ }
+
boolean processData() throws IOException;
Principal getPeerPrincipal();
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegate.java
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegate.java
index 6a4dab02f5..d860a8216c 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegate.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegate.java
@@ -59,6 +59,7 @@ public class NonBlockingConnectionTLSDelegate implements
NonBlockingConnectionDe
private Certificate _peerCertificate;
private boolean _principalChecked;
private volatile boolean _hostChecked;
+ private volatile boolean _needsReadForWrite;
private QpidByteBuffer _netInputBuffer;
private QpidByteBuffer _netOutputBuffer;
private QpidByteBuffer _applicationBuffer;
@@ -139,11 +140,11 @@ public class NonBlockingConnectionTLSDelegate implements
NonBlockingConnectionDe
}
tasksRun = runSSLEngineTasks(_status);
- _applicationBuffer.flip();
if(_applicationBuffer.position() > oldAppBufPos)
{
readData = true;
}
+ _applicationBuffer.flip();
_parent.processAmqpData(_applicationBuffer);
@@ -208,9 +209,18 @@ public class NonBlockingConnectionTLSDelegate implements
NonBlockingConnectionDe
}
}
}
+
+ _needsReadForWrite = _encryptedOutput.isEmpty() &&
+ _sslEngine.getHandshakeStatus() ==
SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
return new WriteResult(bufsSent && _encryptedOutput.isEmpty(),
totalConsumed);
}
+ @Override
+ public boolean needsReadForWrite()
+ {
+ return _needsReadForWrite;
+ }
+
protected void restoreApplicationBufferForWrite()
{
try (QpidByteBuffer oldApplicationBuffer = _applicationBuffer)
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionSchedulingTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionSchedulingTest.java
new file mode 100644
index 0000000000..ecc396461e
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionSchedulingTest.java
@@ -0,0 +1,459 @@
+/*
+ * 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();
+ 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,
+ final Socket client,
+ final SSLContext sslContext) throws Exception
+ {
+ _client = client;
+ try (final ServerSocketChannel listener =
ServerSocketChannel.open())
+ {
+ listener.bind(new
InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
+ _client.setReceiveBufferSize(4096);
+ _client.setSoTimeout(TIMEOUT_MILLIS);
+ _client.connect(listener.getLocalAddress(), TIMEOUT_MILLIS);
+ _channel = listener.accept();
+ }
+ _channel.configureBlocking(false);
+ _channel.socket().setSendBufferSize(4096);
+ when(_engine.getAggregateTicker()).thenReturn(_ticker);
+ when(_engine.hasWork()).thenAnswer(invocation -> _hasWork.get());
+ when(_engine.processPendingIterator()).thenAnswer(invocation ->
_pending.get());
+ doAnswer(invocation ->
+ {
+ final QpidByteBuffer input = invocation.getArgument(0);
+ while (input.hasRemaining())
+ {
+ _received.add((int) input.get());
+ }
+ return null;
+ }).when(_engine).received(any());
+ doAnswer(invocation ->
+ {
+ _hasWork.set(false);
+ return null;
+ }).when(_engine).clearWork();
+ doAnswer(invocation ->
+ {
+ final boolean blocked = invocation.getArgument(0);
+ if (_blocked.getAndSet(blocked) && !blocked)
+ {
+ _engine.notifyWork();
+ }
+ return null;
+ }).when(_engine).setTransportBlockedForWriting(anyBoolean());
+ doAnswer(invocation ->
+ {
+ _closed.complete(null);
+ return null;
+ }).when(_engine).closed();
+ final AmqpPort<?> port = mock(AmqpPort.class);
+ when(port.getNetworkBufferSize()).thenReturn(networkBufferSize);
+ 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);
+ when(port.getSSLContext()).thenReturn(sslContext);
+ when(port.getContextValue(Boolean.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING)).thenReturn(false);
+ when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_WARN_THRESHOLD))
+ .thenReturn(1000);
+ when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD))
+ .thenReturn(10000);
+ final Broker<?> broker = mock(Broker.class);
+ when(broker.getEventLogger()).thenReturn(mock(EventLogger.class));
+ doReturn(broker).when(port).getParent();
+ final AtomicReference<Action<ProtocolEngine>> workListener = new
AtomicReference<>();
+ doAnswer(invocation ->
+ {
+ workListener.set(invocation.getArgument(0));
+ return null;
+ }).when(_engine).setWorkListener(any());
+ doAnswer(invocation ->
+ {
+ _hasWork.set(true);
+ workListener.get().performAction(_engine);
+ return null;
+ }).when(_engine).notifyWork();
+ final TransportEncryption transportEncryption = sslContext == null
+ ? TransportEncryption.NONE
+ : TransportEncryption.TLS;
+ final Set<TransportEncryption> encryptionSet =
Set.of(transportEncryption);
+ _connection = new NonBlockingConnection(_channel, _engine,
encryptionSet, () -> { }, scheduler, port);
+ scheduler.addConnection(_connection);
+ }
+
+ private void submit(final List<Runnable> work)
+ {
+ _pending.set(work.iterator());
+ _engine.notifyWork();
+ }
+
+ private void send(final byte[] bytes)
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.wrap(bytes))
+ {
+ _connection.send(buffer);
+ }
+ }
+
+ private void awaitWriteInterest() throws Exception
+ {
+ awaitSelection(true);
+ }
+
+ private void awaitSelection(final boolean wantsWrite) throws Exception
+ {
+ final SelectionInterest interest = new
SelectionInterest(wantsWrite, new CompletableFuture<>());
+ _selectionInterest.set(interest);
+ _connection.getSelectionTask().wakeup();
+ try
+ {
+ interest.ready().get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+ catch (TimeoutException e)
+ {
+ fail("Socket never returned to readiness selection with
wantsWrite=" + wantsWrite, e);
+ }
+ finally
+ {
+ _selectionInterest.compareAndSet(interest, null);
+ }
+ }
+
+ private void signalSelection()
+ {
+ final SelectionInterest interest = _selectionInterest.get();
+ if (interest != null)
+ {
+ final SelectionKey key =
_channel.keyFor(_connection.getSelectionTask().getSelector());
+ final int expectedOps = (interest.wantsWrite() ?
SelectionKey.OP_WRITE : 0) |
+ (_connection.wantsRead() ? SelectionKey.OP_READ : 0);
+ if (_connection.wantsWrite() == interest.wantsWrite() &&
!_connection.isStateChanged() &&
+ !_connection.isPartialRead() && key != null &&
+ key.isValid() && key.interestOps() == expectedOps)
+ {
+ interest.ready().complete(null);
+ }
+ }
+ }
+
+ private record SelectionInterest(boolean wantsWrite,
CompletableFuture<Void> ready)
+ {
+ }
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegateTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegateTest.java
new file mode 100644
index 0000000000..2eb9bb5392
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSDelegateTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+import javax.net.ssl.SSLSession;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionTLSDelegateTest extends UnitTestBase
+{
+ @Test
+ void handshakeOutputIsFlushedBeforeWaitingForPeerInput() throws Exception
+ {
+ final NonBlockingConnection parent = mock(NonBlockingConnection.class);
+ final SSLEngine engine = mock(SSLEngine.class);
+ final AtomicReference<SSLEngineResult.HandshakeStatus> status =
+ new
AtomicReference<>(SSLEngineResult.HandshakeStatus.NEED_WRAP);
+ when(engine.getHandshakeStatus()).thenAnswer(invocation ->
status.get());
+ when(engine.wrap(any(ByteBuffer[].class),
any(ByteBuffer.class))).thenAnswer(invocation ->
+ {
+ final ByteBuffer output = invocation.getArgument(1);
+ output.put((byte) 1);
+ status.set(SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
+ return new SSLEngineResult(SSLEngineResult.Status.OK,
status.get(), 0, 1);
+ });
+ final SSLSession session = mock(SSLSession.class);
+ when(session.getPacketBufferSize()).thenReturn(1024);
+ when(engine.getSession()).thenReturn(session);
+ when(engine.getEnabledProtocols()).thenReturn(new String[]{"TLSv1.3"});
+ when(engine.getSupportedProtocols()).thenReturn(new
String[]{"TLSv1.3"});
+ when(engine.getEnabledCipherSuites()).thenReturn(new
String[]{"TLS_AES_128_GCM_SHA256"});
+ when(engine.getSupportedCipherSuites()).thenReturn(new
String[]{"TLS_AES_128_GCM_SHA256"});
+ final SSLContext context = mock(SSLContext.class);
+ when(context.createSSLEngine()).thenReturn(engine);
+ final AmqpPort<?> port = mock(AmqpPort.class);
+ when(port.getSSLContext()).thenReturn(context);
+ when(port.getNetworkBufferSize()).thenReturn(4096);
+ when(port.getContextValue(Boolean.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING)).thenReturn(false);
+ when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_WARN_THRESHOLD))
+ .thenReturn(1000);
+ when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD))
+ .thenReturn(10000);
+ final NonBlockingConnectionTLSDelegate delegate = new
NonBlockingConnectionTLSDelegate(parent, port);
+ try (final QpidByteBuffer application = QpidByteBuffer.wrap(new
byte[128]))
+ {
+ final List<QpidByteBuffer> buffers = List.of(application);
+ assertFalse(delegate.doWrite(buffers).isComplete());
+ assertFalse(delegate.needsReadForWrite(), "Buffered handshake
output must still select for writing");
+
+ when(parent.writeToTransport(any())).thenAnswer(invocation ->
+ {
+ long written = 0;
+ final Collection<QpidByteBuffer> encrypted =
invocation.getArgument(0);
+ for (final QpidByteBuffer buffer : encrypted)
+ {
+ written += buffer.remaining();
+ buffer.position(buffer.limit());
+ }
+ return written;
+ });
+
+ assertFalse(delegate.doWrite(buffers).isComplete());
+ assertTrue(application.hasRemaining(), "Application bytes must
wait for the peer handshake response");
+ assertTrue(delegate.needsReadForWrite(), "A writable socket cannot
advance NEED_UNWRAP");
+ }
+ finally
+ {
+ delegate.shutdownInput();
+ delegate.shutdownOutput();
+ }
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSTest.java
new file mode 100644
index 0000000000..303bd7f7ae
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTLSTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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.io.ByteArrayOutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
+
+import org.junit.jupiter.api.BeforeAll;
+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 NonBlockingConnectionTLSTest extends UnitTestBase
+{
+ private static final int NETWORK_BUFFER_SIZE = 64 * 1024;
+ private SSLContext _context;
+
+ @BeforeAll
+ void createTlsContext() throws Exception
+ {
+ _context = TlsTestContext.create();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"TLSv1.2", "TLSv1.3"})
+ void
handshakeWaitsForEncryptedOutputBeforePeerInputAndRestoresWriteInterest(final
String protocol) throws Exception
+ {
+ try (final TlsConnection tls = new TlsConnection(protocol))
+ {
+ final byte[] payload = new byte[]{1, 2, 3};
+ tls.send(payload);
+ tls._writeAllowance = 0;
+ tls.wrapPeer(ByteBuffer.allocate(0));
+ tls._connection.doWork();
+
+ assertTrue(tls._connection.wantsWrite(), "The blocked server
handshake must select for writing");
+ tls._connection.doWork();
+ assertFalse(tls._connection.isStateChanged(), "Blocked encrypted
output must not cause a busy loop");
+ assertFalse(tls._connection.isPartialRead());
+
+ tls._writeAllowance = 1;
+ tls._connection.doWork();
+ assertEquals(1, tls._toPeer.position(), "Only the explicitly
granted write capacity may be consumed");
+ assertTrue(tls._connection.wantsWrite(), "A partial handshake
write must retain write interest");
+
+ tls._writeAllowance = Integer.MAX_VALUE;
+ tls._connection.doWork();
+ assertEquals(SSLEngineResult.HandshakeStatus.NEED_UNWRAP,
tls._server.getHandshakeStatus());
+ assertTrue(tls._connection.wantsRead());
+ assertFalse(tls._connection.wantsWrite(), "Pending application
output must wait for the peer handshake");
+ assertFalse(tls._connection.isStateChanged());
+
+ tls.finishHandshake();
+ assertArrayEquals(payload, tls._peerReceived.toByteArray());
+ tls._peerReceived.reset();
+
+ tls._writeAllowance = 0;
+ tls.send(payload);
+ tls._connection.doWork();
+ assertTrue(tls._connection.wantsWrite(), "NEED_UNWRAP must not
remain cached after the handshake");
+ assertFalse(tls._connection.isStateChanged());
+
+ tls._writeAllowance = Integer.MAX_VALUE;
+ tls._connection.doWork();
+ tls.readAtPeer();
+ assertArrayEquals(payload, tls._peerReceived.toByteArray());
+ assertFalse(tls._connection.wantsWrite());
+ assertFalse(tls._blocked);
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({"TLSv1.2, false", "TLSv1.2, true", "TLSv1.3, false", "TLSv1.3,
true"})
+ void applicationReadRequestsContinuationOnlyWhenOutputIsDrained(final
String protocol,
+ final
boolean blocked) throws Exception
+ {
+ try (final TlsConnection tls = new TlsConnection(protocol))
+ {
+ tls.finishHandshake();
+ final byte[] outbound = new byte[]{11, 12, 13};
+ final byte[] inbound = new byte[]{21, 22, 23};
+ tls._writeAllowance = blocked ? 0 : Integer.MAX_VALUE;
+ tls.send(outbound);
+ tls.wrapPeer(ByteBuffer.wrap(inbound));
+
+ tls._connection.doWork();
+
+ assertArrayEquals(inbound, tls._received.toByteArray(), "The real
TLS engine must decrypt peer input");
+ assertEquals(blocked, tls._connection.wantsWrite());
+ assertEquals(!blocked, tls._connection.isStateChanged(),
+ "Decrypted input must request a continuation only when
encrypted output has drained");
+
+ tls._connection.doWork();
+ assertFalse(tls._connection.isStateChanged(), "A pass without new
input must not keep rescheduling");
+ assertFalse(tls._connection.isPartialRead());
+ tls._writeAllowance = Integer.MAX_VALUE;
+ tls._connection.doWork();
+ tls.readAtPeer();
+ assertArrayEquals(outbound, tls._peerReceived.toByteArray());
+ assertFalse(tls._connection.wantsWrite());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"TLSv1.2", "TLSv1.3"})
+ void bufferedTlsInputRequestsContinuationAfterOutputDrains(final String
protocol) throws Exception
+ {
+ try (final TlsConnection tls = new TlsConnection(protocol))
+ {
+ tls.finishHandshake();
+ final byte[] inbound = new byte[]{31, 32, 33};
+ final byte[] outbound = new byte[]{41, 42, 43};
+ tls.wrapPeer(ByteBuffer.wrap(inbound));
+ tls._readAllowance = 5;
+ tls._writeAllowance = 0;
+ tls.send(outbound);
+ tls._connection.doWork();
+
+ assertEquals(0, tls._received.size());
+ assertTrue(tls._connection.wantsWrite());
+ assertFalse(tls._connection.isStateChanged(), "Buffered TLS input
must not spin behind a blocked write");
+ tls._connection.doWork();
+ assertFalse(tls._connection.isStateChanged());
+ assertFalse(tls._connection.isPartialRead());
+
+ tls._writeAllowance = Integer.MAX_VALUE;
+ tls._connection.doWork();
+ assertFalse(tls._connection.wantsWrite());
+ assertFalse(tls._connection.isPartialRead(), "No new socket bytes
were supplied during the drain");
+ assertTrue(tls._connection.isStateChanged(),
+ "Retained TLS input must request a continuation after
draining");
+
+ tls._readAllowance = Integer.MAX_VALUE;
+ tls._connection.doWork();
+ assertArrayEquals(inbound, tls._received.toByteArray());
+ tls._connection.doWork();
+ assertFalse(tls._connection.isStateChanged(), "Consuming the
buffered record must end the continuation");
+ tls.readAtPeer();
+ assertArrayEquals(outbound, tls._peerReceived.toByteArray());
+ }
+ }
+
+ /** Real TLS engines with explicitly granted socket capacity; all work
runs on the test thread */
+ private final class TlsConnection implements AutoCloseable
+ {
+ private final SSLEngine _server = _context.createSSLEngine();
+ private final SSLEngine _peer = _context.createSSLEngine();
+ private final ByteBuffer _fromPeer = ByteBuffer.allocate(4 *
NETWORK_BUFFER_SIZE);
+ private final ByteBuffer _toPeer = ByteBuffer.allocate(4 *
NETWORK_BUFFER_SIZE);
+ private final ByteArrayOutputStream _received = new
ByteArrayOutputStream();
+ private final ByteArrayOutputStream _peerReceived = new
ByteArrayOutputStream();
+ private final NonBlockingConnection _connection;
+ private int _writeAllowance = Integer.MAX_VALUE;
+ private int _readAllowance = Integer.MAX_VALUE;
+ private boolean _hasWork;
+ private boolean _blocked;
+
+ private TlsConnection(final String protocol) throws Exception
+ {
+ _peer.setUseClientMode(true);
+ _peer.setEnabledProtocols(new String[]{protocol});
+ _peer.beginHandshake();
+ final Socket socket = mock(Socket.class);
+ when(socket.getRemoteSocketAddress()).thenReturn(new
InetSocketAddress("localhost", 1000));
+ when(socket.getLocalSocketAddress()).thenReturn(new
InetSocketAddress("localhost", 5672));
+ final SocketChannel channel = mock(SocketChannel.class);
+ when(channel.socket()).thenReturn(socket);
+ when(channel.read(any(ByteBuffer.class))).thenAnswer(invocation ->
+ {
+ _fromPeer.flip();
+ final int read = transfer(_fromPeer,
invocation.getArgument(0), _readAllowance);
+ _fromPeer.compact();
+ _readAllowance -= read;
+ return read;
+ });
+ when(channel.write(any(ByteBuffer[].class))).thenAnswer(invocation
->
+ {
+ long written = 0;
+ for (final ByteBuffer buffer : (ByteBuffer[])
invocation.getArgument(0))
+ {
+ final int count = transfer(buffer, _toPeer,
_writeAllowance);
+ written += count;
+ _writeAllowance -= count;
+ }
+ return written;
+ });
+ final ProtocolEngine engine = mock(ProtocolEngine.class);
+ when(engine.getAggregateTicker()).thenReturn(new
AggregateTicker());
+ when(engine.processPendingIterator()).thenAnswer(invocation ->
Collections.emptyIterator());
+ when(engine.hasWork()).thenAnswer(invocation -> _hasWork);
+ doAnswer(invocation ->
+ {
+ _hasWork = false; return null;
+ }).when(engine).clearWork();
+ doAnswer(invocation ->
+ {
+ _hasWork = true; return null;
+ }).when(engine).notifyWork();
+ doAnswer(invocation ->
+ {
+ _blocked = invocation.getArgument(0); return null;
+ }).when(engine).setTransportBlockedForWriting(anyBoolean());
+ doAnswer(invocation ->
+ {
+ final QpidByteBuffer input = invocation.getArgument(0);
+ final byte[] bytes = new byte[input.remaining()];
+ input.get(bytes);
+ _received.writeBytes(bytes);
+ return null;
+ }).when(engine).received(any());
+ final SSLContext serverContext = mock(SSLContext.class);
+ when(serverContext.createSSLEngine()).thenReturn(_server);
+ final AmqpPort<?> port = mock(AmqpPort.class);
+ when(port.getNetworkBufferSize()).thenReturn(NETWORK_BUFFER_SIZE);
+ when(port.getSSLContext()).thenReturn(serverContext);
+ when(port.getTlsProtocolAllowList()).thenReturn(List.of(protocol));
+ when(port.getContextValue(Boolean.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING)).thenReturn(false);
+ when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_WARN_THRESHOLD))
+ .thenReturn(1000);
+ when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD))
+ .thenReturn(10000);
+ 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();
+ final Set<TransportEncryption> encryptionSet =
Set.of(TransportEncryption.TLS);
+ _connection = new NonBlockingConnection(channel, engine,
encryptionSet, () -> { }, mock(), port);
+
_connection.setSelectionTask(mock(SelectorThread.SelectionTask.class));
+ }
+
+ private void finishHandshake() throws Exception
+ {
+ for (int step = 0; step < 100; step++)
+ {
+ readAtPeer();
+ if (_peer.getHandshakeStatus() ==
SSLEngineResult.HandshakeStatus.NEED_WRAP)
+ {
+ wrapPeer(ByteBuffer.allocate(0));
+ }
+ _connection.doWork();
+ if (_peer.getHandshakeStatus() ==
SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING &&
+ _server.getHandshakeStatus() ==
SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING &&
+ _fromPeer.position() == 0 && _toPeer.position() == 0)
+ {
+ assertEquals(_peer.getSession().getProtocol(),
_server.getSession().getProtocol());
+ assertFalse(_connection.wantsWrite());
+ return;
+ }
+ }
+ fail("TLS handshake did not complete: peer=" +
_peer.getHandshakeStatus() + ", server=" +
+ _server.getHandshakeStatus());
+ }
+
+ private void wrapPeer(final ByteBuffer application) throws Exception
+ {
+ do
+ {
+ final SSLEngineResult result = _peer.wrap(application,
_fromPeer);
+ assertEquals(SSLEngineResult.Status.OK, result.getStatus());
+ runPeerTasks();
+ assertTrue(result.bytesConsumed() > 0 ||
result.bytesProduced() > 0,
+ "Peer wrap made no progress");
+ }
+ while (application.hasRemaining() || _peer.getHandshakeStatus() ==
SSLEngineResult.HandshakeStatus.NEED_WRAP);
+ }
+
+ private void readAtPeer() throws Exception
+ {
+ _toPeer.flip();
+ while (_toPeer.hasRemaining())
+ {
+ final ByteBuffer application =
ByteBuffer.allocate(NETWORK_BUFFER_SIZE);
+ final SSLEngineResult result = _peer.unwrap(_toPeer,
application);
+ runPeerTasks();
+ _peerReceived.write(application.array(), 0,
application.position());
+ if (result.getStatus() ==
SSLEngineResult.Status.BUFFER_UNDERFLOW ||
+ _peer.getHandshakeStatus() ==
SSLEngineResult.HandshakeStatus.NEED_WRAP)
+ {
+ break;
+ }
+ assertEquals(SSLEngineResult.Status.OK, result.getStatus());
+ assertTrue(result.bytesConsumed() > 0 ||
result.bytesProduced() > 0, "Peer unwrap made no progress");
+ }
+ _toPeer.compact();
+ }
+
+ private void runPeerTasks()
+ {
+ Runnable task;
+ while ((task = _peer.getDelegatedTask()) != null)
+ {
+ task.run();
+ }
+ }
+
+ private void send(final byte[] bytes)
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.wrap(bytes))
+ {
+ _connection.send(buffer);
+ }
+ }
+
+ @Override
+ public void close()
+ {
+ _writeAllowance = Integer.MAX_VALUE;
+ _connection.close();
+ _connection.doWork();
+ _peer.closeOutbound();
+ }
+ }
+
+ private static int transfer(final ByteBuffer source, final ByteBuffer
destination, final int allowance)
+ {
+ final int length = Math.min(allowance, Math.min(source.remaining(),
destination.remaining()));
+ final ByteBuffer part = source.duplicate();
+ part.limit(part.position() + length);
+ destination.put(part);
+ source.position(source.position() + length);
+ return length;
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionWriteTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionWriteTest.java
new file mode 100644
index 0000000000..e24e9c31dc
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionWriteTest.java
@@ -0,0 +1,508 @@
+/*
+ * 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
+ {
+ final AtomicInteger completed = new AtomicInteger();
+ final AtomicInteger received = new AtomicInteger();
+ final List<Runnable> tasks = new ArrayList<>();
+ tasks.add(() -> send(remainingOutput + NETWORK_BUFFER_SIZE / 4));
+ if (pendingTask)
+ {
+ tasks.add(completed::incrementAndGet);
+ }
+ when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+ _writableBytes.set(NETWORK_BUFFER_SIZE / 4);
+ when(_channel.read(any(ByteBuffer.class))).thenAnswer(invocation ->
+ {
+ final ByteBuffer input = invocation.getArgument(0);
+ input.put((byte) 42);
+ return 1;
+ }).thenReturn(0);
+ doAnswer(invocation ->
+ {
+ final QpidByteBuffer input = invocation.getArgument(0);
+ assertEquals(42, input.get());
+ received.incrementAndGet();
+ return null;
+ }).when(_engine).received(any());
+
+ _connection.doWork();
+
+ assertEquals(0, completed.get());
+ assertEquals(0, received.get());
+ assertEquals(0, _writableBytes.get());
+ assertFalse(_connection.isPartialRead());
+ assertTrue(_connection.isStateChanged(), "Freed buffer space must
schedule a continuation");
+ assertTrue(_connection.wantsWrite(), "The partial write must leave
output pending");
+ assertEquals(List.of(true), _blockedTransitions);
+
+ _connection.doWork();
+
+ assertEquals(pendingTask ? 1 : 0, completed.get());
+ assertEquals(1, received.get(), "Incoming data must be read without
further write capacity");
+ assertTrue(_connection.wantsRead(), "Completing the iterator must
restore read interest");
+ assertTrue(_connection.wantsWrite());
+ assertTrue(_connection.isPartialRead());
+ assertFalse(_connection.isStateChanged());
+ assertEquals(List.of(true), _blockedTransitions, "Consumers must
remain blocked until output drains");
+
+ _connection.doWork();
+
+ assertEquals(pendingTask ? 1 : 0, completed.get(), "Pending work must
run exactly once");
+ assertEquals(1, received.get());
+ assertTrue(_connection.wantsRead());
+ assertTrue(_connection.wantsWrite());
+ assertFalse(_connection.isPartialRead());
+ assertFalse(_connection.isStateChanged(), "The connection must return
to selector waiting");
+ assertEquals(List.of(true), _blockedTransitions);
+ }
+
+ @ParameterizedTest
+ @CsvSource({"false, 1024", "true, 1024", "false, 1025", "true, 1025"})
+ void partialWriteAtOrAboveBufferLimitWaitsForSocketReadiness(final boolean
pendingTask,
+ final int
remainingOutput) throws Exception
+ {
+ final AtomicInteger completed = new AtomicInteger();
+ final List<Runnable> tasks = new ArrayList<>();
+ tasks.add(() -> send(remainingOutput + NETWORK_BUFFER_SIZE / 4));
+ if (pendingTask)
+ {
+ tasks.add(completed::incrementAndGet);
+ }
+ when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+ _writableBytes.set(NETWORK_BUFFER_SIZE / 4);
+
+ _connection.doWork();
+
+ assertEquals(0, _writableBytes.get());
+ assertEquals(0, completed.get());
+ assertFalse(_connection.isStateChanged(), "A full buffer must wait for
socket write readiness");
+ assertFalse(_connection.isPartialRead());
+ assertFalse(_connection.wantsRead());
+ assertTrue(_connection.wantsWrite());
+
+ _engine.notifyWork();
+ _connection.doWork();
+
+ assertEquals(0, completed.get());
+ assertFalse(_connection.isStateChanged(), "Retrying with no write
progress must not cause a busy loop");
+ assertFalse(_connection.isPartialRead());
+ assertFalse(_connection.wantsRead());
+ assertTrue(_connection.wantsWrite());
+ assertEquals(List.of(true), _blockedTransitions);
+ verify(_channel, never()).read(any(ByteBuffer.class));
+ }
+
+ @Test
+ void resumedIteratorWaitsAgainWhenOutputReachesBufferLimit()
+ {
+ final AtomicInteger completed = new AtomicInteger();
+ final List<Runnable> tasks = List.of(() -> send(NETWORK_BUFFER_SIZE),
() ->
+ send(NETWORK_BUFFER_SIZE / 4), completed::incrementAndGet);
+ when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+ _writableBytes.set(NETWORK_BUFFER_SIZE / 4);
+
+ _connection.doWork();
+
+ assertTrue(_connection.isStateChanged(), "The partial write must allow
pending work to resume");
+
+ _connection.doWork();
+
+ assertEquals(0, completed.get(), "Refilling the buffer must suspend
the remaining task");
+ assertFalse(_connection.isStateChanged());
+ assertFalse(_connection.isPartialRead());
+ assertFalse(_connection.wantsRead());
+ assertTrue(_connection.wantsWrite());
+ assertEquals(List.of(true), _blockedTransitions);
+
+ _writableBytes.set(NETWORK_BUFFER_SIZE);
+ _connection.doWork();
+
+ assertEquals(1, completed.get(), "Write readiness must eventually
resume the remaining task");
+ assertFalse(_connection.wantsWrite());
+ assertEquals(List.of(true, false), _blockedTransitions);
+ }
+
+ @Test
+ void blockedPendingOutputClearsPreviousReadIndication() throws Exception
+ {
+ when(_channel.read(any(ByteBuffer.class))).thenReturn(1);
+ _connection.doWork();
+ assertTrue(_connection.isPartialRead());
+ final List<Runnable> tasks = List.of(() -> send(NETWORK_BUFFER_SIZE),
() -> { });
+ when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+
+ _connection.doWork();
+
+ assertTrue(_connection.wantsWrite());
+ assertFalse(_connection.isPartialRead(), "Skipping reads must not
reuse the preceding pass's read indication");
+ assertFalse(_connection.isStateChanged());
+ }
+
+ @Test
+ void explicitProtocolWorkRunsWhileOutputIsBlocked()
+ {
+ send(128);
+ _connection.doWork();
+ final AtomicInteger completed = new AtomicInteger();
+
when(_engine.processPendingIterator()).thenReturn(List.<Runnable>of(completed::incrementAndGet).iterator());
+ _engine.notifyWork();
+
+ _connection.doWork();
+
+ assertEquals(1, completed.get());
+ assertEquals(List.of(true), _blockedTransitions);
+ assertFalse(_connection.isStateChanged());
+ }
+
+ @Test
+ void closeDeadlineStillRunsWithBlockedOutput() throws Exception
+ {
+ send(NETWORK_BUFFER_SIZE);
+ _connection.doWork();
+ final AggregateTicker ticker = _engine.getAggregateTicker();
+ when(ticker.getTimeToNextTick(anyLong())).thenReturn(0);
+ when(ticker.tick(anyLong())).thenAnswer(invocation ->
+ {
+ _connection.close();
+ return Integer.MAX_VALUE;
+ });
+
+ assertTrue(_connection.doWork());
+
+ verify(_engine).closed();
+ verify(_channel).close();
+ }
+
+ @Test
+ void pendingTlsWrapDoesNotSelectForReading() throws Exception
+ {
+ final NonBlockingConnectionDelegate delegate =
mock(NonBlockingConnectionDelegate.class);
+ when(delegate.readyForRead()).thenReturn(false);
+ when(delegate.doWrite(any())).thenReturn(new
NonBlockingConnectionDelegate.WriteResult(false, 0));
+ final Field field =
NonBlockingConnection.class.getDeclaredField("_delegate");
+ field.setAccessible(true);
+ ((NonBlockingConnectionDelegate)
field.get(_connection)).shutdownInput();
+ field.set(_connection, delegate);
+
+ send(128);
+ _connection.doWork();
+
+ assertTrue(_connection.wantsWrite());
+ assertFalse(_connection.wantsRead(), "TLS NEED_WRAP cannot make
progress from read readiness");
+ assertFalse(_connection.isStateChanged(), "Pending TLS output must not
reschedule the connection");
+
+ when(delegate.readyForRead()).thenReturn(true);
+ assertTrue(_connection.wantsRead(), "Read interest must return when
TLS can process incoming data");
+ }
+
+ @Test
+ void pendingOutputCanWaitForTlsInput() throws Exception
+ {
+ final AtomicBoolean inputProcessed = new AtomicBoolean();
+ final AtomicInteger completed = new AtomicInteger();
+ final NonBlockingConnectionDelegate delegate =
mock(NonBlockingConnectionDelegate.class);
+ when(delegate.needsReadForWrite()).thenAnswer(invocation ->
!inputProcessed.get());
+ when(delegate.readyForRead()).thenReturn(true);
+ when(delegate.processData()).thenAnswer(invocation ->
+ {
+ inputProcessed.set(true);
+ return false;
+ });
+ when(delegate.doWrite(any())).thenAnswer(invocation ->
+ {
+ long consumed = 0;
+ if (inputProcessed.get())
+ {
+ final Collection<QpidByteBuffer> buffers =
invocation.getArgument(0);
+ for (final QpidByteBuffer buffer : buffers)
+ {
+ consumed += buffer.remaining();
+ buffer.position(buffer.limit());
+ }
+ }
+ return new
NonBlockingConnectionDelegate.WriteResult(inputProcessed.get(), consumed);
+ });
+ final Field field =
NonBlockingConnection.class.getDeclaredField("_delegate");
+ field.setAccessible(true);
+ ((NonBlockingConnectionDelegate)
field.get(_connection)).shutdownInput();
+ field.set(_connection, delegate);
+ try (final QpidByteBuffer input =
QpidByteBuffer.allocate(NETWORK_BUFFER_SIZE))
+ {
+ when(delegate.getNetInputBuffer()).thenReturn(input);
+ final List<Runnable> tasks = List.of(() ->
send(NETWORK_BUFFER_SIZE), completed::incrementAndGet);
+
when(_engine.processPendingIterator()).thenReturn(tasks.iterator());
+
+ _connection.doWork();
+
+ assertTrue(_connection.wantsRead(), "TLS must read the peer's
response with a suspended iterator");
+ assertFalse(_connection.wantsWrite(), "Writable socket readiness
cannot satisfy a TLS input wait");
+ assertFalse(_connection.isStateChanged());
+ assertEquals(0, completed.get());
+
+ when(_channel.read(any(ByteBuffer.class))).thenReturn(1, 0);
+ _connection.doWork();
+ assertTrue(inputProcessed.get());
+ _connection.doWork();
+
+ assertEquals(1, completed.get(), "The TLS input wait must not
strand pending protocol work");
+ assertFalse(_connection.wantsWrite());
+ assertEquals(List.of(true, false), _blockedTransitions);
+ }
+ }
+
+ private void send(final int size)
+ {
+ try (final QpidByteBuffer buffer = QpidByteBuffer.wrap(new byte[size]))
+ {
+ _connection.send(buffer);
+ }
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/TlsTestContext.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/TlsTestContext.java
new file mode 100644
index 0000000000..47d47ee729
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/TlsTestContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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()
+ {
+ // utility class has private constructor
+ }
+
+ static SSLContext create() throws Exception
+ {
+ final KeyCertificatePair certificate =
TlsResourceBuilder.createSelfSigned("CN=localhost");
+ final char[] password = "test-password".toCharArray();
+ final KeyStore keys = KeyStore.getInstance("PKCS12");
+ keys.load(null, password);
+ keys.setKeyEntry("server", certificate.privateKey(), password, new
Certificate[]{certificate.certificate()});
+ final KeyManagerFactory keyManagers =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagers.init(keys, password);
+ final TrustManagerFactory trustManagers = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagers.init(keys);
+ final SSLContext context = SSLContext.getInstance("TLS");
+ context.init(keyManagers.getKeyManagers(),
trustManagers.getTrustManagers(), null);
+ return context;
+ }
+}
diff --git
a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10.java
b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10.java
index eca3192e26..eaaab58a5c 100644
---
a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10.java
+++
b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10.java
@@ -75,6 +75,7 @@ public class ConsumerTarget_0_10 extends
AbstractConsumerTarget<ConsumerTarget_0
private volatile FlowCreditManager_0_10 _creditManager;
private int _deferredMessageCredit;
private long _deferredSizeCredit;
+ private boolean _flushing;
private final StateChangeListener<MessageInstance, EntryState>
_unacknowledgedMessageListener =
new StateChangeListener<>()
@@ -129,8 +130,8 @@ public class ConsumerTarget_0_10 extends
AbstractConsumerTarget<ConsumerTarget_0
{
final AMQPConnection_0_10 amqpConnection =
_session.getAMQPConnection();
- boolean state = !amqpConnection.isTransportBlockedForWriting()
- && getCreditManager().hasCredit();
+ final boolean state = (_flushing ||
!amqpConnection.isTransportBlockedForWriting()) &&
+ getCreditManager().hasCredit();
setNotifyWorkDesired(state);
}
@@ -564,9 +565,19 @@ public class ConsumerTarget_0_10 extends
AbstractConsumerTarget<ConsumerTarget_0
public void flush()
{
- flushCreditState(true);
- while(sendNextMessage());
- stop();
+ _flushing = true;
+ try
+ {
+ flushCreditState(true);
+ updateNotifyWorkDesired();
+ while (sendNextMessage());
+ stop();
+ }
+ finally
+ {
+ _flushing = false;
+ updateNotifyWorkDesired();
+ }
}
@Override
diff --git
a/broker-plugins/amqp-0-10-protocol/src/test/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10FlushTest.java
b/broker-plugins/amqp-0-10-protocol/src/test/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10FlushTest.java
new file mode 100644
index 0000000000..2ed00ec2f9
--- /dev/null
+++
b/broker-plugins/amqp-0-10-protocol/src/test/java/org/apache/qpid/server/protocol/v0_10/ConsumerTarget_0_10FlushTest.java
@@ -0,0 +1,246 @@
+/*
+ *
+ * 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.protocol.v0_10;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+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.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.InOrder;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.consumer.ConsumerOption;
+import org.apache.qpid.server.model.BrokerTestHelper;
+import org.apache.qpid.server.model.Consumer;
+import org.apache.qpid.server.model.Queue;
+import org.apache.qpid.server.protocol.v0_10.transport.DeliveryProperties;
+import org.apache.qpid.server.protocol.v0_10.transport.Header;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageAcceptMode;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageAcquireMode;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageCreditUnit;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageFlowMode;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageFlush;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageProperties;
+import org.apache.qpid.server.protocol.v0_10.transport.MessageTransfer;
+import org.apache.qpid.server.protocol.v0_10.transport.Option;
+import org.apache.qpid.server.store.StoredMemoryMessage;
+import org.apache.qpid.server.transport.AggregateTicker;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ConsumerTarget_0_10FlushTest extends UnitTestBase
+{
+ private static final int MESSAGE_SIZE = 10;
+
+ private final List<MessageTransfer> _transfers = new ArrayList<>();
+
+ private QueueManagingVirtualHost<?> _host;
+ private Queue<?> _queue;
+ private AMQPConnection_0_10 _connection;
+ private ServerSession _session;
+ private ConsumerTarget_0_10 _target;
+
+ @BeforeEach
+ void setUp() throws Exception
+ {
+ _transfers.clear();
+ _target = null;
+ _host = BrokerTestHelper.createVirtualHost(getTestName(), this);
+ _queue = _host.createChild(Queue.class, Map.of(Queue.NAME, "queue"));
+ _connection = mock(AMQPConnection_0_10.class);
+ when(_connection.isIOThread()).thenReturn(true);
+ when(_connection.getAggregateTicker()).thenReturn(new
AggregateTicker());
+ when(_connection.getContextValue(Long.class,
Consumer.SUSPEND_NOTIFICATION_PERIOD)).thenReturn(0L);
+ doAnswer(invocation ->
+ {
+ ((Runnable) invocation.getArgument(0)).run();
+ return CompletableFuture.completedFuture(null);
+ }).when(_connection).doOnIOThreadAsync(any(Runnable.class));
+
+ _session = mock(ServerSession.class);
+ when(_session.getAMQPConnection()).thenReturn(_connection);
+ doReturn(_host).when(_session).getAddressSpace();
+ final Session_0_10 model = mock(Session_0_10.class);
+ when(model.getAMQPConnection()).thenReturn(_connection);
+ when(_session.getModelObject()).thenReturn(model);
+ final ServerConnection connection = mock(ServerConnection.class);
+
when(connection.getConnectionDelegate()).thenReturn(mock(ServerConnectionDelegate.class));
+ when(_session.getConnection()).thenReturn(connection);
+ doAnswer(invocation ->
+ {
+ _transfers.add(invocation.getArgument(0));
+ return null;
+ }).when(_session).sendMessage(any(MessageTransfer.class),
any(Runnable.class));
+ }
+
+ @AfterEach
+ void tearDown()
+ {
+ try
+ {
+ if (_target != null)
+ {
+ _target.close();
+ }
+ }
+ finally
+ {
+ _host.close();
+ }
+ }
+
+ @ParameterizedTest(name = "{0}, blocked={1}, queued={2}, bytes={3},
messages={4}, delivered={5}")
+ @MethodSource("flushCases")
+ void flushDeliversAvailableMessagesWithinCredit(final MessageFlowMode
flowMode,
+ final boolean
transportBlocked,
+ final int queued,
+ final long byteCredit,
+ final long messageCredit,
+ final int
expectedDeliveries) throws Exception
+ {
+
when(_connection.isTransportBlockedForWriting()).thenReturn(transportBlocked);
+ createTarget(flowMode, byteCredit, messageCredit);
+ enqueueMessages(queued);
+ final MessageFlush flush = new MessageFlush("consumer", Option.SYNC);
+
+ new ServerSessionDelegate().command(_session, flush);
+
+ assertEquals(expectedDeliveries, _transfers.size());
+ assertEquals(queued - expectedDeliveries,
_queue.getAvailableMessages());
+ assertFalse(_target.getCreditManager().hasCredit(), "Flush must clear
remaining credit");
+ assertTrue(_target.isSuspended());
+ final InOrder order = inOrder(_session);
+ if (expectedDeliveries > 0)
+ {
+ order.verify(_session,
times(expectedDeliveries)).sendMessage(any(MessageTransfer.class),
+ any(Runnable.class));
+ }
+ else
+ {
+ verify(_session, never()).sendMessage(any(MessageTransfer.class),
any(Runnable.class));
+ }
+ order.verify(_session).processed(flush);
+ order.verify(_session).flushProcessed();
+
+ _target.addCredit(MessageCreditUnit.MESSAGE, 10L);
+ _target.addCredit(MessageCreditUnit.BYTE,
FlowCreditManager_0_10.INFINITE_CREDIT);
+ assertEquals(transportBlocked, _target.isSuspended(),
+ "New credit must still respect transport backpressure after
the flush");
+ verify(_connection,
never()).setTransportBlockedForWriting(anyBoolean());
+ }
+
+ @ParameterizedTest
+ @EnumSource(MessageFlowMode.class)
+ void failedFlushRestoresTransportBackpressure(final MessageFlowMode
flowMode) throws Exception
+ {
+ when(_connection.isTransportBlockedForWriting()).thenReturn(true);
+ createTarget(flowMode, -1L, 2L);
+ enqueueMessages(1);
+ final ConnectionScopedRuntimeException failure = new
ConnectionScopedRuntimeException("Delivery failed");
+
doThrow(failure).when(_session).sendMessage(any(MessageTransfer.class),
any(Runnable.class));
+
+ assertSame(failure,
assertThrows(ConnectionScopedRuntimeException.class, _target::flush));
+
+ assertTrue(_target.getCreditManager().hasCredit());
+ _target.updateNotifyWorkDesired();
+ assertTrue(_target.isSuspended(), "A failed flush must not leave the
consumer ignoring backpressure");
+ when(_connection.isTransportBlockedForWriting()).thenReturn(false);
+ _target.transportStateChanged();
+ assertFalse(_target.isSuspended());
+ verify(_connection,
never()).setTransportBlockedForWriting(anyBoolean());
+ }
+
+ private static Stream<Arguments> flushCases()
+ {
+ final List<Arguments> cases = new ArrayList<>();
+ for (final MessageFlowMode flowMode : MessageFlowMode.values())
+ {
+ for (final boolean blocked : new boolean[]{false, true})
+ {
+ cases.add(Arguments.of(flowMode, blocked, 3, -1L, 2L, 2));
+ cases.add(Arguments.of(flowMode, blocked, 3, 2L *
MESSAGE_SIZE, 3L, 2));
+ cases.add(Arguments.of(flowMode, blocked, 1, -1L, 0L, 0));
+ cases.add(Arguments.of(flowMode, blocked, 1, 0L, 3L, 0));
+ cases.add(Arguments.of(flowMode, blocked, 0, -1L, 2L, 0));
+ cases.add(Arguments.of(flowMode, blocked, 1, -1L, 2L, 1));
+ }
+ }
+ return cases.stream();
+ }
+
+ private void createTarget(final MessageFlowMode flowMode,
+ final long byteCredit,
+ final long messageCredit) throws Exception
+ {
+ final FlowCreditManager_0_10 creditManager = flowMode ==
MessageFlowMode.CREDIT
+ ? new CreditCreditManager(byteCredit, messageCredit)
+ : new WindowCreditManager(byteCredit, messageCredit);
+ _target = new ConsumerTarget_0_10(_session, "consumer",
MessageAcceptMode.EXPLICIT,
+ MessageAcquireMode.PRE_ACQUIRED, flowMode, creditManager,
Map.of(), false);
+ when(_session.getSubscription("consumer")).thenReturn(_target);
+ _queue.addConsumer(_target, null, MessageTransferMessage.class,
"consumer",
+ EnumSet.of(ConsumerOption.ACQUIRES), null);
+ _target.updateNotifyWorkDesired();
+ }
+
+ private void enqueueMessages(final int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ final Header header = new Header(new DeliveryProperties(), new
MessageProperties());
+ final long timeMillis = System.currentTimeMillis();
+ final MessageMetaData_0_10 metaData = new
MessageMetaData_0_10(header, MESSAGE_SIZE, timeMillis);
+ final StoredMemoryMessage<MessageMetaData_0_10> stored = new
StoredMemoryMessage<>(i + 1L, metaData);
+ try (final QpidByteBuffer content = QpidByteBuffer.wrap(new
byte[MESSAGE_SIZE]))
+ {
+ stored.addContent(content);
+ }
+ _queue.enqueue(new MessageTransferMessage(stored, null), null,
null);
+ }
+ }
+}
diff --git
a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ConsumerTarget_0_8.java
b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ConsumerTarget_0_8.java
index b86a1c74e0..feb8346708 100644
---
a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ConsumerTarget_0_8.java
+++
b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/ConsumerTarget_0_8.java
@@ -114,7 +114,18 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
ClientDeliveryMethod deliveryMethod,
boolean multiQueue)
{
- super(channel, consumerTag, filters, creditManager,
deliveryMethod, multiQueue);
+ this(channel, consumerTag, filters, creditManager, deliveryMethod,
multiQueue, false);
+ }
+
+ private NoAckConsumer(final AMQChannel channel,
+ final AMQShortString consumerTag,
+ final FieldTable filters,
+ final FlowCreditManager_0_8 creditManager,
+ final ClientDeliveryMethod deliveryMethod,
+ final boolean multiQueue,
+ final boolean isGet)
+ {
+ super(channel, consumerTag, filters, creditManager,
deliveryMethod, multiQueue, isGet);
_txn = new
AutoCommitTransaction(channel.getAddressSpace().getMessageStore());
}
@@ -172,7 +183,7 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
FlowCreditManager_0_8 creditManager,
ClientDeliveryMethod deliveryMethod)
{
- super(channel, consumerTag, filters, creditManager,
deliveryMethod, false);
+ super(channel, consumerTag, filters, creditManager,
deliveryMethod, false, true);
}
}
@@ -188,7 +199,7 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
consumerTag,
filters, creditManager,
channel.getClientDeliveryMethod(),
- multiQueue, true);
+ multiQueue, true, false);
}
@@ -197,7 +208,7 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
FlowCreditManager_0_8
creditManager,
ClientDeliveryMethod
deliveryMethod)
{
- return new AckConsumer(channel, consumerTag, filters, creditManager,
deliveryMethod, false, false);
+ return new AckConsumer(channel, consumerTag, filters, creditManager,
deliveryMethod, false, false, true);
}
static final class AckConsumer extends ConsumerTarget_0_8
@@ -209,9 +220,10 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
FlowCreditManager_0_8 creditManager,
ClientDeliveryMethod deliveryMethod,
boolean multiQueue,
- final boolean usesCredit)
+ final boolean usesCredit,
+ final boolean isGet)
{
- super(channel, consumerTag, filters, creditManager,
deliveryMethod, multiQueue);
+ super(channel, consumerTag, filters, creditManager,
deliveryMethod, multiQueue, isGet);
_usesCredit = usesCredit;
}
@@ -243,12 +255,25 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
private final Boolean _autoClose;
+ private final boolean _isGet;
+
public ConsumerTarget_0_8(AMQChannel channel,
AMQShortString consumerTag,
FieldTable arguments,
FlowCreditManager_0_8 creditManager,
ClientDeliveryMethod deliveryMethod,
boolean multiQueue)
+ {
+ this(channel, consumerTag, arguments, creditManager, deliveryMethod,
multiQueue, false);
+ }
+
+ private ConsumerTarget_0_8(final AMQChannel channel,
+ final AMQShortString consumerTag,
+ final FieldTable arguments,
+ final FlowCreditManager_0_8 creditManager,
+ final ClientDeliveryMethod deliveryMethod,
+ final boolean multiQueue,
+ final boolean isGet)
{
super(multiQueue, channel.getAMQPConnection());
@@ -258,6 +283,7 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
_creditManager = creditManager;
_deliveryMethod = deliveryMethod;
+ _isGet = isGet;
if (arguments != null)
{
@@ -313,10 +339,11 @@ public abstract class ConsumerTarget_0_8 extends
AbstractConsumerTarget<Consumer
{
final AMQPConnection_0_8 amqpConnection = (AMQPConnection_0_8)
_channel.getAMQPConnection();
- boolean state = _channel.isChannelFlow()
- && !amqpConnection.isTransportBlockedForWriting()
- // this last condition does not need to exist for
browsers and no ack
- && getCreditManager().hasCredit();
+ boolean state = _channel.isChannelFlow() &&
+ // Basic.Get must report queue availability even when its
response will be buffered
+ (_isGet || !amqpConnection.isTransportBlockedForWriting()) &&
+ // this last condition does not need to exist for browsers and
no ack
+ getCreditManager().hasCredit();
setNotifyWorkDesired(state);
}
diff --git
a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
index 608c55d52c..4305b16142 100644
---
a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
+++
b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQChannelTest.java
@@ -20,7 +20,9 @@
*/
package org.apache.qpid.server.protocol.v0_8;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -43,13 +45,17 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.security.Principal;
+import java.util.Map;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import javax.security.auth.Subject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
@@ -63,14 +69,21 @@ import org.apache.qpid.server.message.RoutingResult;
import org.apache.qpid.server.message.ServerMessage;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.BrokerModel;
+import org.apache.qpid.server.model.BrokerTestHelper;
import org.apache.qpid.server.model.Connection;
+import org.apache.qpid.server.model.Consumer;
import org.apache.qpid.server.model.Exchange;
+import org.apache.qpid.server.model.Queue;
import org.apache.qpid.server.model.Session;
import org.apache.qpid.server.model.port.AmqpPort;
import org.apache.qpid.server.protocol.ErrorCodes;
import org.apache.qpid.server.protocol.ProtocolVersion;
import org.apache.qpid.server.protocol.converter.MessageConversionException;
+import org.apache.qpid.server.protocol.v0_8.transport.AMQFrame;
import
org.apache.qpid.server.protocol.v0_8.transport.BasicContentHeaderProperties;
+import org.apache.qpid.server.protocol.v0_8.transport.BasicGetEmptyBody;
+import org.apache.qpid.server.protocol.v0_8.transport.ContentHeaderBody;
+import org.apache.qpid.server.protocol.v0_8.transport.MessagePublishInfo;
import org.apache.qpid.server.protocol.v0_8.transport.MethodRegistry;
import org.apache.qpid.server.security.AccessDeniedException;
import org.apache.qpid.server.security.SubjectExecutionContext;
@@ -82,6 +95,7 @@ import org.apache.qpid.server.store.NullMessageStore;
import org.apache.qpid.server.store.StorableMessageMetaData;
import org.apache.qpid.server.store.StoredMemoryMessage;
import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.transport.AggregateTicker;
import org.apache.qpid.server.txn.LocalTransaction;
import org.apache.qpid.server.util.GZIPUtils.GZIPInflationLimitException;
import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
@@ -154,6 +168,76 @@ class AMQChannelTest extends UnitTestBase
_messageDestination = mock(MessageDestination.class);
}
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void basicGetDeliversAvailableMessageWhileTransportBlocked(final boolean
noAck) throws Exception
+ {
+ try (final ConsumerTestContext context = new ConsumerTestContext())
+ {
+ final AMQMessage message = context.enqueueMessage();
+
when(_amqConnection.isTransportBlockedForWriting()).thenReturn(true);
+
+ context._channel.receiveBasicGet(context._queueName, noAck);
+
+ verify(_amqConnection.getProtocolOutputConverter())
+ .writeGetOk(eq(message), any(InstanceProperties.class),
eq(1), eq(1L), anyInt());
+ verify(_amqConnection, never()).writeFrame(any(AMQFrame.class));
+ verify(_amqConnection,
never()).setTransportBlockedForWriting(anyBoolean());
+ assertEquals(0, context._queue.getAvailableMessages());
+ assertEquals(noAck ? 0 : 1,
context._channel.getUnacknowledgedMessageCount());
+ if (!noAck)
+ {
+ context._channel.receiveBasicAck(1L, false);
+ }
+ assertEquals(0, context._queue.getQueueDepthMessages());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void basicGetReturnsEmptyForEmptyQueueWhileTransportBlocked(final boolean
noAck) throws Exception
+ {
+ try (final ConsumerTestContext context = new ConsumerTestContext())
+ {
+
when(_amqConnection.isTransportBlockedForWriting()).thenReturn(true);
+
+ context._channel.receiveBasicGet(context._queueName, noAck);
+
+ final ArgumentCaptor<AMQFrame> frame =
ArgumentCaptor.forClass(AMQFrame.class);
+ verify(_amqConnection).writeFrame(frame.capture());
+ assertInstanceOf(BasicGetEmptyBody.class,
frame.getValue().getBodyFrame());
+ verifyNoInteractions(_amqConnection.getProtocolOutputConverter());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void subscriptionWaitsForTransportToUnblock(final boolean noAck) throws
Exception
+ {
+ final ClientDeliveryMethod deliveryMethod =
mock(ClientDeliveryMethod.class);
+
when(_amqConnection.createDeliveryMethod(1)).thenReturn(deliveryMethod);
+ try (final ConsumerTestContext context = new ConsumerTestContext())
+ {
+ final AMQMessage message = context.enqueueMessage();
+
when(_amqConnection.isTransportBlockedForWriting()).thenReturn(true);
+ context._channel.receiveBasicConsume(context._queueName,
AMQShortString.valueOf("consumer"),
+ false, noAck, false, true, null);
+
+ assertFalse(context._channel.processPending());
+ assertEquals(0, context._queue.getConsumerCountWithCredit());
+ assertEquals(1, context._queue.getAvailableMessages());
+ verifyNoInteractions(deliveryMethod);
+
+
when(_amqConnection.isTransportBlockedForWriting()).thenReturn(false);
+ context._channel.transportStateChanged();
+ context._channel.processPending();
+
+
verify(deliveryMethod).deliverToClient(any(ConsumerTarget_0_8.class),
eq(message),
+ any(InstanceProperties.class), eq(1L));
+ assertEquals(0, context._queue.getAvailableMessages());
+ }
+ }
+
@Test
void receiveExchangeDeleteWhenIfUsedIsSetAndExchangeHasBindings()
{
@@ -535,4 +619,52 @@ class AMQChannelTest extends UnitTestBase
verify(_amqConnection).closeSessionAsync(channel,
AMQPConnection.CloseReason.RESOURCE_LIMIT,
failure.getMessage());
}
+
+ private final class ConsumerTestContext implements AutoCloseable
+ {
+ private final QueueManagingVirtualHost<?> _host;
+ private final Queue<?> _queue;
+ private final AMQShortString _queueName =
AMQShortString.valueOf("queue");
+ private final AMQChannel _channel;
+
+ private ConsumerTestContext() throws Exception
+ {
+ _host = BrokerTestHelper.createVirtualHost(getTestName(),
AMQChannelTest.this);
+ _queue = _host.createChild(Queue.class, Map.of(Queue.NAME,
_queueName.toString()));
+
when(_virtualHost.getAttainedMessageSource(_queueName.toString())).thenReturn(_queue);
+
when(_virtualHost.getMessageStore()).thenReturn(_host.getMessageStore());
+ when(_amqConnection.isIOThread()).thenReturn(true);
+ when(_amqConnection.getAggregateTicker()).thenReturn(new
AggregateTicker());
+ when(_amqConnection.getContextValue(Long.class,
Consumer.SUSPEND_NOTIFICATION_PERIOD))
+ .thenReturn(Consumer.SUSPEND_NOTIFICATION_PERIOD_DEFAULT);
+ doAnswer(invocation ->
+ {
+ ((Runnable) invocation.getArgument(0)).run();
+ return CompletableFuture.completedFuture(null);
+ }).when(_amqConnection).doOnIOThreadAsync(any(Runnable.class));
+ _channel = new AMQChannel(_amqConnection, 1,
_host.getMessageStore());
+ }
+
+ private AMQMessage enqueueMessage()
+ {
+ final ContentHeaderBody contentHeaderBody = new
ContentHeaderBody(new BasicContentHeaderProperties());
+ final MessageMetaData metaData = new MessageMetaData(new
MessagePublishInfo(), contentHeaderBody);
+ final AMQMessage message = new AMQMessage(new
StoredMemoryMessage<>(1L, metaData));
+ _queue.enqueue(message, null, null);
+ return message;
+ }
+
+ @Override
+ public void close()
+ {
+ try
+ {
+ _channel.close();
+ }
+ finally
+ {
+ _host.close();
+ }
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]