File: SqlDelegatedTransaction.cs

package info (click to toggle)
mono 6.12.0.199%2Bdfsg-6
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 1,296,836 kB
  • sloc: cs: 11,181,803; xml: 2,850,076; ansic: 699,709; cpp: 123,344; perl: 59,361; javascript: 30,841; asm: 21,853; makefile: 20,405; sh: 15,009; python: 4,839; pascal: 925; sql: 859; sed: 16; php: 1
file content (480 lines) | stat: -rw-r--r-- 23,359 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
//------------------------------------------------------------------------------
// <copyright file="SqlDelegatedTransaction.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <owner current="true" primary="true">Microsoft</owner>
// <owner current="true" primary="false">Microsoft</owner>
//------------------------------------------------------------------------------

namespace System.Data.SqlClient {

    using System.Data.Common;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.ConstrainedExecution;
    using System.Threading;
    using SysTx = System.Transactions;

    sealed internal class SqlDelegatedTransaction : SysTx.IPromotableSinglePhaseNotification {
        private static int _objectTypeCount;
        private readonly int _objectID = Interlocked.Increment(ref _objectTypeCount);
        private const int _globalTransactionsTokenVersionSizeInBytes = 4; // the size of the version in the PromotedDTCToken for Global Transactions
        internal int ObjectID {
            get {
                return _objectID;
            }
        }

        // WARNING!!! Multithreaded object!
        // Locking strategy: Any potentailly-multithreaded operation must first lock the associated connection, then
        //  validate this object's active state.  Locked activities should ONLY include Sql-transaction state altering activities
        //  or notifications of same. Updates to the connection's association with the transaction or to the connection pool
        //  may be initiated here AFTER the connection lock is released, but should NOT fall under this class's locking strategy.

        private SqlInternalConnection   _connection;            // the internal connection that is the root of the transaction
        private IsolationLevel          _isolationLevel;        // the IsolationLevel of the transaction we delegated to the server
        private SqlInternalTransaction  _internalTransaction;   // the SQL Server transaction we're delegating to

        private SysTx.Transaction       _atomicTransaction;

        private bool                    _active;                // Is the transaction active?

        internal SqlDelegatedTransaction(SqlInternalConnection connection, SysTx.Transaction tx) {
            Debug.Assert(null != connection, "null connection?");
            _connection = connection;
            _atomicTransaction = tx;
            _active = false;
            SysTx.IsolationLevel systxIsolationLevel = tx.IsolationLevel;

            // We need to map the System.Transactions IsolationLevel to the one
            // that System.Data uses and communicates to SqlServer.  We could
            // arguably do that in Initialize when the transaction is delegated,
            // however it is better to do this before we actually begin the process
            // of delegation, in case System.Transactions adds another isolation
            // level we don't know about -- we can throw the exception at a better
            // place.
            switch (systxIsolationLevel) {
                case SysTx.IsolationLevel.ReadCommitted:    _isolationLevel = IsolationLevel.ReadCommitted;     break;
                case SysTx.IsolationLevel.ReadUncommitted:  _isolationLevel = IsolationLevel.ReadUncommitted;   break;
                case SysTx.IsolationLevel.RepeatableRead:   _isolationLevel = IsolationLevel.RepeatableRead;    break;
                case SysTx.IsolationLevel.Serializable:     _isolationLevel = IsolationLevel.Serializable;      break;
                case SysTx.IsolationLevel.Snapshot:         _isolationLevel = IsolationLevel.Snapshot;          break;
                default:
                    throw SQL.UnknownSysTxIsolationLevel(systxIsolationLevel);
            }
        }

        internal SysTx.Transaction Transaction
        {
            get { return _atomicTransaction; }
        }

