mirror of
https://github.com/tursodatabase/libsql.git
synced 2024-11-27 10:29:00 +00:00
489 lines
13 KiB
C
489 lines
13 KiB
C
|
|
/*
|
|
** This file contains tests related to recovery following application
|
|
** and system crashes (power failures) while writing to the database.
|
|
*/
|
|
|
|
#include "lsmtest.h"
|
|
|
|
/*
|
|
** Structure used by testCksumDatabase() to accumulate checksum values in.
|
|
*/
|
|
typedef struct Cksum Cksum;
|
|
struct Cksum {
|
|
int nRow;
|
|
int cksum1;
|
|
int cksum2;
|
|
};
|
|
|
|
/*
|
|
** tdb_scan() callback used by testCksumDatabase()
|
|
*/
|
|
static void scanCksumDb(
|
|
void *pCtx,
|
|
void *pKey, int nKey,
|
|
void *pVal, int nVal
|
|
){
|
|
Cksum *p = (Cksum *)pCtx;
|
|
int i;
|
|
|
|
p->nRow++;
|
|
for(i=0; i<nKey; i++){
|
|
p->cksum1 += ((u8 *)pKey)[i];
|
|
p->cksum2 += p->cksum1;
|
|
}
|
|
for(i=0; i<nVal; i++){
|
|
p->cksum1 += ((u8 *)pVal)[i];
|
|
p->cksum2 += p->cksum1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
** tdb_scan() callback used by testCountDatabase()
|
|
*/
|
|
static void scanCountDb(
|
|
void *pCtx,
|
|
void *pKey, int nKey,
|
|
void *pVal, int nVal
|
|
){
|
|
Cksum *p = (Cksum *)pCtx;
|
|
p->nRow++;
|
|
|
|
unused_parameter(pKey);
|
|
unused_parameter(nKey);
|
|
unused_parameter(pVal);
|
|
unused_parameter(nVal);
|
|
}
|
|
|
|
|
|
/*
|
|
** Iterate through the entire contents of database pDb. Write a checksum
|
|
** string based on the db contents into buffer zOut before returning. A
|
|
** checksum string is at most 29 (TEST_CKSUM_BYTES) bytes in size:
|
|
**
|
|
** * 32-bit integer (10 bytes)
|
|
** * 1 space (1 byte)
|
|
** * 32-bit hex (8 bytes)
|
|
** * 1 space (1 byte)
|
|
** * 32-bit hex (8 bytes)
|
|
** * nul-terminator (1 byte)
|
|
**
|
|
** The number of entries in the database is returned.
|
|
*/
|
|
int testCksumDatabase(
|
|
TestDb *pDb, /* Database handle */
|
|
char *zOut /* Buffer to write checksum to */
|
|
){
|
|
Cksum cksum;
|
|
memset(&cksum, 0, sizeof(Cksum));
|
|
tdb_scan(pDb, (void *)&cksum, 0, 0, 0, 0, 0, scanCksumDb);
|
|
sprintf(zOut, "%d %x %x",
|
|
cksum.nRow, (u32)cksum.cksum1, (u32)cksum.cksum2
|
|
);
|
|
assert( strlen(zOut)<TEST_CKSUM_BYTES );
|
|
return cksum.nRow;
|
|
}
|
|
|
|
int testCountDatabase(TestDb *pDb){
|
|
Cksum cksum;
|
|
memset(&cksum, 0, sizeof(Cksum));
|
|
tdb_scan(pDb, (void *)&cksum, 0, 0, 0, 0, 0, scanCountDb);
|
|
return cksum.nRow;
|
|
}
|
|
|
|
/*
|
|
** This function is a no-op if *pRc is not 0 when it is called.
|
|
**
|
|
** Otherwise, the two nul-terminated strings z1 and z1 are compared. If
|
|
** they are the same, the function returns without doing anything. Otherwise,
|
|
** an error message is printed, *pRc is set to 1 and the test_failed()
|
|
** function called.
|
|
*/
|
|
void testCompareStr(const char *z1, const char *z2, int *pRc){
|
|
if( *pRc==0 ){
|
|
if( strcmp(z1, z2) ){
|
|
testPrintError("testCompareStr: \"%s\" != \"%s\"\n", z1, z2);
|
|
*pRc = 1;
|
|
test_failed();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
** This function is a no-op if *pRc is not 0 when it is called.
|
|
**
|
|
** Otherwise, the two integers i1 and i2 are compared. If they are equal,
|
|
** the function returns without doing anything. Otherwise, an error message
|
|
** is printed, *pRc is set to 1 and the test_failed() function called.
|
|
*/
|
|
void testCompareInt(int i1, int i2, int *pRc){
|
|
if( *pRc==0 && i1!=i2 ){
|
|
testPrintError("testCompareInt: %d != %d\n", i1, i2);
|
|
*pRc = 1;
|
|
test_failed();
|
|
}
|
|
}
|
|
|
|
void testCaseStart(int *pRc, char *zFmt, ...){
|
|
va_list ap;
|
|
va_start(ap, zFmt);
|
|
vprintf(zFmt, ap);
|
|
printf(" ...");
|
|
va_end(ap);
|
|
*pRc = 0;
|
|
fflush(stdout);
|
|
}
|
|
|
|
/*
|
|
** This function is a no-op if *pRc is non-zero when it is called. Zero
|
|
** is returned in this case.
|
|
**
|
|
** Otherwise, the zFmt (a printf style format string) and following arguments
|
|
** are used to create a test case name. If zPattern is NULL or a glob pattern
|
|
** that matches the test case name, 1 is returned and the test case started.
|
|
** Otherwise, zero is returned and the test case does not start.
|
|
*/
|
|
int testCaseBegin(int *pRc, const char *zPattern, const char *zFmt, ...){
|
|
int res = 0;
|
|
if( *pRc==0 ){
|
|
char *zTest;
|
|
va_list ap;
|
|
|
|
va_start(ap, zFmt);
|
|
zTest = testMallocVPrintf(zFmt, ap);
|
|
va_end(ap);
|
|
if( zPattern==0 || testGlobMatch(zPattern, zTest) ){
|
|
printf("%-50s ...", zTest);
|
|
res = 1;
|
|
}
|
|
testFree(zTest);
|
|
fflush(stdout);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void testCaseFinish(int rc){
|
|
if( rc==0 ){
|
|
printf("Ok\n");
|
|
}else{
|
|
printf("FAILED\n");
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
|
|
void testCaseSkip(){
|
|
printf("Skipped\n");
|
|
}
|
|
|
|
void testSetupSavedLsmdb(
|
|
const char *zCfg,
|
|
const char *zFile,
|
|
Datasource *pData,
|
|
int nRow,
|
|
int *pRc
|
|
){
|
|
if( *pRc==0 ){
|
|
int rc;
|
|
TestDb *pDb;
|
|
rc = tdb_lsm_open(zCfg, zFile, 1, &pDb);
|
|
if( rc==0 ){
|
|
testWriteDatasourceRange(pDb, pData, 0, nRow, &rc);
|
|
testClose(&pDb);
|
|
if( rc==0 ) testSaveDb(zFile, "log");
|
|
}
|
|
*pRc = rc;
|
|
}
|
|
}
|
|
|
|
/*
|
|
** This function is a no-op if *pRc is non-zero when it is called.
|
|
**
|
|
** Open the LSM database identified by zFile and compute its checksum
|
|
** (a string, as returned by testCksumDatabase()). If the checksum is
|
|
** identical to zExpect1 or, if it is not NULL, zExpect2, the test passes.
|
|
** Otherwise, print an error message and set *pRc to 1.
|
|
*/
|
|
static void testCompareCksumLsmdb(
|
|
const char *zFile, /* Path to LSM database */
|
|
int bCompress, /* True if db is compressed */
|
|
const char *zExpect1, /* Expected checksum 1 */
|
|
const char *zExpect2, /* Expected checksum 2 (or NULL) */
|
|
int *pRc /* IN/OUT: Test case error code */
|
|
){
|
|
if( *pRc==0 ){
|
|
char zCksum[TEST_CKSUM_BYTES];
|
|
TestDb *pDb;
|
|
|
|
*pRc = tdb_lsm_open((bCompress?"compression=1 mmap=0":""), zFile, 0, &pDb);
|
|
testCksumDatabase(pDb, zCksum);
|
|
testClose(&pDb);
|
|
|
|
if( *pRc==0 ){
|
|
int r1 = 0;
|
|
int r2 = -1;
|
|
|
|
r1 = strcmp(zCksum, zExpect1);
|
|
if( zExpect2 ) r2 = strcmp(zCksum, zExpect2);
|
|
if( r1 && r2 ){
|
|
if( zExpect2 ){
|
|
testPrintError("testCompareCksumLsmdb: \"%s\" != (\"%s\" OR \"%s\")",
|
|
zCksum, zExpect1, zExpect2
|
|
);
|
|
}else{
|
|
testPrintError("testCompareCksumLsmdb: \"%s\" != \"%s\"",
|
|
zCksum, zExpect1
|
|
);
|
|
}
|
|
*pRc = 1;
|
|
test_failed();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0 /* not used */
|
|
static void testCompareCksumBtdb(
|
|
const char *zFile, /* Path to LSM database */
|
|
const char *zExpect1, /* Expected checksum 1 */
|
|
const char *zExpect2, /* Expected checksum 2 (or NULL) */
|
|
int *pRc /* IN/OUT: Test case error code */
|
|
){
|
|
if( *pRc==0 ){
|
|
char zCksum[TEST_CKSUM_BYTES];
|
|
TestDb *pDb;
|
|
|
|
*pRc = tdb_open("bt", zFile, 0, &pDb);
|
|
testCksumDatabase(pDb, zCksum);
|
|
testClose(&pDb);
|
|
|
|
if( *pRc==0 ){
|
|
int r1 = 0;
|
|
int r2 = -1;
|
|
|
|
r1 = strcmp(zCksum, zExpect1);
|
|
if( zExpect2 ) r2 = strcmp(zCksum, zExpect2);
|
|
if( r1 && r2 ){
|
|
if( zExpect2 ){
|
|
testPrintError("testCompareCksumLsmdb: \"%s\" != (\"%s\" OR \"%s\")",
|
|
zCksum, zExpect1, zExpect2
|
|
);
|
|
}else{
|
|
testPrintError("testCompareCksumLsmdb: \"%s\" != \"%s\"",
|
|
zCksum, zExpect1
|
|
);
|
|
}
|
|
*pRc = 1;
|
|
test_failed();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif /* not used */
|
|
|
|
/* Above this point are reusable test routines. Not clear that they
|
|
** should really be in this file.
|
|
*************************************************************************/
|
|
|
|
/*
|
|
** This test verifies that if a system crash occurs while doing merge work
|
|
** on the db, no data is lost.
|
|
*/
|
|
static void crash_test1(int bCompress, int *pRc){
|
|
const char *DBNAME = "testdb.lsm";
|
|
const DatasourceDefn defn = {TEST_DATASOURCE_RANDOM, 12, 16, 200, 200};
|
|
|
|
const int nRow = 5000; /* Database size */
|
|
const int nIter = 200; /* Number of test iterations */
|
|
const int nWork = 20; /* Maximum lsm_work() calls per iteration */
|
|
const int nPage = 15; /* Pages per lsm_work call */
|
|
|
|
int i;
|
|
int iDot = 0;
|
|
Datasource *pData;
|
|
CksumDb *pCksumDb;
|
|
TestDb *pDb;
|
|
char *zCfg;
|
|
|
|
const char *azConfig[2] = {
|
|
"page_size=1024 block_size=65536 autoflush=16384 safety=2 mmap=0",
|
|
"page_size=1024 block_size=65536 autoflush=16384 safety=2 "
|
|
" compression=1 mmap=0"
|
|
};
|
|
assert( bCompress==0 || bCompress==1 );
|
|
|
|
/* Allocate datasource. And calculate the expected checksums. */
|
|
pData = testDatasourceNew(&defn);
|
|
pCksumDb = testCksumArrayNew(pData, nRow, nRow, 1);
|
|
|
|
/* Setup and save the initial database. */
|
|
|
|
zCfg = testMallocPrintf("%s automerge=7", azConfig[bCompress]);
|
|
testSetupSavedLsmdb(zCfg, DBNAME, pData, 5000, pRc);
|
|
testFree(zCfg);
|
|
|
|
for(i=0; i<nIter && *pRc==0; i++){
|
|
int iWork;
|
|
int testrc = 0;
|
|
|
|
testCaseProgress(i, nIter, testCaseNDot(), &iDot);
|
|
|
|
/* Restore and open the database. */
|
|
testRestoreDb(DBNAME, "log");
|
|
testrc = tdb_lsm_open(azConfig[bCompress], DBNAME, 0, &pDb);
|
|
assert( testrc==0 );
|
|
|
|
/* Call lsm_work() on the db */
|
|
tdb_lsm_prepare_sync_crash(pDb, 1 + (i%(nWork*2)));
|
|
for(iWork=0; testrc==0 && iWork<nWork; iWork++){
|
|
int nWrite = 0;
|
|
lsm_db *db = tdb_lsm(pDb);
|
|
testrc = lsm_work(db, 0, nPage, &nWrite);
|
|
/* assert( testrc!=0 || nWrite>0 ); */
|
|
if( testrc==0 ) testrc = lsm_checkpoint(db, 0);
|
|
}
|
|
tdb_close(pDb);
|
|
|
|
/* Check that the database content is still correct */
|
|
testCompareCksumLsmdb(DBNAME,
|
|
bCompress, testCksumArrayGet(pCksumDb, nRow), 0, pRc);
|
|
}
|
|
|
|
testCksumArrayFree(pCksumDb);
|
|
testDatasourceFree(pData);
|
|
}
|
|
|
|
/*
|
|
** This test verifies that if a system crash occurs while committing a
|
|
** transaction to the log file, no earlier transactions are lost or damaged.
|
|
*/
|
|
static void crash_test2(int bCompress, int *pRc){
|
|
const char *DBNAME = "testdb.lsm";
|
|
const DatasourceDefn defn = {TEST_DATASOURCE_RANDOM, 12, 16, 1000, 1000};
|
|
|
|
const int nIter = 200;
|
|
const int nInsert = 20;
|
|
|
|
int i;
|
|
int iDot = 0;
|
|
Datasource *pData;
|
|
CksumDb *pCksumDb;
|
|
TestDb *pDb;
|
|
|
|
/* Allocate datasource. And calculate the expected checksums. */
|
|
pData = testDatasourceNew(&defn);
|
|
pCksumDb = testCksumArrayNew(pData, 100, 100+nInsert, 1);
|
|
|
|
/* Setup and save the initial database. */
|
|
testSetupSavedLsmdb("", DBNAME, pData, 100, pRc);
|
|
|
|
for(i=0; i<nIter && *pRc==0; i++){
|
|
int iIns;
|
|
int testrc = 0;
|
|
|
|
testCaseProgress(i, nIter, testCaseNDot(), &iDot);
|
|
|
|
/* Restore and open the database. */
|
|
testRestoreDb(DBNAME, "log");
|
|
testrc = tdb_lsm_open("safety=2", DBNAME, 0, &pDb);
|
|
assert( testrc==0 );
|
|
|
|
/* Insert nInsert records into the database. Crash midway through. */
|
|
tdb_lsm_prepare_sync_crash(pDb, 1 + (i%(nInsert+2)));
|
|
for(iIns=0; iIns<nInsert; iIns++){
|
|
void *pKey; int nKey;
|
|
void *pVal; int nVal;
|
|
|
|
testDatasourceEntry(pData, 100+iIns, &pKey, &nKey, &pVal, &nVal);
|
|
testrc = tdb_write(pDb, pKey, nKey, pVal, nVal);
|
|
if( testrc ) break;
|
|
}
|
|
tdb_close(pDb);
|
|
|
|
/* Check that no data was lost when the system crashed. */
|
|
testCompareCksumLsmdb(DBNAME, bCompress,
|
|
testCksumArrayGet(pCksumDb, 100 + iIns),
|
|
testCksumArrayGet(pCksumDb, 100 + iIns + 1),
|
|
pRc
|
|
);
|
|
}
|
|
|
|
testDatasourceFree(pData);
|
|
testCksumArrayFree(pCksumDb);
|
|
}
|
|
|
|
|
|
/*
|
|
** This test verifies that if a system crash occurs when checkpointing
|
|
** the database, data is not lost (assuming that any writes not synced
|
|
** to the db have been synced into the log file).
|
|
*/
|
|
static void crash_test3(int bCompress, int *pRc){
|
|
const char *DBNAME = "testdb.lsm";
|
|
const int nIter = 100;
|
|
const DatasourceDefn defn = {TEST_DATASOURCE_RANDOM, 12, 16, 1000, 1000};
|
|
|
|
int i;
|
|
int iDot = 0;
|
|
Datasource *pData;
|
|
CksumDb *pCksumDb;
|
|
TestDb *pDb;
|
|
|
|
/* Allocate datasource. And calculate the expected checksums. */
|
|
pData = testDatasourceNew(&defn);
|
|
pCksumDb = testCksumArrayNew(pData, 110, 150, 10);
|
|
|
|
/* Setup and save the initial database. */
|
|
testSetupSavedLsmdb("", DBNAME, pData, 100, pRc);
|
|
|
|
for(i=0; i<nIter && *pRc==0; i++){
|
|
int iOpen;
|
|
testCaseProgress(i, nIter, testCaseNDot(), &iDot);
|
|
testRestoreDb(DBNAME, "log");
|
|
|
|
for(iOpen=0; iOpen<5; iOpen++){
|
|
/* Open the database. Insert 10 more records. */
|
|
pDb = testOpen("lsm", 0, pRc);
|
|
testWriteDatasourceRange(pDb, pData, 100+iOpen*10, 10, pRc);
|
|
|
|
/* Schedule a crash simulation then close the db. */
|
|
tdb_lsm_prepare_sync_crash(pDb, 1 + (i%2));
|
|
tdb_close(pDb);
|
|
|
|
/* Open the database and check that the crash did not cause any
|
|
** data loss. */
|
|
testCompareCksumLsmdb(DBNAME, bCompress,
|
|
testCksumArrayGet(pCksumDb, 110 + iOpen*10), 0,
|
|
pRc
|
|
);
|
|
}
|
|
}
|
|
|
|
testDatasourceFree(pData);
|
|
testCksumArrayFree(pCksumDb);
|
|
}
|
|
|
|
void do_crash_test(const char *zPattern, int *pRc){
|
|
struct Test {
|
|
const char *zTest;
|
|
void (*x)(int, int *);
|
|
int bCompress;
|
|
} aTest [] = {
|
|
{ "crash.lsm.1", crash_test1, 0 },
|
|
#ifdef HAVE_ZLIB
|
|
{ "crash.lsm_zip.1", crash_test1, 1 },
|
|
#endif
|
|
{ "crash.lsm.2", crash_test2, 0 },
|
|
{ "crash.lsm.3", crash_test3, 0 },
|
|
};
|
|
int i;
|
|
|
|
for(i=0; *pRc==LSM_OK && i<ArraySize(aTest); i++){
|
|
struct Test *p = &aTest[i];
|
|
if( testCaseBegin(pRc, zPattern, "%s", p->zTest) ){
|
|
p->x(p->bCompress, pRc);
|
|
testCaseFinish(*pRc);
|
|
}
|
|
}
|
|
}
|