Tuesday, September 17, 2013

System.Transactions.TransactionScope Timing out

When using the System.Transactions.TransactionScope there is a default machine setting timeout of 10 minutes.  If you have a long running transaction and it runs for over 10 minutes, it will timeout with the following exception message:

"The operation is not valid for the state of the transaction."

And an inner exception message of:

"Transaction Timeout"


There appear to be three ways to deal with this:

1.) Setting the system.transactions machinesettings maxtimeout in the machine.config on the server.  This is done by adding the following to the machine.config:

<system.transactions>
<machineSettings maxTimeout="00:20:00" />
    </system.transactions>

This is a machine wide setting and it may interfere with administrative policies.  It directly effects all applications running on the server.

2.) Setting allowExeDefinition="MachineToApplication" on the system.transaction section group in the machine.config.  The default is "MachineOnly".  See below for machine.config configuration:

        <sectionGroup name="system.transactions" type="System.Transactions.Configuration.TransactionsSectionGroup, System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null">
           <section name="defaultSettings" type="System.Transactions.Configuration.DefaultSettingsSection, System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null"/>
           <section name="machineSettings" type="System.Transactions.Configuration.MachineSettingsSection, System.Transactions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, Custom=null" allowDefinition="MachineOnly" allowExeDefinition="MachineToApplication"/>
        </sectionGroup>

This allows one to add the following to an application's app.config and override the machine wide setting (the default of 10 minutes or whatever setting is defined in the machine.config.):

<system.transactions>
<machineSettings maxTimeout="00:20:00" />
    </system.transactions>

Although this approach changes a machine wide setting, it does not change the machine wide maxtimeout for the machine.  One will be able to retain whatever value is set for the maxTimeout and one will be able to set whatever value is fit for the specific application in the app.config.  Thus, each app can override the machine wide maxTimeout setting and set its own maxTimeout.  I think this is better than the first approach as it is a bit more secure.  It doesn't change the machine wide maxTimeout setting which changing could consequently expose a DOS situation for other apps that may be on the server.  There may be a company policy that doesn't allow this option as it would give any application the ability to change the setting.

3.) Use reflection to change the setting via custom code.  This approach accesses private data members of Microsoft classes and thus could break in the future.  It doesn't require modifying the machine.config,  it doesn't open up other application to possible DOS situations and it circumvents any company policy.  Below is the code that does this:

    public static class TransactionManagerHelper
    {
        public static void OverrideMaximumTimeout(TimeSpan timeout)
        {
            //TransactionScope inherits a *maximum* timeout from Machine.config.  There's no way to override it from
            //code unless you use reflection.  Hence this code!
            //TransactionManager._cachedMaxTimeout
            var type = typeof(TransactionManager);
            var cachedMaxTimeout = type.GetField("_cachedMaxTimeout", BindingFlags.NonPublic | BindingFlags.Static);
            cachedMaxTimeout.SetValue(null, true);

            //TransactionManager._maximumTimeout
            var maximumTimeout = type.GetField("_maximumTimeout", BindingFlags.NonPublic | BindingFlags.Static);
            maximumTimeout.SetValue(null, timeout);
        }
    }

To use it call the following before creating the TransactionScope object:

TransactionManagerHelper.OverrideMaximumTimeout(TimeSpan.FromMinutes(5));

I've tested all three options.  They will all work.  I also verified that using the reflection method will change the setting for just the one application and not for other applications on the same server.


I propose using the reflection method and using a custom appSetting from the app.config to specify the timeout value passed to OverrideMaximumTimeout.  Another option would be to re-use the System.Transactions.Configuration.DefaultSettingsSection configuration section.  If a Timeout value exists for that configuration section in the app.config then pull the value from there and use it in the call to OverrideMaximumTimeout.

No comments:

Post a Comment