-1

I wrote an integration test to insert two documents with two threads simultaneously, after the test is completed, I expect none of the records inserted during the test exist in the database.

I used this trick for my integration tests(to roll back all test transactions when test finished). I want run two simultaneously task in action part of an integration test, so I wrote following test(I got the idea from this link):

 [TestClass]
 public class MyIntegrationTest : IntegrationTestsBase {

     [TestMethod]
     public void SaveTwoDocumentsSimultaneously_WorkSuccessfully()
     {
        //Assign
        var doc1 = new Document() {Number = "Test1"};
        var doc2 = new Document() {Number = "Test2"};

        //action
        CountdownEvent countdown = new CountdownEvent(2);
        ThreadPool.QueueUserWorkItem(WorkerThread, new object[] { Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete), doc1, countdown });
        ThreadPool.QueueUserWorkItem(WorkerThread, new object[] { Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete), doc2, countdown });
        countdown.Wait();
    
        //assert
        //assertion code for chack two document inserted
        ....
     }
 }

And this is my integration base class (Wrap each test via TransactionScope and rollback it at the end of test run):

 [TestClass]
 public abstract class IntegrationTestsBase
 {
     private TransactionScope _scope;

     [TestInitialize]
     public void Setup()
     {
        this._scope = new TransactionScope(TransactionScopeOption.Required,
         new System.TimeSpan(0, 10, 0));
     }

     [TestCleanup]
     public void Cleanup()
     {
        this._scope.Dispose();
     } 
 }

and this is WorkerThread code(I got from this link):

 private static void WorkerThread(object state)
 {
     if (state is object[] array)
     {
         var transaction = array[0];
         var document = array[1] as Document;
         CountdownEvent countdown = array[2] as CountdownEvent;

         try
         {
             //Create a DependentTransaction from the object passed to the WorkerThread
             DependentTransaction dTx = (DependentTransaction)transaction;

             //Sleep for 1 second to force the worker thread to delay
             Thread.Sleep(1000);
             //Pass the DependentTransaction to the scope, so that work done in the scope becomes part of the transaction passed to the worker thread
             using (TransactionScope ts = new TransactionScope(dTx))
             {
                 //Perform transactional work here.
                 using (var ctx = new PlanningDbContext())
                 {
                     ctx.Documents.Add(doc);
                     ctx.SaveChanges();  //<----exception occures here when second document insert
                 }
                 //Call complete on the transaction scope
                 ts.Complete();
             }

             //Call complete on the dependent transaction
             dTx.Complete();
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex);
        }
        finally
        {
           countdown?.Signal();
        }
    }
}

When I run the test I get following error at saving document point:

System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.Entity.Core.UpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Transactions.TransactionException: The operation is not valid for the state of the transaction.

Where in my code leads to this error and how can I resolve that?

16
  • Isn't that the expected behavior and thus this "works as designed"? You are not supposed to (successfully) do that simultaniously. So don't. Commented Oct 2, 2024 at 8:03
  • @Fildor: Why am I not supposed to successfully do that simultaniously? Commented Oct 2, 2024 at 8:31
  • You are trying to make changes to a transaction that has been completed. That's basically what the error message is saying. I would expect this to be at least flakey but probably fail most of the time if not consistently always. Commented Oct 2, 2024 at 8:39
  • 1
    @Fildor: I edited my last line of my question ;) Commented Oct 2, 2024 at 9:10
  • 1
    Can you please describe what is the end goal here? Also can you please provide a full minimal reproducible example? Commented Oct 5, 2024 at 7:03

2 Answers 2

0

You might be getting this error because you are trying to open up two connections to the database within the same transaction (since you are reusing the same transaction).

In order to resolve the issue, create a single database context and pass it to the method.

Sign up to request clarification or add additional context in comments.

Comments

0

Since you did not provide full example to reproduce I cannot tell with 100% confidence what's happening, however I'd suggest to check your connection setup if MARS is on - https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql/enabling-multiple-active-result-sets

I'd like to point out that in here

             using (TransactionScope ts = new TransactionScope(dTx))
             {
                 //Perform transactional work here.
                 using (var ctx = new PlanningDbContext())
                 {
                     ctx.Documents.Add(doc);
                     ctx.SaveChanges();  //<----exception occures here when second document insert
                 }
                 //Call complete on the transaction scope
                 ts.Complete();
             }

you are disposing your DBContext that has pooled connection to database, with pending transaction, which means, that the underlying updates won't be committed right away and hand till the top most transaction will commit, so your first insert is not fully committed yet, when the second is happening. Depending on your desired IsolationLevel it may or may not lead to unexpected results, like locks or dirty reads, depending on the exact scenario.

In reality your inserts will happen sequentially anyway, so it kind of defies purposes of running them seemingly in parallel on ThreadPool.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.