Index: src/test_vfs.c ================================================================== --- src/test_vfs.c +++ src/test_vfs.c @@ -28,11 +28,15 @@ sqlite3_file base; /* Base class. Must be first */ sqlite3_vfs *pVfs; /* The VFS */ const char *zFilename; /* Filename as passed to xOpen() */ sqlite3_file *pReal; /* The real, underlying file descriptor */ Tcl_Obj *pShmId; /* Shared memory id for Tcl callbacks */ + TestvfsBuffer *pShm; /* Shared memory buffer */ + u32 excllock; /* Mask of exclusive locks */ + u32 sharedlock; /* Mask of shared locks */ + TestvfsFile *pNext; /* Next handle opened on the same file */ }; /* ** An instance of this structure is allocated for each VFS created. The @@ -75,17 +79,19 @@ #define TESTVFS_OPEN_MASK 0x00000080 #define TESTVFS_SYNC_MASK 0x00000100 #define TESTVFS_ALL_MASK 0x000001FF /* -** A shared-memory buffer. +** A shared-memory buffer. There is one of these objects for each shared +** memory region opened by clients. If two clients open the same file, +** there are two TestvfsFile structures but only one TestvfsBuffer structure. */ struct TestvfsBuffer { char *zFile; /* Associated file name */ int n; /* Size of allocated buffer in bytes */ u8 *a; /* Buffer allocated using ckalloc() */ - int nRef; /* Number of references to this object */ + TestvfsFile *pFile; /* List of open handles */ TestvfsBuffer *pNext; /* Next in linked list of all buffers */ }; #define PARENTVFS(x) (((Testvfs *)((x)->pAppData))->pParent) @@ -571,11 +577,11 @@ TestvfsBuffer *pBuffer; /* Buffer to open connection to */ TestvfsFile *pFd; /* The testvfs file structure */ pFd = (TestvfsFile*)pFileDes; p = (Testvfs *)pFd->pVfs->pAppData; - assert( pFd->pShmId && pFd->pShm==0 ); + assert( pFd->pShmId && pFd->pShm==0 && pFd->pNext==0 ); /* Evaluate the Tcl script: ** ** SCRIPT xShmOpen FILENAME */ @@ -605,11 +611,12 @@ pBuffer->pNext = p->pBuffer; p->pBuffer = pBuffer; } /* Connect the TestvfsBuffer to the new TestvfsShm handle and return. */ - pBuffer->nRef++; + pFd->pNext = pBuffer->pFile; + pBuffer->pFile = pFd; pFd->pShm = pBuffer; return SQLITE_OK; } static int tvfsShmSize( @@ -711,10 +718,34 @@ } if( rc==SQLITE_OK && p->mask&TESTVFS_SHMLOCK_MASK && tvfsInjectIoerr(p) ){ rc = SQLITE_IOERR; } + + if( rc==SQLITE_OK ){ + int isLock = (flags & SQLITE_SHM_LOCK); + int isExcl = (flags & SQLITE_SHM_EXCLUSIVE); + u32 mask = (((1<pShm->pFile; p2; p2=p2->pNext){ + if( p2==pFd ) continue; + if( (p2->excllock&mask) || (isExcl && p2->sharedlock&mask) ){ + rc = SQLITE_BUSY; + break; + } + } + if( rc==SQLITE_OK ){ + if( isExcl ) pFd->excllock |= mask; + if( !isExcl ) pFd->sharedlock |= mask; + } + }else{ + if( isExcl ) pFd->excllock &= (~mask); + if( !isExcl ) pFd->sharedlock &= (~mask); + } + } + return rc; } static void tvfsShmBarrier(sqlite3_file *pFile){ TestvfsFile *pFd = (TestvfsFile *)pFile; @@ -733,25 +764,26 @@ ){ int rc = SQLITE_OK; TestvfsFile *pFd = (TestvfsFile *)pFile; Testvfs *p = (Testvfs *)(pFd->pVfs->pAppData); TestvfsBuffer *pBuffer = pFd->pShm; + TestvfsFile **ppFd; assert( pFd->pShmId && pFd->pShm ); -#if 0 - assert( (deleteFlag!=0)==(pBuffer->nRef==1) ); -#endif if( p->pScript && p->mask&TESTVFS_SHMCLOSE_MASK ){ tvfsExecTcl(p, "xShmClose", Tcl_NewStringObj(pFd->pShm->zFile, -1), pFd->pShmId, 0 ); tvfsResultCode(p, &rc); } - pBuffer->nRef--; - if( pBuffer->nRef==0 ){ + for(ppFd=&pBuffer->pFile; *ppFd!=pFd; ppFd=&((*ppFd)->pNext)); + assert( (*ppFd)==pFd ); + *ppFd = pFd->pNext; + + if( pBuffer->pFile==0 ){ TestvfsBuffer **pp; for(pp=&p->pBuffer; *pp!=pBuffer; pp=&((*pp)->pNext)); *pp = (*pp)->pNext; ckfree((char *)pBuffer->a); ckfree((char *)pBuffer); Index: test/wal3.test ================================================================== --- test/wal3.test +++ test/wal3.test @@ -314,12 +314,112 @@ db close set ::locks [list] sqlite3 db test.db -vfs T catchsql { SELECT * FROM x } } {1 {locking protocol}} +db close +T delete + + +#------------------------------------------------------------------------- +# Only one client may run recovery at a time. Test this mechanism. +# +# When client-2 tries to open a read transaction while client-1 is +# running recovery, it fails to obtain a lock on an aReadMark[] slot +# (because they are all locked by recovery). It then tries to obtain +# a shared lock on the RECOVER lock to see if there really is a +# recovery running or not. +# +# This block of tests checks the effect of an SQLITE_BUSY or SQLITE_IOERR +# being returned when client-2 attempts a shared lock on the RECOVER byte. +# +# An SQLITE_BUSY should be converted to an SQLITE_BUSY_RECOVERY. An +# SQLITE_IOERR should be returned to the caller. +# +do_test wal3-5.1 { + faultsim_delete_and_reopen + execsql { + PRAGMA journal_mode = WAL; + CREATE TABLE t1(a, b); + INSERT INTO t1 VALUES(1, 2); + INSERT INTO t1 VALUES(3, 4); + } + faultsim_save_and_close +} {} + +testvfs T -default 1 +T script method_callback + +proc method_callback {method args} { + if {$method == "xShmBarrier"} { + incr ::barrier_count + if {$::barrier_count == 1} { + # This code is executed within the xShmBarrier() callback invoked + # by the client running recovery as part of writing the recovered + # wal-index header. If a second client attempts to access the + # database now, it reads a corrupt (partially written) wal-index + # header. But it cannot even get that far, as the first client + # is still holding all the locks (recovery takes an exclusive lock + # on *all* db locks, preventing access by any other client). + # + # If global variable ::wal3_do_lockfailure is non-zero, then set + # things up so that an IO error occurs within an xShmLock() callback + # made by the second client (aka [db2]). + # + sqlite3 db2 test.db + if { $::wal3_do_lockfailure } { T filter xShmLock } + set ::testrc [ catch { db2 eval "SELECT * FROM t1" } ::testmsg ] + T filter {} + db2 close + } + } + + if {$method == "xShmLock"} { + foreach {file handle spec} $args break + if { $spec == "2 1 lock shared" } { + return SQLITE_IOERR + } + } + + return SQLITE_OK +} + +# Test a normal SQLITE_BUSY return. +# +T filter xShmBarrier +set testrc "" +set testmsg "" +set barrier_count 0 +set wal3_do_lockfailure 0 +do_test wal3-5.2 { + faultsim_restore_and_reopen + execsql { SELECT * FROM t1 } +} {1 2 3 4} +do_test wal3-5.3 { + list $::testrc $::testmsg +} {1 {database is locked}} +db close + +# Test an SQLITE_IOERR return. +# +T filter xShmBarrier +set barrier_count 0 +set wal3_do_lockfailure 1 +set testrc "" +set testmsg "" +do_test wal3-5.4 { + faultsim_restore_and_reopen + execsql { SELECT * FROM t1 } +} {1 2 3 4} +do_test wal3-5.5 { + list $::testrc $::testmsg +} {1 {disk I/O error}} db close T delete -finish_test +#------------------------------------------------------------------------- +# When opening a read-transaction on a database +# +finish_test Index: test/walfault.test ================================================================== --- test/walfault.test +++ test/walfault.test @@ -319,8 +319,49 @@ catch { db eval { ROLLBACK TO spoint } } catch { db eval { COMMIT } } set n [db one {SELECT count(*) FROM abc}] if {$n != 1 && $n != 2} { error "Incorrect number of rows: $n" } } + +do_test walfault-10-pre1 { + faultsim_delete_and_reopen + execsql { + PRAGMA journal_mode = WAL; + PRAGMA wal_checkpoint = 0; + CREATE TABLE z(zz INTEGER PRIMARY KEY, zzz BLOB); + CREATE INDEX zzzz ON z(zzz); + INSERT INTO z VALUES(NULL, randomblob(800)); + INSERT INTO z VALUES(NULL, randomblob(800)); + INSERT INTO z SELECT NULL, randomblob(800) FROM z; + INSERT INTO z SELECT NULL, randomblob(800) FROM z; + INSERT INTO z SELECT NULL, randomblob(800) FROM z; + INSERT INTO z SELECT NULL, randomblob(800) FROM z; + INSERT INTO z SELECT NULL, randomblob(800) FROM z; + } + faultsim_save_and_close +} {} +do_faultsim_test walfault-10 -prep { + faultsim_restore_and_reopen + execsql { + PRAGMA cache_size = 10; + BEGIN; + UPDATE z SET zzz = randomblob(799); + } + + set ::stmt [sqlite3_prepare db "SELECT zzz FROM z WHERE zz IN (1, 2, 3)" -1] + sqlite3_step $::stmt +} -body { + execsql { INSERT INTO z VALUES(NULL, NULL) } +} -test { + sqlite3_finalize $::stmt + faultsim_integrity_check + + faultsim_test_result {0 {}} + catch { db eval { ROLLBACK } } + faultsim_integrity_check + + set n [db eval {SELECT count(*), sum(length(zzz)) FROM z}] + if {$n != "64 51200"} { error "Incorrect data: $n" } +} finish_test