############################################################################### # # thread.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 ############################################################################### checkForSQLiteDirectories $test_channel true set handle_counts [getSQLiteHandleCounts $test_channel] set memory_used [reportSQLiteResources $test_channel] ############################################################################### # # NOTE: How many test threads should be created and used for these tests? This # value must be at least two. The first test thread (at index 0) is # always the "master thread" and just repeatedly calls the Thread.Abort() # method on other random test threads that appear to be alive until all # test threads appear to have died. All other test threads will attempt # to open a connection, execute one or more queries against it, and then # close it. # if {![info exists count(1)]} then { set count(1) 10 } # # NOTE: How many milliseconds should the master thread initially wait until it # starts to abort the other test threads? # if {![info exists count(2)]} then { set count(2) 1000 } # # NOTE: How many milliseconds (maximum) should the master thread wait between # aborting other random test threads? # if {![info exists count(3)]} then { set count(3) 1000 } # # NOTE: How many bytes should be used for the five random data blobs contained # in the test database? # if {![info exists count(4)]} then { set count(4) 1000 } ############################################################################### # # NOTE: This test is disabled for .NET Core due to its lack of support for # the Thread.Abort method (i.e. throws PlatformNotSupportedException # when called). # runTest {test thread-1.1 {Thread.Abort() impact on native resources} -setup { setupDb [set fileName thread-1.1.db] tputs $test_channel [appendArgs \ "---- using " $count(1) " test threads (with one master thread)\n"] tputs $test_channel [appendArgs \ "---- initial wait will be " $count(2) " milliseconds\n"] tputs $test_channel [appendArgs \ "---- subsequent maximum wait will be " $count(3) " milliseconds\n"] tputs $test_channel [appendArgs \ "---- random data blob size will be " $count(4) " bytes\n"] } -body { sql execute $db [subst { CREATE TABLE t1(x INTEGER PRIMARY KEY, y BLOB); INSERT INTO t1 (y) VALUES(RANDOMBLOB(${count(4)})); INSERT INTO t1 (y) VALUES(RANDOMBLOB(${count(4)})); INSERT INTO t1 (y) VALUES(RANDOMBLOB(${count(4)})); INSERT INTO t1 (y) VALUES(RANDOMBLOB(${count(4)})); INSERT INTO t1 (y) VALUES(RANDOMBLOB(${count(4)})); }] # # NOTE: The data and temporary directories must be reset here (after the # setupDb call above) because they allocate some SQLite memory and # this test requires an extremely accurate reading. # sql execute $db { PRAGMA data_store_directory = ""; PRAGMA temp_store_directory = ""; } # # NOTE: Close the database now, freeing any native SQLite memory and/or # resources that it may be using at this point; however, do not # actually delete the database file because it is needed in the C# # code for this test. # cleanupDb $fileName db true false false; unset -nocomplain db # # NOTE: Setup the variables used in the dynamically substituted C# code # for the main body of this test (below). # set id [object invoke Interpreter.GetActive NextId] set dataSource [file join [getDatabaseDirectory] $fileName] set sql { \ SELECT x, y FROM t1 ORDER BY x; \ } unset -nocomplain results errors set code [compileCSharpWith [subst { using System; using System.Collections.Generic; using System.Data.SQLite; using System.Diagnostics; using System.Threading; using Eagle._Containers.Public; namespace _Dynamic${id} { public static class Test${id} { public static LongList RunTestThreads() { // // NOTE: This is the total number of data bytes seen in the second // column of the query result rows seen by all test threads. // This value will vary greatly based upon how many loop // iterations are performed prior to each test thread being // aborted. // long sum = 0; // // NOTE: This is the total number of query result rows seen by all // the test threads. // long rows = 0; // // NOTE: This is the total number of exceptions caught by all the // test threads. // long errors = 0; // // NOTE: This is the total number of test threads to create. // int count = ${count(1)}; // // NOTE: Create the array of thread objects. // Thread\[\] thread = new Thread\[count\]; // // NOTE: Create a random number generator suitable for waiting a // random number of milliseconds between each loop iteration // and then selecting a random thread index. // Random random = new Random(); // // NOTE: Create the event that will be used to synchronize all the // created threads so that they start doing their actual test // "work" at approximately the same time. This is also used // to make sure that all test threads are inside of a try // block before attempting to abort them. // using (ManualResetEvent goEvent = new ManualResetEvent(false)) { // // NOTE: Create a (reusable) delegate that will contain the code // that most of the created test threads are to execute. // This code will open, use, and close a single database // connection. Multiple commands and data readers will also // be used. // ThreadStart threadStart1 = delegate() { try { // // NOTE: Wait forever for the "GO" signal so that all threads // can start working at approximately the same time. // This is also used to make sure that all test threads // are inside of a try block before attempting to abort // them. // goEvent.WaitOne(); // // NOTE: Create a new connection object. We purposely avoid // putting this inside a "using" block to help test our // cleanup via the garbage collector. // SQLiteConnection connection = new SQLiteConnection( "Data Source=${dataSource};[getTestProperties]"); // // NOTE: Open the connection. After this point, native memory // and resources have been allocated by this thread. // connection.Open(); // // NOTE: Loop forever until the first thread signals us to stop // via calling Thread.Abort() method on our associated // thread object. // while (true) { // // NOTE: Create a dictionary to temporarily hold the values // from the data reader for this loop iteration. // Dictionary results = new Dictionary(); // // NOTE: Create a new command object using the connection // object for this thread. Again, avoid putting this // inside a "using" block to help test our cleanup via // the garbage collector. // SQLiteCommand command = new SQLiteCommand("${sql}", connection); // // NOTE: Execute the query and get the resulting data reader // object. Again, avoid putting this inside a "using" // block to help test our cleanup via the garbage // collector. // SQLiteDataReader reader = command.ExecuteReader(); // // NOTE: Start processing each available query result row. // This processing (or any of the above processing) // may be stopped at any time due to this test thread // being aborted. // while (reader.Read()) { results.Add((long)reader\["x"\], reader\["y"\] as byte\[\]); Interlocked.Add(ref sum, results\[(long)reader\["x"\]\].Length); Interlocked.Increment(ref rows); } // // NOTE: Close the data reader for this loop iteration as we // are done with it. // reader.Close(); reader = null; // // NOTE: Dispose the command for this loop iteration as we // are done with it. // command.Dispose(); command = null; } // // NOTE: Close the connection for this test thread as we are // done with it. Since the above loop is infinite, it // should only be exited via this test thread being // aborted; therefore, execution should never reach this // point. // connection.Close(); connection = null; } catch (Exception e) { Interlocked.Increment(ref errors); Trace.WriteLine(e); } }; // // NOTE: Create a (reusable) delegate that will contain the code // that half the created threads are to execute. This code // will repeatedly call the Thread.Abort() method on all the // other test threads until they all appear to be dead. // ThreadStart threadStart2 = delegate() { try { // // NOTE: Wait forever for the "GO" signal so that all threads // can start working at approximately the same time. // This is also used to make sure that all test threads // are inside of a try block before attempting to abort // them. // goEvent.WaitOne(); // // NOTE: Give the other test threads a slight head start to // make sure that they are fully alive prior to trying // to abort any of them. // Thread.Sleep(${count(2)}); // // NOTE: Loop forever until all test threads appear to be dead. // while (true) { // // NOTE: Wait a random number of milliseconds, up to a full // second. // Thread.Sleep(random.Next(0, ${count(3)})); // // NOTE: Select a random thread to abort. // int index = random.Next(1, count); // // NOTE: If the thread appears to be alive, try to abort // it. // try { if (thread\[index\].IsAlive) thread\[index\].Abort(); } catch (Exception e) { Trace.WriteLine(e); } // // NOTE: If all the other threads are dead, presumably due // to being aborted, stop now. This check is simpler, // and possibly more reliable, than checking if any of // the test threads are still alive via their IsAlive // property. // if (Interlocked.Increment(ref errors) == count) break; else Interlocked.Decrement(ref errors); } } catch (Exception e) { Interlocked.Increment(ref errors); Trace.WriteLine(e); } }; // // NOTE: Create each of the test threads with a suitable stack // size. We must specify a stack size here because the // default one for the process would be the same as the // parent executable (the Eagle shell), which is 16MB, // too large to be useful. // for (int index = 0; index < count; index++) { // // NOTE: Figure out what kind of thread to create (i.e. one // that uses a connection or one that calls the GC). // ThreadStart threadStart; if (index == 0) threadStart = threadStart2; else threadStart = threadStart1; thread\[index\] = new Thread(threadStart, 1048576); // // NOTE: Name each thread for a better debugging experience. // thread\[index\].Name = String.Format( "[file rootname ${fileName}] #{0}", index); } // // NOTE: Start all the threads now. They should not actually do // any of the test "work" until we signal the event. // for (int index = 0; index < count; index++) thread\[index\].Start(); // // NOTE: Send the signal that all threads should start doing // their test "work" now. // goEvent.Set(); /* GO */ // // NOTE: Wait forever for each thread to finish its test "work" // and then die. // for (int index = 0; index < count; index++) thread\[index\].Join(); } // // NOTE: Return a list of integers with total number of data bytes // seen, total number of query result rows seen, and the total // number of exceptions caught by all the test threads. // LongList counts = new LongList(); counts.Add(sum); counts.Add(rows); counts.Add(errors); return counts; } /////////////////////////////////////////////////////////////////////// public static void Main() { // do nothing. } } } }] true false true results errors [list System.Data.SQLite.dll Eagle.dll] \ WarningLevel 0] list $code $results \ [expr {[info exists errors] ? $errors : ""}] \ [expr {$code eq "Ok" ? [catch { object invoke _Dynamic${id}.Test${id} RunTestThreads } result] : [set result ""]}] $result \ [collectGarbage $test_channel [expr {$count(1) * 1000}] false] \ [getSQLiteHandleCounts $test_channel] \ [reportSQLiteResources $test_channel] } -cleanup { cleanupDb $fileName unset -nocomplain result results errors code sql dataSource id db fileName } -time true -constraints [fixConstraints {eagle command.object monoBug28\ monoCrash211 monoCrash42 monoBug46 command.sql compile.DATA SQLite\ System.Data.SQLite compileCSharp !dotNetCore}] -match regexp -result \ [appendArgs "^Ok System#CodeDom#Compiler#CompilerResults#\\d+ \\{\\} 0 \\{\\d+\ \\d+ " $count(1) "\\} \\{\\} \\{" $handle_counts "\\} " $memory_used \$]} ############################################################################### unset -nocomplain count ############################################################################### unset -nocomplain memory_used handle_counts ############################################################################### runSQLiteTestEpilogue runTestEpilogue