gitlab.com/CoiaPrant/sqlite3@v1.19.1/testdata/tcl/thread005.test (about)

     1  # 2009 March 11
     2  #
     3  # The author disclaims copyright to this source code.  In place of
     4  # a legal notice, here is a blessing:
     5  #
     6  #    May you do good and not evil.
     7  #    May you find forgiveness for yourself and forgive others.
     8  #    May you share freely, never taking more than you give.
     9  #
    10  #***********************************************************************
    11  #
    12  # Test a race-condition that shows up in shared-cache mode.
    13  #
    14  # $Id: thread005.test,v 1.5 2009/03/26 14:48:07 danielk1977 Exp $
    15  
    16  set testdir [file dirname $argv0]
    17  
    18  source $testdir/tester.tcl
    19  if {[run_thread_tests]==0} { finish_test ; return }
    20  ifcapable !shared_cache {
    21    finish_test
    22    return
    23  }
    24  
    25  db close
    26  
    27  # Use shared-cache mode for these tests.
    28  # 
    29  set ::enable_shared_cache [sqlite3_enable_shared_cache]
    30  sqlite3_enable_shared_cache 1
    31  
    32  #-------------------------------------------------------------------------
    33  # This test attempts to hit the race condition fixed by commit [6363].
    34  #
    35  proc runsql {zSql {db {}}} {
    36    set rc SQLITE_OK
    37    while {$rc=="SQLITE_OK" && $zSql ne ""} {
    38      set STMT [sqlite3_prepare_v2 $db $zSql -1 zSql]
    39      while {[set rc [sqlite3_step $STMT]] eq "SQLITE_ROW"} { }
    40      set rc [sqlite3_finalize $STMT]
    41    }
    42    return $rc
    43  }
    44  do_test thread005-1.1 {
    45    sqlite3 db test.db
    46    db eval { CREATE TABLE t1(a, b) }
    47    db close
    48  } {}
    49  for {set ii 2} {$ii < 500} {incr ii} {
    50    unset -nocomplain finished
    51    thread_spawn finished(0) {sqlite3_open test.db}
    52    thread_spawn finished(1) {sqlite3_open test.db}
    53    if {![info exists finished(0)]} { vwait finished(0) }
    54    if {![info exists finished(1)]} { vwait finished(1) }
    55  
    56    do_test thread005-1.$ii {
    57      runsql { BEGIN }                       $finished(0)
    58      runsql { INSERT INTO t1 VALUES(1, 2) } $finished(0)
    59  
    60      # If the race-condition was hit, then $finished(0 and $finished(1)
    61      # will not use the same pager cache. In this case the next statement
    62      # can be executed succesfully. However, if the race-condition is not
    63      # hit, then $finished(1) will be blocked by the write-lock held by 
    64      # $finished(0) on the shared-cache table t1 and the statement will
    65      # return SQLITE_LOCKED.
    66      #
    67      runsql { SELECT * FROM t1 }            $finished(1)
    68    } {SQLITE_LOCKED}
    69  
    70    sqlite3_close $finished(0)
    71    sqlite3_close $finished(1)
    72  }
    73  
    74  
    75  #-------------------------------------------------------------------------
    76  # This test tries to exercise a race-condition that existed in shared-cache
    77  # mode at one point. The test uses two threads; each has a database connection
    78  # open on the same shared cache. The schema of the database is:
    79  #
    80  #    CREATE TABLE t1(a INTEGER PRIMARY KEY, b UNIQUE);
    81  #
    82  # One thread is a reader and the other thread a reader and a writer. The 
    83  # writer thread repeats the following transaction as fast as possible:
    84  # 
    85  #      BEGIN;
    86  #        DELETE FROM t1 WHERE a = (SELECT max(a) FROM t1);
    87  #        INSERT INTO t1 VALUES(NULL, NULL);
    88  #        UPDATE t1 SET b = a WHERE a = (SELECT max(a) FROM t1);
    89  #        SELECT count(*) FROM t1 WHERE b IS NULL;
    90  #      COMMIT;
    91  #
    92  # The reader thread does the following over and over as fast as possible:
    93  #
    94  #      BEGIN;
    95  #        SELECT count(*) FROM t1 WHERE b IS NULL;
    96  #      COMMIT;
    97  #
    98  # The test runs for 20 seconds or until one of the "SELECT count(*)" 
    99  # statements returns a non-zero value. If an SQLITE_LOCKED error occurs,
   100  # the connection issues a ROLLBACK immediately to abandon the current
   101  # transaction.
   102  #
   103  # If everything is working correctly, the "SELECT count(*)" statements 
   104  # should never return a value other than 0. The "INSERT" statement 
   105  # executed by the writer adds a row with "b IS NULL" to the table, but
   106  # the subsequent UPDATE statement sets its "b" value to an integer
   107  # immediately afterwards.
   108  #
   109  # However, before the race-condition was fixed, if the reader's SELECT
   110  # statement hit an error (say an SQLITE_LOCKED) at the same time as the
   111  # writer was executing the UPDATE statement, then it could incorrectly
   112  # rollback the statement-transaction belonging to the UPDATE statement.
   113  # The UPDATE statement would still be reported as successful to the user,
   114  # but it would have no effect on the database contents.
   115  # 
   116  # Note that it has so far only proved possible to hit this race-condition
   117  # when using an ATTACHed database. There doesn't seem to be any reason
   118  # for this, other than that operating on an ATTACHed database means there
   119  # are a few more mutex grabs and releases during the window of time open
   120  # for the race-condition. Maybe this encourages the scheduler to context
   121  # switch or something...
   122  #
   123  
   124  forcedelete test.db test2.db
   125  unset -nocomplain finished
   126  
   127  do_test thread005-2.1 {
   128    sqlite3 db test.db
   129    execsql { ATTACH 'test2.db' AS aux }
   130    execsql {
   131      CREATE TABLE aux.t1(a INTEGER PRIMARY KEY, b UNIQUE);
   132      INSERT INTO t1 VALUES(1, 1);
   133      INSERT INTO t1 VALUES(2, 2);
   134    }
   135    db close
   136  } {}
   137  
   138  
   139  set ThreadProgram {
   140    proc execsql {zSql {db {}}} {
   141      if {$db eq ""} {set db $::DB}
   142  
   143      set lRes [list]
   144      set rc SQLITE_OK
   145  
   146      while {$rc=="SQLITE_OK" && $zSql ne ""} {
   147        set STMT [sqlite3_prepare_v2 $db $zSql -1 zSql]
   148        while {[set rc [sqlite3_step $STMT]] eq "SQLITE_ROW"} {
   149          for {set i 0} {$i < [sqlite3_column_count $STMT]} {incr i} {
   150            lappend lRes [sqlite3_column_text $STMT 0]
   151          }
   152        }
   153        set rc [sqlite3_finalize $STMT]
   154      }
   155  
   156      if {$rc != "SQLITE_OK"} { error "$rc [sqlite3_errmsg $db]" }
   157      return $lRes
   158    }
   159  
   160    if {$isWriter} {
   161      set Sql {
   162        BEGIN;
   163          DELETE FROM t1 WHERE a = (SELECT max(a) FROM t1);
   164          INSERT INTO t1 VALUES(NULL, NULL);
   165          UPDATE t1 SET b = a WHERE a = (SELECT max(a) FROM t1);
   166          SELECT count(*) FROM t1 WHERE b IS NULL;
   167        COMMIT;
   168      }
   169    } else {
   170      set Sql {
   171        BEGIN;
   172        SELECT count(*) FROM t1 WHERE b IS NULL;
   173        COMMIT;
   174      }
   175    }
   176  
   177    set ::DB [sqlite3_open test.db]
   178  
   179    execsql { ATTACH 'test2.db' AS aux }
   180  
   181    set result "ok"
   182    set finish [expr [clock_seconds]+5]
   183    while {$result eq "ok" && [clock_seconds] < $finish} {
   184      set rc [catch {execsql $Sql} msg]
   185      if {$rc} {
   186        if {[string match "SQLITE_LOCKED*" $msg]} {
   187          catch { execsql ROLLBACK }
   188        } else {
   189          sqlite3_close $::DB
   190          error $msg
   191        }
   192      } elseif {$msg ne "0"} {
   193        set result "failed"
   194      }
   195    }
   196  
   197    sqlite3_close $::DB
   198    set result
   199  }
   200  
   201  # There is a race-condition in btree.c that means that if two threads
   202  # attempt to open the same database at roughly the same time, and there
   203  # does not already exist a shared-cache corresponding to that database,
   204  # then two shared-caches can be created instead of one. Things still more
   205  # or less work, but the two database connections do not use the same
   206  # shared-cache.
   207  #
   208  # If the threads run by this test hit this race-condition, the tests
   209  # fail (because SQLITE_BUSY may be unexpectedly returned instead of
   210  # SQLITE_LOCKED). To prevent this from happening, open a couple of
   211  # connections to test.db and test2.db now to make sure that there are
   212  # already shared-caches in memory for all databases opened by the
   213  # test threads.
   214  #
   215  sqlite3 db test.db
   216  sqlite3 db test2.db
   217  
   218  puts "Running thread-tests for ~20 seconds"
   219  thread_spawn finished(0) {set isWriter 0} $ThreadProgram
   220  thread_spawn finished(1) {set isWriter 1} $ThreadProgram
   221  if {![info exists finished(0)]} { vwait finished(0) }
   222  if {![info exists finished(1)]} { vwait finished(1) }
   223  
   224  catch { db close }
   225  catch { db2 close }
   226  
   227  do_test thread005-2.2 {
   228    list $finished(0) $finished(1)
   229  } {ok ok}
   230  
   231  do_test thread005-2.3 {
   232    sqlite3 db test.db
   233    execsql { ATTACH 'test2.db' AS aux }
   234    execsql { SELECT count(*) FROM t1 WHERE b IS NULL }
   235  } {0}
   236  
   237  sqlite3_enable_shared_cache $::enable_shared_cache
   238  finish_test