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.1.1.1 2012/02/21 17:04:16 misho 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
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>