【CMU15445】Fall 2019, Project 4: Logging & Recovery 实验记录

目录

    • 实验准备
    • 实验测试
    • Task 1: LOG MANAGER
      • 1.1 Flush Thread 的实现
        • 1.1.1 Flush Once
        • 1.1.2 Flush Loop
        • 1.1.3 RunFlushThread
        • 1.1.4 StopFlushThread
        • 1.1.5 TriggerFlush
      • 1.2 AppendLogRecord 的实现
      • 1.3 测试
    • Task 2: SYSTEM RECOVERY
      • 2.1 反序列化 LogRecord
      • 2.2 Redo 的实现
        • 2.2.1 RedoCommitOrAbortLog
        • 2.2.2 RedoInsertLog
        • 2.2.3 RedoMarkedDeleteLog
        • 2.2.4 RedoApplyDeleteLog
        • 2.2.5 RedoRollbackDeleteLog
        • 2.2.6 RedoUpdateLog
        • 2.2.7 RedoNewPageLog
      • 2.3 Undo 的实现
      • 2.4 测试
    • Task 3: CHECKPOINTS

实验准备

  • 官方说明:https://15445.courses.cs.cmu.edu/fall2019/project4/

实验测试

Task 1:

cd build
make log_manager_test
./test/log_manager_test

Task 1: LOG MANAGER

Log Manager 有一个全局的实例,负责实现日志的写入和将日志 flush 到磁盘中。

日志类型有固定的多种,当一个事件发生时,程序会调用 log_manager 写入相应类型的日志。日志类型有如下:

/** The type of the log record. */
enum class LogRecordType {
  INVALID = 0,
  INSERT,
  MARKDELETE,
  APPLYDELETE,
  ROLLBACKDELETE,
  UPDATE,
  BEGIN,
  COMMIT,
  ABORT,
  /** Creating a new page in the table heap. */
  NEWPAGE,
};

比如当 buffer pool 调用 NewPage() 时,该函数会通过 log_manager 来追加一个 NEWPAGE 类型的日志记录。

log_manager 主要负责两件事情:

  1. 其他组件通过调用 log_manager 的 AppendLogRecord() 方法来追加日志到日志缓冲区中
  2. 运行着一个后台线程(称之为 Flush Thread),负责定期将日志缓冲区的数据刷入磁盘

代码主要写在 recovery/log_manager.cpp 文件中。

1.1 Flush Thread 的实现

这里需要实现两个函数:

  • RunFlushThread():启动 Flush Thread 的运行
  • StopFlushThread():停止 Flush Thread 的运行

该任务的难点在于多线程代码的编写,包括线程间的通信和同步等。我们一步步来实现。

1.1.1 Flush Once

log_manager 存在两个缓冲区:log bufferflush buffer,每次追加日志时向 log buffer 追加数据,当运行 flush 时,Flush Thread 需要将两个 buffer 互换,将互换后的 flush buffer 写入磁盘。

这里涉及到 LogManager 的如下几个成员变量:

  • log_buffer_:用于追加数据的 buffer
  • flush buffer_:Flush Thread 在往磁盘刷入数据时的 buffer
  • log_buffer_offset_:log buffer 的偏移量
  • flush_buffer_offset_:flush buffer 的偏移量
  • persistent_lsn_:记录目前已写入磁盘的日志的序列号

代码实现:

void LogManager::FlushOnce() {
  if (log_buffer_offset_ > 0) {
    // 互换 log buffer 和 flush buffer
    std::swap(log_buffer_, flush_buffer_);
    std::swap(log_buffer_offset_, flush_buffer_offset_);
    // 将 flush buffer 写入磁盘,并重置 flush buffer offset
    disk_manager_->WriteLog(flush_buffer_, flush_buffer_offset_);
    flush_buffer_offset_ = 0;
    // 更新 persisten_lsn,表示已写入磁盘的日志的序列号
    persistent_lsn_ = next_lsn_ - 1;
  }
}
1.1.2 Flush Loop

