1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using System.Collections;
6 using System.Globalization;
7 using System.IO;
8 using System.Runtime.Serialization;
9 using System.Runtime.Serialization.Formatters.Binary;
10 using System.Threading;
11 using System.Transactions.Configuration;
12 using System.Transactions.Distributed;
13 
14 namespace System.Transactions
15 {
HostCurrentTransactionCallback()16     public delegate Transaction HostCurrentTransactionCallback();
17 
TransactionStartedEventHandler(object sender, TransactionEventArgs e)18     public delegate void TransactionStartedEventHandler(object sender, TransactionEventArgs e);
19 
20     public static class TransactionManager
21     {
22         // Revovery Information Version
23         private const int RecoveryInformationVersion1 = 1;
24         private const int CurrentRecoveryVersion = RecoveryInformationVersion1;
25 
26         // Hashtable of promoted transactions, keyed by identifier guid.  This is used by
27         // FindPromotedTransaction to support transaction equivalence when a transaction is
28         // serialized and then deserialized back in this app-domain.
29         private static Hashtable s_promotedTransactionTable;
30 
31         // Sorted Table of transaction timeouts
32         private static TransactionTable s_transactionTable;
33 
34         private static TransactionStartedEventHandler s_distributedTransactionStartedDelegate;
35         public static event TransactionStartedEventHandler DistributedTransactionStarted
36         {
37             add
38             {
39                 lock (ClassSyncObject)
40                 {
41                     s_distributedTransactionStartedDelegate = (TransactionStartedEventHandler)System.Delegate.Combine(s_distributedTransactionStartedDelegate, value);
42                     if (value != null)
43                     {
44                         ProcessExistingTransactions(value);
45                     }
46                 }
47             }
48 
49             remove
50             {
51                 lock (ClassSyncObject)
52                 {
53                     s_distributedTransactionStartedDelegate = (TransactionStartedEventHandler)System.Delegate.Remove(s_distributedTransactionStartedDelegate, value);
54                 }
55             }
56         }
57 
ProcessExistingTransactions(TransactionStartedEventHandler eventHandler)58         internal static void ProcessExistingTransactions(TransactionStartedEventHandler eventHandler)
59         {
60             lock (PromotedTransactionTable)
61             {
62                 // Manual use of IDictionaryEnumerator instead of foreach to avoid DictionaryEntry box allocations.
63                 IDictionaryEnumerator e = PromotedTransactionTable.GetEnumerator();
64                 while (e.MoveNext())
65                 {
66                     WeakReference weakRef = (WeakReference)e.Value;
67                     Transaction tx = (Transaction)weakRef.Target;
68                     if (tx != null)
69                     {
70                         TransactionEventArgs args = new TransactionEventArgs();
71                         args._transaction = tx.InternalClone();
72                         eventHandler(args._transaction, args);
73                     }
74                 }
75             }
76         }
77 
FireDistributedTransactionStarted(Transaction transaction)78         internal static void FireDistributedTransactionStarted(Transaction transaction)
79         {
80             TransactionStartedEventHandler localStartedEventHandler = null;
81             lock (ClassSyncObject)
82             {
83                 localStartedEventHandler = s_distributedTransactionStartedDelegate;
84             }
85 
86             if (null != localStartedEventHandler)
87             {
88                 TransactionEventArgs args = new TransactionEventArgs();
89                 args._transaction = transaction.InternalClone();
90                 localStartedEventHandler(args._transaction, args);
91             }
92         }
93 
94         // Data storage for current delegate
95         internal static HostCurrentTransactionCallback s_currentDelegate = null;
96         internal static bool s_currentDelegateSet = false;
97 
98         // CurrentDelegate
99         //
100         // Store a delegate to be used to query for an external current transaction.
101         public static HostCurrentTransactionCallback HostCurrentCallback
102         {
103             // get_HostCurrentCallback is used from get_CurrentTransaction, which doesn't have any permission requirements.
104             // We don't expose what is returned from this property in that case.  But we don't want just anybody being able
105             // to retrieve the value.
106             get
107             {
108                 // Note do not add trace notifications to this method.  It is called
109                 // at the startup of SQLCLR and tracing has too much working set overhead.
110                 return s_currentDelegate;
111             }
112             set
113             {
114                 // Note do not add trace notifications to this method.  It is called
115                 // at the startup of SQLCLR and tracing has too much working set overhead.
116                 if (value == null)
117                 {
118                     throw new ArgumentNullException(nameof(value));
119                 }
120 
121                 lock (ClassSyncObject)
122                 {
123                     if (s_currentDelegateSet)
124                     {
125                         throw new InvalidOperationException(SR.CurrentDelegateSet);
126                     }
127                     s_currentDelegateSet = true;
128                 }
129 
130                 s_currentDelegate = value;
131             }
132         }
133 
Reenlist( Guid resourceManagerIdentifier, byte[] recoveryInformation, IEnlistmentNotification enlistmentNotification)134         public static Enlistment Reenlist(
135             Guid resourceManagerIdentifier,
136             byte[] recoveryInformation,
137             IEnlistmentNotification enlistmentNotification)
138         {
139             if (resourceManagerIdentifier == Guid.Empty)
140             {
141                 throw new ArgumentException(SR.BadResourceManagerId, nameof(resourceManagerIdentifier));
142             }
143 
144             if (null == recoveryInformation)
145             {
146                 throw new ArgumentNullException(nameof(recoveryInformation));
147             }
148 
149             if (null == enlistmentNotification)
150             {
151                 throw new ArgumentNullException(nameof(enlistmentNotification));
152             }
153 
154             TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
155             if (etwLog.IsEnabled())
156             {
157                 etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.Reenlist");
158                 etwLog.TransactionManagerReenlist(resourceManagerIdentifier);
159             }
160 
161             // Put the recovery information into a stream.
162             MemoryStream stream = new MemoryStream(recoveryInformation);
163             int recoveryInformationVersion = 0;
164             string nodeName = null;
165             byte[] resourceManagerRecoveryInformation = null;
166 
167             try
168             {
169                 BinaryReader reader = new BinaryReader(stream);
170                 recoveryInformationVersion = reader.ReadInt32();
171 
172                 if (recoveryInformationVersion == TransactionManager.RecoveryInformationVersion1)
173                 {
174                     nodeName = reader.ReadString();
175 
176                     resourceManagerRecoveryInformation = reader.ReadBytes(recoveryInformation.Length - checked((int)stream.Position));
177                 }
178                 else
179                 {
180                     if (etwLog.IsEnabled())
181                     {
182                         etwLog.TransactionExceptionTrace(TraceSourceType.TraceSourceBase, TransactionExceptionType.UnrecognizedRecoveryInformation, nameof(recoveryInformation), string.Empty);
183                     }
184 
185                     throw new ArgumentException(SR.UnrecognizedRecoveryInformation, nameof(recoveryInformation));
186                 }
187             }
188             catch (EndOfStreamException e)
189             {
190                 if (etwLog.IsEnabled())
191                 {
192                     etwLog.TransactionExceptionTrace(TraceSourceType.TraceSourceBase, TransactionExceptionType.UnrecognizedRecoveryInformation, nameof(recoveryInformation), e.ToString());
193                 }
194                 throw new ArgumentException(SR.UnrecognizedRecoveryInformation, nameof(recoveryInformation), e);
195             }
196             catch (FormatException e)
197             {
198                 if (etwLog.IsEnabled())
199                 {
200                     etwLog.TransactionExceptionTrace(TraceSourceType.TraceSourceBase, TransactionExceptionType.UnrecognizedRecoveryInformation, nameof(recoveryInformation), e.ToString());
201                 }
202                 throw new ArgumentException(SR.UnrecognizedRecoveryInformation, nameof(recoveryInformation), e);
203             }
204             finally
205             {
206                 stream.Dispose();
207             }
208 
209             DistributedTransactionManager transactionManager = CheckTransactionManager(nodeName);
210 
211             // Now ask the Transaction Manager to reenlist.
212             object syncRoot = new object();
213             Enlistment returnValue = new Enlistment(enlistmentNotification, syncRoot);
214             EnlistmentState.EnlistmentStatePromoted.EnterState(returnValue.InternalEnlistment);
215 
216             returnValue.InternalEnlistment.PromotedEnlistment =
217                 transactionManager.ReenlistTransaction(
218                     resourceManagerIdentifier,
219                     resourceManagerRecoveryInformation,
220                     (RecoveringInternalEnlistment)returnValue.InternalEnlistment
221                     );
222 
223             if (etwLog.IsEnabled())
224             {
225                 etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.Reenlist");
226             }
227 
228             return returnValue;
229         }
230 
231 
CheckTransactionManager(string nodeName)232         private static DistributedTransactionManager CheckTransactionManager(string nodeName)
233         {
234             DistributedTransactionManager tm = DistributedTransactionManager;
235             if (!((tm.NodeName == null && (nodeName == null || nodeName.Length == 0)) ||
236                   (tm.NodeName != null && tm.NodeName.Equals(nodeName))))
237             {
238                 throw new ArgumentException(SR.InvalidRecoveryInformation, "recoveryInformation");
239             }
240             return tm;
241         }
242 
RecoveryComplete(Guid resourceManagerIdentifier)243         public static void RecoveryComplete(Guid resourceManagerIdentifier)
244         {
245             if (resourceManagerIdentifier == Guid.Empty)
246             {
247                 throw new ArgumentException(SR.BadResourceManagerId, nameof(resourceManagerIdentifier));
248             }
249 
250             TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
251             if (etwLog.IsEnabled())
252             {
253                 etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.RecoveryComplete");
254                 etwLog.TransactionManagerRecoveryComplete(resourceManagerIdentifier);
255             }
256 
257             DistributedTransactionManager.ResourceManagerRecoveryComplete(resourceManagerIdentifier);
258 
259             if (etwLog.IsEnabled())
260             {
261                 etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.RecoveryComplete");
262             }
263         }
264 
265 
266         // Object for synchronizing access to the entire class( avoiding lock( typeof( ... )) )
267         private static object s_classSyncObject;
268 
269         // Helper object for static synchronization
270         private static object ClassSyncObject => LazyInitializer.EnsureInitialized(ref s_classSyncObject);
271 
272         internal static IsolationLevel DefaultIsolationLevel
273         {
274             get
275             {
276                 TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
277                 if (etwLog.IsEnabled())
278                 {
279                     etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultIsolationLevel");
280                     etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultIsolationLevel");
281                 }
282 
283                 return IsolationLevel.Serializable;
284             }
285         }
286 
287 
288         private static DefaultSettingsSection s_defaultSettings;
289         private static DefaultSettingsSection DefaultSettings
290         {
291             get
292             {
293                 if (s_defaultSettings == null)
294                 {
295                     s_defaultSettings = DefaultSettingsSection.GetSection();
296                 }
297 
298                 return s_defaultSettings;
299             }
300         }
301 
302 
303         private static MachineSettingsSection s_machineSettings;
304         private static MachineSettingsSection MachineSettings
305         {
306             get
307             {
308                 if (s_machineSettings == null)
309                 {
310                     s_machineSettings = MachineSettingsSection.GetSection();
311                 }
312 
313                 return s_machineSettings;
314             }
315         }
316 
317         private static bool s_defaultTimeoutValidated;
318         private static TimeSpan s_defaultTimeout;
319         public static TimeSpan DefaultTimeout
320         {
321             get
322             {
323                 TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
324                 if (etwLog.IsEnabled())
325                 {
326                     etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultTimeout");
327                 }
328 
329                 if (!s_defaultTimeoutValidated)
330                 {
331                     s_defaultTimeout = ValidateTimeout(DefaultSettings.Timeout);
332                     // If the timeout value got adjusted, it must have been greater than MaximumTimeout.
333                     if (s_defaultTimeout != DefaultSettings.Timeout)
334                     {
335                         if (etwLog.IsEnabled())
336                         {
337                             etwLog.ConfiguredDefaultTimeoutAdjusted();
338                         }
339                     }
340                     s_defaultTimeoutValidated = true;
341                 }
342 
343                 if (etwLog.IsEnabled())
344                 {
345                     etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultTimeout");
346                 }
347                 return s_defaultTimeout;
348             }
349         }
350 
351 
352         private static bool s_cachedMaxTimeout;
353         private static TimeSpan s_maximumTimeout;
354         public static TimeSpan MaximumTimeout
355         {
356             get
357             {
358                 TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
359                 if (etwLog.IsEnabled())
360                 {
361                     etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultMaximumTimeout");
362                 }
363 
364                 LazyInitializer.EnsureInitialized(ref s_maximumTimeout, ref s_cachedMaxTimeout, ref s_classSyncObject, () => MachineSettings.MaxTimeout);
365 
366                 if (etwLog.IsEnabled())
367                 {
368                     etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultMaximumTimeout");
369                 }
370 
371                 return s_maximumTimeout;
372             }
373         }
374 
375 
376         // This routine writes the "header" for the recovery information, based on the
377         // type of the calling object and its provided parameter collection.  This information
378         // we be read back by the static Reenlist method to create the necessary transaction
379         // manager object with the right parameters in order to do a ReenlistTransaction call.
GetRecoveryInformation(string startupInfo, byte[] resourceManagerRecoveryInformation)380         internal static byte[] GetRecoveryInformation(string startupInfo, byte[] resourceManagerRecoveryInformation)
381         {
382             TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
383             if (etwLog.IsEnabled())
384             {
385                 etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.GetRecoveryInformation");
386             }
387 
388             MemoryStream stream = new MemoryStream();
389             byte[] returnValue = null;
390 
391             try
392             {
393                 // Manually write the recovery information
394                 BinaryWriter writer = new BinaryWriter(stream);
395 
396                 writer.Write(TransactionManager.CurrentRecoveryVersion);
397                 if (startupInfo != null)
398                 {
399                     writer.Write(startupInfo);
400                 }
401                 else
402                 {
403                     writer.Write("");
404                 }
405                 writer.Write(resourceManagerRecoveryInformation);
406                 writer.Flush();
407                 returnValue = stream.ToArray();
408             }
409             finally
410             {
411                 stream.Dispose();
412             }
413 
414             if (etwLog.IsEnabled())
415             {
416                 etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.GetRecoveryInformation");
417             }
418 
419             return returnValue;
420         }
421 
ConvertToByteArray(object thingToConvert)422         internal static byte[] ConvertToByteArray(object thingToConvert)
423         {
424             MemoryStream streamToWrite = new MemoryStream();
425             byte[] returnValue = null;
426 
427             try
428             {
429                 // First seralize the type to the stream.
430                 IFormatter formatter = new BinaryFormatter();
431                 formatter.Serialize(streamToWrite, thingToConvert);
432 
433                 returnValue = new byte[streamToWrite.Length];
434 
435                 streamToWrite.Position = 0;
436                 streamToWrite.Read(returnValue, 0, Convert.ToInt32(streamToWrite.Length, CultureInfo.InvariantCulture));
437             }
438             finally
439             {
440                 streamToWrite.Dispose();
441             }
442 
443             return returnValue;
444         }
445 
446         /// <summary>
447         /// This static function throws an ArgumentOutOfRange if the specified IsolationLevel is not within
448         /// the range of valid values.
449         /// </summary>
450         /// <param name="transactionIsolationLevel">
451         /// The IsolationLevel value to validate.
452         /// </param>
ValidateIsolationLevel(IsolationLevel transactionIsolationLevel)453         internal static void ValidateIsolationLevel(IsolationLevel transactionIsolationLevel)
454         {
455             switch (transactionIsolationLevel)
456             {
457                 case IsolationLevel.Serializable:
458                 case IsolationLevel.RepeatableRead:
459                 case IsolationLevel.ReadCommitted:
460                 case IsolationLevel.ReadUncommitted:
461                 case IsolationLevel.Unspecified:
462                 case IsolationLevel.Chaos:
463                 case IsolationLevel.Snapshot:
464                     break;
465                 default:
466                     throw new ArgumentOutOfRangeException(nameof(transactionIsolationLevel));
467             }
468         }
469 
470 
471         /// <summary>
472         /// This static function throws an ArgumentOutOfRange if the specified TimeSpan does not meet
473         /// requirements of a valid transaction timeout.  Timeout values must be positive.
474         /// </summary>
475         /// <param name="transactionTimeout">
476         /// The TimeSpan value to validate.
477         /// </param>
ValidateTimeout(TimeSpan transactionTimeout)478         internal static TimeSpan ValidateTimeout(TimeSpan transactionTimeout)
479         {
480             if (transactionTimeout < TimeSpan.Zero)
481             {
482                 throw new ArgumentOutOfRangeException(nameof(transactionTimeout));
483             }
484 
485             if (MaximumTimeout != TimeSpan.Zero)
486             {
487                 if (transactionTimeout > MaximumTimeout || transactionTimeout == TimeSpan.Zero)
488                 {
489                     return MaximumTimeout;
490                 }
491             }
492 
493             return transactionTimeout;
494         }
495 
FindPromotedTransaction(Guid transactionIdentifier)496         internal static Transaction FindPromotedTransaction(Guid transactionIdentifier)
497         {
498             Hashtable promotedTransactionTable = PromotedTransactionTable;
499             WeakReference weakRef = (WeakReference)promotedTransactionTable[transactionIdentifier];
500             if (null != weakRef)
501             {
502                 Transaction tx = weakRef.Target as Transaction;
503                 if (null != tx)
504                 {
505                     return tx.InternalClone();
506                 }
507                 else  // an old, moldy weak reference.  Let's get rid of it.
508                 {
509                     lock (promotedTransactionTable)
510                     {
511                         promotedTransactionTable.Remove(transactionIdentifier);
512                     }
513                 }
514             }
515 
516             return null;
517         }
518 
FindOrCreatePromotedTransaction(Guid transactionIdentifier, DistributedTransaction dtx)519         internal static Transaction FindOrCreatePromotedTransaction(Guid transactionIdentifier, DistributedTransaction dtx)
520         {
521             Transaction tx = null;
522             Hashtable promotedTransactionTable = PromotedTransactionTable;
523             lock (promotedTransactionTable)
524             {
525                 WeakReference weakRef = (WeakReference)promotedTransactionTable[transactionIdentifier];
526                 if (null != weakRef)
527                 {
528                     tx = weakRef.Target as Transaction;
529                     if (null != tx)
530                     {
531                         // If we found a transaction then dispose it
532                         dtx.Dispose();
533                         return tx.InternalClone();
534                     }
535                     else
536                     {
537                         // an old, moldy weak reference.  Let's get rid of it.
538                         lock (promotedTransactionTable)
539                         {
540                             promotedTransactionTable.Remove(transactionIdentifier);
541                         }
542                     }
543                 }
544 
545                 tx = new Transaction(dtx);
546 
547                 // Since we are adding this reference to the table create an object that will clean that entry up.
548                 tx._internalTransaction._finalizedObject = new FinalizedObject(tx._internalTransaction, dtx.Identifier);
549 
550                 weakRef = new WeakReference(tx, false);
551                 promotedTransactionTable[dtx.Identifier] = weakRef;
552             }
553             dtx.SavedLtmPromotedTransaction = tx;
554 
555             FireDistributedTransactionStarted(tx);
556 
557             return tx;
558         }
559 
560         // Table for promoted transactions
561         internal static Hashtable PromotedTransactionTable =>
562             LazyInitializer.EnsureInitialized(ref s_promotedTransactionTable, ref s_classSyncObject, () => new Hashtable(100));
563 
564         // Table for transaction timeouts
565         internal static TransactionTable TransactionTable =>
566             LazyInitializer.EnsureInitialized(ref s_transactionTable, ref s_classSyncObject, () => new TransactionTable());
567 
568         // Fault in a DistributedTransactionManager if one has not already been created.
569         internal static DistributedTransactionManager distributedTransactionManager;
570         internal static DistributedTransactionManager DistributedTransactionManager =>
571             // If the distributed transaction manager is not configured, throw an exception
572             LazyInitializer.EnsureInitialized(ref distributedTransactionManager, ref s_classSyncObject, () => new DistributedTransactionManager());
573     }
574 }
575