From e5bd459f589816624355a4cb89a869618d824f32 Mon Sep 17 00:00:00 2001 From: augurier <14434658+augurier@user.noreply.gitee.com> Date: Tue, 31 Dec 2024 16:14:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=83=A8=E5=88=86=E5=AE=9E=E9=AA=8C=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=EF=BC=8C=E5=AE=8C=E5=96=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ test/parallel_test.cc | 21 +++++++++------ test/recover_test.cc | 70 +++++++++++++++++++++++------------------------ 3 files changed, 123 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 3bc814d..c7103d2 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,81 @@ metadb ## 3. 测试 ### 3.1 正确性测试 +相关代码在`test/`下 +#### 3.1.1 封装函数功能 +相关代码在`helper.cc`中 +在所有测试中,绝大部分key的值数字>0,同时也支持单独写入key<=0,方便测试观察。 +value一共使用到了三个字段:name,address,age。name是`"customer#+"key`,address每次在给定的城市数组中抽取一个,age限定在0~100, 因此后两者会有大量重复,测试中的索引都是建立在这两个字段上。 +由于测试中需要检查写入时与查询时,拥有相应的字段的kv是否对应,因此需要维护一个`set`。又因为测试涉及到并发,因此封装了一个线程安全的`ThreadSafeSet`类,对于set的操作有锁保护。具体测试中使用两个该类统计了两类key:address为shanghai的key和age为20的key。 +``` +ThreadSafeSet shanghaiKeys; +ThreadSafeSet age20Keys; +``` + +下面大致介绍一下封装函数的功能: + +InsertOneField:只插一条特定数据的测试(让key<=0, 和批量写入区别开)。使用它是为了保证有些地方的写入必须正确,并通过追踪对应的key发现系统中潜在的问题。 + +DeleteOneField:只删一条特定数据的测试,功能与单条插入相似,主要用来测试delete。 +GetOneField:只读一条特定数据的测试,与上面两条对应。 + +对于所有涉及随机性的函数,传参一个随机种子。只要随机种子一致,随机生成的内容就一致,相应的读需要和相应的写、删保持一致。此外测试的数据量也是可以修改的。 + +InsertFieldData:批量写入,按照上面提到的测试规则生成kv,并调用putfields。检查返回状态是否ok。同时对于生成的字段,如果是address为shanghai或age为20,统计入相应的set。 + +DeleteFieldData:批量删除,按照规则(>0)生成key,并调用delete。检查返回状态是否ok。 + +WriteFieldData:与InsertFieldData相似,但生成kv后不直接调用putfields,而是写入writebatch,全部生成完在调用write把batch写入数据库。用来测试write功能,检查返回状态是否ok。 + +GetFieldData:测试读,按照规则(>0)生成key,并调用getfield。由于并发时不一定能读到,提供一个allownotfound参数。如果true,返回状态可以是notfound。如果false,就只能是ok。对于读到的field,检测是否被正确解析,以及每个字段是否满足规则。 + +GetDeleteData:检查对应种子的所有生成数据有没有删除干净(getfield的所有返回状态都是notfound)。 + +findKeysByCity:调用`FindKeysByField({"address", "Shanghai"})`,检查返回的所有key是否都在shanghaiKeys中(insert时统计的)。 + +findKeysByCityIndex:提供一个参数haveIndex,表明数据库有没有该索引(address)。调用`querybyindex({"address", "Shanghai"})`,如果haveindex检测返回状态ok,并且所有的key都要在shanghaiKeys中。 + +findKeysByAgeIndex:与上条相同,检测age字段。 + +checkDataInKVAndIndex: 在并发写删与恢复测试中,不能保证每个种子序列的所有key都在数据库中。但需要保证的是,kv和index中的数据是一致的。这里先后调用QueryByIndex和FindKeysByField,比较得到的key,确保两者一致。 + +#### 3.1.2 基础测试 +相关代码在`basic_function_test.cc`中 +这一部分主要测试每个功能在正常使用中是否正确,按照逻辑简单调用封装的功能函数。 + +TestLab1流程: 批量写 -> 必须读到 -> findkeysbycity -> 批量删 -> 必须全读不到。 + +TestLab2流程:批量写 -> 创索引address,age -> 索引查询address,age -> 删索引address -> 索引查询address(haveindex=false) -> 索引查询age -> 批量删 -> 索引查询age(索引还在能查,但返回的key数量为0) -> write -> 必须读到 -> 索引查询age。 + +至此,上面的流程基本覆盖了我们数据库的每个基础功能。 + +#### 3.1.2 并发测试 +相关代码在`parallel_test.cc`中 +这一部分主要测试读、写、创删索引之间的并发。每个测试中并发线程的数量也是可以修改的。 + +TestReadPut:创索引 -> 并发:两线程写(不同随机种子)三线程读(不保证能读到)-> 读两次写相应的随机种子(必须读到)-> 索引查(返回的key在两次写入中) -> 检查两数据库一致性(checkDataInKVAndIndex)。 + +TestPutCreatei:先批量写入一次数据 -> 并发:一线程创索引,一线程忙等,至数据库开始创建索引后单条写入 -> 检测索引创建是否成功(创建索引前批量写的数据,能通过索引查得到)-> 读单条写入 -> 检测数据库一致性。 + +TestCreateiCreatei:先批量写入一次数据 -> 并发:三线程创索引address -> 索引查 -> 一致性 -> 并发:两线程删索引address,一线程创索引age -> 索引查(address无,age有)-> 一致性。 + +TestPutDeleteOne(有索引时,大量并发put与delete相同key,确保kvdb和indexdb的一致性): 创索引 -> 并发:对于100条数据,10线程插入,10线程删除 -> 一致性。 + +TestPutDelete:创索引 -> 并发:两线程写0、1种子数据,两线程删0、1种子数据 -> 一致性。 + +TestWrite(在之前基础上并发所有,并加入write):创索引address、批量写种子2 -> 并发:线程0创索引age,其他线程忙等至开始创建,线程1批量写种子0,线程2write种子1, 线程3删索引age -> 检测:种子012所有数据都应被读到,一致性, age索引被删除。 +这里的测试也可以加入delete,或不删索引age检测age的一致性,具体见注释。 + +至此,上面的流程基本覆盖了我们数据库的每个基础功能之间的并发。 + +#### 3.1.2 恢复测试 +相关代码在`recover_test.cc`中 +这一部分主要测试正常与异常的恢复。 + +TestNormalRecover:创索引、批量写、此时之前测试都检测过能被读到 -> delete db -> 重新open -> 读数据、索引查(之前写入的数据仍能被读到)。 + +TestParalRecover**该测试比较特别,需要运行两次**:创索引 -> 并发:线程0批量写,线程1write,线程2delete,线程3 在单条插入后,deletedb。线程3导致了其他线程错误,测试会终止(模拟数据库崩溃),这会导致各线程在各种奇怪的时间点崩溃。此时注释掉上半部分代码,运行下半部分:单条写入能被读到,并检测一致性。 +这里我们运行了几十次,前半部分的崩溃报错有多种,但后半部分的运行都是成功的。同时也追踪了恢复的运行过程,确实有数据从metadb中被正确解析。 ### 3.2 性能测试 测试、分析、优化 diff --git a/test/parallel_test.cc b/test/parallel_test.cc index f9cfcfc..6406c16 100644 --- a/test/parallel_test.cc +++ b/test/parallel_test.cc @@ -158,6 +158,7 @@ TEST(TestCreateiCreatei, Parallel) { //检查 findKeysByCityIndex(db, false); findKeysByAgeIndex(db, true); + checkDataInKVAndIndex(db, "age"); delete db; } @@ -181,14 +182,14 @@ TEST(TestPutDeleteOne, Parallel) { std::vector threads(thread_num_); for (size_t i = 0; i < thread_num_; i++) { - if (i % 2 == 0) {//2线程删除索引address + if (i % 2 == 0) { threads[i] = std::thread([db](){ for (size_t j = 0; j < 100; j++) { InsertOneField(db, std::to_string(j)); } }); - } else {//1线程创建索引age + } else { threads[i] = std::thread([db](){ for (size_t j = 0; j < 100; j++) { @@ -257,7 +258,7 @@ TEST(TestWrite, Parallel) { age20Keys.clear(); db->CreateIndexOnField("address", op); InsertFieldData(db, 2); //先填点数据,让创建索引的时间久一点 - int thread_num_ = 5; + int thread_num_ = 4; std::vector threads(thread_num_); threads[0] = std::thread([db](){db->CreateIndexOnField("age", op);}); threads[1] = std::thread([db](){ @@ -270,15 +271,15 @@ TEST(TestWrite, Parallel) { continue; } WriteFieldData(db, 1);}); + // threads[3] = std::thread([db](){ + // while (db->GetIndexStatus("age") == NotExist){ + // continue; + // } + // DeleteFieldData(db, 0);}); threads[3] = std::thread([db](){ while (db->GetIndexStatus("age") == NotExist){ continue; } - DeleteFieldData(db, 0);}); - threads[4] = std::thread([db](){ - while (db->GetIndexStatus("age") == NotExist){ - continue; - } db->DeleteIndex("age", op);}); for (auto& t : threads) { @@ -288,6 +289,10 @@ TEST(TestWrite, Parallel) { } //检查 + //如果加入delete线程就不能保证下面被读到 + GetFieldData(db, false, 0); + GetFieldData(db, false, 1); + GetFieldData(db, false, 2); checkDataInKVAndIndex(db); ASSERT_EQ(db->GetIndexStatus("age"), NotExist); //删除索引的请求应该被pend在创建之上 //删掉最后一个线程,可以测试创建age索引时并发的写入能不能保持age的一致性 diff --git a/test/recover_test.cc b/test/recover_test.cc index d2b104d..1c93ebc 100644 --- a/test/recover_test.cc +++ b/test/recover_test.cc @@ -38,48 +38,48 @@ TEST(TestNormalRecover, Recover) { TEST(TestParalRecover, Recover) { //第一次运行 - // fielddb::DestroyDB("testdb3.2",Options()); - // FieldDB *db = new FieldDB(); + fielddb::DestroyDB("testdb3.2",Options()); + FieldDB *db = new FieldDB(); - // if(OpenDB("testdb3.2", &db).ok() == false) { - // std::cerr << "open db failed" << std::endl; - // abort(); - // } - // db->CreateIndexOnField("address", op); - // db->CreateIndexOnField("age", op); - // int thread_num_ = 4; - // std::vector threads(thread_num_); - // threads[0] = std::thread([db](){ - // InsertFieldData(db); - // }); - // threads[1] = std::thread([db](){ - // WriteFieldData(db); - // }); - // threads[2] = std::thread([db](){ - // DeleteFieldData(db); - // }); - // threads[3] = std::thread([db](){ - // InsertOneField(db); - // delete db; - // }); - // for (auto& t : threads) { - // if (t.joinable()) { - // t.join(); - // } - // } + if(OpenDB("testdb3.2", &db).ok() == false) { + std::cerr << "open db failed" << std::endl; + abort(); + } + db->CreateIndexOnField("address", op); + db->CreateIndexOnField("age", op); + int thread_num_ = 4; + std::vector threads(thread_num_); + threads[0] = std::thread([db](){ + InsertFieldData(db); + }); + threads[1] = std::thread([db](){ + WriteFieldData(db); + }); + threads[2] = std::thread([db](){ + DeleteFieldData(db); + }); + threads[3] = std::thread([db](){ + InsertOneField(db); + delete db; + }); + for (auto& t : threads) { + if (t.joinable()) { + t.join(); + } + } //线程3导致了其他线程错误,测试会终止(模拟数据库崩溃) //这会导致各线程在各种奇怪的时间点崩溃 //第二次运行注释掉上面的代码,运行下面的代码测试恢复 //第二次运行 - FieldDB *db = new FieldDB(); - if(OpenDB("testdb3.2", &db).ok() == false) { - std::cerr << "open db failed" << std::endl; - abort(); - } - GetOneField(db); - checkDataInKVAndIndex(db); + // FieldDB *db = new FieldDB(); + // if(OpenDB("testdb3.2", &db).ok() == false) { + // std::cerr << "open db failed" << std::endl; + // abort(); + // } + // GetOneField(db); + // checkDataInKVAndIndex(db); //这里会出现两个数字,如果>1说明除了线程3插入的一条数据,其他线程也有数据在崩溃前被正确恢复了 }