Fixed a serious bug in the TransactionGroup which would cause the TransactionGroup to crash when all the transactions it gets passed in the constructor were in the ended state; fixed another serious bug that would occur when transactions reached the ended state during the constructor; wrote unit tests that reproduced those bugs

git-svn-id: file:///srv/devel/repo-conversion/nusu@121 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2009-02-26 19:47:41 +00:00
parent a2f53639d4
commit 190e32ee56
6 changed files with 164 additions and 26 deletions

View File

@ -98,7 +98,10 @@ namespace Nuclex.Support.Tracking {
IObservationSubscriber subscriber = this.mockery.NewMock<IObservationSubscriber>(); IObservationSubscriber subscriber = this.mockery.NewMock<IObservationSubscriber>();
Expect.AtLeast(0).On(subscriber).Method("ProgressUpdated"); Expect.AtLeast(0).On(subscriber).Method("ProgressUpdated");
Expect.Once.On(subscriber).Method("Ended"); // This should no be called because otherwise, the 'Ended' event would be raised
// to the transaction group before all transactions have been added into
// the internal list, leading to an early ending or even multiple endings.
Expect.Never.On(subscriber).Method("Ended");
using( using(
ObservedWeightedTransaction<Transaction> test = ObservedWeightedTransaction<Transaction> test =

View File

@ -58,7 +58,12 @@ namespace Nuclex.Support.Tracking {
// prevent object coupling where none is neccessary and to save some processing time. // prevent object coupling where none is neccessary and to save some processing time.
this.progress = 1.0f; this.progress = 1.0f;
progressUpdateCallback(); progressUpdateCallback();
endedCallback();
// Do not call the ended callback here. This constructor is called when the
// TransactionGroup constructs its list of transactions. If this is called and
// the first transaction to be added to the group happens to be in the ended
// state, the transactionGroup will immediately think it has ended!
//!DONT!endedCallback();
return; return;

View File

@ -18,12 +18,12 @@ License along with this library
*/ */
#endregion #endregion
#if UNITTEST
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
#if UNITTEST
using NUnit.Framework; using NUnit.Framework;
using NMock2; using NMock2;

View File

@ -18,13 +18,13 @@ License along with this library
*/ */
#endregion #endregion
#if UNITTEST
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
#if UNITTEST
using NUnit.Framework; using NUnit.Framework;
using NMock2; using NMock2;

View File

@ -21,6 +21,7 @@ License along with this library
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
#if UNITTEST #if UNITTEST
@ -139,6 +140,53 @@ namespace Nuclex.Support.Tracking {
#endregion // class TestTransaction #endregion // class TestTransaction
#region class ChainEndingTransaction
/// <summary>
/// Transaction that ends another transaction when its Ended property is called
/// </summary>
private class ChainEndingTransaction : Transaction {
/// <summary>Initializes a new chain ending transaction</summary>
public ChainEndingTransaction() {
this.chainedTransaction = new TestTransaction();
}
/// <summary>Transitions the transaction into the ended state</summary>
public void End() {
OnAsyncEnded();
}
/// <summary>
/// Transaction that will end when this transaction's ended property is accessed
/// </summary>
public TestTransaction ChainedTransaction {
get { return this.chainedTransaction; }
}
/// <summary>Whether the transaction has ended already</summary>
public override bool Ended {
get {
if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
this.chainedTransaction.End();
}
return base.Ended;
}
}
/// <summary>
/// Transaction that will end when this transaction's ended property is accessed
/// </summary>
private TestTransaction chainedTransaction;
/// <summary>Whether we already ended the chained transaction and ourselves</summary>
private int endedCalled;
}
#endregion // class ChainEndingTransaction
/// <summary>Initialization routine executed before each test is run</summary> /// <summary>Initialization routine executed before each test is run</summary>
[SetUp] [SetUp]
public void Setup() { public void Setup() {
@ -239,6 +287,63 @@ namespace Nuclex.Support.Tracking {
} }
} }
/// <summary>
/// Verifies that the transaction group immediately enters the ended state when
/// the contained transactions have already ended before the constructor
/// </summary>
/// <remarks>
/// This was a bug at one time and should prevent a regression
/// </remarks>
[Test]
public void TestAlreadyEndedTransactions() {
using(
TransactionGroup<Transaction> testTransactionGroup =
new TransactionGroup<Transaction>(
new Transaction[] { Transaction.EndedDummy, Transaction.EndedDummy }
)
) {
Assert.IsTrue(testTransactionGroup.Wait(1000));
}
}
/// <summary>
/// Verifies that the transaction group doesn't think it's already ended when
/// the first transaction being added is in the ended state
/// </summary>
/// <remarks>
/// This was a bug at one time and should prevent a regression
/// </remarks>
[Test]
public void TestAlreadyEndedTransactionAsFirstTransaction() {
using(
TransactionGroup<Transaction> testTransactionGroup =
new TransactionGroup<Transaction>(
new Transaction[] { Transaction.EndedDummy, new TestTransaction() }
)
) {
Assert.IsFalse(testTransactionGroup.Ended);
}
}
/// <summary>
/// Verifies that a transaction ending while the constructor is running doesn't
/// wreak havoc on the transaction group
/// </summary>
[Test]
public void TestTransactionEndingDuringConstructor() {
ChainEndingTransaction chainTransaction = new ChainEndingTransaction();
using(
TransactionGroup<Transaction> testTransactionGroup =
new TransactionGroup<Transaction>(
new Transaction[] { chainTransaction.ChainedTransaction, chainTransaction }
)
) {
Assert.IsFalse(testTransactionGroup.Ended);
chainTransaction.End();
Assert.IsTrue(testTransactionGroup.Ended);
}
}
/// <summary>Mocks a subscriber for the events of a transaction</summary> /// <summary>Mocks a subscriber for the events of a transaction</summary>
/// <param name="transaction">Transaction to mock an event subscriber for</param> /// <param name="transaction">Transaction to mock an event subscriber for</param>
/// <returns>The mocked event subscriber</returns> /// <returns>The mocked event subscriber</returns>

View File

@ -21,6 +21,7 @@ License along with this library
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Threading;
using Nuclex.Support.Collections; using Nuclex.Support.Collections;
@ -35,17 +36,18 @@ namespace Nuclex.Support.Tracking {
public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged; public event EventHandler<ProgressReportEventArgs> AsyncProgressChanged;
/// <summary>Initializes a new transaction group</summary> /// <summary>Initializes a new transaction group</summary>
/// <param name="childs">Transactions to track with this group</param> /// <param name="children">Transactions to track with this group</param>
/// <remarks> /// <remarks>
/// Uses a default weighting factor of 1.0 for all transactions. /// Uses a default weighting factor of 1.0 for all transactions.
/// </remarks> /// </remarks>
public TransactionGroup(IEnumerable<TransactionType> childs) public TransactionGroup(IEnumerable<TransactionType> children) {
: this() { List<ObservedWeightedTransaction<TransactionType>> childrenList =
new List<ObservedWeightedTransaction<TransactionType>>();
// Construct a WeightedTransaction with the default weight for each // Construct a WeightedTransaction with the default weight for each
// transaction and wrap it in an ObservedTransaction // transaction and wrap it in an ObservedTransaction
foreach(TransactionType transaction in childs) { foreach(TransactionType transaction in children) {
this.children.Add( childrenList.Add(
new ObservedWeightedTransaction<TransactionType>( new ObservedWeightedTransaction<TransactionType>(
new WeightedTransaction<TransactionType>(transaction), new WeightedTransaction<TransactionType>(transaction),
new ObservedWeightedTransaction<TransactionType>.ReportDelegate( new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
@ -60,20 +62,26 @@ namespace Nuclex.Support.Tracking {
// Since all transactions have a weight of 1.0, the total weight is // Since all transactions have a weight of 1.0, the total weight is
// equal to the number of transactions in our list // equal to the number of transactions in our list
this.totalWeight = (float)this.children.Count; this.totalWeight = (float)childrenList.Count;
// Thread.MemoryBarrier(); // not needed because children is volatile
this.children = childrenList;
// Any asyncEnded events being receiving from the transactions until now
// would have been ignored, so we need to check again here
asyncChildEnded();
} }
/// <summary>Initializes a new transaction group</summary> /// <summary>Initializes a new transaction group</summary>
/// <param name="childs">Transactions to track with this group</param> /// <param name="children">Transactions to track with this group</param>
public TransactionGroup( public TransactionGroup(
IEnumerable<WeightedTransaction<TransactionType>> childs IEnumerable<WeightedTransaction<TransactionType>> children
) ) {
: this() { List<ObservedWeightedTransaction<TransactionType>> childrenList =
new List<ObservedWeightedTransaction<TransactionType>>();
// Construct an ObservedTransaction around each of the WeightedTransactions // Construct an ObservedTransaction around each of the WeightedTransactions
foreach(WeightedTransaction<TransactionType> transaction in childs) { foreach(WeightedTransaction<TransactionType> transaction in children) {
this.children.Add( childrenList.Add(
new ObservedWeightedTransaction<TransactionType>( new ObservedWeightedTransaction<TransactionType>(
transaction, transaction,
new ObservedWeightedTransaction<TransactionType>.ReportDelegate( new ObservedWeightedTransaction<TransactionType>.ReportDelegate(
@ -89,11 +97,11 @@ namespace Nuclex.Support.Tracking {
this.totalWeight += transaction.Weight; this.totalWeight += transaction.Weight;
} }
} this.children = childrenList;
/// <summary>Performs common initialization for the public constructors</summary> // Any asyncEnded events being receiving from the transactions until now
private TransactionGroup() { // would have been ignored, so we need to check again here
this.children = new List<ObservedWeightedTransaction<TransactionType>>(); asyncChildEnded();
} }
/// <summary>Immediately releases all resources owned by the object</summary> /// <summary>Immediately releases all resources owned by the object</summary>
@ -162,6 +170,10 @@ namespace Nuclex.Support.Tracking {
/// Called when the progress of one of the observed transactions changes /// Called when the progress of one of the observed transactions changes
/// </summary> /// </summary>
private void asyncProgressUpdated() { private void asyncProgressUpdated() {
if(this.children == null) {
return;
}
float totalProgress = 0.0f; float totalProgress = 0.0f;
// Calculate the sum of the progress reported by our child transactions, // Calculate the sum of the progress reported by our child transactions,
@ -184,6 +196,15 @@ namespace Nuclex.Support.Tracking {
/// </summary> /// </summary>
private void asyncChildEnded() { private void asyncChildEnded() {
// If a transaction reports its end durign the constructor, it will end up here
// where the collection has not been assigned yet, allowing us to skip the
// check until all transactions are there (otherwise, we might invoke
// OnAsyncended() early, because all transactions in the list seem to have ended
// despite the fact that the constructor hasn't finished adding transactions yet)
if(this.children == null) {
return;
}
// If there's still at least one transaction going, don't report that // If there's still at least one transaction going, don't report that
// the transaction group has finished yet. // the transaction group has finished yet.
for(int index = 0; index < this.children.Count; ++index) for(int index = 0; index < this.children.Count; ++index)
@ -191,12 +212,14 @@ namespace Nuclex.Support.Tracking {
return; return;
// All child transactions have ended, so the set has now ended as well // All child transactions have ended, so the set has now ended as well
if(Interlocked.Exchange(ref this.endedCalled, 1) == 0) {
OnAsyncEnded(); OnAsyncEnded();
}
} }
/// <summary>Transactions being managed in the set</summary> /// <summary>Transactions being managed in the set</summary>
private List<ObservedWeightedTransaction<TransactionType>> children; private volatile List<ObservedWeightedTransaction<TransactionType>> children;
/// <summary> /// <summary>
/// Wrapper collection for exposing the child transactions under the /// Wrapper collection for exposing the child transactions under the
/// WeightedTransaction interface /// WeightedTransaction interface
@ -204,6 +227,8 @@ namespace Nuclex.Support.Tracking {
private volatile WeightedTransactionWrapperCollection<TransactionType> wrapper; private volatile WeightedTransactionWrapperCollection<TransactionType> wrapper;
/// <summary>Summed weight of all transactions in the set</summary> /// <summary>Summed weight of all transactions in the set</summary>
private float totalWeight; private float totalWeight;
/// <summary>Whether we already called OnAsyncEnded</summary>
private int endedCalled;
} }