我们需要写一个函数 FlushLoopTask(),在 Flush Thread 线程创建后运行这个函数,它通过不断调用 FlushOnce() 来完成在后台定期地将日志缓冲区的数据刷入磁盘。

实验介绍中提到了需要运行 FlushOnce 的三种情况:

  1. 当 log buffer 满了时
  2. 每过了 log_timeout 秒时
  3. 当 buffer pool 需要从 replacer 换出一个页时

因此该线程运行的逻辑应该是:不断循环,等待满足以上三种条件之一,满足后调用一次 FlushOnce() 来将日志缓冲区的数据刷入磁盘。

为了实现“等待满足条件”这个逻辑,这里的代码实现使用了 C++ 的 condition variable

补充:condition variable 的用法
_______________________________________
首先使用 std::mutex 创建一个 std::unique_lock:

std::mutex mu;
std::unique_lock<std::mutex> lock(mu);

然后创建一个 condition variable:std::condition_variable cv;
接着调用 condition variable 的 wait_for 方法来等待:

cv.wait_for(lock, std::chrono::seconds(log_timeout), func);

这一条 wait_for 会一直等待直到满足以下条件之一:

  1. 有其他线程调用了 cv.notify_all();
  2. 过了 log_timeout
  3. 函数 func() 会返回 true

以上条件不满足时,wait_for 会释放掉锁 mu,当条件一直满足时,wait_for() 便会返回,同时锁住 mu

借助 condition variable,可以实现代码:

void LogManager::FlushLoopTask() {
  std::unique_lock<std::mutex> lock(latch_);
  while (true) {
    cv_.wait_for(lock, std::chrono::seconds(log_timeout), [&] { return need_flush_.load(); });
    if (!enable_logging) {
      return;
    }
    // flush
    this->FlushOnce();
    need_flush_ = false;
    flush_once_cv_.notify_all();
  }
}

使用独占锁 latch_ 来防止 flush 线程和追加日志线程同时运行导致冲突。每次完成 FlushOnce 后,都会再调用另一个条件变量 flush_once_cv_.notify_all() 来告诉别人已完成 flush。

1.1.3 RunFlushThread

函数 RunFlushThread() 负责开启 Flush Thread 并在后台一直运行,同时需要设置 enable_logging = true 来开启日志记录:

void LogManager::RunFlushThread() {
  if (enable_logging) {
    return;
  }
  enable_logging = true;
  flush_thread_ = new std::thread(&LogManager::FlushLoopTask, this);
}
1.1.4 StopFlushThread

函数 StopFlushThread() 负责停止 Flush Thread,并设置 enable_logging = false

/*
 * Stop and join the flush thread, set enable_logging = false
 */
void LogManager::StopFlushThread() {
  if (!enable_logging) {
    return;
  }
  enable_logging = false;
  flush_thread_->join();
  delete flush_thread_;
  latch_.lock();
  FlushOnce();
  latch_.unlock();
}

注意这里使用 join() 来等待线程结束,同时再次调用一次 FlushOnce() 保证日志缓冲区的数据全被刷入磁盘中。

1.1.5 TriggerFlush

该函数是供其他组件调用,用于触发一次 flush:

void LogManager::TriggerFlush() {
  need_flush_ = true;
  cv_.notify_all();
}

按照要求,当 buffer pool 从 replacer 换出一个 Page 时,就需要进行一次日志的 flush,因此 buffer_pool_manager 就是通过调用这个 TriggerFlush() 来触发日志的 flush。因此需要修改 BufferPoolManager 的代码:

auto BufferPoolManager::VictimByReplacer(frame_id_t *frame_id) -> bool {
  bool success = replacer_->Victim(frame_id);
  if (!success) {
    return false;
  }
  // flush log to disk when victim a dirty page
  if (enable_logging) {
    auto &page = pages_[*frame_id];
    if (page.IsDirty() && page.GetLSN() > log_manager_->GetPersistentLSN()) {
      log_manager_->TriggerFlush();
    }
  }
  return true;
}

