ACID isn’t the trip it used to be

Transactions. If you don’t know why you need them, it’s high time you learnt. But don’t look at me, you should have known that stuff by the end of semester one at uni.

If you do know why you need transactions you also know what a drama they are when the transaction context spans multiple machines and sometimes extended periods. Microsoft has been farting around with this stuff since time out of mind. I remember back in the late nineties, the DTC (distributed transaction controller) was going to be all things to everyone and we were all going to go home early and have long transactional vacations in the Caribbean (or Europe, if like me you prefer snow to sand).

It was all terribly complicated and you had to go through all manner of contortions and it leaked like a bucket with no bottom, and no one had a clear idea of how COM worked, much less COM+, there were huge teams of confused people spouting acronyms and getting in each others’ way, and lordy what a mess.

I wandered off and worked on smaller systems for people who never heard of transactions, even if they really should have, and other people who really didn’t need them at all, met a nice girl and on the whole managed to stay out of trouble. Mostly.

Finally it all caught up with me. This afternoon I was debugging some code that does several things to several databases potentially on multiple servers and it went pear shaped. This was not terribly surprising with unfinished code, but it did leave the aggregate system with inconsistent state, and while I cleaned up the mess it occurred to me that a distributed transaction was required.

I could tell you about it, but let’s skip a whole bunch of tiresome research, and just say that I’ve discovered the System.Transaction namespace and (with particular glee) TransactionScope.

Here’s a code snippet from this afternoon’s labours that illustrates very nicely how ridiculously easy it is to use:

   60             using (TransactionScope scope = new TransactionScope())

   61             {

   62               var selectedAssetDevice = (

   63                 from a in dcAtomdb.Assets

   64                 join d in dcAtomdb.Devices

   65                 on a.AttachedDeviceID equals d.Id

   66                 where a.Id == selectedAsset.Id

   67                 select new { a.AttachedDeviceID, d.DeviceNumber }).FirstOrDefault();

   68               if (selectedAssetDevice.DeviceNumber == _deviceNumber.ToString())

   69                 DialogResult = DialogResult.OK;

   70               else

   71               {

   72                 string msg =string.Format(

   73                   "You are changing the device associated with {0}",

   74                   selectedAsset.AssetName);

   75                 DialogResult dr = MessageBox.Show(

   76                   this, msg, "Overwrite", MessageBoxButtons.OKCancel,

   77                   MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);

   78                 if (dr == DialogResult.OK)

   79                 {

   80                   try

   81                   {

   82                     dcAspnetdb.RegisterDevice(ServerName, DatabaseName, DeviceNumber);

   83                     Guid? deviceId = Guid.Empty;

   84                     dcAtomdb.GetDeviceId(_deviceNumber.ToString(), ref deviceId);

   85                     dcAtomdb.SetAssetDevice(selectedAsset.Id, deviceId);

   86                     scope.Complete();

   87                   }

   88                   catch (Exception ex)

   89                   {

   90                     MessageBox.Show(

   91                       this, string.Format("Transaction failed: {0}", ex.Message),

   92                       "Update Failed", MessageBoxButtons.OK, MessageBoxIcon.Error);

   93                   }

   94                   DialogResult = DialogResult.OK;

   95                 }

   96                 else

   97                   DialogResult = DialogResult.None;

   98               }

   99             }

It does everything. Including escalation to DTS if necessary. The code is simple, because to roll back you don’t have to do anything but let the transaction scope object go out of scope without committing. So essentially you define the transaction scope literally and explicitly with a using statement on a TransactionScope instance, and you write normal exception handlers, and the last thing you do on the success path is invoke scope.Complete() to commit the transaction. All the DTC enlistment waffle is taken care of automatically, including working out whether it’s necessary at all.

Fantastic stuff, Microsoft. It took a decade, but it was worth it.

Published 04-20-2009 21:43 by peterw