#include "storage/shadow_file.h"
#include <algorithm>
#include "common/exception/io.h"
#include "common/exception/runtime.h"
#include "common/file_system/virtual_file_system.h"
#include "common/serializer/buffer_reader.h"
#include "common/serializer/buffered_file.h"
#include "common/serializer/deserializer.h"
#include "common/serializer/serializer.h"
#include "main/client_context.h"
#include "main/db_config.h"
#include "storage/buffer_manager/buffer_manager.h"
#include "storage/buffer_manager/memory_manager.h"
#include "storage/database_header.h"
#include "storage/file_db_id_utils.h"
#include "storage/file_handle.h"
#include "storage/storage_manager.h"
#include <format>
using namespace lbug::common;
using namespace lbug::main;
namespace lbug {
namespace storage {
namespace {
class BoundedPageReader final : public Reader {
public:
BoundedPageReader(uint8_t* data, uint64_t dataSize, std::string outOfBoundsMessage)
: data{data}, dataSize{dataSize}, outOfBoundsMessage{std::move(outOfBoundsMessage)} {}
void read(uint8_t* outputData, uint64_t size) override {
if (size > dataSize - readOffset) {
throw RuntimeException(outOfBoundsMessage);
}
std::memcpy(outputData, data + readOffset, size);
readOffset += size;
}
bool finished() override { return readOffset >= dataSize; }
uint64_t getReadOffset() const override { return readOffset; }
private:
uint8_t* data;
uint64_t dataSize;
uint64_t readOffset = 0;
std::string outOfBoundsMessage;
};
}
void ShadowPageRecord::serialize(Serializer& serializer) const {
serializer.write<file_idx_t>(originalFileIdx);
serializer.write<page_idx_t>(originalPageIdx);
}
ShadowPageRecord ShadowPageRecord::deserialize(Deserializer& deserializer) {
file_idx_t originalFileIdx = INVALID_FILE_IDX;
page_idx_t originalPageIdx = INVALID_PAGE_IDX;
deserializer.deserializeValue<file_idx_t>(originalFileIdx);
deserializer.deserializeValue<page_idx_t>(originalPageIdx);
return ShadowPageRecord{originalFileIdx, originalPageIdx};
}
ShadowFile::ShadowFile(BufferManager& bm, VirtualFileSystem* vfs, const std::string& databasePath)
: bm{bm}, shadowFilePath{StorageUtils::getShadowFilePath(databasePath)}, vfs{vfs},
shadowingFH{nullptr} {
DASSERT(vfs);
}
void ShadowFile::setDatabasePath(const std::string& databasePath_) {
shadowFilePath = StorageUtils::getShadowFilePath(databasePath_);
}
void ShadowFile::clearShadowPage(file_idx_t originalFile, page_idx_t originalPage) {
std::unique_lock lck{mtx};
if (hasShadowPage(originalFile, originalPage)) {
shadowPagesMap.at(originalFile).erase(originalPage);
if (shadowPagesMap.at(originalFile).empty()) {
shadowPagesMap.erase(originalFile);
}
}
}
page_idx_t ShadowFile::createShadowPage(file_idx_t originalFile, page_idx_t originalPage) {
DASSERT(!hasShadowPage(originalFile, originalPage));
std::unique_lock lck{mtx};
const auto shadowPageIdx = getOrCreateShadowingFH()->addNewPage();
shadowPageRecords.push_back({originalFile, originalPage});
return shadowPageIdx;
}
void ShadowFile::publishShadowPage(file_idx_t originalFile, page_idx_t originalPage,
page_idx_t shadowPageIdx) {
std::unique_lock lck{mtx};
shadowPagesMap[originalFile][originalPage] = shadowPageIdx;
hasShadowPages.store(true, std::memory_order_release);
}
ShadowFile::ShadowSavepoint ShadowFile::createSavepoint() const {
std::shared_lock lck{mtx};
return shadowPageRecords.size();
}
void ShadowFile::rollbackToSavepoint(ShadowSavepoint savepoint) {
std::unique_lock lck{mtx};
DASSERT(savepoint <= shadowPageRecords.size());
if (savepoint == shadowPageRecords.size()) {
return;
}
for (auto i = savepoint; i < shadowPageRecords.size(); i++) {
const auto& record = shadowPageRecords[i];
auto fileIt = shadowPagesMap.find(record.originalFileIdx);
if (fileIt != shadowPagesMap.end()) {
fileIt->second.erase(record.originalPageIdx);
if (fileIt->second.empty()) {
shadowPagesMap.erase(fileIt);
}
}
}
if (shadowingFH != nullptr) {
const auto numPages = shadowingFH->getNumPages();
for (auto pageIdx = savepoint + 1; pageIdx < numPages; pageIdx++) {
shadowingFH->removePageFromFrameIfNecessary(static_cast<page_idx_t>(pageIdx));
}
shadowingFH->removePageIdxAndTruncateIfNecessary(static_cast<page_idx_t>(savepoint + 1));
}
shadowPageRecords.resize(savepoint);
hasShadowPages.store(!shadowPagesMap.empty(), std::memory_order_relaxed);
}
page_idx_t ShadowFile::getShadowPage(file_idx_t originalFile, page_idx_t originalPage) const {
DASSERT(hasShadowPage(originalFile, originalPage));
return shadowPagesMap.at(originalFile).at(originalPage);
}
bool ShadowFile::readShadowVersionIfExists(file_idx_t originalFile, page_idx_t originalPage,
const std::function<void(uint8_t*)>& readOp) const {
if (!hasShadowPages.load(std::memory_order_acquire)) {
return false;
}
std::shared_lock lck{mtx};
if (!hasShadowPage(originalFile, originalPage)) {
return false;
}
shadowingFH->optimisticReadPage(shadowPagesMap.at(originalFile).at(originalPage), readOp);
return true;
}
void ShadowFile::applyShadowPages(StorageManager& storageManager, ClientContext& context) const {
const auto pageBuffer = std::make_unique<uint8_t[]>(LBUG_PAGE_SIZE);
page_idx_t shadowPageIdx = 1; auto dataFH = storageManager.getDataFH();
auto dataFileInfo = dataFH->getFileInfo();
DASSERT(shadowingFH);
for (const auto& record : shadowPageRecords) {
shadowingFH->readPageFromDisk(pageBuffer.get(), shadowPageIdx++);
dataFileInfo->writeFile(pageBuffer.get(), LBUG_PAGE_SIZE,
record.originalPageIdx * LBUG_PAGE_SIZE);
MemoryManager::Get(context)->getBufferManager()->updateFrameIfPageIsInFrame(
record.originalFileIdx, pageBuffer.get(), record.originalPageIdx);
}
dataFileInfo->syncFile();
}
static uuid getOldDatabaseID(FileInfo& dataFileInfo) {
auto oldHeader = DatabaseHeader::readDatabaseHeader(dataFileInfo);
if (!oldHeader.has_value()) {
throw InternalException("Found a shadow file for database {} but no valid database header. "
"The database is corrupted, please recreate it.");
}
return oldHeader->databaseID;
}
void ShadowFile::replayShadowPageRecords(ClientContext& context) {
replayShadowPageRecords(context, context.getDatabasePath());
}
void ShadowFile::replayShadowPageRecords(ClientContext& context, const std::string& databasePath,
std::optional<uuid> legacyDatabaseID) {
if (context.getDBConfig()->readOnly) {
throw RuntimeException("Couldn't replay shadow pages under read-only mode. Please re-open "
"the database with read-write mode to replay shadow pages.");
}
auto vfs = VirtualFileSystem::GetUnsafe(context);
auto shadowFilePath = StorageUtils::getShadowFilePath(databasePath);
auto shadowFileInfo = vfs->openFile(shadowFilePath, FileOpenFlags(FileFlags::READ_ONLY));
std::unique_ptr<FileInfo> dataFileInfo;
try {
dataFileInfo = vfs->openFile(databasePath,
FileOpenFlags{FileFlags::WRITE | FileFlags::READ_ONLY, FileLockType::WRITE_LOCK});
} catch (IOException& e) {
throw RuntimeException(std::format(
"Found shadow file {} but no corresponding database file. This file "
"may have been left behind from a previous database with the same name. If it is safe "
"to do so, please delete this file and restart the database.",
shadowFilePath));
}
replayShadowPageRecordsCore(*shadowFileInfo, *dataFileInfo,
context.getDBConfig()->maxDBSize / LBUG_PAGE_SIZE, legacyDatabaseID);
}
void ShadowFile::replayShadowPageRecordsForStorageManager(ClientContext& context,
StorageManager& storageManager, std::optional<uuid> legacyDatabaseID) {
if (context.getDBConfig()->readOnly) {
throw RuntimeException("Couldn't replay shadow pages under read-only mode. Please re-open "
"the database with read-write mode to replay shadow pages.");
}
auto vfs = VirtualFileSystem::GetUnsafe(context);
auto shadowFilePath = StorageUtils::getShadowFilePath(storageManager.getDatabasePath());
auto shadowFileInfo = vfs->openFile(shadowFilePath, FileOpenFlags(FileFlags::READ_ONLY));
auto* dataFH = storageManager.getDataFH();
replayShadowPageRecordsCore(*shadowFileInfo, *dataFH->getFileInfo(),
context.getDBConfig()->maxDBSize / LBUG_PAGE_SIZE, legacyDatabaseID);
const auto fileSize = dataFH->getFileInfo()->getFileSize();
const auto numPages = (fileSize + dataFH->getPageSize() - 1) / dataFH->getPageSize();
if (numPages > dataFH->getNumPages()) {
dataFH->addNewPages(numPages - dataFH->getNumPages());
}
}
void ShadowFile::replayShadowPageRecordsCore(FileInfo& shadowFileInfo, FileInfo& dataFileInfo,
uint64_t maxDatabasePages, std::optional<uuid> legacyDatabaseID) {
const auto shadowFileSize = shadowFileInfo.getFileSize();
if (shadowFileSize < LBUG_PAGE_SIZE + sizeof(uint64_t)) {
throw RuntimeException(
std::format("Cannot replay shadow file {}: file is too small.", shadowFileInfo.path));
}
ShadowFileHeader header;
const auto headerBuffer = std::make_unique<uint8_t[]>(LBUG_PAGE_SIZE);
shadowFileInfo.readFromFile(headerBuffer.get(), LBUG_PAGE_SIZE, 0);
memcpy(&header, headerBuffer.get(), sizeof(ShadowFileHeader));
const auto maxShadowPagesInFile = shadowFileSize / LBUG_PAGE_SIZE - 1;
if (header.numShadowPages == INVALID_PAGE_IDX || header.numShadowPages > maxShadowPagesInFile) {
throw RuntimeException(
std::format("Cannot replay shadow file {}: invalid shadow page count {}.",
shadowFileInfo.path, header.numShadowPages));
}
if (header.numShadowPages > maxDatabasePages) {
throw RuntimeException(std::format(
"Cannot replay shadow file {}: it needs {} shadow pages, but max_db_size limits "
"the database to {} pages ({} bytes). Reopen the database with a larger "
"max_db_size, keeping the WAL and shadow files in place.",
shadowFileInfo.path, header.numShadowPages, maxDatabasePages,
maxDatabasePages * LBUG_PAGE_SIZE));
}
const auto recordsOffset = (static_cast<uint64_t>(header.numShadowPages) + 1) * LBUG_PAGE_SIZE;
const auto recordsSize = sizeof(uint64_t) + static_cast<uint64_t>(header.numShadowPages) *
(sizeof(file_idx_t) + sizeof(page_idx_t));
if (recordsOffset > shadowFileSize || recordsSize > shadowFileSize - recordsOffset) {
throw RuntimeException(std::format(
"Cannot replay shadow file {}: page records are truncated.", shadowFileInfo.path));
}
const auto oldDatabaseID = getOldDatabaseID(dataFileInfo);
const bool usesLegacyDatabaseID = header.databaseID.value != oldDatabaseID.value &&
legacyDatabaseID.has_value() &&
header.databaseID.value == legacyDatabaseID->value;
if (header.databaseID.value != oldDatabaseID.value && !usesLegacyDatabaseID) {
FileDBIDUtils::verifyDatabaseID(shadowFileInfo, oldDatabaseID, header.databaseID);
}
auto reader = std::make_unique<BufferedFileReader>(shadowFileInfo);
reader->resetReadOffset(recordsOffset);
Deserializer deSer(std::move(reader));
uint64_t numShadowPageRecords = 0;
deSer.deserializeValue(numShadowPageRecords);
if (numShadowPageRecords != header.numShadowPages) {
throw RuntimeException(std::format(
"Cannot replay shadow file {}: header declares {} pages but the record list declares "
"{}.",
shadowFileInfo.path, header.numShadowPages, numShadowPageRecords));
}
const auto dataFileSize = dataFileInfo.getFileSize();
const auto currentDataPages =
dataFileSize / LBUG_PAGE_SIZE + static_cast<uint64_t>(dataFileSize % LBUG_PAGE_SIZE != 0);
if (currentDataPages > maxDatabasePages) {
throw RuntimeException(std::format(
"Cannot replay shadow file {}: database file exceeds the configured maximum size.",
shadowFileInfo.path));
}
std::vector<ShadowPageRecord> records;
records.reserve(numShadowPageRecords);
for (auto i = 0u; i < numShadowPageRecords; i++) {
const auto record = ShadowPageRecord::deserialize(deSer);
if (record.originalFileIdx == INVALID_FILE_IDX ||
record.originalPageIdx == INVALID_PAGE_IDX) {
throw RuntimeException(
std::format("Cannot replay shadow file {}: invalid target page {}.",
shadowFileInfo.path, record.originalPageIdx));
}
if (record.originalPageIdx >= maxDatabasePages) {
throw RuntimeException(std::format(
"Cannot replay shadow file {}: its target page {} is beyond the {} pages "
"({} bytes) allowed by max_db_size. Reopen the database with a larger "
"max_db_size, keeping the WAL and shadow files in place.",
shadowFileInfo.path, record.originalPageIdx, maxDatabasePages,
maxDatabasePages * LBUG_PAGE_SIZE));
}
records.push_back(record);
}
const auto headerRecord =
std::find_if(records.rbegin(), records.rend(), [](const auto& record) {
return record.originalPageIdx == StorageConstants::DB_HEADER_PAGE_IDX;
});
if (headerRecord == records.rend()) {
throw RuntimeException(std::format(
"Cannot replay shadow file {}: database header page is missing.", shadowFileInfo.path));
}
const auto headerShadowPageIdx = static_cast<page_idx_t>(
records.size() - static_cast<size_t>(std::distance(records.rbegin(), headerRecord)));
const auto headerPage = std::make_unique<uint8_t[]>(LBUG_PAGE_SIZE);
shadowFileInfo.readFromFile(headerPage.get(), LBUG_PAGE_SIZE,
headerShadowPageIdx * LBUG_PAGE_SIZE);
Deserializer headerDeserializer{
std::make_unique<BoundedPageReader>(headerPage.get(), LBUG_PAGE_SIZE,
std::format("Cannot recover committed checkpoint: shadow file {} holds a "
"corrupt database header page.",
shadowFileInfo.path))};
const auto checkpointHeader = DatabaseHeader::deserialize(headerDeserializer);
if (checkpointHeader.databaseID.value != oldDatabaseID.value) {
throw RuntimeException(std::format(
"Cannot recover committed checkpoint: shadow file {} does not match the Database "
"ID "
"of {}. Do not delete the shadow file; it holds committed pages. Restore the "
"database file it was written for.",
shadowFileInfo.path, dataFileInfo.path));
}
for (const auto& record : records) {
if (record.originalPageIdx >= checkpointHeader.dataFileNumPages) {
throw RuntimeException(
std::format("Cannot replay shadow file {}: invalid target page {}.",
shadowFileInfo.path, record.originalPageIdx));
}
}
const auto pageBuffer = std::make_unique<uint8_t[]>(LBUG_PAGE_SIZE);
page_idx_t shadowPageIdx = 1;
for (const auto& record : records) {
shadowFileInfo.readFromFile(pageBuffer.get(), LBUG_PAGE_SIZE,
shadowPageIdx * LBUG_PAGE_SIZE);
dataFileInfo.writeFile(pageBuffer.get(), LBUG_PAGE_SIZE,
record.originalPageIdx * LBUG_PAGE_SIZE);
shadowPageIdx++;
}
dataFileInfo.syncFile();
}
void ShadowFile::flushAll(const uuid& ownerDatabaseID) const {
ShadowFileHeader header;
header.numShadowPages = shadowPageRecords.size();
header.databaseID = CHECKPOINT_BUNDLE_DATABASE_ID;
header.ownerDatabaseID = ownerDatabaseID;
const auto headerBuffer = std::make_unique<uint8_t[]>(LBUG_PAGE_SIZE);
memcpy(headerBuffer.get(), &header, sizeof(ShadowFileHeader));
DASSERT(shadowingFH && !shadowingFH->isInMemoryMode());
shadowingFH->writePageToFile(headerBuffer.get(), 0);
shadowingFH->flushAllDirtyPagesInFrames();
const auto writer = std::make_shared<BufferedFileWriter>(*shadowingFH->getFileInfo());
writer->setFileOffset(shadowingFH->getNumPages() * LBUG_PAGE_SIZE);
Serializer ser(writer);
DASSERT(shadowPageRecords.size() + 1 == shadowingFH->getNumPages());
ser.serializeVector(shadowPageRecords);
writer->flush();
writer->sync();
}
void ShadowFile::clear(BufferManager& bm, bool syncParentDirectory) {
DASSERT(shadowingFH);
std::unique_lock lck{mtx};
bm.removeFilePagesFromFrames(*shadowingFH);
shadowPagesMap.clear();
shadowPageRecords.clear();
hasShadowPages.store(false, std::memory_order_release);
shadowingFH->resetToZeroPagesAndPageCapacity();
shadowingFH->resetFileInfo();
vfs->removeFileIfExists(shadowFilePath);
if (syncParentDirectory) {
vfs->syncParentDirectory(shadowFilePath);
}
}
void ShadowFile::reset() {
if (shadowingFH == nullptr) {
return;
}
std::unique_lock lck{mtx};
shadowPagesMap.clear();
shadowPageRecords.clear();
hasShadowPages.store(false, std::memory_order_release);
if (shadowingFH->getFileInfo() != nullptr) {
bm.removeFilePagesFromFrames(*shadowingFH);
shadowingFH->resetToZeroPagesAndPageCapacity();
shadowingFH->resetFileInfo();
vfs->removeFileIfExists(shadowFilePath);
vfs->syncParentDirectory(shadowFilePath);
}
}
FileHandle* ShadowFile::getOrCreateShadowingFH() {
if (!shadowingFH) {
shadowingFH = bm.getFileHandle(shadowFilePath,
FileHandle::O_PERSISTENT_FILE_CREATE_NOT_EXISTS, vfs, nullptr);
} else if (shadowingFH->getFileInfo() == nullptr) {
shadowingFH->setFileInfo(vfs->openFile(shadowFilePath,
FileOpenFlags(
FileFlags::WRITE | FileFlags::READ_ONLY | FileFlags::CREATE_IF_NOT_EXISTS),
nullptr));
}
if (shadowingFH->getNumPages() == 0) {
shadowingFH->addNewPage();
}
return shadowingFH;
}
} }