当换出的页是脏页,并且这个页的最新 LSN 大于已持久化的 LSN 的话,就需要进行一次 log flush,从而保证数据库的修改写入磁盘时,相应的日志也被持久化了。

1.2 AppendLogRecord 的实现

该函数实现日志的追加,其他组件就是在需要记录日志时,构造一个 LogRecord 对象并传入 AppendLogRecord() 来完成日志的追加。

这个函数是将日志序列化到 log buffer 中,同时注意维护好 log_buffer_offset。

不同类型的日志具有不同的序列化格式,具体格式要求可以参考实验说明。

auto LogManager::AppendLogRecord(LogRecord *log_record) -> lsn_t {
  std::unique_lock<std::mutex> lock(latch_);
  // 更新 LSN
  log_record->lsn_ = next_lsn_;
  next_lsn_++;
  // 校验 buffer 的大小,如果 buffer 满了的话,需要进行一次 log flush
  if (log_buffer_offset_ + log_record->GetSize() > LOG_BUFFER_SIZE) {
      need_flush_ = true;
      cv_.notify_all();
      flush_once_cv_.wait(lock, [&] { return log_buffer_offset_ + log_record->GetSize() <= LOG_BUFFER_SIZE; });
  }
  // serialize the must have fields(20 bytes in total)
  memcpy(log_buffer_ + log_buffer_offset_, reinterpret_cast<char*>(log_record), LogRecord::HEADER_SIZE);
  int pos = log_buffer_offset_ + LogRecord::HEADER_SIZE;
  const auto log_record_type = log_record->GetLogRecordType();
  if (log_record_type == LogRecordType::INSERT) {
    AppendInsertLogRecord(log_record, pos);
  } else if (log_record_type == LogRecordType::MARKDELETE || log_record_type == LogRecordType::APPLYDELETE ||
             log_record_type == LogRecordType::ROLLBACKDELETE) {
    AppendDeleteLogRecord(log_record, pos);
  } else if (log_record_type == LogRecordType::UPDATE) {
    AppendUpdateLogRecord(log_record, pos);
  } else if (log_record_type == LogRecordType::NEWPAGE) {
    AppendNewPageLogRecord(log_record, pos);
  }
  log_buffer_offset_ += log_record->GetSize();
  return log_record->GetLSN();
}

void LogManager::AppendInsertLogRecord(LogRecord *log_record, int pos) {
  memcpy(log_buffer_ + pos, &log_record->GetInsertRID(), sizeof(RID));
  pos += sizeof(RID);
  const Tuple &insert_tuple = log_record->GetInserteTuple();
  insert_tuple.SerializeTo(log_buffer_ + pos);
}

void LogManager::AppendDeleteLogRecord(LogRecord *log_record, int pos) {
  memcpy(log_buffer_ + pos, &log_record->GetDeleteRID(), sizeof(RID));
  pos += sizeof(RID);
  const Tuple &delete_tuple = log_record->delete_tuple_;
  delete_tuple.SerializeTo(log_buffer_ + pos);
}

void LogManager::AppendUpdateLogRecord(LogRecord *log_record, int pos) {
  memcpy(log_buffer_ + pos, &log_record->update_rid_, sizeof(RID));
  pos += sizeof(RID);
  const Tuple &old_tuple = log_record->old_tuple_;
  const Tuple &new_tuple = log_record->new_tuple_;
  const auto old_tuple_size = old_tuple.GetLength();
  old_tuple.SerializeTo(log_buffer_ + pos);
  pos += static_cast<int>(sizeof(old_tuple_size) + old_tuple_size);
  new_tuple.SerializeTo(log_buffer_ + pos);
}

