- Home
- LLBLGen Pro
- Architecture
Distributed Transaction Handling (SQL Server)
Joined: 23-Apr-2004
Hi,
Does anyone have any idea what I can do about the following situation:
I've got a TestSuite which uses a COM+ transaction to rollback changes made to the database during the test run... This works just fine until I implemented an internal retry mechanism within the Busniess Layer. The idea is that some DB exceptions should be retried, such as timeout, concurrency etc.
The problem is that LLBLGen starts and rollsback a transaction which I then attempt to retry. But in the context of the tests, the BL is called within the context of a Distributed Transaction and the retry causes another exception: "Distributed transaction completed. Either enlist this session in a new transaction or the NULL transaction."
Since the BL is unaware of its participation in the COM+ transaction and since I don't want to introduce Serviced Components into the BL, does anyone have any ideas to get around this problem?
Personally I think I'm stuck, and will have to remove the COM+ transaction from the tests...
Would appreciate any other ideas...
The following is some pseudo code to illustrate the predicament:
[Transaction(TransactionOption.Required)]
public class TestSuite
{
public void DoTest()
{
ServiceConfig config = new ServiceConfig();
config.Transaction = TransactionOption.Required;
ServiceDomain.Enter(config);
BLTest.DoTest();
ContextUtil.SetAbort();
ServiceDomain.Leave();
}
}
The following is in another DLL which does NOT have a reference to System.EnterpriseServices
public class BLTest
{
public static void DoTest()
{
int attempt = 0;
int maxRetries = 3;
while(true)
{
try
{
MyEntity entity = CreateNewEntity();
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.SaveEntity(entity);
break;
}
}
catch (ORMException ex)
{
if (attempt++ >= maxRetries)
throw;
}
}
}
}
Joined: 17-Aug-2003
The problem is: if a DB transaction fails, it has to be rolled back. You can't simply leave it at that and retry, you have to go back to the start of the DB transaction, to actually retry it, in effect a rollback + restart all over action.
Adapter doesn't rollback transactions started from the outside though, only transactions it starts itself, which is logical as it is the maintainer. So if you start a transaction first, then call a set of recursive saves for example, it will not roll back the transaction on an exception, just fire the exception. it is then up to you what to do.
With non-com+ transactions you could use the SaveTransaction() functionality to save till a given savepoint to simulate nested transactions and retry without having to save all previous stuff again. In COM+ savepoints don't make sense as the transaction is controlled by COM+, not by the db.
Joined: 23-Apr-2004
Otis wrote:
The problem is: if a DB transaction fails, it has to be rolled back. You can't simply leave it at that and retry, you have to go back to the start of the DB transaction, to actually retry it, in effect a rollback + restart all over action.
Adapter doesn't rollback transactions started from the outside though, only transactions it starts itself, which is logical as it is the maintainer. So if you start a transaction first, then call a set of recursive saves for example, it will not roll back the transaction on an exception, just fire the exception. it is then up to you what to do.
With non-com+ transactions you could use the SaveTransaction() functionality to save till a given savepoint to simulate nested transactions and retry without having to save all previous stuff again.
Exactly..
If the outer transaction was simply an ADO.NET transaction, SaveTransaction() would be perfect... but since this is a COM+ transaction... there is no way for the BL to know about the outer COM+ transaction which was started in the TestSuite class.
Otis wrote:
In COM+ savepoints don't make sense as the transaction is controlled by COM+, not by the db.
mmm... If COM+ did support savepoints, I could overload the BL class contructor and pass in the outer COM+ transaction without needing to derive the class from ServicedComponent which would be fine...
Is there really no COM+ savepoint equivelant?
[EDIT] Or does COM+ support nested transactions (in the context of ServiceDomain of the TestSuite example)?
Thanks for your input.
Joined: 17-Aug-2003
MTS did support it, COM+ should too, I have no idea how you can define nested COM+ transactions in .NET, as you are either IN a transaction or not: creating a new context doesn't create a new transaction inside another one, if I'm not mistaken.
Joined: 23-Apr-2004
Otis wrote:
MTS did support it, COM+ should too, I have no idea how you can define nested COM+ transactions in .NET, as you are either IN a transaction or not: creating a new context doesn't create a new transaction inside another one, if I'm not mistaken.
Yes I think you are right. I will post the answer here, if I find one!
Cheers.
Joined: 04-Feb-2004
Yes you can do this. My COM+ sample that I put on the site should facilitate this functionality nicely. Basically, your client would start a transacition, then your objects doing DB updates would have TransactionOption.Required. These objects would then enlist in the clients transaction. I beleive that you should be able to retry the failed objects transaction (maybe) and if it fails again, then vote on the state of the transaction. When all objects return, the client checks the overall state of the entire transaction, and commits or rolls it back.
I think I can get a code sample for you relatively quickly.
Joined: 04-Feb-2004
Sorry for the long answer, just hope it helps you out.
So here is the scenario:
The test runs and inserts a new record into the category table. If this fails with an orm exception, it will re-try the transaction 3 times. On each pass it will change the category name, because we are assuming that the unique constraint is causing the exception to fail.
If it makes it to the end, both a new shipper and a new category are created. If it doesnt or something crazy happens, the code bombs out and all is rolled back.
The only objects registed in my com+ library are the RequiresTxManager and the ComPlusAdapter context object that is being used in each controller. This sample test was created using the code that Frans has posted in the 3rd party section.
[Test()]
public void NestedTransaction()
{
ITransactionController requiredTxController = RequiresTxManager.SerializableTxController();
ShipperController shipperController = new ShipperController();
CategoryController categoryController = new CategoryController();
try
{
int attempt = 0;
int maxRetries = 3;
bool onError = true;
while(onError)
{
try
{
CategoriesEntity newCategory = new CategoriesEntity();
if (attempt > 0)
{
newCategory.CategoryName = "AdultBeverages" + attempt;
}
else
{
newCategory.CategoryName = "Adult Beverages";
}
newCategory.IsNew=true;
requiredTxController.ExecuteMethod(categoryController,
"SaveCategory", new object[]{newCategory});
onError = false;
}
catch (TargetInvocationException ex)
{
if (ex.InnerException is ORMQueryExecutionException )
{
if (attempt++ >=maxRetries)
{
onError = false;
throw;
}
}
else
{
throw ex;
}
}
}
ShippersEntity newShipper = new ShippersEntity();
newShipper.CompanyName = "New Shipper";
newShipper.Phone = "111-222-3333";
requiredTxController.ExecuteMethod(shipperController,
"SaveShipper", new object[]{newShipper});
requiredTxController.Commit();
}
catch(System.Reflection.TargetInvocationException ex)
{
requiredTxController.Rollback();
Console.WriteLine(ex.InnerException.ToString());
}
finally
{
RequiresTxManager.DisposeAll(shipperController, categoryController);
}
}
The sample will run with server or library activation. With server activation, youll need to move all dependent objects into the GAC and mark all objects in the GAC used by COM+ in the event chain as serializable, because nunit will talk to the app via a transparent proxy object. Youll also need to create a dllhost.exe.config, for the connection string and put it into C:\windows\system32\ because server activated objects run as dllhost.exe OR allow activation in COMPlusAdapterContext class.
Also note, that if you want to debug the COM+ server code, youll need to attach to dllhost.exe with server activation mode on.
Other notes about the code. SerializableTxController() returns a new instance of an object that inherits from TransactionControllerBase. TransactionControllerBase implements ITransactionController. ITransactionController defines a methods named ExecuteMethod, Rollback, and Commit.
Here is the implementaiton of ITransactionController for TransactionControllerBase:
public object ExecuteMethod(object o, string method, params object[] aArgs)
{
try
{
// use reflection to invoke the business method within the current transaction scope
return o.GetType().InvokeMember(method,BindingFlags.InvokeMethod, null, o, aArgs);
}
catch (Exception ex)
{
// if an exception occurs, mark the current transaction as such so that we can
// roll it back later
if (ContextUtil.IsInTransaction)
{
ContextUtil.DisableCommit();
}
throw ex;
}
}
public void Rollback()
{
if (ContextUtil.IsInTransaction)
{
ContextUtil.SetAbort();
}
}
public void Commit()
{
if (ContextUtil.IsInTransaction)
{
if(ContextUtil.MyTransactionVote == TransactionVote.Commit)
{
ContextUtil.SetComplete();
}
else
{
ContextUtil.SetAbort();
}
}
}
Joined: 23-Apr-2004
Wow, thanks for your in-depth and eligant answer. Devildog for LLBLGen MVP.
I downloaded the COM+ library from the 3rd party section and went through the code (and learned some about COM+). Great stuff BTW
The only thing is that for my particular scenario, the BL is part of a web product which is to be deployed on shared hosts... which means that I cannot have any COM+ specific stuff in there at all. The only reason I'm using COM+ is to create an outer transaction in my TestSuite which will perform 'cleanup' after each of my tests run. For this I'm not using serviced components but Services Without Componenets (see http://blogs.msdn.com/florinlazar/archive/2004/07/24/194199.aspx for details). This TestSuite will not be deployed along with the application.
The problem I encountered is that the BL is completely unaware of the outer COM+ transaction. When a rollback occurs in the BL after the 1st attempt, the subsequent retry attempts to start another transaction and fails. This is because the retry transaction is still within the context of the doomed outer COM+ transaction.
The reason I was getting at nesting a COM+ transaction into the BL was to be able to create a new inner COM+ transaction which would act like a savepoint for the outer test COM+ transaction. The BL transaction would now be created in the context of the inner COM+ transaction not the outer one. This inner COM+ transaction could be rolledback and the retry could be attemped. The other thing was that the Services Without Componenets (SWC) method does not necesitate any of the normal COM+ registrations etc which means that it will work on a shared web host.
It turns out that you can indeed nest Services Without Componenets by simply nesting as follows:
ServiceConfig outer = new ServiceConfig();
outer.Transaction = TransactionOption.Required;
ServiceDomain.Enter(outer);
ServiceConfig inner = new ServiceConfig();
inner.Transaction = TransactionOption.RequiresNew;
ServiceDomain.Enter(inner);
ContextUtil.SetAbort();
ServiceDomain.Leave();
ContextUtil.SetAbort();
ServiceDomain.Leave();
Try / Finally blocks are of course required for the Leave() method calls and I'm not sure if the ContextUtil.SetAbort() is aborting the correct transaction either...
However, this works perfectly in theory... but unfortunately falls foul of IsolationLevel protection for the purpose of my scenario with the TestSuite. The outer transaction which created the test data is still ongoing, and the inner transaction was blocking.
In my tests, the TestSuite creates records in the DB during the [SetUp]. The Test examines the DB before the the BL is called to ensure that a row exists in the DB. The BL is called and it modifies / deletes the row (or whatever the business action is). The Test completes by checking that the DB has changed as expected by re-fetching the row and Asserts the result.
Even when I set all IsolationLevels, in ServiceConfig and LLBLGen to use IsolationLevel.ReadUncommitted, I was getting deadlocks... and couldn't figure out why.
To conclude, I have modified my tests so that they do not use an outer COM+ transaction for tests which will cause a BL retry. Database cleanup for these tests is performed manually on a test by test basis.
While I've ended up with something less elegant than I would have liked, I have to move on and make progress...
I will definately revisit this when I get a chance...
Thanks for your help Devildog and I will be sure to use your library for projects that require proper COM+ support.