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 438cc4b6f2 QPID-8788: [Broker-J] Connection close can exceed the 
configured timeout under transport backpressure (#463)
438cc4b6f2 is described below

commit 438cc4b6f2c15398df4b99a6df3c7bf413b07420
Author: Kato Kaito <[email protected]>
AuthorDate: Mon Nov 2 13:36:58 2026 +0100

    QPID-8788: [Broker-J] Connection close can exceed the configured timeout 
under transport backpressure (#463)
---
 .../org/apache/qpid/server/model/Connection.java   |   4 +-
 .../server/protocol/ConnectionClosingTicker.java   |  34 +-
 .../qpid/server/transport/AMQPConnection.java      |   4 +
 .../server/transport/AbstractAMQPConnection.java   |  26 +
 .../server/transport/NonBlockingConnection.java    | 110 ++-
 .../NonBlockingConnectionTLSDelegate.java          |  32 +-
 .../server/transport/ServerNetworkConnection.java  |  10 +
 .../protocol/ConnectionClosingTickerTest.java      | 109 +++
 .../transport/NativeCloseDeadlineTestBase.java     | 279 ++++++++
 .../NonBlockingConnectionCloseSchedulingTest.java  | 197 ++++++
 .../transport/NonBlockingConnectionCloseTest.java  | 770 +++++++++++++++++++++
 .../NonBlockingConnectionExceptionTest.java        |  23 +-
 .../NonBlockingConnectionIdleTimeoutTest.java      |  31 +-
 .../transport/NonBlockingConnectionTestHelper.java |  84 +++
 .../server/protocol/v0_10/ServerConnection.java    |  13 +-
 .../v0_10/NativeCloseDeadline_0_10Test.java        |  92 +++
 .../protocol/v0_8/AMQPConnection_0_8Impl.java      |  18 +-
 .../protocol/v0_8/AMQPConnection_0_8Test.java      |   6 +-
 .../protocol/v0_8/NativeCloseDeadline_0_8Test.java |  66 ++
 .../protocol/v1_0/AMQPConnection_1_0Impl.java      |  27 +-
 .../protocol/v1_0/NativeCloseDeadline_1_0Test.java |  55 ++
 .../AMQPConnection_1_0CloseExistingTest.java       | 298 ++++++++
 22 files changed, 2166 insertions(+), 122 deletions(-)

diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/model/Connection.java 
b/broker-core/src/main/java/org/apache/qpid/server/model/Connection.java
index f72d0451c4..33230d3485 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/Connection.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/Connection.java
@@ -59,7 +59,9 @@ public interface Connection<X extends Connection<X>> extends 
ConfiguredObject<X>
     int DEFAULT_MAX_MESSAGE_DECOMPRESSION_SIZE = 100 * 1024 * 1024;
 
     String CLOSE_RESPONSE_TIMEOUT = "connection.closeResponseTimeout";
-    @ManagedContextDefault(name = CLOSE_RESPONSE_TIMEOUT)
+    @ManagedContextDefault(name = CLOSE_RESPONSE_TIMEOUT,
+            description = "Maximum time in milliseconds to send the connection 
close and await the peer's response," +
+                    " starting when closing is requested. A non-positive value 
expires immediately.")
     long DEFAULT_CLOSE_RESPONSE_TIMEOUT = 2000L;
 
     String MAX_MESSAGE_SIZE = "qpid.max_message_size";
diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/protocol/ConnectionClosingTicker.java
 
b/broker-core/src/main/java/org/apache/qpid/server/protocol/ConnectionClosingTicker.java
index 2a82329678..4d063df1c3 100644
--- 
a/broker-core/src/main/java/org/apache/qpid/server/protocol/ConnectionClosingTicker.java
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/protocol/ConnectionClosingTicker.java
@@ -20,38 +20,54 @@
  */
 package org.apache.qpid.server.protocol;
 
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongSupplier;
+
 import org.apache.qpid.server.transport.ServerNetworkConnection;
 import org.apache.qpid.server.transport.network.Ticker;
 
 public class ConnectionClosingTicker implements Ticker
 {
-    private final long _timeoutTime;
+    private final long _startTimeNanos;
+    private final long _timeoutNanos;
+    private final LongSupplier _nanoTime;
     private final ServerNetworkConnection _network;
 
-    public ConnectionClosingTicker(final long timeoutTime, final 
ServerNetworkConnection network)
+    public ConnectionClosingTicker(final long timeoutMillis, final 
ServerNetworkConnection network)
     {
-        _timeoutTime = timeoutTime;
+        this(timeoutMillis, network, System::nanoTime);
+    }
+
+    ConnectionClosingTicker(final long timeoutMillis,
+                            final ServerNetworkConnection network,
+                            final LongSupplier nanoTime)
+    {
+        _nanoTime = nanoTime;
+        _startTimeNanos = nanoTime.getAsLong();
+        _timeoutNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0L, 
timeoutMillis));
         _network = network;
     }
 
     @Override
     public int getTimeToNextTick(final long currentTime)
     {
-        if (_network.getScheduledTime() > 0)
+        final long elapsed = _nanoTime.getAsLong() - _startTimeNanos;
+        if (elapsed >= _timeoutNanos)
         {
-            return (int) (_timeoutTime - _network.getScheduledTime());
+            return 0;
         }
-
-        return (int) (_timeoutTime - currentTime);
+        final long remaining = _timeoutNanos - elapsed;
+        final long millis = TimeUnit.NANOSECONDS.toMillis(remaining);
+        return (int) Math.min(Integer.MAX_VALUE, millis + (remaining % 
1_000_000L == 0 ? 0 : 1));
     }
 
     @Override
     public int tick(final long currentTime)
     {
-        int nextTick = getTimeToNextTick(currentTime);
+        final int nextTick = getTimeToNextTick(currentTime);
         if(nextTick <= 0)
         {
-            _network.close();
+            _network.abort();
         }
         return nextTick;
     }
diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
 
b/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
index 257bf7f430..e78fff1345 100644
--- 
a/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
@@ -120,6 +120,10 @@ public interface AMQPConnection<C extends 
AMQPConnection<C>>
 
     void sendConnectionCloseAsync(CloseReason reason, String description);
 
+    void startCloseDeadline();
+
+    CompletableFuture<Void> getTransportClosedFuture();
+
     boolean isIOThread();
     CompletableFuture<Void> doOnIOThreadAsync(final Runnable task);
 
diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
 
b/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
index 0195eb3692..1e65641110 100644
--- 
a/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
@@ -64,6 +64,7 @@ import org.apache.qpid.server.model.State;
 import org.apache.qpid.server.model.TaskExecutorProvider;
 import org.apache.qpid.server.model.Transport;
 import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.protocol.ConnectionClosingTicker;
 import org.apache.qpid.server.security.AccessDeniedException;
 import org.apache.qpid.server.security.SubjectExecutionContext;
 import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
@@ -114,6 +115,7 @@ public abstract class AbstractAMQPConnection<C extends 
AbstractAMQPConnection<C,
 
     private String _clientId;
     private volatile boolean _stopped;
+    private boolean _closeDeadlineStarted;
 
     private final AtomicLong _messagesIn = new AtomicLong();
     private final AtomicLong _messagesOut = new AtomicLong();
@@ -325,6 +327,24 @@ public abstract class AbstractAMQPConnection<C extends 
AbstractAMQPConnection<C,
         _stopped = true;
     }
 
+    @Override
+    public final void startCloseDeadline()
+    {
+        synchronized (this)
+        {
+            if (_closeDeadlineStarted || _transportClosedFuture.isDone())
+            {
+                return;
+            }
+            final long timeoutMillis = getContextValue(Long.class, 
Connection.CLOSE_RESPONSE_TIMEOUT);
+            final ConnectionClosingTicker deadline = new 
ConnectionClosingTicker(timeoutMillis, _network);
+            _network.setCloseDeadline(deadline);
+            _aggregateTicker.addTicker(deadline);
+            _closeDeadlineStarted = true;
+        }
+        notifyWork();
+    }
+
     @Override
     public boolean isConnectionStopped()
     {
@@ -799,6 +819,12 @@ public abstract class AbstractAMQPConnection<C extends 
AbstractAMQPConnection<C,
         return getSessionModels().size();
     }
 
+    @Override
+    public final CompletableFuture<Void> getTransportClosedFuture()
+    {
+        return _transportClosedFuture.copy();
+    }
+
     protected void markTransportClosed()
     {
         _transportClosedFuture.complete(null);
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 1d0cc188fe..f721a99c81 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
@@ -44,6 +44,7 @@ import org.apache.qpid.server.logging.EventLogger;
 import org.apache.qpid.server.logging.messages.NetworkMessages;
 import org.apache.qpid.server.model.Broker;
 import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.transport.network.Ticker;
 import org.apache.qpid.server.transport.network.TransportEncryption;
 import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
 import org.apache.qpid.server.util.SystemUtils;
@@ -83,6 +84,8 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
     private final AtomicBoolean _hasShutdown = new AtomicBoolean();
     private volatile long _bufferedSize;
     private String _selectedHost;
+    private volatile boolean _aborted;
+    private volatile Ticker _closeDeadline;
 
     public NonBlockingConnection(final SocketChannel socketChannel,
                                  final ProtocolEngine protocolEngine,
@@ -158,6 +161,22 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
         return this;
     }
 
+    @Override
+    public synchronized void setCloseDeadline(final Ticker deadline)
+    {
+        if (_closeDeadline == null)
+        {
+            _closeDeadline = deadline;
+        }
+    }
+
+    @Override
+    public void abort()
+    {
+        _aborted = true;
+        close();
+    }
+
     @Override
     public void close()
     {
@@ -276,29 +295,50 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
 
                     final boolean processPendingComplete = processPending();
 
-                    if (processPendingComplete)
+                    if (isCloseDeadlineExpired())
                     {
-                        _pendingIterator = null;
+                        abort();
                     }
 
-                    if (processPendingComplete || 
_delegate.needsReadForWrite())
+                    if (!_closed.get())
                     {
-                        final boolean dataRead = doRead();
-                        doWrite();
+                        if (processPendingComplete)
+                        {
+                            _pendingIterator = null;
+                        }
 
-                        if (_fullyWritten && (dataRead || 
(_delegate.needsWork() &&
-                                _delegate.getNetInputBuffer().position() != 
0)))
+                        if (processPendingComplete || 
_delegate.needsReadForWrite())
                         {
-                            _protocolEngine.notifyWork();
+                            final boolean dataRead = doRead();
+                            // Processing incoming frames may close the 
connection or start its close deadline.
+                            if (isCloseDeadlineExpired())
+                            {
+                                abort();
+                            }
+
+                            if (!_closed.get())
+                            {
+                                doWrite();
+
+                                if (_fullyWritten && (dataRead || 
(_delegate.needsWork() &&
+                                        
_delegate.getNetInputBuffer().position() != 0)))
+                                {
+                                    _protocolEngine.notifyWork();
+                                }
+                            }
                         }
-                    }
 
-                    if (!processPendingComplete && (_fullyWritten || 
getBufferedSize() < _port.getNetworkBufferSize()))
-                    {
-                        _protocolEngine.notifyWork();
-                    }
+                        if (!_closed.get())
+                        {
+                            if (!processPendingComplete &&
+                                    (_fullyWritten || getBufferedSize() < 
_port.getNetworkBufferSize()))
+                            {
+                                _protocolEngine.notifyWork();
+                            }
 
-                    
_protocolEngine.setTransportBlockedForWriting(!_fullyWritten);
+                            
_protocolEngine.setTransportBlockedForWriting(!_fullyWritten);
+                        }
+                    }
                 }
 
             }
@@ -358,7 +398,7 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
 
         final int networkBufferSize = _port.getNetworkBufferSize();
 
-        while(_pendingIterator.hasNext())
+        while (!_closed.get() && !isCloseDeadlineExpired() && 
_pendingIterator.hasNext())
         {
             long size = getBufferedSize();
             if(size >= networkBufferSize)
@@ -377,6 +417,11 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
             }
         }
 