void LogManager::AppendNewPageLogRecord(LogRecord *log_record, int pos) {
  memcpy(log_buffer_ + pos, &log_record->prev_page_id_, sizeof(page_id_t));
  pos += sizeof(page_id_t);
  memcpy(log_buffer_ + pos, &log_record->page_id_, sizeof(page_id_t));
}

完成 AppendLogRecord() 后,需要在几个事件发生时,通过调用该函数来记下日志。本实验中,TablePage 类的几个方法如 TablePage::InsertTuple 等都已实现日志的记录,我们需要为事务开始、提交或者终止时添加日志的记录,所需要修改的函数如下:

  • TransactionManager::Begin
  • TransactionManager::Commit
  • TransactionManager::Abort

1.3 测试

【CMU15445】Fall 2019, Project 4: Logging & Recovery 实验记录_第1张图片

实现以上代码后,可以顺利通过 log_manager_test。

Task 2: SYSTEM RECOVERY

本实验使用一种粗暴的方式完成从日志的系统恢复:

  • Redo 阶段:从头开始逐条遍历日志,重放每条日志的行为,同时维护出一份 ATT 表(活跃事务表),即系统崩溃时还有哪些事务在活跃。
  • Undo 阶段:遍历 ATT 表中的每个事务,倒序遍历它的所有日志,逐个进行撤销

2.1 反序列化 LogRecord

读取日志时,我们需要从之前序列化到磁盘的日志数据反序列化为一个个的 LogRecord 对象,因此我们需要先实现 DeserializeLogRecord() 方法:

/*
 * deserialize a log record from log buffer
 * @return: true means deserialize succeed, otherwise can't deserialize cause
 * incomplete log record
 */
auto LogRecovery::DeserializeLogRecord(const char *data, LogRecord *log_record) -> bool {
  const auto *const src_record = reinterpret_cast<const LogRecord *>(data);
  if (src_record->size_ < 20 || data + src_record->size_ > log_buffer_ + LOG_BUFFER_SIZE) {
    return false;
  }
  memcpy(reinterpret_cast<char *>(log_record), data, LogRecord::HEADER_SIZE);
  int offset = LogRecord::HEADER_SIZE;
  if (log_record->GetLogRecordType() == LogRecordType::INVALID) {
    return false;
  }
  const auto log_record_type = log_record->GetLogRecordType();
  if (log_record_type == LogRecordType::INSERT) {
    log_record->insert_rid_ = *(reinterpret_cast<const RID *>(data + offset));
    offset += sizeof(RID);
    log_record->insert_tuple_.DeserializeFrom(data + offset);
  } else if (log_record_type == LogRecordType::MARKDELETE || log_record_type == LogRecordType::APPLYDELETE ||
             log_record_type == LogRecordType::ROLLBACKDELETE) {
    log_record->delete_rid_ = *(reinterpret_cast<const RID *>(data + offset));
    offset += sizeof(RID);
    log_record->delete_tuple_.DeserializeFrom(data + offset);
  } else if (log_record_type == LogRecordType::UPDATE) {
    log_record->update_rid_ = *(reinterpret_cast<const RID *>(data + offset));
    offset += sizeof(RID);
    log_record->old_tuple_.DeserializeFrom(data + offset);
    offset += static_cast<int>(sizeof(uint32_t) + log_record->old_tuple_.GetLength());
    log_record->new_tuple_.DeserializeFrom(data + offset);
  } else if (log_record_type == LogRecordType::NEWPAGE) {
    log_record->prev_page_id_ = *(reinterpret_cast<const page_id_t *>(data + offset));
    offset += static_cast<int>(sizeof(page_id_t));
    log_record->page_id_ = *(reinterpret_cast<const page_id_t *>(data + offset));
  }
  return true;
}

2.2 Redo 的实现

Redo 阶段从头遍历每个日志,并根据日志的类型采取相应的重放动作。

