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