0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2024-12-15 23:48:49 +00:00
2023-10-16 13:58:16 +02:00

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);
}
}
}