在重放 NEWPAGE 日志时注意,崩溃前已经创建出了磁盘的页,也就是磁盘中已经分配了相应的空间,因此 Redo 重放时不需要重新在磁盘中申请分配空间,只需要对这块空间进行重新初始化就好了。由于重放 NEWPAGE 行为,我们需要得知是初始化哪个 page,而原代码没有在日志中记录这个 page_id,因此我们需要修改一下原来的代码:

void TablePage::Init(page_id_t page_id, uint32_t page_size, page_id_t prev_page_id, LogManager *log_manager,
                     Transaction *txn) {
  // Set the page ID.
  memcpy(GetData(), &page_id, sizeof(page_id));
  // Log that we are creating a new page.
  if (enable_logging) {
    LogRecord log_record = LogRecord(txn->GetTransactionId(), txn->GetPrevLSN(), LogRecordType::NEWPAGE, prev_page_id, page_id);
    lsn_t lsn = log_manager->AppendLogRecord(&log_record);
    SetLSN(lsn);
    
    txn->SetPrevLSN(lsn);
  }
  ....

下面开始编写 Redo 的逻辑,这个过程需要维护两个数据结构:

  • lsn_mapping_:将日志 LSN 映射到该日志在磁盘中的 offset
  • active_txn_:即 ATT 表,记录了数据库崩溃时仍然活跃的事务,以及这些事务的最后一条日志的 LSN

代码:

void LogRecovery::Redo() {
  offset_ = 0;
  LogRecord record;
  while (disk_manager_->ReadLog(log_buffer_, LOG_BUFFER_SIZE, offset_)) {
    size_t buffer_offset = 0;
    while (DeserializeLogRecord(log_buffer_ + buffer_offset, &record)) {
      const auto lsn = record.GetLSN();
      const auto record_size = record.GetSize();
      buffer_offset += record_size;
      lsn_mapping_.insert({lsn, offset_});   // lsn -> 该日志在磁盘中的 offset
      offset_ += record_size;
      active_txn_[record.GetTxnId()] = lsn;  // 记录 ATT 表,txn_id -> 最后一条日志的 lsn
      const auto record_type = record.GetLogRecordType();
      switch (record_type) {
        case LogRecordType::COMMIT:
        case LogRecordType::ABORT:
          RedoCommitOrAbortLog(record);
          break;
        case LogRecordType::INSERT:
          RedoInsertLog(record);
          break;
        case LogRecordType::MARKDELETE:
          RedoMarkedDeleteLog(record);
          break;
        case LogRecordType::APPLYDELETE:
          RedoApplyDeleteLog(record);
          break;
        case LogRecordType::ROLLBACKDELETE:
          RedoRollbackDeleteLog(record);
          break;
        case LogRecordType::UPDATE:
          RedoUpdateLog(record);
          break;
        case LogRecordType::NEWPAGE:
          RedoNewPageLog(record);
          break;
        default:
          break;
      }
    }
  }
}
2.2.1 RedoCommitOrAbortLog

Commit 和 Abort 代表事务的结束,因此需要将该事务从 ATT 表中移除:

void LogRecovery::RedoCommitOrAbortLog(LogRecord &log_record) {
  active_txn_.erase(log_record.GetTxnId());
}
2.2.2 RedoInsertLog

重新执行 insert:

void LogRecovery::RedoInsertLog(LogRecord &log_record) {
  auto &rid = log_record.GetInsertRID();
  const auto page_id = rid.GetPageId();
  auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
  bool is_dirty = false;
  if (page->GetLSN() < log_record.GetLSN()) {
    is_dirty = true;
    page->WLatch();
    page->InsertTuple(log_record.insert_tuple_, &rid, nullptr, nullptr, nullptr);
    page->WUnlatch();
  }
  buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
2.2.3 RedoMarkedDeleteLog
void LogRecovery::RedoMarkedDeleteLog(LogRecord &log_record) {
  auto &rid = log_record.GetDeleteRID();
  const auto page_id = rid.GetPageId();
  auto *page = buffer_pool_manager_->FetchPage(page_id);
  bool is_dirty = false;
  if (page->GetLSN() < log_record.GetLSN()) {
    is_dirty = true;
    auto *table_page = reinterpret_cast<TablePage *>(page);
    table_page->WLatch();
    table_page->MarkDelete(rid, nullptr, nullptr, nullptr);
    table_page->WUnlatch();
  }
  buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
2.2.4 RedoApplyDeleteLog
void LogRecovery::RedoApplyDeleteLog(LogRecord &log_record) {
  auto &rid = log_record.GetDeleteRID();
  const auto page_id = rid.GetPageId();
  auto *page = buffer_pool_manager_->FetchPage(page_id);
  bool is_dirty = false;
  if (page->GetLSN() < log_record.GetLSN()) {
    is_dirty = true;
    auto *table_page = reinterpret_cast<TablePage *>(page);
    table_page->WLatch();
    table_page->ApplyDelete(rid, nullptr, nullptr);
    table_page->WUnlatch();
  }
  buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
2.2.5 RedoRollbackDeleteLog
void LogRecovery::RedoRollbackDeleteLog(LogRecord &log_record) {
  auto &rid = log_record.GetDeleteRID();
  const auto page_id = rid.GetPageId();
  auto *page = buffer_pool_manager_->FetchPage(page_id);
  bool is_dirty = false;
  if (page->GetLSN() < log_record.GetLSN()) {
    is_dirty = true;
    auto *table_page = reinterpret_cast<TablePage *>(page);
    table_page->WLatch();
    table_page->RollbackDelete(rid, nullptr, nullptr);
    table_page->WUnlatch();
  }
  buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
2.2.6 RedoUpdateLog
void LogRecovery::RedoUpdateLog(LogRecord &log_record) {
  auto &rid = log_record.update_rid_;
  const auto page_id = rid.GetPageId();
  auto *page = buffer_pool_manager_->FetchPage(page_id);
  bool is_dirty = false;
  if (page->GetLSN() < log_record.GetLSN()) {
    is_dirty = true;
    auto *table_page = reinterpret_cast<TablePage *>(page);
    table_page->WLatch();
    table_page->UpdateTuple(log_record.new_tuple_, &log_record.old_tuple_, rid, nullptr, nullptr, nullptr);
    table_page->WUnlatch();
  }
  buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}
2.2.7 RedoNewPageLog
void LogRecovery::RedoNewPageLog(LogRecord &log_record) {
  auto *page = buffer_pool_manager_->FetchPage(log_record.page_id_);
  const auto page_id = log_record.page_id_; 
  auto *table_page = reinterpret_cast<TablePage *>(page);
  bool is_dirty = false;
  if (table_page->GetLSN() < log_record.GetLSN()) {
    is_dirty = true;
    const auto prev_page_id = log_record.prev_page_id_;
    table_page->WLatch();
    table_page->Init(page_id, PAGE_SIZE, prev_page_id, nullptr, nullptr);
    table_page->WUnlatch();

    if (prev_page_id != INVALID_PAGE_ID) {
      auto *prev_page = buffer_pool_manager_->FetchPage(prev_page_id);
      auto *prev_table_page = reinterpret_cast<TablePage *>(prev_page);
      prev_table_page->WLatch();
      prev_table_page->SetNextPageId(page_id);
      prev_table_page->WUnlatch();
      buffer_pool_manager_->UnpinPage(prev_page_id, true);
    }
  }
  buffer_pool_manager_->UnpinPage(page_id, is_dirty);
}

2.3 Undo 的实现

Undo 阶段遍历 ATT 表的每个事务,对每个事务倒序遍历其对应的所有日志,逐个进行撤销。

这里所说的”撤销“,其实就是做一个补偿操作,比如对 Insert 日志的补偿操作就是 ApplyDelete。实验说明中列出了如下需要做的补偿操作:Insert <-> ApplyDelete, MarkDelete <-> RollBackDelete, Update <-> Update, NewPage <-> (do nothing)

void LogRecovery::Undo() {
  LogRecord log_record;
  for (const auto& item: active_txn_) {
    auto lsn = item.second;
    while (lsn != INVALID_LSN) {
      const auto offset = lsn_mapping_[lsn];
      disk_manager_->ReadLog(log_buffer_, LOG_BUFFER_SIZE, offset);
      DeserializeLogRecord(log_buffer_, &log_record);
      const auto log_record_type = log_record.log_record_type_;
      if (log_record_type == LogRecordType::BEGIN) {
        break;
      }
      switch (log_record_type) {
        case LogRecordType::INSERT:
          UndoInsertLog(log_record);
          break;
        case LogRecordType::MARKDELETE:
          UndoMarkDeleteLog(log_record);
          break;
        case LogRecordType::UPDATE:
          UndoUpdateLog(log_record);
          break;
        case LogRecordType::NEWPAGE:
          UndoNewPageLog(log_record);
          break;
        default:
          break;
      }
      lsn = log_record.GetPrevLSN();
    }
  }
}

void LogRecovery::UndoInsertLog(LogRecord &log_record) {
  const auto &rid = log_record.GetInsertRID();
  const auto page_id = rid.GetPageId();
  auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
  page->WLatch();
  page->ApplyDelete(rid, nullptr, nullptr);
  page->WUnlatch();
  buffer_pool_manager_->UnpinPage(page_id, true);
}

void LogRecovery::UndoMarkDeleteLog(LogRecord &log_record) {
  const auto &rid = log_record.GetDeleteRID();
  const auto page_id = rid.GetPageId();
  auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
  page->WLatch();
  page->RollbackDelete(rid, nullptr, nullptr);
  page->WUnlatch();
  buffer_pool_manager_->UnpinPage(page_id, true);
}

void LogRecovery::UndoUpdateLog(LogRecord &log_record) {
  const auto &rid = log_record.update_rid_;
  const auto page_id = rid.GetPageId();
  auto *page = reinterpret_cast<TablePage*>(buffer_pool_manager_->FetchPage(page_id));
  page->WLatch();
  page->UpdateTuple(log_record.old_tuple_, &log_record.new_tuple_, rid, nullptr, nullptr, nullptr);
  page->WUnlatch();
  buffer_pool_manager_->UnpinPage(page_id, true);
}

void LogRecovery::UndoNewPageLog(LogRecord &log_record) {}

2.4 测试

完成以上代码后 pass 所有关于 Redo 和 Undo 的测试。

【CMU15445】Fall 2019, Project 4: Logging & Recovery 实验记录_第2张图片

Task 3: CHECKPOINTS

这个 task 很简单,代码量也很小。本实验的思路是,做快照时阻塞住所有的事务,将所有日志、buffer pool 的数据全部刷入磁盘中,快照结束后再恢复事务的继续执行。

void CheckpointManager::BeginCheckpoint() {
  // Block all the transactions and ensure that both the WAL and all dirty buffer pool pages are persisted to disk,
  // creating a consistent checkpoint. Do NOT allow transactions to resume at the end of this method, resume them
  // in CheckpointManager::EndCheckpoint() instead. This is for grading purposes.
  transaction_manager_->BlockAllTransactions();
  log_manager_->StopFlushThread();
  buffer_pool_manager_->FlushAllPages();
}

void CheckpointManager::EndCheckpoint() {
  // Allow transactions to resume, completing the checkpoint.
  log_manager_->RunFlushThread();
  transaction_manager_->ResumeTransactions();
}

测试:

【CMU15445】Fall 2019, Project 4: Logging & Recovery 实验记录_第3张图片

至此,算是通关了 CMU 15445 的 Fall 2019 的全部实验了~

你可能感兴趣的:(CMU15445,数据库,CMU15445)