3

I have a Windows Service application which is performing some calls to SQL Server. I have a particular unit of work to do which involves saving one row to the Message table and updating multiple rows in the Buffer table.

I have wrapped these two SQL statements into a TransactionScope to ensure that they either both get committed, or neither get committed.

The high level code looks like this:

public static void Save(Message message)
{
    using (var transactionScope = new TransactionScope())
    {
        MessageData.Save(message.TransactionType,
                         message.Version,
                         message.CaseNumber,
                         message.RouteCode,
                         message.BufferSetIdentifier,
                         message.InternalPatientNumber,
                         message.DistrictNumber,
                         message.Data,
                         message.DateAssembled,
                         (byte)MessageState.Inserted);

        BufferLogic.FlagSetAsAssembled(message.BufferSetIdentifier);

        transactionScope.Complete();
    }
}

This has all worked perfectly on my development machine with a local SQL Server installation.

On deploying the Windows Service to a server (but connecting back to my local machine's SQL Server) I am intermittently getting this error message:

System.ArgumentNullException: Value cannot be null.
   at System.Threading.Monitor.Enter(Object obj)
   at System.Data.ProviderBase.DbConnectionPool.TransactedConnectionPool.TransactionEnded(Transaction transaction, DbConnectionInternal transactedObject)
   at System.Data.SqlClient.SqlDelegatedTransaction.SinglePhaseCommit(SinglePhaseEnlistment enlistment)
   at System.Transactions.TransactionStateDelegatedCommitting.EnterState(InternalTransaction tx)
   at System.Transactions.CommittableTransaction.Commit()
   at System.Transactions.TransactionScope.InternalDispose()
   at System.Transactions.TransactionScope.Dispose()
   at OpenLink.Logic.MessageLogic.Save(Message message) in E:\DevTFS\P0628Temp\OpenLink\OpenLink.Logic\MessageLogic.cs:line 30
   at OpenLinkMessageAssembler.OpenLinkMessageAssemblerService.RunService() in E:\DevTFS\P0628Temp\OpenLink\OpenLinkMessageAssembler\OpenLinkMessageAssemblerService.cs:line 99

I believe the line of code being referred to by the exception is where the using block is closed, thus calling the Dispose() method of the TransactionScope. I'm at a bit of a loss here, as the exception seems to be thrown by the internal workings of the TransactionScope class.

One thing that may be significant is that when installing on the server, I had to enable some of the settings for the Distributed Transaction Coordinator to allow network access This got me into thinking that when it's all on my local machine, DTC is probably not used.

Could DTC be part of the cause of this exception?

I also considered whether it was to do with connection pools being maxed out, but would expect a more useful exception than what I'm getting. I kept running the query in this question to check the connection pool size, and it never exceeded four.

My ultimate question is, why is this error intermittently occurring? How can I diagnose what's causing it?

Edit: Threading

@Joe suggested this could be a threading issue. I've therefore included the skeleton code of my Windows Service below to see if it is problematic.

Note that the EventLogger class writes only to the Windows event log and does not connect to SQL Server.

partial class OpenLinkMessageAssemblerService : ServiceBase
{
    private volatile bool _isStopping;
    private readonly ManualResetEvent _stoppedEvent;
    private readonly int _stopTimeout = Convert.ToInt32(ConfigurationManager.AppSettings["ServiceOnStopTimeout"]);
    Thread _workerThread;

    public OpenLinkMessageAssemblerService()
    {
        InitializeComponent();
        _isStopping = false;
        _stoppedEvent = new ManualResetEvent(false);
        ServiceName = "OpenLinkMessageAssembler";
    }

    protected override void OnStart(string[] args)
    {
        try
        {
            _workerThread = new Thread(RunService) { IsBackground = true };
            _workerThread.Start();
        }
        catch (Exception exception)
        {
            EventLogger.LogError(ServiceName, exception.ToString());
            throw;
        }
    }

    protected override void OnStop()
    {
        // Set the global flag so it can be picked up by the worker thread
        _isStopping = true;

        // Allow worker thread to exit cleanly until timeout occurs
        if (!_stoppedEvent.WaitOne(_stopTimeout))
        {
            _workerThread.Abort();
        }
    }

    private void RunService()
    {
        // Check global flag which indicates whether service has been told to stop
        while (!_isStopping)
        {
            try
            {
                var buffersToAssemble = BufferLogic.GetNextSetForAssembly();

                if (!buffersToAssemble.Any())
                {
                    Thread.Sleep(30000);
                    continue;
                }

                ... // Some validation code removed here for clarity

                string assembledMessage = string.Empty;
                buffersToAssemble.ForEach(b => assembledMessage += b.Data);

                var messageParser = new MessageParser(assembledMessage);
                var message = messageParser.Parse();

                MessageLogic.Save(message); // <-- This calls the method which results in the exception
            }
            catch (Exception exception)
            {
                EventLogger.LogError(ServiceName, exception.ToString());
                throw;
            }
        }
        _stoppedEvent.Set();
    }
}
Community
  • 1
  • 1
Sir Crispalot
  • 4,792
  • 1
  • 39
  • 64
  • Is it a multi-threaded application? Is it possible that you're sharing database connections between threads? – Joe Sep 14 '12 at 17:37
  • It is a Windows Service with two threads. One thread handles the start/stop requests and fires off a new background thread which actually does all the work. However the first thread never writes to the database, only the background thread. – Sir Crispalot Sep 15 '12 at 10:05
  • It does smell like a thread-safety problem, so I have to ask: are you absolutely sure there can only ever be one background thread? Do you open/close connections on every database access (as you should) or do you reuse connections (which could cause this if multiple threads try to use the same connection)? Is the Save method that creates the TransactionScope run on the background thread (i.e. the same thread as the DB access)? – Joe Sep 15 '12 at 11:25
  • Hi @Joe, my question has been updated with the main structure where threading can be seen. – Sir Crispalot Sep 17 '12 at 15:51

3 Answers3

0

Check you have setup Your your web server and separate db server if you have them separate.

http://itknowledgeexchange.techtarget.com/sql-server/how-to-configure-dtc-on-windows-2008/

For Logging i would Suggest put int a try catch inside the transaction scope However if you logging to database you will need to make use of transaction scope suppress function

 using(TransactionScope scope4 = new 
        TransactionScope(TransactionScopeOption.Suppress)) 
    {
     ...
    } 
P6345uk
  • 703
  • 10
  • 26
  • There's no web server involved as it's a Windows Service. Saying that though, I haven't verified the DTC setup on my local machine where the SQL Server database resides. I'll try this on Monday when I'm back at work. – Sir Crispalot Sep 15 '12 at 10:03
  • I checked the DTC setup on my local machine and it was fine. I don't think this is the issue, as it is an intermittent problem. If DTC wasn't configured correctly, it probably wouldn't work at all. – Sir Crispalot Sep 18 '12 at 07:38
  • Did you Log the error inside the transaction scope ? and log all the information you are trying to save to database (Message). to see if there is a particular correlation between what information cause this issue. – P6345uk Sep 18 '12 at 10:24
0

I worked around this by stopping the transaction from being escalated to DTC. By using SQL 2008 instead of SQL 2005, the transaction does not get escalated, and all is fine.

Community
  • 1
  • 1
Sir Crispalot
  • 4,792
  • 1
  • 39
  • 64
0

You do not mention your .Net version but according to http://support.microsoft.com/kb/960754, there is an issue with 2.50727.4016 version of System.Data.dll.

If your server has this older version, I would try to get the updated one from Microsoft.

Jack Bolding
  • 3,801
  • 3
  • 39
  • 44