1 //-----------------------------------------------------------------------------
2 // Copyright (c) Microsoft Corporation.  All rights reserved.
3 //-----------------------------------------------------------------------------
4 
5 namespace System.Runtime
6 {
7     using System;
8     using System.Diagnostics;
9     using System.Diagnostics.CodeAnalysis;
10     using System.Threading;
11     using System.Transactions;
12 
13     // AsyncResult starts acquired; Complete releases.
14     [Fx.Tag.SynchronizationPrimitive(Fx.Tag.BlocksUsing.ManualResetEvent, SupportsAsync = true, ReleaseMethod = "Complete")]
15     abstract class TransactedAsyncResult : AsyncResult
16     {
17         IAsyncResult deferredTransactionalResult;
18         TransactionSignalScope transactionContext;
19 
TransactedAsyncResult(AsyncCallback callback, object state)20         protected TransactedAsyncResult(AsyncCallback callback, object state)
21             : base(callback, state)
22         {
23             SetBeforePrepareAsyncCompletionAction(BeforePrepareAsyncCompletion);
24             SetCheckSyncValidationFunc(CheckSyncValidation);
25         }
26 
OnContinueAsyncCompletion(IAsyncResult result)27         protected override bool OnContinueAsyncCompletion(IAsyncResult result)
28         {
29             if (this.transactionContext != null && !this.transactionContext.Signal(result))
30             {
31                 // The TransactionScope isn't cleaned up yet and can't be done on this thread.  Must defer
32                 // the callback (which is likely to attempt to commit the transaction) until later.
33                 return false;
34             }
35 
36             this.transactionContext = null;
37             return true;
38         }
39 
CheckSyncValidation(IAsyncResult result)40         bool CheckSyncValidation(IAsyncResult result)
41         {
42             if (result.CompletedSynchronously)
43             {
44                 // Once we pass the check, we know that we own forward progress, so transactionContext is correct. Verify its state.
45                 if (this.transactionContext != null)
46                 {
47                     if (this.transactionContext.State != TransactionSignalState.Completed)
48                     {
49                         ThrowInvalidAsyncResult("Check/SyncContinue cannot be called from within the PrepareTransactionalCall using block.");
50                     }
51                     else if (this.transactionContext.IsSignalled)
52                     {
53                         // This is most likely to happen when result.CompletedSynchronously registers differently here and in the callback, which
54                         // is the fault of 'result'.
55                         ThrowInvalidAsyncResult(result);
56                     }
57                 }
58             }
59             else if (object.ReferenceEquals(result, this.deferredTransactionalResult))
60             {
61                 // The transactionContext may not be current if forward progress has been made via the callback. Instead,
62                 // use deferredTransactionalResult to see if we are supposed to execute a post-transaction callback.
63                 //
64                 // Once we pass the check, we know that we own forward progress, so transactionContext is correct. Verify its state.
65                 if (this.transactionContext == null || !this.transactionContext.IsSignalled)
66                 {
67                     ThrowInvalidAsyncResult(result);
68                 }
69                 this.deferredTransactionalResult = null;
70             }
71             else
72             {
73                 return false;
74             }
75 
76             this.transactionContext = null;
77             return true;
78         }
79 
BeforePrepareAsyncCompletion()80         void BeforePrepareAsyncCompletion()
81         {
82             if (this.transactionContext != null)
83             {
84                 // It might be an old, leftover one, if an exception was thrown within the last using (PrepareTransactionalCall()) block.
85                 if (this.transactionContext.IsPotentiallyAbandoned)
86                 {
87                     this.transactionContext = null;
88                 }
89                 else
90                 {
91                     this.transactionContext.Prepared();
92                 }
93             }
94         }
95 
PrepareTransactionalCall(Transaction transaction)96         protected IDisposable PrepareTransactionalCall(Transaction transaction)
97         {
98             if (this.transactionContext != null && !this.transactionContext.IsPotentiallyAbandoned)
99             {
100                 ThrowInvalidAsyncResult("PrepareTransactionalCall should only be called as the object of non-nested using statements. If the Begin succeeds, Check/SyncContinue must be called before another PrepareTransactionalCall.");
101             }
102 
103             return this.transactionContext = transaction == null ? null : new TransactionSignalScope(this, transaction);
104         }
105 
106         enum TransactionSignalState
107         {
108             Ready = 0,
109             Prepared,
110             Completed,
111             Abandoned,
112         }
113 
114         class TransactionSignalScope : SignalGate<IAsyncResult>, IDisposable
115         {
116             TransactionScope transactionScope;
117             TransactedAsyncResult parent;
118 
TransactionSignalScope(TransactedAsyncResult result, Transaction transaction)119             public TransactionSignalScope(TransactedAsyncResult result, Transaction transaction)
120             {
121                 Fx.Assert(transaction != null, "Null Transaction provided to AsyncResult.TransactionSignalScope.");
122                 this.parent = result;
123                 this.transactionScope = TransactionHelper.CreateTransactionScope(transaction);
124             }
125 
126             public TransactionSignalState State { get; private set; }
127 
128             public bool IsPotentiallyAbandoned
129             {
130                 get
131                 {
132                     return State == TransactionSignalState.Abandoned || (State == TransactionSignalState.Completed && !IsSignalled);
133                 }
134             }
135 
Prepared()136             public void Prepared()
137             {
138                 if (State != TransactionSignalState.Ready)
139                 {
140                     AsyncResult.ThrowInvalidAsyncResult("PrepareAsyncCompletion should only be called once per PrepareTransactionalCall.");
141                 }
142                 State = TransactionSignalState.Prepared;
143             }
144 
IDisposable.Dispose()145             void IDisposable.Dispose()
146             {
147                 if (State == TransactionSignalState.Ready)
148                 {
149                     State = TransactionSignalState.Abandoned;
150                 }
151                 else if (State == TransactionSignalState.Prepared)
152                 {
153                     State = TransactionSignalState.Completed;
154                 }
155                 else
156                 {
157                     AsyncResult.ThrowInvalidAsyncResult("PrepareTransactionalCall should only be called in a using. Dispose called multiple times.");
158                 }
159 
160                 try
161                 {
162                     TransactionHelper.CompleteTransactionScope(ref this.transactionScope);
163                 }
164                 catch (Exception exception)
165                 {
166                     if (Fx.IsFatal(exception))
167                     {
168                         throw;
169                     }
170 
171                     // Complete and Dispose are not expected to throw.  If they do it can mess up the AsyncResult state machine.
172                     throw Fx.Exception.AsError(new InvalidOperationException(SRCore.AsyncTransactionException));
173                 }
174 
175                 // This will release the callback to run, or tell us that we need to defer the callback to Check/SyncContinue.
176                 //
177                 // It's possible to avoid this Interlocked when CompletedSynchronously is true, but we have no way of knowing that
178                 // from here, and adding a way would add complexity to the AsyncResult transactional calling pattern. This
179                 // unnecessary Interlocked only happens when: PrepareTransactionalCall is called with a non-null transaction,
180                 // PrepareAsyncCompletion is reached, and the operation completes synchronously or with an exception.
181                 IAsyncResult result;
182                 if (State == TransactionSignalState.Completed && Unlock(out result))
183                 {
184                     if (this.parent.deferredTransactionalResult != null)
185                     {
186                         AsyncResult.ThrowInvalidAsyncResult(this.parent.deferredTransactionalResult);
187                     }
188                     this.parent.deferredTransactionalResult = result;
189                 }
190             }
191         }
192     }
193 }
194