+        if (_closed.get() || isCloseDeadlineExpired())
+        {
+            return false;
+        }
+
         boolean complete = !_pendingIterator.hasNext();
         if (getBufferedSize() >= networkBufferSize)
         {
@@ -400,10 +445,16 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
 
         try
         {
-            shutdownInput();
-            shutdownFinalWrite();
-            _protocolEngine.closed();
-            shutdownOutput();
+            try
+            {
+                shutdownInput();
+                shutdownFinalWrite();
+                _protocolEngine.closed();
+            }
+            finally
+            {
+                shutdownOutput();
+            }
         }
         finally
         {
@@ -443,7 +494,7 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
         {
             final long startTime = System.currentTimeMillis();
             int cnt = 0;
-            while (!doWrite())
+            while (!checkForWriteAbort() && !doWrite())
             {
                 if (cnt % _finalWriteThreshold == 0 && 
System.currentTimeMillis() - startTime > _finalWriteTimeout)
                 {
@@ -459,6 +510,21 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
         }
     }
 
+    private boolean isCloseDeadlineExpired()
+    {
+        final Ticker deadline = _closeDeadline;
+        return deadline != null && 
deadline.getTimeToNextTick(System.currentTimeMillis()) <= 0;
+    }
+
+    boolean checkForWriteAbort()
+    {
+        if (!_aborted && isCloseDeadlineExpired())
+        {
+            abort();
+        }
+        return _aborted;
+    }
+
     private void shutdownOutput()
     {
         try
@@ -538,6 +604,10 @@ public class NonBlockingConnection implements 
ServerNetworkConnection, ByteBuffe
 
     long writeToTransport(final Collection<QpidByteBuffer> buffers) throws 
IOException
     {
+        if (checkForWriteAbort())
+        {
+            return 0L;
+        }
         final long written = QpidByteBuffer.write(_socketChannel, buffers, 
_maxGatheringWriteBuffers);
         if (LOGGER.isDebugEnabled())
         {
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 d860a8216c..8175552aac 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
@@ -139,7 +139,7 @@ public class NonBlockingConnectionTLSDelegate implements 
NonBlockingConnectionDe
                 LOGGER.debug("SSLEngine closed, discarded {} byte(s)", 
remaining);
             }
 
-            tasksRun = runSSLEngineTasks(_status);
+            tasksRun = runSSLEngineTasks(_status, false);
             if(_applicationBuffer.position() > oldAppBufPos)
             {
                 readData = true;
@@ -175,11 +175,15 @@ public class NonBlockingConnectionTLSDelegate implements 
NonBlockingConnectionDe
     }
 
     @Override
-    public WriteResult doWrite(Collection<QpidByteBuffer> buffers) throws 
IOException
+    public WriteResult doWrite(final Collection<QpidByteBuffer> buffers) 
throws IOException
     {
         final int bufCount = buffers.size();
 
-        int totalConsumed = wrapBufferArray(buffers);
+        final int totalConsumed = wrapBufferArray(buffers);
+        if (_parent.checkForWriteAbort())
+        {
+            return new WriteResult(false, totalConsumed);
+        }
 
         boolean bufsSent = true;
         final Iterator<QpidByteBuffer> itr = buffers.iterator();
@@ -257,12 +261,16 @@ public class NonBlockingConnectionTLSDelegate implements 
NonBlockingConnectionDe
 
     }
 
-    private int wrapBufferArray(Collection<QpidByteBuffer> buffers) throws 
SSLException
+    private int wrapBufferArray(final Collection<QpidByteBuffer> buffers) 
throws SSLException
     {
         int totalConsumed = 0;
         boolean encrypted;
         do
         {
+            if (_parent.checkForWriteAbort())
+            {
+                return totalConsumed;
+            }
             if(_sslEngine.getHandshakeStatus() != 
SSLEngineResult.HandshakeStatus.NEED_UNWRAP)
             {
                 if(_netOutputBuffer.remaining() < 
_sslEngine.getSession().getPacketBufferSize())
@@ -297,7 +305,11 @@ public class NonBlockingConnectionTLSDelegate implements 
NonBlockingConnectionDe
                 }
                 encrypted = _status.bytesProduced() > 0;
                 totalConsumed += _status.bytesConsumed();
-                runSSLEngineTasks(_status);
+                runSSLEngineTasks(_status, true);
+                if (_parent.checkForWriteAbort())
+                {
+                    return totalConsumed;
+                }
                 if(encrypted && _netOutputBuffer.remaining() < 
_sslEngine.getSession().getPacketBufferSize())
                 {
                     _netOutputBuffer.flip();
@@ -352,12 +364,12 @@ public class NonBlockingConnectionTLSDelegate implements 
NonBlockingConnectionDe
         return totalConsumed;
     }
 
-    private boolean runSSLEngineTasks(final SSLEngineResult status)
+    private boolean runSSLEngineTasks(final SSLEngineResult status, final 
boolean writing)
     {
         if(status.getHandshakeStatus() == 
SSLEngineResult.HandshakeStatus.NEED_TASK)
         {
             Runnable task;
-            while((task = _sslEngine.getDelegatedTask()) != null)
+            while ((!writing || !_parent.checkForWriteAbort()) && (task = 
_sslEngine.getDelegatedTask()) != null)
             {
                 task.run();
             }
@@ -461,7 +473,11 @@ public class NonBlockingConnectionTLSDelegate implements 
NonBlockingConnectionDe
     @Override
     public void shutdownOutput()
     {
-
+        for (final QpidByteBuffer buffer : _encryptedOutput)
+        {
+            buffer.dispose();
+        }
+        _encryptedOutput.clear();
         if (_netOutputBuffer != null)
         {
             _netOutputBuffer.dispose();
diff --git 
a/broker-core/src/main/java/org/apache/qpid/server/transport/ServerNetworkConnection.java
 
b/broker-core/src/main/java/org/apache/qpid/server/transport/ServerNetworkConnection.java
index eab77cb883..ba75cc9739 100644
--- 
a/broker-core/src/main/java/org/apache/qpid/server/transport/ServerNetworkConnection.java
+++ 
b/broker-core/src/main/java/org/apache/qpid/server/transport/ServerNetworkConnection.java
@@ -23,9 +23,19 @@ import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 
 import org.apache.qpid.server.transport.network.NetworkConnection;
+import org.apache.qpid.server.transport.network.Ticker;
 
 public interface ServerNetworkConnection extends NetworkConnection
 {
+    default void setCloseDeadline(final Ticker deadline)
+    {
+    }
+
+    default void abort()
+    {
+        close();
+    }
+
     String getTransportInfo();
 
     long getScheduledTime();
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/protocol/ConnectionClosingTickerTest.java
 
b/broker-core/src/test/java/org/apache/qpid/server/protocol/ConnectionClosingTickerTest.java
new file mode 100644
index 0000000000..c93711f7c5
--- /dev/null
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/protocol/ConnectionClosingTickerTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+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.transport.ServerNetworkConnection;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class ConnectionClosingTickerTest extends UnitTestBase
+{
+    private final AtomicLong _nanoTime = new AtomicLong();
+    private final ServerNetworkConnection _network = 
mock(ServerNetworkConnection.class);
+
+    @BeforeEach
+    void setUp()
+    {
+        _nanoTime.set(0);
+        reset(_network);
+    }
+
+    @Test
+    void expiresAtDeadlineAndRoundsUpUntilThen()
+    {
+        final ConnectionClosingTicker ticker = new 
ConnectionClosingTicker(1000, _network, _nanoTime::get);
+        assertEquals(1000, ticker.tick(0));
+        _nanoTime.set(TimeUnit.MILLISECONDS.toNanos(999) + 1);
+        assertEquals(1, ticker.tick(0));
+        verify(_network, never()).abort();
+
+        _nanoTime.set(TimeUnit.MILLISECONDS.toNanos(1000));
+        assertEquals(0, ticker.tick(0));
+        verify(_network).abort();
+        verify(_network, never()).close();
+    }
+
+    @Test
+    void wallClockChangesAndWorkerSchedulingDoNotExtendDeadline()
+    {
+        when(_network.getScheduledTime()).thenReturn(1L);
+        final ConnectionClosingTicker ticker = new 
ConnectionClosingTicker(1000, _network, _nanoTime::get);
+        _nanoTime.set(TimeUnit.MILLISECONDS.toNanos(500));
+        assertEquals(500, ticker.getTimeToNextTick(Long.MIN_VALUE));
+        assertEquals(500, ticker.getTimeToNextTick(Long.MAX_VALUE));
+        _nanoTime.set(TimeUnit.MILLISECONDS.toNanos(1000));
+        assertEquals(0, ticker.tick(0));
+        verify(_network).abort();
+    }
+
+    @Test
+    void nanoTimeWrapDoesNotExtendDeadline()
+    {
+        final long start = Long.MAX_VALUE - TimeUnit.MILLISECONDS.toNanos(500);
+        _nanoTime.set(start);
+        final ConnectionClosingTicker ticker = new 
ConnectionClosingTicker(1000, _network, _nanoTime::get);
+        _nanoTime.set(start + TimeUnit.MILLISECONDS.toNanos(750));
+        assertEquals(250, ticker.tick(0));
+        _nanoTime.set(start + TimeUnit.MILLISECONDS.toNanos(1000));
+        assertEquals(0, ticker.tick(0));
+        verify(_network).abort();
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {0, -1, Long.MIN_VALUE})
+    void nonPositiveTimeoutExpiresImmediately(final long timeout)
+    {
+        final ConnectionClosingTicker ticker = new 
ConnectionClosingTicker(timeout, _network, _nanoTime::get);
+        assertEquals(0, ticker.tick(0));
+        verify(_network).abort();
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {Integer.MAX_VALUE, (long) Integer.MAX_VALUE + 1, 
Long.MAX_VALUE})
+    void largeTimeoutDoesNotOverflowToImmediateExpiry(final long timeout)
+    {
+        final ConnectionClosingTicker ticker = new 
ConnectionClosingTicker(timeout, _network, _nanoTime::get);
+        assertEquals(Integer.MAX_VALUE, ticker.tick(0));
+        verify(_network, never()).abort();
+    }
+}
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/transport/NativeCloseDeadlineTestBase.java
 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NativeCloseDeadlineTestBase.java
new file mode 100644
index 0000000000..c9fe2e4253
--- /dev/null
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NativeCloseDeadlineTestBase.java
@@ -0,0 +1,279 @@
+/*
+ * 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.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createPort;
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createSocketChannel;
+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.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+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.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.IntStream;
+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.MethodSource;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.configuration.updater.CurrentThreadTaskExecutor;
+import org.apache.qpid.server.configuration.updater.TaskExecutor;
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.BrokerModel;
+import org.apache.qpid.server.model.Connection;
+import org.apache.qpid.server.model.Port;
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.model.SystemConfig;
+import org.apache.qpid.server.model.Transport;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.plugin.ProtocolEngineCreator;
+import org.apache.qpid.server.transport.network.Ticker;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.server.util.Action;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+public abstract class NativeCloseDeadlineTestBase extends UnitTestBase
+{
+    protected static final int NETWORK_BUFFER_SIZE = 1024;
+
+    private final AtomicBoolean _writable = new AtomicBoolean();
+
+    protected Broker<?> _broker;
+    protected AmqpPort<?> _port;
+    protected NonBlockingConnection _network;
+    protected AggregateTicker _ticker;
+
+    private SocketChannel _socket;
+    private TaskExecutor _taskExecutor;
+    private MultiVersionProtocolEngine _engine;
+
+    @BeforeEach
+    void setUpNativeTransport() throws Exception
+    {
+        _writable.set(false);
+        _taskExecutor = CurrentThreadTaskExecutor.newStartedInstance();
+        final SystemConfig<?> systemConfig = mock(SystemConfig.class);
+        when(systemConfig.getEventLogger()).thenReturn(new EventLogger());
+        _broker = mock(Broker.class);
+        doReturn(systemConfig).when(_broker).getParent();
+        doReturn(Broker.class).when(_broker).getCategoryClass();
+        when(_broker.getModel()).thenReturn(BrokerModel.getInstance());
+        when(_broker.getTaskExecutor()).thenReturn(_taskExecutor);
+        when(_broker.getChildExecutor()).thenReturn(_taskExecutor);
+        when(_broker.getEventLogger()).thenReturn(new EventLogger());
+        when(_broker.getNetworkBufferSize()).thenReturn(NETWORK_BUFFER_SIZE);
+
+        _port = createPort(_broker, NETWORK_BUFFER_SIZE, 0L);
+        doReturn(Port.class).when(_port).getCategoryClass();
+        when(_port.getModel()).thenReturn(BrokerModel.getInstance());
+        when(_port.getTaskExecutor()).thenReturn(_taskExecutor);
+        when(_port.getChildExecutor()).thenReturn(_taskExecutor);
+        setCloseTimeout(30_000);
+        final AuthenticationProvider<?> authenticationProvider = 
mock(AuthenticationProvider.class);
+        
when(authenticationProvider.getAvailableMechanisms(anyBoolean())).thenReturn(List.of("PLAIN"));
+        
doReturn(authenticationProvider).when(_port).getAuthenticationProvider();
+
+        _socket = createSocketChannel(_writable::get);
+        _engine = new MultiVersionProtocolEngine(_broker, Set.of(), null, 
_port, Transport.TCP, 1L,
+                new ProtocolEngineCreator[0], null);
+        _ticker = _engine.getAggregateTicker();
+        _network = new NonBlockingConnection(_socket, _engine, 
Set.of(TransportEncryption.NONE), () -> { },
+                mock(NetworkConnectionScheduler.class), _port);
+        _network.setSelectionTask(mock(SelectorThread.SelectionTask.class));
+    }
+
+    @AfterEach
+    void tearDownNativeTransport()
+    {
+        try
+        {
+            if (_network != null)
+            {
+                _writable.set(true);
+                _network.close();
+                _network.doWork();
+            }
+        }
+        finally
+        {
+            _taskExecutor.stop();
+        }
+    }
+
+    protected abstract AMQPConnection<?> createConnection(final Protocol 
protocol) throws Exception;
+
+    protected abstract void receivePeerClose(final AMQPConnection<?> 
connection) throws Exception;
+
+    protected abstract Stream<Protocol> protocols();
+
+    protected void setCloseTimeout(final long timeoutMillis)
+    {
+        
when(_port.getContext()).thenReturn(Map.of(Connection.CLOSE_RESPONSE_TIMEOUT, 
Long.toString(timeoutMillis)));
+    }
+
+    protected Stream<Arguments> protocolsAndSizes()
+    {
+        return protocols().flatMap(protocol -> Stream.of(NETWORK_BUFFER_SIZE - 
1, NETWORK_BUFFER_SIZE,
+                2 * NETWORK_BUFFER_SIZE).map(size -> Arguments.of(protocol, 
size)));
+    }
+
+    protected void installProtocol(final ProtocolEngine protocol) throws 
Exception
+    {
+        final Field delegate = 
MultiVersionProtocolEngine.class.getDeclaredField("_delegate");
+        delegate.setAccessible(true);
+        delegate.set(_engine, protocol);
+        final Field listener = 
MultiVersionProtocolEngine.class.getDeclaredField("_workListener");
+        listener.setAccessible(true);
+        protocol.setWorkListener(((AtomicReference<Action<ProtocolEngine>>) 
listener.get(_engine)).get());
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocolsAndSizes")
+    protected void testRequestedCloseHasDeadlineWithBlockedOutput(final 
Protocol protocol, final int bufferedBytes)
+            throws Exception
+    {
+        setCloseTimeout(0);
+        final AMQPConnection<?> connection = createConnection(protocol);
+        fillOutput(bufferedBytes);
+        
connection.sendConnectionCloseAsync(AMQPConnection.CloseReason.MANAGEMENT, 
"close");
+
+        assertTrue(_network.doWork(), protocol + ": requested close remained 
pending with " + bufferedBytes +
+                " buffered bytes; next ticker=" + 
_ticker.getTimeToNextTick(System.currentTimeMillis()));
+        verify(_socket).close();
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocols")
+    protected void testCloseResumesAfterSocketDrains(final Protocol protocol) 
throws Exception
+    {
+        final AMQPConnection<?> connection = createConnection(protocol);
+        fillOutput(NETWORK_BUFFER_SIZE);
+        
connection.sendConnectionCloseAsync(AMQPConnection.CloseReason.MANAGEMENT, 
"close");
+        assertFalse(_network.doWork());
+        verify(_socket, never()).close();
+
+        _writable.set(true);
+        assertFalse(_network.doWork());
+        assertTrue(_ticker.getTimeToNextTick(System.currentTimeMillis()) < 
Integer.MAX_VALUE,
+                "Draining the socket must retain the deadline while awaiting 
the peer's close response");
+        assertTrue(connection.isClosing(), "The protocol close must be sent 
when output can drain");
+        verify(_socket, never()).close();
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocols")
+    protected void testPeerCloseIsBoundedWithBlockedOutput(final Protocol 
protocol) throws Exception
+    {
+        setCloseTimeout(0);
+        final AMQPConnection<?> connection = createConnection(protocol);
+        fillOutput(128);
+        _engine.setIOThread(Thread.currentThread());
+        try
+        {
+            receivePeerClose(connection);
+        }
+        finally
+        {
+            _engine.setIOThread(null);
+        }
+        assertEquals(1, getTickers().size(), "Peer close must start the same 
bounded closing phase");
+        assertTrue(_network.doWork(), protocol + ": peer close did not reach 
native shutdown while output was paused");
+        verify(_socket).close();
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocols")
+    protected void testPeerCloseDuringReadSkipsExpiredWrite(final Protocol 
protocol) throws Exception
+    {
+        setCloseTimeout(0);
+        final AMQPConnection<?> connection = createConnection(protocol);
+        _writable.set(true);
+        when(_socket.read(any(ByteBuffer.class))).thenAnswer(invocation ->
+        {
+            // Invoke the protocol handler within doRead so its deadline 
starts after the initial expiry check.
+            receivePeerClose(connection);
+            return 0;
+        });
+
+        assertTrue(_network.doWork(), protocol + ": close received during read 
must finish in this work cycle");
+
+        verify(_socket).read(any(ByteBuffer.class));
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        verify(_socket).close();
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocols")
+    protected void testRepeatedCloseKeepsOriginalDeadline(final Protocol 
protocol) throws Exception
+    {
+        final AMQPConnection<?> connection = createConnection(protocol);
+        fillOutput(NETWORK_BUFFER_SIZE);
+        IntStream.range(0, 32).parallel().forEach(i ->
+                
connection.sendConnectionCloseAsync(AMQPConnection.CloseReason.MANAGEMENT, 
"First close"));
+        final List<Ticker> tickers = getTickers();
+        assertEquals(1, tickers.size(), "Closing must arm a deadline before 
any queued task runs");
+        final Ticker original = tickers.get(0);
+
+        IntStream.range(0, 32).parallel().forEach(i ->
+                
connection.sendConnectionCloseAsync(AMQPConnection.CloseReason.MANAGEMENT, 
"Repeated close"));
+        _writable.set(true);
+        _network.doWork();
+
+        assertEquals(1, tickers.size(), "Repeated requests and the close task 
must not add another timer");
+        assertSame(original, tickers.get(0));
+    }
+
+    private List<Ticker> getTickers() throws ReflectiveOperationException
+    {
+        final Field tickers = 
AggregateTicker.class.getDeclaredField("_tickers");
+        tickers.setAccessible(true);
+        return (List<Ticker>) tickers.get(_ticker);
+    }
+
+    private void fillOutput(final int size)
+    {
+        try (final QpidByteBuffer buffer = QpidByteBuffer.wrap(new byte[size]))
+        {
+            _network.send(buffer);
+        }
+        assertFalse(_network.doWork());
+        assertTrue(_network.wantsWrite(), "The native socket must retain 
unwritten output");
+        assertEquals(Integer.MAX_VALUE, 
_ticker.getTimeToNextTick(System.currentTimeMillis()));
+    }
+}
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionCloseSchedulingTest.java
 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionCloseSchedulingTest.java
new file mode 100644
index 0000000000..1d7e0e09f5
--- /dev/null
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionCloseSchedulingTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createPort;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+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.protocol.ConnectionClosingTicker;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.server.util.Action;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionCloseSchedulingTest extends UnitTestBase
+{
+    private static final int TIMEOUT_MILLIS = 10000;
+    private final List<Thread> _threads = new CopyOnWriteArrayList<>();
+    private NetworkConnectionScheduler _scheduler;
+
+    @BeforeEach
+    void startScheduler()
+    {
+        _threads.clear();
+        _scheduler = new NetworkConnectionScheduler(getTestName(), 1, 2, 1L, 
task ->
+        {
+            final Thread thread = new Thread(task, getTestName() + "-" + 
_threads.size());
+            _threads.add(thread);
+            return thread;
+        });
+        _scheduler.start();
+    }
+
+    @AfterEach
+    void stopScheduler() throws InterruptedException
+    {
+        _scheduler.close();
+        try
+        {
+            for (final Thread thread : _threads)
+            {
+                thread.join(TIMEOUT_MILLIS);
+                assertFalse(thread.isAlive(), "Scheduler thread did not 
terminate: " + thread.getName());
+            }
+        }
+        finally
+        {
+            for (final Thread thread : _threads)
+            {
+                thread.interrupt();
+            }
+            for (final Thread thread : _threads)
+            {
+                thread.join(TIMEOUT_MILLIS);
+            }
+        }
+    }
+
+    @Test
+    void deadlineClosesBlockedSocketWithoutPeerActivity() throws Exception
+    {
+        final AggregateTicker ticker = new AggregateTicker();
+        final ProtocolEngine engine = createProtocolEngine(ticker);
+        final CompletableFuture<Void> blocked = new CompletableFuture<>();
+        final CompletableFuture<Void> closed = new CompletableFuture<>();
+        try (final ServerSocketChannel listener = ServerSocketChannel.open();
+             final Socket client = new Socket();
+             final SocketChannel channel = acceptConnection(listener, client))
+        {
+            channel.configureBlocking(false);
+            channel.socket().setSendBufferSize(4096);
+            final NonBlockingConnection connection = createConnection(channel, 
engine, blocked, closed);
+            final AtomicBoolean pendingTaskRan = new AtomicBoolean();
+            final List<Runnable> work = List.of(() ->
+            {
+                try (final QpidByteBuffer output = QpidByteBuffer.wrap(new 
byte[2 * 1024 * 1024]))
+                {
+                    connection.send(output);
+                }
+            }, () -> pendingTaskRan.set(true));
+            when(engine.processPendingIterator()).thenReturn(work.iterator());
+            _scheduler.addConnection(connection);
+            try
+            {
+                engine.notifyWork();
+                blocked.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+                ticker.addTicker(new ConnectionClosingTicker(100, connection));
+                engine.notifyWork();
+                closed.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+
+                assertFalse(pendingTaskRan.get(), "The socket must close while 
protocol work is still blocked");
+                assertFalse(channel.isOpen());
+            }
+            finally
+            {
+                connection.abort();
+                closed.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+            }
+        }
+    }
+
+    private SocketChannel acceptConnection(final ServerSocketChannel listener, 
final Socket client) throws IOException
+    {
+        listener.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 
0));
+        client.setReceiveBufferSize(4096);
+        client.connect(listener.getLocalAddress(), TIMEOUT_MILLIS);
+        return listener.accept();
+    }
+
+    private ProtocolEngine createProtocolEngine(final AggregateTicker ticker)
+    {
+        final ProtocolEngine engine = mock(ProtocolEngine.class);
+        final AtomicBoolean hasWork = new AtomicBoolean();
+        final AtomicReference<Action<ProtocolEngine>> workListener = new 
AtomicReference<>();
+        when(engine.getAggregateTicker()).thenReturn(ticker);
+        when(engine.hasWork()).thenAnswer(invocation -> hasWork.get());
+        doAnswer(invocation ->
+        {
+            hasWork.set(false);
+            return null;
+        }).when(engine).clearWork();
+        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();
+        return engine;
+    }
+
+    private NonBlockingConnection createConnection(final SocketChannel channel,
+                                                   final ProtocolEngine engine,
+                                                   final 
CompletableFuture<Void> blocked,
+                                                   final 
CompletableFuture<Void> closed)
+    {
+        return new NonBlockingConnection(channel, engine, 
Set.of(TransportEncryption.NONE), () -> { },
+                _scheduler, createPort(1024, 1000L))
+        {
+            @Override
+            public boolean doWork()
+            {
+                final boolean result = super.doWork();
+                if (result)
+                {
+                    closed.complete(null);
+                }
+                else if (wantsWrite())
+                {
+                    blocked.complete(null);
+                }
+                return result;
+            }
+        };
+    }
+}
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionCloseTest.java
 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionCloseTest.java
new file mode 100644
index 0000000000..a753abb6c8
--- /dev/null
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionCloseTest.java
@@ -0,0 +1,770 @@
+/*
+ * 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.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createPort;
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createSocketChannel;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+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.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+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.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.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.InOrder;
+
+import org.apache.qpid.server.bytebuffer.QpidByteBuffer;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.protocol.ConnectionClosingTicker;
+import org.apache.qpid.server.transport.network.Ticker;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class NonBlockingConnectionCloseTest extends UnitTestBase
+{
+    private static final int BUFFER_SIZE = 1024;
+    private final AtomicBoolean _writable = new AtomicBoolean();
+    private AggregateTicker _ticker;
+    private SocketChannel _socket;
+    private ProtocolEngine _engine;
+    private NetworkConnectionScheduler _scheduler;
+    private SelectorThread.SelectionTask _selectionTask;
+    private AmqpPort<?> _port;
+    private SSLEngine _sslEngine;
+    private NonBlockingConnection _connection;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        _ticker = new AggregateTicker();
+        _writable.set(false);
+        _connection = null;
+        _sslEngine = null;
+        QpidByteBuffer.deinitialisePool();
+        QpidByteBuffer.initialisePool(BUFFER_SIZE, 16, 0.5);
+        _socket = createSocketChannel(_writable::get);
+        _engine = mock(ProtocolEngine.class);
+        when(_engine.getAggregateTicker()).thenReturn(_ticker);
+        when(_engine.processPendingIterator()).thenAnswer(invocation -> 
Collections.emptyIterator());
+        _scheduler = mock(NetworkConnectionScheduler.class);
+        _selectionTask = mock(SelectorThread.SelectionTask.class);
+        _port = createPort(BUFFER_SIZE, 1000L);
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        try
+        {
+            if (_connection != null)
+            {
+                _connection.abort();
+                _connection.doWork();
+            }
+        }
+        finally
+        {
+            QpidByteBuffer.deinitialisePool();
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void expiredDeadlineDiscardsOutputWithoutFinalWrite(final 
TransportEncryption encryption) throws Exception
+    {
+        createConnection(encryption);
+        fillOutput();
+        _ticker.addTicker(new ConnectionClosingTicker(0, _connection));
+        clearInvocations(_socket, _engine, _selectionTask);
+
+        assertTrue(_connection.doWork());
+        assertTrue(_connection.doWork(), "Shutdown must be idempotent");
+
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        verify(_engine, never()).processPendingIterator();
+        verify(_selectionTask).wakeup();
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void deadlineExpiresDuringPendingWork(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        final Runnable pending = mock(Runnable.class);
+        final List<Runnable> work = List.of(() ->
+        {
+            queueOutput();
+            expired.set(true);
+        }, pending);
+        when(_engine.processPendingIterator()).thenReturn(work.iterator());
+
+        assertTrue(_connection.doWork(), "Deadline expiry must stop the active 
pending-work loop");
+
+        verify(pending, never()).run();
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void deadlineInstalledByLastPendingTaskDiscardsOutput(final 
TransportEncryption encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        final List<Runnable> work = List.of(() ->
+        {
+            queueOutput();
+            _connection.setCloseDeadline(new ConnectionClosingTicker(0, 
_connection));
+        });
+        when(_engine.processPendingIterator()).thenReturn(work.iterator());
+
+        assertTrue(_connection.doWork(), "A deadline installed during 
processing must prevent the final pending write");
+
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @CsvSource({"NONE, true", "NONE, false", "TLS, true", "TLS, false"})
+    void deadlineExpiresDuringSuccessfulWrite(final TransportEncryption 
encryption,
+                                              final boolean pendingWork) 
throws Exception
+    {
+        createConnection(encryption);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        queueOutput();
+        final Runnable pending = mock(Runnable.class);
+        when(_engine.processPendingIterator()).thenReturn(pendingWork ?
+                List.of(pending).iterator() : Collections.emptyIterator());
+        when(_socket.write(any(ByteBuffer[].class))).thenAnswer(invocation ->
+        {
+            long written = 0;
+            for (final ByteBuffer buffer : (ByteBuffer[]) 
invocation.getArgument(0))
+            {
+                written += buffer.remaining();
+                buffer.position(buffer.limit());
+            }
+            expired.set(true);
+            return written;
+        });
+
+        assertTrue(_connection.doWork(), "Successful writes must not postpone 
deadline enforcement");
+
+        verify(pending, never()).run();
+        verify(_socket).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void abortDuringPendingWorkStopsFurtherTasks(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        final Runnable pending = mock(Runnable.class);
+        final List<Runnable> work = List.of(() ->
+        {
+            queueOutput();
+            _connection.abort();
+        }, pending);
+        when(_engine.processPendingIterator()).thenReturn(work.iterator());
+
+        assertTrue(_connection.doWork());
+
+        verify(pending, never()).run();
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @Test
+    void deadlineExpiresAfterLastTlsWriteSkipsFurtherIo() throws Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        try (final QpidByteBuffer output = 
QpidByteBuffer.allocateDirect(BUFFER_SIZE))
+        {
+            _connection.send(output);
+        }
+        when(_socket.write(any(ByteBuffer[].class))).thenAnswer(invocation ->
+        {
+            long written = 0;
+            for (final ByteBuffer buffer : (ByteBuffer[]) 
invocation.getArgument(0))
+            {
+                written += buffer.remaining();
+                buffer.position(buffer.limit());
+            }
+            expired.set(true);
+            // Only wrapping after expiry matters, regardless of how the 
output was batched.
+            clearInvocations(_sslEngine);
+            return written;
+        });
+
+        assertTrue(_connection.doWork());
+
+        verify(_socket).write(any(ByteBuffer[].class));
+        verify(_sslEngine, never()).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        verify(_engine, never()).setTransportBlockedForWriting(anyBoolean());
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @CsvSource({"false, 512", "false, 2048", "true, 512", "true, 2048"})
+    void deadlineExpiresDuringTlsWrap(final boolean finalWrite, final int 
bufferedBytes) throws Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        _writable.set(true);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        final Runnable delegatedTask = mock(Runnable.class);
+        when(_sslEngine.getDelegatedTask()).thenReturn(delegatedTask, 
(Runnable) null);
+        doAnswer(invocation ->
+        {
+            final SSLEngineResult result = 
wrapTlsOutput(invocation.getArgument(0), invocation.getArgument(1),
+                    SSLEngineResult.HandshakeStatus.NEED_TASK);
+            expired.set(true);
+            return result;
+        }).when(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        queueOutput(bufferedBytes);
+        if (finalWrite)
+        {
+            _connection.close();
+        }
+
+        assertTrue(_connection.doWork());
+
+        assertAll(() -> verify(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class)),
+                  () -> verify(delegatedTask, never()).run(),
+                  () -> verify(_socket, 
never()).write(any(ByteBuffer[].class)),
+                  this::assertClosedAndReleased);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void deadlineExpiresDuringLastTlsWrap(final boolean finalWrite) throws 
Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        _writable.set(true);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        doAnswer(invocation ->
+        {
+            final SSLEngineResult result = 
wrapTlsOutput(invocation.getArgument(0), invocation.getArgument(1),
+                    SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
+            
when(_sslEngine.getHandshakeStatus()).thenReturn(SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
+            expired.set(true);
+            return result;
+        }).when(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        queueOutput(BUFFER_SIZE / 2);
+        if (finalWrite)
+        {
+            _connection.close();
+        }
+
+        assertTrue(_connection.doWork());
+
+        verify(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void abortDuringTlsWrapStopsFurtherOutput(final boolean finalWrite) throws 
Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        _writable.set(true);
+        doAnswer(invocation ->
+        {
+            final SSLEngineResult result = 
wrapTlsOutput(invocation.getArgument(0), invocation.getArgument(1),
+                    SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING);
+            _connection.abort();
+            return result;
+        }).when(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        queueOutput();
+        if (finalWrite)
+        {
+            _connection.close();
+        }
+
+        assertTrue(_connection.doWork());
+
+        assertAll(() -> verify(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class)),
+                  () -> verify(_socket, 
never()).write(any(ByteBuffer[].class)),
+                  this::assertClosedAndReleased);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void deadlineExpiresDuringTlsTask(final boolean finalWrite) throws 
Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        _writable.set(true);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        final Runnable laterTask = mock(Runnable.class);
+        when(_sslEngine.getDelegatedTask()).thenReturn(() -> 
expired.set(true), laterTask, null);
+        doAnswer(invocation -> wrapTlsOutput(invocation.getArgument(0), 
invocation.getArgument(1),
+                SSLEngineResult.HandshakeStatus.NEED_TASK))
+                .when(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        queueOutput();
+        if (finalWrite)
+        {
+            _connection.close();
+        }
+
+        assertTrue(_connection.doWork());
+
+        assertAll(() -> verify(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class)),
+                  () -> verify(laterTask, never()).run(),
+                  () -> verify(_socket, 
never()).write(any(ByteBuffer[].class)),
+                  this::assertClosedAndReleased);
+    }
+
+    @Test
+    void gracefulTlsCloseWrapsOutputAndRunsTasksBeforeDeadline() throws 
Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        _writable.set(true);
+        installDeadline(new AtomicBoolean());
+        final Runnable delegatedTask = mock(Runnable.class);
+        when(_sslEngine.getDelegatedTask()).thenReturn(delegatedTask, 
(Runnable) null);
+        doAnswer(invocation -> wrapTlsOutput(invocation.getArgument(0), 
invocation.getArgument(1),
+                SSLEngineResult.HandshakeStatus.NEED_TASK))
+                .when(_sslEngine).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        queueOutput();
+        _connection.close();
+
+        assertTrue(_connection.doWork());
+
+        verify(delegatedTask).run();
+        verify(_socket).write(any(ByteBuffer[].class));
+        assertFalse(_connection.wantsWrite(), "Graceful close must wrap and 
drain the plaintext backlog");
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void gracefulCloseDuringPendingWorkFlushesOutput(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        final Runnable pending = mock(Runnable.class);
+        final List<Runnable> work = List.of(() ->
+        {
+            queueOutput();
+            _connection.close();
+        }, pending);
+        when(_engine.processPendingIterator()).thenReturn(work.iterator());
+
+        assertTrue(_connection.doWork());
+
+        verify(pending, never()).run();
+        verify(_socket).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @Test
+    void expiryDoesNotWaitForTlsUnwrap() throws Exception
+    {
+        createConnection(TransportEncryption.TLS);
+        
when(_sslEngine.getHandshakeStatus()).thenReturn(SSLEngineResult.HandshakeStatus.NEED_UNWRAP);
+        fillOutput();
+        _ticker.addTicker(new ConnectionClosingTicker(0, _connection));
+        clearInvocations(_socket, _sslEngine);
+
+        assertTrue(_connection.doWork());
+
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        verify(_sslEngine, never()).wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void abortCanOvertakeGracefulTransportClose(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        fillOutput();
+        _connection.close();
+        _connection.abort();
+        clearInvocations(_socket);
+
+        assertTrue(_connection.doWork());
+
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void finalWriteRetriesCannotOutliveProtocolDeadline(final 
TransportEncryption encryption) throws Exception
+    {
+        createConnection(encryption);
+        fillOutput();
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        doAnswer(invocation ->
+        {
+            expired.set(true);
+            return 0L;
+        }).when(_socket).write(any(ByteBuffer[].class));
+        _connection.close();
+        clearInvocations(_socket);
+
+        assertTrue(_connection.doWork());
+
+        verify(_socket).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @Test
+    void repeatedDeadlineDoesNotExtendFinalWrite() throws Exception
+    {
+        createConnection(TransportEncryption.NONE);
+        fillOutput();
+        _connection.setCloseDeadline(new ConnectionClosingTicker(0, 
_connection));
+        _connection.setCloseDeadline(new ConnectionClosingTicker(30_000, 
_connection));
+        _connection.close();
+        clearInvocations(_socket);
+
+        assertTrue(_connection.doWork());
+
+        verify(_socket, never()).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void gracefulTransportCloseStillFlushesOutput(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        fillOutput();
+        _writable.set(true);
+        clearInvocations(_socket);
+        _connection.close();
+
+        assertTrue(_connection.doWork());
+
+        verify(_socket).write(any(ByteBuffer[].class));
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void directTransportCloseIsBounded(final TransportEncryption encryption) 
throws Exception
+    {
+        when(_port.getContextValue(Long.class, 
AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(0L);
+        createConnection(encryption);
+        fillOutput();
+        _connection.close();
+
+        assertTrue(_connection.doWork(), "Direct native close must reach the 
final-write timeout");
+
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void abortReleasesOutputEvenIfProtocolCleanupFails(final 
TransportEncryption encryption) throws Exception
+    {
+        createConnection(encryption);
+        fillOutput();
+        doThrow(new IllegalStateException("Protocol cleanup 
failed")).when(_engine).closed();
+        _connection.abort();
+
+        assertThrows(IllegalStateException.class, _connection::doWork);
+
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void expiredDeadlineInstalledDuringReadDiscardsOutput(final 
TransportEncryption encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        receiveDuringRead(() ->
+        {
+            queueOutput();
+            final Ticker deadline = new ConnectionClosingTicker(0, 
_connection);
+            _connection.setCloseDeadline(deadline);
+            _ticker.addTicker(deadline);
+            _connection.close();
+        });
+
+        final boolean closed = _connection.doWork();
+
+        verify(_engine).received(any(QpidByteBuffer.class));
+        assertNoIoAfterReadClose(closed);
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void deadlineExpiresDuringReadDiscardsOutput(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        final AtomicBoolean expired = new AtomicBoolean();
+        installDeadline(expired);
+        receiveDuringRead(() ->
+        {
+            queueOutput();
+            expired.set(true);
+        });
+
+        final boolean closed = _connection.doWork();
+
+        verify(_engine).received(any(QpidByteBuffer.class));
+        assertNoIoAfterReadClose(closed);
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void abortDuringReadDiscardsOutput(final TransportEncryption encryption) 
throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        receiveDuringRead(() ->
+        {
+            queueOutput();
+            _connection.abort();
+        });
+
+        final boolean closed = _connection.doWork();
+
+        verify(_engine).received(any(QpidByteBuffer.class));
+        assertNoIoAfterReadClose(closed);
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void gracefulCloseDuringReadFlushesOutput(final TransportEncryption 
encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        receiveDuringRead(() ->
+        {
+            queueOutput();
+            _connection.close();
+        });
+
+        assertTrue(_connection.doWork());
+
+        verify(_engine).received(any(QpidByteBuffer.class));
+        verify(_socket, atLeastOnce()).write(any(ByteBuffer[].class));
+        final InOrder shutdownOrder = inOrder(_engine, _socket);
+        shutdownOrder.verify(_engine).setIOThread(null);
+        shutdownOrder.verify(_socket, 
atLeastOnce()).write(any(ByteBuffer[].class));
+        verify(_engine, never()).setTransportBlockedForWriting(anyBoolean());
+        assertFalse(_connection.wantsWrite(), "Graceful close must drain 
queued output");
+        assertClosedAndReleased();
+    }
+
+    @ParameterizedTest
+    @EnumSource(TransportEncryption.class)
+    void unexpiredDeadlineInstalledDuringReadAllowsWrite(final 
TransportEncryption encryption) throws Exception
+    {
+        createConnection(encryption);
+        _writable.set(true);
+        receiveDuringRead(() ->
+        {
+            queueOutput();
+            installDeadline(new AtomicBoolean());
+        });
+
+        assertFalse(_connection.doWork());
+
+        verify(_engine).received(any(QpidByteBuffer.class));
+        verify(_socket).write(any(ByteBuffer[].class));
+        verify(_socket, never()).close();
+    }
+
+    private void receiveDuringRead(final Runnable action) throws Exception
+    {
+        // Supply a TLS application-data record; the mocked unwrap below 
produces one application byte.
+        final byte[] record = {23, 3, 3, 0, 1, 1};
+        when(_socket.read(any(ByteBuffer.class))).thenAnswer(invocation ->
+        {
+            final ByteBuffer target = invocation.getArgument(0);
+            target.put(record);
+            return record.length;
+        });
+        if (_sslEngine != null)
+        {
+            when(_sslEngine.unwrap(any(ByteBuffer.class), 
any(ByteBuffer.class))).thenAnswer(invocation ->
+            {
+                final ByteBuffer source = invocation.getArgument(0);
+                final ByteBuffer target = invocation.getArgument(1);
+                final int consumed = source.remaining();
+                source.position(source.limit());
+                target.put((byte) 1);
+                return new SSLEngineResult(SSLEngineResult.Status.OK,
+                        SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 
consumed, 1);
+            });
+        }
+        doAnswer(invocation ->
+        {
+            final QpidByteBuffer input = invocation.getArgument(0);
+            input.position(input.limit());
+            action.run();
+            return null;
+        }).when(_engine).received(any(QpidByteBuffer.class));
+    }
+
+    private void assertNoIoAfterReadClose(final boolean closed)
+    {
+        assertAll(() -> assertTrue(closed, "Read-side closure must finish in 
the same work cycle"),
+                  () -> verify(_socket, 
never()).write(any(ByteBuffer[].class)),
+                  () ->
+                  {
+                      if (_sslEngine != null)
+                      {
+                          verify(_sslEngine, 
never()).wrap(any(ByteBuffer[].class), any(ByteBuffer.class));
+                      }
+                  },
+                  () -> verify(_engine, 
never()).setTransportBlockedForWriting(anyBoolean()),
+                  this::assertClosedAndReleased);
+    }
+
+    private void assertClosedAndReleased() throws IOException
+    {
+        verify(_socket).close();
+        verify(_engine).closed();
+        verify(_scheduler).removeConnection(_connection);
+        assertEquals(0, QpidByteBuffer.getNumberOfBuffersInUse(), "Shutdown 
must release buffers");
+    }
+
+    private void createConnection(final TransportEncryption encryption) throws 
Exception
+    {
+        if (encryption == TransportEncryption.TLS)
+        {
+            configureTls();
+        }
+        _connection = new NonBlockingConnection(_socket, _engine, 
Set.of(encryption), () -> { }, _scheduler, _port);
+        _connection.setSelectionTask(_selectionTask);
+    }
+
+    private void configureTls() throws Exception
+    {
+        final SSLContext sslContext = mock(SSLContext.class);
+        _sslEngine = mock(SSLEngine.class);
+        final SSLSession session = mock(SSLSession.class);
+        when(_port.getSSLContext()).thenReturn(sslContext);
+        when(sslContext.createSSLEngine()).thenReturn(_sslEngine);
+        when(_sslEngine.getSession()).thenReturn(session);
+        when(_sslEngine.getEnabledProtocols()).thenReturn(new 
String[]{"TLSv1.3"});
+        when(_sslEngine.getSupportedProtocols()).thenReturn(new 
String[]{"TLSv1.3"});
+        when(_sslEngine.getEnabledCipherSuites()).thenReturn(new 
String[]{"TLS_AES_128_GCM_SHA256"});
+        when(_sslEngine.getSupportedCipherSuites()).thenReturn(new 
String[]{"TLS_AES_128_GCM_SHA256"});
+        when(session.getPacketBufferSize()).thenReturn(BUFFER_SIZE);
+        when(session.getApplicationBufferSize()).thenReturn(BUFFER_SIZE);
+        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(0);
+        when(_port.getContextValue(Integer.class, 
AmqpPort.PORT_DIAGNOSIS_OF_SSL_ENGINE_LOOPING_BREAK_THRESHOLD))
+                .thenReturn(0);
+        
when(_sslEngine.getHandshakeStatus()).thenReturn(SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING);
+        when(_sslEngine.wrap(any(ByteBuffer[].class), 
any(ByteBuffer.class))).thenAnswer(invocation ->
+                wrapTlsOutput(invocation.getArgument(0), 
invocation.getArgument(1),
+                        SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING));
+    }
+
+    private SSLEngineResult wrapTlsOutput(final ByteBuffer[] sources,
+                                          final ByteBuffer target,
+                                          final 
SSLEngineResult.HandshakeStatus handshakeStatus)
+    {
+        int count = 0;
+        for (int i = 0; i < sources.length && target.hasRemaining(); i++)
+        {
+            while (sources[i].hasRemaining() && target.hasRemaining())
+            {
+                target.put(sources[i].get());
+                count++;
+            }
+        }
+        return new SSLEngineResult(SSLEngineResult.Status.OK, handshakeStatus, 
count, count);
+    }
+
+    private void fillOutput()
+    {
+        queueOutput();
+        assertFalse(_connection.doWork());
+        assertTrue(_connection.wantsWrite() || _connection.wantsRead(),
+                "Blocked output must wait for socket capacity or TLS peer 
input");
+        assertTrue(QpidByteBuffer.getNumberOfBuffersInUse() > 0);
+    }
+
+    private void queueOutput()
+    {
+        queueOutput(2 * BUFFER_SIZE);
+    }
+
+    private void queueOutput(final int size)
+    {
+        try (final QpidByteBuffer buffer = 
QpidByteBuffer.allocateDirect(Math.max(BUFFER_SIZE, size)))
+        {
+            buffer.limit(size);
+            _connection.send(buffer);
+        }
+    }
+
+    private void installDeadline(final AtomicBoolean expired)
+    {
+        final Ticker deadline = mock(Ticker.class);
+        when(deadline.getTimeToNextTick(anyLong())).thenAnswer(invocation -> 
expired.get() ? 0 : 1000);
+        _connection.setCloseDeadline(deadline);
+        _ticker.addTicker(deadline);
+    }
+}
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
index 3532a38769..042c34409e 100644
--- 
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionExceptionTest.java
@@ -21,17 +21,16 @@
 
 package org.apache.qpid.server.transport;
 
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createPort;
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createSocketChannel;
 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.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import java.net.InetSocketAddress;
-import java.net.Socket;
 import java.nio.ByteBuffer;
 import java.nio.channels.SocketChannel;
 import java.util.Collections;
@@ -41,8 +40,6 @@ 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.server.util.ConnectionScopedRuntimeException;
@@ -56,14 +53,8 @@ class NonBlockingConnectionExceptionTest extends UnitTestBase
     @BeforeEach
     void setUp()
     {
-        final Broker<?> broker = mock(Broker.class);
-        when(broker.getEventLogger()).thenReturn(mock(EventLogger.class));
-        _port = mock(AmqpPort.class);
-        doReturn(broker).when(_port).getParent();
-        when(_port.getNetworkBufferSize()).thenReturn(128);
+        _port = createPort(128, 100L);
         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);
         _scheduler = mock(NetworkConnectionScheduler.class);
     }
 
@@ -72,8 +63,8 @@ class NonBlockingConnectionExceptionTest extends UnitTestBase
     {
         final ProtocolEngine failingEngine = mock(ProtocolEngine.class);
         final ProtocolEngine healthyEngine = mock(ProtocolEngine.class);
-        final SocketChannel failingChannel = mock(SocketChannel.class);
-        final SocketChannel healthyChannel = mock(SocketChannel.class);
+        final SocketChannel failingChannel = createSocketChannel(() -> false);
+        final SocketChannel healthyChannel = createSocketChannel(() -> false);
         final NonBlockingConnection failing = createConnection(failingEngine, 
failingChannel);
         final NonBlockingConnection healthy = createConnection(healthyEngine, 
healthyChannel);
         final ArithmeticException failure = new ArithmeticException("Injected 
decoding failure");
@@ -101,10 +92,6 @@ class NonBlockingConnectionExceptionTest extends 
UnitTestBase
     private NonBlockingConnection createConnection(final ProtocolEngine 
engine, final SocketChannel channel)
             throws Exception
     {
-        final Socket socket = mock(Socket.class);
-        when(socket.getRemoteSocketAddress()).thenReturn(new 
InetSocketAddress("localhost", 1000));
-        when(socket.getLocalSocketAddress()).thenReturn(new 
InetSocketAddress("localhost", 5672));
-        when(channel.socket()).thenReturn(socket);
         when(channel.read(any(ByteBuffer.class))).thenAnswer(invocation ->
         {
             final ByteBuffer input = invocation.getArgument(0);
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
index 2d344a2ba5..13c259b03c 100644
--- 
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
@@ -19,13 +19,14 @@
 
 package org.apache.qpid.server.transport;
 
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createPort;
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createSocketChannel;
 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;
@@ -33,9 +34,6 @@ 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;
@@ -50,8 +48,6 @@ 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;
@@ -63,15 +59,9 @@ class NonBlockingConnectionIdleTimeoutTest extends 
UnitTestBase
     private NonBlockingConnection _connection;
 
     @BeforeEach
-    void beforeEach()
+    void beforeEach() throws Exception
     {
-        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);
+        final SocketChannel socketChannel = createSocketChannel(() -> false);
 
         _ticker = mock(AggregateTicker.class);
         _protocolEngine = mock(ProtocolEngine.class);
@@ -79,15 +69,8 @@ class NonBlockingConnectionIdleTimeoutTest extends 
UnitTestBase
         when(_protocolEngine.processPendingIterator()).thenAnswer(invocation 
-> Collections.emptyIterator());
 
         final NetworkConnectionScheduler scheduler = 
mock(NetworkConnectionScheduler.class);
-        final AmqpPort<?> port = mock(AmqpPort.class);
-        when(port.getNetworkBufferSize()).thenReturn(1024);
+        final AmqpPort<?> port = createPort(1024, 100L);
         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);
@@ -237,6 +220,10 @@ class NonBlockingConnectionIdleTimeoutTest extends 
UnitTestBase
     {
         final Field delegateField = 
NonBlockingConnection.class.getDeclaredField("_delegate");
         delegateField.setAccessible(true);
+        final NonBlockingConnectionDelegate originalDelegate =
+                (NonBlockingConnectionDelegate) delegateField.get(_connection);
+        originalDelegate.shutdownInput();
+        originalDelegate.shutdownOutput();
         delegateField.set(_connection, delegate);
     }
 
diff --git 
a/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTestHelper.java
 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTestHelper.java
new file mode 100644
index 0000000000..4a704b9e9a
--- /dev/null
+++ 
b/broker-core/src/test/java/org/apache/qpid/server/transport/NonBlockingConnectionTestHelper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+import java.nio.channels.SocketChannel;
+import java.util.function.BooleanSupplier;
+
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.port.AmqpPort;
+
+final class NonBlockingConnectionTestHelper
+{
+    private NonBlockingConnectionTestHelper()
+    {
+        // utility class has private constructor
+    }
+
+    static AmqpPort<?> createPort(final int bufferSize, final long 
finalWriteTimeout)
+    {
+        final Broker<?> broker = mock(Broker.class);
+        when(broker.getEventLogger()).thenReturn(new EventLogger());
+        return createPort(broker, bufferSize, finalWriteTimeout);
+    }
+
+    static AmqpPort<?> createPort(final Broker<?> broker, final int 
bufferSize, final long finalWriteTimeout)
+    {
+        final AmqpPort<?> port = mock(AmqpPort.class);
+        doReturn(broker).when(port).getParent();
+        when(port.getNetworkBufferSize()).thenReturn(bufferSize);
+        when(port.getContextValue(Integer.class, 
AmqpPort.FINAL_WRITE_THRESHOLD)).thenReturn(1);
+        when(port.getContextValue(Long.class, 
AmqpPort.FINAL_WRITE_TIMEOUT)).thenReturn(finalWriteTimeout);
+        when(port.getContextValue(Integer.class, 
AmqpPort.MAX_GATHERING_WRITE_BUFFERS)).thenReturn(1024);
+        return port;
+    }
+
+    static SocketChannel createSocketChannel(final BooleanSupplier writable) 
throws IOException
+    {
+        final Socket socket = mock(Socket.class);
+        when(socket.getRemoteSocketAddress()).thenReturn(new 
InetSocketAddress("localhost", 10000));
+        when(socket.getLocalSocketAddress()).thenReturn(new 
InetSocketAddress("localhost", 5672));
+        final SocketChannel channel = mock(SocketChannel.class);
+        when(channel.socket()).thenReturn(socket);
+        when(channel.write(any(ByteBuffer[].class))).thenAnswer(invocation ->
+        {
+            long written = 0;
+            if (writable.getAsBoolean())
+            {
+                for (final ByteBuffer buffer : (ByteBuffer[]) 
invocation.getArgument(0))
+                {
+                    written += buffer.remaining();
+                    buffer.position(buffer.limit());
+                }
+            }
+            return written;
+        });
+        return channel;
+    }
+}
diff --git 
a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnection.java
 
b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnection.java
index 9702be4759..ed37fe8e36 100644
--- 
a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnection.java
+++ 
b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnection.java
@@ -52,7 +52,6 @@ import org.apache.qpid.server.model.Broker;
 import org.apache.qpid.server.model.NamedAddressSpace;
 import org.apache.qpid.server.model.Transport;
 import org.apache.qpid.server.model.port.AmqpPort;
-import org.apache.qpid.server.protocol.ConnectionClosingTicker;
 import org.apache.qpid.server.protocol.ErrorCodes;
 import org.apache.qpid.server.protocol.v0_10.transport.Binary;
 import org.apache.qpid.server.protocol.v0_10.transport.ConnectionClose;
@@ -69,7 +68,6 @@ import 
org.apache.qpid.server.protocol.v0_10.transport.SessionDetachCode;
 import org.apache.qpid.server.protocol.v0_10.transport.SessionDetached;
 import org.apache.qpid.server.session.AMQPSession;
 import org.apache.qpid.server.transport.AMQPConnection;
-import org.apache.qpid.server.transport.ServerNetworkConnection;
 import org.apache.qpid.server.transport.network.NetworkConnection;
 import org.apache.qpid.server.util.Action;
 import org.apache.qpid.server.util.ServerScopedRuntimeException;
@@ -164,14 +162,9 @@ public class ServerConnection extends ConnectionInvoker
             lock.notifyAll();
         }
 
-        if(state == State.CLOSING)
+        if (state == State.CLOSING || state == State.CLOSE_RCVD)
         {
-            long timeoutTime = System.currentTimeMillis() + 
getAmqpConnection().getContextValue(Long.class, 
org.apache.qpid.server.model.Connection.CLOSE_RESPONSE_TIMEOUT);
-
-            getAmqpConnection().getAggregateTicker().addTicker(new 
ConnectionClosingTicker(timeoutTime, (ServerNetworkConnection) 
getNetworkConnection()));
-
-            // trigger a wakeup to ensure the ticker will be taken into account
-            getAmqpConnection().notifyWork();
+            getAmqpConnection().startCloseDeadline();
         }
     }
 
@@ -290,6 +283,7 @@ public class ServerConnection extends ConnectionInvoker
 
     void sendConnectionCloseAsync(final ConnectionCloseCode replyCode, final 
String message)
     {
+        getAmqpConnection().startCloseDeadline();
         addAsyncTask(object ->
         {
             if(!isClosing())
@@ -649,6 +643,7 @@ public class ServerConnection extends ConnectionInvoker
 
     protected void sendConnectionClose(ConnectionCloseCode replyCode, String 
replyText, Option... _options)
     {
+        getAmqpConnection().startCloseDeadline();
         connectionClose(replyCode, replyText, _options);
     }
 
diff --git 
a/broker-plugins/amqp-0-10-protocol/src/test/java/org/apache/qpid/server/protocol/v0_10/NativeCloseDeadline_0_10Test.java
 
b/broker-plugins/amqp-0-10-protocol/src/test/java/org/apache/qpid/server/protocol/v0_10/NativeCloseDeadline_0_10Test.java
new file mode 100644
index 0000000000..93c8aae86a
--- /dev/null
+++ 
b/broker-plugins/amqp-0-10-protocol/src/test/java/org/apache/qpid/server/protocol/v0_10/NativeCloseDeadline_0_10Test.java
@@ -0,0 +1,92 @@
+/*
+ * 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.assertTrue;
+
+import java.lang.reflect.Field;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.model.Transport;
+import org.apache.qpid.server.protocol.v0_10.transport.ConnectionClose;
+import org.apache.qpid.server.protocol.v0_10.transport.ConnectionCloseCode;
+import org.apache.qpid.server.protocol.v0_10.transport.ConnectionStartOk;
+import org.apache.qpid.server.protocol.v0_10.transport.ProtocolHeader;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.transport.NativeCloseDeadlineTestBase;
+
+class NativeCloseDeadline_0_10Test extends NativeCloseDeadlineTestBase
+{
+    @ParameterizedTest
+    @NullAndEmptySource
+    @ValueSource(strings = "UNKNOWN")
+    void testAuthenticationRejectionHasCloseDeadline(final String mechanism) 
throws Exception
+    {
+        setCloseTimeout(0);
+        final AMQPConnection_0_10Impl connection = new 
AMQPConnection_0_10Impl(_broker, _network, _port, Transport.TCP,
+                1L, _ticker);
+        installProtocol(connection);
+        final ServerConnection server = getServerConnection(connection);
+        final ServerConnectionDelegate delegate = 
server.getConnectionDelegate();
+        delegate.init(server, new ProtocolHeader(1, 0, 10));
+
+        final ConnectionStartOk startOk = new ConnectionStartOk();
+        startOk.setMechanism(mechanism);
+        delegate.connectionStartOk(server, startOk);
+
+        assertTrue(_network.doWork(), "Authentication rejection must honor the 
configured close deadline");
+    }
+
+    @Override
+    protected void receivePeerClose(final AMQPConnection<?> connection) throws 
Exception
+    {
+        final ServerConnection server = getServerConnection(connection);
+        final ConnectionClose connectionClose = new 
ConnectionClose(ConnectionCloseCode.NORMAL,
+                "Peer close reproduction");
+        server.getConnectionDelegate().connectionClose(server, 
connectionClose);
+    }
+
+    @Override
+    protected AMQPConnection<?> createConnection(final Protocol protocol) 
throws Exception
+    {
+        final AMQPConnection_0_10Impl connection = new 
AMQPConnection_0_10Impl(_broker, _network, _port, Transport.TCP,
+                1L, _ticker);
+        getServerConnection(connection).setState(ServerConnection.State.OPEN);
+        installProtocol(connection);
+        return connection;
+    }
+
+    @Override
+    protected Stream<Protocol> protocols()
+    {
+        return Stream.of(Protocol.AMQP_0_10);
+    }
+
+    private ServerConnection getServerConnection(final AMQPConnection<?> 
connection) throws ReflectiveOperationException
+    {
+        final Field delegate = 
AMQPConnection_0_10Impl.class.getDeclaredField("_connection");
+        delegate.setAccessible(true);
+        return (ServerConnection) delegate.get(connection);
+    }
+}
diff --git 
a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
 
b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
index f21a1a7abe..8708716a02 100644
--- 
a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
+++ 
b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
@@ -55,7 +55,6 @@ import org.apache.qpid.server.model.Transport;
 import org.apache.qpid.server.model.port.AmqpPort;
 import org.apache.qpid.server.plugin.ConnectionPropertyEnricher;
 import org.apache.qpid.server.properties.ConnectionStartProperties;
-import org.apache.qpid.server.protocol.ConnectionClosingTicker;
 import org.apache.qpid.server.protocol.ErrorCodes;
 import org.apache.qpid.server.protocol.ProtocolVersion;
 import org.apache.qpid.server.protocol.v0_8.transport.AMQDataBlock;
@@ -604,6 +603,7 @@ public class AMQPConnection_0_8Impl
 
     private void sendConnectionClose(final int channelId, final AMQFrame frame)
     {
+        startCloseDeadline();
         if (_orderlyClose.compareAndSet(false, true))
         {
             try
@@ -613,19 +613,7 @@ public class AMQPConnection_0_8Impl
             }
             finally
             {
-                try
-                {
-                    writeFrame(frame);
-                }
-                finally
-                {
-                    final long timeoutTime = System.currentTimeMillis()
-                                             + getContextValue(Long.class, 
Connection.CLOSE_RESPONSE_TIMEOUT);
-
-                    getAggregateTicker().addTicker(new 
ConnectionClosingTicker(timeoutTime, getNetwork()));
-                    // trigger a wakeup to ensure the ticker will be taken 
into account
-                    notifyWork();
-                }
+                writeFrame(frame);
             }
         }
     }
@@ -866,6 +854,7 @@ public class AMQPConnection_0_8Impl
     public void sendConnectionCloseAsync(final CloseReason reason, final 
String description)
     {
         stopConnection();
+        startCloseDeadline();
         final int cause;
         switch(reason)
         {
@@ -1102,6 +1091,7 @@ public class AMQPConnection_0_8Impl
 
         try
         {
+            startCloseDeadline();
             if (_orderlyClose.compareAndSet(false, true))
             {
                 completeAndCloseAllChannels();
diff --git 
a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
 
b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
index db01465aab..1b3728edb1 100644
--- 
a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
+++ 
b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Test.java
@@ -608,6 +608,7 @@ class AMQPConnection_0_8Test extends UnitTestBase
     @Test
     void testIncompleteDiscardRetainsCloseTimeout()
     {
+        
when(_port.getContext()).thenReturn(Map.of(Connection.CLOSE_RESPONSE_TIMEOUT, 
"0"));
         final AMQPConnection_0_8Impl connection = new 
AMQPConnection_0_8Impl(_broker, _network, _port, _transport,
                 _protocol, 0, _ticker);
         openConnection(connection, ProtocolVersion.v0_8);
@@ -623,9 +624,8 @@ class AMQPConnection_0_8Test extends UnitTestBase
             assertFalse(frame.hasRemaining());
             verify(_network, never()).close();
 
-            final long closeTimeout = connection.getContextValue(Long.class, 
Connection.CLOSE_RESPONSE_TIMEOUT);
-            _ticker.tick(System.currentTimeMillis() + closeTimeout + 1);
-            verify(_network).close();
+            _ticker.tick(System.currentTimeMillis());
+            verify(_network).abort();
         }
     }
 
diff --git 
a/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/NativeCloseDeadline_0_8Test.java
 
b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/NativeCloseDeadline_0_8Test.java
new file mode 100644
index 0000000000..fb2d80a620
--- /dev/null
+++ 
b/broker-plugins/amqp-0-8-protocol/src/test/java/org/apache/qpid/server/protocol/v0_8/NativeCloseDeadline_0_8Test.java
@@ -0,0 +1,66 @@
+/*
+ * 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_8;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.stream.Stream;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.model.Transport;
+import org.apache.qpid.server.protocol.ProtocolVersion;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.transport.NativeCloseDeadlineTestBase;
+
+class NativeCloseDeadline_0_8Test extends NativeCloseDeadlineTestBase
+{
+    @Override
+    protected void receivePeerClose(final AMQPConnection<?> connection)
+    {
+        ((AMQPConnection_0_8Impl) connection).receiveConnectionClose(200, 
null, 0, 0);
+    }
+
+    @Override
+    protected AMQPConnection<?> createConnection(final Protocol protocol) 
throws Exception
+    {
+        final AMQPConnection_0_8Impl connection = new 
AMQPConnection_0_8Impl(_broker, _network, _port, Transport.TCP,
+                protocol, 1L, _ticker);
+        final ProtocolVersion version = switch (protocol)
+        {
+            case AMQP_0_8 -> ProtocolVersion.v0_8;
+            case AMQP_0_9 -> ProtocolVersion.v0_9;
+            default -> ProtocolVersion.v0_91;
+        };
+        final Method setVersion = 
AMQPConnection_0_8Impl.class.getDeclaredMethod("setProtocolVersion",
+                ProtocolVersion.class);
+        setVersion.setAccessible(true);
+        setVersion.invoke(connection, version);
+        final Field state = 
AMQPConnection_0_8Impl.class.getDeclaredField("_state");
+        state.setAccessible(true);
+        state.set(connection, AMQPConnection_0_8Impl.ConnectionState.OPEN);
+        installProtocol(connection);
+        return connection;
+    }
+
+    @Override
+    protected Stream<Protocol> protocols()
+    {
+        return Stream.of(Protocol.AMQP_0_8, Protocol.AMQP_0_9, 
Protocol.AMQP_0_9_1);
+    }
+}
diff --git 
a/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
 
b/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
index 4b34581a53..880838e584 100644
--- 
a/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
+++ 
b/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
@@ -60,7 +60,6 @@ import org.apache.qpid.server.model.Protocol;
 import org.apache.qpid.server.model.Transport;
 import org.apache.qpid.server.model.port.AmqpPort;
 import org.apache.qpid.server.plugin.ConnectionPropertyEnricher;
-import org.apache.qpid.server.protocol.ConnectionClosingTicker;
 import 
org.apache.qpid.server.protocol.v1_0.codec.DescribedTypeConstructorRegistry;
 import org.apache.qpid.server.protocol.v1_0.codec.FrameWriter;
 import org.apache.qpid.server.protocol.v1_0.codec.ProtocolHandler;
@@ -372,7 +371,7 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
         _saslComplete = true;
         disposeSaslNegotiator();
         _connectionState = ConnectionState.CLOSED;
-        addCloseTicker();
+        startCloseDeadline();
     }
 
     private void disposeSaslNegotiator()
@@ -514,6 +513,7 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
                         "Connection close sent before connection was opened");
                 break;
             case OPENED:
+                startCloseDeadline();
                 _connectionState = ConnectionState.CLOSE_RECEIVED;
                 closeReceived();
                 if(close.getError() != null)
@@ -527,7 +527,6 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
                 sendClose(new Close());
                 _connectionState = ConnectionState.CLOSED;
                 _orderlyClose.set(true);
-                addCloseTicker();
                 break;
             case CLOSE_SENT:
                 closeReceived();
@@ -969,14 +968,17 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
             error.setInfo(Map.of(Symbols.SOLE_CONNECTION_ENFORCEMENT, true));
 
             final EventLogger logger = getEventLogger();
-            final List<CompletableFuture<Void>> rescheduleFutures = new 
ArrayList<>();
+            final List<CompletableFuture<?>> rescheduleFutures = new 
ArrayList<>();
             for (final AMQPConnection_1_0<?> connection : 
e.getExistingConnections())
             {
                 if (!connection.isClosing())
                 {
                     LOGGER.debug("Closing existing connection '{}': {}",
                             connection.getName(), e.getMessage());
-                    rescheduleFutures.add(connection.doOnIOThreadAsync(() -> 
connection.close(error)));
+                    connection.stopConnection();
+                    connection.startCloseDeadline();
+                    
rescheduleFutures.add(CompletableFuture.anyOf(connection.getTransportClosedFuture(),
+                            connection.doOnIOThreadAsync(() -> 
connection.close(error))));
                     logger.message(ResourceLimitMessages.INFO(
                             String.format("Closing existing connection '%s'", 
connection.getName()), e.getMessage()));
                 }
@@ -1158,12 +1160,10 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
             case OPENED:
                 sendClose(close);
                 _connectionState = ConnectionState.CLOSE_SENT;
-                addCloseTicker();
                 break;
             case CLOSE_RECEIVED:
                 sendClose(close);
                 _connectionState = ConnectionState.CLOSED;
-                addCloseTicker();
                 break;
             case CLOSE_SENT:
             case CLOSED:
@@ -1458,16 +1458,6 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
         }
     }
 
-    private void addCloseTicker()
-    {
-        long timeoutTime = System.currentTimeMillis() + 
getContextValue(Long.class, Connection.CLOSE_RESPONSE_TIMEOUT);
-
-        getAggregateTicker().addTicker(new 
ConnectionClosingTicker(timeoutTime, getNetwork()));
-
-        // trigger a wakeup to ensure the ticker will be taken into account
-        notifyWork();
-    }
-
     @Override
     public boolean isTransportBlockedForWriting()
     {
@@ -1544,8 +1534,8 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
     @Override
     public void sendConnectionCloseAsync(final CloseReason reason, final 
String description)
     {
-
         stopConnection();
+        startCloseDeadline();
         final ErrorCondition cause;
         switch(reason)
         {
@@ -1753,6 +1743,7 @@ public class AMQPConnection_1_0Impl extends 
AbstractAMQPConnection<AMQPConnectio
 
     private void sendClose(Close closeToSend)
     {
+        startCloseDeadline();
         sendFrame(CONNECTION_CONTROL_CHANNEL, closeToSend);
         closeSender();
     }
diff --git 
a/broker-plugins/amqp-1-0-protocol/src/test/java/org/apache/qpid/server/protocol/v1_0/NativeCloseDeadline_1_0Test.java
 
b/broker-plugins/amqp-1-0-protocol/src/test/java/org/apache/qpid/server/protocol/v1_0/NativeCloseDeadline_1_0Test.java
new file mode 100644
index 0000000000..5febff3d35
--- /dev/null
+++ 
b/broker-plugins/amqp-1-0-protocol/src/test/java/org/apache/qpid/server/protocol/v1_0/NativeCloseDeadline_1_0Test.java
@@ -0,0 +1,55 @@
+/*
+ * 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.v1_0;
+
+import java.lang.reflect.Field;
+import java.util.stream.Stream;
+
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.model.Transport;
+import org.apache.qpid.server.protocol.v1_0.type.transport.Close;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.transport.NativeCloseDeadlineTestBase;
+
+class NativeCloseDeadline_1_0Test extends NativeCloseDeadlineTestBase
+{
+    @Override
+    protected void receivePeerClose(final AMQPConnection<?> connection)
+    {
+        ((AMQPConnection_1_0Impl) connection).receiveClose(0, new Close());
+    }
+
+    @Override
+    protected AMQPConnection<?> createConnection(final Protocol protocol) 
throws Exception
+    {
+        final AMQPConnection_1_0Impl connection = new 
AMQPConnection_1_0Impl(_broker, _network, _port, Transport.TCP,
+                1L, _ticker);
+        final Field state = 
AMQPConnection_1_0Impl.class.getDeclaredField("_connectionState");
+        state.setAccessible(true);
+        state.set(connection, ConnectionState.OPENED);
+        installProtocol(connection);
+        return connection;
+    }
+
+    @Override
+    protected Stream<Protocol> protocols()
+    {
+        return Stream.of(Protocol.AMQP_1_0);
+    }
+}
diff --git 
a/broker-plugins/amqp-1-0-protocol/src/test/java/org/apache/qpid/server/transport/AMQPConnection_1_0CloseExistingTest.java
 
b/broker-plugins/amqp-1-0-protocol/src/test/java/org/apache/qpid/server/transport/AMQPConnection_1_0CloseExistingTest.java
new file mode 100644
index 0000000000..fc6b32cc24
--- /dev/null
+++ 
b/broker-plugins/amqp-1-0-protocol/src/test/java/org/apache/qpid/server/transport/AMQPConnection_1_0CloseExistingTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createPort;
+import static 
org.apache.qpid.server.transport.NonBlockingConnectionTestHelper.createSocketChannel;
+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.isNull;
+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.io.IOException;
+import java.nio.channels.SocketChannel;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.security.auth.Subject;
+
+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.Broker;
+import org.apache.qpid.server.model.BrokerTestHelper;
+import org.apache.qpid.server.model.Connection;
+import org.apache.qpid.server.model.NamedAddressSpace;
+import org.apache.qpid.server.model.Port;
+import org.apache.qpid.server.model.Protocol;
+import org.apache.qpid.server.model.Transport;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.plugin.ProtocolEngineCreator;
+import org.apache.qpid.server.protocol.v1_0.AMQPConnection_1_0Impl;
+import org.apache.qpid.server.protocol.v1_0.ProtocolEngineCreator_1_0_0;
+import org.apache.qpid.server.protocol.v1_0.constants.Bytes;
+import org.apache.qpid.server.protocol.v1_0.constants.Symbols;
+import org.apache.qpid.server.protocol.v1_0.type.Symbol;
+import 
org.apache.qpid.server.protocol.v1_0.type.extensions.soleconn.SoleConnectionEnforcementPolicy;
+import 
org.apache.qpid.server.protocol.v1_0.type.extensions.soleconn.StrongConnectionEstablishmentLimiter;
+import org.apache.qpid.server.protocol.v1_0.type.transport.Open;
+import org.apache.qpid.server.security.SubjectCreator;
+import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
+import 
org.apache.qpid.server.security.auth.manager.AnonymousAuthenticationManager;
+import org.apache.qpid.server.security.limit.ConnectionSlot;
+import org.apache.qpid.server.session.AMQPSession;
+import org.apache.qpid.server.transport.network.TransportEncryption;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+class AMQPConnection_1_0CloseExistingTest extends UnitTestBase
+{
+    private static final int BUFFER_SIZE = 1024;
+
+    private final List<ConnectionFixture> _connections = new ArrayList<>();
+    private Broker<?> _broker;
+    private AmqpPort<?> _port;
+
+    @BeforeEach
+    void setUp()
+    {
+        _broker = BrokerTestHelper.createNewBrokerMock();
+        when(_broker.getNetworkBufferSize()).thenReturn(BUFFER_SIZE);
+        when(_broker.getContextValue(Integer.class, 
Connection.MAX_MESSAGE_SIZE))
+                .thenReturn(Connection.DEFAULT_MAX_MESSAGE_SIZE);
+        when(_broker.getContextValue(Boolean.class, 
Broker.BROKER_MSG_AUTH)).thenReturn(false);
+        when(_broker.getContextValue(Integer.class, 
Broker.MESSAGE_COMPRESSION_THRESHOLD_SIZE)).thenReturn(0);
+        _port = createPort(_broker, BUFFER_SIZE, 1000L);
+        when(_port.getContextValue(Integer.class, Connection.MAX_MESSAGE_SIZE))
+                .thenReturn(Connection.DEFAULT_MAX_MESSAGE_SIZE);
+        doReturn(_broker.getModel()).when(_port).getModel();
+        doReturn(_broker.getTaskExecutor()).when(_port).getTaskExecutor();
+        doReturn(_broker.getChildExecutor()).when(_port).getChildExecutor();
+        
when(_port.getContext()).thenReturn(Map.of(Connection.CLOSE_RESPONSE_TIMEOUT, 
"30000"));
+        when(_port.getContextValue(Long.class, 
Port.CONNECTION_MAXIMUM_AUTHENTICATION_DELAY)).thenReturn(30000L);
+
+        final AnonymousAuthenticationManager authenticationProvider = 
mock(AnonymousAuthenticationManager.class);
+        
when(authenticationProvider.getMechanisms()).thenReturn(List.of(AnonymousAuthenticationManager.MECHANISM_NAME));
+        when(authenticationProvider.getAvailableMechanisms(false))
+                
.thenReturn(List.of(AnonymousAuthenticationManager.MECHANISM_NAME));
+        when(authenticationProvider.getAnonymousPrincipal()).thenReturn(() -> 
"anonymous");
+        
doReturn(authenticationProvider).when(_port).getAuthenticationProvider();
+        final SubjectCreator subjectCreator = mock(SubjectCreator.class);
+        when(_port.getSubjectCreator(anyBoolean(), 
isNull())).thenReturn(subjectCreator);
+        when(subjectCreator.createSubjectWithGroups(any())).thenReturn(new 
Subject(true,
+                Set.of(new AuthenticatedPrincipal(() -> "anonymous")), 
Set.of(), Set.of()));
+
+        final NamedAddressSpace addressSpace = mock(NamedAddressSpace.class);
+        when(addressSpace.getId()).thenReturn(UUID.randomUUID());
+        when(addressSpace.getPrincipal()).thenReturn(() -> "test");
+        when(addressSpace.isActive()).thenReturn(true);
+        when(addressSpace.authoriseCreateConnection(any())).thenReturn(true);
+        when(_port.getAddressSpace("test")).thenReturn(addressSpace);
+        final StrongConnectionEstablishmentLimiter limiter = new 
StrongConnectionEstablishmentLimiter();
+        final Map<AMQPConnection<?>, ConnectionSlot> slots = new HashMap<>();
+        doAnswer(invocation ->
+        {
+            final AMQPConnection<?> connection = invocation.getArgument(0);
+            slots.put(connection, limiter.register(connection));
+            return null;
+        }).when(addressSpace).registerConnection(any());
+        doAnswer(invocation ->
+        {
+            final ConnectionSlot slot = 
slots.remove(invocation.getArgument(0));
+            if (slot != null)
+            {
+                slot.free();
+            }
+            return null;
+        }).when(addressSpace).deregisterConnection(any());
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        for (final ConnectionFixture connection : _connections)
+        {
+            connection._network.abort();
+            connection._network.doWork();
+        }
+        _connections.clear();
+    }
+
+    @Test
+    void closeExistingDeadlineAbortsBlockedConnectionAndResumesOpen() throws 
IOException
+    {
+        
when(_port.getContext()).thenReturn(Map.of(Connection.CLOSE_RESPONSE_TIMEOUT, 
"0"));
+        final ConnectionFixture existing = newConnection();
+        existing.open();
+        final CompletableFuture<Void> pending = existing.blockOutput();
+        final ConnectionFixture replacement = newConnection();
+
+        replacement.open();
+        assertFalse(replacement._connection.isOpen());
+        assertFalse(existing._connection.isClosing(), "The queued close has 
not run");
+
+        assertTrue(existing._network.doWork(), "The policy must install a 
deadline before queuing the close");
+        verify(existing._socket).close();
+        assertFalse(pending.isDone(), "Backpressure must prevent queued 
protocol work from running");
+        assertFalse(replacement._network.doWork());
+        assertTrue(replacement._connection.isOpen(), "Transport closure must 
release the waiting open");
+    }
+
+    @Test
+    void transportLossBeforeQueuedCloseResumesOpen() throws IOException
+    {
+        final ConnectionFixture existing = newConnection();
+        existing.open();
+        final CompletableFuture<Void> pending = existing.blockOutput();
+        final ConnectionFixture replacement = newConnection();
+        replacement.open();
+        assertFalse(replacement._connection.isOpen());
+        assertFalse(existing._network.doWork());
+        assertFalse(existing._connection.isClosing(), "Backpressure must 
prevent the close task from running");
+
+        existing._network.abort();
+        assertTrue(existing._network.doWork());
+        assertFalse(pending.isDone());
+        assertFalse(replacement._network.doWork());
+        assertTrue(replacement._connection.isOpen(), "Transport loss must 
complete the policy's waiting future");
+    }
+
+    @Test
+    void queuedCloseResumesOpenWithoutWaitingForTransportClosure() throws 
IOException
+    {
+        final ConnectionFixture existing = newConnection();
+        existing.open();
+        final ConnectionFixture replacement = newConnection();
+        replacement.open();
+        assertFalse(replacement._connection.isOpen());
+
+        assertFalse(existing._network.doWork());
+        assertTrue(existing._connection.isClosing());
+        assertFalse(existing._connection.isClosed());
+        verify(existing._socket, never()).close();
+        assertFalse(replacement._network.doWork());
+        assertTrue(replacement._connection.isOpen());
+    }
+
+    @Test
+    void queuedCloseTakesPrecedenceOverPendingSessionWork() throws IOException
+    {
+        final ConnectionFixture existing = newConnection();
+        existing.open();
+        final AMQPSession<?, ?> session = mock(AMQPSession.class);
+        when(session.processPending()).thenReturn(true, false);
+        existing._connection.notifyWork(session);
+        final ConnectionFixture replacement = newConnection();
+
+        replacement.open();
+        assertFalse(replacement._connection.isOpen());
+        assertFalse(existing._network.doWork());
+
+        verify(session, never()).processPending();
+        assertTrue(existing._connection.isClosing(), "Pending session work 
must not delay the queued close");
+        verify(existing._socket, never()).close();
+        assertFalse(replacement._network.doWork());
+        assertTrue(replacement._connection.isOpen(), "The replacement must 
open while session work remains pending");
+    }
+
+    private ConnectionFixture newConnection() throws IOException
+    {
+        final ConnectionFixture fixture = new 
ConnectionFixture(_connections.size());
+        _connections.add(fixture);
+        return fixture;
+    }
+
+    private final class ConnectionFixture
+    {
+        private final AtomicBoolean _writable = new AtomicBoolean(true);
+        private final SocketChannel _socket = 
createSocketChannel(_writable::get);
+        private final NonBlockingConnection _network;
+        private AMQPConnection_1_0Impl _connection;
+
+        private ConnectionFixture(final long id) throws IOException
+        {
+            final ProtocolEngineCreator creator = new 
ProtocolEngineCreator_1_0_0()
+            {
+                @Override
+                public ProtocolEngine newProtocolEngine(final Broker<?> broker,
+                                                        final 
ServerNetworkConnection networkConnection,
+                                                        final AmqpPort<?> port,
+                                                        final Transport 
transport,
+                                                        final long 
connectionId,
+                                                        final AggregateTicker 
ticker)
+                {
+                    _connection = (AMQPConnection_1_0Impl) 
super.newProtocolEngine(broker, networkConnection, port,
+                            transport, connectionId, ticker);
+                    return _connection;
+                }
+            };
+            final MultiVersionProtocolEngine engine = new 
MultiVersionProtocolEngine(_broker,
+                    Set.of(Protocol.AMQP_1_0), Protocol.AMQP_1_0, _port, 
Transport.TCP, id,
+                    new ProtocolEngineCreator[]{creator}, null);
+            _network = new NonBlockingConnection(_socket, engine, 
Set.of(TransportEncryption.NONE), () -> { },
+                    mock(NetworkConnectionScheduler.class), _port);
+            
_network.setSelectionTask(mock(SelectorThread.SelectionTask.class));
+            engine.setNetworkConnection(_network);
+            try (final QpidByteBuffer header = 
QpidByteBuffer.wrap(Bytes.amqpHeader()))
+            {
+                engine.received(header);
+            }
+        }
+
+        private void open()
+        {
+            final Open open = new Open();
+            open.setContainerId("shared-container");
+            open.setHostname("test");
+            open.setDesiredCapabilities(new 
Symbol[]{Symbols.SOLE_CONNECTION_FOR_CONTAINER});
+            
open.setProperties(Map.of(Symbols.SOLE_CONNECTION_ENFORCEMENT_POLICY,
+                    
SoleConnectionEnforcementPolicy.CLOSE_EXISTING.getValue()));
+            _connection.receiveOpen(0, open);
+        }
+
+        private CompletableFuture<Void> blockOutput()
+        {
+            assertTrue(_connection.isOpen());
+            _writable.set(false);
+            try (final QpidByteBuffer output = QpidByteBuffer.wrap(new byte[2 
* BUFFER_SIZE]))
+            {
+                _network.send(output);
+            }
+            final CompletableFuture<Void> pending = 
_connection.doOnIOThreadAsync(() -> { });
+            assertFalse(_network.doWork());
+            assertTrue(_network.wantsWrite());
+            assertFalse(pending.isDone());
+            return pending;
+        }
+    }
+}


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

Reply via email to