blob: 6249ce11af09cd131813283d5ab12e6a83f08bdd [file] [log] [blame]
dand3f8f942010-04-13 11:35:01 +00001# 2010 April 13
dan7c246102010-04-12 19:00:29 +00002#
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#***********************************************************************
dand3f8f942010-04-13 11:35:01 +000011# This file implements regression tests for SQLite library. The
12# focus of this file is testing the operation of the library in
dancd11fb22010-04-26 10:40:52 +000013# "PRAGMA journal_mode=WAL" mode with multiple threads.
dan7c246102010-04-12 19:00:29 +000014#
15
16set testdir [file dirname $argv0]
17
18source $testdir/tester.tcl
dan5e0ce872010-04-28 17:48:44 +000019source $testdir/lock_common.tcl
dan7c246102010-04-12 19:00:29 +000020if {[run_thread_tests]==0} { finish_test ; return }
dan5cf53532010-05-01 16:40:20 +000021ifcapable !wal { finish_test ; return }
dan7c246102010-04-12 19:00:29 +000022
dancd11fb22010-04-26 10:40:52 +000023set sqlite_walsummary_mmap_incr 64
24
danb4e3a6f2010-04-29 08:47:28 +000025# How long, in seconds, to run each test for. If a test is set to run for
26# 0 seconds, it is omitted entirely.
dancd11fb22010-04-26 10:40:52 +000027#
drh529b1872010-05-03 17:24:45 +000028unset -nocomplain seconds
dan31c03902010-04-29 14:51:33 +000029set seconds(walthread-1) 20
30set seconds(walthread-2) 20
danb4e3a6f2010-04-29 08:47:28 +000031set seconds(walthread-3) 20
dan31c03902010-04-29 14:51:33 +000032set seconds(walthread-4) 20
danbb16ff72010-05-01 08:30:34 +000033set seconds(walthread-5) 1
dan5e0ce872010-04-28 17:48:44 +000034
35# The parameter is the name of a variable in the callers context. The
36# variable may or may not exist when this command is invoked.
37#
38# If the variable does exist, its value is returned. Otherwise, this
39# command uses [vwait] to wait until it is set, then returns the value.
40# In other words, this is a version of the [set VARNAME] command that
41# blocks until a variable exists.
42#
43proc wait_for_var {varname} {
44 if {0==[uplevel [list info exists $varname]]} {
45 uplevel [list vwait $varname]
dan7c246102010-04-12 19:00:29 +000046 }
dan5e0ce872010-04-28 17:48:44 +000047 uplevel [list set $varname]
48}
49
danb4e3a6f2010-04-29 08:47:28 +000050# The argument is the name of a list variable in the callers context. The
51# first element of the list is removed and returned. For example:
52#
53# set L {a b c}
54# set x [lshift L]
55# assert { $x == "a" && $L == "b c" }
56#
dan5e0ce872010-04-28 17:48:44 +000057proc lshift {lvar} {
58 upvar $lvar L
59 set ret [lindex $L 0]
60 set L [lrange $L 1 end]
61 return $ret
62}
63
64
65#-------------------------------------------------------------------------
66# do_thread_test TESTNAME OPTIONS...
67#
68# where OPTIONS are:
69#
70# -seconds SECONDS How many seconds to run the test for
71# -init SCRIPT Script to run before test.
72# -thread NAME COUNT SCRIPT Scripts to run in threads (or processes).
73# -processes BOOLEAN True to use processes instead of threads.
danb4e3a6f2010-04-29 08:47:28 +000074# -check SCRIPT Script to run after test.
dan5e0ce872010-04-28 17:48:44 +000075#
76proc do_thread_test {args} {
dane45d4422010-04-28 18:17:23 +000077
dan5e0ce872010-04-28 17:48:44 +000078 set A $args
79
80 set P(testname) [lshift A]
81 set P(seconds) 5
82 set P(init) ""
83 set P(threads) [list]
84 set P(processes) 0
danb4e3a6f2010-04-29 08:47:28 +000085 set P(check) {
86 set ic [db eval "PRAGMA integrity_check"]
87 if {$ic != "ok"} { error $ic }
88 }
dan5e0ce872010-04-28 17:48:44 +000089
90 unset -nocomplain ::done
91
92 while {[llength $A]>0} {
93 set a [lshift A]
94 switch -glob -- $a {
95 -seconds {
96 set P(seconds) [lshift A]
97 }
98
99 -init {
100 set P(init) [lshift A]
101 }
102
103 -processes {
104 set P(processes) [lshift A]
105 }
106
danb4e3a6f2010-04-29 08:47:28 +0000107 -check {
108 set P(check) [lshift A]
109 }
110
dan5e0ce872010-04-28 17:48:44 +0000111 -thread {
112 set name [lshift A]
113 set count [lshift A]
114 set prg [lshift A]
115 lappend P(threads) [list $name $count $prg]
116 }
117
118 default {
119 error "Unknown option: $a"
120 }
121 }
dan7c246102010-04-12 19:00:29 +0000122 }
dan5e0ce872010-04-28 17:48:44 +0000123
danb4e3a6f2010-04-29 08:47:28 +0000124 if {$P(seconds) == 0} {
125 puts "Skipping $P(testname)"
126 return
127 }
128
dan5e0ce872010-04-28 17:48:44 +0000129 puts "Running $P(testname) for $P(seconds) seconds..."
130
131 catch { db close }
mistachkinfda06be2011-08-02 00:57:34 +0000132 forcedelete test.db test.db-journal test.db-wal
dan5e0ce872010-04-28 17:48:44 +0000133
134 sqlite3 db test.db
135 eval $P(init)
danbb16ff72010-05-01 08:30:34 +0000136 catch { db close }
dan5e0ce872010-04-28 17:48:44 +0000137
138 foreach T $P(threads) {
139 set name [lindex $T 0]
140 set count [lindex $T 1]
141 set prg [lindex $T 2]
142
143 for {set i 1} {$i <= $count} {incr i} {
danb4e3a6f2010-04-29 08:47:28 +0000144 set vars "
145 set E(pid) $i
146 set E(nthread) $count
147 set E(seconds) $P(seconds)
148 "
149 set program [string map [list %TEST% $prg %VARS% $vars] {
dan5e0ce872010-04-28 17:48:44 +0000150
danb4e3a6f2010-04-29 08:47:28 +0000151 %VARS%
dan5e0ce872010-04-28 17:48:44 +0000152
153 proc usleep {ms} {
154 set ::usleep 0
155 after $ms {set ::usleep 1}
156 vwait ::usleep
157 }
danb4e3a6f2010-04-29 08:47:28 +0000158
159 proc integrity_check {{db db}} {
160 set ic [$db eval {PRAGMA integrity_check}]
161 if {$ic != "ok"} {error $ic}
162 }
163
dan5e0ce872010-04-28 17:48:44 +0000164 proc busyhandler {n} { usleep 10 ; return 0 }
165
166 sqlite3 db test.db
167 db busy busyhandler
danb4e3a6f2010-04-29 08:47:28 +0000168 db eval { SELECT randomblob($E(pid)*5) }
dan5e0ce872010-04-28 17:48:44 +0000169
170 set ::finished 0
danb4e3a6f2010-04-29 08:47:28 +0000171 after [expr $E(seconds) * 1000] {set ::finished 1}
dane45d4422010-04-28 18:17:23 +0000172 proc tt_continue {} { update ; expr ($::finished==0) }
dan5e0ce872010-04-28 17:48:44 +0000173
174 set rc [catch { %TEST% } msg]
175
dane45d4422010-04-28 18:17:23 +0000176 catch { db close }
dan5e0ce872010-04-28 17:48:44 +0000177 list $rc $msg
178 }]
179
180 if {$P(processes)==0} {
181 sqlthread spawn ::done($name,$i) $program
182 } else {
183 testfixture_nb ::done($name,$i) $program
184 }
185 }
186 }
187
188 set report " Results:"
189 foreach T $P(threads) {
190 set name [lindex $T 0]
191 set count [lindex $T 1]
192 set prg [lindex $T 2]
193
194 set reslist [list]
195 for {set i 1} {$i <= $count} {incr i} {
196 set res [wait_for_var ::done($name,$i)]
197 lappend reslist [lindex $res 1]
198 do_test $P(testname).$name.$i [list lindex $res 0] 0
199 }
200
201 append report " $name $reslist"
202 }
203 puts $report
danb4e3a6f2010-04-29 08:47:28 +0000204
205 sqlite3 db test.db
206 set res ""
207 if {[catch $P(check) msg]} { set res $msg }
208 do_test $P(testname).check [list set {} $res] ""
dan5e0ce872010-04-28 17:48:44 +0000209}
dan7c246102010-04-12 19:00:29 +0000210
danb4e3a6f2010-04-29 08:47:28 +0000211# A wrapper around [do_thread_test] which runs the specified test twice.
212# Once using processes, once using threads. This command takes the same
213# arguments as [do_thread_test], except specifying the -processes switch
214# is illegal.
215#
216proc do_thread_test2 {args} {
217 set name [lindex $args 0]
218 if {[lsearch $args -processes]>=0} { error "bad option: -processes"}
219 uplevel [lreplace $args 0 0 do_thread_test "$name-threads" -processes 0]
220 uplevel [lreplace $args 0 0 do_thread_test "$name-processes" -processes 1]
221}
222
dan7c246102010-04-12 19:00:29 +0000223#--------------------------------------------------------------------------
danb4e3a6f2010-04-29 08:47:28 +0000224# Start 10 threads. Each thread performs both read and write
dan5e0ce872010-04-28 17:48:44 +0000225# transactions. Each read transaction consists of:
dan7c246102010-04-12 19:00:29 +0000226#
227# 1) Reading the md5sum of all but the last table row,
228# 2) Running integrity check.
229# 3) Reading the value stored in the last table row,
230# 4) Check that the values read in steps 1 and 3 are the same, and that
231# the md5sum of all but the last table row has not changed.
232#
233# Each write transaction consists of:
234#
235# 1) Modifying the contents of t1 (inserting, updating, deleting rows).
236# 2) Appending a new row to the table containing the md5sum() of all
237# rows in the table.
238#
239# Each of the N threads runs N read transactions followed by a single write
240# transaction in a loop as fast as possible.
241#
dane45d4422010-04-28 18:17:23 +0000242# There is also a single checkpointer thread. It runs the following loop:
dan7c246102010-04-12 19:00:29 +0000243#
dan5a299f92010-05-03 11:05:08 +0000244# 1) Execute "PRAGMA wal_checkpoint"
dan7c246102010-04-12 19:00:29 +0000245# 2) Sleep for 500 ms.
246#
danb4e3a6f2010-04-29 08:47:28 +0000247do_thread_test2 walthread-1 -seconds $seconds(walthread-1) -init {
248 execsql {
249 PRAGMA journal_mode = WAL;
250 CREATE TABLE t1(x PRIMARY KEY);
251 PRAGMA lock_status;
252 INSERT INTO t1 VALUES(randomblob(100));
253 INSERT INTO t1 VALUES(randomblob(100));
254 INSERT INTO t1 SELECT md5sum(x) FROM t1;
dan7c246102010-04-12 19:00:29 +0000255 }
danb4e3a6f2010-04-29 08:47:28 +0000256} -thread main 10 {
257
258 proc read_transaction {} {
259 set results [db eval {
260 BEGIN;
261 PRAGMA integrity_check;
262 SELECT md5sum(x) FROM t1 WHERE rowid != (SELECT max(rowid) FROM t1);
263 SELECT x FROM t1 WHERE rowid = (SELECT max(rowid) FROM t1);
264 SELECT md5sum(x) FROM t1 WHERE rowid != (SELECT max(rowid) FROM t1);
265 COMMIT;
266 }]
267
268 if {[llength $results]!=4
269 || [lindex $results 0] != "ok"
270 || [lindex $results 1] != [lindex $results 2]
271 || [lindex $results 2] != [lindex $results 3]
272 } {
273 error "Failed read transaction: $results"
274 }
275 }
276
277 proc write_transaction {} {
278 db eval {
279 BEGIN;
dan809badc2012-08-07 13:55:41 +0000280 INSERT INTO t1 VALUES(randomblob(101 + $::E(pid)));
281 INSERT INTO t1 VALUES(randomblob(101 + $::E(pid)));
danb4e3a6f2010-04-29 08:47:28 +0000282 INSERT INTO t1 SELECT md5sum(x) FROM t1;
283 COMMIT;
284 }
285 }
286
danf9b76712010-06-01 14:12:45 +0000287 # Turn off auto-checkpoint. Otherwise, an auto-checkpoint run by a
288 # writer may cause the dedicated checkpoint thread to return an
289 # SQLITE_BUSY error.
290 #
291 db eval { PRAGMA wal_autocheckpoint = 0 }
292
danb4e3a6f2010-04-29 08:47:28 +0000293 set nRun 0
294 while {[tt_continue]} {
295 read_transaction
296 write_transaction
297 incr nRun
298 }
299 set nRun
300
301} -thread ckpt 1 {
302 set nRun 0
303 while {[tt_continue]} {
dan5a299f92010-05-03 11:05:08 +0000304 db eval "PRAGMA wal_checkpoint"
danb4e3a6f2010-04-29 08:47:28 +0000305 usleep 500
306 incr nRun
307 }
308 set nRun
dan7c246102010-04-12 19:00:29 +0000309}
310
dane45d4422010-04-28 18:17:23 +0000311#--------------------------------------------------------------------------
danb4e3a6f2010-04-29 08:47:28 +0000312# This test has clients run the following procedure as fast as possible
313# in a loop:
dane45d4422010-04-28 18:17:23 +0000314#
danb4e3a6f2010-04-29 08:47:28 +0000315# 1. Open a database handle.
316# 2. Execute a read-only transaction on the db.
317# 3. Do "PRAGMA journal_mode = XXX", where XXX is one of WAL or DELETE.
318# Ignore any SQLITE_BUSY error.
319# 4. Execute a write transaction to insert a row into the db.
320# 5. Run "PRAGMA integrity_check"
321#
322# At present, there are 4 clients in total. 2 do "journal_mode = WAL", and
323# two do "journal_mode = DELETE".
324#
325# Each client returns a string of the form "W w, R r", where W is the
326# number of write-transactions performed using a WAL journal, and D is
327# the number of write-transactions performed using a rollback journal.
328# For example, "192 w, 185 r".
329#
330do_thread_test2 walthread-2 -seconds $seconds(walthread-2) -init {
331 execsql { CREATE TABLE t1(x INTEGER PRIMARY KEY, y UNIQUE) }
332} -thread RB 2 {
dane45d4422010-04-28 18:17:23 +0000333
danb4e3a6f2010-04-29 08:47:28 +0000334 db close
335 set nRun 0
336 set nDel 0
337 while {[tt_continue]} {
338 sqlite3 db test.db
339 db busy busyhandler
340 db eval { SELECT * FROM sqlite_master }
341 catch { db eval { PRAGMA journal_mode = DELETE } }
342 db eval {
343 BEGIN;
344 INSERT INTO t1 VALUES(NULL, randomblob(100+$E(pid)));
dane45d4422010-04-28 18:17:23 +0000345 }
danb4e3a6f2010-04-29 08:47:28 +0000346 incr nRun 1
347 incr nDel [file exists test.db-journal]
348 if {[file exists test.db-journal] + [file exists test.db-wal] != 1} {
349 error "File-system looks bad..."
dane45d4422010-04-28 18:17:23 +0000350 }
danb4e3a6f2010-04-29 08:47:28 +0000351 db eval COMMIT
352
353 integrity_check
354 db close
355 }
356 list $nRun $nDel
357 set {} "[expr $nRun-$nDel] w, $nDel r"
358
359} -thread WAL 2 {
360 db close
361 set nRun 0
362 set nDel 0
363 while {[tt_continue]} {
364 sqlite3 db test.db
365 db busy busyhandler
366 db eval { SELECT * FROM sqlite_master }
367 catch { db eval { PRAGMA journal_mode = WAL } }
368 db eval {
369 BEGIN;
370 INSERT INTO t1 VALUES(NULL, randomblob(110+$E(pid)));
371 }
372 incr nRun 1
373 incr nDel [file exists test.db-journal]
374 if {[file exists test.db-journal] + [file exists test.db-wal] != 1} {
375 error "File-system looks bad..."
376 }
377 db eval COMMIT
378
379 integrity_check
380 db close
381 }
382 set {} "[expr $nRun-$nDel] w, $nDel r"
383}
384
dan31c03902010-04-29 14:51:33 +0000385do_thread_test walthread-3 -seconds $seconds(walthread-3) -init {
danb4e3a6f2010-04-29 08:47:28 +0000386 execsql {
387 PRAGMA journal_mode = WAL;
388 CREATE TABLE t1(cnt PRIMARY KEY, sum1, sum2);
389 CREATE INDEX i1 ON t1(sum1);
390 CREATE INDEX i2 ON t1(sum2);
391 INSERT INTO t1 VALUES(0, 0, 0);
392 }
393} -thread t 10 {
394
395 set nextwrite $E(pid)
396
397 proc wal_hook {zDb nEntry} {
danf9b76712010-06-01 14:12:45 +0000398 if {$nEntry>10} {
399 set rc [catch { db eval {PRAGMA wal_checkpoint} } msg]
400 if {$rc && $msg != "database is locked"} { error $msg }
401 }
danb4e3a6f2010-04-29 08:47:28 +0000402 return 0
403 }
404 db wal_hook wal_hook
405
406 while {[tt_continue]} {
407 set max 0
408 while { $max != ($nextwrite-1) && [tt_continue] } {
409 set max [db eval { SELECT max(cnt) FROM t1 }]
410 }
411
412 if {[tt_continue]} {
413 set sum1 [db eval { SELECT sum(cnt) FROM t1 }]
414 set sum2 [db eval { SELECT sum(sum1) FROM t1 }]
415 db eval { INSERT INTO t1 VALUES($nextwrite, $sum1, $sum2) }
416 incr nextwrite $E(nthread)
417 integrity_check
418 }
419 }
420
421 set {} ok
422} -check {
423 puts " Final db contains [db eval {SELECT count(*) FROM t1}] rows"
424 puts " Final integrity-check says: [db eval {PRAGMA integrity_check}]"
425
426 # Check that the contents of the database are Ok.
427 set c 0
428 set s1 0
429 set s2 0
430 db eval { SELECT cnt, sum1, sum2 FROM t1 ORDER BY cnt } {
431 if {$c != $cnt || $s1 != $sum1 || $s2 != $sum2} {
432 error "database content is invalid"
433 }
434 incr s2 $s1
435 incr s1 $c
436 incr c 1
dane45d4422010-04-28 18:17:23 +0000437 }
438}
439
dan31c03902010-04-29 14:51:33 +0000440do_thread_test2 walthread-4 -seconds $seconds(walthread-4) -init {
441 execsql {
442 PRAGMA journal_mode = WAL;
443 CREATE TABLE t1(a INTEGER PRIMARY KEY, b UNIQUE);
444 }
445} -thread r 1 {
446 # This connection only ever reads the database. Therefore the
447 # busy-handler is not required. Disable it to check that this is true.
danfa408ad2010-05-25 15:23:51 +0000448 #
449 # UPDATE: That is no longer entirely true - as we don't use a blocking
450 # lock to enter RECOVER state. Which means there is a small chance a
451 # reader can see an SQLITE_BUSY.
452 #
453 while {[tt_continue]} {
454 integrity_check
455 }
dan31c03902010-04-29 14:51:33 +0000456 set {} ok
457} -thread w 1 {
458
459 proc wal_hook {zDb nEntry} {
drh5def0842010-05-05 20:00:25 +0000460 if {$nEntry>15} {db eval {PRAGMA wal_checkpoint}}
dan31c03902010-04-29 14:51:33 +0000461 return 0
462 }
463 db wal_hook wal_hook
464 set row 1
465 while {[tt_continue]} {
466 db eval { REPLACE INTO t1 VALUES($row, randomblob(300)) }
467 incr row
468 if {$row == 10} { set row 1 }
469 }
470
471 set {} ok
472}
473
danbb16ff72010-05-01 08:30:34 +0000474
475# This test case attempts to provoke a deadlock condition that existed in
476# the unix VFS at one point. The problem occurred only while recovering a
477# very large wal file (one that requires a wal-index larger than the
478# initial default allocation of 64KB).
479#
danbb16ff72010-05-01 08:30:34 +0000480do_thread_test walthread-5 -seconds $seconds(walthread-5) -init {
481
482 proc log_file_size {nFrame pgsz} {
483 expr {12 + ($pgsz+16)*$nFrame}
484 }
485
486 execsql {
487 PRAGMA page_size = 1024;
488 PRAGMA journal_mode = WAL;
489 CREATE TABLE t1(x);
490 BEGIN;
491 INSERT INTO t1 VALUES(randomblob(900));
492 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 2 */
493 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 4 */
494 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 8 */
495 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 16 */
496 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 32 */
497 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 64 */
498 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 128 */
499 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 256 */
500 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 512 */
501 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 1024 */
502 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 2048 */
503 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 4096 */
504 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 8192 */
505 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 16384 */
506 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 32768 */
507 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 65536 */
508 COMMIT;
509 }
510
mistachkinfda06be2011-08-02 00:57:34 +0000511 forcecopy test.db-wal bak.db-wal
512 forcecopy test.db bak.db
danbb16ff72010-05-01 08:30:34 +0000513 db close
514
mistachkinfda06be2011-08-02 00:57:34 +0000515 forcecopy bak.db-wal test.db-wal
516 forcecopy bak.db test.db
danbb16ff72010-05-01 08:30:34 +0000517
518 if {[file size test.db-wal] < [log_file_size [expr 64*1024] 1024]} {
519 error "Somehow failed to create a large log file"
520 }
521 puts "Database with large log file recovered. Now running clients..."
522} -thread T 5 {
523 db eval { SELECT count(*) FROM t1 }
524}
drhd0b26772010-05-03 19:20:46 +0000525unset -nocomplain seconds
danbb16ff72010-05-01 08:30:34 +0000526
dan7c246102010-04-12 19:00:29 +0000527finish_test