# 2010 April 13
#
# The author disclaims copyright to this source code. In place of
# a legal notice, here is a blessing:
#
# May you do good and not evil.
# May you find forgiveness for yourself and forgive others.
# May you share freely, never taking more than you give.
#
#***********************************************************************
# This file implements regression tests for SQLite library. The
# focus of this file is testing the operation of the library in
# "PRAGMA journal_mode=WAL" mode with multiple threads.
#
set testdir [file dirname $argv0]
source $testdir/tester.tcl
source $testdir/lock_common.tcl
if {[run_thread_tests]==0} { finish_test ; return }
ifcapable !wal { finish_test ; return }
set sqlite_walsummary_mmap_incr 64
# How long, in seconds, to run each test for. If a test is set to run for
# 0 seconds, it is omitted entirely.
#
unset -nocomplain seconds
set seconds(walthread-1) 20
set seconds(walthread-2) 20
set seconds(walthread-3) 20
set seconds(walthread-4) 20
set seconds(walthread-5) 1
# The parameter is the name of a variable in the callers context. The
# variable may or may not exist when this command is invoked.
#
# If the variable does exist, its value is returned. Otherwise, this
# command uses [vwait] to wait until it is set, then returns the value.
# In other words, this is a version of the [set VARNAME] command that
# blocks until a variable exists.
#
proc wait_for_var {varname} {
if {0==[uplevel [list info exists $varname]]} {
uplevel [list vwait $varname]
}
uplevel [list set $varname]
}
# The argument is the name of a list variable in the callers context. The
# first element of the list is removed and returned. For example:
#
# set L {a b c}
# set x [lshift L]
# assert { $x == "a" && $L == "b c" }
#
proc lshift {lvar} {
upvar $lvar L
set ret [lindex $L 0]
set L [lrange $L 1 end]
return $ret
}
#-------------------------------------------------------------------------
# do_thread_test TESTNAME OPTIONS...
#
# where OPTIONS are:
#
# -seconds SECONDS How many seconds to run the test for
# -init SCRIPT Script to run before test.
# -thread NAME COUNT SCRIPT Scripts to run in threads (or processes).
# -processes BOOLEAN True to use processes instead of threads.
# -check SCRIPT Script to run after test.
#
proc do_thread_test {args} {
set A $args
set P(testname) [lshift A]
set P(seconds) 5
set P(init) ""
set P(threads) [list]
set P(processes) 0
set P(check) {
set ic [db eval "PRAGMA integrity_check"]
if {$ic != "ok"} { error $ic }
}
unset -nocomplain ::done
while {[llength $A]>0} {
set a [lshift A]
switch -glob -- $a {
-seconds {
set P(seconds) [lshift A]
}
-init {
set P(init) [lshift A]
}
-processes {
set P(processes) [lshift A]
}
-check {
set P(check) [lshift A]
}
-thread {
set name [lshift A]
set count [lshift A]
set prg [lshift A]
lappend P(threads) [list $name $count $prg]
}
default {
error "Unknown option: $a"
}
}
}
if {$P(seconds) == 0} {
puts "Skipping $P(testname)"
return
}
puts "Running $P(testname) for $P(seconds) seconds..."
catch { db close }
forcedelete test.db test.db-journal test.db-wal
sqlite3 db test.db
eval $P(init)
catch { db close }
foreach T $P(threads) {
set name [lindex $T 0]
set count [lindex $T 1]
set prg [lindex $T 2]
for {set i 1} {$i <= $count} {incr i} {
set vars "
set E(pid) $i
set E(nthread) $count
set E(seconds) $P(seconds)
"
set program [string map [list %TEST% $prg %VARS% $vars] {
%VARS%
proc usleep {ms} {
set ::usleep 0
after $ms {set ::usleep 1}
vwait ::usleep
}
proc integrity_check {{db db}} {
set ic [$db eval {PRAGMA integrity_check}]
if {$ic != "ok"} {error $ic}
}
proc busyhandler {n} { usleep 10 ; return 0 }
sqlite3 db test.db
db busy busyhandler
db eval { SELECT randomblob($E(pid)*5) }
set ::finished 0
after [expr $E(seconds) * 1000] {set ::finished 1}
proc tt_continue {} { update ; expr ($::finished==0) }
set rc [catch { %TEST% } msg]
catch { db close }
list $rc $msg
}]
if {$P(processes)==0} {
sqlthread spawn ::done($name,$i) $program
} else {
testfixture_nb ::done($name,$i) $program
}
}
}
set report " Results:"
foreach T $P(threads) {
set name [lindex $T 0]
set count [lindex $T 1]
set prg [lindex $T 2]
set reslist [list]
for {set i 1} {$i <= $count} {incr i} {
set res [wait_for_var ::done($name,$i)]
lappend reslist [lindex $res 1]
do_test $P(testname).$name.$i [list lindex $res 0] 0
}
append report " $name $reslist"
}
puts $report
sqlite3 db test.db
set res ""
if {[catch $P(check) msg]} { set res $msg }
do_test $P(testname).check [list set {} $res] ""
}
# A wrapper around [do_thread_test] which runs the specified test twice.
# Once using processes, once using threads. This command takes the same
# arguments as [do_thread_test], except specifying the -processes switch
# is illegal.
#
proc do_thread_test2 {args} {
set name [lindex $args 0]
if {[lsearch $args -processes]>=0} { error "bad option: -processes"}
uplevel [lreplace $args 0 0 do_thread_test "$name-threads" -processes 0]
uplevel [lreplace $args 0 0 do_thread_test "$name-processes" -processes 1]
}
#--------------------------------------------------------------------------
# Start 10 threads. Each thread performs both read and write
# transactions. Each read transaction consists of:
#
# 1) Reading the md5sum of all but the last table row,
# 2) Running integrity check.
# 3) Reading the value stored in the last table row,
# 4) Check that the values read in steps 1 and 3 are the same, and that
# the md5sum of all but the last table row has not changed.
#
# Each write transaction consists of:
#
# 1) Modifying the contents of t1 (inserting, updating, deleting rows).
# 2) Appending a new row to the table containing the md5sum() of all
# rows in the table.
#
# Each of the N threads runs N read transactions followed by a single write
# transaction in a loop as fast as possible.
#
# There is also a single checkpointer thread. It runs the following loop:
#
# 1) Execute "PRAGMA wal_checkpoint"
# 2) Sleep for 500 ms.
#
do_thread_test2 walthread-1 -seconds $seconds(walthread-1) -init {
execsql {
PRAGMA journal_mode = WAL;
CREATE TABLE t1(x PRIMARY KEY);
PRAGMA lock_status;
INSERT INTO t1 VALUES(randomblob(100));
INSERT INTO t1 VALUES(randomblob(100));
INSERT INTO t1 SELECT md5sum(x) FROM t1;
}
} -thread main 10 {
proc read_transaction {} {
set results [db eval {
BEGIN;
PRAGMA integrity_check;
SELECT md5sum(x) FROM t1 WHERE rowid != (SELECT max(rowid) FROM t1);
SELECT x FROM t1 WHERE rowid = (SELECT max(rowid) FROM t1);
SELECT md5sum(x) FROM t1 WHERE rowid != (SELECT max(rowid) FROM t1);
COMMIT;
}]
if {[llength $results]!=4
|| [lindex $results 0] != "ok"
|| [lindex $results 1] != [lindex $results 2]
|| [lindex $results 2] != [lindex $results 3]
} {
error "Failed read transaction: $results"
}
}
proc write_transaction {} {
db eval {
BEGIN;
INSERT INTO t1 VALUES(randomblob(100));
INSERT INTO t1 VALUES(randomblob(100));
INSERT INTO t1 SELECT md5sum(x) FROM t1;
COMMIT;
}
}
# Turn off auto-checkpoint. Otherwise, an auto-checkpoint run by a
# writer may cause the dedicated checkpoint thread to return an
# SQLITE_BUSY error.
#
db eval { PRAGMA wal_autocheckpoint = 0 }
set nRun 0
while {[tt_continue]} {
read_transaction
write_transaction
incr nRun
}
set nRun
} -thread ckpt 1 {
set nRun 0
while {[tt_continue]} {
db eval "PRAGMA wal_checkpoint"
usleep 500
incr nRun
}
set nRun
}
#--------------------------------------------------------------------------
# This test has clients run the following procedure as fast as possible
# in a loop:
#
# 1. Open a database handle.
# 2. Execute a read-only transaction on the db.
# 3. Do "PRAGMA journal_mode = XXX", where XXX is one of WAL or DELETE.
# Ignore any SQLITE_BUSY error.
# 4. Execute a write transaction to insert a row into the db.
# 5. Run "PRAGMA integrity_check"
#
# At present, there are 4 clients in total. 2 do "journal_mode = WAL", and
# two do "journal_mode = DELETE".
#
# Each client returns a string of the form "W w, R r", where W is the
# number of write-transactions performed using a WAL journal, and D is
# the number of write-transactions performed using a rollback journal.
# For example, "192 w, 185 r".
#
do_thread_test2 walthread-2 -seconds $seconds(walthread-2) -init {
execsql { CREATE TABLE t1(x INTEGER PRIMARY KEY, y UNIQUE) }
} -thread RB 2 {
db close
set nRun 0
set nDel 0
while {[tt_continue]} {
sqlite3 db test.db
db busy busyhandler
db eval { SELECT * FROM sqlite_master }
catch { db eval { PRAGMA journal_mode = DELETE } }
db eval {
BEGIN;
INSERT INTO t1 VALUES(NULL, randomblob(100+$E(pid)));
}
incr nRun 1
incr nDel [file exists test.db-journal]
if {[file exists test.db-journal] + [file exists test.db-wal] != 1} {
error "File-system looks bad..."
}
db eval COMMIT
integrity_check
db close
}
list $nRun $nDel
set {} "[expr $nRun-$nDel] w, $nDel r"
} -thread WAL 2 {
db close
set nRun 0
set nDel 0
while {[tt_continue]} {
sqlite3 db test.db
db busy busyhandler
db eval { SELECT * FROM sqlite_master }
catch { db eval { PRAGMA journal_mode = WAL } }
db eval {
BEGIN;
INSERT INTO t1 VALUES(NULL, randomblob(110+$E(pid)));
}
incr nRun 1
incr nDel [file exists test.db-journal]
if {[file exists test.db-journal] + [file exists test.db-wal] != 1} {
error "File-system looks bad..."
}
db eval COMMIT
integrity_check
db close
}
set {} "[expr $nRun-$nDel] w, $nDel r"
}
do_thread_test walthread-3 -seconds $seconds(walthread-3) -init {
execsql {
PRAGMA journal_mode = WAL;
CREATE TABLE t1(cnt PRIMARY KEY, sum1, sum2);
CREATE INDEX i1 ON t1(sum1);
CREATE INDEX i2 ON t1(sum2);
INSERT INTO t1 VALUES(0, 0, 0);
}
} -thread t 10 {
set nextwrite $E(pid)
proc wal_hook {zDb nEntry} {
if {$nEntry>10} {
set rc [catch { db eval {PRAGMA wal_checkpoint} } msg]
if {$rc && $msg != "database is locked"} { error $msg }
}
return 0
}
db wal_hook wal_hook
while {[tt_continue]} {
set max 0
while { $max != ($nextwrite-1) && [tt_continue] } {
set max [db eval { SELECT max(cnt) FROM t1 }]
}
if {[tt_continue]} {
set sum1 [db eval { SELECT sum(cnt) FROM t1 }]
set sum2 [db eval { SELECT sum(sum1) FROM t1 }]
db eval { INSERT INTO t1 VALUES($nextwrite, $sum1, $sum2) }
incr nextwrite $E(nthread)
integrity_check
}
}
set {} ok
} -check {
puts " Final db contains [db eval {SELECT count(*) FROM t1}] rows"
puts " Final integrity-check says: [db eval {PRAGMA integrity_check}]"
# Check that the contents of the database are Ok.
set c 0
set s1 0
set s2 0
db eval { SELECT cnt, sum1, sum2 FROM t1 ORDER BY cnt } {
if {$c != $cnt || $s1 != $sum1 || $s2 != $sum2} {
error "database content is invalid"
}
incr s2 $s1
incr s1 $c
incr c 1
}
}
do_thread_test2 walthread-4 -seconds $seconds(walthread-4) -init {
execsql {
PRAGMA journal_mode = WAL;
CREATE TABLE t1(a INTEGER PRIMARY KEY, b UNIQUE);
}
} -thread r 1 {
# This connection only ever reads the database. Therefore the
# busy-handler is not required. Disable it to check that this is true.
#
# UPDATE: That is no longer entirely true - as we don't use a blocking
# lock to enter RECOVER state. Which means there is a small chance a
# reader can see an SQLITE_BUSY.
#
while {[tt_continue]} {
integrity_check
}
set {} ok
} -thread w 1 {
proc wal_hook {zDb nEntry} {
if {$nEntry>15} {db eval {PRAGMA wal_checkpoint}}
return 0
}
db wal_hook wal_hook
set row 1
while {[tt_continue]} {
db eval { REPLACE INTO t1 VALUES($row, randomblob(300)) }
incr row
if {$row == 10} { set row 1 }
}
set {} ok
}
# This test case attempts to provoke a deadlock condition that existed in
# the unix VFS at one point. The problem occurred only while recovering a
# very large wal file (one that requires a wal-index larger than the
# initial default allocation of 64KB).
#
do_thread_test walthread-5 -seconds $seconds(walthread-5) -init {
proc log_file_size {nFrame pgsz} {
expr {12 + ($pgsz+16)*$nFrame}
}
execsql {
PRAGMA page_size = 1024;
PRAGMA journal_mode = WAL;
CREATE TABLE t1(x);
BEGIN;
INSERT INTO t1 VALUES(randomblob(900));
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 2 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 4 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 8 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 16 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 32 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 64 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 128 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 256 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 512 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 1024 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 2048 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 4096 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 8192 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 16384 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 32768 */
INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 65536 */
COMMIT;
}
forcecopy test.db-wal bak.db-wal
forcecopy test.db bak.db
db close
forcecopy bak.db-wal test.db-wal
forcecopy bak.db test.db
if {[file size test.db-wal] < [log_file_size [expr 64*1024] 1024]} {
error "Somehow failed to create a large log file"
}
puts "Database with large log file recovered. Now running clients..."
} -thread T 5 {
db eval { SELECT count(*) FROM t1 }
}
unset -nocomplain seconds
finish_test
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>