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)

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)

Index: www/news.wiki ================================================================== --- www/news.wiki +++ www/news.wiki @@ -48,10 +48,12 @@ 1.0.107.0 - January XX, 2018 (release scheduled)