        public void Initialize() {
            // if we get here, then we know for certain that we're the delegated
            // transaction.
            SqlInternalConnection connection = _connection;
            SqlConnection usersConnection = connection.Connection;

            Bid.Trace("<sc.SqlDelegatedTransaction.Initialize|RES|CPOOL> %d#, Connection %d#, delegating transaction.\n", ObjectID, connection.ObjectID);
            
            RuntimeHelpers.PrepareConstrainedRegions();
            try {
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();

                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    if (connection.IsEnlistedInTransaction) { // defect first
                        Bid.Trace("<sc.SqlDelegatedTransaction.Initialize|RES|CPOOL> %d#, Connection %d#, was enlisted, now defecting.\n", ObjectID, connection.ObjectID);
                        connection.EnlistNull();
                    }

                    _internalTransaction = new SqlInternalTransaction(connection, TransactionType.Delegated, null);

                    connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Begin, null, _isolationLevel, _internalTransaction, true);

                    // Handle case where ExecuteTran didn't produce a new transaction, but also didn't throw.
                    if (null == connection.CurrentTransaction)
                    {
                        connection.DoomThisConnection();
                        throw ADP.InternalError(ADP.InternalErrorCode.UnknownTransactionFailure);
                    }

                    _active = true;
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.StackOverflowException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.Threading.ThreadAbortException e)  {
                usersConnection.Abort(e);
                throw;
            }
        }

        internal bool IsActive {
            get {
                return _active;
            }
        }

        public Byte [] Promote() {
            // Operations that might be affected by multi-threaded use MUST be done inside the lock.
            //  Don't read values off of the connection outside the lock unless it doesn't really matter
            //  from an operational standpoint (i.e. logging connection's ObjectID should be fine,
            //  but the PromotedDTCToken can change over calls. so that must be protected).
            SqlInternalConnection connection = GetValidConnection();

            Exception promoteException;
            byte[] returnValue = null;
            SqlConnection usersConnection = connection.Connection;

            Bid.Trace("<sc.SqlDelegatedTransaction.Promote|RES|CPOOL> %d#, Connection %d#, promoting transaction.\n", ObjectID, connection.ObjectID);

            RuntimeHelpers.PrepareConstrainedRegions();
            try {
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();

                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    lock (connection) {
                        try {
                            // Now that we've acquired the lock, make sure we still have valid state for this operation.
                            ValidateActiveOnConnection(connection);

                            connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Promote, null, IsolationLevel.Unspecified, _internalTransaction, true);
                            returnValue = _connection.PromotedDTCToken;

                            // For Global Transactions, we need to set the Transaction Id since we use a Non-MSDTC Promoter type.
                            if(_connection.IsGlobalTransaction) {
                                if (SysTxForGlobalTransactions.SetDistributedTransactionIdentifier == null) {
                                    throw SQL.UnsupportedSysTxForGlobalTransactions();
                                }

                                if(!_connection.IsGlobalTransactionsEnabledForServer) {
                                    throw SQL.GlobalTransactionsNotEnabled();
                                }

                                SysTxForGlobalTransactions.SetDistributedTransactionIdentifier.Invoke(_atomicTransaction, new object[] { this, GetGlobalTxnIdentifierFromToken() });
                            }

                            promoteException = null;
                        }
                        catch (SqlException e) {
                            promoteException = e;

                            ADP.TraceExceptionWithoutRethrow(e);

                            // Doom the connection, to make sure that the transaction is
                            // eventually rolled back.
                            // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event
                            connection.DoomThisConnection();
                        }
                        catch (InvalidOperationException e)
                        {
                            promoteException = e;
                            ADP.TraceExceptionWithoutRethrow(e);
                            connection.DoomThisConnection();
                        }
                    }
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.StackOverflowException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.Threading.ThreadAbortException e)  {
                usersConnection.Abort(e);
                throw;
            }

            if (promoteException != null) {
                throw SQL.PromotionFailed(promoteException);
            }

