[activemq-nms-amqp] branch master updated: AMQNET-627: Transactions implementation adjustments

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

[activemq-nms-amqp] branch master updated: AMQNET-627: Transactions implementation adjustments

michaelpearce
This is an automated email from the ASF dual-hosted git repository.

michaelpearce pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/activemq-nms-amqp.git


The following commit(s) were added to refs/heads/master by this push:
     new 143b5b4  AMQNET-627: Transactions implementation adjustments
     new da88e1c  Merge pull request #36 from Havret/transactions_integration_tests
143b5b4 is described below

commit 143b5b4683cdaf36144544d76a8a32931e4a5829
Author: Havret <[hidden email]>
AuthorDate: Fri Nov 1 20:57:07 2019 +0100

    AMQNET-627: Transactions implementation adjustments
---
 apache-nms-amqp.sln.DotSettings                    |    1 +
 src/NMS.AMQP/Apache-NMS-AMQP.csproj                |   10 +-
 src/NMS.AMQP/Message/InboundMessageDispatch.cs     |    5 +
 src/NMS.AMQP/NmsLocalTransactionContext.cs         |    8 +-
 src/NMS.AMQP/NmsSession.cs                         |   20 +-
 src/NMS.AMQP/Provider/Amqp/AmqpConsumer.cs         |   42 +-
 .../Amqp/AmqpLinkExtensions.cs}                    |   26 +-
 src/NMS.AMQP/Provider/Amqp/AmqpProducer.cs         |    6 +-
 src/NMS.AMQP/Provider/Amqp/AmqpSendTask.cs         |   64 +
 src/NMS.AMQP/Provider/Amqp/AmqpSession.cs          |   21 +-
 .../Provider/Amqp/AmqpTransactionContext.cs        |   11 +-
 .../Provider/Amqp/AmqpTransactionCoordinator.cs    |   69 +-
 .../Integration/FailoverIntegrationTest.cs         |   10 +-
 .../Integration/ProducerIntegrationTest.cs         |   15 +-
 .../Integration/TransactionsIntegrationTest.cs     | 1473 ++++++++++++++++++++
 test/Apache-NMS-AMQP-Test/TestAmqp/TestAmqpPeer.cs |  172 ++-
 16 files changed, 1867 insertions(+), 86 deletions(-)

diff --git a/apache-nms-amqp.sln.DotSettings b/apache-nms-amqp.sln.DotSettings
index 8e25730..5c57e20 100644
--- a/apache-nms-amqp.sln.DotSettings
+++ b/apache-nms-amqp.sln.DotSettings
@@ -1,3 +1,4 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
  <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JMS/@EntryIndexedValue">JMS</s:String>
+ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TX/@EntryIndexedValue">TX</s:String></wpf:ResourceDictionary>
  <s:Boolean x:Key="/Default/UserDictionary/Words/=Amqp/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/src/NMS.AMQP/Apache-NMS-AMQP.csproj b/src/NMS.AMQP/Apache-NMS-AMQP.csproj
index dfb16a4..7e030c5 100644
--- a/src/NMS.AMQP/Apache-NMS-AMQP.csproj
+++ b/src/NMS.AMQP/Apache-NMS-AMQP.csproj
@@ -49,8 +49,8 @@ with the License.  You may obtain a copy of the License at
     </PropertyGroup>
 
     <ItemGroup>
-        <None Include="..\..\LICENSE.txt" Pack="true" PackagePath="LICENSE.txt"/>
-        <None Include="..\..\NOTICE.txt" Pack="true" PackagePath="NOTICE.txt"/>
+        <None Include="..\..\LICENSE.txt" Pack="true" PackagePath="LICENSE.txt" />
+        <None Include="..\..\NOTICE.txt" Pack="true" PackagePath="NOTICE.txt" />
     </ItemGroup>
 
     <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
@@ -74,8 +74,8 @@ with the License.  You may obtain a copy of the License at
 
     <ItemGroup>
         <!-- AMQPNetLite.Core is .NET Standard 1.3 package -->
-        <PackageReference Include="AMQPNetLite.Core" Version="2.2.0"/>
-        <PackageReference Include="Apache.NMS" Version="1.8.0"/>
-        <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0"/>
+        <PackageReference Include="AMQPNetLite.Core" Version="2.3.0" />
+        <PackageReference Include="Apache.NMS" Version="1.8.0" />
+        <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.9.0" />
     </ItemGroup>
 </Project>
diff --git a/src/NMS.AMQP/Message/InboundMessageDispatch.cs b/src/NMS.AMQP/Message/InboundMessageDispatch.cs
index e069a64..a611614 100644
--- a/src/NMS.AMQP/Message/InboundMessageDispatch.cs
+++ b/src/NMS.AMQP/Message/InboundMessageDispatch.cs
@@ -28,5 +28,10 @@ namespace Apache.NMS.AMQP.Message
 
         public int RedeliveryCount => Message?.Facade.RedeliveryCount ?? 0;
         public bool EnqueueFirst { get; set; }
+
+        public override string ToString()
+        {
+            return $"[{nameof(InboundMessageDispatch)}] MessageId: {Message.NMSMessageId}, {nameof(ConsumerId)}: {ConsumerId}";
+        }
     }
 }
\ No newline at end of file
diff --git a/src/NMS.AMQP/NmsLocalTransactionContext.cs b/src/NMS.AMQP/NmsLocalTransactionContext.cs
index 998c8de..76c2be4 100644
--- a/src/NMS.AMQP/NmsLocalTransactionContext.cs
+++ b/src/NMS.AMQP/NmsLocalTransactionContext.cs
@@ -157,7 +157,7 @@ namespace Apache.NMS.AMQP
 
             try
             {
-                await this.connection.Commit(this.transactionInfo, nextTx);
+                await this.connection.Commit(this.transactionInfo, nextTx).ConfigureAwait(false);
                 OnTransactionCommitted();
                 Reset();
                 this.transactionInfo = nextTx;
@@ -167,6 +167,10 @@ namespace Apache.NMS.AMQP
                 Tracer.Info($"Commit failed for transaction :{oldTransactionId}");
                 throw;
             }
+            catch (Exception e)
+            {
+                throw NMSExceptionSupport.Create(e);
+            }
             finally
             {
                 try
@@ -176,7 +180,7 @@ namespace Apache.NMS.AMQP
                     // one to recover our state.
                     if (nextTx.ProviderTxId == null)
                     {
-                        await Begin();
+                        await Begin().ConfigureAwait(false);
                     }
                 }
                 catch (Exception e)
diff --git a/src/NMS.AMQP/NmsSession.cs b/src/NMS.AMQP/NmsSession.cs
index 9904098..f8930f1 100644
--- a/src/NMS.AMQP/NmsSession.cs
+++ b/src/NMS.AMQP/NmsSession.cs
@@ -67,7 +67,25 @@ namespace Apache.NMS.AMQP
         internal async Task Begin()
         {
             await Connection.CreateResource(SessionInfo).ConfigureAwait(false);
-            await TransactionContext.Begin().ConfigureAwait(false);
+
+            try
+            {
+                // We always keep an open TX if transacted so start now.
+                await TransactionContext.Begin().ConfigureAwait(false);
+            }
+            catch (Exception)
+            {
+                // failed, close the AMQP session before we throw
+                try
+                {
+                    await Connection.DestroyResource(SessionInfo).ConfigureAwait(false);
+                }
+                catch (Exception)
+                {
+                    // Ignore, throw original error
+                }
+                throw;
+            }
         }
 
         public void Close()
diff --git a/src/NMS.AMQP/Provider/Amqp/AmqpConsumer.cs b/src/NMS.AMQP/Provider/Amqp/AmqpConsumer.cs
index b342750..685d9a3 100644
--- a/src/NMS.AMQP/Provider/Amqp/AmqpConsumer.cs
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpConsumer.cs
@@ -239,17 +239,7 @@ namespace Apache.NMS.AMQP.Provider.Amqp
                         envelope.IsDelivered = true;
                         break;
                     case AckType.ACCEPTED:
-                        AmqpTransactionContext transactionalState = session.TransactionContext;
-                        if (transactionalState != null)
-                        {
-                            receiverLink.Complete(message, transactionalState.GetTxnAcceptState());
-                            transactionalState.RegisterTxConsumer(this);
-                        }
-                        else
-                        {
-                            receiverLink.Accept(message);
-                        }
-                        RemoveMessage(envelope);
+                        HandleAccepted(envelope, message);
                         break;
                     case AckType.RELEASED:
                         receiverLink.Release(message);
@@ -260,8 +250,8 @@ namespace Apache.NMS.AMQP.Provider.Amqp
                         RemoveMessage(envelope);
                         break;
                     default:
-                        Tracer.Error($"Unsupported Ack Type for message: {envelope.Message}");
-                        throw new ArgumentException($"Unsupported Ack Type for message: {envelope.Message}");
+                        Tracer.ErrorFormat("Unsupported Ack Type for message: {0}", envelope);
+                        throw new ArgumentException($"Unsupported Ack Type for message: {envelope}");
                 }
             }
             else
@@ -270,6 +260,32 @@ namespace Apache.NMS.AMQP.Provider.Amqp
             }
         }
 
