summaryrefslogtreecommitdiffstats
path: root/src/test_pcache.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/test_pcache.c467
1 files changed, 467 insertions, 0 deletions
diff --git a/src/test_pcache.c b/src/test_pcache.c
new file mode 100644
index 0000000..5266d67
--- /dev/null
+++ b/src/test_pcache.c
@@ -0,0 +1,467 @@
+/*
+** 2008 November 18
+**
+** 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 code used for testing the SQLite system.
+** None of the code in this file goes into a deliverable build.
+**
+** This file contains an application-defined pager cache
+** implementation that can be plugged in in place of the
+** default pcache. This alternative pager cache will throw
+** some errors that the default cache does not.
+**
+** This pagecache implementation is designed for simplicity
+** not speed.
+*/
+#include "sqlite3.h"
+#include <string.h>
+#include <assert.h>
+
+/*
+** Global data used by this test implementation. There is no
+** mutexing, which means this page cache will not work in a
+** multi-threaded test.
+*/
+typedef struct testpcacheGlobalType testpcacheGlobalType;
+struct testpcacheGlobalType {
+ void *pDummy; /* Dummy allocation to simulate failures */
+ int nInstance; /* Number of current instances */
+ unsigned discardChance; /* Chance of discarding on an unpin (0-100) */
+ unsigned prngSeed; /* Seed for the PRNG */
+ unsigned highStress; /* Call xStress aggressively */
+};
+static testpcacheGlobalType testpcacheGlobal;
+
+/*
+** Initializer.
+**
+** Verify that the initializer is only called when the system is
+** uninitialized. Allocate some memory and report SQLITE_NOMEM if
+** the allocation fails. This provides a means to test the recovery
+** from a failed initialization attempt. It also verifies that the
+** the destructor always gets call - otherwise there would be a
+** memory leak.
+*/
+static int testpcacheInit(void *pArg){
+ assert( pArg==(void*)&testpcacheGlobal );
+ assert( testpcacheGlobal.pDummy==0 );
+ assert( testpcacheGlobal.nInstance==0 );
+ testpcacheGlobal.pDummy = sqlite3_malloc(10);
+ return testpcacheGlobal.pDummy==0 ? SQLITE_NOMEM : SQLITE_OK;
+}
+
+/*
+** Destructor
+**
+** Verify that this is only called after initialization.
+** Free the memory allocated by the initializer.
+*/
+static void testpcacheShutdown(void *pArg){
+ assert( pArg==(void*)&testpcacheGlobal );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance==0 );
+ sqlite3_free( testpcacheGlobal.pDummy );
+ testpcacheGlobal.pDummy = 0;
+}
+
+/*
+** Number of pages in a cache.
+**
+** The number of pages is a hard upper bound in this test module.
+** If more pages are requested, sqlite3PcacheFetch() returns NULL.
+**
+** If testing with in-memory temp tables, provide a larger pcache.
+** Some of the test cases need this.
+*/
+#if defined(SQLITE_TEMP_STORE) && SQLITE_TEMP_STORE>=2
+# define TESTPCACHE_NPAGE 499
+#else
+# define TESTPCACHE_NPAGE 217
+#endif
+#define TESTPCACHE_RESERVE 17
+
+/*
+** Magic numbers used to determine validity of the page cache.
+*/
+#define TESTPCACHE_VALID 0x364585fd
+#define TESTPCACHE_CLEAR 0xd42670d4
+
+/*
+** Private implementation of a page cache.
+*/
+typedef struct testpcache testpcache;
+struct testpcache {
+ int szPage; /* Size of each page. Multiple of 8. */
+ int szExtra; /* Size of extra data that accompanies each page */
+ int bPurgeable; /* True if the page cache is purgeable */
+ int nFree; /* Number of unused slots in a[] */
+ int nPinned; /* Number of pinned slots in a[] */
+ unsigned iRand; /* State of the PRNG */
+ unsigned iMagic; /* Magic number for sanity checking */
+ struct testpcachePage {
+ sqlite3_pcache_page page; /* Base class */
+ unsigned key; /* The key for this page. 0 means unallocated */
+ int isPinned; /* True if the page is pinned */
+ } a[TESTPCACHE_NPAGE]; /* All pages in the cache */
+};
+
+/*
+** Get a random number using the PRNG in the given page cache.
+*/
+static unsigned testpcacheRandom(testpcache *p){
+ unsigned x = 0;
+ int i;
+ for(i=0; i<4; i++){
+ p->iRand = (p->iRand*69069 + 5);
+ x = (x<<8) | ((p->iRand>>16)&0xff);
+ }
+ return x;
+}
+
+
+/*
+** Allocate a new page cache instance.
+*/
+static sqlite3_pcache *testpcacheCreate(
+ int szPage,
+ int szExtra,
+ int bPurgeable
+){
+ int nMem;
+ char *x;
+ testpcache *p;
+ int i;
+ assert( testpcacheGlobal.pDummy!=0 );
+ szPage = (szPage+7)&~7;
+ nMem = sizeof(testpcache) + TESTPCACHE_NPAGE*(szPage+szExtra);
+ p = sqlite3_malloc( nMem );
+ if( p==0 ) return 0;
+ x = (char*)&p[1];
+ p->szPage = szPage;
+ p->szExtra = szExtra;
+ p->nFree = TESTPCACHE_NPAGE;
+ p->nPinned = 0;
+ p->iRand = testpcacheGlobal.prngSeed;
+ p->bPurgeable = bPurgeable;
+ p->iMagic = TESTPCACHE_VALID;
+ for(i=0; i<TESTPCACHE_NPAGE; i++, x += (szPage+szExtra)){
+ p->a[i].key = 0;
+ p->a[i].isPinned = 0;
+ p->a[i].page.pBuf = (void*)x;
+ p->a[i].page.pExtra = (void*)&x[szPage];
+ }
+ testpcacheGlobal.nInstance++;
+ return (sqlite3_pcache*)p;
+}
+
+/*
+** Set the cache size
+*/
+static void testpcacheCachesize(sqlite3_pcache *pCache, int newSize){
+ testpcache *p = (testpcache*)pCache;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+}
+
+/*
+** Return the number of pages in the cache that are being used.
+** This includes both pinned and unpinned pages.
+*/
+static int testpcachePagecount(sqlite3_pcache *pCache){
+ testpcache *p = (testpcache*)pCache;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+ return TESTPCACHE_NPAGE - p->nFree;
+}
+
+/*
+** Fetch a page.
+*/
+static sqlite3_pcache_page *testpcacheFetch(
+ sqlite3_pcache *pCache,
+ unsigned key,
+ int createFlag
+){
+ testpcache *p = (testpcache*)pCache;
+ int i, j;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+
+ /* See if the page is already in cache. Return immediately if it is */
+ for(i=0; i<TESTPCACHE_NPAGE; i++){
+ if( p->a[i].key==key ){
+ if( !p->a[i].isPinned ){
+ p->nPinned++;
+ assert( p->nPinned <= TESTPCACHE_NPAGE - p->nFree );
+ p->a[i].isPinned = 1;
+ }
+ return &p->a[i].page;
+ }
+ }
+
+ /* If createFlag is 0, never allocate a new page */
+ if( createFlag==0 ){
+ return 0;
+ }
+
+ /* If no pages are available, always fail */
+ if( p->nPinned==TESTPCACHE_NPAGE ){
+ return 0;
+ }
+
+ /* Do not allocate the last TESTPCACHE_RESERVE pages unless createFlag is 2 */
+ if( p->nPinned>=TESTPCACHE_NPAGE-TESTPCACHE_RESERVE && createFlag<2 ){
+ return 0;
+ }
+
+ /* Do not allocate if highStress is enabled and createFlag is not 2.
+ **
+ ** The highStress setting causes pagerStress() to be called much more
+ ** often, which exercises the pager logic more intensely.
+ */
+ if( testpcacheGlobal.highStress && createFlag<2 ){
+ return 0;
+ }
+
+ /* Find a free page to allocate if there are any free pages.
+ ** Withhold TESTPCACHE_RESERVE free pages until createFlag is 2.
+ */
+ if( p->nFree>TESTPCACHE_RESERVE || (createFlag==2 && p->nFree>0) ){
+ j = testpcacheRandom(p) % TESTPCACHE_NPAGE;
+ for(i=0; i<TESTPCACHE_NPAGE; i++, j = (j+1)%TESTPCACHE_NPAGE){
+ if( p->a[j].key==0 ){
+ p->a[j].key = key;
+ p->a[j].isPinned = 1;
+ memset(p->a[j].page.pBuf, 0, p->szPage);
+ memset(p->a[j].page.pExtra, 0, p->szExtra);
+ p->nPinned++;
+ p->nFree--;
+ assert( p->nPinned <= TESTPCACHE_NPAGE - p->nFree );
+ return &p->a[j].page;
+ }
+ }
+
+ /* The prior loop always finds a freepage to allocate */
+ assert( 0 );
+ }
+
+ /* If this cache is not purgeable then we have to fail.
+ */
+ if( p->bPurgeable==0 ){
+ return 0;
+ }
+
+ /* If there are no free pages, recycle a page. The page to
+ ** recycle is selected at random from all unpinned pages.
+ */
+ j = testpcacheRandom(p) % TESTPCACHE_NPAGE;
+ for(i=0; i<TESTPCACHE_NPAGE; i++, j = (j+1)%TESTPCACHE_NPAGE){
+ if( p->a[j].key>0 && p->a[j].isPinned==0 ){
+ p->a[j].key = key;
+ p->a[j].isPinned = 1;
+ memset(p->a[j].page.pBuf, 0, p->szPage);
+ memset(p->a[j].page.pExtra, 0, p->szExtra);
+ p->nPinned++;
+ assert( p->nPinned <= TESTPCACHE_NPAGE - p->nFree );
+ return &p->a[j].page;
+ }
+ }
+
+ /* The previous loop always finds a page to recycle. */
+ assert(0);
+ return 0;
+}
+
+/*
+** Unpin a page.
+*/
+static void testpcacheUnpin(
+ sqlite3_pcache *pCache,
+ sqlite3_pcache_page *pOldPage,
+ int discard
+){
+ testpcache *p = (testpcache*)pCache;
+ int i;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+
+ /* Randomly discard pages as they are unpinned according to the
+ ** discardChance setting. If discardChance is 0, the random discard
+ ** never happens. If discardChance is 100, it always happens.
+ */
+ if( p->bPurgeable
+ && (100-testpcacheGlobal.discardChance) <= (testpcacheRandom(p)%100)
+ ){
+ discard = 1;
+ }
+
+ for(i=0; i<TESTPCACHE_NPAGE; i++){
+ if( &p->a[i].page==pOldPage ){
+ /* The pOldPage pointer always points to a pinned page */
+ assert( p->a[i].isPinned );
+ p->a[i].isPinned = 0;
+ p->nPinned--;
+ assert( p->nPinned>=0 );
+ if( discard ){
+ p->a[i].key = 0;
+ p->nFree++;
+ assert( p->nFree<=TESTPCACHE_NPAGE );
+ }
+ return;
+ }
+ }
+
+ /* The pOldPage pointer always points to a valid page */
+ assert( 0 );
+}
+
+
+/*
+** Rekey a single page.
+*/
+static void testpcacheRekey(
+ sqlite3_pcache *pCache,
+ sqlite3_pcache_page *pOldPage,
+ unsigned oldKey,
+ unsigned newKey
+){
+ testpcache *p = (testpcache*)pCache;
+ int i;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+
+ /* If there already exists another page at newKey, verify that
+ ** the other page is unpinned and discard it.
+ */
+ for(i=0; i<TESTPCACHE_NPAGE; i++){
+ if( p->a[i].key==newKey ){
+ /* The new key is never a page that is already pinned */
+ assert( p->a[i].isPinned==0 );
+ p->a[i].key = 0;
+ p->nFree++;
+ assert( p->nFree<=TESTPCACHE_NPAGE );
+ break;
+ }
+ }
+
+ /* Find the page to be rekeyed and rekey it.
+ */
+ for(i=0; i<TESTPCACHE_NPAGE; i++){
+ if( p->a[i].key==oldKey ){
+ /* The oldKey and pOldPage parameters match */
+ assert( &p->a[i].page==pOldPage );
+ /* Page to be rekeyed must be pinned */
+ assert( p->a[i].isPinned );
+ p->a[i].key = newKey;
+ return;
+ }
+ }
+
+ /* Rekey is always given a valid page to work with */
+ assert( 0 );
+}
+
+
+/*
+** Truncate the page cache. Every page with a key of iLimit or larger
+** is discarded.
+*/
+static void testpcacheTruncate(sqlite3_pcache *pCache, unsigned iLimit){
+ testpcache *p = (testpcache*)pCache;
+ unsigned int i;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+ for(i=0; i<TESTPCACHE_NPAGE; i++){
+ if( p->a[i].key>=iLimit ){
+ p->a[i].key = 0;
+ if( p->a[i].isPinned ){
+ p->nPinned--;
+ assert( p->nPinned>=0 );
+ }
+ p->nFree++;
+ assert( p->nFree<=TESTPCACHE_NPAGE );
+ }
+ }
+}
+
+/*
+** Destroy a page cache.
+*/
+static void testpcacheDestroy(sqlite3_pcache *pCache){
+ testpcache *p = (testpcache*)pCache;
+ assert( p->iMagic==TESTPCACHE_VALID );
+ assert( testpcacheGlobal.pDummy!=0 );
+ assert( testpcacheGlobal.nInstance>0 );
+ p->iMagic = TESTPCACHE_CLEAR;
+ sqlite3_free(p);
+ testpcacheGlobal.nInstance--;
+}
+
+
+/*
+** Invoke this routine to register or unregister the testing pager cache
+** implemented by this file.
+**
+** Install the test pager cache if installFlag is 1 and uninstall it if
+** installFlag is 0.
+**
+** When installing, discardChance is a number between 0 and 100 that
+** indicates the probability of discarding a page when unpinning the
+** page. 0 means never discard (unless the discard flag is set).
+** 100 means always discard.
+*/
+void installTestPCache(
+ int installFlag, /* True to install. False to uninstall. */
+ unsigned discardChance, /* 0-100. Chance to discard on unpin */
+ unsigned prngSeed, /* Seed for the PRNG */
+ unsigned highStress /* Call xStress aggressively */
+){
+ static const sqlite3_pcache_methods2 testPcache = {
+ 1,
+ (void*)&testpcacheGlobal,
+ testpcacheInit,
+ testpcacheShutdown,
+ testpcacheCreate,
+ testpcacheCachesize,
+ testpcachePagecount,
+ testpcacheFetch,
+ testpcacheUnpin,
+ testpcacheRekey,
+ testpcacheTruncate,
+ testpcacheDestroy,
+ };
+ static sqlite3_pcache_methods2 defaultPcache;
+ static int isInstalled = 0;
+
+ assert( testpcacheGlobal.nInstance==0 );
+ assert( testpcacheGlobal.pDummy==0 );
+ assert( discardChance<=100 );
+ testpcacheGlobal.discardChance = discardChance;
+ testpcacheGlobal.prngSeed = prngSeed ^ (prngSeed<<16);
+ testpcacheGlobal.highStress = highStress;
+ if( installFlag!=isInstalled ){
+ if( installFlag ){
+ sqlite3_config(SQLITE_CONFIG_GETPCACHE2, &defaultPcache);
+ assert( defaultPcache.xCreate!=testpcacheCreate );
+ sqlite3_config(SQLITE_CONFIG_PCACHE2, &testPcache);
+ }else{
+ assert( defaultPcache.xCreate!=0 );
+ sqlite3_config(SQLITE_CONFIG_PCACHE2, &defaultPcache);
+ }
+ isInstalled = installFlag;
+ }
+}