            return returnValue;
        }

        // Called by transaction to initiate abort sequence
        public void Rollback(SysTx.SinglePhaseEnlistment enlistment) {
            Debug.Assert(null != enlistment, "null enlistment?");

            SqlInternalConnection connection = GetValidConnection();
            SqlConnection usersConnection = connection.Connection;

            Bid.Trace("<sc.SqlDelegatedTransaction.Rollback|RES|CPOOL> %d#, Connection %d#, aborting transaction.\n", ObjectID, connection.ObjectID);

            RuntimeHelpers.PrepareConstrainedRegions();
            try {
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();

                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    lock (connection) {
                        try {
                            // Now that we've acquired the lock, make sure we still have valid state for this operation.
                            ValidateActiveOnConnection(connection);
                            _active = false; // set to inactive first, doesn't matter how the execute completes, this transaction is done.
                            _connection = null;  // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event

                            // If we haven't already rolled back (or aborted) then tell the SQL Server to roll back
                            if (!_internalTransaction.IsAborted) {
                                connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Rollback, null, IsolationLevel.Unspecified, _internalTransaction, true);
                            }
                        }
                        catch (SqlException e) {
                            ADP.TraceExceptionWithoutRethrow(e);

                            // Doom the connection, to make sure that the transaction is
                            // eventually rolled back.
                            // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event
                            connection.DoomThisConnection();

                            // Unlike SinglePhaseCommit, a rollback is a rollback, regardless 
                            // of how it happens, so SysTx won't throw an exception, and we
                            // don't want to throw an exception either, because SysTx isn't 
                            // handling it and it may create a fail fast scenario. In the end,
                            // there is no way for us to communicate to the consumer that this
                            // failed for more serious reasons than usual.
                            // 
                            // This is a bit like "should you throw if Close fails", however,
                            // it only matters when you really need to know.  In that case, 
                            // we have the tracing that we're doing to fallback on for the
                            // investigation.
                        }
                        catch (InvalidOperationException e) {
                            ADP.TraceExceptionWithoutRethrow(e);
                            connection.DoomThisConnection();
                        }
                    }

                    // it doesn't matter whether the rollback succeeded or not, we presume
                    // that the transaction is aborted, because it will be eventually.
                    connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
                    enlistment.Aborted();
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.StackOverflowException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.Threading.ThreadAbortException e)  {
                usersConnection.Abort(e);
                throw;
            }
        }

        // Called by the transaction to initiate commit sequence
        public void SinglePhaseCommit(SysTx.SinglePhaseEnlistment enlistment) {
            Debug.Assert(null != enlistment, "null enlistment?");

            SqlInternalConnection connection = GetValidConnection();
            SqlConnection usersConnection = connection.Connection;

            Bid.Trace("<sc.SqlDelegatedTransaction.SinglePhaseCommit|RES|CPOOL> %d#, Connection %d#, committing transaction.\n", ObjectID, connection.ObjectID);

            RuntimeHelpers.PrepareConstrainedRegions();
            try {
#if DEBUG
                TdsParser.ReliabilitySection tdsReliabilitySection = new TdsParser.ReliabilitySection();

                RuntimeHelpers.PrepareConstrainedRegions();
                try {
                    tdsReliabilitySection.Start();
#else
                {
#endif //DEBUG
                    // If the connection is dooomed, we can be certain that the
                    // transaction will eventually be rolled back, and we shouldn't
                    // attempt to commit it.
                    if (connection.IsConnectionDoomed) {
                        lock (connection) {
                            _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
                            _connection = null;
                        }

                        enlistment.Aborted(SQL.ConnectionDoomed());
                    }
                    else {
                        Exception commitException;
                        lock (connection) {
                            try {
                                // Now that we've acquired the lock, make sure we still have valid state for this operation.
                                ValidateActiveOnConnection(connection);

                                _active = false; // set to inactive first, doesn't matter how the rest completes, this transaction is done.
                                _connection = null;   // Set prior to ExecuteTransaction call in case this initiates a TransactionEnd event

                                connection.ExecuteTransaction(SqlInternalConnection.TransactionRequest.Commit, null, IsolationLevel.Unspecified, _internalTransaction, true);
                                commitException = null;
                            }
                            catch (SqlException e) {
                                commitException = e;

                                ADP.TraceExceptionWithoutRethrow(e);

                                // Doom the connection, to make sure that the transaction is
                                // eventually rolled back.
                                // VSTS 144562: doom the connection while having the lock on it to prevent race condition with "Transaction Ended" Event
                                connection.DoomThisConnection();
                            }
                            catch (InvalidOperationException e) {
                                commitException = e;
                                ADP.TraceExceptionWithoutRethrow(e);
                                connection.DoomThisConnection();
                            }
                        }
                        if (commitException != null) {
                            // connection.ExecuteTransaction failed with exception
                            if (_internalTransaction.IsCommitted) {
                                // Even though we got an exception, the transaction
                                // was committed by the server.
                                enlistment.Committed();
                            }
                            else if (_internalTransaction.IsAborted) {
                                // The transaction was aborted, report that to
                                // SysTx.
                                enlistment.Aborted(commitException);
                            }
                            else {
                                // The transaction is still active, we cannot
                                // know the state of the transaction.
                                enlistment.InDoubt(commitException);
                            }

                            // We eat the exception.  This is called on the SysTx
                            // thread, not the applications thread.  If we don't 
                            // eat the exception an UnhandledException will occur,
                            // causing the process to FailFast.
                        }

                        connection.CleanupConnectionOnTransactionCompletion(_atomicTransaction);
                        if (commitException == null) {
                            // connection.ExecuteTransaction succeeded
                            enlistment.Committed();
                        }
                    }
                }
#if DEBUG
                finally {
                    tdsReliabilitySection.Stop();
                }
#endif //DEBUG
            }
            catch (System.OutOfMemoryException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.StackOverflowException e) {
                usersConnection.Abort(e);
                throw;
            }
            catch (System.Threading.ThreadAbortException e)  {
                usersConnection.Abort(e);
                throw;
            }
        }

        // Event notification that transaction ended. This comes from the subscription to the Transaction's
        //  ended event via the internal connection. If it occurs without a prior Rollback or SinglePhaseCommit call,
        //  it indicates the transaction was ended externally (generally that one the the DTC participants aborted
        //  the transaction).
        internal void TransactionEnded(SysTx.Transaction transaction) {
            SqlInternalConnection connection = _connection;

            if (connection != null) {
                Bid.Trace("<sc.SqlDelegatedTransaction.TransactionEnded|RES|CPOOL> %d#, Connection %d#, transaction completed externally.\n", ObjectID, connection.ObjectID);

                lock (connection) {
                    if (_atomicTransaction.Equals(transaction)) {
                        // No need to validate active on connection, this operation can be called on completed transactions
                        _active = false;
                        _connection = null;
                    }
                }
            }
        }

        // Check for connection validity
        private SqlInternalConnection GetValidConnection() {
            SqlInternalConnection connection = _connection;
            if (null == connection) {
                throw ADP.ObjectDisposed(this);
            }

            return connection;
        }

        // Dooms connection and throws and error if not a valid, active, delegated transaction for the given
        //  connection. Designed to be called AFTER a lock is placed on the connection, otherwise a normal return
        //  may not be trusted.
        private void ValidateActiveOnConnection(SqlInternalConnection connection) {
            bool valid = _active && (connection == _connection) && (connection.DelegatedTransaction == this);

            if (!valid) {
                // Invalid indicates something BAAAD happened (Commit after TransactionEnded, for instance)
                //  Doom anything remotely involved.
                if (null != connection) {
                    connection.DoomThisConnection();
                }
                if (connection != _connection && null != _connection) {
                    _connection.DoomThisConnection();
                }

                throw ADP.InternalError(ADP.InternalErrorCode.UnpooledObjectHasWrongOwner);  //
            }
        }

        // Get the server-side Global Transaction Id from the PromotedDTCToken
        // Skip first 4 bytes since they contain the version
        private Guid GetGlobalTxnIdentifierFromToken() {
            byte[] txnGuid = new byte[16];
            Array.Copy(_connection.PromotedDTCToken, _globalTransactionsTokenVersionSizeInBytes /* Skip the version */, txnGuid, 0, txnGuid.Length);
            return new Guid(txnGuid);
        }
    }
}