Index: Doc/Extra/Provider/version.html
==================================================================
--- Doc/Extra/Provider/version.html
+++ Doc/Extra/Provider/version.html
@@ -45,10 +45,12 @@
Version History
1.0.107.0 - January XX, 2018 (release scheduled)
- Updated to SQLite 3.22.0.
- Improve performance of type name lookups by removing superfluous locking and string creation.
+ - Support asynchronous completion of distributed transactions. Fix for [5cee5409f8].
+ - Add experimental WaitForEnlistmentReset method to the SQLiteConnection class. Pursuant to [7e1dd697dc].
- Fix some internal memory accounting present only in the debug build.
- Make sure inbound native delegates are unhooked before adding a connection to the pool. Fix for [0e48e80333].
- Add preliminary support for the .NET Framework 4.7.1.
- Updates to internal DbType mapping related lookup tables. Pursuant to [a799e3978f].
Index: Setup/data/verify.lst
==================================================================
--- Setup/data/verify.lst
+++ Setup/data/verify.lst
@@ -863,10 +863,11 @@
Tests/tkt-544dba0a2f.eagle
Tests/tkt-5535448538.eagle
Tests/tkt-56b42d99c1.eagle
Tests/tkt-58ed318f2f.eagle
Tests/tkt-59edc1018b.eagle
+ Tests/tkt-5cee5409f8.eagle
Tests/tkt-6434e23a0f.eagle
Tests/tkt-647d282d11.eagle
Tests/tkt-69cf6e5dc8.eagle
Tests/tkt-6c6ecccc5f.eagle
Tests/tkt-71bedaca19.eagle
Index: System.Data.SQLite/SQLiteConnection.cs
==================================================================
--- System.Data.SQLite/SQLiteConnection.cs
+++ System.Data.SQLite/SQLiteConnection.cs
@@ -16,10 +16,11 @@
using System.ComponentModel;
using System.Reflection;
using System.Runtime.InteropServices;
using System.IO;
using System.Text;
+ using System.Threading;
/////////////////////////////////////////////////////////////////////////////////////////////////
///
/// This class represents a single value to be returned
@@ -1478,10 +1479,16 @@
/// The default isolation level for new transactions
///
private IsolationLevel _defaultIsolation;
#if !PLATFORM_COMPACTFRAMEWORK
+ ///
+ /// This object is used with lock statements to synchronize access to the
+ /// field, below.
+ ///
+ internal readonly object _enlistmentSyncRoot = new object();
+
///
/// Whether or not the connection is enlisted in a distrubuted transaction
///
internal SQLiteEnlistment _enlistment;
#endif
@@ -2031,11 +2038,11 @@
// unless the caller used a negative number, in that case
// skip sleeping at all because we do not want to block
// this thread forever.
//
if (retry && (retryMilliseconds >= 0))
- System.Threading.Thread.Sleep(retryMilliseconds);
+ Thread.Sleep(retryMilliseconds);
//
// NOTE: There is no point in calling the native API to copy
// zero pages as it does nothing; therefore, stop now.
//
@@ -2918,37 +2925,46 @@
null, null));
if (_sql != null)
{
#if !PLATFORM_COMPACTFRAMEWORK
- if (_enlistment != null)
+ lock (_enlistmentSyncRoot) /* TRANSACTIONAL */
{
- // If the connection is enlisted in a transaction scope and the scope is still active,
- // we cannot truly shut down this connection until the scope has completed. Therefore make a
- // hidden connection temporarily to hold open the connection until the scope has completed.
- SQLiteConnection cnn = new SQLiteConnection();
+ SQLiteEnlistment enlistment = _enlistment;
+ _enlistment = null;
+
+ if (enlistment != null)
+ {
+ // If the connection is enlisted in a transaction scope and the scope is still active,
+ // we cannot truly shut down this connection until the scope has completed. Therefore make a
+ // hidden connection temporarily to hold open the connection until the scope has completed.
+ SQLiteConnection cnn = new SQLiteConnection();
#if DEBUG
- cnn._debugString = HelperMethods.StringFormat(
- CultureInfo.InvariantCulture,
- "closeThreadId = {0}, {1}{2}{2}{3}",
- HelperMethods.GetThreadId(), _sql,
- Environment.NewLine, _debugString);
+ cnn._debugString = HelperMethods.StringFormat(
+ CultureInfo.InvariantCulture,
+ "closeThreadId = {0}, {1}{2}{2}{3}",
+ HelperMethods.GetThreadId(), _sql,
+ Environment.NewLine, _debugString);
#endif
- cnn._sql = _sql;
- cnn._transactionLevel = _transactionLevel;
- cnn._transactionSequence = _transactionSequence;
- cnn._enlistment = _enlistment;
- cnn._connectionState = _connectionState;
- cnn._version = _version;
-
- cnn._enlistment._transaction._cnn = cnn;
- cnn._enlistment._disposeConnection = true;
-
- _sql = null;
- _enlistment = null;
+ cnn._sql = _sql;
+ cnn._transactionLevel = _transactionLevel;
+ cnn._transactionSequence = _transactionSequence;
+ cnn._enlistment = enlistment;
+ cnn._connectionState = _connectionState;
+ cnn._version = _version;
+
+ SQLiteTransaction transaction = enlistment._transaction;
+
+ if (transaction != null)
+ transaction._cnn = cnn;
+
+ enlistment._disposeConnection = true;
+
+ _sql = null;
+ }
}
#endif
if (_sql != null)
{
_sql.Close(_disposing);
@@ -3395,32 +3411,150 @@
/// Manual distributed transaction enlistment support
///
/// The distributed transaction to enlist in
public override void EnlistTransaction(System.Transactions.Transaction transaction)
{
- CheckDisposed();
-
- if (_enlistment != null && transaction == _enlistment._scope)
- return;
- else if (_enlistment != null)
- throw new ArgumentException("Already enlisted in a transaction");
-
- if (_transactionLevel > 0 && transaction != null)
- throw new ArgumentException("Unable to enlist in transaction, a local transaction already exists");
- else if (transaction == null)
- throw new ArgumentNullException("Unable to enlist in transaction, it is null");
-
- bool strictEnlistment = ((_flags & SQLiteConnectionFlags.StrictEnlistment) ==
- SQLiteConnectionFlags.StrictEnlistment);
-
- _enlistment = new SQLiteEnlistment(this, transaction,
- GetFallbackDefaultIsolationLevel(), strictEnlistment,
- strictEnlistment);
-
- OnChanged(this, new ConnectionEventArgs(
- SQLiteConnectionEventType.EnlistTransaction, null, null, null, null,
- null, null, new object[] { _enlistment }));
+ CheckDisposed();
+
+ lock (_enlistmentSyncRoot) /* TRANSACTIONAL */
+ {
+ if (_enlistment != null && transaction == _enlistment._scope)
+ return;
+ else if (_enlistment != null)
+ throw new ArgumentException("Already enlisted in a transaction");
+
+ if (_transactionLevel > 0 && transaction != null)
+ throw new ArgumentException("Unable to enlist in transaction, a local transaction already exists");
+ else if (transaction == null)
+ throw new ArgumentNullException("Unable to enlist in transaction, it is null");
+
+ bool strictEnlistment = ((_flags & SQLiteConnectionFlags.StrictEnlistment) ==
+ SQLiteConnectionFlags.StrictEnlistment);
+
+ _enlistment = new SQLiteEnlistment(this, transaction,
+ GetFallbackDefaultIsolationLevel(), strictEnlistment,
+ strictEnlistment);
+
+ OnChanged(this, new ConnectionEventArgs(
+ SQLiteConnectionEventType.EnlistTransaction, null, null, null, null,
+ null, null, new object[] { _enlistment }));
+ }
+ }
+
+ ///
+ /// ]]>EXPERIMENTAL]]>
+ /// Waits for the enlistment associated with this connection to be reset.
+ ///
+ ///
+ /// The approximate maximum number of milliseconds to wait before timing
+ /// out the wait operation.
+ ///
+ ///
+ /// Non-zero if the enlistment assciated with this connection was reset;
+ /// otherwise, zero. It should be noted that this method returning a
+ /// non-zero value does not necessarily guarantee that the connection
+ /// can enlist in a new transaction (i.e. due to potentical race with
+ /// other threads); therefore, callers should generally use try/catch
+ /// when calling the method.
+ ///
+ public bool WaitForEnlistmentReset(
+ int timeoutMilliseconds
+ )
+ {
+ CheckDisposed();
+
+ if (timeoutMilliseconds < 0)
+ throw new ArgumentException("timeout cannot be negative");
+
+ const int defaultMilliseconds = 100;
+ int sleepMilliseconds;
+
+ if (timeoutMilliseconds == 0)
+ {
+ sleepMilliseconds = 0;
+ }
+ else
+ {
+ sleepMilliseconds = Math.Min(
+ timeoutMilliseconds / 10, defaultMilliseconds);
+
+ if (sleepMilliseconds == 0)
+ sleepMilliseconds = defaultMilliseconds;
+ }
+
+ DateTime start = DateTime.UtcNow;
+
+ while (true)
+ {
+ //
+ // NOTE: Attempt to acquire the necessary lock without blocking.
+ // This method will treat a failure to obtain the lock the
+ // same as the enlistment not being reset yet. Both will
+ // advance toward the timeout.
+ //
+ bool locked = Monitor.TryEnter(_enlistmentSyncRoot);
+
+ try
+ {
+ if (locked)
+ {
+ //
+ // NOTE: Is there still an enlistment? If not, we are
+ // done. There is a potential race condition in
+ // the caller if another thread is able to setup
+ // a new enlistment at any point prior to our
+ // caller fully dealing with the result of this
+ // method. However, that should generally never
+ // happen because this class is not intended to
+ // be used by multiple concurrent threads, with
+ // the notable exception of an active enlistment
+ // being asynchronously committed or rolled back
+ // by the .NET Framework.
+ //
+ if (_enlistment == null)
+ return true;
+ }
+ }
+ finally
+ {
+ if (locked)
+ {
+ Monitor.Exit(_enlistmentSyncRoot);
+ locked = false;
+ }
+ }
+
+ //
+ // NOTE: A timeout value of zero is special. It means never
+ // sleep.
+ //
+ if (sleepMilliseconds == 0)
+ return false;
+
+ //
+ // NOTE: How much time has elapsed since we first starting
+ // waiting?
+ //
+ DateTime now = DateTime.UtcNow;
+ TimeSpan elapsed = now.Subtract(start);
+
+ //
+ // NOTE: Are we done wait?
+ //
+ double totalMilliseconds = elapsed.TotalMilliseconds;
+
+ if ((totalMilliseconds < 0) || /* Time went backward? */
+ (totalMilliseconds >= (double)timeoutMilliseconds))
+ {
+ return false;
+ }
+
+ //
+ // NOTE: Sleep for a bit and then try again.
+ //
+ Thread.Sleep(sleepMilliseconds);
+ }
}
#endif
///
/// Looks for a key in the array of key/values of the parameter string. If not found, return the specified default value
Index: System.Data.SQLite/SQLiteEnlistment.cs
==================================================================
--- System.Data.SQLite/SQLiteEnlistment.cs
+++ System.Data.SQLite/SQLiteEnlistment.cs
@@ -104,11 +104,11 @@
///////////////////////////////////////////////////////////////////////////
private void Cleanup(SQLiteConnection cnn)
{
- if (_disposeConnection)
+ if (_disposeConnection && (cnn != null))
cnn.Dispose();
_transaction = null;
_scope = null;
}
@@ -182,27 +182,51 @@
///////////////////////////////////////////////////////////////////////////
#region IEnlistmentNotification Members
public void Commit(Enlistment enlistment)
{
- CheckDisposed();
-
- SQLiteConnection cnn = _transaction.Connection;
- cnn._enlistment = null;
-
- try
- {
- _transaction.IsValid(true);
- _transaction.Connection._transactionLevel = 1;
- _transaction.Commit();
-
- enlistment.Done();
- }
- finally
- {
- Cleanup(cnn);
- }
+ CheckDisposed();
+
+ SQLiteConnection cnn = null;
+
+ try
+ {
+ while (true)
+ {
+ cnn = _transaction.Connection;
+
+ if (cnn == null)
+ break;
+
+ lock (cnn._enlistmentSyncRoot) /* TRANSACTIONAL */
+ {
+ //
+ // NOTE: This check is necessary to detect the case where
+ // the SQLiteConnection.Close() method changes the
+ // connection associated with our transaction (i.e.
+ // to avoid a race (condition) between grabbing the
+ // Connection property and locking its enlistment).
+ //
+ if (!Object.ReferenceEquals(cnn, _transaction.Connection))
+ continue;
+
+ cnn._enlistment = null;
+
+ _transaction.IsValid(true); /* throw */
+ cnn._transactionLevel = 1;
+ _transaction.Commit();
+
+ break;
+ }
+ }
+
+ enlistment.Done();
+ }
+ finally
+ {
+ Cleanup(cnn);
+ }
}
///////////////////////////////////////////////////////////////////////////
public void InDoubt(Enlistment enlistment)
@@ -225,24 +249,49 @@
///////////////////////////////////////////////////////////////////////////
public void Rollback(Enlistment enlistment)
{
- CheckDisposed();
-
- SQLiteConnection cnn = _transaction.Connection;
- cnn._enlistment = null;
-
- try
- {
- _transaction.Rollback();
- enlistment.Done();
- }
- finally
- {
- Cleanup(cnn);
- }
+ CheckDisposed();
+
+ SQLiteConnection cnn = null;
+
+ try
+ {
+ while (true)
+ {
+ cnn = _transaction.Connection;
+
+ if (cnn == null)
+ break;
+
+ lock (cnn._enlistmentSyncRoot) /* TRANSACTIONAL */
+ {
+ //
+ // NOTE: This check is necessary to detect the case where
+ // the SQLiteConnection.Close() method changes the
+ // connection associated with our transaction (i.e.
+ // to avoid a race (condition) between grabbing the
+ // Connection property and locking its enlistment).
+ //
+ if (!Object.ReferenceEquals(cnn, _transaction.Connection))
+ continue;
+
+ cnn._enlistment = null;
+
+ _transaction.Rollback();
+
+ break;
+ }
+ }
+
+ enlistment.Done();
+ }
+ finally
+ {
+ Cleanup(cnn);
+ }
}
#endregion
}
}
#endif // !PLATFORM_COMPACT_FRAMEWORK
ADDED Tests/tkt-5cee5409f8.eagle
Index: Tests/tkt-5cee5409f8.eagle
==================================================================
--- /dev/null
+++ Tests/tkt-5cee5409f8.eagle
@@ -0,0 +1,219 @@
+###############################################################################
+#
+# tkt-5cee5409f8.eagle --
+#
+# Written by Joe Mistachkin.
+# Released to the public domain, use at your own risk!
+#
+###############################################################################
+
+package require Eagle
+package require Eagle.Library
+package require Eagle.Test
+
+runTestPrologue
+
+###############################################################################
+
+package require System.Data.SQLite.Test
+runSQLiteTestPrologue
+
+###############################################################################
+
+runTest {test tkt-5cee5409f8-1.1 {asynchronous transaction handling} -setup {
+ setupDb [set fileName tkt-5cee5409f8-1.1.db]
+} -body {
+ sql execute $db "CREATE TABLE t1(x INTEGER);"
+
+ set id [object invoke Interpreter.GetActive NextId]
+ set dataSource [file join [getDatabaseDirectory] $fileName]
+
+ unset -nocomplain results errors
+
+ set code [compileCSharpWith [subst {
+ using System;
+ using System.Data.SQLite;
+ using System.Threading;
+ using System.Transactions;
+
+ namespace _Dynamic${id}
+ {
+ public static class Test${id}
+ {
+ #region Private EnlistmentNotification Class
+ private sealed class EnlistmentNotification : IEnlistmentNotification
+ {
+ #region Private Data
+ private bool forceRollback;
+ #endregion
+
+ /////////////////////////////////////////////////////////////////////
+
+ #region Private Constructors
+ private EnlistmentNotification(bool forceRollback)
+ {
+ this.forceRollback = forceRollback;
+ }
+ #endregion
+
+ /////////////////////////////////////////////////////////////////////
+
+ #region IEnlistmentNotification Members
+ public void Commit(Enlistment enlistment)
+ {
+ enlistment.Done();
+ }
+
+ /////////////////////////////////////////////////////////////////////
+
+ public void InDoubt(Enlistment enlistment)
+ {
+ enlistment.Done();
+ }
+
+ /////////////////////////////////////////////////////////////////////
+
+ public void Prepare(PreparingEnlistment preparingEnlistment)
+ {
+ if (forceRollback)
+ preparingEnlistment.ForceRollback();
+ else
+ preparingEnlistment.Prepared();
+ }
+
+ /////////////////////////////////////////////////////////////////////
+
+ public void Rollback(Enlistment enlistment)
+ {
+ enlistment.Done();
+ }
+ #endregion
+
+ /////////////////////////////////////////////////////////////////////
+
+ #region Public Static Methods
+ public static void UseDistributedTransaction(bool forceRollback)
+ {
+ Transaction.Current.EnlistDurable(
+ Guid.NewGuid(), new EnlistmentNotification(forceRollback),
+ EnlistmentOptions.None);
+ }
+ #endregion
+ }
+ #endregion
+
+ ///////////////////////////////////////////////////////////////////////
+
+ #region Private Static Data
+ private static int resetCount;
+ private static int timeoutCount;
+ #endregion
+
+ ///////////////////////////////////////////////////////////////////////
+
+ #region Private Static Methods
+ private static void DoTransactions(SQLiteConnection connection)
+ {
+ Random random = new Random();
+
+ for (int iteration = 0; iteration < 10000; iteration++)
+ {
+ using (TransactionScope transactionScope = new TransactionScope())
+ {
+ EnlistmentNotification.UseDistributedTransaction(false);
+
+ TransactionInformation transactionInformation =
+ Transaction.Current.TransactionInformation;
+
+ if (transactionInformation.DistributedIdentifier.Equals(
+ Guid.Empty))
+ {
+ throw new Exception("distributed identifier is empty");
+ }
+
+ connection.EnlistTransaction(Transaction.Current);
+
+ using (SQLiteCommand command = connection.CreateCommand())
+ {
+ command.CommandText = "INSERT INTO t1(x) VALUES(?);";
+ command.Parameters.Add(new SQLiteParameter("", iteration));
+ command.ExecuteNonQuery();
+ }
+
+ transactionScope.Complete();
+ }
+
+ Thread.Sleep(random.Next(10));
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+
+ private static void WaitOnEnlistments(object state)
+ {
+ SQLiteConnection connection = (SQLiteConnection)state;
+ Random random = new Random();
+
+ for (int iteration = 0; iteration < 1000; iteration++)
+ {
+ if (connection.WaitForEnlistmentReset(1))
+ Interlocked.Increment(ref resetCount);
+ else
+ Interlocked.Increment(ref timeoutCount);
+
+ Thread.Sleep(random.Next(100));
+ }
+ }
+ #endregion
+
+ ///////////////////////////////////////////////////////////////////////
+
+ #region Public Static Methods
+ public static string DoTest()
+ {
+ using (SQLiteConnection connection = new SQLiteConnection(
+ "Data Source=${dataSource};[getTestProperties]"))
+ {
+ ThreadPool.QueueUserWorkItem(WaitOnEnlistments, connection);
+
+ connection.Open();
+
+ DoTransactions(connection);
+ }
+
+ int count1 = Interlocked.CompareExchange(ref resetCount, 0, 0);
+ int count2 = Interlocked.CompareExchange(ref timeoutCount, 0, 0);
+
+ return String.Format("{0}, {1}", count1, count2);
+ }
+
+ ///////////////////////////////////////////////////////////////////////
+
+ public static void Main()
+ {
+ // do nothing.
+ }
+ #endregion
+ }
+ }
+ }] true true true results errors System.Data.SQLite.dll]
+
+ list $code $results \
+ [expr {[info exists errors] ? $errors : ""}] \
+ [expr {$code eq "Ok" ? [catch {
+ object invoke _Dynamic${id}.Test${id} DoTest
+ } result] : [set result ""]}] \
+ [expr {[lindex $result 0] > 0}] \
+ [expr {[lindex $result 1] > 0}]
+} -cleanup {
+ cleanupDb $fileName
+
+ unset -nocomplain result results errors code dataSource id db fileName
+} -constraints {eagle command.object monoBug211 monoBug54 command.sql\
+compile.DATA SQLite System.Data.SQLite compileCSharp} -match regexp -result \
+{^Ok System#CodeDom#Compiler#CompilerResults#\d+ \{\} 0 True True$}}
+
+###############################################################################
+
+runSQLiteTestEpilogue
+runTestEpilogue
Index: readme.htm
==================================================================
--- readme.htm
+++ readme.htm
@@ -211,10 +211,12 @@
1.0.107.0 - January XX, 2018 (release scheduled)
- Updated to SQLite 3.22.0.
- Improve performance of type name lookups by removing superfluous locking and string creation.
+ - Support asynchronous completion of distributed transactions. Fix for [5cee5409f8].
+ - Add experimental WaitForEnlistmentReset method to the SQLiteConnection class. Pursuant to [7e1dd697dc].
- Fix some internal memory accounting present only in the debug build.
- Make sure inbound native delegates are unhooked before adding a connection to the pool. Fix for [0e48e80333].
- Add preliminary support for the .NET Framework 4.7.1.
- Updates to internal DbType mapping related lookup tables. Pursuant to [a799e3978f].
Index: www/news.wiki
==================================================================
--- www/news.wiki
+++ www/news.wiki
@@ -48,10 +48,12 @@
1.0.107.0 - January XX, 2018 (release scheduled)
- Updated to [https://www.sqlite.org/draft/releaselog/3_22_0.html|SQLite 3.22.0].
- Improve performance of type name lookups by removing superfluous locking and string creation.
+ - Support asynchronous completion of distributed transactions. Fix for [5cee5409f8].
+ - Add experimental WaitForEnlistmentReset method to the SQLiteConnection class. Pursuant to [7e1dd697dc].
- Fix some internal memory accounting present only in the debug build.
- Make sure inbound native delegates are unhooked before adding a connection to the pool. Fix for [0e48e80333].
- Add preliminary support for the .NET Framework 4.7.1.
- Updates to internal DbType mapping related lookup tables. Pursuant to [a799e3978f].