1 //
2 // Transaction.cs
3 //
4 // Author:
5 //	Atsushi Enomoto  <atsushi@ximian.com>
6 //	Ankit Jain	 <JAnkit@novell.com>
7 //
8 // (C)2005 Novell Inc,
9 // (C)2006 Novell Inc,
10 //
11 
12 using System.Collections;
13 using System.Collections.Generic;
14 using System.Security.Permissions;
15 using System.Runtime.Serialization;
16 using System.Threading;
17 
18 namespace System.Transactions
19 {
20 	[Serializable]
21 	public class Transaction : IDisposable, ISerializable
22 	{
23 		[ThreadStatic]
24 		static Transaction ambient;
25 
26 		IsolationLevel level;
27 		TransactionInformation info;
28 
29 		ArrayList dependents = new ArrayList ();
30 
31 		/* Volatile enlistments */
32 		List <IEnlistmentNotification> volatiles;
33 
34 		/* Durable enlistments
35 		   Durable RMs can also have 2 Phase commit but
36 		   not in LTM, and that is what we are supporting
37 		   right now
38 		 */
39 		List <ISinglePhaseNotification> durables;
40 
41 		IPromotableSinglePhaseNotification pspe = null;
42 
AsyncCommit()43 		delegate void AsyncCommit ();
44 
45 		AsyncCommit asyncCommit = null;
46 		bool committing;
47 		bool committed = false;
48 		bool aborted = false;
49 		TransactionScope scope = null;
50 
51 		Exception innerException;
52 		Guid tag = Guid.NewGuid ();
53 
54 		internal List <IEnlistmentNotification> Volatiles {
55 			get {
56 				if (volatiles == null)
57 					volatiles = new List <IEnlistmentNotification> ();
58 				return volatiles;
59 			}
60 		}
61 
62 		internal List <ISinglePhaseNotification> Durables {
63 			get {
64 				if (durables == null)
65 					durables = new List <ISinglePhaseNotification> ();
66 				return durables;
67 			}
68 		}
69 
70 		internal IPromotableSinglePhaseNotification Pspe { get { return pspe; } }
71 
Transaction()72 		internal Transaction ()
73 		{
74 			info = new TransactionInformation ();
75 			level = IsolationLevel.Serializable;
76 		}
77 
Transaction(Transaction other)78 		internal Transaction (Transaction other)
79 		{
80 			level = other.level;
81 			info = other.info;
82 			dependents = other.dependents;
83 			volatiles = other.Volatiles;
84 			durables = other.Durables;
85 			pspe = other.Pspe;
86 		}
87 
88 		[MonoTODO]
ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)89 		void ISerializable.GetObjectData (SerializationInfo info,
90 			StreamingContext context)
91 		{
92 			throw new NotImplementedException ();
93 		}
94 
95 		public event TransactionCompletedEventHandler TransactionCompleted;
96 
97 		public static Transaction Current {
98 			get {
99 				EnsureIncompleteCurrentScope ();
100 				return CurrentInternal;
101 			}
102 			set {
103 				EnsureIncompleteCurrentScope ();
104 				CurrentInternal = value;
105 			}
106 		}
107 
108 		internal static Transaction CurrentInternal {
109 			get { return ambient; }
110 			set { ambient = value; }
111 		}
112 
113 		public IsolationLevel IsolationLevel {
114 			get {
115 				EnsureIncompleteCurrentScope ();
116 				return level;
117 			}
118 		}
119 
120 		public TransactionInformation TransactionInformation {
121 			get {
122 				EnsureIncompleteCurrentScope ();
123 				return info;
124 			}
125 		}
126 
Clone()127 		public Transaction Clone ()
128 		{
129 			return new Transaction (this);
130 		}
131 
Dispose()132 		public void Dispose ()
133 		{
134 			if (TransactionInformation.Status == TransactionStatus.Active)
135 				Rollback();
136 		}
137 
138 		[MonoTODO]
DependentClone( DependentCloneOption cloneOption)139 		public DependentTransaction DependentClone (
140 			DependentCloneOption cloneOption)
141 		{
142 			DependentTransaction d =
143 				new DependentTransaction (this, cloneOption);
144 			dependents.Add (d);
145 			return d;
146 		}
147 
148 		[MonoTODO ("Only SinglePhase commit supported for durable resource managers.")]
149 		[PermissionSetAttribute (SecurityAction.LinkDemand)]
EnlistDurable(Guid resourceManagerIdentifier, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions)150 		public Enlistment EnlistDurable (Guid resourceManagerIdentifier,
151 			IEnlistmentNotification enlistmentNotification,
152 			EnlistmentOptions enlistmentOptions)
153 		{
154 			throw new NotImplementedException ("DTC unsupported, only SinglePhase commit supported for durable resource managers.");
155 		}
156 
157 		[MonoTODO ("Only Local Transaction Manager supported. Cannot have more than 1 durable resource per transaction. Only EnlistmentOptions.None supported yet.")]
158 		[PermissionSetAttribute (SecurityAction.LinkDemand)]
EnlistDurable(Guid resourceManagerIdentifier, ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions)159 		public Enlistment EnlistDurable (Guid resourceManagerIdentifier,
160 			ISinglePhaseNotification singlePhaseNotification,
161 			EnlistmentOptions enlistmentOptions)
162 		{
163 			EnsureIncompleteCurrentScope ();
164 			if (pspe != null || Durables.Count > 0)
165 				throw new NotImplementedException ("DTC unsupported, multiple durable resource managers aren't supported.");
166 
167 			if (enlistmentOptions != EnlistmentOptions.None)
168 				throw new NotImplementedException ("EnlistmentOptions other than None aren't supported");
169 
170 			Durables.Add (singlePhaseNotification);
171 
172 			/* FIXME: Enlistment ?? */
173 			return new Enlistment ();
174 		}
175 
EnlistPromotableSinglePhase( IPromotableSinglePhaseNotification promotableSinglePhaseNotification)176 		public bool EnlistPromotableSinglePhase (
177 			IPromotableSinglePhaseNotification promotableSinglePhaseNotification)
178 		{
179 			EnsureIncompleteCurrentScope ();
180 
181 			// The specs aren't entirely clear on whether we can have volatile RMs along with a PSPE, but
182 			// I'm assuming that yes based on: http://social.msdn.microsoft.com/Forums/br/windowstransactionsprogramming/thread/3df6d4d3-0d82-47c4-951a-cd31140950b3
183 			if (pspe != null || Durables.Count > 0)
184 				return false;
185 
186 			pspe = promotableSinglePhaseNotification;
187 			pspe.Initialize();
188 
189 			return true;
190 		}
191 
SetDistributedTransactionIdentifier(IPromotableSinglePhaseNotification promotableNotification, Guid distributedTransactionIdentifier)192 		public void SetDistributedTransactionIdentifier (IPromotableSinglePhaseNotification promotableNotification, Guid distributedTransactionIdentifier)
193 		{
194 			throw new NotImplementedException ();
195 		}
196 
EnlistPromotableSinglePhase(IPromotableSinglePhaseNotification promotableSinglePhaseNotification, Guid promoterType)197  		public bool EnlistPromotableSinglePhase (IPromotableSinglePhaseNotification promotableSinglePhaseNotification, Guid promoterType)
198 		{
199 			throw new NotImplementedException ();
200 		}
201 
GetPromotedToken()202 		public byte[] GetPromotedToken ()
203 		{
204 			throw new NotImplementedException ();
205 		}
206 
207 		public Guid PromoterType
208 		{
209 			get { throw new NotImplementedException (); }
210 		}
211 
212 		[MonoTODO ("EnlistmentOptions being ignored")]
EnlistVolatile( IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions)213 		public Enlistment EnlistVolatile (
214 			IEnlistmentNotification enlistmentNotification,
215 			EnlistmentOptions enlistmentOptions)
216 		{
217 			return EnlistVolatileInternal (enlistmentNotification, enlistmentOptions);
218 		}
219 
220 		[MonoTODO ("EnlistmentOptions being ignored")]
EnlistVolatile( ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions)221 		public Enlistment EnlistVolatile (
222 			ISinglePhaseNotification singlePhaseNotification,
223 			EnlistmentOptions enlistmentOptions)
224 		{
225 			/* FIXME: Anything extra reqd for this? */
226 			return EnlistVolatileInternal (singlePhaseNotification, enlistmentOptions);
227 		}
228 
EnlistVolatileInternal( IEnlistmentNotification notification, EnlistmentOptions options)229 		private Enlistment EnlistVolatileInternal (
230 			IEnlistmentNotification notification,
231 			EnlistmentOptions options)
232 		{
233 			EnsureIncompleteCurrentScope ();
234 			/* FIXME: Handle options.EnlistDuringPrepareRequired */
235 			Volatiles.Add (notification);
236 
237 			/* FIXME: Enlistment.. ? */
238 			return new Enlistment ();
239 		}
240 
241 		[MonoTODO ("Only Local Transaction Manager supported. Cannot have more than 1 durable resource per transaction.")]
242 		[PermissionSetAttribute (SecurityAction.LinkDemand)]
PromoteAndEnlistDurable( Guid manager, IPromotableSinglePhaseNotification promotableNotification, ISinglePhaseNotification notification, EnlistmentOptions options)243 		public Enlistment PromoteAndEnlistDurable (
244 			Guid manager,
245 			IPromotableSinglePhaseNotification promotableNotification,
246 			ISinglePhaseNotification notification,
247 			EnlistmentOptions options)
248 		{
249 			throw new NotImplementedException ("DTC unsupported, multiple durable resource managers aren't supported.");
250 		}
251 
Equals(object obj)252 		public override bool Equals (object obj)
253 		{
254 			return Equals (obj as Transaction);
255 		}
256 
257 		// FIXME: Check whether this is correct (currently, GetHashCode() uses 'dependents' but this doesn't)
Equals(Transaction t)258 		private bool Equals (Transaction t)
259 		{
260 			if (ReferenceEquals (t, this))
261 				return true;
262 			if (ReferenceEquals (t, null))
263 				return false;
264 			return this.level == t.level &&
265 				this.info == t.info;
266 		}
267 
operator ==(Transaction x, Transaction y)268 		public static bool operator == (Transaction x, Transaction y)
269 		{
270 			if (ReferenceEquals (x, null))
271 				return ReferenceEquals (y, null);
272 			return x.Equals (y);
273 		}
274 
operator !=(Transaction x, Transaction y)275 		public static bool operator != (Transaction x, Transaction y)
276 		{
277 			return !(x == y);
278 		}
279 
GetHashCode()280 		public override int GetHashCode ()
281 		{
282 			return (int) level ^ info.GetHashCode () ^ dependents.GetHashCode ();
283 		}
284 
Rollback()285 		public void Rollback ()
286 		{
287 			Rollback (null);
288 		}
289 
Rollback(Exception e)290 		public void Rollback (Exception e)
291 		{
292 			EnsureIncompleteCurrentScope ();
293 			Rollback (e, null);
294 		}
295 
Rollback(Exception ex, object abortingEnlisted)296 		internal void Rollback (Exception ex, object abortingEnlisted)
297 		{
298 			if (aborted)
299 			{
300 				FireCompleted ();
301 				return;
302 			}
303 
304 			/* See test ExplicitTransaction7 */
305 			if (info.Status == TransactionStatus.Committed)
306 				throw new TransactionException ("Transaction has already been committed. Cannot accept any new work.");
307 
308 			// Save thrown exception as 'reason' of transaction's abort.
309 			innerException = ex;
310 
311 			SinglePhaseEnlistment e = new SinglePhaseEnlistment();
312 			foreach (IEnlistmentNotification prep in Volatiles)
313 				if (prep != abortingEnlisted)
314 					prep.Rollback (e);
315 
316 			var durables = Durables;
317 			if (durables.Count > 0 && durables [0] != abortingEnlisted)
318 				durables [0].Rollback (e);
319 
320 			if (pspe != null && pspe != abortingEnlisted)
321 				pspe.Rollback (e);
322 
323 			Aborted = true;
324 
325 			FireCompleted ();
326 		}
327 
328 		bool Aborted {
329 			get { return aborted; }
330 			set {
331 				aborted = value;
332 				if (aborted)
333 					info.Status = TransactionStatus.Aborted;
334 			}
335 		}
336 
337 		internal TransactionScope Scope {
338 			get { return scope; }
339 			set { scope = value; }
340 		}
341 
BeginCommitInternal(AsyncCallback callback)342 		protected IAsyncResult BeginCommitInternal (AsyncCallback callback)
343 		{
344 			if (committed || committing)
345 				throw new InvalidOperationException ("Commit has already been called for this transaction.");
346 
347 			this.committing = true;
348 
349 			asyncCommit = new AsyncCommit (DoCommit);
350 			return asyncCommit.BeginInvoke (callback, null);
351 		}
352 
EndCommitInternal(IAsyncResult ar)353 		protected void EndCommitInternal (IAsyncResult ar)
354 		{
355 			asyncCommit.EndInvoke (ar);
356 		}
357 
CommitInternal()358 		internal void CommitInternal ()
359 		{
360 			if (committed || committing)
361 				throw new InvalidOperationException ("Commit has already been called for this transaction.");
362 
363 			this.committing = true;
364 
365 			try {
366 				DoCommit ();
367 			}
368 			catch (TransactionException)
369 			{
370 				throw;
371 			}
372 			catch (Exception ex)
373 			{
374 				throw new TransactionAbortedException("Transaction failed", ex);
375 			}
376 		}
377 
DoCommit()378 		private void DoCommit ()
379 		{
380 			/* Scope becomes null in TransactionScope.Dispose */
381 			if (Scope != null) {
382 				/* See test ExplicitTransaction8 */
383 				Rollback (null, null);
384 				CheckAborted ();
385 			}
386 
387 			var volatiles = Volatiles;
388 			var durables = Durables;
389 			if (volatiles.Count == 1 && durables.Count == 0)
390 			{
391 				/* Special case */
392 				ISinglePhaseNotification single = volatiles[0] as ISinglePhaseNotification;
393 				if (single != null)
394 				{
395 					DoSingleCommit(single);
396 					Complete();
397 					return;
398 				}
399 			}
400 
401 			if (volatiles.Count > 0)
402 				DoPreparePhase();
403 
404 			if (durables.Count > 0)
405 				DoSingleCommit(durables[0]);
406 
407 			if (pspe != null)
408 				DoSingleCommit(pspe);
409 
410 			if (volatiles.Count > 0)
411 				DoCommitPhase();
412 
413 			Complete();
414 		}
415 
Complete()416 		private void Complete ()
417 		{
418 			committing = false;
419 			committed = true;
420 
421 			if (!aborted)
422 				info.Status = TransactionStatus.Committed;
423 
424 			FireCompleted ();
425 		}
426 
InitScope(TransactionScope scope)427 		internal void InitScope (TransactionScope scope)
428 		{
429 			/* See test NestedTransactionScope10 */
430 			CheckAborted ();
431 
432 			/* See test ExplicitTransaction6a */
433 			if (committed)
434 				throw new InvalidOperationException ("Commit has already been called on this transaction.");
435 
436 			Scope = scope;
437 		}
438 
PrepareCallbackWrapper(object state)439 		static void PrepareCallbackWrapper(object state)
440 		{
441 			PreparingEnlistment enlist = state as PreparingEnlistment;
442 
443 			try
444 			{
445 				enlist.EnlistmentNotification.Prepare(enlist);
446 			}
447 			catch (Exception ex)
448 			{
449 				// Oops! Unhandled exception.. we should notify
450 				// to our caller thread that preparing has failed.
451 				// This usually happends when an exception is
452 				// thrown by code enlistment.Rollback() methods
453 				// executed inside prepare.ForceRollback(ex).
454 				enlist.Exception = ex;
455 
456 				// Just in case enlistment did not call Prepared()
457 				// we need to manually set WH to avoid transaction
458 				// from failing due to transaction timeout.
459 				if (!enlist.IsPrepared)
460 					((ManualResetEvent)enlist.WaitHandle).Set();
461 			}
462 		}
463 
DoPreparePhase()464 		void DoPreparePhase ()
465 		{
466 			// Call prepare on all volatile managers.
467 			foreach (IEnlistmentNotification enlist in Volatiles)
468 			{
469 				PreparingEnlistment pe = new PreparingEnlistment (this, enlist);
470 				ThreadPool.QueueUserWorkItem (new WaitCallback(PrepareCallbackWrapper), pe);
471 
472 				/* Wait (with timeout) for manager to prepare */
473 				TimeSpan timeout = Scope != null ? Scope.Timeout : TransactionManager.DefaultTimeout;
474 
475 				// FIXME: Should we managers in parallel or on-by-one?
476 				if (!pe.WaitHandle.WaitOne(timeout, true))
477 				{
478 					this.Aborted = true;
479 					throw new TimeoutException("Transaction timedout");
480 				}
481 
482 				if (pe.Exception != null)
483 				{
484 					innerException = pe.Exception;
485 					Aborted = true;
486 					break;
487 				}
488 
489 				if (!pe.IsPrepared)
490 				{
491 					/* FIXME: if not prepared & !aborted as yet, then
492 						this is inDoubt ? . For now, setting aborted = true */
493 					Aborted = true;
494 					break;
495 				}
496 			}
497 
498 			/* Either InDoubt(tmp) or Prepare failed and
499 			   Tx has rolledback */
500 			CheckAborted ();
501 		}
502 
DoCommitPhase()503 		void DoCommitPhase ()
504 		{
505 			foreach (IEnlistmentNotification enlisted in Volatiles) {
506 				Enlistment e = new Enlistment ();
507 				enlisted.Commit (e);
508 				/* Note: e.Done doesn't matter for volatile RMs */
509 			}
510 		}
511 
DoSingleCommit(ISinglePhaseNotification single)512 		void DoSingleCommit (ISinglePhaseNotification single)
513 		{
514 			if (single == null)
515 				return;
516 
517 			single.SinglePhaseCommit (new SinglePhaseEnlistment (this, single));
518 			CheckAborted ();
519 		}
520 
DoSingleCommit(IPromotableSinglePhaseNotification single)521 		void DoSingleCommit (IPromotableSinglePhaseNotification single)
522 		{
523 			if (single == null)
524 				return;
525 
526 			single.SinglePhaseCommit (new SinglePhaseEnlistment (this, single));
527 			CheckAborted ();
528 		}
529 
CheckAborted()530 		void CheckAborted ()
531 		{
532 			if (aborted)
533 				throw new TransactionAbortedException ("Transaction has aborted", innerException);
534 		}
535 
FireCompleted()536 		void FireCompleted ()
537 		{
538 			if (TransactionCompleted != null)
539 				TransactionCompleted (this, new TransactionEventArgs(this));
540 		}
541 
EnsureIncompleteCurrentScope()542 		static void EnsureIncompleteCurrentScope ()
543 		{
544 			if (CurrentInternal == null)
545 				return;
546 			if (CurrentInternal.Scope != null && CurrentInternal.Scope.IsComplete)
547 				throw new InvalidOperationException ("The current TransactionScope is already complete");
548 		}
549   }
550 }
551 
552