This is an automated email from the ASF dual-hosted git repository.
vavrtom 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 d65a81b813 QPID-8749: [Broker-J] AMQP connections may not close while
processing pending writes (#423)
d65a81b813 is described below
commit d65a81b8130b729baaa1cd9a3f2bb09e3cdc5447
Author: Daniil Kirilyuk <[email protected]>
AuthorDate: Fri Jul 31 13:44:52 2026 +0200
QPID-8749: [Broker-J] AMQP connections may not close while processing
pending writes (#423)
---
.../qpid/server/bytebuffer/QpidByteBuffer.java | 7 +
.../server/bytebuffer/QpidByteBufferFactory.java | 46 +++-
.../apache/qpid/server/model/port/AmqpPort.java | 8 +
.../qpid/server/model/port/AmqpPortImpl.java | 32 ++-
.../server/transport/NonBlockingConnection.java | 47 ++--
.../NonBlockingConnectionPlainDelegate.java | 28 +--
.../qpid/server/bytebuffer/QpidByteBufferTest.java | 109 +++++++++
.../qpid/server/model/port/AmqpPortImplTest.java | 59 +++++
.../NonBlockingConnectionIdleTimeoutTest.java | 249 +++++++++++++++++++++
.../NonBlockingConnectionPlainDelegateTest.java | 112 +++++++++
.../transport/NonBlockingConnectionTest.java | 1 +
.../server/transport/TCPandSSLTransportTest.java | 1 +
12 files changed, 654 insertions(+), 45 deletions(-)
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBuffer.java
b/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBuffer.java
index eb49b56f2e..92965f248f 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBuffer.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBuffer.java
@@ -84,6 +84,13 @@ public interface QpidByteBuffer extends AutoCloseable
return QpidByteBufferFactory.write(channel, qpidByteBuffers);
}
+ static long write(final GatheringByteChannel channel,
+ final Collection<QpidByteBuffer> qpidByteBuffers,
+ final int maxBuffers) throws IOException
+ {
+ return QpidByteBufferFactory.write(channel, qpidByteBuffers,
maxBuffers);
+ }
+
static QpidByteBuffer wrap(ByteBuffer wrap)
{
return QpidByteBufferFactory.wrap(wrap);
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBufferFactory.java
b/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBufferFactory.java
index fd74860abf..e4fc3a84d7 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBufferFactory.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/bytebuffer/QpidByteBufferFactory.java
@@ -265,15 +265,49 @@ final class QpidByteBufferFactory
}
}
- static long write(GatheringByteChannel channel, Collection<QpidByteBuffer>
qpidByteBuffers)
- throws IOException
+ static long write(final GatheringByteChannel channel, final
Collection<QpidByteBuffer> qpidByteBuffers) throws IOException
{
- List<ByteBuffer> byteBuffers = new ArrayList<>();
- for (QpidByteBuffer qpidByteBuffer : qpidByteBuffers)
+ return write(channel, qpidByteBuffers, Integer.MAX_VALUE);
+ }
+
+ static long write(final GatheringByteChannel channel,
+ final Collection<QpidByteBuffer> qpidByteBuffers,
+ final int maxBuffers) throws IOException
+ {
+ if (maxBuffers <= 0)
{
- Collections.addAll(byteBuffers,
getUnderlyingBuffers(qpidByteBuffer));
+ throw new IllegalArgumentException("Maximum number of buffers must
be greater than zero");
}
- return channel.write(byteBuffers.toArray(new
ByteBuffer[byteBuffers.size()]));
+
+ final List<ByteBuffer> byteBuffers = new ArrayList<>();
+
+ outer: for (final QpidByteBuffer qpidByteBuffer : qpidByteBuffers)
+ {
+ if (qpidByteBuffer instanceof MultiQpidByteBuffer
multiQpidByteBuffer)
+ {
+ for (final SingleQpidByteBuffer fragment :
multiQpidByteBuffer.getFragments())
+ {
+ if (fragment.hasRemaining())
+ {
+ byteBuffers.add(fragment.getUnderlyingBuffer());
+ if (byteBuffers.size() == maxBuffers)
+ {
+ break outer;
+ }
+ }
+ }
+ }
+ else if (qpidByteBuffer.hasRemaining())
+ {
+ byteBuffers.add(((SingleQpidByteBuffer)
qpidByteBuffer).getUnderlyingBuffer());
+ if (byteBuffers.size() == maxBuffers)
+ {
+ break;
+ }
+ }
+ }
+
+ return byteBuffers.isEmpty() ? 0L :
channel.write(byteBuffers.toArray(new ByteBuffer[0]));
}
static QpidByteBuffer wrap(ByteBuffer wrap)
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
index 229a6ee7f0..2d9a9badd4 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPort.java
@@ -159,6 +159,14 @@ public interface AmqpPort<X extends AmqpPort<X>> extends
Port<X>
" If the connection does not close this time, it will be aborted.")
long DEFAULT_FINAL_WRITE_TIMEOUT = 1000L;
+ String MAX_GATHERING_WRITE_BUFFERS =
"qpid.port.amqp.maxGatheringWriteBuffers";
+ int MINIMUM_MAX_GATHERING_WRITE_BUFFERS = 1;
+ int MAXIMUM_MAX_GATHERING_WRITE_BUFFERS = 1024;
+ @SuppressWarnings("unused")
+ @ManagedContextDefault(name = MAX_GATHERING_WRITE_BUFFERS,
+ description = "Maximum number of underlying NIO buffers included
in one gathering socket write.")
+ int DEFAULT_MAX_GATHERING_WRITE_BUFFERS =
MAXIMUM_MAX_GATHERING_WRITE_BUFFERS;
+
@ManagedAttribute( defaultValue = AmqpPort.DEFAULT_AMQP_TCP_NO_DELAY )
boolean isTcpNoDelay();
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
index ab810b603a..07ab4d181c 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/model/port/AmqpPortImpl.java
@@ -350,20 +350,37 @@ public class AmqpPortImpl extends
AbstractPort<AmqpPortImpl> implements AmqpPort
}
validateThreadPoolSettings(this);
+ validateMaxGatheringWriteBuffers(getContextValue(Integer.class,
MAX_GATHERING_WRITE_BUFFERS));
}
@Override
protected void validateChange(final ConfiguredObject<?>
proxyForValidation, final Set<String> changedAttributes)
{
super.validateChange(proxyForValidation, changedAttributes);
- AmqpPort changed = (AmqpPort) proxyForValidation;
+ final AmqpPort<?> changed = (AmqpPort<?>) proxyForValidation;
if (changedAttributes.contains(THREAD_POOL_SIZE) ||
changedAttributes.contains(NUMBER_OF_SELECTORS))
{
validateThreadPoolSettings(changed);
}
+ if (changedAttributes.contains(CONTEXT))
+ {
+ final String configuredValue =
changed.getContext().get(MAX_GATHERING_WRITE_BUFFERS);
+ try
+ {
+ validateMaxGatheringWriteBuffers(configuredValue == null
+ ? DEFAULT_MAX_GATHERING_WRITE_BUFFERS
+ : Integer.parseInt(configuredValue));
+ }
+ catch (NumberFormatException e)
+ {
+ final String message = "Maximum number of gathering write
buffers on Port %s must be an integer"
+ .formatted(getName());
+ throw new IllegalConfigurationException(message, e);
+ }
+ }
}
- private void validateThreadPoolSettings(final AmqpPort changed)
+ private void validateThreadPoolSettings(final AmqpPort<?> changed)
{
if (changed.getThreadPoolSize() < 1)
{
@@ -379,6 +396,17 @@ public class AmqpPortImpl extends
AbstractPort<AmqpPortImpl> implements AmqpPort
}
}
+ private void validateMaxGatheringWriteBuffers(final int
maxGatheringWriteBuffers)
+ {
+ if (maxGatheringWriteBuffers < MINIMUM_MAX_GATHERING_WRITE_BUFFERS ||
maxGatheringWriteBuffers > MAXIMUM_MAX_GATHERING_WRITE_BUFFERS)
+ {
+ final String message = "Maximum number of gathering write buffers
%d on Port %s must be between %d and %d"
+ .formatted(maxGatheringWriteBuffers, getName(),
MINIMUM_MAX_GATHERING_WRITE_BUFFERS,
+ MAXIMUM_MAX_GATHERING_WRITE_BUFFERS);
+ throw new IllegalConfigurationException(message);
+ }
+ }
+
private SSLContext createSslContext()
{
KeyStore keyStore = getKeyStore();
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 d8a8857d82..343e2b3343 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
@@ -63,6 +63,7 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
private final Runnable _onTransportEncryptionAction;
private final int _finalWriteThreshold;
private final long _finalWriteTimeout;
+ private final int _maxGatheringWriteBuffers;
private volatile boolean _fullyWritten = true;
@@ -83,8 +84,8 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
private volatile long _bufferedSize;
private String _selectedHost;
- public NonBlockingConnection(SocketChannel socketChannel,
- ProtocolEngine protocolEngine,
+ public NonBlockingConnection(final SocketChannel socketChannel,
+ final ProtocolEngine protocolEngine,
final Set<TransportEncryption> encryptionSet,
final Runnable onTransportEncryptionAction,
final NetworkConnectionScheduler scheduler,
@@ -118,6 +119,7 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
}
_finalWriteThreshold = port.getContextValue(Integer.class,
AmqpPort.FINAL_WRITE_THRESHOLD);
_finalWriteTimeout = port.getContextValue(Long.class,
AmqpPort.FINAL_WRITE_TIMEOUT);
+ _maxGatheringWriteBuffers = port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS);
final Broker broker = (Broker<?>) port.getParent();
_eventLogger = broker.getEventLogger();
@@ -259,33 +261,36 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
{
try
{
- long currentTime = System.currentTimeMillis();
- int tick = getTicker().getTimeToNextTick(currentTime);
+ final long currentTime = System.currentTimeMillis();
+ final int tick = getTicker().getTimeToNextTick(currentTime);
if (tick <= 0)
{
getTicker().tick(currentTime);
}
+ _scheduledTime = 0L;
- _protocolEngine.setIOThread(Thread.currentThread());
-
- boolean processPendingComplete = processPending();
-
- if(processPendingComplete)
+ if (!_closed.get())
{
- _pendingIterator = null;
- _protocolEngine.setTransportBlockedForWriting(false);
- boolean dataRead = doRead();
- _protocolEngine.setTransportBlockedForWriting(!doWrite());
+ _protocolEngine.setIOThread(Thread.currentThread());
+
+ final boolean processPendingComplete = processPending();
- if (!_fullyWritten || dataRead || (_delegate.needsWork()
&& _delegate.getNetInputBuffer().position() != 0))
+ if (processPendingComplete)
+ {
+ _pendingIterator = null;
+ _protocolEngine.setTransportBlockedForWriting(false);
+ final boolean dataRead = doRead();
+
_protocolEngine.setTransportBlockedForWriting(!doWrite());
+
+ if (!_fullyWritten || dataRead ||
(_delegate.needsWork() && _delegate.getNetInputBuffer().position() != 0))
+ {
+ _protocolEngine.notifyWork();
+ }
+ }
+ else
{
_protocolEngine.notifyWork();
}
-
- }
- else
- {
- _protocolEngine.notifyWork();
}
}
@@ -523,9 +528,9 @@ public class NonBlockingConnection implements
ServerNetworkConnection, ByteBuffe
}
}
- long writeToTransport(Collection<QpidByteBuffer> buffers) throws
IOException
+ long writeToTransport(final Collection<QpidByteBuffer> buffers) throws
IOException
{
- long written = QpidByteBuffer.write(_socketChannel, buffers);
+ final long written = QpidByteBuffer.write(_socketChannel, buffers,
_maxGatheringWriteBuffers);
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Written " + written + " bytes");
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegate.java
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegate.java
index d355f4b613..5e1f398688 100644
---
a/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegate.java
+++
b/broker-core/src/main/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegate.java
@@ -99,29 +99,25 @@ public class NonBlockingConnectionPlainDelegate implements
NonBlockingConnection
}
-
@Override
- public WriteResult doWrite(Collection<QpidByteBuffer> buffers) throws
IOException
+ public WriteResult doWrite(final Collection<QpidByteBuffer> buffers)
throws IOException
{
- long bytesToWrite = 0L;
- if(!buffers.isEmpty())
- {
- for (QpidByteBuffer buf : buffers)
- {
- bytesToWrite += buf.remaining();
- }
- }
- if(bytesToWrite == 0L)
+ if (buffers.isEmpty())
{
return new WriteResult(true, 0);
}
- else
- {
- long bytesWritten = _parent.writeToTransport(buffers);
- return new WriteResult(bytesWritten >= bytesToWrite, bytesWritten);
+ final long bytesWritten = _parent.writeToTransport(buffers);
+ boolean complete = true;
+ for (final QpidByteBuffer buffer : buffers)
+ {
+ if (buffer.hasRemaining())
+ {
+ complete = false;
+ break;
+ }
}
-
+ return new WriteResult(complete, bytesWritten);
}
@Override
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/bytebuffer/QpidByteBufferTest.java
b/broker-core/src/test/java/org/apache/qpid/server/bytebuffer/QpidByteBufferTest.java
index a3e4523ff5..6b9ad47ca6 100644
---
a/broker-core/src/test/java/org/apache/qpid/server/bytebuffer/QpidByteBufferTest.java
+++
b/broker-core/src/test/java/org/apache/qpid/server/bytebuffer/QpidByteBufferTest.java
@@ -27,6 +27,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
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.Mockito.mock;
+import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -38,8 +41,11 @@ import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.InvalidMarkException;
+import java.nio.channels.GatheringByteChannel;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -922,6 +928,109 @@ public class QpidByteBufferTest extends UnitTestBase
}
}
+ @Test
+ public void testGatheringWriteLimitsUnderlyingBuffers() throws Exception
+ {
+ final List<QpidByteBuffer> buffers = new ArrayList<>();
+ final List<Integer> batchSizes = new ArrayList<>();
+
+ try (final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final GatheringByteChannel channel = createWritingChannel(output,
batchSizes))
+ {
+ for (int i = 0; i < 5; i++)
+ {
+ buffers.add(QpidByteBuffer.wrap(new byte[] {(byte) i}));
+ }
+
+ while (hasRemaining(buffers))
+ {
+ QpidByteBuffer.write(channel, buffers, 2);
+ }
+
+ assertEquals(List.of(2, 2, 1), batchSizes, "Unexpected gathering
write batch sizes");
+ assertArrayEquals(new byte[] {0, 1, 2, 3, 4},
output.toByteArray(), "Gathering write changed buffer order");
+ }
+ finally
+ {
+ for (final QpidByteBuffer buffer : buffers)
+ {
+ buffer.dispose();
+ }
+ }
+ }
+
+ @Test
+ public void testGatheringWriteLimitsCompositeBufferFragments() throws
Exception
+ {
+ final byte[] content = new byte[BUFFER_FRAGMENT_SIZE * 3];
+ for (int i = 0; i < content.length; i++)
+ {
+ content[i] = (byte) i;
+ }
+
+ final List<Integer> batchSizes = new ArrayList<>();
+
+ try (final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final GatheringByteChannel channel = createWritingChannel(output,
batchSizes);
+ final QpidByteBuffer buffer =
QpidByteBuffer.allocateDirect(content.length))
+ {
+ buffer.put(content);
+ buffer.flip();
+
+ assertEquals(BUFFER_FRAGMENT_SIZE * 2,
QpidByteBuffer.write(channel, List.of(buffer), 2),
+ "Unexpected first gathering write size");
+ assertEquals(BUFFER_FRAGMENT_SIZE, QpidByteBuffer.write(channel,
List.of(buffer), 2),
+ "Unexpected second gathering write size");
+
+ assertEquals(List.of(2, 1), batchSizes, "Exhausted fragments
should not count towards the limit");
+ assertArrayEquals(content, output.toByteArray(), "Gathering write
changed fragment order");
+ }
+ }
+
+ @Test
+ public void testGatheringWriteRejectsInvalidMaximum() throws Exception
+ {
+ try (final GatheringByteChannel channel =
mock(GatheringByteChannel.class))
+ {
+ assertThrows(IllegalArgumentException.class, () ->
QpidByteBuffer.write(channel, List.of(_parent), 0));
+ assertThrows(IllegalArgumentException.class, () ->
QpidByteBuffer.write(channel, List.of(_parent), -1));
+ }
+ }
+
+ private GatheringByteChannel createWritingChannel(final
ByteArrayOutputStream output,
+ final List<Integer>
batchSizes) throws IOException
+ {
+ final GatheringByteChannel channel = mock(GatheringByteChannel.class);
+ when(channel.write(any(ByteBuffer[].class))).thenAnswer(invocation ->
+ {
+ final ByteBuffer[] byteBuffers = invocation.getArgument(0);
+ batchSizes.add(byteBuffers.length);
+ long written = 0;
+ for (final ByteBuffer byteBuffer : byteBuffers)
+ {
+ while (byteBuffer.hasRemaining())
+ {
+ output.write(byteBuffer.get());
+ written++;
+ }
+ }
+ return written;
+ });
+ return channel;
+ }
+
+ private boolean hasRemaining(final List<QpidByteBuffer> buffers)
+ {
+ for (final QpidByteBuffer buffer : buffers)
+ {
+ if (buffer.hasRemaining())
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void doDeflateInflate(final byte[] input,
final QpidByteBuffer inputBuf,
final boolean direct) throws IOException
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/model/port/AmqpPortImplTest.java
b/broker-core/src/test/java/org/apache/qpid/server/model/port/AmqpPortImplTest.java
index 5da1320d46..45eaaec0da 100644
---
a/broker-core/src/test/java/org/apache/qpid/server/model/port/AmqpPortImplTest.java
+++
b/broker-core/src/test/java/org/apache/qpid/server/model/port/AmqpPortImplTest.java
@@ -42,6 +42,8 @@ import java.util.Set;
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.ValueSource;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.configuration.updater.CurrentThreadTaskExecutor;
@@ -234,6 +236,63 @@ public class AmqpPortImplTest extends UnitTestBase
"Exception not thrown for number of selectors equal to thread
pool size");
}
+ @Test
+ public void testDefaultMaxGatheringWriteBuffers()
+ {
+ _port = createPort(getTestName());
+
+ assertEquals(AmqpPort.DEFAULT_MAX_GATHERING_WRITE_BUFFERS,
+ _port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS),
+ "Unexpected default maximum gathering write buffer count");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 1, 16, 1024 })
+ public void testValidMaxGatheringWriteBuffersOnCreate(final int value)
+ {
+ _port = createPort(getTestName(), Map.of(AmqpPort.CONTEXT,
Map.of(AmqpPort.MAX_GATHERING_WRITE_BUFFERS,
+ String.valueOf(value))));
+
+ assertEquals(value, _port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS),
+ "Unexpected maximum gathering write buffer count");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { -1, 0, 1025 })
+ public void testInvalidMaxGatheringWriteBuffersOnCreate(final int value)
+ {
+ final Map<String, Object> attributes = Map.of(AmqpPort.CONTEXT,
Map.of(AmqpPort.MAX_GATHERING_WRITE_BUFFERS,
+ String.valueOf(value)));
+
+ assertThrows(IllegalConfigurationException.class, () ->
createPort(getTestName(), attributes),
+ "Creation should reject an out-of-range maximum gathering
write buffer count");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { -1, 0, 1025 })
+ public void testInvalidMaxGatheringWriteBuffersOnChange(final int value)
+ {
+ _port = createPort(getTestName());
+ final Map<String, Object> context =
Map.of(AmqpPort.MAX_GATHERING_WRITE_BUFFERS, String.valueOf(value));
+
+ assertThrows(IllegalConfigurationException.class, () ->
+ _port.setAttributes(Map.of(AmqpPort.CONTEXT, context)),
+ "Update should reject an out-of-range maximum gathering write
buffer count");
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 1, 16, 1024 })
+ public void testValidMaxGatheringWriteBuffersOnChange(final int value)
+ {
+ _port = createPort(getTestName());
+ final Map<String, Object> context =
Map.of(AmqpPort.MAX_GATHERING_WRITE_BUFFERS, String.valueOf(value));
+
+ _port.setAttributes(Map.of(AmqpPort.CONTEXT, context));
+
+ assertEquals(value, _port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS),
+ "Unexpected updated maximum gathering write buffer count");
+ }
+
@Test
public void testConnectionCounting()
{
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionIdleTimeoutTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionIdleTimeoutTest.java
new file mode 100644
index 0000000000..2d344a2ba5
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionIdleTimeoutTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.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.times;
+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.net.SocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+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 NonBlockingConnectionIdleTimeoutTest extends UnitTestBase
+{
+ private ProtocolEngine _protocolEngine;
+ private AggregateTicker _ticker;
+ private NonBlockingConnection _connection;
+
+ @BeforeEach
+ void beforeEach()
+ {
+ final SocketAddress localAddress = mock(SocketAddress.class);
+ when(localAddress.toString()).thenReturn("127.0.0.1:5672");
+ final Socket socket = mock(Socket.class);
+ when(socket.getRemoteSocketAddress()).thenReturn(new
InetSocketAddress("localhost", 1000));
+ when(socket.getLocalSocketAddress()).thenReturn(localAddress);
+ final SocketChannel socketChannel = mock(SocketChannel.class);
+ when(socketChannel.socket()).thenReturn(socket);
+
+ _ticker = mock(AggregateTicker.class);
+ _protocolEngine = mock(ProtocolEngine.class);
+ when(_protocolEngine.getAggregateTicker()).thenReturn(_ticker);
+ when(_protocolEngine.processPendingIterator()).thenAnswer(invocation
-> Collections.emptyIterator());
+
+ final NetworkConnectionScheduler scheduler =
mock(NetworkConnectionScheduler.class);
+ final AmqpPort<?> port = mock(AmqpPort.class);
+ when(port.getNetworkBufferSize()).thenReturn(1024);
+ when(port.getContextValue(Integer.class,
AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(100);
+ when(port.getContextValue(Long.class,
AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(100L);
+ when(port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
+ final EventLogger eventLogger = mock(EventLogger.class);
+ final Broker<?> broker = mock(Broker.class);
+ when(broker.getEventLogger()).thenReturn(eventLogger);
+ doReturn(broker).when(port).getParent();
+
+ _connection = new NonBlockingConnection(socketChannel,
_protocolEngine, Set.of(TransportEncryption.NONE),
+ () -> { }, scheduler, port);
+ final SelectorThread.SelectionTask _selectionTask =
mock(SelectorThread.SelectionTask.class);
+ _connection.setSelectionTask(_selectionTask);
+ }
+
+ @AfterEach
+ void afterEach()
+ {
+ _connection.close();
+ _connection.doWork();
+ }
+
+ @Test
+ void immediateRerunUsesCurrentTimeForTicker()
+ {
+ final List<Long> observedScheduledTimes = new ArrayList<>();
+ when(_ticker.getTimeToNextTick(anyLong())).thenAnswer(invocation ->
+ {
+ final long scheduledTime = _connection.getScheduledTime();
+ observedScheduledTimes.add(scheduledTime);
+ return scheduledTime > 0 ? 1 : 0;
+ });
+ when(_ticker.tick(anyLong())).thenReturn(Integer.MAX_VALUE);
+
+ assertTrue(_connection.setScheduled(), "Connection should be
scheduled");
+ final long initialScheduledTime = _connection.getScheduledTime();
+
+ _connection.doWork();
+
+ assertEquals(initialScheduledTime, observedScheduledTimes.get(0),
+ "First ticker evaluation should see the original scheduled
time");
+ assertEquals(0, _connection.getScheduledTime(),
+ "Scheduled time should be cleared after the first ticker
evaluation");
+ verify(_ticker, never()).tick(anyLong());
+
+ _connection.doWork();
+
+ assertEquals(0, observedScheduledTimes.get(1), "Immediate rerun should
evaluate the ticker against current time");
+ verify(_ticker, times(1)).tick(anyLong());
+ }
+
+ @Test
+ void tickerCloseSkipsOrdinaryIoAndRunsShutdown() throws Exception
+ {
+ final NonBlockingConnectionDelegate delegate =
mock(NonBlockingConnectionDelegate.class);
+ when(delegate.doWrite(any())).thenReturn(new
NonBlockingConnectionDelegate.WriteResult(true, 0));
+ injectDelegate(delegate);
+ when(_ticker.getTimeToNextTick(anyLong())).thenReturn(0);
+ doAnswer(invocation ->
+ {
+ _connection.close();
+ return Integer.MAX_VALUE;
+ }).when(_ticker).tick(anyLong());
+ assertTrue(_connection.setScheduled(), "Connection should be
scheduled");
+
+ _connection.doWork();
+
+ verify(_protocolEngine, never()).processPendingIterator();
+ verify(_protocolEngine,
never()).setTransportBlockedForWriting(anyBoolean());
+ verify(delegate, never()).readyForRead();
+ verify(delegate, times(1)).doWrite(any());
+ verify(_protocolEngine, times(1)).closed();
+ verify(delegate, times(1)).shutdownInput();
+ verify(delegate, times(1)).shutdownOutput();
+ }
+
+ @Test
+ void blockedPendingOutputDoesNotStarveReadIdleTicker() throws Exception
+ {
+ final int readDelay = 1000;
+ final long lastReadTime = System.currentTimeMillis() - 2 * readDelay;
+ final long timeoutTime = lastReadTime + readDelay;
+ final AtomicBoolean readerIdle = new AtomicBoolean();
+ final AggregateTicker aggregateTicker = new AggregateTicker();
+ when(_protocolEngine.getAggregateTicker()).thenReturn(aggregateTicker);
+ when(_protocolEngine.getLastReadTime()).thenReturn(lastReadTime);
+ doAnswer(invocation ->
+ {
+ readerIdle.set(true);
+ _connection.close();
+ return null;
+ }).when(_protocolEngine).readerIdle();
+ aggregateTicker.addTicker(new ServerIdleReadTimeoutTicker(_connection,
_protocolEngine, readDelay));
+
+ final Iterator<Runnable> pendingIterator = new Iterator<>()
+ {
+ private boolean _outputQueued;
+
+ @Override
+ public boolean hasNext()
+ {
+ return true;
+ }
+
+ @Override
+ public Runnable next()
+ {
+ if (!_outputQueued)
+ {
+ _outputQueued = true;
+ return () -> _connection.send(QpidByteBuffer.wrap(new
byte[1024]));
+ }
+ return () -> { };
+ }
+ };
+
when(_protocolEngine.processPendingIterator()).thenReturn(pendingIterator);
+
+ final NonBlockingConnectionDelegate delegate =
mock(NonBlockingConnectionDelegate.class);
+ when(delegate.doWrite(any())).thenAnswer(invocation ->
+ {
+ if (!readerIdle.get())
+ {
+ return new NonBlockingConnectionDelegate.WriteResult(false, 0);
+ }
+
+ long bytesConsumed = 0;
+ final Collection<QpidByteBuffer> buffers =
invocation.getArgument(0);
+ for (final QpidByteBuffer buffer : buffers)
+ {
+ bytesConsumed += buffer.remaining();
+ buffer.position(buffer.limit());
+ }
+ return new NonBlockingConnectionDelegate.WriteResult(true,
bytesConsumed);
+ });
+ injectDelegate(delegate);
+
+ assertTrue(_connection.setScheduled(), "Connection should be
scheduled");
+ injectScheduledTime(timeoutTime - 1);
+
+ _connection.doWork();
+
+ assertEquals(0, _connection.getScheduledTime(), "Scheduled time should
be cleared while pending output remains blocked");
+ verify(_protocolEngine, never()).readerIdle();
+ verify(delegate, times(2)).doWrite(any());
+
+ _connection.doWork();
+
+ verify(_protocolEngine, times(1)).readerIdle();
+ verify(_protocolEngine, times(1)).processPendingIterator();
+ verify(delegate, times(3)).doWrite(any());
+ verify(_protocolEngine, times(1)).closed();
+ }
+
+ private void injectDelegate(final NonBlockingConnectionDelegate delegate)
throws Exception
+ {
+ final Field delegateField =
NonBlockingConnection.class.getDeclaredField("_delegate");
+ delegateField.setAccessible(true);
+ delegateField.set(_connection, delegate);
+ }
+
+ private void injectScheduledTime(final long scheduledTime) throws Exception
+ {
+ final Field scheduledTimeField =
NonBlockingConnection.class.getDeclaredField("_scheduledTime");
+ scheduledTimeField.setAccessible(true);
+ scheduledTimeField.setLong(_connection, scheduledTime);
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegateTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegateTest.java
new file mode 100644
index 0000000000..3d445a28ce
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionPlainDelegateTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.Mockito.doAnswer;
+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.util.Collection;
+import java.util.List;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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 NonBlockingConnectionPlainDelegateTest extends UnitTestBase
+{
+ private NonBlockingConnection _connection;
+ private NonBlockingConnectionPlainDelegate _delegate;
+
+ @BeforeEach
+ void beforeEach()
+ {
+ _connection = mock(NonBlockingConnection.class);
+ final AmqpPort<?> port = mock(AmqpPort.class);
+ when(port.getNetworkBufferSize()).thenReturn(1024);
+ _delegate = new NonBlockingConnectionPlainDelegate(_connection, port);
+ }
+
+ @AfterEach
+ void afterEach()
+ {
+ _delegate.shutdownInput();
+ }
+
+ @Test
+ void emptyCollectionIsComplete() throws Exception
+ {
+ final NonBlockingConnectionDelegate.WriteResult result =
_delegate.doWrite(List.of());
+
+ assertTrue(result.isComplete(), "Empty collection should be complete");
+ assertEquals(0, result.getBytesConsumed(), "Empty collection should
not consume bytes");
+ verify(_connection, never()).writeToTransport(any());
+ }
+
+ @Test
+ void partialWriteIsIncomplete() throws Exception
+ {
+ try (final QpidByteBuffer first = QpidByteBuffer.wrap(new byte[] { 1,
2 });
+ final QpidByteBuffer second = QpidByteBuffer.wrap(new byte[] { 3
}))
+ {
+ final List<QpidByteBuffer> buffers = List.of(first, second);
+ when(_connection.writeToTransport(buffers)).thenReturn(2L);
+
+ final NonBlockingConnectionDelegate.WriteResult result =
_delegate.doWrite(buffers);
+
+ assertFalse(result.isComplete(), "Buffers with remaining data
should be incomplete");
+ assertEquals(2, result.getBytesConsumed(), "Unexpected consumed
byte count");
+ }
+ }
+
+ @Test
+ void fullyConsumedWriteIsComplete() throws Exception
+ {
+ try (final QpidByteBuffer first = QpidByteBuffer.wrap(new byte[] { 1,
2 });
+ final QpidByteBuffer second = QpidByteBuffer.wrap(new byte[] { 3
}))
+ {
+ final List<QpidByteBuffer> buffers = List.of(first, second);
+ doAnswer(invocation ->
+ {
+ final Collection<QpidByteBuffer> writtenBuffers =
invocation.getArgument(0);
+ for (final QpidByteBuffer buffer : writtenBuffers)
+ {
+ buffer.position(buffer.limit());
+ }
+ return 3L;
+ }).when(_connection).writeToTransport(buffers);
+
+ final NonBlockingConnectionDelegate.WriteResult result =
_delegate.doWrite(buffers);
+
+ assertTrue(result.isComplete(), "Fully consumed buffers should be
complete");
+ assertEquals(3, result.getBytesConsumed(), "Unexpected consumed
byte count");
+ }
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTest.java
index b18416a8fd..4153fc4b3e 100644
---
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTest.java
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTest.java
@@ -96,6 +96,7 @@ class NonBlockingConnectionTest
when(broker.getEventLogger()).thenReturn(eventLogger);
when(port.getContextValue(Integer.class,
AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(100);
when(port.getContextValue(Long.class,
AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(100L);
+ when(port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
when(port.getParent()).thenReturn(broker);
final Set<TransportEncryption> encryptionSet =
Set.of(TransportEncryption.NONE);
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/transport/TCPandSSLTransportTest.java
b/broker-core/src/test/java/org/apache/qpid/server/transport/TCPandSSLTransportTest.java
index 6024869117..014ca02752 100644
---
a/broker-core/src/test/java/org/apache/qpid/server/transport/TCPandSSLTransportTest.java
+++
b/broker-core/src/test/java/org/apache/qpid/server/transport/TCPandSSLTransportTest.java
@@ -212,6 +212,7 @@ public class TCPandSSLTransportTest extends UnitTestBase
when(port.getContextValue(Integer.class,
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD)).thenReturn(1005);
when(port.getContextValue(Integer.class,
AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(100);
when(port.getContextValue(Long.class,
AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(100L);
+ when(port.getContextValue(Integer.class,
AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
final ObjectMapper mapper = new ObjectMapper();
final JavaType type =
mapper.getTypeFactory().constructCollectionType(List.class, String.class);
final List<String> allowList =
mapper.readValue(Broker.DEFAULT_SECURITY_TLS_PROTOCOL_ALLOW_LIST, type);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]