Index: main.mk ================================================================== --- main.mk +++ main.mk @@ -67,10 +67,11 @@ memjournal.o \ mutex.o mutex_noop.o mutex_unix.o mutex_w32.o \ notify.o opcodes.o os.o os_unix.o os_win.o \ pager.o pcache.o pcache1.o pragma.o prepare.o printf.o \ random.o resolve.o rowset.o rtree.o select.o sqlite3rbu.o status.o \ + server.o \ table.o threads.o tokenize.o treeview.o trigger.o \ update.o userauth.o util.o vacuum.o \ vdbeapi.o vdbeaux.o vdbeblob.o vdbemem.o vdbesort.o \ vdbetrace.o wal.o walker.o where.o wherecode.o whereexpr.o \ utf.o vtab.o @@ -142,10 +143,12 @@ $(TOP)/src/printf.c \ $(TOP)/src/random.c \ $(TOP)/src/resolve.c \ $(TOP)/src/rowset.c \ $(TOP)/src/select.c \ + $(TOP)/src/server.c \ + $(TOP)/src/server.h \ $(TOP)/src/status.c \ $(TOP)/src/shell.c \ $(TOP)/src/sqlite.h.in \ $(TOP)/src/sqlite3ext.h \ $(TOP)/src/sqliteInt.h \ Index: src/pager.c ================================================================== --- src/pager.c +++ src/pager.c @@ -704,10 +704,13 @@ PCache *pPCache; /* Pointer to page cache object */ #ifndef SQLITE_OMIT_WAL Wal *pWal; /* Write-ahead log used by "journal_mode=wal" */ char *zWal; /* File name for write-ahead log */ #endif +#ifdef SQLITE_SERVER_EDITION + Server *pServer; +#endif }; /* ** Indexes for use with Pager.aStat[]. The Pager.aStat[] array contains ** the values accessed by passing SQLITE_DBSTATUS_CACHE_HIT, CACHE_MISS @@ -833,10 +836,16 @@ # define pagerRollbackWal(x) 0 # define pagerWalFrames(v,w,x,y) 0 # define pagerOpenWalIfPresent(z) SQLITE_OK # define pagerBeginReadTransaction(z) SQLITE_OK #endif + +#ifdef SQLITE_SERVER_EDITION +# define pagerIsServer(x) ((x)->pServer!=0) +#else +# define pagerIsServer(x) 0 +#endif #ifndef NDEBUG /* ** Usage: ** @@ -1130,10 +1139,11 @@ int rc = SQLITE_OK; assert( !pPager->exclusiveMode || pPager->eLock==eLock ); assert( eLock==NO_LOCK || eLock==SHARED_LOCK ); assert( eLock!=NO_LOCK || pagerUseWal(pPager)==0 ); + assert( eLock!=NO_LOCK || pagerIsServer(pPager)==0 ); if( isOpen(pPager->fd) ){ assert( pPager->eLock>=eLock ); rc = pPager->noLock ? SQLITE_OK : sqlite3OsUnlock(pPager->fd, eLock); if( pPager->eLock!=UNKNOWN_LOCK ){ pPager->eLock = (u8)eLock; @@ -1805,10 +1815,16 @@ sqlite3BitvecDestroy(pPager->pInJournal); pPager->pInJournal = 0; releaseAllSavepoints(pPager); +#ifdef SQLITE_SERVER_EDITION + if( pagerIsServer(pPager) ){ + sqlite3ServerEnd(pPager->pServer); + pPager->eState = PAGER_OPEN; + }else +#endif if( pagerUseWal(pPager) ){ assert( !isOpen(pPager->jfd) ); sqlite3WalEndReadTransaction(pPager->pWal); pPager->eState = PAGER_OPEN; }else if( !pPager->exclusiveMode ){ @@ -2103,10 +2119,15 @@ if( rc==SQLITE_OK && bCommit && isOpen(pPager->fd) ){ rc = sqlite3OsFileControl(pPager->fd, SQLITE_FCNTL_COMMIT_PHASETWO, 0); if( rc==SQLITE_NOTFOUND ) rc = SQLITE_OK; } +#ifdef SQLITE_SERVER_EDITION + if( pagerIsServer(pPager) ){ + rc2 = sqlite3ServerReleaseWriteLocks(pPager->pServer); + }else +#endif if( !pPager->exclusiveMode && (!pagerUseWal(pPager) || sqlite3WalExclusiveMode(pPager->pWal, 0)) ){ rc2 = pagerUnlockDb(pPager, SHARED_LOCK); pPager->changeCountDone = 0; @@ -4076,10 +4097,17 @@ assert( db || pPager->pWal==0 ); sqlite3WalClose(pPager->pWal, db, pPager->ckptSyncFlags, pPager->pageSize, (db && (db->flags & SQLITE_NoCkptOnClose) ? 0 : pTmp) ); pPager->pWal = 0; +#endif +#ifdef SQLITE_SERVER_EDITION + if( pPager->pServer ){ + sqlite3ServerDisconnect(pPager->pServer, pPager->fd); + pPager->pServer = 0; + sqlite3_free(pPager->zJournal); + } #endif pager_reset(pPager); if( MEMDB ){ pager_unlock(pPager); }else{ @@ -5048,10 +5076,82 @@ } } return rc; } + +#ifdef SQLITE_SERVER_EDITION +static int pagerServerConnect(Pager *pPager){ + int rc = SQLITE_OK; + if( pPager->tempFile==0 ){ + int iClient = 0; + pPager->noLock = 1; + pPager->journalMode = PAGER_JOURNALMODE_PERSIST; + rc = sqlite3ServerConnect(pPager, &pPager->pServer, &iClient); + if( rc==SQLITE_OK ){ + pPager->zJournal = sqlite3_mprintf( + "%s-journal%d", pPager->zFilename, iClient + ); + if( pPager->zJournal==0 ){ + rc = SQLITE_NOMEM_BKPT; + } + } + } + return rc; +} + +int sqlite3PagerRollbackJournal(Pager *pPager, int iClient){ + int rc; + char *zJrnl = sqlite3_mprintf("%s-journal%d", pPager->zFilename, iClient); + + if( zJrnl ){ + int bExists = 0; + sqlite3_file *jfd = 0; + sqlite3_vfs * const pVfs = pPager->pVfs; + + rc = sqlite3OsAccess(pVfs, zJrnl, SQLITE_ACCESS_EXISTS, &bExists); + if( rc==SQLITE_OK && bExists ){ + int flags = SQLITE_OPEN_READWRITE|SQLITE_OPEN_MAIN_JOURNAL; + rc = sqlite3OsOpenMalloc(pVfs, zJrnl, &jfd, flags, &flags); + } + assert( rc==SQLITE_OK || jfd==0 ); + if( jfd ){ + sqlite3_file *saved_jfd = pPager->jfd; + u8 saved_eState = pPager->eState; + u8 saved_eLock = pPager->eLock; + i64 saved_journalOff = pPager->journalOff; + i64 saved_journalHdr = pPager->journalHdr; + + pPager->eLock = EXCLUSIVE_LOCK; + pPager->eState = PAGER_WRITER_DBMOD; + pPager->jfd = jfd; + rc = pagerSyncHotJournal(pPager); + if( rc==SQLITE_OK ) rc = pager_playback(pPager, 1); + + pPager->jfd = saved_jfd; + pPager->eState = saved_eState; + pPager->eLock = saved_eLock; + pPager->journalOff = saved_journalOff; + pPager->journalHdr = saved_journalHdr; + + sqlite3OsCloseFree(jfd); + if( rc==SQLITE_OK ){ + rc = sqlite3OsDelete(pVfs, zJrnl, 0); + } + } + sqlite3_free(zJrnl); + }else{ + rc = SQLITE_NOMEM_BKPT; + } + + return rc; +} + +#else +# define pagerServerConnect(pPager) SQLITE_OK +#endif + /* ** This function is called to obtain a shared lock on the database file. ** It is illegal to call sqlite3PagerGet() until after this function ** has been successfully called. If a shared-lock is already held when @@ -5088,11 +5188,13 @@ assert( sqlite3PcacheRefCount(pPager->pPCache)==0 ); assert( assert_pager_state(pPager) ); assert( pPager->eState==PAGER_OPEN || pPager->eState==PAGER_READER ); assert( pPager->errCode==SQLITE_OK ); - if( !pagerUseWal(pPager) && pPager->eState==PAGER_OPEN ){ + if( !pagerUseWal(pPager) + && !pagerIsServer(pPager) + && pPager->eState==PAGER_OPEN ){ int bHotJournal = 1; /* True if there exists a hot journal-file */ assert( !MEMDB ); assert( pPager->tempFile==0 || pPager->eLock==EXCLUSIVE_LOCK ); @@ -5267,12 +5369,23 @@ */ rc = pagerOpenWalIfPresent(pPager); #ifndef SQLITE_OMIT_WAL assert( pPager->pWal==0 || rc==SQLITE_OK ); #endif + + if( rc==SQLITE_OK && pagerUseWal(pPager)==0 ){ + rc = pagerServerConnect(pPager); + } } +#ifdef SQLITE_SERVER_EDITION + if( pagerIsServer(pPager) ){ + assert( rc==SQLITE_OK ); + pager_reset(pPager); + rc = sqlite3ServerBegin(pPager->pServer); + }else +#endif if( pagerUseWal(pPager) ){ assert( rc==SQLITE_OK ); rc = pagerBeginReadTransaction(pPager); } @@ -5562,10 +5675,16 @@ Pager *pPager, /* The pager open on the database file */ Pgno pgno, /* Page number to fetch */ DbPage **ppPage, /* Write a pointer to the page here */ int flags /* PAGER_GET_XXX flags */ ){ +#ifdef SQLITE_SERVER_EDITION + if( pagerIsServer(pPager) ){ + int rc = sqlite3ServerLock(pPager->pServer, pgno, 0); + if( rc!=SQLITE_OK ) return rc; + } +#endif return pPager->xGet(pPager, pgno, ppPage, flags); } /* ** Acquire a page if it is already in the in-memory cache. Do @@ -5863,10 +5982,17 @@ ); assert( assert_pager_state(pPager) ); assert( pPager->errCode==0 ); assert( pPager->readOnly==0 ); CHECK_PAGE(pPg); + +#ifdef SQLITE_SERVER_EDITION + if( pagerIsServer(pPager) ){ + rc = sqlite3ServerLock(pPager->pServer, pPg->pgno, 1); + if( rc!=SQLITE_OK ) return rc; + } +#endif /* The journal file needs to be opened. Higher level routines have already ** obtained the necessary locks to begin the write-transaction, but the ** rollback journal might not yet be open. Open it now if this is the case. ** @@ -6142,11 +6268,14 @@ UNUSED_PARAMETER(isDirectMode); #else # define DIRECT_MODE isDirectMode #endif - if( !pPager->changeCountDone && ALWAYS(pPager->dbSize>0) ){ + if( 0==pagerIsServer(pPager) + && !pPager->changeCountDone + && ALWAYS(pPager->dbSize>0) + ){ PgHdr *pPgHdr; /* Reference to page 1 */ assert( !pPager->tempFile && isOpen(pPager->fd) ); /* Open page 1 of the file for writing. */ Index: src/pager.h ================================================================== --- src/pager.h +++ src/pager.h @@ -233,7 +233,11 @@ void enable_simulated_io_errors(void); #else # define disable_simulated_io_errors() # define enable_simulated_io_errors() #endif + +#ifdef SQLITE_SERVER_EDITION + int sqlite3PagerRollbackJournal(Pager*, int); +#endif #endif /* SQLITE_PAGER_H */ ADDED src/server.c Index: src/server.c ================================================================== --- /dev/null +++ src/server.c @@ -0,0 +1,485 @@ +/* +** 2017 April 24 +** +** 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. +** +************************************************************************* +*/ + +#include "sqliteInt.h" + +/* +** HMA file layout: +** +** 4 bytes - DMS slot. All connections read-lock this slot. +** +** 16*4 bytes - locking slots. Connections hold a read-lock on a locking slot +** when they are connected, a write lock when they have an open +** transaction. +** +** N*4 bytes - Page locking slots. N is HMA_PAGELOCK_SLOTS. +** +** Page lock slot format: +** +** Least significant HMA_CLIENT_SLOTS used for read-locks. If bit 0 is set, +** client 0 holds a read-lock. +** +** If (v) is the value of the locking slot and (v>>HMA_CLIENT_SLOTS) is +** not zero, then the write-lock holder is client ((v>>HMA_CLIENT_SLOTS)-1). +** +*/ + +#ifdef SQLITE_SERVER_EDITION + +#define HMA_CLIENT_SLOTS 16 +#define HMA_PAGELOCK_SLOTS (256*1024) + +#define HMA_FILE_SIZE (4 + 4*HMA_CLIENT_SLOTS + 4*HMA_PAGELOCK_SLOTS) + +#include "unistd.h" +#include "fcntl.h" +#include "sys/mman.h" +#include "sys/types.h" +#include "sys/stat.h" + +typedef struct ServerHMA ServerHMA; + +struct ServerGlobal { + sqlite3_mutex *mutex; + ServerHMA *pHma; +}; +static struct ServerGlobal g_server; + +/* +** There is one instance of the following structure for each distinct +** HMA file opened by clients within this process. +*/ +struct ServerHMA { + char *zName; /* hma file path */ + int fd; /* Fd open on hma file */ + int nClient; /* Current number of clients */ + Server *aClient[HMA_CLIENT_SLOTS]; /* Local (this process) clients */ + u32 *aMap; /* MMapped hma file */ + ServerHMA *pNext; /* Next HMA in this process */ + + dev_t st_dev; + ino_t st_ino; +}; + +struct Server { + ServerHMA *pHma; /* Hma file object */ + int iClient; /* Client id */ + Pager *pPager; /* Associated pager object */ + + int nAlloc; /* Allocated size of aLock[] array */ + int nLock; /* Number of entries in aLock[] */ + u32 *aLock; /* Mapped lock file */ +}; + +#define SERVER_WRITE_LOCK 3 +#define SERVER_READ_LOCK 2 +#define SERVER_NO_LOCK 1 + +static int posixLock(int fd, int iSlot, int eLock, int bBlock){ + int res; + struct flock l; + short aType[4] = {0, F_UNLCK, F_RDLCK, F_WRLCK}; + assert( eLock==SERVER_WRITE_LOCK + || eLock==SERVER_READ_LOCK + || eLock==SERVER_NO_LOCK + ); + memset(&l, 0, sizeof(l)); + l.l_type = aType[eLock]; + l.l_whence = SEEK_SET; + l.l_start = iSlot*sizeof(u32); + l.l_len = 1; + + res = fcntl(fd, (bBlock ? F_SETLKW : F_SETLK), &l); + return (res==0 ? SQLITE_OK : SQLITE_BUSY); +} + +static int serverMapFile(ServerHMA *p){ + assert( p->aMap==0 ); + p->aMap = mmap(0, HMA_FILE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, p->fd, 0); + if( p->aMap==0 ){ + return SQLITE_ERROR; + } + return SQLITE_OK; +} + + +static void serverDecrHmaRefcount(ServerHMA *pHma){ + if( pHma ){ + pHma->nClient--; + if( pHma->nClient<=0 ){ + ServerHMA **pp; + if( pHma->aMap ) munmap(pHma->aMap, HMA_FILE_SIZE); + if( pHma->fd>=0 ) close(pHma->fd); + for(pp=&g_server.pHma; *pp!=pHma; pp=&(*pp)->pNext); + *pp = pHma->pNext; + sqlite3_free(pHma); + } + } +} + + +static int serverOpenHma(Pager *pPager, const char *zPath, ServerHMA **ppHma){ + struct stat sStat; /* Structure populated by stat() */ + int res; /* result of stat() */ + int rc = SQLITE_OK; /* Return code */ + ServerHMA *pHma = 0; + + assert( sqlite3_mutex_held(g_server.mutex) ); + + res = stat(zPath, &sStat); + if( res!=0 ){ + sqlite3_log(SQLITE_CANTOPEN, "Failed to stat(%s)", zPath); + rc = SQLITE_ERROR; + }else{ + for(pHma=g_server.pHma; pHma; pHma=pHma->pNext){ + if( sStat.st_dev==pHma->st_dev && sStat.st_ino==pHma->st_ino ) break; + } + if( pHma==0 ){ + int nPath = strlen(zPath); + int nByte = sizeof(ServerHMA) + nPath+1 + 4; + + pHma = (ServerHMA*)sqlite3_malloc(nByte); + if( pHma==0 ){ + rc = SQLITE_NOMEM; + }else{ + int i; + memset(pHma, 0, nByte); + pHma->zName = (char*)&pHma[1]; + pHma->nClient = 1; + pHma->st_dev = sStat.st_dev; + pHma->st_ino = sStat.st_ino; + pHma->pNext = g_server.pHma; + g_server.pHma = pHma; + + memcpy(pHma->zName, zPath, nPath); + memcpy(&pHma->zName[nPath], "-hma", 5); + + pHma->fd = open(pHma->zName, O_RDWR|O_CREAT, 0644); + if( pHma->fd<0 ){ + sqlite3_log(SQLITE_CANTOPEN, "Failed to open(%s)", pHma->zName); + rc = SQLITE_ERROR; + } + + if( rc==SQLITE_OK ){ + /* Write-lock the DMS slot. If successful, initialize the hma file. */ + rc = posixLock(pHma->fd, 0, SERVER_WRITE_LOCK, 0); + if( rc==SQLITE_OK ){ + res = ftruncate(pHma->fd, HMA_FILE_SIZE); + if( res!=0 ){ + sqlite3_log(SQLITE_CANTOPEN, + "Failed to ftruncate(%s)", pHma->zName + ); + rc = SQLITE_ERROR; + } + if( rc==SQLITE_OK ){ + rc = serverMapFile(pHma); + } + if( rc==SQLITE_OK ){ + memset(pHma->aMap, 0, HMA_FILE_SIZE); + }else{ + rc = SQLITE_ERROR; + } + }else{ + rc = serverMapFile(pHma); + } + for(i=0; rc==SQLITE_OK && ifd, 0, SERVER_READ_LOCK, 1); + } + } + + if( rc!=SQLITE_OK ){ + serverDecrHmaRefcount(pHma); + pHma = 0; + } + } + }else{ + pHma->nClient++; + } + } + + *ppHma = pHma; + return rc; +} + +static u32 *serverPageLockSlot(Server *p, Pgno pgno){ + int iSlot = pgno % HMA_PAGELOCK_SLOTS; + return &p->pHma->aMap[1 + HMA_CLIENT_SLOTS + iSlot]; +} +static u32 *serverClientSlot(Server *p, int iClient){ + return &p->pHma->aMap[1 + iClient]; +} + +/* +** Close the "connection" and *-hma file. This deletes the object passed +** as the first argument. +*/ +void sqlite3ServerDisconnect(Server *p, sqlite3_file *dbfd){ + if( p->pHma ){ + ServerHMA *pHma = p->pHma; + sqlite3_mutex_enter(g_server.mutex); + if( p->iClient>=0 ){ + u32 *pSlot = serverClientSlot(p, p->iClient); + *pSlot = 0; + assert( pHma->aClient[p->iClient]==p ); + pHma->aClient[p->iClient] = 0; + posixLock(pHma->fd, p->iClient+1, SERVER_NO_LOCK, 0); + } + if( dbfd + && pHma->nClient==1 + && SQLITE_OK==sqlite3OsLock(dbfd, SQLITE_LOCK_EXCLUSIVE) + ){ + unlink(pHma->zName); + } + serverDecrHmaRefcount(pHma); + sqlite3_mutex_leave(g_server.mutex); + } + sqlite3_free(p->aLock); + sqlite3_free(p); +} + +static int serverRollbackClient(Server *p, int iBlock){ + int rc; + + sqlite3_log(SQLITE_NOTICE, "Rolling back failed client %d", iBlock); + + /* Roll back any journal file for client iBlock. */ + rc = sqlite3PagerRollbackJournal(p->pPager, iBlock); + + /* Clear any locks held by client iBlock from the HMA file. */ + if( rc==SQLITE_OK ){ + int i; + for(i=0; i>HMA_CLIENT_SLOTS)==iBlock+1 ){ + n = n & ((1<iClient = -1; + p->pPager = pPager; + + sqlite3_mutex_enter(g_server.mutex); + rc = serverOpenHma(pPager, zPath, &p->pHma); + + /* File is now mapped. Find a free client slot. */ + if( rc==SQLITE_OK ){ + int i; + Server **aClient = p->pHma->aClient; + int fd = p->pHma->fd; + for(i=0; iHMA_CLIENT_SLOTS ){ + rc = SQLITE_BUSY; + }else{ + u32 *pSlot = serverClientSlot(p, i); + *piClient = p->iClient = i; + aClient[i] = p; + *pSlot = 1; + } + } + } + + sqlite3_mutex_leave(g_server.mutex); + } + + if( rc!=SQLITE_OK ){ + sqlite3ServerDisconnect(p, 0); + p = 0; + } + *ppOut = p; + return rc; +} + +static int serverOvercomeLock(Server *p, int bWrite, u32 v, int *pbRetry){ + int rc = SQLITE_OK; + int bLocal = 0; + int iBlock = ((int)(v>>HMA_CLIENT_SLOTS))-1; + + if( iBlock<0 ){ + for(iBlock=0; iBlockiClient && (v & (1<pHma->aClient[iBlock] ){ + bLocal = 1; + }else{ + rc = posixLock(p->pHma->fd, iBlock+1, SERVER_WRITE_LOCK, 0); + } + + if( bLocal==0 && rc==SQLITE_OK ){ + rc = serverRollbackClient(p, iBlock); + + /* Release the lock on slot iBlock */ + posixLock(p->pHma->fd, iBlock+1, SERVER_NO_LOCK, 0); + if( rc==SQLITE_OK ){ + *pbRetry = 1; + } + }else{ + assert( rc==SQLITE_OK || rc==SQLITE_BUSY ); + rc = SQLITE_OK; + } + sqlite3_mutex_leave(g_server.mutex); + + return rc; +} + +/* +** Begin a transaction. +*/ +int sqlite3ServerBegin(Server *p){ + return posixLock(p->pHma->fd, p->iClient+1, SERVER_WRITE_LOCK, 0); +} + +/* +** End a transaction (and release all locks). +*/ +int sqlite3ServerEnd(Server *p){ + int i; + for(i=0; inLock; i++){ + u32 *pSlot = serverPageLockSlot(p, p->aLock[i]); + while( 1 ){ + u32 v = *pSlot; + u32 n = v; + if( (v>>HMA_CLIENT_SLOTS)==p->iClient+1 ){ + n = n & ((1 << HMA_CLIENT_SLOTS)-1); + } + n = n & ~(1 << p->iClient); + if( __sync_val_compare_and_swap(pSlot, v, n)==v ) break; + } + } + p->nLock = 0; + return posixLock(p->pHma->fd, p->iClient+1, SERVER_READ_LOCK, 0); +} + +/* +** Release all write-locks. +*/ +int sqlite3ServerReleaseWriteLocks(Server *p){ + int rc = SQLITE_OK; + return rc; +} + +/* +** Lock page pgno for reading (bWrite==0) or writing (bWrite==1). +*/ +int sqlite3ServerLock(Server *p, Pgno pgno, int bWrite){ + int rc = SQLITE_OK; + + /* Grow the aLock[] array, if required */ + if( p->nLock==p->nAlloc ){ + int nNew = p->nAlloc ? p->nAlloc*2 : 128; + u32 *aNew; + aNew = (u32*)sqlite3_realloc(p->aLock, sizeof(u32)*nNew); + if( aNew==0 ){ + rc = SQLITE_NOMEM_BKPT; + }else{ + p->aLock = aNew; + p->nAlloc = nNew; + } + } + if( rc==SQLITE_OK ){ + u32 *pSlot = serverPageLockSlot(p, pgno); + u32 v = *pSlot; + + /* Check if the required lock is already held. If so, exit this function + ** early. Otherwise, add an entry to the aLock[] array to record the fact + ** that the lock may need to be released. */ + if( bWrite ){ + int iLock = ((int)(v>>HMA_CLIENT_SLOTS)) - 1; + if( iLock==p->iClient ) goto server_lock_out; + if( iLock<0 ){ + p->aLock[p->nLock++] = pgno; + } + }else{ + if( v & (1<iClient) ) goto server_lock_out; + p->aLock[p->nLock++] = pgno; + } + + while( 1 ){ + u32 n; + + while( (bWrite && (v & ~(1 << p->iClient))) || (v >> HMA_CLIENT_SLOTS) ){ + int bRetry = 0; + rc = serverOvercomeLock(p, bWrite, v, &bRetry); + if( rc!=SQLITE_OK ) goto server_lock_out; + if( bRetry==0 ){ + /* There is a conflicting lock. Cannot obtain this lock. */ + sqlite3_log(SQLITE_BUSY_DEADLOCK, "Conflict at page %d", (int)pgno); + rc = SQLITE_BUSY_DEADLOCK; + goto server_lock_out; + } + v = *pSlot; + } + + if( bWrite ){ + n = v | ((p->iClient+1) << HMA_CLIENT_SLOTS); + }else{ + n = v | (1 << p->iClient); + } + if( __sync_val_compare_and_swap(pSlot, v, n)==v ) break; + v = *pSlot; + } + } + +server_lock_out: + return rc; +} + +#endif /* ifdef SQLITE_SERVER_EDITION */ Index: src/sqlite.h.in ================================================================== --- src/sqlite.h.in +++ src/sqlite.h.in @@ -495,10 +495,11 @@ #define SQLITE_IOERR_VNODE (SQLITE_IOERR | (27<<8)) #define SQLITE_IOERR_AUTH (SQLITE_IOERR | (28<<8)) #define SQLITE_LOCKED_SHAREDCACHE (SQLITE_LOCKED | (1<<8)) #define SQLITE_BUSY_RECOVERY (SQLITE_BUSY | (1<<8)) #define SQLITE_BUSY_SNAPSHOT (SQLITE_BUSY | (2<<8)) +#define SQLITE_BUSY_DEADLOCK (SQLITE_BUSY | (3<<8)) #define SQLITE_CANTOPEN_NOTEMPDIR (SQLITE_CANTOPEN | (1<<8)) #define SQLITE_CANTOPEN_ISDIR (SQLITE_CANTOPEN | (2<<8)) #define SQLITE_CANTOPEN_FULLPATH (SQLITE_CANTOPEN | (3<<8)) #define SQLITE_CANTOPEN_CONVPATH (SQLITE_CANTOPEN | (4<<8)) #define SQLITE_CORRUPT_VTAB (SQLITE_CORRUPT | (1<<8)) Index: src/sqliteInt.h ================================================================== --- src/sqliteInt.h +++ src/sqliteInt.h @@ -1099,10 +1099,11 @@ #include "vdbe.h" #include "pager.h" #include "pcache.h" #include "os.h" #include "mutex.h" +#include "server.h" /* The SQLITE_EXTRA_DURABLE compile-time option used to set the default ** synchronous setting to EXTRA. It is no longer supported. */ #ifdef SQLITE_EXTRA_DURABLE Index: src/vdbeaux.c ================================================================== --- src/vdbeaux.c +++ src/vdbeaux.c @@ -2637,25 +2637,26 @@ sqlite3VdbeEnter(p); /* Check for one of the special errors */ mrc = p->rc & 0xff; isSpecialError = mrc==SQLITE_NOMEM || mrc==SQLITE_IOERR - || mrc==SQLITE_INTERRUPT || mrc==SQLITE_FULL; + || mrc==SQLITE_INTERRUPT || mrc==SQLITE_FULL + || p->rc==SQLITE_BUSY_DEADLOCK; if( isSpecialError ){ - /* If the query was read-only and the error code is SQLITE_INTERRUPT, - ** no rollback is necessary. Otherwise, at least a savepoint - ** transaction must be rolled back to restore the database to a - ** consistent state. + /* If the query was read-only and the error code is SQLITE_INTERRUPT + ** or SQLITE_BUSY_SERVER, no rollback is necessary. Otherwise, at + ** least a savepoint transaction must be rolled back to restore the + ** database to a consistent state. ** ** Even if the statement is read-only, it is important to perform ** a statement or transaction rollback operation. If the error ** occurred while writing to the journal, sub-journal or database ** file as part of an effort to free up cache space (see function ** pagerStress() in pager.c), the rollback is required to restore ** the pager to a consistent state. */ - if( !p->readOnly || mrc!=SQLITE_INTERRUPT ){ + if( !p->readOnly || (mrc!=SQLITE_INTERRUPT && mrc!=SQLITE_BUSY) ){ if( (mrc==SQLITE_NOMEM || mrc==SQLITE_FULL) && p->usesStmtJournal ){ eStatementOp = SAVEPOINT_ROLLBACK; }else{ /* We are forced to roll back the active transaction. Before doing ** so, abort any other statements this handle currently has active. Index: test/permutations.test ================================================================== --- test/permutations.test +++ test/permutations.test @@ -270,10 +270,18 @@ } test_suite "fts5" -prefix "" -description { All FTS5 tests. } -files [glob -nocomplain $::testdir/../ext/fts5/test/*.test] + +test_suite "server" -prefix "" -description { + All server-edition tests. +} -files [ + test_set \ + [glob -nocomplain $::testdir/server*.test] \ + -exclude *server1.test +] test_suite "fts5-light" -prefix "" -description { All FTS5 tests. } -files [ test_set \ ADDED test/server2.test Index: test/server2.test ================================================================== --- /dev/null +++ test/server2.test @@ -0,0 +1,101 @@ +# 2017 April 25 +# +# 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 script is testing the server mode of SQLite. +# + + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix server2 + +#------------------------------------------------------------------------- +# Check that the *-hma file is deleted correctly. +# +do_execsql_test 1.0 { + CREATE TABLE t1(a, b); +} {} +do_test 1.1 { + file exists test.db-hma +} {1} +do_test 1.2 { + db close + file exists test.db-hma +} {0} +do_test 1.3 { + sqlite3 db test.db + db eval { CREATE TABLE t2(a, b) } + sqlite3 db2 test.db + db2 eval { CREATE TABLE t3(a, b) } + file exists test.db-hma +} {1} +do_test 1.4 { + db2 close + file exists test.db-hma +} {1} +do_test 1.5 { + db close + file exists test.db-hma +} {0} + + +#------------------------------------------------------------------------- +# +reset_db +sqlite3 db2 test.db + +do_execsql_test 2.0 { + CREATE TABLE t1(a, b); + CREATE TABLE t2(c, d); +} + +# Two concurrent transactions committed. +# +do_test 2.1 { + db eval { + BEGIN; + INSERT INTO t1 VALUES(1, 2); + } + db2 eval { + BEGIN; + INSERT INTO t2 VALUES(3, 4); + } +} {} +do_test 2.2 { + lsort [glob test.db*] +} {test.db test.db-hma test.db-journal0 test.db-journal1} +do_test 2.3.1 { db eval COMMIT } {} +do_test 2.3.2 { db2 eval COMMIT } {} +do_execsql_test 2.4 {SELECT * FROM t1, t2} {1 2 3 4} +do_test 2.5 { + lsort [glob test.db*] +} {test.db test.db-hma test.db-journal0 test.db-journal1} + +do_test 2.6 { + execsql {BEGIN} + execsql {INSERT INTO t1 VALUES(5, 6)} + + execsql {BEGIN} db2 + catchsql {INSERT INTO t1 VALUES(7, 8)} db2 +} {1 {database is locked}} +do_test 2.7 { + # Transaction is automatically rolled back in this case. + sqlite3_get_autocommit db2 +} {1} +do_test 2.8 { + execsql COMMIT + execsql { SELECT * FROM t1 } db2 +} {1 2 5 6} + + + +finish_test + ADDED test/server3.test Index: test/server3.test ================================================================== --- /dev/null +++ test/server3.test @@ -0,0 +1,45 @@ +# 2017 April 25 +# +# 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 script is testing the server mode of SQLite. +# + + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +source $testdir/lock_common.tcl +set testprefix server3 + +db close + +do_multiclient_test tn { + do_test $tn.1 { + sql1 { CREATE TABLE t1(a, b) } + sql2 { CREATE TABLE t2(a, b) } + } {} + + do_test $tn.2 { + sql1 { + INSERT INTO t2 VALUES(1, 2); + BEGIN; + INSERT INTO t1 VALUES(1, 2); + } + } {} + + do_test $tn.3 { csql2 { SELECT * FROM t1 } } {1 {database is locked}} + do_test $tn.4 { csql2 { SELECT * FROM t1 } } {1 {database is locked}} + do_test $tn.5 { sql2 { SELECT * FROM t2 } } {1 2} + + +} + +finish_test + ADDED test/servercrash.test Index: test/servercrash.test ================================================================== --- /dev/null +++ test/servercrash.test @@ -0,0 +1,68 @@ +# 2017 April 27 +# +# 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. +# +#*********************************************************************** +# + + +set testdir [file dirname $argv0] +source $testdir/tester.tcl +set testprefix servercrash + +ifcapable !crashtest { + finish_test + return +} +do_not_use_codec + +do_execsql_test 1.0 { + PRAGMA page_siBlockze = 4096; + PRAGMA auto_vacuum = OFF; + CREATE TABLE t1(a, b); + CREATE TABLE t2(c, d); + + INSERT INTO t1 VALUES(1, 2), (3, 4); + INSERT INTO t2 VALUES(1, 2), (3, 4); +} + +for {set i 0} {$i < 10} {incr i} { + do_test 1.$i.1 { + crashsql -delay 1 -file test.db { INSERT INTO t1 VALUES(5, 6) } + } {1 {child process exited abnormally}} + + do_execsql_test 1.$i.2 { + SELECT * FROM t1 + } {1 2 3 4} +} + +for {set i 0} {$i < 10} {incr i} { + do_test 2.$i.1 { + crashsql -delay 1 -file test.db { INSERT INTO t1 VALUES(5, 6) } + } {1 {child process exited abnormally}} + + do_test 2.$i.2 { + sqlite3 dbX test.db + execsql { SELECT * FROM t1 } dbX + } {1 2 3 4} + dbX close +} + +db close +for {set i 0} {$i < 10} {incr i} { + do_test 3.$i.1 { + crashsql -delay 1 -file test.db { INSERT INTO t1 VALUES(5, 6) } + } {1 {child process exited abnormally}} + + sqlite3 db test.db + do_execsql_test 3.$i.2 { SELECT * FROM t1 } {1 2 3 4} + db close +} + +finish_test + Index: test/tester.tcl ================================================================== --- test/tester.tcl +++ test/tester.tcl @@ -584,10 +584,14 @@ proc reset_db {} { catch {db close} forcedelete test.db forcedelete test.db-journal forcedelete test.db-wal + for {set i 0} {$i < 16} {incr i} { + forcedelete test.db-journal$i + } + sqlite3 db ./test.db set ::DB [sqlite3_connection_pointer db] if {[info exists ::SETUP_SQL]} { db eval $::SETUP_SQL } Index: tool/mksqlite3c.tcl ================================================================== --- tool/mksqlite3c.tcl +++ tool/mksqlite3c.tcl @@ -112,10 +112,11 @@ pager.h parse.h pcache.h pragma.h rtree.h + server.h sqlite3session.h sqlite3.h sqlite3ext.h sqlite3rbu.h sqliteicu.h @@ -317,10 +318,11 @@ pcache.c pcache1.c rowset.c pager.c wal.c + server.c btmutex.c btree.c backup.c