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 file delete -force 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 239