With the help of Microsoft, I finally understood how the COM+ transaction timeout works. The COM+ transaction timeout, is a timeout on the transaction, not on the method call. The method will execute, as long as it has to execute. The moment the first transaction is enlisted, the transaction timeout starts to work. If the transaction takes more time than the specified timeout, the transaction is marked for abortion, but is not canceled, it continues to run. When the COM+ method calls SetComplete, only then an exception is thrown. The timeout exception is the following:
System.Runtime.InteropServices.ComException: {"The root transaction wanted to commit, but transaction aborted (Exception from HRESULT: 0x8004E002)"}.
A transaction is enlisted as soon as a connection is opened to the database or as soon as something is written to a message queue.
Suppose the transaction timeout is 10 seconds. Let us consider the following 3 cases:
1. The COM+ method immediately enlists a transaction that takes 11 seconds.
In this case the timeout exception is thrown after 11 seconds, because the transaction took longer than the timeout.
2. The COM+ method sleeps for 6 seconds (or does some calculations) and then enlists a transaction that takes 5 seconds.
In this case the method completes without an error, although the total execution time is also 11 seconds.
3. The COM+ method immediately enlists a transaction that takes 5 seconds, and then sleeps for 6 seconds.
In this case the timeout exception is thrown after 11 seconds, because the time between the enlistment of the transaction and the SetComplete call is 11 seconds, which is more than the timeout.
In summary: If the time between the first enlistment of the transaction and the SetComplete call is more than the transaction timeout, the timeout exception is thrown. However, the COM+ method always runs, for as long as it has to run, irrespective to the transaction timeout.
To test the above cases, here is the code in VB.NET:
1. Project Test1BO.vbproj as Class Library signed with a strong key, having the two files:
1.1 AssemblyInfo.vb:
Imports System.EnterpriseServices
Imports System.Reflection
Imports System.Runtime.InteropServices
<Assembly: ApplicationActivation(ActivationOption.Server)>
<Assembly: ApplicationAccessControl(CType(AccessChecksLevelOption.Application, Boolean))>
<Assembly: Guid("799facfd-af56-4496-bc18-618e2522e5f7")>
<Assembly: AssemblyVersion("1.0.0.0")>
<Assembly: AssemblyFileVersion("1.0.0.0")>
1.2 ClassComPlus.vb
Imports System.EnterpriseServices
Imports System.Data.SqlClient
Imports System.Transactions
<Transaction(TransactionOption.Required, isolation:=TransactionIsolationLevel.Serializable, timeout:=10), _
EventTrackingEnabled(True), _
JustInTimeActivation(True)>
Public Class ClassComPlus
Inherits ServicedComponent
Public Function DbExecuteNonQuery(
connectionString As String, cmdText As String,
sleepSecondsBefore As Integer, sleepSecondsAfter As Integer) As Integer
Try
Threading.Thread.Sleep(sleepSecondsBefore * 1000)
Dim result = 0
Using cn = New SqlConnection(connectionString)
cn.Open()
Dim cmd = New SqlCommand(cmdText, cn)
result = cmd.ExecuteNonQuery()
End Using
Threading.Thread.Sleep(sleepSecondsAfter * 1000)
ContextUtil.SetComplete()
Return result
Catch
ContextUtil.SetAbort()
Throw
End Try
End Function
Test1BO must be registered in COM+ for example with the following RegisterComPlus.bat file in the folder where the dll is created:
set regsvcs=C:\Windows\Microsoft.NET\Framework\v4.0.30319\regsvcs
set topdir=%~dp0
set dllname=Test1BO
%regsvcs% /u "%topdir%\%dllname%.dll"
%regsvcs% "%topdir%\%dllname%.dll"
pause
2. Project Test1CA.vbproj as Console Application with reference to Test1BO.dll has just Module1.vb:
Module Module1
Private Sub MyTrace(s As String)
Console.WriteLine(String.Format("{0} {1}", Now.ToString("yyyy-MM-dd HH:mm:ss"), s))
End Sub
Private Sub TestDbExecNonQuery()
Dim obj = New Test1BO.ClassComPlus
Dim connectionString = ConfigurationManager.ConnectionStrings("DB1").ConnectionString
Dim cmdText = "WAITFOR DELAY '00:00:011'"
Try
MyTrace("TestDbExecNonQuery begin " & cmdText & ", 0, 0")
obj.DbExecuteNonQuery(connectionString, cmdText, 0, 0)
MyTrace("TestDbExecNonQuery end")
Catch ex As Exception
MyTrace("TestDbExecNonQuery exception")
MyTrace(ex.Message)
End Try
cmdText = "WAITFOR DELAY '00:00:05'"
Try
MyTrace("TestDbExecNonQuery begin " & cmdText & ", 6, 0")
obj.DbExecuteNonQuery(connectionString, cmdText, 6, 0)
MyTrace("TestDbExecNonQuery end")
Catch ex As Exception
MyTrace("TestDbExecNonQuery exception")
MyTrace(ex.Message)
End Try
Try
MyTrace("TestDbExecNonQuery begin " & cmdText & ", 0, 6")
obj.DbExecuteNonQuery(connectionString, cmdText, 0, 6)
MyTrace("TestDbExecNonQuery end")
Catch ex As Exception
MyTrace("TestDbExecNonQuery exception")
MyTrace(ex.Message)
End Try
obj.Dispose()
End Sub
Sub Main()
TestDbExecNonQuery()
Console.WriteLine("Press any key to close")
Console.ReadKey()
End Sub
End Module
Running the above, the following output is expected:
2016-12-09 16:47:17 TestDbExecNonQuery begin WAITFOR DELAY '00:00:011', 0, 0
2016-12-09 16:47:29 TestDbExecNonQuery exception
2016-12-09 16:47:29 The root transaction wanted to commit, but transaction aborted (Exception from HRESULT: 0x8004E002)
2016-12-09 16:47:29 TestDbExecNonQuery begin WAITFOR DELAY '00:00:05', 6, 0
2016-12-09 16:47:40 TestDbExecNonQuery end
2016-12-09 16:47:40 TestDbExecNonQuery begin WAITFOR DELAY '00:00:05', 0, 6
2016-12-09 16:47:51 TestDbExecNonQuery exception
2016-12-09 16:47:51 The root transaction wanted to commit, but transaction aborted (Exception from HRESULT: 0x8004E002)
Press any key to close