diff --git a/main.mk b/main.mk index f5b6231a9e..718ba49a8b 100644 --- a/main.mk +++ b/main.mk @@ -59,7 +59,7 @@ LIBOBJ+= alter.o analyze.o attach.o auth.o \ main.o malloc.o mem0.o mem1.o mem2.o mem3.o mem5.o \ memjournal.o \ mutex.o mutex_noop.o mutex_os2.o mutex_unix.o mutex_w32.o \ - opcodes.o os.o os_os2.o os_unix.o os_win.o \ + notify.o opcodes.o os.o os_os2.o os_unix.o os_win.o \ pager.o parse.o pcache.o pcache1.o pragma.o prepare.o printf.o \ random.o resolve.o rowset.o rtree.o select.o status.o \ table.o tokenize.o trigger.o \ @@ -112,6 +112,7 @@ SRC = \ $(TOP)/src/mutex_os2.c \ $(TOP)/src/mutex_unix.c \ $(TOP)/src/mutex_w32.c \ + $(TOP)/src/notify.c \ $(TOP)/src/os.c \ $(TOP)/src/os.h \ $(TOP)/src/os_common.h \ diff --git a/manifest b/manifest index 5ed729050c..da1b9cf72d 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Bump\sthe\sversion\snumber\sto\s3.6.12.\s(CVS\s6347) -D 2009-03-16T12:30:52 +C Add\sthe\ssqlite3_unlock_notify()\sAPI.\s(CVS\s6348) +D 2009-03-16T13:19:36 F Makefile.arm-wince-mingw32ce-gcc fcd5e9cd67fe88836360bb4f9ef4cb7f8e2fb5a0 F Makefile.in d64baddbf55cdf33ff030e14da837324711a4ef7 F Makefile.linux-gcc d53183f4aa6a9192d249731c90dbdffbd2c68654 @@ -83,7 +83,7 @@ F ext/rtree/tkt3363.test 6662237ea75bb431cd5d262dfc9535e1023315fc F ext/rtree/viewrtree.tcl 09526398dae87a5a87c5aac2b3854dbaf8376869 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 F ltmain.sh 3ff0879076df340d2e23ae905484d8c15d5fdea8 -F main.mk 2193e5939dbf91449f9b72178d543d31b2315360 +F main.mk bbb170882a34fe51dbd2d2e9c450c6cc0dad3325 F mkdll.sh 7d09b23c05d56532e9d44a50868eb4b12ff4f74a F mkextu.sh 416f9b7089d80e5590a29692c9d9280a10dbad9f F mkextw.sh 4123480947681d9b434a5e7b1ee08135abe409ac @@ -100,12 +100,12 @@ F src/alter.c b95815ccc92477b1b5874fb0fe20f65c867e9cc8 F src/analyze.c 3585d1a4c480ee85b65cf0a676e05d2c29eb6bdb F src/attach.c d34589d5c85d81e755e4a8fc946d313915a6fa6d F src/auth.c c8b2ab5c8bad4bd90ed7c294694f48269162c627 -F src/backup.c 2d3f31148d7b086c5c72d9edcd04fc2751b0aa6e +F src/backup.c 0082d0e5a63f04e88faee0dff0a7d63d3e92a78d F src/bitvec.c 44f7059ac1f874d364b34af31b9617e52223ba75 F src/btmutex.c 341502bc496dc0840dcb00cde65680fb0e85c3ab -F src/btree.c 6e7501d7a207dcc15b099e67231bc8cc86ef7fe9 +F src/btree.c 7d1c7e15ef8f5b139a9267c3f053d10f8885759b F src/btree.h 96a019c9f28da38e79940512d7800e419cd8c702 -F src/btreeInt.h 0a4884e6152d7cae9c741e91b830064c19fd2c05 +F src/btreeInt.h 2f1fe3c5f48800de8b542e05a746796cfdda13bd F src/build.c 7e5e93011bab2f142cb03faa72bb64ff30e321fc F src/callback.c 09c6fedc77a45db99ba25a75d61382830314b357 F src/complete.c cb14e06dbe79dee031031f0d9e686ff306afe07c @@ -122,7 +122,7 @@ F src/insert.c 71286d081a919a27ef22eaeccbe2718f93dc6aa9 F src/journal.c e00df0c0da8413ab6e1bb7d7cab5665d4a9000d0 F src/legacy.c 8b3b95d48d202614946d7ce7256e7ba898905c3b F src/loadext.c 3f96631089fc4f3871a67f02f2e4fc7ea4d51edc -F src/main.c fd98b89bcc80d27e303c913c8facab9ee939ef15 +F src/main.c 99955156dd3167e79a492187cf05e3a73196fbe2 F src/malloc.c b9c59b33539af74641362a7496ecc3efd6477f6d F src/mem0.c f2f84062d1f35814d6535c9f9e33de3bfb3b132c F src/mem1.c 3bfb39e4f60b0179713a7c087b2d4f0dc205735f @@ -136,6 +136,7 @@ F src/mutex_noop.c f5a07671f25a1a9bd7c10ad7107bc2585446200f F src/mutex_os2.c 6b5a74f812082a8483c3df05b47bbaac2424b9a0 F src/mutex_unix.c 2f936339dfef1a4c142db290d575a3509b77315f F src/mutex_w32.c f4b6a4a48f1dfff7f0089cba9b5a371691f17b8b +F src/notify.c 5787adee6f119c7d36fd8937d31d680467e01ca5 F src/os.c ed93a6b46132a602c4fd7a58142e2981c829c79d F src/os.h fa3f4aa0119ff721a2da4b47ffd74406ac864c05 F src/os_common.h 8c61457df58f1a4bd5f5adc3e90e01b37bf7afbc @@ -149,21 +150,21 @@ F src/pcache.c fcf7738c83c4d3e9d45836b2334c8a368cc41274 F src/pcache.h 9b927ccc5a538e31b4c3bc7eec4f976db42a1324 F src/pcache1.c f12518540ba776df3051215c4244e9cdc06b09cd F src/pragma.c 22ed04836aab8ce134c53be1ca896f3ad20fabdb -F src/prepare.c 1a2d40c0c5fb9f244cf5e3aacc33abcc1e7b095a +F src/prepare.c ebd40f8c6a33c52bfea2db710394d6e82c7e594b F src/printf.c 9866a9a9c4a90f6d4147407f373df3fd5d5f9b6f F src/random.c 676b9d7ac820fe81e6fb2394ac8c10cff7f38628 F src/resolve.c 094e44450371fb27869eb8bf679aacbe51fdc56d F src/rowset.c ba9375f37053d422dd76965a9c370a13b6e1aac4 F src/select.c 4d0b77fd76ff80f09a798ee98953e344c9de8fbb F src/shell.c 0a11f831603f17fea20ca97133c0f64e716af4a7 -F src/sqlite.h.in 14f4d065bafed8500ea558a75a8e2be89c784d61 +F src/sqlite.h.in 5efbb12037347fc1341ca996a5e629734ea1212e F src/sqlite3ext.h 1db7d63ab5de4b3e6b83dd03d1a4e64fef6d2a17 -F src/sqliteInt.h ae2dc2e2a063edfae3043e725981e69855bd3c9c +F src/sqliteInt.h 22332127b67d6eabb4bd09cca4ca74fd17f65238 F src/sqliteLimit.h ffe93f5a0c4e7bd13e70cd7bf84cfb5c3465f45d F src/status.c 237b193efae0cf6ac3f0817a208de6c6c6ef6d76 F src/table.c 332ab0ea691e63862e2a8bdfe2c0617ee61062a3 -F src/tclsqlite.c c18d6b71b3a01ded68e4479b128116e67eecfd2c -F src/test1.c f88b447699786d58a0136a3a48b12990abc72c8a +F src/tclsqlite.c 8a472804b901d4559213eeda538c2eadb2ad7f2a +F src/test1.c 17300af44640eea439778f5b5e03e0d68a6f00a2 F src/test2.c 71c22e2974f8094fe0fd1eba8f27872dde9b2a39 F src/test3.c 88a246b56b824275300e6c899634fbac1dc94b14 F src/test4.c f79ab52d27ff49b784b631a42e2ccd52cfd5c84c @@ -176,7 +177,7 @@ F src/test_async.c 75771172f91735f55ebbd2cbfe4b9804bd030206 F src/test_autoext.c f53b0cdf7bf5f08100009572a5d65cdb540bd0ad F src/test_backup.c 5b41518c5499dafe65177b0813b71ac356ee9df1 F src/test_btree.c d7b8716544611c323860370ee364e897c861f1b0 -F src/test_config.c 9dd62f4bb725ad87d28b187b07377cb4f4a43197 +F src/test_config.c a05378089b6773ba36b85727dedf9ec0a16424ce F src/test_devsym.c 9f4bc2551e267ce7aeda195f3897d0f30c5228f4 F src/test_func.c a55c4d5479ff2eb5c0a22d4d88e9528ab59c953b F src/test_hexio.c 2f1122aa3f012fa0142ee3c36ce5c902a70cd12f @@ -191,7 +192,7 @@ F src/test_pcache.c 29464896d9c67832e4eef916c0682b98d7283d00 F src/test_schema.c 4b4bf7bb329326458c491b0e6facd4c8c4c5b479 F src/test_server.c f0a403b5f699c09bd2b1236b6f69830fd6221f6b F src/test_tclvar.c 9e42fa59d3d2f064b7ab8628e7ab2dc8a9fe93d4 -F src/test_thread.c adb9175c466e1f487295b5b957603fc0a88ea293 +F src/test_thread.c 6805d05c3b7e08d51b31662b148cf2f51b9ca4cc F src/test_wsd.c c297d7d6b8a990239e1bd25935e81d612d8ae31d F src/tokenize.c 6987fb7f0d6a87ac53499aee568cabb05eb4bea8 F src/trigger.c 21f39db410cdc32166a94900ac1b3df98ea560e6 @@ -199,14 +200,14 @@ F src/update.c 8ededddcde6f7b6da981dd0429a5d34518a475b7 F src/utf.c 1da9c832dba0fa8f865b5b902d93f420a1ee4245 F src/util.c 469d74f5bf09ed6398702c7da2ef8a34e979a1c1 F src/vacuum.c 4929a585ef0fb1dfaf46302f8a9c4aa30c2d9cf5 -F src/vdbe.c a193ab2ce10ccb70825615870c5b4413dc3be636 +F src/vdbe.c 230b0b7e73d90e001e811fca94b95b664b6ece66 F src/vdbe.h d70a68bee196ab228914a3902c79dbd24342a0f2 F src/vdbeInt.h d12bc259b34d3d610ebf05d648eb6346d48478c3 F src/vdbeapi.c ffd5d8b493590da6e09fd54b1bea1a9d38247f11 -F src/vdbeaux.c cf84955182b48cd8c65c52c143e150bb4a71f2da +F src/vdbeaux.c feeafee5f9de51c0d30907e0600ce4db5d032df8 F src/vdbeblob.c 2852bae14c87129835938db58a77c3121e3ae962 F src/vdbemem.c 543a79d722734d2f8b7ad70f08218c30bcc5bbf5 -F src/vtab.c e39e011d7443a8d574b1b9cde207a35522e6df43 +F src/vtab.c bf409d2dc068e1bb82beeb9eef120ccfff541afb F src/walker.c 42bd3f00ca2ef5ae842304ec0d59903ef051412d F src/where.c ac555c8f6ef71a80944b31dcb212f7127c9ae30c F test/aggerror.test a867e273ef9e3d7919f03ef4f0e8c0d2767944f2 @@ -232,7 +233,7 @@ F test/autoinc.test ab549b48b389cabd92967b86c379ec8b31fa6c16 F test/autovacuum.test 61260e25744189ff766f61ca3df23c1eeec0060e F test/autovacuum_ioerr2.test 598b0663074d3673a9c1bc9a16e80971313bafe6 F test/avtrans.test 1e901d8102706b63534dbd2bdd4d8f16c4082650 -F test/backup.test 9542691cb9199b1d27051756377ef12e27404cd5 +F test/backup.test 5e487ec8dad73e9d249e9bb9ca5346a03b601b07 F test/backup2.test 04b84c97b4b5d63b6756592c6d7afe578b52c3cf F test/backup_ioerr.test a9b8084e488154341719833783ac9db321e14284 F test/backup_malloc.test 1e063c6d75143d0d6e0ae77971dd690070369387 @@ -391,7 +392,7 @@ F test/in2.test 5d4c61d17493c832f7d2d32bef785119e87bde75 F test/in3.test 3cbf58c87f4052cee3a58b37b6389777505aa0c0 F test/in4.test f795d65cbcb402d3e5c016ada8f9521d6119eca8 F test/incrblob.test 4b9437bbb38724343dadbbcca6356bc2a9b435d1 -F test/incrblob2.test 5cca1c3cb29064c504b3b0cc3e2cd43e8053cfdf +F test/incrblob2.test 7ef4581745dd80155a451637aa779b49df90787d F test/incrblob_err.test c577c91d4ed9e8336cdb188b15d6ee2a6fe9604e F test/incrvacuum.test d0fb6ef6d747ef5c5ebe878aafa72dd3e178856b F test/incrvacuum2.test 46ef65f377e3937cfd1ba66e818309dab46f590d @@ -477,6 +478,8 @@ F test/misuse.test 30b3a458e5a70c31e74c291937b6c82204c59f33 F test/mutex1.test 1e5c196d5170bbe3a7d8370b1b905e8c86a9e07c F test/mutex2.test bfeaeac2e73095b2ac32285d2756e3a65e681660 F test/nan.test c627d79b3d36ea892563fd67584b3e8a18f0618a +F test/notify1.test 9a985a94f34de1b24daf25fd86b6d5033ba532d0 +F test/notify2.test 828511802c3e899d91f753771cefbe1d41dcb854 F test/notnull.test 44d600f916b770def8b095a9962dbe3be5a70d82 F test/null.test a8b09b8ed87852742343b33441a9240022108993 F test/openv2.test f5dd6b23e4dce828eb211649b600763c42a668df @@ -524,7 +527,7 @@ F test/selectA.test 06d1032fa9009314c95394f2ca2e60d9f7ae8532 F test/selectB.test 31e81ac9af7d224850e0706350f070ecb92fcbc7 F test/selectC.test ae49d258c875bc1712898f1632062bc5c01a7470 F test/server1.test f5b790d4c0498179151ca8a7715a65a7802c859c -F test/shared.test 2f6c65de8123c130b92e4e18a516f669eaa02fea +F test/shared.test 3b448dc0f7a9356e641894ed81c27599f39d809d F test/shared2.test 0ee9de8964d70e451936a48c41cb161d9134ccf4 F test/shared3.test 9c880afc081d797da514ef64bccf36f3fce2f09c F test/shared4.test d0fadacb50bb6981b2fb9dc6d1da30fa1edddf83 @@ -549,7 +552,7 @@ F test/substr.test 18f57c4ca8a598805c4d64e304c418734d843c1a F test/sync.test ded6b39d8d8ca3c0c5518516c6371b3316d3e3a3 F test/table.test bf1bc3d9634342a3470bdf64b6190e7445b6b8a6 F test/tableapi.test 505031f15b18a750184d967d2c896cf88fcc969c -F test/tclsqlite.test 413a8a887d89ea8fa7055e8d118ffb03b0a4c91a +F test/tclsqlite.test 8b1150d0486c4848c70d96422513a91c5342be0e F test/tempdb.test b88ac8a19823cf771d742bf61eef93ef337c06b1 F test/temptable.test 5d8ca46be28cc06c887c5a77df650843b7edbae1 F test/temptrigger.test 03093be9967942623232dfdf2a63b832d4e0e4fa @@ -597,7 +600,7 @@ F test/tkt2817.test 94646b604c7dbae7058782f6582c05e200700aa9 F test/tkt2820.test 017fdee33aaef7abc092beab6088816f1942304b F test/tkt2822.test a2b27a58df62d1b2e712f91dbe42ad3b7e0e77cc F test/tkt2832.test 85cf382ff406de9de35534b86bc7227d609140c0 -F test/tkt2854.test a2bc584ac26bcebe174229e7a1ad4e6d43c3d569 +F test/tkt2854.test b81dc3144901b123fe5674471adf5a47ca48a7c3 F test/tkt2920.test a8737380e4ae6424e00c0273dc12775704efbebf F test/tkt2927.test 4752868b9eeeb07a217f7f19f4cbaac98d6d086d F test/tkt2942.test c5c87d179799ca6d1fbe83c815510b87cd5ec7ce @@ -690,7 +693,7 @@ F tool/lempar.c aeba88b8566ff66f8a67c96b3eb2dd95e7d8908d F tool/mkkeywordhash.c 8e57fbe8c4fe2f1800f9190fd361231cb8558407 F tool/mkopts.tcl 66ac10d240cc6e86abd37dc908d50382f84ff46e x F tool/mkspeedsql.tcl a1a334d288f7adfe6e996f2e712becf076745c97 -F tool/mksqlite3c.tcl a416823e4c06e8d8e18e141e69a8a0e8e12ad861 +F tool/mksqlite3c.tcl 671833bd775e76ebd922b9e82c2508a344562511 F tool/mksqlite3internalh.tcl 7b43894e21bcb1bb39e11547ce7e38a063357e87 F tool/omittest.tcl 27d6f6e3b1e95aeb26a1c140e6eb57771c6d794a F tool/opcodeDoc.awk b3a2a3d5d3075b8bd90b7afe24283efdd586659c @@ -704,7 +707,7 @@ F tool/speedtest16.c c8a9c793df96db7e4933f0852abb7a03d48f2e81 F tool/speedtest2.tcl ee2149167303ba8e95af97873c575c3e0fab58ff F tool/speedtest8.c 2902c46588c40b55661e471d7a86e4dd71a18224 F tool/speedtest8inst1.c 293327bc76823f473684d589a8160bde1f52c14e -P 324a1aff300b7349b9fc1dea56d640d86500f100 -R 3843a549b4ba7feed5cd440b16a43795 -U drh -Z 9336bd2967fe6f29f86215585309dca6 +P 2fcccca3e56e2e3a95bdedeb01ab7da1b24b7ac2 +R 5be2b218449f212c10c446da99e1fb31 +U danielk1977 +Z 7d9c2a290b69196df9bf164217432ca4 diff --git a/manifest.uuid b/manifest.uuid index 42e5174816..b775deb5c1 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -2fcccca3e56e2e3a95bdedeb01ab7da1b24b7ac2 \ No newline at end of file +b649a6cc5bfefddd6a04b1183647d2923e0a0daa \ No newline at end of file diff --git a/src/backup.c b/src/backup.c index 9c55804790..781b4b0bca 100644 --- a/src/backup.c +++ b/src/backup.c @@ -12,7 +12,7 @@ ** This file contains the implementation of the sqlite3_backup_XXX() ** API functions and the related features. ** -** $Id: backup.c,v 1.12 2009/02/16 17:55:47 shane Exp $ +** $Id: backup.c,v 1.13 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "sqliteInt.h" #include "btreeInt.h" @@ -292,10 +292,10 @@ int sqlite3_backup_step(sqlite3_backup *p, int nPage){ int bCloseTrans = 0; /* True if src db requires unlocking */ /* If the source pager is currently in a write-transaction, return - ** SQLITE_LOCKED immediately. + ** SQLITE_BUSY immediately. */ if( p->pDestDb && p->pSrc->pBt->inTransaction==TRANS_WRITE ){ - rc = SQLITE_LOCKED; + rc = SQLITE_BUSY; }else{ rc = SQLITE_OK; } diff --git a/src/btree.c b/src/btree.c index b012e51598..ed12137b82 100644 --- a/src/btree.c +++ b/src/btree.c @@ -9,7 +9,7 @@ ** May you share freely, never taking more than you give. ** ************************************************************************* -** $Id: btree.c,v 1.572 2009/03/12 14:43:28 danielk1977 Exp $ +** $Id: btree.c,v 1.573 2009/03/16 13:19:36 danielk1977 Exp $ ** ** This file implements a external (disk-based) database using BTrees. ** See the header comment on "btreeInt.h" for additional information. @@ -109,8 +109,9 @@ static int querySharedCacheTableLock(Btree *p, Pgno iTab, u8 eLock){ /* If some other connection is holding an exclusive lock, the ** requested lock may not be obtained. */ - if( pBt->pExclusive && pBt->pExclusive!=p ){ - return SQLITE_LOCKED; + if( pBt->pWriter!=p && pBt->isExclusive ){ + sqlite3ConnectionBlocked(p->db, pBt->pWriter->db); + return SQLITE_LOCKED_SHAREDCACHE; } /* This (along with setSharedCacheTableLock()) is where @@ -137,7 +138,12 @@ static int querySharedCacheTableLock(Btree *p, Pgno iTab, u8 eLock){ for(pIter=pBt->pLock; pIter; pIter=pIter->pNext){ if( pIter->pBtree!=p && pIter->iTable==iTab && (pIter->eLock!=eLock || eLock!=READ_LOCK) ){ - return SQLITE_LOCKED; + sqlite3ConnectionBlocked(p->db, pIter->pBtree->db); + if( eLock==WRITE_LOCK ){ + assert( p==pBt->pWriter ); + pBt->isPending = 1; + } + return SQLITE_LOCKED_SHAREDCACHE; } } } @@ -233,7 +239,7 @@ static void clearAllSharedCacheTableLocks(Btree *p){ while( *ppIter ){ BtLock *pLock = *ppIter; - assert( pBt->pExclusive==0 || pBt->pExclusive==pLock->pBtree ); + assert( pBt->isExclusive==0 || pBt->pWriter==pLock->pBtree ); if( pLock->pBtree==p ){ *ppIter = pLock->pNext; sqlite3_free(pLock); @@ -242,8 +248,22 @@ static void clearAllSharedCacheTableLocks(Btree *p){ } } - if( pBt->pExclusive==p ){ - pBt->pExclusive = 0; + assert( pBt->isPending==0 || pBt->pWriter ); + if( pBt->pWriter==p ){ + pBt->pWriter = 0; + pBt->isExclusive = 0; + pBt->isPending = 0; + }else if( pBt->nTransaction==2 ){ + /* This function is called when connection p is concluding its + ** transaction. If there currently exists a writer, and p is not + ** that writer, then the number of locks held by connections other + ** than the writer must be about to drop to zero. In this case + ** set the isPending flag to 0. + ** + ** If there is not currently a writer, then BtShared.isPending must + ** be zero already. So this next line is harmless in that case. + */ + pBt->isPending = 0; } } #endif /* SQLITE_OMIT_SHARED_CACHE */ @@ -2052,6 +2072,7 @@ static int newDatabase(BtShared *pBt){ ** proceed. */ int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ + sqlite3 *pBlock = 0; BtShared *pBt = p->pBt; int rc = SQLITE_OK; @@ -2073,25 +2094,27 @@ int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ goto trans_begun; } +#ifndef SQLITE_OMIT_SHARED_CACHE /* If another database handle has already opened a write transaction ** on this shared-btree structure and a second write transaction is - ** requested, return SQLITE_BUSY. + ** requested, return SQLITE_LOCKED. */ - if( pBt->inTransaction==TRANS_WRITE && wrflag ){ - rc = SQLITE_BUSY; - goto trans_begun; - } - -#ifndef SQLITE_OMIT_SHARED_CACHE - if( wrflag>1 ){ + if( (wrflag && pBt->inTransaction==TRANS_WRITE) || pBt->isPending ){ + pBlock = pBt->pWriter->db; + }else if( wrflag>1 ){ BtLock *pIter; for(pIter=pBt->pLock; pIter; pIter=pIter->pNext){ if( pIter->pBtree!=p ){ - rc = SQLITE_BUSY; - goto trans_begun; + pBlock = pIter->pBtree->db; + break; } } } + if( pBlock ){ + sqlite3ConnectionBlocked(p->db, pBlock); + rc = SQLITE_LOCKED_SHAREDCACHE; + goto trans_begun; + } #endif do { @@ -2129,9 +2152,10 @@ int sqlite3BtreeBeginTrans(Btree *p, int wrflag){ pBt->inTransaction = p->inTrans; } #ifndef SQLITE_OMIT_SHARED_CACHE - if( wrflag>1 ){ - assert( !pBt->pExclusive ); - pBt->pExclusive = p; + if( wrflag ){ + assert( !pBt->pWriter ); + pBt->pWriter = p; + pBt->isExclusive = (wrflag>1); } #endif } @@ -2940,8 +2964,10 @@ static int btreeCursor( if( NEVER(pBt->readOnly) ){ return SQLITE_READONLY; } - if( checkForReadConflicts(p, iTable, 0, 0) ){ - return SQLITE_LOCKED; + rc = checkForReadConflicts(p, iTable, 0, 0); + if( rc!=SQLITE_OK ){ + assert( rc==SQLITE_LOCKED_SHAREDCACHE ); + return rc; } } @@ -5968,9 +5994,10 @@ static int checkForReadConflicts( #endif ){ sqlite3 *dbOther = p->pBtree->db; - if( dbOther==0 || - (dbOther!=db && (dbOther->flags & SQLITE_ReadUncommitted)==0) ){ - return SQLITE_LOCKED; + assert(dbOther); + if( dbOther!=db && (dbOther->flags & SQLITE_ReadUncommitted)==0 ){ + sqlite3ConnectionBlocked(db, dbOther); + return SQLITE_LOCKED_SHAREDCACHE; } } } @@ -6007,8 +6034,11 @@ int sqlite3BtreeInsert( assert( pBt->inTransaction==TRANS_WRITE ); assert( !pBt->readOnly ); assert( pCur->wrFlag ); - if( checkForReadConflicts(pCur->pBtree, pCur->pgnoRoot, pCur, nKey) ){ - return SQLITE_LOCKED; /* The table pCur points to has a read lock */ + rc = checkForReadConflicts(pCur->pBtree, pCur->pgnoRoot, pCur, nKey); + if( rc ){ + /* The table pCur points to has a read lock */ + assert( rc==SQLITE_LOCKED_SHAREDCACHE ); + return rc; } if( pCur->eState==CURSOR_FAULT ){ return pCur->skip; @@ -6104,10 +6134,11 @@ int sqlite3BtreeDelete(BtCursor *pCur){ return SQLITE_ERROR; /* The cursor is not pointing to anything */ } assert( pCur->wrFlag ); - if( checkForReadConflicts(pCur->pBtree, pCur->pgnoRoot, - pCur, pCur->info.nKey) - ){ - return SQLITE_LOCKED; /* The table pCur points to has a read lock */ + rc = checkForReadConflicts(p, pCur->pgnoRoot, pCur, pCur->info.nKey); + if( rc!=SQLITE_OK ){ + /* The table pCur points to has a read lock */ + assert( rc==SQLITE_LOCKED_SHAREDCACHE ); + return rc; } /* Restore the current cursor position (a no-op if the cursor is not in @@ -6538,7 +6569,8 @@ static int btreeDropTable(Btree *p, Pgno iTable, int *piMoved){ ** occur. */ if( pBt->pCursor ){ - return SQLITE_LOCKED; + sqlite3ConnectionBlocked(p->db, pBt->pCursor->pBtree->db); + return SQLITE_LOCKED_SHAREDCACHE; } rc = sqlite3BtreeGetPage(pBt, (Pgno)iTable, &pPage, 0); @@ -7378,14 +7410,16 @@ void *sqlite3BtreeSchema(Btree *p, int nBytes, void(*xFree)(void *)){ } /* -** Return true if another user of the same shared btree as the argument -** handle holds an exclusive lock on the sqlite_master table. +** Return SQLITE_LOCKED_SHAREDCACHE if another user of the same shared +** btree as the argument handle holds an exclusive lock on the +** sqlite_master table. Otherwise SQLITE_OK. */ int sqlite3BtreeSchemaLocked(Btree *p){ int rc; assert( sqlite3_mutex_held(p->db->mutex) ); sqlite3BtreeEnter(p); - rc = (querySharedCacheTableLock(p, MASTER_ROOT, READ_LOCK)!=SQLITE_OK); + rc = querySharedCacheTableLock(p, MASTER_ROOT, READ_LOCK); + assert( rc==SQLITE_OK || rc==SQLITE_LOCKED_SHAREDCACHE ); sqlite3BtreeLeave(p); return rc; } @@ -7423,6 +7457,8 @@ int sqlite3BtreeLockTable(Btree *p, int iTab, u8 isWriteLock){ ** to change the length of the data stored. */ int sqlite3BtreePutData(BtCursor *pCsr, u32 offset, u32 amt, void *z){ + int rc; + assert( cursorHoldsMutex(pCsr) ); assert( sqlite3_mutex_held(pCsr->pBtree->db->mutex) ); assert(pCsr->isIncrblobHandle); @@ -7443,8 +7479,11 @@ int sqlite3BtreePutData(BtCursor *pCsr, u32 offset, u32 amt, void *z){ } assert( !pCsr->pBt->readOnly && pCsr->pBt->inTransaction==TRANS_WRITE ); - if( checkForReadConflicts(pCsr->pBtree, pCsr->pgnoRoot, pCsr, 0) ){ - return SQLITE_LOCKED; /* The table pCur points to has a read lock */ + rc = checkForReadConflicts(pCsr->pBtree, pCsr->pgnoRoot, pCsr, 0); + if( rc!=SQLITE_OK ){ + /* The table pCur points to has a read lock */ + assert( rc==SQLITE_LOCKED_SHAREDCACHE ); + return rc; } if( pCsr->eState==CURSOR_INVALID || !pCsr->apPage[pCsr->iPage]->intKey ){ return SQLITE_ERROR; diff --git a/src/btreeInt.h b/src/btreeInt.h index 32aec96151..4652521df3 100644 --- a/src/btreeInt.h +++ b/src/btreeInt.h @@ -9,7 +9,7 @@ ** May you share freely, never taking more than you give. ** ************************************************************************* -** $Id: btreeInt.h,v 1.42 2009/02/03 16:51:25 danielk1977 Exp $ +** $Id: btreeInt.h,v 1.43 2009/03/16 13:19:36 danielk1977 Exp $ ** ** This file implements a external (disk-based) database using BTrees. ** For a detailed discussion of BTrees, refer to @@ -356,6 +356,24 @@ struct Btree { ** may not be modified once it is initially set as long as nRef>0. ** The pSchema field may be set once under BtShared.mutex and ** thereafter is unchanged as long as nRef>0. +** +** isPending: +** +** If a BtShared client fails to obtain a write-lock on a database +** table (because there exists one or more read-locks on the table), +** the shared-cache enters 'pending-lock' state and isPending is +** set to true. +** +** The shared-cache leaves the 'pending lock' state when either of +** the following occur: +** +** 1) The current writer (BtShared.pWriter) concludes its transaction, OR +** 2) The number of locks held by other connections drops to zero. +** +** while in the 'pending-lock' state, no connection may start a new +** transaction. +** +** This feature is included to help prevent writer-starvation. */ struct BtShared { Pager *pPager; /* The page cache */ @@ -385,7 +403,9 @@ struct BtShared { int nRef; /* Number of references to this structure */ BtShared *pNext; /* Next on a list of sharable BtShared structs */ BtLock *pLock; /* List of locks held on this shared-btree struct */ - Btree *pExclusive; /* Btree with an EXCLUSIVE lock on the whole db */ + Btree *pWriter; /* Btree with currently open write transaction */ + u8 isExclusive; /* True if pWriter has an EXCLUSIVE lock on the db */ + u8 isPending; /* If waiting for read-locks to clear */ #endif u8 *pTmpSpace; /* BtShared.pageSize bytes of space for tmp use */ }; diff --git a/src/main.c b/src/main.c index e4f92e8f02..9f900cc891 100644 --- a/src/main.c +++ b/src/main.c @@ -14,7 +14,7 @@ ** other files are for internal use by SQLite and should not be ** accessed by users of the library. ** -** $Id: main.c,v 1.530 2009/02/26 07:15:59 danielk1977 Exp $ +** $Id: main.c,v 1.531 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -629,6 +629,12 @@ int sqlite3_close(sqlite3 *db){ } } sqlite3ResetInternalSchema(db, 0); + + /* Tell the code in notify.c that the connection no longer holds any + ** locks and does not require any further unlock-notify callbacks. + */ + sqlite3ConnectionClosed(db); + assert( db->nDb<=2 ); assert( db->aDb==db->aDbStatic ); for(j=0; jaFunc.a); j++){ diff --git a/src/notify.c b/src/notify.c new file mode 100644 index 0000000000..c4babbd700 --- /dev/null +++ b/src/notify.c @@ -0,0 +1,310 @@ +/* +** 2009 March 3 +** +** 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 contains the implementation of the sqlite3_unlock_notify() +** API method and its associated functionality. +** +** $Id: notify.c,v 1.1 2009/03/16 13:19:36 danielk1977 Exp $ +*/ +#include "sqliteInt.h" +#include "btreeInt.h" + +/* Omit this entire file if SQLITE_ENABLE_UNLOCK_NOTIFY is not defined. */ +#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY + +/* +** Public interfaces: +** +** sqlite3ConnectionBlocked() +** sqlite3ConnectionUnlocked() +** sqlite3ConnectionClosed() +** sqlite3_unlock_notify() +*/ + +#define assertMutexHeld() \ + assert( sqlite3_mutex_held(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MASTER)) ) + +/* +** Head of a linked list of all sqlite3 objects created by this process +** for which either sqlite3.pBlockingConnection or sqlite3.pUnlockConnection +** is not NULL. This variable may only accessed while the STATIC_MASTER +** mutex is held. +*/ +static sqlite3 *SQLITE_WSD sqlite3BlockedList = 0; + +#ifndef NDEBUG +/* +** This function is a complex assert() that verifies the following +** properties of the blocked connections list: +** +** 1) Each entry in the list has a non-NULL value for either +** pUnlockConnection or pBlockingConnection, or both. +** +** 2) All entries in the list that share a common value for +** xUnlockNotify are grouped together. +** +** 3) If the argument db is not NULL, then none of the entries in the +** blocked connections list have pUnlockConnection or pBlockingConnection +** set to db. This is used when closing connection db. +*/ +static void checkListProperties(sqlite3 *db){ + sqlite3 *p; + for(p=sqlite3BlockedList; p; p=p->pNextBlocked){ + int seen = 0; + sqlite3 *p2; + + /* Verify property (1) */ + assert( p->pUnlockConnection || p->pBlockingConnection ); + + /* Verify property (2) */ + for(p2=sqlite3BlockedList; p2!=p; p2=p2->pNextBlocked){ + if( p2->xUnlockNotify==p->xUnlockNotify ) seen = 1; + assert( p2->xUnlockNotify==p->xUnlockNotify || !seen ); + assert( db==0 || p->pUnlockConnection!=db ); + assert( db==0 || p->pBlockingConnection!=db ); + } + } +} +#else +# define checkListProperties(x) +#endif + +/* +** Remove connection db from the blocked connections list. If connection +** db is not currently a part of the list, this function is a no-op. +*/ +static void removeFromBlockedList(sqlite3 *db){ + sqlite3 **pp; + assertMutexHeld(); + for(pp=&sqlite3BlockedList; *pp; pp = &(*pp)->pNextBlocked){ + if( *pp==db ){ + *pp = (*pp)->pNextBlocked; + break; + } + } +} + +/* +** Add connection db to the blocked connections list. It is assumed +** that it is not already a part of the list. +*/ +static void addToBlockedList(sqlite3 *db){ + sqlite3 **pp; + assertMutexHeld(); + for( + pp=&sqlite3BlockedList; + *pp && (*pp)->xUnlockNotify!=db->xUnlockNotify; + pp=&(*pp)->pNextBlocked + ); + db->pNextBlocked = *pp; + *pp = db; +} + +/* +** Obtain the STATIC_MASTER mutex. +*/ +static void enterMutex(){ + sqlite3_mutex_enter(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MASTER)); + checkListProperties(0); +} + +/* +** Release the STATIC_MASTER mutex. +*/ +static void leaveMutex(){ + assertMutexHeld(); + checkListProperties(0); + sqlite3_mutex_leave(sqlite3MutexAlloc(SQLITE_MUTEX_STATIC_MASTER)); +} + +/* +** Register an unlock-notify callback. +*/ +int sqlite3_unlock_notify( + sqlite3 *db, + void (*xNotify)(void **, int), + void *pArg +){ + int rc = SQLITE_OK; + + sqlite3_mutex_enter(db->mutex); + enterMutex(); + + if( 0==db->pBlockingConnection ){ + /* The blocking transaction has been concluded. Or there never was a + ** blocking transaction. In either case, invoke the notify callback + ** immediately. + */ + xNotify(&pArg, 1); + }else{ + sqlite3 *p; + + for(p=db->pBlockingConnection; p && p!=db; p=p->pUnlockConnection); + if( p ){ + rc = SQLITE_LOCKED; /* Deadlock detected. */ + }else{ + db->pUnlockConnection = db->pBlockingConnection; + db->xUnlockNotify = xNotify; + db->pUnlockArg = pArg; + removeFromBlockedList(db); + addToBlockedList(db); + } + } + + leaveMutex(); + assert( !db->mallocFailed ); + sqlite3Error(db, rc, (rc?"database is deadlocked":0)); + sqlite3_mutex_leave(db->mutex); + return rc; +} + +/* +** This function is called while stepping or preparing a statement +** associated with connection db. The operation will return SQLITE_LOCKED +** to the user because it requires a lock that will not be available +** until connection pBlocker concludes its current transaction. +*/ +void sqlite3ConnectionBlocked(sqlite3 *db, sqlite3 *pBlocker){ + enterMutex(); + if( db->pBlockingConnection==0 && db->pUnlockConnection==0 ){ + addToBlockedList(db); + } + db->pBlockingConnection = pBlocker; + leaveMutex(); +} + +/* +** The transaction opened by database db has just finished. Locks held +** by database connection db have been released. +** +** This function loops through each entry in the blocked connections +** list and does the following: +** +** 1) If the sqlite3.pBlockingConnection member of a list entry is +** set to db, then set pBlockingConnection=0. +** +** 2) If the sqlite3.pUnlockConnection member of a list entry is +** set to db, then invoke the configured unlock-notify callback and +** set pUnlockConnection=0. +** +** 3) If the two steps above mean that pBlockingConnection==0 and +** pUnlockConnection==0, remove the entry from the blocked connections +** list. +*/ +void sqlite3ConnectionUnlocked(sqlite3 *db){ + void (*xUnlockNotify)(void **, int) = 0; /* Unlock-notify cb to invoke */ + int nArg = 0; /* Number of entries in aArg[] */ + sqlite3 **pp; /* Iterator variable */ + + void *aStatic[16]; + void **aArg = aStatic; + void **aDyn = 0; + + enterMutex(); /* Enter STATIC_MASTER mutex */ + + /* This loop runs once for each entry in the blocked-connections list. */ + for(pp=&sqlite3BlockedList; *pp; /* no-op */ ){ + sqlite3 *p = *pp; + + /* Step 1. */ + if( p->pBlockingConnection==db ){ + p->pBlockingConnection = 0; + } + + /* Step 2. */ + if( p->pUnlockConnection==db ){ + assert( p->xUnlockNotify ); + if( p->xUnlockNotify!=xUnlockNotify && nArg!=0 ){ + xUnlockNotify(aArg, nArg); + nArg = 0; + } + + sqlite3BeginBenignMalloc(); + assert( aArg==aDyn || (aDyn==0 && aArg==aStatic) ); + assert( nArg<=ArraySize(aStatic) || aArg==aDyn ); + if( (!aDyn && nArg==ArraySize(aStatic)) + || (aDyn && nArg==(sqlite3DbMallocSize(db, aDyn)/sizeof(void*))) + ){ + /* The aArg[] array needs to grow. */ + void **pNew = (void **)sqlite3Malloc(nArg*sizeof(void *)*2); + if( pNew ){ + memcpy(pNew, aArg, nArg*sizeof(void *)); + sqlite3_free(aDyn); + aDyn = aArg = pNew; + }else{ + /* This occurs when the array of context pointers that need to + ** be passed to the unlock-notify callback is larger than the + ** aStatic[] array allocated on the stack and the attempt to + ** allocate a larger array from the heap has failed. + ** + ** This is a difficult situation to handle. Returning an error + ** code to the caller is insufficient, as even if an error code + ** is returned the transaction on connection db will still be + ** closed and the unlock-notify callbacks on blocked connections + ** will go unissued. This might cause the application to wait + ** indefinitely for an unlock-notify callback that will never + ** arrive. + ** + ** Instead, invoke the unlock-notify callback with the context + ** array already accumulated. We can then clear the array and + ** begin accumulating any further context pointers without + ** requiring any dynamic allocation. This is sub-optimal because + ** it means that instead of one callback with a large array of + ** context pointers the application will receive two or more + ** callbacks with smaller arrays of context pointers, which will + ** reduce the applications ability to prioritize multiple + ** connections. But it is the best that can be done under the + ** circumstances. + */ + xUnlockNotify(aArg, nArg); + nArg = 0; + } + } + sqlite3EndBenignMalloc(); + + aArg[nArg++] = p->pUnlockArg; + xUnlockNotify = p->xUnlockNotify; + p->pUnlockConnection = 0; + p->xUnlockNotify = 0; + p->pUnlockArg = 0; + } + + /* Step 3. */ + if( p->pBlockingConnection==0 && p->pUnlockConnection==0 ){ + /* Remove connection p from the blocked connections list. */ + *pp = p->pNextBlocked; + p->pNextBlocked = 0; + }else{ + pp = &p->pNextBlocked; + } + } + + if( nArg!=0 ){ + xUnlockNotify(aArg, nArg); + } + sqlite3_free(aDyn); + leaveMutex(); /* Leave STATIC_MASTER mutex */ +} + +/* +** This is called when the database connection passed as an argument is +** being closed. The connection is removed from the blocked list. +*/ +void sqlite3ConnectionClosed(sqlite3 *db){ + sqlite3ConnectionUnlocked(db); + enterMutex(); + removeFromBlockedList(db); + checkListProperties(db); + leaveMutex(); +} +#endif + diff --git a/src/prepare.c b/src/prepare.c index 72f8a61f9c..0142af557f 100644 --- a/src/prepare.c +++ b/src/prepare.c @@ -13,7 +13,7 @@ ** interface, and routines that contribute to loading the database schema ** from disk. ** -** $Id: prepare.c,v 1.108 2009/03/05 04:20:32 shane Exp $ +** $Id: prepare.c,v 1.109 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "sqliteInt.h" @@ -570,10 +570,10 @@ static int sqlite3Prepare( rc = sqlite3BtreeSchemaLocked(pBt); if( rc ){ const char *zDb = db->aDb[i].zName; - sqlite3Error(db, SQLITE_LOCKED, "database schema is locked: %s", zDb); + sqlite3Error(db, rc, "database schema is locked: %s", zDb); (void)sqlite3SafetyOff(db); testcase( db->flags & SQLITE_ReadUncommitted ); - return sqlite3ApiExit(db, SQLITE_LOCKED); + return sqlite3ApiExit(db, rc); } } } diff --git a/src/sqlite.h.in b/src/sqlite.h.in index 1b954d5f32..9e58b6290b 100644 --- a/src/sqlite.h.in +++ b/src/sqlite.h.in @@ -30,7 +30,7 @@ ** the version number) and changes its name to "sqlite3.h" as ** part of the build process. ** -** @(#) $Id: sqlite.h.in,v 1.433 2009/02/18 18:37:59 drh Exp $ +** @(#) $Id: sqlite.h.in,v 1.434 2009/03/16 13:19:36 danielk1977 Exp $ */ #ifndef _SQLITE3_H_ #define _SQLITE3_H_ @@ -383,6 +383,8 @@ int sqlite3_exec( #define SQLITE_IOERR_CLOSE (SQLITE_IOERR | (16<<8)) #define SQLITE_IOERR_DIR_CLOSE (SQLITE_IOERR | (17<<8)) +#define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8) ) + /* ** CAPI3REF: Flags For File Open Operations {H10230} ** @@ -5349,6 +5351,127 @@ int sqlite3_backup_finish(sqlite3_backup *p); int sqlite3_backup_remaining(sqlite3_backup *p); int sqlite3_backup_pagecount(sqlite3_backup *p); +/* +** CAPI3REF: Unlock Notification +** EXPERIMENTAL +** +** When running in shared-cache mode, a database operation may fail with +** an SQLITE_LOCKED error if the required locks on the shared-cache or +** individual tables within the shared-cache cannot be obtained. See +** [SQLite Shared-Cache Mode] for a description of shared-cache locking. +** This API may be used to register a callback that SQLite will invoke +** when the connection currently holding the required lock relinquishes it. +** This API is only available if the library was compiled with the +** SQLITE_ENABLE_UNLOCK_NOTIFY C-preprocessor symbol defined. +** +** See Also: [Using the SQLite Unlock Notification Feature]. +** +** Shared-cache locks are released when a database connection concludes +** its current transaction, either by committing it or rolling it back. +** +** When a connection (known as the blocked connection) fails to obtain a +** shared-cache lock and SQLITE_LOCKED is returned to the caller, the +** identity of the database connection (the blocking connection) that +** has locked the required resource is stored internally. After an +** application receives an SQLITE_LOCKED error, it may call the +** sqlite3_unlock_notify() method with the blocked connection handle as +** the first argument to register for a callback that will be invoked +** when the blocking connections current transaction is concluded. The +** callback is invoked from within the [sqlite3_step] or [sqlite3_close] +** call that concludes the blocking connections transaction. +** +** If sqlite3_unlock_notify() is called in a multi-threaded application, +** there is a chance that the blocking connection will have already +** concluded its transaction by the time sqlite3_unlock_notify() is invoked. +** If this happens, then the specified callback is invoked immediately, +** from within the call to sqlite3_unlock_notify(). +** +** If the blocked connection is attempting to obtain a write-lock on a +** shared-cache table, and more than one other connection currently holds +** a read-lock on the same table, then SQLite arbitrarily selects one of +** the other connections to use as the blocking connection. +** +** There may be at most one unlock-notify callback registered by a +** blocked connection. If sqlite3_unlock_notify() is called when the +** blocked connection already has a registered unlock-notify callback, +** then the new callback replaces the old. If sqlite3_unlock_notify() is +** called with a NULL pointer as its second argument, then any existing +** unlock-notify callback is cancelled. The blocked connections +** unlock-notify callback may also be canceled by closing the blocked +** connection using [sqlite3_close()]. +** +** The unlock-notify callback is not reentrant. If an application invokes +** any sqlite3_xxx API functions from within an unlock-notify callback, a +** crash or deadlock may be the result. +** +** Unless deadlock is detected (see below), sqlite3_unlock_notify() always +** returns SQLITE_OK. +** +** Callback Invocation Details +** +** When an unlock-notify callback is registered, the application provides a +** single void* pointer that is passed to the callback when it is invoked. +** However, the signature of the callback function allows SQLite to pass +** it an array of void* context pointers. The first argument passed to +** an unlock-notify callback is a pointer to an array of void* pointers, +** and the second is the number of entries in the array. +** +** When a blocking connections transaction is concluded, there may be +** more than one blocked connection that has registered for an unlock-notify +** callback. If two or more such blocked connections have specified the +** same callback function, then instead of invoking the callback function +** multiple times, it is invoked once with the set of void* context pointers +** specified by the blocked connections bundled together into an array. +** This gives the application an opportunity to prioritize any actions +** related to the set of unblocked database connections. +** +** Deadlock Detection +** +** Assuming that after registering for an unlock-notify callback a +** database waits for the callback to be issued before taking any further +** action (a reasonable assumption), then using this API may cause the +** application to deadlock. For example, if connection X is waiting for +** connection Y's transaction to be concluded, and similarly connection +** Y is waiting on connection X's transaction, then neither connection +** will proceed and the system may remain deadlocked indefinitely. +** +** To avoid this scenario, the sqlite3_unlock_notify() performs deadlock +** detection. If a given call to sqlite3_unlock_notify() would put the +** system in a deadlocked state, then SQLITE_LOCKED is returned and no +** unlock-notify callback is registered. The system is said to be in +** a deadlocked state if connection A has registered for an unlock-notify +** callback on the conclusion of connection B's transaction, and connection +** B has itself registered for an unlock-notify callback when connection +** A's transaction is concluded. Indirect deadlock is also detected, so +** the system is also considered to be deadlocked if connection B has +** registered for an unlock-notify callback on the conclusion of connection +** C's transaction, where connection C is waiting on connection A. Any +** number of levels of indirection are allowed. +** +** The "DROP TABLE" Exception +** +** When a call to [sqlite3_step()] returns SQLITE_LOCKED, it is almost +** always appropriate to call sqlite3_unlock_notify(). There is however, +** one exception. When executing a "DROP TABLE" or "DROP INDEX" statement, +** SQLite checks if there are any currently executing SELECT statements +** that belong to the same connection. If there are, SQLITE_LOCKED is +** returned. In this case there is no "blocking connection", so invoking +** sqlite3_unlock_notify() results in the unlock-notify callback being +** invoked immediately. If the application then re-attempts the "DROP TABLE" +** or "DROP INDEX" query, an infinite loop might be the result. +** +** One way around this problem is to check the extended error code returned +** by an sqlite3_step() call. If there is a blocking connection, then the +** extended error code is set to SQLITE_LOCKED_SHAREDCACHE. Otherwise, in +** the special "DROP TABLE/INDEX" case, the extended error code is just +** SQLITE_LOCKED. +*/ +int sqlite3_unlock_notify( + sqlite3 *pBlocked, /* Waiting connection */ + void (*xNotify)(void **apArg, int nArg), /* Callback function to invoke */ + void *pNotifyArg /* Argument to pass to xNotify */ +); + /* ** Undo the hack that converts floating point types to integer for ** builds on processors without floating point support. diff --git a/src/sqliteInt.h b/src/sqliteInt.h index 2726a46759..fafd5f8466 100644 --- a/src/sqliteInt.h +++ b/src/sqliteInt.h @@ -11,7 +11,7 @@ ************************************************************************* ** Internal interface definitions for SQLite. ** -** @(#) $Id: sqliteInt.h,v 1.840 2009/03/02 17:18:48 shane Exp $ +** @(#) $Id: sqliteInt.h,v 1.841 2009/03/16 13:19:36 danielk1977 Exp $ */ #ifndef _SQLITEINT_H_ #define _SQLITEINT_H_ @@ -808,6 +808,17 @@ struct sqlite3 { Savepoint *pSavepoint; /* List of active savepoints */ int nSavepoint; /* Number of non-transaction savepoints */ u8 isTransactionSavepoint; /* True if the outermost savepoint is a TS */ + +#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY + /* The following variables are all protected by the STATIC_MASTER + ** mutex, not by sqlite3.mutex. They are used by code in notify.c. + */ + sqlite3 *pBlockingConnection; /* Connection that caused SQLITE_LOCKED */ + sqlite3 *pUnlockConnection; /* Connection to watch for unlock */ + void *pUnlockArg; /* Argument to xUnlockNotify */ + void (*xUnlockNotify)(void **, int); /* Unlock notify callback */ + sqlite3 *pNextBlocked; /* Next in list of all blocked connections */ +#endif }; /* @@ -2749,6 +2760,17 @@ int sqlite3IsMemJournal(sqlite3_file *); u32 sqlite3Get4byte(const u8*); void sqlite3Put4byte(u8*, u32); +#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY + void sqlite3ConnectionBlocked(sqlite3 *, sqlite3 *); + void sqlite3ConnectionUnlocked(sqlite3 *db); + void sqlite3ConnectionClosed(sqlite3 *db); +#else + #define sqlite3ConnectionBlocked(x,y) + #define sqlite3ConnectionUnlocked(x) + #define sqlite3ConnectionClosed(x) +#endif + + #ifdef SQLITE_SSE #include "sseInt.h" #endif diff --git a/src/tclsqlite.c b/src/tclsqlite.c index 585360f568..a429662d89 100644 --- a/src/tclsqlite.c +++ b/src/tclsqlite.c @@ -12,7 +12,7 @@ ** A TCL Interface to SQLite. Append this file to sqlite3.c and ** compile the whole thing to build a TCL-enabled version of SQLite. ** -** $Id: tclsqlite.c,v 1.237 2009/02/17 16:29:11 danielk1977 Exp $ +** $Id: tclsqlite.c,v 1.238 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "tcl.h" #include @@ -109,6 +109,7 @@ struct SqliteDb { SqlFunc *pFunc; /* List of SQL functions */ Tcl_Obj *pUpdateHook; /* Update hook script (if any) */ Tcl_Obj *pRollbackHook; /* Rollback hook script (if any) */ + Tcl_Obj *pUnlockNotify; /* Unlock notify script (if any) */ SqlCollate *pCollate; /* List of SQL collation functions */ int rc; /* Return code of most recent sqlite3_exec() */ Tcl_Obj *pCollateNeeded; /* Collation needed script */ @@ -574,6 +575,31 @@ static void DbRollbackHandler(void *clientData){ } } +#ifdef SQLITE_TEST +static void setTestUnlockNotifyVars(Tcl_Interp *interp, int iArg, int nArg){ + char zBuf[64]; + sprintf(zBuf, "%d", iArg); + Tcl_SetVar(interp, "sqlite_unlock_notify_arg", zBuf, TCL_GLOBAL_ONLY); + sprintf(zBuf, "%d", nArg); + Tcl_SetVar(interp, "sqlite_unlock_notify_argcount", zBuf, TCL_GLOBAL_ONLY); +} +#else + #define setTestUnlockNotifyVars(x,y,z) +#endif + +static void DbUnlockNotify(void **apArg, int nArg){ + int i; + for(i=0; iinterp, i, nArg); + assert( pDb->pUnlockNotify); + Tcl_EvalObjEx(pDb->interp, pDb->pUnlockNotify, flags); + Tcl_DecrRefCount(pDb->pUnlockNotify); + pDb->pUnlockNotify = 0; + } +} + static void DbUpdateHandler( void *p, int op, @@ -993,8 +1019,8 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ "profile", "progress", "rekey", "restore", "rollback_hook", "status", "timeout", "total_changes", "trace", - "transaction", "update_hook", "version", - 0 + "transaction", "unlock_notify", "update_hook", + "version", 0 }; enum DB_enum { DB_AUTHORIZER, DB_BACKUP, DB_BUSY, @@ -1007,7 +1033,8 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ DB_PROFILE, DB_PROGRESS, DB_REKEY, DB_RESTORE, DB_ROLLBACK_HOOK, DB_STATUS, DB_TIMEOUT, DB_TOTAL_CHANGES, DB_TRACE, - DB_TRANSACTION, DB_UPDATE_HOOK, DB_VERSION, + DB_TRANSACTION, DB_UNLOCK_NOTIFY, DB_UPDATE_HOOK, + DB_VERSION, }; /* don't leave trailing commas on DB_enum, it confuses the AIX xlc compiler */ @@ -2450,6 +2477,42 @@ static int DbObjCmd(void *cd, Tcl_Interp *interp, int objc,Tcl_Obj *const*objv){ break; } + /* + ** $db unlock_notify ?script? + */ + case DB_UNLOCK_NOTIFY: { +#ifndef SQLITE_ENABLE_UNLOCK_NOTIFY + Tcl_AppendResult(interp, "unlock_notify not available in this build", 0); + rc = TCL_ERROR; +#else + if( objc!=2 && objc!=3 ){ + Tcl_WrongNumArgs(interp, 2, objv, "?SCRIPT?"); + rc = TCL_ERROR; + }else{ + void (*xNotify)(void **, int) = 0; + void *pNotifyArg = 0; + + if( pDb->pUnlockNotify ){ + Tcl_DecrRefCount(pDb->pUnlockNotify); + pDb->pUnlockNotify = 0; + } + + if( objc==3 ){ + xNotify = DbUnlockNotify; + pNotifyArg = (void *)pDb; + pDb->pUnlockNotify = objv[2]; + Tcl_IncrRefCount(pDb->pUnlockNotify); + } + + if( sqlite3_unlock_notify(pDb->db, xNotify, pNotifyArg) ){ + Tcl_AppendResult(interp, sqlite3_errmsg(pDb->db), 0); + rc = TCL_ERROR; + } + } +#endif + break; + } + /* ** $db update_hook ?script? ** $db rollback_hook ?script? diff --git a/src/test1.c b/src/test1.c index 326c67daf3..e3e8df1431 100644 --- a/src/test1.c +++ b/src/test1.c @@ -13,7 +13,7 @@ ** is not included in the SQLite library. It is used for automated ** testing of the SQLite library. ** -** $Id: test1.c,v 1.347 2009/02/03 16:51:25 danielk1977 Exp $ +** $Id: test1.c,v 1.348 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "sqliteInt.h" #include "tcl.h" @@ -125,6 +125,7 @@ const char *sqlite3TestErrorName(int rc){ case SQLITE_ABORT: zName = "SQLITE_ABORT"; break; case SQLITE_BUSY: zName = "SQLITE_BUSY"; break; case SQLITE_LOCKED: zName = "SQLITE_LOCKED"; break; + case SQLITE_LOCKED_SHAREDCACHE: zName = "SQLITE_LOCKED_SHAREDCACHE";break; case SQLITE_NOMEM: zName = "SQLITE_NOMEM"; break; case SQLITE_READONLY: zName = "SQLITE_READONLY"; break; case SQLITE_INTERRUPT: zName = "SQLITE_INTERRUPT"; break; @@ -3661,6 +3662,24 @@ static int test_step( return TCL_OK; } +static int test_sql( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + sqlite3_stmt *pStmt; + + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "STMT"); + return TCL_ERROR; + } + + if( getStmtPointer(interp, Tcl_GetString(objv[1]), &pStmt) ) return TCL_ERROR; + Tcl_SetResult(interp, (char *)sqlite3_sql(pStmt), TCL_VOLATILE); + return TCL_OK; +} + /* ** Usage: sqlite3_column_count STMT ** @@ -4816,6 +4835,40 @@ static int test_pcache_stats( return TCL_OK; } +static void test_unlock_notify_cb(void **aArg, int nArg){ + int ii; + for(ii=0; ii + +/* +** A pointer to an instance of this structure is passed as the user-context +** pointer when registering for an unlock-notify callback. +*/ +typedef struct UnlockNotification UnlockNotification; +struct UnlockNotification { + int fired; /* True after unlock event has occured */ + pthread_cond_t cond; /* Condition variable to wait on */ + pthread_mutex_t mutex; /* Mutex to protect structure */ +}; + +/* +** This function is an unlock-notify callback registered with SQLite. +*/ +static void blocking_step_notify(void **apArg, int nArg){ + int i; + for(i=0; imutex); + p->fired = 1; + pthread_cond_signal(&p->cond); + pthread_mutex_unlock(&p->mutex); + } +} + +/* +** This function is a wrapper around the SQLite function sqlite3_step(). +** It functions in the same way as step(), except that if a required +** shared-cache lock cannot be obtained, this function may block waiting for +** the lock to become available. In this scenario the normal API step() +** function always returns SQLITE_LOCKED. +** +** If this function returns SQLITE_LOCKED, the caller should rollback +** the current transaction (if any) and try again later. Otherwise, the +** system may become deadlocked. +*/ +int sqlite3_blocking_step(sqlite3_stmt *pStmt){ + int rc = SQLITE_OK; + + while( rc==SQLITE_OK && SQLITE_LOCKED==(rc = sqlite3_step(pStmt)) ){ + sqlite3 *db = sqlite3_db_handle(pStmt); + UnlockNotification un; + + /* Initialize the UnlockNotification structure. */ + un.fired = 0; + pthread_mutex_init(&un.mutex, 0); + pthread_cond_init(&un.cond, 0); + + rc = sqlite3_unlock_notify(db, blocking_step_notify, (void *)&un); + assert( rc==SQLITE_LOCKED || rc==SQLITE_OK ); + + /* The call to sqlite3_unlock_notify() always returns either + ** SQLITE_LOCKED or SQLITE_OK. + ** + ** If SQLITE_LOCKED was returned, then the system is deadlocked. In this + ** case this function needs to return SQLITE_LOCKED to the caller so + ** that it can roll back the current transaction. Simply leaving rc + ** as it is is enough to accomplish that, as the next test of the + ** while() condition above will fail and the current value of rc + ** (SQLITE_LOCKED) will be returned to the caller. sqlite3_reset() is + ** not called on the statement handle, so the caller can still use either + ** sqlite3_finalize() or reset() to collect the statement's error code + ** after this function returns. + ** + ** Otherwise, if SQLITE_OK was returned, do two things: + ** + ** 1) Reset the SQL statement. + ** 2) Block until the unlock-notify callback is invoked. + */ + if( rc==SQLITE_OK ){ + sqlite3_reset(pStmt); + pthread_mutex_lock(&un.mutex); + if( !un.fired ){ + pthread_cond_wait(&un.cond, &un.mutex); + } + pthread_mutex_unlock(&un.mutex); + } + + /* Destroy the mutex and condition variables created at the top of + ** the while loop. */ + pthread_cond_destroy(&un.cond); + pthread_mutex_destroy(&un.mutex); + } + + return rc; +} +/* END_SQLITE_BLOCKING_STEP */ + +/* +** Usage: sqlite3_blocking_step STMT +** +** Advance the statement to the next row. +*/ +static int blocking_step_proc( + void * clientData, + Tcl_Interp *interp, + int objc, + Tcl_Obj *CONST objv[] +){ + /* Functions from test1.c */ + void *sqlite3TestTextToPtr(const char *); + const char *sqlite3TestErrorName(int); + + sqlite3_stmt *pStmt; + int rc; + + if( objc!=2 ){ + Tcl_WrongNumArgs(interp, 1, objv, "STMT"); + return TCL_ERROR; + } + + pStmt = (sqlite3_stmt*)sqlite3TestTextToPtr(Tcl_GetString(objv[1])); + rc = sqlite3_blocking_step(pStmt); + + Tcl_SetResult(interp, (char *)sqlite3TestErrorName(rc), 0); + return TCL_OK; +} + +#endif +/* +** End of implementation of [sqlite3_blocking_step]. +************************************************************************/ + /* ** Register commands with the TCL interpreter. */ int SqlitetestThread_Init(Tcl_Interp *interp){ Tcl_CreateObjCommand(interp, "sqlthread", sqlthread_proc, 0, 0); Tcl_CreateObjCommand(interp, "clock_seconds", clock_seconds_proc, 0, 0); +#if defined(OS_UNIX) && defined(SQLITE_ENABLE_UNLOCK_NOTIFY) + Tcl_CreateObjCommand(interp, "sqlite3_blocking_step", blocking_step_proc,0,0); +#endif return TCL_OK; } #else diff --git a/src/vdbe.c b/src/vdbe.c index b5a991baea..26e84e32cf 100644 --- a/src/vdbe.c +++ b/src/vdbe.c @@ -43,7 +43,7 @@ ** in this file for details. If in doubt, do not deviate from existing ** commenting and indentation practices when changing or adding code. ** -** $Id: vdbe.c,v 1.824 2009/03/05 04:20:32 shane Exp $ +** $Id: vdbe.c,v 1.825 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "sqliteInt.h" #include "vdbeInt.h" @@ -4819,7 +4819,7 @@ case OP_TableLock: { assert( (p->btreeMask & (1<aDb[p1].pBt, pOp->p2, isWriteLock); - if( rc==SQLITE_LOCKED ){ + if( (rc&0xFF)==SQLITE_LOCKED ){ const char *z = pOp->p4.z; sqlite3SetString(&p->zErrMsg, db, "database table is locked: %s", z); } @@ -4834,8 +4834,8 @@ case OP_TableLock: { ** xBegin method for that table. ** ** Also, whether or not P4 is set, check that this is not being called from -** within a callback to a virtual table xSync() method. If it is, set the -** error code to SQLITE_LOCKED. +** within a callback to a virtual table xSync() method. If it is, the error +** code will be set to SQLITE_LOCKED. */ case OP_VBegin: { sqlite3_vtab *pVtab = pOp->p4.pVtab; diff --git a/src/vdbeaux.c b/src/vdbeaux.c index 64cd2f99ea..42e3f13c72 100644 --- a/src/vdbeaux.c +++ b/src/vdbeaux.c @@ -14,7 +14,7 @@ ** to version 2.8.7, all this code was combined into the vdbe.c source file. ** But that file was getting too big so this subroutines were split out. ** -** $Id: vdbeaux.c,v 1.441 2009/03/05 04:20:32 shane Exp $ +** $Id: vdbeaux.c,v 1.442 2009/03/16 13:19:36 danielk1977 Exp $ */ #include "sqliteInt.h" #include "vdbeInt.h" @@ -1744,6 +1744,14 @@ int sqlite3VdbeHalt(Vdbe *p){ p->rc = SQLITE_NOMEM; } + /* If the auto-commit flag is set to true, then any locks that were held + ** by connection db have now been released. Call sqlite3ConnectionUnlocked() + ** to invoke any required unlock-notify callbacks. + */ + if( db->autoCommit ){ + sqlite3ConnectionUnlocked(db); + } + return SQLITE_OK; } diff --git a/src/vtab.c b/src/vtab.c index 83c4a33950..4bf6785038 100644 --- a/src/vtab.c +++ b/src/vtab.c @@ -11,7 +11,7 @@ ************************************************************************* ** This file contains code used to help implement virtual tables. ** -** $Id: vtab.c,v 1.81 2008/12/10 19:26:24 drh Exp $ +** $Id: vtab.c,v 1.82 2009/03/16 13:19:36 danielk1977 Exp $ */ #ifndef SQLITE_OMIT_VIRTUALTABLE #include "sqliteInt.h" @@ -711,7 +711,7 @@ int sqlite3VtabBegin(sqlite3 *db, sqlite3_vtab *pVtab){ /* Special case: If db->aVTrans is NULL and db->nVTrans is greater ** than zero, then this function is being called from within a ** virtual module xSync() callback. It is illegal to write to - ** virtual module tables in this case, so return SQLITE_LOCKED. + ** virtual module tables in this case, so return SQLITE_MISUSE. */ if( sqlite3VtabInSync(db) ){ return SQLITE_LOCKED; diff --git a/test/backup.test b/test/backup.test index 61e57b75f6..24ba6996c8 100644 --- a/test/backup.test +++ b/test/backup.test @@ -11,7 +11,7 @@ # This file implements regression tests for SQLite library. The # focus of this file is testing the sqlite3_backup_XXX API. # -# $Id: backup.test,v 1.8 2009/02/12 17:01:50 danielk1977 Exp $ +# $Id: backup.test,v 1.9 2009/03/16 13:19:36 danielk1977 Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl @@ -736,7 +736,7 @@ do_test backup-7.2.1 { } {} do_test backup-7.2.2 { B step 5000 -} {SQLITE_LOCKED} +} {SQLITE_BUSY} do_test backup-7.2.3 { execsql { ROLLBACK } B step 5000 diff --git a/test/incrblob2.test b/test/incrblob2.test index 49e67502da..5f7c1d432c 100644 --- a/test/incrblob2.test +++ b/test/incrblob2.test @@ -12,7 +12,7 @@ # Test that it is possible to have two open blob handles on a single # blob object. # -# $Id: incrblob2.test,v 1.9 2008/10/03 08:44:54 danielk1977 Exp $ +# $Id: incrblob2.test,v 1.10 2009/03/16 13:19:36 danielk1977 Exp $ # set testdir [file dirname $argv0] @@ -265,7 +265,7 @@ ifcapable shared_cache { do_test incrblob2-5.3 { set blob [db incrblob t1 data 1] catchsql { INSERT INTO t1 VALUES(3, 'klmno') } db2 - } {1 {database is locked}} + } {1 {database table is locked}} do_test incrblob2-5.4 { close $blob diff --git a/test/notify1.test b/test/notify1.test new file mode 100644 index 0000000000..4869bc4b94 --- /dev/null +++ b/test/notify1.test @@ -0,0 +1,470 @@ +# 2009 March 04 +# +# 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 sqlite3_unlock_notify() API. +# +# $Id: notify1.test,v 1.1 2009/03/16 13:19:36 danielk1977 Exp $ + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +ifcapable !unlock_notify||!shared_cache { + finish_test + return +} +db close +set ::enable_shared_cache [sqlite3_enable_shared_cache 1] + +#------------------------------------------------------------------------- +# Warm body test. Test that an unlock-notify callback can be registered +# and that it is invoked. +# +do_test notify1-1.1 { + sqlite3 db test.db + sqlite3 db2 test.db + execsql { CREATE TABLE t1(a, b) } +} {} +do_test notify1-1.2 { + execsql { + BEGIN; + INSERT INTO t1 VALUES(1, 2); + } + catchsql { INSERT INTO t1 VALUES(3, 4) } db2 +} {1 {database table is locked}} +do_test notify1-1.3 { + set zScript "" + db2 unlock_notify { + set zScript "db2 eval { INSERT INTO t1 VALUES(3, 4) }" + } + execsql { SELECT * FROM t1 } +} {1 2} +do_test notify1-1.4 { + set zScript +} {} +do_test notify1-1.5 { + execsql { COMMIT } + eval $zScript + execsql { SELECT * FROM t1 } +} {1 2 3 4} + +#------------------------------------------------------------------------- +# The following tests, notify1-2.*, test that deadlock is detected +# correctly. +# +do_test notify1-2.1 { + execsql { + CREATE TABLE t2(a, b); + INSERT INTO t2 VALUES('I', 'II'); + } +} {} + +# +# Test for simple deadlock involving two database connections. +# +# 1. Grab a write-lock on t1 with [db]. Then grab a read-lock on t2 with [db2]. +# 2. Try to grab a read-lock on t1 with [db2] (fails). +# 3. Have [db2] wait on the read-lock it failed to obtain in step 2. +# 4. Try to grab a write-lock on t2 with [db] (fails). +# 5. Try to have [db] wait on the lock from step 4. Fails, as the system +# would be deadlocked (since [db2] is already waiting on [db], and this +# operation would have [db] wait on [db2]). +# +do_test notify1-2.2.1 { + execsql { + BEGIN; + INSERT INTO t1 VALUES(5, 6); + } + execsql { + BEGIN; + SELECT * FROM t2; + } db2 +} {I II} +do_test notify1-2.2.2 { + catchsql { SELECT * FROM t1 } db2 +} {1 {database table is locked: t1}} +do_test notify1-2.2.3 { + db2 unlock_notify {lappend unlock_notify db2} +} {} +do_test notify1-2.2.4 { + catchsql { INSERT INTO t2 VALUES('III', 'IV') } +} {1 {database table is locked: t2}} +do_test notify1-2.2.5 { + set rc [catch { db unlock_notify {lappend unlock_notify db} } msg] + list $rc $msg +} {1 {database is deadlocked}} + +# +# Test for slightly more complex deadlock involving three database +# connections: db, db2 and db3. +# +do_test notify1-2.3.1 { + db close + db2 close + file delete -force test.db test2.db test3.db + foreach con {db db2 db3} { + sqlite3 $con test.db + $con eval { ATTACH 'test2.db' AS aux2 } + $con eval { ATTACH 'test3.db' AS aux3 } + } + execsql { + CREATE TABLE main.t1(a, b); + CREATE TABLE aux2.t2(a, b); + CREATE TABLE aux3.t3(a, b); + } +} {} +do_test notify1-2.3.2 { + execsql { BEGIN ; INSERT INTO t1 VALUES(1, 2) } db + execsql { BEGIN ; INSERT INTO t2 VALUES(1, 2) } db2 + execsql { BEGIN ; INSERT INTO t3 VALUES(1, 2) } db3 +} {} +do_test notify1-2.3.3 { + catchsql { SELECT * FROM t2 } db +} {1 {database table is locked: t2}} +do_test notify1-2.3.4 { + catchsql { SELECT * FROM t3 } db2 +} {1 {database table is locked: t3}} +do_test notify1-2.3.5 { + catchsql { SELECT * FROM t1 } db3 +} {1 {database table is locked: t1}} +do_test notify1-2.3.6 { + set lUnlock [list] + db unlock_notify {lappend lUnlock db} + db2 unlock_notify {lappend lUnlock db2} +} {} +do_test notify1-2.3.7 { + set rc [catch { db3 unlock_notify {lappend lUnlock db3} } msg] + list $rc $msg +} {1 {database is deadlocked}} +do_test notify1-2.3.8 { + execsql { COMMIT } + set lUnlock +} {} +do_test notify1-2.3.9 { + db3 unlock_notify {lappend lUnlock db3} + set lUnlock +} {db3} +do_test notify1-2.3.10 { + execsql { COMMIT } db2 + set lUnlock +} {db3 db} +do_test notify1-2.3.11 { + execsql { COMMIT } db3 + set lUnlock +} {db3 db db2} +catch { db3 close } +catch { db2 close } +catch { db close } + +#------------------------------------------------------------------------- +# The following tests, notify1-3.* and notify1-4.*, test that callbacks +# can be issued when there are many (>16) connections waiting on a single +# unlock event. +# +foreach {tn nConn} {3 20 4 76} { + do_test notify1-$tn.1 { + sqlite3 db test.db + execsql { + BEGIN; + INSERT INTO t1 VALUES('a', 'b'); + } + } {} + set lUnlock [list] + set lUnlockFinal [list] + for {set ii 1} {$ii <= $nConn} {incr ii} { + do_test notify1-$tn.2.$ii.1 { + set cmd "db$ii" + sqlite3 $cmd test.db + catchsql { SELECT * FROM t1 } $cmd + } {1 {database table is locked: t1}} + do_test notify1-$tn.2.$ii.2 { + $cmd unlock_notify "lappend lUnlock $ii" + } {} + lappend lUnlockFinal $ii + } + do_test notify1-$tn.3 { + set lUnlock + } {} + do_test notify1-$tn.4 { + execsql {COMMIT} + lsort -integer $lUnlock + } $lUnlockFinal + do_test notify1-$tn.5 { + for {set ii 1} {$ii <= $nConn} {incr ii} { + "db$ii" close + } + } {} +} +db close + +#------------------------------------------------------------------------- +# These tests, notify1-5.*, test that a malloc() failure that occurs while +# allocating an array to use as an argument to an unlock-notify callback +# is handled correctly. +# +source $testdir/malloc_common.tcl +breakpoint +do_malloc_test notify1-5 -tclprep { + set ::lUnlock [list] + execsql { + CREATE TABLE t1(a, b); + BEGIN; + INSERT INTO t1 VALUES('a', 'b'); + } + for {set ii 1} {$ii <= 60} {incr ii} { + set cmd "db$ii" + sqlite3 $cmd test.db + catchsql { SELECT * FROM t1 } $cmd + $cmd unlock_notify "lappend ::lUnlock $ii" + } +} -sqlbody { + COMMIT; +} -cleanup { + # One of two things should have happened: + # + # 1) The transaction opened by [db] was not committed. No unlock-notify + # callbacks were invoked, OR + # 2) The transaction opened by [db] was committed and 60 unlock-notify + # callbacks were invoked. + # + do_test notify1-5.systemstate { + expr { ([llength $::lUnlock]==0 && [sqlite3_get_autocommit db]==0) + || ([llength $::lUnlock]==60 && [sqlite3_get_autocommit db]==1) + } + } {1} + for {set ii 1} {$ii <= 60} {incr ii} { "db$ii" close } +} + +#------------------------------------------------------------------------- +# Test cases notify1-6.* test cases where the following occur: +# +# notify1-6.1.*: Test encountering an SQLITE_LOCKED error when the +# "blocking connection" has already been set by a previous +# SQLITE_LOCKED. +# +# notify1-6.2.*: Test encountering an SQLITE_LOCKED error when already +# waiting on an unlock-notify callback. +# +# notify1-6.3.*: Test that if an SQLITE_LOCKED error is encountered while +# already waiting on an unlock-notify callback, and then +# the blocker that caused the SQLITE_LOCKED commits its +# transaction, the unlock-notify callback is not invoked. +# +# notify1-6.4.*: Like 6.3.*, except that instead of the second blocker +# committing its transaction, the first does. The +# unlock-notify callback is therefore invoked. +# +db close +do_test notify1-6.1.1 { + file delete -force test.db test2.db + foreach conn {db db2 db3} { + sqlite3 $conn test.db + execsql { ATTACH 'test2.db' AS two } $conn + } + execsql { + CREATE TABLE t1(a, b); + CREATE TABLE two.t2(a, b); + } + execsql { + BEGIN; + INSERT INTO t1 VALUES(1, 2); + } db2 + execsql { + BEGIN; + INSERT INTO t2 VALUES(1, 2); + } db3 +} {} +do_test notify1-6.1.2 { + catchsql { SELECT * FROM t2 } +} {1 {database table is locked: t2}} +do_test notify1-6.1.3 { + catchsql { SELECT * FROM t1 } +} {1 {database table is locked: t1}} + +do_test notify1-6.2.1 { + set unlocked 0 + db unlock_notify {set unlocked 1} + set unlocked +} {0} +do_test notify1-6.2.2 { + catchsql { SELECT * FROM t2 } +} {1 {database table is locked: t2}} +do_test notify1-6.2.3 { + execsql { COMMIT } db2 + set unlocked +} {1} + +do_test notify1-6.3.1 { + execsql { + BEGIN; + INSERT INTO t1 VALUES(3, 4); + } db2 +} {} +do_test notify1-6.3.2 { + catchsql { SELECT * FROM t1 } +} {1 {database table is locked: t1}} +do_test notify1-6.3.3 { + set unlocked 0 + db unlock_notify {set unlocked 1} + set unlocked +} {0} +do_test notify1-6.3.4 { + catchsql { SELECT * FROM t2 } +} {1 {database table is locked: t2}} +do_test notify1-6.3.5 { + execsql { COMMIT } db3 + set unlocked +} {0} + +do_test notify1-6.4.1 { + execsql { + BEGIN; + INSERT INTO t2 VALUES(3, 4); + } db3 + catchsql { SELECT * FROM t2 } +} {1 {database table is locked: t2}} +do_test notify1-6.4.2 { + execsql { COMMIT } db2 + set unlocked +} {1} +do_test notify1-6.4.3 { + execsql { COMMIT } db3 +} {} +db close +db2 close +db3 close + +#------------------------------------------------------------------------- +# Test cases notify1-7.* tests that when more than one distinct +# unlock-notify function is registered, all are invoked correctly. +# +proc unlock_notify {} { + incr ::unlock_notify +} +do_test notify1-7.1 { + foreach conn {db db2 db3} { + sqlite3 $conn test.db + } + execsql { + BEGIN; + INSERT INTO t1 VALUES(5, 6); + } +} {} +do_test notify1-7.2 { + catchsql { SELECT * FROM t1 } db2 +} {1 {database table is locked: t1}} +do_test notify1-7.3 { + catchsql { SELECT * FROM t1 } db3 +} {1 {database table is locked: t1}} +do_test notify1-7.4 { + set unlock_notify 0 + db2 unlock_notify unlock_notify + sqlite3_unlock_notify db3 +} {SQLITE_OK} +do_test notify1-7.5 { + set unlock_notify +} {0} +do_test notify1-7.6 { + execsql { COMMIT } + set unlock_notify +} {2} + +#------------------------------------------------------------------------- +# Test cases notify1-8.* tests that the correct SQLITE_LOCKED extended +# error code is returned in various scenarios. +# +do_test notify1-8.1 { + execsql { + BEGIN; + INSERT INTO t1 VALUES(7, 8); + } + sqlite3_extended_result_codes db2 1 + catchsql { SELECT * FROM t1 } db2 +} {1 {database table is locked: t1}} +do_test notify1-8.2 { + sqlite3_extended_errcode db2 +} {SQLITE_LOCKED_SHAREDCACHE} + +do_test notify1-8.3 { + execsql { + COMMIT; + BEGIN EXCLUSIVE; + } + catchsql { SELECT * FROM t1 } db2 +} {1 {database schema is locked: main}} +do_test notify1-8.4 { + sqlite3_extended_errcode db2 +} {SQLITE_LOCKED_SHAREDCACHE} + +do_test notify1-8.X { + execsql { COMMIT } +} {} + +#------------------------------------------------------------------------- +# Test cases notify1-9.* test the shared-cache 'pending-lock' feature. +# +do_test notify1-9.1 { + execsql { + CREATE TABLE t2(a, b); + BEGIN; + SELECT * FROM t1; + } db2 +} {1 2 3 4 5 6 7 8} +do_test notify1-9.2 { + execsql { SELECT * FROM t1 } db3 +} {1 2 3 4 5 6 7 8} +do_test notify1-9.3 { + catchsql { + BEGIN; + INSERT INTO t1 VALUES(9, 10); + } +} {1 {database table is locked: t1}} +do_test notify1-9.4 { + catchsql { SELECT * FROM t2 } db3 +} {1 {database table is locked}} +do_test notify1-9.5 { + execsql { COMMIT } db2 + execsql { SELECT * FROM t2 } db3 +} {} +do_test notify1-9.6 { + execsql { COMMIT } +} {} + +do_test notify1-9.7 { + execsql { + BEGIN; + SELECT * FROM t1; + } db2 +} {1 2 3 4 5 6 7 8} +do_test notify1-9.8 { + execsql { SELECT * FROM t1 } db3 +} {1 2 3 4 5 6 7 8} +do_test notify1-9.9 { + catchsql { + BEGIN; + INSERT INTO t1 VALUES(9, 10); + } +} {1 {database table is locked: t1}} +do_test notify1-9.10 { + catchsql { SELECT * FROM t2 } db3 +} {1 {database table is locked}} +do_test notify1-9.11 { + execsql { COMMIT } + execsql { SELECT * FROM t2 } db3 +} {} +do_test notify1-9.12 { + execsql { COMMIT } db2 +} {} + +db close +db2 close +db3 close +sqlite3_enable_shared_cache $::enable_shared_cache +finish_test diff --git a/test/notify2.test b/test/notify2.test new file mode 100644 index 0000000000..c6df6e53aa --- /dev/null +++ b/test/notify2.test @@ -0,0 +1,207 @@ +# 2009 March 04 +# +# 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. +# +#*********************************************************************** +# +# $Id: notify2.test,v 1.1 2009/03/16 13:19:36 danielk1977 Exp $ + +set testdir [file dirname $argv0] +source $testdir/tester.tcl + +# The tests in this file test the sqlite3_blocking_step() function in +# test_thread.c. sqlite3_blocking_step() is not an SQLite API function, +# it is just a demonstration of how the sqlite3_unlock_notify() function +# can be used to synchronize multi-threaded access to SQLite databases +# in shared-cache mode. +# +# Since the implementation of sqlite3_blocking_step() is included on the +# website as example code, it is important to test that it works. +# +# notify2-1.*: +# +# This test uses $nThread threads. Each thread opens the main database +# and attaches two other databases. Each database contains a single table. +# +# Each thread repeats transactions over and over for 20 seconds. Each +# transaction consists of 3 operations. Each operation is either a read +# or a write of one of the tables. The read operations verify an invariant +# to make sure that things are working as expected. If an SQLITE_LOCKED +# error is returned the current transaction is rolled back immediately. +# +# This exercise is repeated twice, once using sqlite3_step(), and the +# other using sqlite3_blocking_step(). The results are compared to ensure +# that sqlite3_blocking_step() resulted in higher transaction throughput. +# + +if {[info commands sqlite3_blocking_step] eq ""} { + finish_test + return +} +db close +set ::enable_shared_cache [sqlite3_enable_shared_cache 1] +source $testdir/thread_common.tcl + +# Number of threads to run simultaneously. +# +set nThread 3 +set nSecond 5 + +# The Tcl script executed by each of the $nThread threads used by this test. +# +set ThreadProgram { + + # Proc used by threads to execute SQL. + # + proc execsql_blocking {db zSql} { + set lRes [list] + set rc SQLITE_OK + + while {$rc=="SQLITE_OK" && $zSql ne ""} { + set STMT [sqlite3_prepare_v2 $db $zSql -1 zSql] + while {[set rc [$::xStep $STMT]] eq "SQLITE_ROW"} { + for {set i 0} {$i < [sqlite3_column_count $STMT]} {incr i} { + lappend lRes [sqlite3_column_text $STMT 0] + } + } + set rc [sqlite3_finalize $STMT] + } + + if {$rc != "SQLITE_OK"} { error "$rc [sqlite3_errmsg $db]" } + return $lRes + } + + proc select_one {args} { + set n [llength $args] + lindex $args [expr int($n*rand())] + } + + # Open a database connection. Attach the two auxillary databases. + set ::DB [sqlite3_open test.db] + execsql_blocking $::DB { + ATTACH 'test2.db' AS aux2; + ATTACH 'test3.db' AS aux3; + } + + # This loop runs for ~20 seconds. + # + set iStart [clock_seconds] + while { ([clock_seconds]-$iStart) < $nSecond } { + + # Each transaction does 3 operations. Each operation is either a read + # or write of a randomly selected table (t1, t2 or t3). Set the variables + # $SQL(1), $SQL(2) and $SQL(3) to the SQL commands used to implement + # each operation. + # + for {set ii 1} {$ii <= 3} {incr ii} { + set SQL($ii) [string map [list xxx [select_one t1 t2 t3]] [select_one { + SELECT + (SELECT b FROM xxx WHERE a=(SELECT max(a) FROM xxx))==total(a) + FROM xxx WHERE a!=(SELECT max(a) FROM xxx); + } { + DELETE FROM xxx WHERE a<(SELECT max(a)-100 FROM xxx); + INSERT INTO xxx SELECT NULL, total(a) FROM xxx; + }]] + } + + # Execute the SQL transaction. + # + set rc [catch { execsql_blocking $::DB " + BEGIN; + $SQL(1); + $SQL(2); + $SQL(3); + COMMIT; + " + } msg] + + if {$rc && [string match "SQLITE_LOCKED*" $msg]} { + # Hit an SQLITE_LOCKED error. Rollback the current transaction. + execsql_blocking $::DB ROLLBACK + } elseif {$rc} { + # Hit some other kind of error. This is a malfunction. + error $msg + } else { + # No error occured. Check that any SELECT statements in the transaction + # returned "1". Otherwise, the invariant was false, indicating that + # some malfunction has occured. + foreach r $msg { if {$r != 1} { puts "Invariant check failed: $msg" } } + } + } + + # Close the database connection and return 0. + # + sqlite3_close $::DB + expr 0 +} + +foreach {iTest xStep} {1 sqlite3_blocking_step 2 sqlite3_step} { + file delete -force test.db test2.db test3.db + + set ThreadSetup "set xStep $xStep ; set nSecond $nSecond" + + # Set up the database schema used by this test. Each thread opens file + # test.db as the main database, then attaches files test2.db and test3.db + # as auxillary databases. Each file contains a single table (t1, t2 and t3, in + # files test.db, test2.db and test3.db, respectively). + # + do_test notify2-$iTest.1.1 { + sqlite3 db test.db + execsql { + ATTACH 'test2.db' AS aux2; + ATTACH 'test3.db' AS aux3; + CREATE TABLE main.t1(a INTEGER PRIMARY KEY, b); + CREATE TABLE aux2.t2(a INTEGER PRIMARY KEY, b); + CREATE TABLE aux3.t3(a INTEGER PRIMARY KEY, b); + INSERT INTO t1 SELECT NULL, 0; + INSERT INTO t2 SELECT NULL, 0; + INSERT INTO t3 SELECT NULL, 0; + } + } {} + do_test notify2-$iTest.1.2 { + db close + } {} + + + # Launch $nThread threads. Then wait for them to finish. + # + puts "Running $xStep test for $nSecond seconds" + unset -nocomplain finished + for {set ii 0} {$ii < $nThread} {incr ii} { + thread_spawn finished($ii) $ThreadSetup $ThreadProgram + } + for {set ii 0} {$ii < $nThread} {incr ii} { + do_test notify2-$iTest.2.$ii { + if {![info exists finished($ii)]} { vwait finished($ii) } + set finished($ii) + } {0} + } + + # Count the total number of succesful writes. + do_test notify2-$iTest.3.1 { + sqlite3 db test.db + execsql { + ATTACH 'test2.db' AS aux2; + ATTACH 'test3.db' AS aux3; + } + set anWrite($xStep) [execsql { + SELECT (SELECT max(a) FROM t1) + + (SELECT max(a) FROM t2) + + (SELECT max(a) FROM t3) + }] + db close + } {} +} + +do_test notify2-3 { + expr {$anWrite(sqlite3_blocking_step) > $anWrite(sqlite3_step)} +} {1} + +sqlite3_enable_shared_cache $::enable_shared_cache +finish_test + diff --git a/test/shared.test b/test/shared.test index d57f54e739..9b04946420 100644 --- a/test/shared.test +++ b/test/shared.test @@ -9,7 +9,7 @@ # #*********************************************************************** # -# $Id: shared.test,v 1.35 2008/11/21 00:10:35 aswift Exp $ +# $Id: shared.test,v 1.36 2009/03/16 13:19:36 danielk1977 Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl @@ -875,7 +875,7 @@ do_test shared-$av.11.4 { } {0 {}} do_test shared-$av.11.5 { catchsql {INSERT INTO abc2 VALUES(1, 2, 3);} db2 -} {1 {database is locked}} +} {1 {database table is locked}} do_test shared-$av.11.6 { catchsql {SELECT * FROM abc2} } {0 {}} diff --git a/test/tclsqlite.test b/test/tclsqlite.test index a8ac08ddf6..e752aa9fac 100644 --- a/test/tclsqlite.test +++ b/test/tclsqlite.test @@ -15,7 +15,7 @@ # interface is pretty well tested. This file contains some addition # tests for fringe issues that the main test suite does not cover. # -# $Id: tclsqlite.test,v 1.72 2009/02/04 22:46:47 drh Exp $ +# $Id: tclsqlite.test,v 1.73 2009/03/16 13:19:36 danielk1977 Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl @@ -35,7 +35,7 @@ do_test tcl-1.1 { do_test tcl-1.2 { set v [catch {db bogus} msg] lappend v $msg -} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, update_hook, or version}} +} {1 {bad option "bogus": must be authorizer, backup, busy, cache, changes, close, collate, collation_needed, commit_hook, complete, copy, enable_load_extension, errorcode, eval, exists, function, incrblob, interrupt, last_insert_rowid, nullvalue, onecolumn, profile, progress, rekey, restore, rollback_hook, status, timeout, total_changes, trace, transaction, unlock_notify, update_hook, or version}} do_test tcl-1.2.1 { set v [catch {db cache bogus} msg] lappend v $msg diff --git a/test/tkt2854.test b/test/tkt2854.test index 7bff008e19..862524a775 100644 --- a/test/tkt2854.test +++ b/test/tkt2854.test @@ -9,7 +9,7 @@ # #*********************************************************************** # -# $Id: tkt2854.test,v 1.3 2008/07/12 14:52:21 drh Exp $ +# $Id: tkt2854.test,v 1.4 2009/03/16 13:19:36 danielk1977 Exp $ set testdir [file dirname $argv0] source $testdir/tester.tcl @@ -56,7 +56,7 @@ do_test tkt2854-1.2 { } {} do_test tkt2854-1.3 { catchsql { BEGIN EXCLUSIVE } db -} {1 {database is locked}} +} {1 {database table is locked}} do_test tkt2854-1.4 { execsql { SELECT * FROM abc } db3 } {} @@ -99,10 +99,10 @@ do_test tkt2854-1.11 { } {SQLITE_ERROR SQLITE_LOCKED} do_test tkt2854-1.12 { list [sqlite3_step $::STMT2] [sqlite3_finalize $::STMT2] -} {SQLITE_BUSY SQLITE_BUSY} +} {SQLITE_ERROR SQLITE_LOCKED} do_test tkt2854-1.13 { list [sqlite3_step $::STMT3] [sqlite3_finalize $::STMT3] -} {SQLITE_BUSY SQLITE_BUSY} +} {SQLITE_ERROR SQLITE_LOCKED} do_test tkt2854-1.14 { # A regular "BEGIN" doesn't touch any databases. So it succeeds. list [sqlite3_step $::STMT4] [sqlite3_finalize $::STMT4] @@ -136,7 +136,7 @@ do_test tkt2854-1.19 { do_test tkt2854-1.20 { execsql {BEGIN IMMEDIATE} db4 catchsql {BEGIN EXCLUSIVE} db -} {1 {database is locked}} +} {1 {database table is locked}} do_test tkt2854-1.21 { execsql {SELECT * FROM abc} db2 } {} diff --git a/tool/mksqlite3c.tcl b/tool/mksqlite3c.tcl index 739f27e33d..857ecfb7f5 100644 --- a/tool/mksqlite3c.tcl +++ b/tool/mksqlite3c.tcl @@ -287,6 +287,7 @@ foreach file { complete.c main.c + notify.c fts3.c fts3_expr.c