/** * @brief read tuple record from log file and add them tuples to recovery txn * @param recovery txn */ void AriesFrontendLogger::DeleteTuple(concurrency::Transaction *recovery_txn) { TupleRecord tuple_record(LOGRECORD_TYPE_ARIES_TUPLE_DELETE); // Check for torn log write if (ReadTupleRecordHeader(tuple_record, log_file, log_file_size) == false) { return; } auto txn_id = tuple_record.GetTransactionId(); if (recovery_txn_table.find(txn_id) == recovery_txn_table.end()) { LOG_TRACE("Delete txd id %d not found in recovery txn table", (int)txn_id); return; } auto table = GetTable(tuple_record); ItemPointer delete_location = tuple_record.GetDeleteLocation(); // Try to delete the tuple bool status = table->DeleteTuple(recovery_txn, delete_location); if (status == false) { // TODO: We need to abort on failure ! recovery_txn->SetResult(Result::RESULT_FAILURE); return; } auto txn = recovery_txn_table.at(txn_id); txn->RecordDelete(delete_location); }
/** * @brief read tuple record from log file and add them tuples to recovery txn * @param recovery txn */ void AriesFrontendLogger::UpdateTuple(concurrency::Transaction *recovery_txn) { TupleRecord tuple_record(LOGRECORD_TYPE_ARIES_TUPLE_UPDATE); // Check for torn log write if (ReadTupleRecordHeader(tuple_record, log_file, log_file_size) == false) { return; } auto txn_id = tuple_record.GetTransactionId(); if (recovery_txn_table.find(txn_id) == recovery_txn_table.end()) { LOG_TRACE("Update txd id %d not found in recovery txn table", (int)txn_id); return; } auto txn = recovery_txn_table.at(txn_id); auto table = GetTable(tuple_record); auto tuple = ReadTupleRecordBody(table->GetSchema(), recovery_pool, log_file, log_file_size); // Check for torn log write if (tuple == nullptr) { return; } // First, redo the delete ItemPointer delete_location = tuple_record.GetDeleteLocation(); bool status = table->DeleteTuple(recovery_txn, delete_location); if (status == false) { recovery_txn->SetResult(Result::RESULT_FAILURE); } else { txn->RecordDelete(delete_location); auto target_location = tuple_record.GetInsertLocation(); auto tile_group_id = target_location.block; auto tuple_slot = target_location.offset; auto &manager = catalog::Manager::GetInstance(); auto tile_group = manager.GetTileGroup(tile_group_id); // Create new tile group if table doesn't already have that tile group if (tile_group == nullptr) { table->AddTileGroupWithOid(tile_group_id); tile_group = manager.GetTileGroup(tile_group_id); if (max_oid < tile_group_id) { max_oid = tile_group_id; } } // Do the insert ! auto inserted_tuple_slot = tile_group->InsertTuple( recovery_txn->GetTransactionId(), tuple_slot, tuple); if (inserted_tuple_slot == INVALID_OID) { recovery_txn->SetResult(Result::RESULT_FAILURE); } else { txn->RecordInsert(target_location); } } delete tuple; }
/** * @brief Recovery system based on log file */ void AriesFrontendLogger::DoRecovery() { // Set log file size log_file_size = GetLogFileSize(log_file_fd); // Go over the log size if needed if (log_file_size > 0) { bool reached_end_of_file = false; // Start the recovery transaction auto &txn_manager = concurrency::TransactionManager::GetInstance(); // Although we call BeginTransaction here, recovery txn will not be // recoreded in log file since we are in recovery mode auto recovery_txn = txn_manager.BeginTransaction(); // Go over each log record in the log file while (reached_end_of_file == false) { // Read the first byte to identify log record type // If that is not possible, then wrap up recovery auto record_type = GetNextLogRecordType(log_file, log_file_size); switch (record_type) { case LOGRECORD_TYPE_TRANSACTION_BEGIN: AddTransactionToRecoveryTable(); break; case LOGRECORD_TYPE_TRANSACTION_END: RemoveTransactionFromRecoveryTable(); break; case LOGRECORD_TYPE_TRANSACTION_COMMIT: MoveCommittedTuplesToRecoveryTxn(recovery_txn); break; case LOGRECORD_TYPE_TRANSACTION_ABORT: AbortTuplesFromRecoveryTable(); break; case LOGRECORD_TYPE_ARIES_TUPLE_INSERT: InsertTuple(recovery_txn); break; case LOGRECORD_TYPE_ARIES_TUPLE_DELETE: DeleteTuple(recovery_txn); break; case LOGRECORD_TYPE_ARIES_TUPLE_UPDATE: UpdateTuple(recovery_txn); break; default: reached_end_of_file = true; break; } } // Commit the recovery transaction txn_manager.CommitTransaction(); // Finally, abort ACTIVE transactions in recovery_txn_table AbortActiveTransactions(); // After finishing recovery, set the next oid with maximum oid // observed during the recovery auto &manager = catalog::Manager::GetInstance(); manager.SetNextOid(max_oid); } }
TEST_F(GarbageCollectionTests, DeleteTest) { auto &epoch_manager = concurrency::EpochManagerFactory::GetInstance(); epoch_manager.Reset(1); std::vector<std::unique_ptr<std::thread>> gc_threads; gc::GCManagerFactory::Configure(1); auto &gc_manager = gc::GCManagerFactory::GetInstance(); auto storage_manager = storage::StorageManager::GetInstance(); // create database auto database = TestingExecutorUtil::InitializeDatabase("DELETE_DB"); oid_t db_id = database->GetOid(); EXPECT_TRUE(storage_manager->HasDatabase(db_id)); // create a table with only one key const int num_key = 1; std::unique_ptr<storage::DataTable> table(TestingTransactionUtil::CreateTable( num_key, "DELETE_TABLE", db_id, INVALID_OID, 1234, true)); EXPECT_TRUE(gc_manager.GetTableCount() == 1); gc_manager.StartGC(gc_threads); const int delete_num = 1; DeleteTuple(table.get(), delete_num, num_key); // count garbage num auto old_num = GarbageNum(table.get()); auto recycle_num = RecycledNum(table.get()); // there should be only one garbage // generated by the last update EXPECT_EQ(1, old_num); // nothing is recycled yet. EXPECT_EQ(0, recycle_num); epoch_manager.SetCurrentEpochId(2); // get expired epoch id. // as the current epoch id is set to 2, // the expected expired epoch id should be 1. auto expired_eid = epoch_manager.GetExpiredEpochId(); EXPECT_EQ(1, expired_eid); auto current_eid = epoch_manager.GetCurrentEpochId(); EXPECT_EQ(2, current_eid); // sleep a while for gc to unlink expired version. std::this_thread::sleep_for(std::chrono::seconds(1)); old_num = GarbageNum(table.get()); recycle_num = RecycledNum(table.get()); EXPECT_EQ(1, old_num); EXPECT_EQ(0, recycle_num); epoch_manager.SetCurrentEpochId(3); // sleep a while for gc to recycle expired version. std::this_thread::sleep_for(std::chrono::seconds(1)); // there should be no garbage old_num = GarbageNum(table.get()); recycle_num = RecycledNum(table.get()); EXPECT_EQ(0, old_num); // there should be two versions to be recycled by the GC: // the deleted version and the empty version. // however, the txn will explicitly pass one version (the deleted // version) to the GC manager. // The GC itself should be responsible for recycling the // empty version. EXPECT_EQ(1, recycle_num); gc_manager.StopGC(); gc::GCManagerFactory::Configure(0); table.release(); // DROP! TestingExecutorUtil::DeleteDatabase("DELETE_DB"); auto &txn_manager = concurrency::TransactionManagerFactory::GetInstance(); auto txn = txn_manager.BeginTransaction(); EXPECT_THROW( catalog::Catalog::GetInstance()->GetDatabaseObject("DATABASE0", txn), CatalogException); txn_manager.CommitTransaction(txn); // EXPECT_FALSE(storage_manager->HasDatabase(db_id)); for (auto &gc_thread : gc_threads) { gc_thread->join(); } }