+        private void HandleAccepted(InboundMessageDispatch envelope, global::Amqp.Message message)
+        {
+            Tracer.DebugFormat("Accepted Ack of message: {0}", envelope);
+            
+            if (session.IsTransacted)
+            {
+                if (!session.IsTransactionFailed)
+                {
+                    var transactionalState = session.TransactionContext;
+                    receiverLink.Complete(message, transactionalState.GetTxnAcceptState());
+                    transactionalState.RegisterTxConsumer(this);
+                }
+                else
+                {
+                    Tracer.DebugFormat("Skipping ack of message {0} in failed transaction.", envelope);
+                }
+                
+            }
+            else
+            {
+                receiverLink.Accept(message);
+            }
+
+            RemoveMessage(envelope);
+        }
+
         private void AddMessage(InboundMessageDispatch envelope)
         {
             lock (syncRoot)
diff --git a/src/NMS.AMQP/Message/InboundMessageDispatch.cs b/src/NMS.AMQP/Provider/Amqp/AmqpLinkExtensions.cs
similarity index 59%
copy from src/NMS.AMQP/Message/InboundMessageDispatch.cs
copy to src/NMS.AMQP/Provider/Amqp/AmqpLinkExtensions.cs
index e069a64..6f850b1 100644
--- a/src/NMS.AMQP/Message/InboundMessageDispatch.cs
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpLinkExtensions.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -14,19 +14,23 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-using Apache.NMS.AMQP.Meta;
-using Apache.NMS.AMQP.Util;
 
-namespace Apache.NMS.AMQP.Message
+using System.Threading.Tasks;
+using Amqp;
+using Amqp.Framing;
+
+namespace Apache.NMS.AMQP.Provider.Amqp
 {
-    public class InboundMessageDispatch
+    internal static class AmqpLinkExtensions
     {
-        public Id ConsumerId { get; set; }
-        public ConsumerInfo ConsumerInfo { get; set; }
-        public NmsMessage Message { get; set; }
-        public bool IsDelivered { get; set; }
+        internal static bool IsDetaching(this Link link)
+        {
+            return link.LinkState >= LinkState.DetachPipe;
+        }
 
-        public int RedeliveryCount => Message?.Facade.RedeliveryCount ?? 0;
-        public bool EnqueueFirst { get; set; }
+        internal static Task<Outcome> SendAsync(this SenderLink link, global::Amqp.Message message, DeliveryState deliveryState, long timeoutMillis)
+        {
+            return new AmqpSendTask(link, message, deliveryState, timeoutMillis).Task;
+        }
     }
 }
\ No newline at end of file
diff --git a/src/NMS.AMQP/Provider/Amqp/AmqpProducer.cs b/src/NMS.AMQP/Provider/Amqp/AmqpProducer.cs
index d3982dd..3eb8e01 100644
--- a/src/NMS.AMQP/Provider/Amqp/AmqpProducer.cs
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpProducer.cs
@@ -130,7 +130,11 @@ namespace Apache.NMS.AMQP.Provider.Amqp
 
                 try
                 {
-                    
+                    // If the transaction has failed due to remote termination etc then we just indicate
+                    // the send has succeeded until the a new transaction is started.
+                    if (session.IsTransacted && session.IsTransactionFailed)
+                        return;
+
                     var transactionalState = session.TransactionContext?.GetTxnEnrolledState();
 
                     if (envelope.SendAsync)
diff --git a/src/NMS.AMQP/Provider/Amqp/AmqpSendTask.cs b/src/NMS.AMQP/Provider/Amqp/AmqpSendTask.cs
new file mode 100644
index 0000000..f090edd
--- /dev/null
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpSendTask.cs
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Amqp;
+using Amqp.Framing;
+using Apache.NMS.AMQP.Meta;
+using Apache.NMS.AMQP.Util;
+
+namespace Apache.NMS.AMQP.Provider.Amqp
+{
+    internal class AmqpSendTask : TaskCompletionSource<Outcome>
+    {
+        private readonly Timer timer;
+        
+        public AmqpSendTask(SenderLink link, global::Amqp.Message message, DeliveryState deliveryState, long timeoutMillis)
+        {
+            if (timeoutMillis != ConnectionInfo.INFINITE)
+            {
+                this.timer = new Timer(OnTimer, this, timeoutMillis, -1);
+            }
+            
+            try
+            {
+                link.Send(message, deliveryState, OnOutcome, this);
+            }
+            catch (Exception e)
+            {
+                this.timer?.Dispose();
+                this.SetException(ExceptionSupport.Wrap(e));
+            }
+        }
+        
+        private static void OnOutcome(ILink link, global::Amqp.Message message, Outcome outcome, object state)
+        {
+            var thisPtr = (AmqpSendTask) state;
+            thisPtr.timer?.Dispose();
+            thisPtr.TrySetResult(outcome);
+        }
+        
+        private static void OnTimer(object state)
+        {
+            var thisPtr = (AmqpSendTask) state;
+            thisPtr.timer.Dispose();
+            thisPtr.TrySetException(new TimeoutException());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/NMS.AMQP/Provider/Amqp/AmqpSession.cs b/src/NMS.AMQP/Provider/Amqp/AmqpSession.cs
index ee81348..2fb8ecb 100644
--- a/src/NMS.AMQP/Provider/Amqp/AmqpSession.cs
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpSession.cs
@@ -53,6 +53,10 @@ namespace Apache.NMS.AMQP.Provider.Amqp
         public IEnumerable<AmqpConsumer> Consumers => consumers.Values.ToArray();
         public Id SessionId => SessionInfo.Id;
 
+        internal bool IsTransacted => SessionInfo.IsTransacted;
+
+        internal bool IsTransactionFailed => TransactionContext?.IsTransactionFailed ?? false;
+
         public Task Start()
         {
             TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
@@ -82,7 +86,6 @@ namespace Apache.NMS.AMQP.Provider.Amqp
         public void Close()
         {
             TimeSpan timeout = TimeSpan.FromMilliseconds(SessionInfo.closeTimeout);
-            TransactionContext?.Close(timeout);
             UnderlyingSession.Close(timeout);
             Connection.RemoveSession(SessionInfo.Id);
         }
@@ -153,8 +156,8 @@ namespace Apache.NMS.AMQP.Provider.Amqp
         }
 
         /// <summary>
-        ///     Perform re-send of all delivered but not yet acknowledged messages for all consumers
-        ///     active in this Session.
+        /// Perform re-send of all delivered but not yet acknowledged messages for all consumers
+        /// active in this Session.
         /// </summary>
         public void Recover()
         {
@@ -173,8 +176,8 @@ namespace Apache.NMS.AMQP.Provider.Amqp
         /// Roll back the currently running Transaction
         /// </summary>
         /// <param name="transactionInfo">The TransactionInfo describing the transaction being rolled back.</param>
-        /// <param name="nextTransactionInfo">The JmsTransactionInfo describing the transaction that should be started immediately.</param>
-        /// <exception cref="IllegalStateException"></exception>
+        /// <param name="nextTransactionInfo">The TransactionInfo describing the transaction that should be started immediately.</param>
+        /// <exception cref="Exception">throws Exception if an error occurs while performing the operation.</exception>
         public Task Rollback(TransactionInfo transactionInfo, TransactionInfo nextTransactionInfo)
         {
             if (!SessionInfo.IsTransacted)
@@ -185,13 +188,19 @@ namespace Apache.NMS.AMQP.Provider.Amqp
             return TransactionContext.Rollback(transactionInfo, nextTransactionInfo);
         }
 
+        /// <summary>
+        /// Commit the currently running Transaction.
+        /// </summary>
+        /// <param name="transactionInfo">the TransactionInfo describing the transaction being committed.</param>
+        /// <param name="nextTransactionInfo">the TransactionInfo describing the transaction that should be started immediately.</param>
+        /// <exception cref="Exception">throws Exception if an error occurs while performing the operation.</exception>
         public Task Commit(TransactionInfo transactionInfo, TransactionInfo nextTransactionInfo)
         {
             if (!SessionInfo.IsTransacted)
             {
                 throw new IllegalStateException("Non-transacted Session cannot commit a TX.");
             }
-            
+
             return TransactionContext.Commit(transactionInfo, nextTransactionInfo);
         }
     }
diff --git a/src/NMS.AMQP/Provider/Amqp/AmqpTransactionContext.cs b/src/NMS.AMQP/Provider/Amqp/AmqpTransactionContext.cs
index 6934f5f..6753e4e 100644
--- a/src/NMS.AMQP/Provider/Amqp/AmqpTransactionContext.cs
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpTransactionContext.cs
@@ -40,6 +40,8 @@ namespace Apache.NMS.AMQP.Provider.Amqp
             this.session = session;
         }
 
+        public bool IsTransactionFailed => coordinator != null && coordinator.IsDetaching();
+        
         public TransactionalState GetTxnEnrolledState()
         {
             return this.cachedTransactedState;
@@ -63,8 +65,9 @@ namespace Apache.NMS.AMQP.Provider.Amqp
 
             Tracer.Debug($"TX Context{this} rolling back current TX[{this.current}]");
 
-            await this.coordinator.DischargeAsync(this.txnId, true).ConfigureAwait(false);
             this.current = null;
+            await this.coordinator.DischargeAsync(this.txnId, true).ConfigureAwait(false);
+            
 
             PostRollback();
 
@@ -97,8 +100,8 @@ namespace Apache.NMS.AMQP.Provider.Amqp
 
             Tracer.Debug($"TX Context{this} committing back current TX[{this.current}]");
 
-            await this.coordinator.DischargeAsync(this.txnId, false).ConfigureAwait(false);
             this.current = null;
+            await this.coordinator.DischargeAsync(this.txnId, false).ConfigureAwait(false);
 
             PostCommit();
 
@@ -115,12 +118,12 @@ namespace Apache.NMS.AMQP.Provider.Amqp
             if (this.current != null)
                 throw new NMSException("Begin called while a TX is still Active.");
 
-            if (this.coordinator == null || this.coordinator.IsClosed)
+            if (this.coordinator == null || this.coordinator.IsDetaching())
             {
                 this.coordinator = new AmqpTransactionCoordinator(this.session);
             }
 
-            this.txnId = await this.coordinator.DeclareAsync();
+            this.txnId = await this.coordinator.DeclareAsync().ConfigureAwait(false);
             this.current = transactionInfo.Id;
             transactionInfo.ProviderTxId = this.txnId;
             this.cachedTransactedState = new TransactionalState { TxnId = this.txnId };
diff --git a/src/NMS.AMQP/Provider/Amqp/AmqpTransactionCoordinator.cs b/src/NMS.AMQP/Provider/Amqp/AmqpTransactionCoordinator.cs
index a832343..d26e8fb 100644
--- a/src/NMS.AMQP/Provider/Amqp/AmqpTransactionCoordinator.cs
+++ b/src/NMS.AMQP/Provider/Amqp/AmqpTransactionCoordinator.cs
@@ -26,48 +26,67 @@ namespace Apache.NMS.AMQP.Provider.Amqp
 {
     public class AmqpTransactionCoordinator : SenderLink
     {
-        public AmqpTransactionCoordinator(AmqpSession session) : base(session.UnderlyingSession, GetName(), new Attach { Target = new Coordinator(), Source = new Source() }, null)
-        {
-        }
+        private static readonly global::Amqp.Message DeclareMessage = new global::Amqp.Message(new Declare());
 
-        private static string GetName() => "transaction-link-" + Guid.NewGuid().ToString("N").Substring(0, 5);
+        private readonly AmqpSession session;
 
-        public Task<byte[]> DeclareAsync()
+        public AmqpTransactionCoordinator(AmqpSession session) : base(session.UnderlyingSession, GetName(), new Attach
+        {
+            Target = new Coordinator
+            {
+                Capabilities = new[] { TxnCapabilities.LocalTransactions }
+            },
+            Source = new Source
+            {
+                Outcomes = new[] { SymbolUtil.ATTACH_OUTCOME_ACCEPTED, SymbolUtil.ATTACH_OUTCOME_REJECTED, SymbolUtil.ATTACH_OUTCOME_RELEASED, SymbolUtil.ATTACH_OUTCOME_MODIFIED },
+            },
+            SndSettleMode = SenderSettleMode.Unsettled,
+            RcvSettleMode = ReceiverSettleMode.First
+        }, null)
         {
-            var message = new global::Amqp.Message(new Declare());
-            var tcs = new TaskCompletionSource<byte[]>();
-            Send(message, null, OnDeclareOutcome, tcs);
-            return tcs.Task;
+            this.session = session;
         }
 
-        private static void OnDeclareOutcome(ILink link, global::Amqp.Message message, Outcome outcome, object state)
+        private static string GetName() => "transaction-link-" + Guid.NewGuid().ToString("N").Substring(0, 5);
+
+        public async Task<byte[]> DeclareAsync()
         {
-            var tcs = (TaskCompletionSource<byte[]>) state;
+            var outcome = await this.SendAsync(DeclareMessage, null, this.session.Connection.Info.requestTimeout).ConfigureAwait(false);
             if (outcome.Descriptor.Code == MessageSupport.DECLARED_INSTANCE.Descriptor.Code)
-                tcs.SetResult(((Declared) outcome).TxnId);
+            {
+                return ((Declared) outcome).TxnId;
+            }
             else if (outcome.Descriptor.Code == MessageSupport.REJECTED_INSTANCE.Descriptor.Code)
-                tcs.SetException(new AmqpException(((Rejected) outcome).Error));
+            {
+                var rejected = (Rejected) outcome;
+                var rejectedError = rejected.Error ?? new Error(ErrorCode.InternalError);
+                throw new AmqpException(rejectedError);
+            }
             else
-                tcs.SetCanceled();
+            {
+                throw new NMSException(outcome.ToString(), ErrorCode.InternalError);
+            }
         }
 
-        public Task DischargeAsync(byte[] txnId, bool fail)
+        public async Task DischargeAsync(byte[] txnId, bool fail)
         {
             var message = new global::Amqp.Message(new Discharge { TxnId = txnId, Fail = fail });
-            var tcs = new TaskCompletionSource<bool>();
-            Send(message, null, OnDischargeOutcome, tcs);
-            return tcs.Task;
-        }
+            var outcome = await this.SendAsync(message, null, this.session.Connection.Info.requestTimeout).ConfigureAwait(false);
 
-        private static void OnDischargeOutcome(ILink link, global::Amqp.Message message, Outcome outcome, object state)
-        {
-            var tcs = (TaskCompletionSource<bool>) state;
             if (outcome.Descriptor.Code == MessageSupport.ACCEPTED_INSTANCE.Descriptor.Code)
-                tcs.SetResult(true);
+            {
+                // accepted, do nothing
+            }
             else if (outcome.Descriptor.Code == MessageSupport.REJECTED_INSTANCE.Descriptor.Code)
-                tcs.SetException(new AmqpException(((Rejected) outcome).Error));
+            {
+                var rejected = (Rejected) outcome;
+                var rejectedError = rejected.Error ?? new Error(ErrorCode.TransactionRollback);
+                throw new TransactionRolledBackException(rejectedError.Condition, rejectedError.Description);
+            }
             else
-                tcs.SetCanceled();
+            {
+                throw new NMSException(outcome.ToString(), ErrorCode.InternalError);
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/test/Apache-NMS-AMQP-Test/Integration/FailoverIntegrationTest.cs b/test/Apache-NMS-AMQP-Test/Integration/FailoverIntegrationTest.cs
index a3b6306..ffe6a62 100644
--- a/test/Apache-NMS-AMQP-Test/Integration/FailoverIntegrationTest.cs
+++ b/test/Apache-NMS-AMQP-Test/Integration/FailoverIntegrationTest.cs
@@ -1038,11 +1038,13 @@ namespace NMS.AMQP.Test.Integration
                 ITopic topic = session.GetTopic(topicName);
 
                 // Expect a link to a topic node, which we will then refuse
-                testPeer.ExpectSenderAttach(targetMatcher: source =>
+                testPeer.ExpectSenderAttach(targetMatcher: x =>
                 {
-                    Assert.AreEqual(topicName, source.Address);
-                    Assert.IsFalse(source.Dynamic);
-                    Assert.AreEqual((uint) TerminusDurability.NONE, source.Durable);
+                    Target target = (Target) x;
+
+                    Assert.AreEqual(topicName, target.Address);
+                    Assert.IsFalse(target.Dynamic);
+                    Assert.AreEqual((uint) TerminusDurability.NONE, target.Durable);
                 }, sourceMatcher: Assert.NotNull, refuseLink: true);
 
                 //Expect the detach response to the test peer closing the producer link after refusal.
diff --git a/test/Apache-NMS-AMQP-Test/Integration/ProducerIntegrationTest.cs b/test/Apache-NMS-AMQP-Test/Integration/ProducerIntegrationTest.cs
index b2b2d7e..ac9f351 100644
--- a/test/Apache-NMS-AMQP-Test/Integration/ProducerIntegrationTest.cs
+++ b/test/Apache-NMS-AMQP-Test/Integration/ProducerIntegrationTest.cs
@@ -206,11 +206,16 @@ namespace NMS.AMQP.Test.Integration
                 testPeer.ExpectBegin();
 
                 string queueName = "myQueue";
-                Action<Target> targetMatcher = null;
-                if (anonymousProducer)
-                    targetMatcher = target => Assert.IsNull(target.Address);
-                else
-                    targetMatcher = target => Assert.AreEqual(queueName, target.Address);
+                Action<object> targetMatcher = t =>
+                {
+                    var target = t as Target;
+                    Assert.IsNotNull(target);
+                    if (anonymousProducer)
+                        Assert.IsNull(target.Address);
+                    else
+                        Assert.AreEqual(queueName, target.Address);
+                };
+                
 
                 testPeer.ExpectSenderAttach(targetMatcher: targetMatcher, sourceMatcher: Assert.NotNull, senderSettled: false);
 
diff --git a/test/Apache-NMS-AMQP-Test/Integration/TransactionsIntegrationTest.cs b/test/Apache-NMS-AMQP-Test/Integration/TransactionsIntegrationTest.cs
new file mode 100644
index 0000000..9242c58
--- /dev/null
+++ b/test/Apache-NMS-AMQP-Test/Integration/TransactionsIntegrationTest.cs
@@ -0,0 +1,1473 @@
+/*
+ * 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.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Transactions;
+using Amqp;
+using Amqp.Framing;
+using Amqp.Transactions;
+using Apache.NMS;
+using Apache.NMS.AMQP.Util;
+using NMS.AMQP.Test.TestAmqp;
+using NUnit.Framework;
+using IConnection = Apache.NMS.IConnection;
+using ISession = Apache.NMS.ISession;
+
+namespace NMS.AMQP.Test.Integration
+{
+    [TestFixture]
+    public class TransactionsIntegrationTest : IntegrationTestFixture
+    {
+        [Test, Timeout(20_000)]
+        public void TestTransactionRolledBackOnSessionClose()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+
+                // Closed session should roll-back the TX with a failed discharge
+                testPeer.ExpectDischarge(txnId, true);
+                testPeer.ExpectEnd();
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                session.Close();
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestTransactionCommitFailWithEmptyRejectedDisposition()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a Declared disposition state containing the txnId.
+
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId1);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a producer to use in provoking creation of the AMQP transaction
+                testPeer.ExpectSenderAttach();
+                IMessageProducer producer = session.CreateProducer(queue);
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+                Action<DeliveryState> stateMatcher = state =>
+                {
+                    Assert.IsInstanceOf<TransactionalState>(state);
+                    var transactionalState = (TransactionalState) state;
+                    CollectionAssert.AreEqual(txnId1, transactionalState.TxnId);
+                    Assert.IsNull(transactionalState.Outcome);
+                };
+                testPeer.ExpectTransfer(messageMatcher: Assert.NotNull, stateMatcher: stateMatcher, responseState: new TransactionalState
+                {
+                    Outcome = new Accepted(),
+                    TxnId = txnId1
+                }, responseSettled: true);
+
+                producer.Send(session.CreateMessage());
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with rejected and settled disposition to indicate the commit failed
+                testPeer.ExpectDischarge(txnId1, dischargeState: false, responseState: new Rejected());
+
+                // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId2 = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId2);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Commit(), "Commit operation should have failed.");
+
+                // session should roll back on close
+                testPeer.ExpectDischarge(txnId2, true);
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestProducedMessagesAfterCommitOfSentMessagesFails()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a Declared disposition state containing the txnId.
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId1);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a producer to use in provoking creation of the AMQP transaction
+                testPeer.ExpectSenderAttach();
+                IMessageProducer producer = session.CreateProducer(queue);
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+                Action<DeliveryState> stateMatcher = state =>
+                {
+                    Assert.IsInstanceOf<TransactionalState>(state);
+                    var transactionalState = (TransactionalState) state;
+                    CollectionAssert.AreEqual(txnId1, transactionalState.TxnId);
+                    Assert.IsNull(transactionalState.Outcome);
+                };
+                testPeer.ExpectTransfer(messageMatcher: Assert.NotNull, stateMatcher: stateMatcher, responseState: new TransactionalState
+                {
+                    Outcome = new Accepted(),
+                    TxnId = txnId1
+                }, responseSettled: true);
+
+                producer.Send(session.CreateMessage());
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with rejected and settled disposition to indicate the commit failed
+                testPeer.ExpectDischarge(txnId1, false, new Rejected() { Error = new Error(ErrorCode.InternalError) { Description = "Unknown error" } });
+
+                // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId2 = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId2);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Commit(), "Commit operation should have failed.");
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+                stateMatcher = state =>
+                {
+                    Assert.IsInstanceOf<TransactionalState>(state);
+                    var transactionalState = (TransactionalState) state;
+                    CollectionAssert.AreEqual(txnId2, transactionalState.TxnId);
+                    Assert.IsNull(transactionalState.Outcome);
+                };
+                testPeer.ExpectTransfer(messageMatcher: Assert.NotNull, stateMatcher: stateMatcher, responseState: new TransactionalState
+                {
+                    Outcome = new Accepted(),
+                    TxnId = txnId2
+                }, responseSettled: true);
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                producer.Send(session.CreateMessage());
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestProducedMessagesAfterRollbackSentMessagesFails()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a Declared disposition state containing the txnId.
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                byte[] txnId2 = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId1);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a producer to use in provoking creation of the AMQP transaction
+                testPeer.ExpectSenderAttach();
+                IMessageProducer producer = session.CreateProducer(queue);
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+
+                Action<DeliveryState> stateMatcher = state =>
+                {
+                    Assert.IsInstanceOf<TransactionalState>(state);
+                    var transactionalState = (TransactionalState) state;
+                    CollectionAssert.AreEqual(txnId1, transactionalState.TxnId);
+                    Assert.IsNull(transactionalState.Outcome);
+                };
+                testPeer.ExpectTransfer(messageMatcher: Assert.NotNull, stateMatcher: stateMatcher, responseState: new TransactionalState
+                {
+                    Outcome = new Accepted(),
+                    TxnId = txnId1
+                }, responseSettled: true);
+
+                producer.Send(session.CreateMessage());
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with rejected and settled disposition to indicate the rollback failed
+                testPeer.ExpectDischarge(txnId1, true, new Rejected() { Error = new Error(ErrorCode.InternalError) { Description = "Unknown error" } });
+
+                // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                testPeer.ExpectDeclare(txnId2);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Rollback(), "Rollback operation should have failed.");
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+                stateMatcher = state =>
+                {
+                    Assert.IsInstanceOf<TransactionalState>(state);
+                    var transactionalState = (TransactionalState) state;
+                    CollectionAssert.AreEqual(txnId2, transactionalState.TxnId);
+                    Assert.IsNull(transactionalState.Outcome);
+                };
+                testPeer.ExpectTransfer(messageMatcher: Assert.NotNull, stateMatcher: stateMatcher, responseState: new TransactionalState
+                {
+                    Outcome = new Accepted(),
+                    TxnId = txnId2
+                }, responseSettled: true);
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                producer.Send(session.CreateMessage());
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestCommitTransactedSessionWithConsumerReceivingAllMessages()
+        {
+            DoCommitTransactedSessionWithConsumerTestImpl(1, 1, false, false);
+        }
+
+        [Test, Timeout(20_000), Ignore("Until deferred close is implemented for AmqpConsumer")]
+        public void TestCommitTransactedSessionWithConsumerReceivingAllMessagesAndCloseBefore()
+        {
+            DoCommitTransactedSessionWithConsumerTestImpl(1, 1, true, true);
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestCommitTransactedSessionWithConsumerReceivingAllMessagesAndCloseAfter()
+        {
+            DoCommitTransactedSessionWithConsumerTestImpl(1, 1, true, false);
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestCommitTransactedSessionWithConsumerReceivingSomeMessages()
+        {
+            DoCommitTransactedSessionWithConsumerTestImpl(5, 2, false, false);
+        }
+
+        [Test, Timeout(20_000), Ignore("Until deferred close is implemented for AmqpConsumer")]
+        public void TestCommitTransactedSessionWithConsumerReceivingSomeMessagesAndClosesBefore()
+        {
+            DoCommitTransactedSessionWithConsumerTestImpl(5, 2, true, true);
+        }
+
+        [Test, Timeout(20_000), Ignore("Until deferred close is implemented for AmqpConsumer")]
+        public void TestCommitTransactedSessionWithConsumerReceivingSomeMessagesAndClosesAfter()
+        {
+            DoCommitTransactedSessionWithConsumerTestImpl(5, 2, true, false);
+        }
+
+        private void DoCommitTransactedSessionWithConsumerTestImpl(int transferCount, int consumeCount, bool closeConsumer, bool closeBeforeCommit)
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                testPeer.ExpectReceiverAttach();
+                testPeer.ExpectLinkFlowRespondWithTransfer(message: CreateMessageWithContent(), transferCount);
+
+                for (int i = 1; i <= consumeCount; i++)
+                {
+                    // Then expect a *settled* TransactionalState disposition for each message once received by the consumer
+                    testPeer.ExpectDisposition(settled: true, stateMatcher: state =>
+                    {
+                        Assert.IsInstanceOf<TransactionalState>(state);
+                        var transactionalState = (TransactionalState) state;
+                        Assert.AreEqual(txnId, transactionalState.TxnId);
+                        Assert.IsInstanceOf<Accepted>(transactionalState.Outcome);
+                    });
+                }
+
+                IMessageConsumer messageConsumer = session.CreateConsumer(queue);
+
+                for (int i = 1; i <= consumeCount; i++)
+                {
+                    IMessage receivedMessage = messageConsumer.Receive(TimeSpan.FromSeconds(3));
+                    Assert.NotNull(receivedMessage);
+                    Assert.IsInstanceOf<ITextMessage>(receivedMessage);
+                }
+
+                // Expect the consumer to close now
+                if (closeConsumer && closeBeforeCommit)
+                {
+                    // Expect the client to then drain off all credit from the link.
+                    testPeer.ExpectLinkFlow(drain: true, sendDrainFlowResponse: true);
+
+                    // Expect the messages that were not consumed to be released
+                    int unconsumed = transferCount - consumeCount;
+                    for (int i = 1; i <= unconsumed; i++)
+                    {
+                        testPeer.ExpectDispositionThatIsReleasedAndSettled();
+                    }
+
+                    // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                    // and reply with accepted and settled disposition to indicate the commit succeeded
+                    testPeer.ExpectDischarge(txnId, dischargeState: false);
+
+                    // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                    // reply with a declared disposition state containing the txnId.
+                    testPeer.ExpectDeclare(txnId);
+
+                    // Now the deferred close should be performed.
+                    testPeer.ExpectDetach(expectClosed: true, sendResponse: true, replyClosed: true);
+
+                    messageConsumer.Close();
+                }
+                else
+                {
+                    // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                    // and reply with accepted and settled disposition to indicate the commit succeeded
+                    testPeer.ExpectDischarge(txnId, dischargeState: false);
+
+                    // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                    // reply with a declared disposition state containing the txnId.
+                    testPeer.ExpectDeclare(txnId);
+                }
+
+                session.Commit();
+
+                if (closeConsumer && !closeBeforeCommit)
+                {
+                    testPeer.ExpectDetach(expectClosed: true, sendResponse: true, replyClosed: true);
+
+                    // Expect the messages that were not consumed to be released
+                    int unconsumed = transferCount - consumeCount;
+                    for (int i = 1; i <= unconsumed; i++)
+                    {
+                        testPeer.ExpectDispositionThatIsReleasedAndSettled();
+                    }
+
+                    messageConsumer.Close();
+                }
+
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+                testPeer.ExpectClose();
+
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestConsumerWithNoMessageCanCloseBeforeCommit()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                testPeer.ExpectReceiverAttach();
+                testPeer.ExpectLinkFlow();
+
+                // TODO: qpid-jms extend 2 additional flow links
+                // 1) Drain related with deferred consumer close, this feature is currently
+                //    not implemented.
+                // 2) Consumer pull - not implemented
+                // testPeer.ExpectLinkFlow(drain: true, sendDrainFlowResponse: true);
+                // testPeer.ExpectLinkFlow(drain: false, sendDrainFlowResponse: false);
+
+                testPeer.ExpectDetach(expectClosed: true, sendResponse: true, replyClosed: true);
+
+                IMessageConsumer messageConsumer = session.CreateConsumer(queue);
+                Assert.IsNull(messageConsumer.ReceiveNoWait());
+
+                messageConsumer.Close();
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with accepted and settled disposition to indicate the commit succeeded
+                testPeer.ExpectDischarge(txnId, dischargeState: false);
+
+                // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                testPeer.ExpectDeclare(txnId);
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                session.Commit();
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestConsumerWithNoMessageCanCloseBeforeRollback()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                testPeer.ExpectReceiverAttach();
+                testPeer.ExpectLinkFlow();
+
+                // TODO: qpid-jms extend 2 additional flow links
+                // 1) Drain related with deferred consumer close, this feature is currently
+                //    not implemented.
+                // 2) Consumer pull - not implemented
+                // testPeer.ExpectLinkFlow(drain: true, sendDrainFlowResponse: true);
+                // testPeer.ExpectLinkFlow(drain: false, sendDrainFlowResponse: false);
+
+                testPeer.ExpectDetach(expectClosed: true, sendResponse: true, replyClosed: true);
+
+                IMessageConsumer messageConsumer = session.CreateConsumer(queue);
+                Assert.IsNull(messageConsumer.ReceiveNoWait());
+
+                messageConsumer.Close();
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with accepted and settled disposition to indicate the commit succeeded
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                testPeer.ExpectDeclare(txnId);
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                session.Rollback();
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestProducedMessagesOnTransactedSessionCarryTxnId()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a producer to use in provoking creation of the AMQP transaction
+                testPeer.ExpectSenderAttach();
+
+                IMessageProducer producer = session.CreateProducer(queue);
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+                testPeer.ExpectTransfer(messageMatcher: Assert.NotNull,
+                    stateMatcher: state =>
+                    {
+                        Assert.IsInstanceOf<TransactionalState>(state);
+                        TransactionalState transactionalState = (TransactionalState) state;
+                        CollectionAssert.AreEqual(txnId, transactionalState.TxnId);
+                        Assert.IsNull(transactionalState.Outcome);
+                    },
+                    responseState: new TransactionalState() { TxnId = txnId, Outcome = new Accepted() },
+                    responseSettled: true);
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                producer.Send(session.CreateMessage());
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestProducedMessagesOnTransactedSessionCanBeReused()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a producer to use in provoking creation of the AMQP transaction
+                testPeer.ExpectSenderAttach();
+
+                IMessageProducer producer = session.CreateProducer(queue);
+
+                // Expect the message which was sent under the current transaction. Check it carries
+                // TransactionalState with the above txnId but has no outcome. Respond with a
+                // TransactionalState with Accepted outcome.
+                IMessage message = session.CreateMessage();
+                for (int i = 0; i < 3; i++)
+                {
+                    testPeer.ExpectTransfer(messageMatcher: Assert.NotNull,
+                        stateMatcher: state =>
+                        {
+                            Assert.IsInstanceOf<TransactionalState>(state);
+                            TransactionalState transactionalState = (TransactionalState) state;
+                            CollectionAssert.AreEqual(txnId, transactionalState.TxnId);
+                            Assert.IsNull(transactionalState.Outcome);
+                        },
+                        responseState: new TransactionalState() { TxnId = txnId, Outcome = new Accepted() },
+                        responseSettled: true);
+
+                    message.Properties.SetInt("sequence", i);
+
+                    producer.Send(message);
+                }
+
+                // Expect rollback on close without a commit call.
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+                testPeer.ExpectClose();
+
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestRollbackTransactedSessionWithConsumerReceivingAllMessages()
+        {
+            DoRollbackTransactedSessionWithConsumerTestImpl(1, 1, false);
+        }
+
+        [Test, Timeout(20_000), Ignore("Until deferred close is implemented for AmqpConsumer")]
+        public void TestRollbackTransactedSessionWithConsumerReceivingAllMessagesThenCloses()
+        {
+            DoRollbackTransactedSessionWithConsumerTestImpl(1, 1, true);
+        }
+
+        [Test, Timeout(20_000), Ignore("TODO: Fix")]
+        public void TestRollbackTransactedSessionWithConsumerReceivingSomeMessages()
+        {
+            DoRollbackTransactedSessionWithConsumerTestImpl(5, 2, false);
+        }
+
+        [Test, Timeout(20_000), Ignore("Until deferred close is implemented for AmqpConsumer")]
+        public void TestRollbackTransactedSessionWithConsumerReceivingSomeMessagesThenCloses()
+        {
+            DoRollbackTransactedSessionWithConsumerTestImpl(5, 2, true);
+        }
+
+        private void DoRollbackTransactedSessionWithConsumerTestImpl(int transferCount, int consumeCount, bool closeConsumer)
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                testPeer.ExpectReceiverAttach();
+                testPeer.ExpectLinkFlowRespondWithTransfer(message: CreateMessageWithContent(), transferCount);
+
+                for (int i = 1; i <= consumeCount; i++)
+                {
+                    // Then expect a *settled* TransactionalState disposition for each message once received by the consumer
+                    testPeer.ExpectDisposition(settled: true, stateMatcher: state =>
+                    {
+                        Assert.IsInstanceOf<TransactionalState>(state);
+                        var transactionalState = (TransactionalState) state;
+                        Assert.AreEqual(txnId, transactionalState.TxnId);
+                        Assert.IsInstanceOf<Accepted>(transactionalState.Outcome);
+                    });
+                }
+
+                IMessageConsumer messageConsumer = session.CreateConsumer(queue);
+
+                for (int i = 1; i <= consumeCount; i++)
+                {
+                    IMessage receivedMessage = messageConsumer.Receive(TimeSpan.FromSeconds(3));
+                    Assert.IsNotNull(receivedMessage);
+                    Assert.IsInstanceOf<ITextMessage>(receivedMessage);
+                }
+
+                // Expect the consumer to be 'stopped' prior to rollback by issuing a 'drain'
+                testPeer.ExpectLinkFlow(drain: true, sendDrainFlowResponse: true, creditMatcher: c => Assert.AreEqual(0, c));
+
+                if (closeConsumer)
+                {
+                    // Expect the messages that were not consumed to be released
+                    int unconsumed = transferCount - consumeCount;
+                    for (int i = 1; i <= unconsumed; i++)
+                    {
+                        testPeer.ExpectDispositionThatIsReleasedAndSettled();
+                    }
+
+                    // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                    // and reply with accepted and settled disposition to indicate the commit succeeded
+                    testPeer.ExpectDischarge(txnId, dischargeState: false);
+
+                    // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                    // reply with a declared disposition state containing the txnId.
+                    testPeer.ExpectDeclare(txnId);
+
+                    // Now the deferred close should be performed.
+                    testPeer.ExpectDetach(expectClosed: true, sendResponse: true, replyClosed: true);
+
+                    messageConsumer.Close();
+                }
+                else
+                {
+                    // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                    // and reply with accepted and settled disposition to indicate the rollback succeeded
+                    testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                    // Then expect an unsettled 'declare' transfer to the txn coordinator, and
+                    // reply with a declared disposition state containing the txnId.
+                    testPeer.ExpectDeclare(txnId);
+
+                    // Expect the messages that were not consumed to be released
+                    int unconsumed = transferCount - consumeCount;
+                    for (int i = 1; i <= unconsumed; i++)
+                    {
+                        testPeer.ExpectDispositionThatIsReleasedAndSettled();
+                    }
+
+                    // Expect the consumer to be 'started' again as rollback completes
+                    testPeer.ExpectLinkFlow(drain: false, sendDrainFlowResponse: false, creditMatcher: c => Assert.Greater(c, 0));
+                }
+
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+                session.Rollback();
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        // TODO:
+        // TestRollbackTransactedSessionWithPrefetchFullBeforeStoppingConsumer
+        // TestRollbackTransactedSessionWithPrefetchFullyUtilisedByDrainWhenStoppingConsumer
+
+        [Test, Timeout(20_000)]
+        public void TestDefaultOutcomeIsModifiedForConsumerSourceOnTransactedSession()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                string queueName = "myQueue";
+                IQueue queue = session.GetQueue(queueName);
+
+                testPeer.ExpectReceiverAttach(linkNameMatcher: Assert.IsNotNull, targetMatcher: Assert.IsNotNull, sourceMatcher: source =>
+                {
+                    Assert.AreEqual(queueName, source.Address);
+                    Assert.IsFalse(source.Dynamic);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_ACCEPTED);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_REJECTED);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_RELEASED);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_MODIFIED);
+
+                    Assert.IsInstanceOf<Modified>(source.DefaultOutcome);
+                    Modified modified = (Modified) source.DefaultOutcome;
+                    Assert.IsTrue(modified.DeliveryFailed);
+                    Assert.IsFalse(modified.UndeliverableHere);
+                });
+
+                testPeer.ExpectLinkFlow();
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+                session.CreateConsumer(queue);
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestCoordinatorLinkSupportedOutcomes()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach(sourceMatcher: s =>
+                {
+                    Source source = (Source) s;
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_ACCEPTED);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_REJECTED);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_RELEASED);
+                    CollectionAssert.Contains(source.Outcomes, SymbolUtil.ATTACH_OUTCOME_MODIFIED);
+                });
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId);
+
+                connection.CreateSession(AcknowledgementMode.Transactional);
+
+                //Expect rollback on close
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestRollbackErrorCoordinatorClosedOnCommit()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                byte[] txnId2 = { 1, 2, 3, 4 };
+
+                testPeer.ExpectDeclare(txnId1);
+                testPeer.RemotelyCloseLastCoordinatorLinkOnDischarge(txnId: txnId1, dischargeState: false, nextTxnId: txnId2);
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclare(txnId2);
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Commit(), "Transaction should have rolled back");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestRollbackErrorWhenCoordinatorRemotelyClosed()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+                testPeer.RemotelyCloseLastCoordinatorLink();
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                testPeer.WaitForAllMatchersToComplete(2000);
+
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclare(txnId);
+
+                testPeer.ExpectDischarge(txnId, true);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Commit(), "Transaction should have rolled back");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestNMSErrorCoordinatorClosedOnRollback()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                byte[] txnId2 = { 1, 2, 3, 4 };
+
+                testPeer.ExpectDeclare(txnId1);
+                testPeer.RemotelyCloseLastCoordinatorLinkOnDischarge(txnId: txnId1, dischargeState: true, nextTxnId: txnId2);
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclare(txnId2);
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                Assert.Catch<NMSException>(() => session.Rollback(), "Rollback should have thrown a NMSException");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestNMSExceptionOnRollbackWhenCoordinatorRemotelyClosed()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+                testPeer.RemotelyCloseLastCoordinatorLink();
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                testPeer.WaitForAllMatchersToComplete(2000);
+
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclare(txnId);
+
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                Assert.Catch<NMSException>(() => session.Rollback(), "Rollback should have thrown a NMSException");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestSendAfterCoordinatorLinkClosedDuringTX()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a Declared disposition state containing the txnId.
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a producer to use in provoking creation of the AMQP transaction
+                testPeer.ExpectSenderAttach();
+
+                // Close the link, the messages should now just get dropped on the floor.
+                testPeer.RemotelyCloseLastCoordinatorLink();
+
+                IMessageProducer producer = session.CreateProducer(queue);
+
+                testPeer.WaitForAllMatchersToComplete(2000);
+
+                producer.Send(session.CreateMessage());
+
+                // Expect that a new link will be created in order to start the next TX.
+                txnId = new byte[] { 1, 2, 3, 4 };
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclare(txnId);
+
+                // Expect that the session TX will rollback on close.
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Commit(), "Commit operation should have failed.");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestReceiveAfterCoordinatorLinkClosedDuringTX()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a Declared disposition state containing the txnId.
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Create a consumer and send it an initial message for receive to process.
+                testPeer.ExpectReceiverAttach();
+                testPeer.ExpectLinkFlowRespondWithTransfer(message: CreateMessageWithNullContent());
+
+                // Close the link, the messages should now just get dropped on the floor.
+                testPeer.RemotelyCloseLastCoordinatorLink();
+
+                IMessageConsumer consumer = session.CreateConsumer(queue);
+
+                testPeer.WaitForAllMatchersToComplete(2000);
+
+                // receiving the message would normally ack it, since the TX is failed this
+                // should not result in a disposition going out.
+                IMessage received = consumer.Receive();
+                Assert.IsNotNull(received);
+
+                // Expect that a new link will be created in order to start the next TX.
+                txnId = new byte[] { 1, 2, 3, 4 };
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclare(txnId);
+
+                // Expect that the session TX will rollback on close.
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                Assert.Catch<TransactionRolledBackException>(() => session.Commit(), "Commit operation should have failed.");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestSessionCreateFailsOnDeclareTimeout()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer, "nms.requestTimeout=500");
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+                testPeer.ExpectDeclareButDoNotRespond();
+
+                // Expect the AMQP session to be closed due to the NMS session creation failure.
+                testPeer.ExpectEnd();
+
+                // TODO: Replace NMSException with sth more specific, in qpid-jms it is JmsOperationTimedOutException
+                Assert.Catch<NMSException>(() => connection.CreateSession(AcknowledgementMode.Transactional), "Should have timed out waiting for declare.");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestSessionCreateFailsOnDeclareRejection()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a Rejected disposition state to indicate failure.
+                testPeer.ExpectDeclareAndReject();
+
+                // Expect the AMQP session to be closed due to the NMS session creation failure.
+                testPeer.ExpectEnd();
+
+                Assert.Catch<NMSException>(() => connection.CreateSession(AcknowledgementMode.Transactional));
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestSessionCreateFailsOnCoordinatorLinkRefusal()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+
+                // Expect coordinator link, refuse it, expect detach reply
+                string errorMessage = "CoordinatorLinkRefusal-breadcrumb";
+                testPeer.ExpectCoordinatorAttach(refuseLink: true, error: new Error(ErrorCode.NotImplemented) { Description = errorMessage });
+                testPeer.ExpectDetach(expectClosed: true, sendResponse: false, replyClosed: false);
+
+                // Expect the AMQP session to be closed due to the NMS session creation failure.
+                testPeer.ExpectEnd();
+
+                NMSException exception = Assert.Catch<NMSException>(() => connection.CreateSession(AcknowledgementMode.Transactional));
+                Assert.IsTrue(exception.Message.Contains(errorMessage), "Expected exception message to contain breadcrumb");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestTransactionRolledBackOnSessionCloseTimesOut()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer, "nms.requestTimeout=500");
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+
+                // Closed session should roll-back the TX with a failed discharge
+                testPeer.ExpectDischargeButDoNotRespond(txnId, dischargeState: true);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                // TODO: Replace NMSException with sth more specific, in qpid-jms it is JmsOperationTimedOutException
+                Assert.Catch<NMSException>(() => session.Close(), "Should have timed out waiting for discharge.");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestTransactionRolledBackTimesOut()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer, "nms.requestTimeout=500");
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                byte[] txnId2 = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId1);
+
+                // Expect discharge but don't respond so that the request timeout kicks in and fails
+                // the discharge.  The pipelined declare should arrive as well and be discharged as the
+                // client attempts to recover to a known good state.
+                testPeer.ExpectDischargeButDoNotRespond(txnId1, dischargeState: true);
+
+                // Session should throw from the rollback and then try and recover.
+                testPeer.ExpectDeclare(txnId2);
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                // TODO: Replace NMSException with sth more specific, in qpid-jms it is JmsOperationTimedOutException
+                Assert.Catch<NMSException>(() => session.Rollback(), "Should have timed out waiting for discharge.");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestTransactionCommitTimesOut()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer, "nms.requestTimeout=500");
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                byte[] txnId2 = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId1);
+
+                // Expect discharge but don't respond so that the request timeout kicks in and fails
+                // the discharge.  The pipelined declare should arrive as well and be discharged as the
+                // client attempts to recover to a known good state.
+                testPeer.ExpectDischargeButDoNotRespond(txnId1, dischargeState: false);
+                testPeer.ExpectDeclare(txnId2);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                // TODO: Replace NMSException with sth more specific, in qpid-jms it is JmsOperationTimedOutException
+                Assert.Catch<NMSException>(() => session.Commit(), "Should have timed out waiting for discharge.");
+
+                // Session rolls back on close
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000), Ignore("TODO: Fix")]
+        public void TestTransactionCommitTimesOutAndNoNextBeginTimesOut()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer, "nms.requestTimeout=500");
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                byte[] txnId1 = { 5, 6, 7, 8 };
+                byte[] txnId2 = { 1, 2, 3, 4 };
+                testPeer.ExpectDeclare(txnId1);
+
+                // Expect discharge and don't respond so that the request timeout kicks in
+                // Expect pipelined declare and don't response so that the request timeout kicks in.
+                // The commit operation should throw a timed out exception at that point.
+                testPeer.ExpectDischargeButDoNotRespond(txnId1, dischargeState: false);
+                testPeer.ExpectDeclareButDoNotRespond();
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+
+                // After the pipelined operations both time out, the session should attempt to
+                // recover by creating a new TX, then on close the session should roll it back
+                testPeer.ExpectDeclare(txnId2);
+                testPeer.ExpectDischarge(txnId2, dischargeState: true);
+
+                // TODO: Replace NMSException with sth more specific, in qpid-jms it is JmsOperationTimedOutException
+                Assert.Catch<NMSException>(() => session.Commit(), "Should have timed out waiting for discharge.");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000), Ignore("TODO: Fix")]
+        public void TestRollbackWithNoResponseForSuspendConsumer()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                testPeer.ExpectReceiverAttach();
+                testPeer.ExpectLinkFlowRespondWithTransfer(message: CreateMessageWithContent(), count: 1);
+
+                // Then expect a *settled* TransactionalState disposition for the message once received by the consumer
+                testPeer.ExpectDisposition(settled: true, state =>
+                {
+                    var transactionalState = (TransactionalState) state;
+                    CollectionAssert.AreEqual(txnId, transactionalState.TxnId);
+                    Assert.IsInstanceOf<Accepted>(transactionalState.Outcome);
+                });
+
+                // Read one so we try to suspend on rollback
+                IMessageConsumer messageConsumer = session.CreateConsumer(queue);
+                IMessage receivedMessage = messageConsumer.Receive(TimeSpan.FromSeconds(3));
+
+                Assert.NotNull(receivedMessage);
+                Assert.IsInstanceOf<ITextMessage>(receivedMessage);
+
+                // Expect the consumer to be 'stopped' prior to rollback by issuing a 'drain'
+                testPeer.ExpectLinkFlow(drain: true, sendDrainFlowResponse: false);
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with accepted and settled disposition to indicate the rollback succeeded
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                testPeer.ExpectDeclare(txnId);
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+
+                Assert.Catch<NMSException>(() => session.Rollback(), "Should throw a timed out exception");
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(1000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestConsumerMessageOrderOnTransactedSession()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                int messageCount = 10;
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = { 5, 6, 7, 8 };
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Expect the browser enumeration to create a underlying consumer
+                testPeer.ExpectReceiverAttach();
+
+                // Expect initial credit to be sent, respond with some messages that are tagged with
+                // a sequence number we can use to determine if order is maintained.
+                testPeer.ExpectLinkFlowRespondWithTransfer(CreateMessageWithNullContent(), count: messageCount, addMessageNumberProperty: true);
+
+                for (int i = 1; i <= messageCount; i++)
+                {
+                    // Then expect an *settled* TransactionalState disposition for each message once received by the consumer
+                    testPeer.ExpectSettledTransactionalDisposition(txnId);
+                }
+
+                IMessageConsumer consumer = session.CreateConsumer(queue);
+                for (int i = 0; i < messageCount; i++)
+                {
+                    IMessage message = consumer.Receive(TimeSpan.FromMilliseconds(500));
+                    Assert.IsNotNull(message);
+                    Assert.AreEqual(i, message.Properties.GetInt(TestAmqpPeer.MESSAGE_NUMBER));
+                }
+
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with accepted and settled disposition to indicate the rollback succeeded
+                testPeer.ExpectDischarge(txnId, true);
+                testPeer.ExpectEnd();
+
+                session.Close();
+
+                testPeer.ExpectClose();
+                connection.Close();
+
+                testPeer.WaitForAllMatchersToComplete(3000);
+            }
+        }
+
+        [Test, Timeout(20_000)]
+        public void TestConsumeManyWithSingleTXPerMessage()
+        {
+            using (TestAmqpPeer testPeer = new TestAmqpPeer())
+            {
+                IConnection connection = EstablishConnection(testPeer);
+                connection.Start();
+
+                int messageCount = 10;
+
+                testPeer.ExpectBegin();
+                testPeer.ExpectCoordinatorAttach();
+
+                var txnIdQueue = new Queue<byte[]>(3);
+                txnIdQueue.Enqueue(new byte[] { 1, 2, 3, 4 });
+                txnIdQueue.Enqueue(new byte[] { 2, 4, 6, 8 });
+                txnIdQueue.Enqueue(new byte[] { 5, 4, 3, 2 });
+
+                // First expect an unsettled 'declare' transfer to the txn coordinator, and
+                // reply with a declared disposition state containing the txnId.
+                byte[] txnId = txnIdQueue.Dequeue();
+                txnIdQueue.Enqueue(txnId);
+                testPeer.ExpectDeclare(txnId);
+
+                ISession session = connection.CreateSession(AcknowledgementMode.Transactional);
+                IQueue queue = session.GetQueue("myQueue");
+
+                // Expect the browser enumeration to create a underlying consumer
+                testPeer.ExpectReceiverAttach();
+
+                // Expect initial credit to be sent, respond with some messages that are tagged with
+                // a sequence number we can use to determine if order is maintained.
+                testPeer.ExpectLinkFlowRespondWithTransfer(message: CreateMessageWithNullContent(), count: messageCount, addMessageNumberProperty: true);
+
+                IMessageConsumer consumer = session.CreateConsumer(queue);
+
+                for (int i = 0; i < messageCount; i++)
+                {
+                    // Then expect an *settled* TransactionalState disposition for each message once received by the consumer
+                    testPeer.ExpectSettledTransactionalDisposition(txnId);
+
+                    IMessage message = consumer.Receive(TimeSpan.FromMilliseconds(500));
+                    Assert.NotNull(message);
+                    Assert.AreEqual(i, message.Properties.GetInt(TestAmqpPeer.MESSAGE_NUMBER));
+
+                    // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                    // and reply with accepted and settled disposition to indicate the commit succeeded
+                    testPeer.ExpectDischarge(txnId, dischargeState: false);
+
+                    // Expect the next transaction to start.
+                    txnId = txnIdQueue.Dequeue();
+                    txnIdQueue.Enqueue(txnId);
+                    testPeer.ExpectDeclare(txnId);
+                    
+                    session.Commit();
+                }
+                
+                // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+                // and reply with accepted and settled disposition to indicate the rollback succeeded
+                testPeer.ExpectDischarge(txnId, dischargeState: true);
+                testPeer.ExpectEnd();
+
+                session.Close();
+                
+                testPeer.ExpectClose();
+                connection.Close();
+                
+                testPeer.WaitForAllMatchersToComplete(3000);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/Apache-NMS-AMQP-Test/TestAmqp/TestAmqpPeer.cs b/test/Apache-NMS-AMQP-Test/TestAmqp/TestAmqpPeer.cs
index 71f7f02..6c2f147 100644
--- a/test/Apache-NMS-AMQP-Test/TestAmqp/TestAmqpPeer.cs
+++ b/test/Apache-NMS-AMQP-Test/TestAmqp/TestAmqpPeer.cs
@@ -26,6 +26,7 @@ using System.Threading;
 using Amqp;
 using Amqp.Framing;
 using Amqp.Sasl;
+using Amqp.Transactions;
 using Amqp.Types;
 using Apache.NMS.AMQP.Util;
 using NLog;
@@ -55,6 +56,7 @@ namespace NMS.AMQP.Test.TestAmqp
 
         private ushort lastInitiatedChannel = 0;
         private uint lastInitiatedLinkHandle;
+        private uint lastInitiatedCoordinatorLinkHandle = 0;
 
         private readonly LinkedList<IMatcher> matchers = new LinkedList<IMatcher>();
         private readonly object matchersLock = new object();
@@ -273,7 +275,7 @@ namespace NMS.AMQP.Test.TestAmqp
             );
         }
 
-        public void ExpectLinkFlowRespondWithTransfer(Amqp.Message message, int count = 1)
+        public void ExpectLinkFlowRespondWithTransfer(Amqp.Message message, int count = 1, bool addMessageNumberProperty = false)
         {
             Action<uint> creditMatcher = credit => Assert.Greater(credit, 0);
 
@@ -283,7 +285,7 @@ namespace NMS.AMQP.Test.TestAmqp
                 drain: false,
                 sendDrainFlowResponse: false,
                 sendSettled: false,
-                addMessageNumberProperty: false,
+                addMessageNumberProperty: addMessageNumberProperty,
                 creditMatcher: creditMatcher,
                 nextIncomingId: 1
             );
@@ -306,7 +308,7 @@ namespace NMS.AMQP.Test.TestAmqp
 
             FrameMatcher<Flow> flowMatcher = new FrameMatcher<Flow>()
                 .WithAssertion(flow => Assert.AreEqual(drain, flow.Drain))
-                .WithAssertion(flow => creditMatcher(flow.LinkCredit));
+                .WithAssertion(flow => creditMatcher?.Invoke(flow.LinkCredit));
 
             if (nextIncomingId != null)
             {
@@ -419,19 +421,20 @@ namespace NMS.AMQP.Test.TestAmqp
             ExpectSenderAttach(sourceMatcher: Assert.NotNull, targetMatcher: Assert.NotNull);
         }
 
-        public void ExpectSenderAttach(Action<Source> sourceMatcher,
-            Action<Target> targetMatcher,
+        public void ExpectSenderAttach(Action<object> sourceMatcher,
+            Action<object> targetMatcher,
             bool refuseLink = false,
             uint creditAmount = 100,
-            bool senderSettled = false)
+            bool senderSettled = false,
+            Error error = null)
         {
             var attachMatcher = new FrameMatcher<Attach>()
                 .WithAssertion(attach => Assert.IsNotNull(attach.LinkName))
                 .WithAssertion(attach => Assert.AreEqual(attach.Role, Role.SENDER))
                 .WithAssertion(attach => Assert.AreEqual(senderSettled ? SenderSettleMode.Settled : SenderSettleMode.Unsettled, attach.SndSettleMode))
                 .WithAssertion(attach => Assert.AreEqual(attach.RcvSettleMode, ReceiverSettleMode.First))
-                .WithAssertion(attach => sourceMatcher(attach.Source as Source))
-                .WithAssertion(attach => targetMatcher(attach.Target as Target))
+                .WithAssertion(attach => sourceMatcher(attach.Source))
+                .WithAssertion(attach => targetMatcher(attach.Target))
                 .WithOnComplete(context =>
                 {
                     var attach = new Attach()
@@ -452,11 +455,21 @@ namespace NMS.AMQP.Test.TestAmqp
 
                     lastInitiatedLinkHandle = context.Command.Handle;
 
+                    if (context.Command.Target is Coordinator)
+                    {
+                        lastInitiatedCoordinatorLinkHandle = context.Command.Handle;
+                    }
+
                     context.SendCommand(attach);
 
                     if (refuseLink)
                     {
                         var detach = new Detach { Closed = true, Handle = context.Command.Handle };
+                        if (error != null)
+                        {
+                            detach.Error = error;
+                        }
+
                         context.SendCommand(detach);
                     }
                     else
@@ -684,6 +697,19 @@ namespace NMS.AMQP.Test.TestAmqp
             AddMatcher(dispositionMatcher);
         }
 
+        public void ExpectSettledTransactionalDisposition(byte[] txnId)
+        {
+            void StateMatcher(DeliveryState state)
+            {
+                Assert.IsInstanceOf<TransactionalState>(state);
+                var transactionalState = (TransactionalState) state;
+                Assert.IsInstanceOf<Accepted>(transactionalState.Outcome);
+                CollectionAssert.AreEqual(txnId, transactionalState.TxnId);
+            }
+
+            ExpectDisposition(settled: true, StateMatcher);
+        }
+
         public void ExpectTransferButDoNotRespond(Action<Amqp.Message> messageMatcher)
         {
             ExpectTransfer(messageMatcher: messageMatcher,
@@ -705,6 +731,17 @@ namespace NMS.AMQP.Test.TestAmqp
             );
         }
 
+        public void ExpectTransfer(Action<Amqp.Message> messageMatcher, Action<DeliveryState> stateMatcher, DeliveryState responseState, bool responseSettled)
+        {
+            ExpectTransfer(messageMatcher: messageMatcher,
+                stateMatcher: stateMatcher,
+                settled: false,
+                sendResponseDisposition: true,
+                responseState: responseState,
+                responseSettled: responseSettled
+            );
+        }
+
         public void ExpectTransfer(Action<Amqp.Message> messageMatcher,
             Action<DeliveryState> stateMatcher,
             bool settled,
@@ -862,7 +899,124 @@ namespace NMS.AMQP.Test.TestAmqp
                 responseSourceOverride: responseSourceOverride,
                 desiredCapabilitiesMatcher: linkDesiredCapabilitiesMatcher);
         }
-        
+
+        public void ExpectCoordinatorAttach(Action<object> sourceMatcher = null, bool refuseLink = false, Error error = null)
+        {
+            Action<object> coordinatorMatcher = Assert.IsInstanceOf<Coordinator>;
+            sourceMatcher = sourceMatcher ?? Assert.IsNotNull;
+
+            ExpectSenderAttach(sourceMatcher: sourceMatcher, targetMatcher: coordinatorMatcher, refuseLink: refuseLink, error: error);
+        }
+
+        public void ExpectDeclare(byte[] txnId)
+        {
+            ExpectTransfer(messageMatcher: DeclareMatcher, stateMatcher: Assert.IsNull, settled: false, sendResponseDisposition: true, responseState: new Declared() { TxnId = txnId },
+                responseSettled: true);
+        }
+
+        public void ExpectDeclareButDoNotRespond()
+        {
+            ExpectTransfer(messageMatcher: DeclareMatcher, stateMatcher: Assert.IsNull, settled: false, sendResponseDisposition: false, responseState: null, responseSettled: false);
+        }
+
+
+        public void ExpectDeclareAndReject()
+        {
+            ExpectTransfer(messageMatcher: DeclareMatcher, stateMatcher: Assert.IsNull, responseState: new Rejected(), responseSettled: true);
+        }
+
+        void DeclareMatcher(Amqp.Message message)
+        {
+            Assert.IsNotNull(message);
+            Assert.IsInstanceOf<AmqpValue>(message.BodySection);
+            Assert.IsInstanceOf<Declare>(((AmqpValue) message.BodySection).Value);
+        }
+
+        public void ExpectDischarge(byte[] txnId, bool dischargeState)
+        {
+            ExpectDischarge(txnId: txnId, dischargeState: dischargeState, new Accepted());
+        }
+
+        public void ExpectDischargeButDoNotRespond(byte[] txnId, bool dischargeState)
+        {
+            ExpectTransfer(messageMatcher: m => DischargeMatcher(m, txnId, dischargeState),
+                stateMatcher: Assert.IsNull,
+                settled: false,
+                sendResponseDisposition: false,
+                responseState: null,
+                responseSettled: true);
+        }
+
+        public void ExpectDischarge(byte[] txnId, bool dischargeState, DeliveryState responseState)
+        {
+            // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+            // and reply with given response and settled disposition to indicate the outcome.
+            ExpectTransfer(messageMatcher: m => DischargeMatcher(m, txnId, dischargeState),
+                stateMatcher: Assert.IsNull,
+                settled: false,
+                sendResponseDisposition: true,
+                responseState: responseState,
+                responseSettled: true);
+        }
+
+        private void DischargeMatcher(Amqp.Message message, byte[] txnId, bool dischargeState)
+        {
+            Assert.IsNotNull(message);
+            var bodySection = message.BodySection as AmqpValue;
+            Assert.IsNotNull(bodySection);
+            var discharge = bodySection.Value as Discharge;
+            Assert.AreEqual(dischargeState, discharge.Fail);
+            CollectionAssert.AreEqual(txnId, discharge.TxnId);
+        }
+
+        public void RemotelyCloseLastCoordinatorLinkOnDischarge(byte[] txnId, bool dischargeState, byte[] nextTxnId)
+        {
+            // Expect an unsettled 'discharge' transfer to the txn coordinator containing the txnId,
+            // and reply with given response and settled disposition to indicate the outcome.
+            void DischargeMatcher(Amqp.Message message)
+            {
+                Assert.IsNotNull(message);
+                var bodySection = message.BodySection as AmqpValue;
+                Assert.IsNotNull(bodySection);
+                var discharge = bodySection.Value as Discharge;
+                Assert.AreEqual(dischargeState, discharge.Fail);
+                CollectionAssert.AreEqual(txnId, discharge.TxnId);
+            }
+
+            ExpectTransfer(messageMatcher: DischargeMatcher, stateMatcher: Assert.IsNull, settled: false, sendResponseDisposition: false, responseState: null, responseSettled: false);
+
+            RemotelyCloseLastCoordinatorLink(expectDetachResponse: true, closed: true, error: new Error(ErrorCode.TransactionRollback) { Description = "Discharge of TX failed." });
+        }
+
+        public void RemotelyCloseLastCoordinatorLink()
+        {
+            RemotelyCloseLastCoordinatorLink(expectDetachResponse: true, closed: true, error: new Error(ErrorCode.TransactionRollback) { Description = "Discharge of TX failed." });
+        }
+
+        private void RemotelyCloseLastCoordinatorLink(bool expectDetachResponse, bool closed, Error error)
+        {
+            lock (matchersLock)
+            {
+                var matcher = GetLastMatcher();
+                matcher.WithOnComplete(context =>
+                {
+                    var detach = new Detach { Closed = true, Handle = lastInitiatedCoordinatorLinkHandle };
+                    if (error != null)
+                    {
+                        detach.Error = error;
+                    }
+
+                    context.SendCommand(detach);
+                });
+
+                if (expectDetachResponse)
+                {
+                    var detachMatcher = new FrameMatcher<Detach>().WithAssertion(detach => Assert.AreEqual(closed, detach.Closed));
+                    AddMatcher(detachMatcher);
+                }
+            }
+        }
+
         public void PurgeExpectations()
         {
             lock (matchersLock)