diff --git a/src/buffer/buffer_manager.cc b/src/buffer/buffer_manager.cc new file mode 100644 index 0000000..b9fb914 --- /dev/null +++ b/src/buffer/buffer_manager.cc @@ -0,0 +1,244 @@ + +#include +#include +#include +#include +#include + +#include "buffer/buffer_manager.h" +#include "common/macros.h" +#include "storage/file.h" + +#define UNUSED(p) ((void)(p)) + +namespace buzzdb { + +char* BufferFrame::get_data() { + return reinterpret_cast(data.data()); +} + +BufferFrame::BufferFrame(size_t page_size) { + this->data = std::vector(page_size); + this->count = 0; + this->dirty = false; +} + +BufferManager::BufferManager(size_t page_size, size_t page_count) { + this->page_size = page_size; + this->page_count = page_count; + for(size_t i=0; i(page_size)); + } +} + + + +BufferManager::~BufferManager() { + for (size_t i = 0; i < pool_.size(); i++) + { + if(pool_[i]->dirty){ + flush(i); + } + } +} + +std::pair BufferManager::page_in_fifo_queue(uint64_t page_id){ + + bool found_page = false; + uint64_t page_frame_id = INT_MAX; + for(size_t i = 0; i < fifo_queue_.size(); i++){ + auto frame_id = fifo_queue_[i]; + if(pool_[frame_id]->page_id==page_id){ + found_page=true; + page_frame_id=frame_id; + pool_[page_frame_id]->count++; + fifo_queue_.erase(fifo_queue_.begin() + i); + lru_queue_.push_back(frame_id); + break; + } + } + return std::make_pair(found_page, page_frame_id); +} + +//find page in lru queue +std::pair BufferManager::page_in_lru_queue(uint64_t page_id){ + bool found_page = false; + uint64_t page_frame_id = INT_MAX; + for(size_t i = 0; i < lru_queue_.size(); i++){ + auto frame_id = lru_queue_[i]; + if(pool_[frame_id]->page_id==page_id){ + found_page=true; + page_frame_id=frame_id; + pool_[page_frame_id]->count++; + lru_queue_.erase(lru_queue_.begin() + i); + lru_queue_.push_back(frame_id); + break; + } + } + return std::make_pair(found_page, page_frame_id); +} + +// read page to data +void BufferManager::read_frame(uint64_t frame_id){ + file_use_mutex_.lock(); + int16_t segment_id = BufferManager::get_segment_id(pool_[frame_id]->page_id); + uint64_t segment_page_id = BufferManager::get_segment_page_id(pool_[frame_id]->page_id); + auto file_handle = File::open_file(std::to_string(segment_id).c_str(), File::WRITE); + if(file_handle->size() >= segment_page_id * page_size + page_size){ + file_handle->read_block(segment_page_id * page_size, page_size, pool_[frame_id]->get_data()); + } + file_use_mutex_.unlock(); +} + +// write frame to disk +void BufferManager::flush(uint64_t frame_id){ + file_use_mutex_.lock(); + uint16_t segment_id = BufferManager::get_segment_id(pool_[frame_id]->page_id); + uint64_t segment_page_id = BufferManager::get_segment_page_id(pool_[frame_id]->page_id); + auto file_handle = File::open_file(std::to_string(segment_id).c_str(), File::WRITE); + file_handle->write_block(pool_[frame_id]->get_data(), segment_page_id * page_size, page_size); + file_use_mutex_.unlock(); +} + +void BufferManager::lock_frame(uint64_t frame_id, bool exclusive){ + // std::cout << "current frame count in lock: "<< pool_[frame_id]->count.load() << std::endl; + if(!exclusive){ + pool_[frame_id]->mutex_.lock_shared(); + } else{ + pool_[frame_id]->mutex_.lock(); + pool_[frame_id]->exclusive_thread_id = std::this_thread::get_id(); + } + pool_[frame_id]->exclusive = exclusive; +} + +void BufferManager::unlock_frame(uint64_t frame_id){ + + pool_[frame_id]->count--; + // std::cout << "current frame count in unlock: "<< pool_[frame_id]->count << std::endl; + if(!pool_[frame_id]->exclusive){ + pool_[frame_id]->mutex_.unlock_shared(); + } else{ + pool_[frame_id]->mutex_.unlock(); + pool_[frame_id]->exclusive = false; + } +} + +BufferFrame& BufferManager::fix_page(uint64_t page_id, bool exclusive) { + fifo_mutex_.lock(); + lru_mutex_.lock(); + std::cout << "page_id " << page_id<< " this_thread_id before lock: " << std::this_thread::get_id() << std::endl; + // find in lru queue + std::pair lru_result = page_in_lru_queue(page_id); + if(lru_result.first){ + // std::cout << "lru result count after unlock: " << pool_[lru_result.second]->count << std::endl; + fifo_mutex_.unlock(); + lru_mutex_.unlock(); + lock_frame(lru_result.second, exclusive); + return *pool_[lru_result.second]; + } + // find in fifo queue + std::pair fifo_result = page_in_fifo_queue(page_id); + if(fifo_result.first){ + // std::cout << "fifo after unlock: " << pool_[fifo_result.second]->count << std::endl; + fifo_mutex_.unlock(); + lru_mutex_.unlock(); + lock_frame(fifo_result.second, exclusive); + return *pool_[fifo_result.second]; + } + + // find a free slot + uint64_t page_frame_id = 0; + bool found_page = false; + // buffer is full + if(buffer_size == page_count){ + // evict unfixed page from fifo queue + for(size_t i = 0; i < fifo_queue_.size(); i++){ + auto frame_id = fifo_queue_[i]; + // std::cout << "current frame count fifo: "<< pool_[frame_id]->count << std::endl; + if(pool_[frame_id]->count == 0){ + found_page=true; + page_frame_id=frame_id; + if(pool_[frame_id]->dirty){ + flush(frame_id); + } + fifo_queue_.erase(fifo_queue_.begin() + i); + break; + } + } + if(!found_page){ + // evict unfixed page from lru queue + for(size_t i = 0; i < lru_queue_.size(); i++){ + auto frame_id = lru_queue_[i]; + // std::cout << "current frame count lru: "<< pool_[frame_id]->count << std::endl; + if(pool_[frame_id]->count == 0){ + found_page=true; + page_frame_id=frame_id; + if(pool_[frame_id]->dirty){ + flush(frame_id); + } + lru_queue_.erase(lru_queue_.begin() + i); + break; + } + } + } + if(!found_page){ + fifo_mutex_.unlock(); + lru_mutex_.unlock(); + throw buffer_full_error{}; + } + } else{ + page_frame_id = buffer_size; + buffer_size.fetch_add(1); + } + + lock_frame(page_frame_id, true); + pool_[page_frame_id]->page_id = page_id; + pool_[page_frame_id]->frame_id = page_frame_id; + pool_[page_frame_id]->dirty = false; + pool_[page_frame_id]->count = 2; + fifo_queue_.push_back(page_frame_id); + fifo_mutex_.unlock(); + lru_mutex_.unlock(); + // std::cout << "this_thread_id after unlock: " << std::this_thread::get_id() << std::endl; + read_frame(page_frame_id); + // std::cout << "this_thread_id after unlock: " << std::this_thread::get_id() << std::endl; + unlock_frame(page_frame_id); + // std::cout << "this_thread_id after unlock: " << std::this_thread::get_id() << std::endl; + lock_frame(page_frame_id, exclusive); + // std::cout << "this_thread_id after unlock: " << std::this_thread::get_id() << std::endl; + + return *pool_[page_frame_id]; +} + + + +void BufferManager::unfix_page(BufferFrame& page, bool is_dirty) { + if(!page.dirty){ + page.dirty = is_dirty; + } + for(uint64_t i = 0; i < pool_.size(); i++){ + if(pool_[i].get() == &page){ + unlock_frame(i); + } + } +} + + +std::vector BufferManager::get_fifo_list() const { + std::vector ans(fifo_queue_.size()); + for(size_t i = 0; i < fifo_queue_.size(); i++){ + ans[i] = pool_[fifo_queue_[i]]->page_id; + } + return ans; +} + + +std::vector BufferManager::get_lru_list() const { + std::vector ans(lru_queue_.size()); + for(size_t i = 0; i < lru_queue_.size(); i++){ + ans[i] = pool_[lru_queue_[i]]->page_id; + } + return ans; +} + +} // namespace buzzdb diff --git a/src/include/buffer/buffer_manager.h b/src/include/buffer/buffer_manager.h new file mode 100644 index 0000000..be5db28 --- /dev/null +++ b/src/include/buffer/buffer_manager.h @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include "common/macros.h" +#include +#include +#include +#include +#include +namespace buzzdb { + +class BufferFrame { +private: + friend class BufferManager; + +public: + BufferFrame(size_t page_size); + BufferFrame(){}; + uint64_t page_id; + uint64_t frame_id; + std::vector data; + bool dirty; + bool exclusive; + int count; + mutable std::shared_mutex mutex_; + std::thread::id exclusive_thread_id; + /// Returns a pointer to this page's data. + char* get_data(); + BufferFrame(const BufferFrame& other) + : page_id(other.page_id), + frame_id(other.frame_id), + data(other.data), + dirty(other.dirty), + exclusive(other.exclusive) {} + BufferFrame& operator=(BufferFrame other) { + std::swap(this->page_id, other.page_id); + std::swap(this->frame_id, other.frame_id); + std::swap(this->data, other.data); + std::swap(this->dirty, other.dirty); + std::swap(this->exclusive, other.exclusive); + return *this; + } +}; + + +class buffer_full_error +: public std::exception { +public: + const char* what() const noexcept override { + return "buffer is full"; + } +}; + + +class BufferManager { +private: + + std::vector> pool_; + std::atomic buffer_size{0}; + size_t page_size; + size_t page_count; + mutable std::mutex lru_mutex_; + mutable std::mutex fifo_mutex_; + mutable std::mutex file_use_mutex_; + std::vector> lock_table_; + std::vector>> use_counters_; + +public: + /// Constructor. + std::vector fifo_queue_; + std::vector lru_queue_; + /// @param[in] page_size Size in bytes that all pages will have. + /// @param[in] page_count Maximum number of pages that should reside in + // memory at the same time. + BufferManager(size_t page_size, size_t page_count); + + /// Destructor. Writes all dirty pages to disk. + ~BufferManager(); + + /// Returns a reference to a `BufferFrame` object for a given page id. When + /// the page is not loaded into memory, it is read from disk. Otherwise the + /// loaded page is used. + /// When the page cannot be loaded because the buffer is full, throws the + /// exception `buffer_full_error`. + /// Is thread-safe w.r.t. other concurrent calls to `fix_page()` and + /// `unfix_page()`. + /// @param[in] page_id Page id of the page that should be loaded. + /// @param[in] exclusive If `exclusive` is true, the page is locked + /// exclusively. Otherwise it is locked + /// non-exclusively (shared). + BufferFrame& fix_page(uint64_t page_id, bool exclusive); + + /// Takes a `BufferFrame` reference that was returned by an earlier call to + /// `fix_page()` and unfixes it. When `is_dirty` is / true, the page is + /// written back to disk eventually. + void unfix_page(BufferFrame& page, bool is_dirty); + + /// Returns the page ids of all pages (fixed and unfixed) that are in the + /// FIFO list in FIFO order. + /// Is not thread-safe. + std::vector get_fifo_list() const; + + /// Returns the page ids of all pages (fixed and unfixed) that are in the + /// LRU list in LRU order. + /// Is not thread-safe. + std::vector get_lru_list() const; + + /// Returns the segment id for a given page id which is contained in the 16 + /// most significant bits of the page id. + static constexpr uint16_t get_segment_id(uint64_t page_id) { + return page_id >> 48; + } + + /// Returns the page id within its segment for a given page id. This + /// corresponds to the 48 least significant bits of the page id. + static constexpr uint64_t get_segment_page_id(uint64_t page_id) { + return page_id & ((1ull << 48) - 1); + } + + std::pair page_in_lru_queue(uint64_t page_id); + std::pair page_in_fifo_queue(uint64_t page_id); + void lock_frame(uint64_t frame_id, bool exclusive); + void unlock_frame(uint64_t frame_id); + void read_frame(uint64_t frame_id); + void flush(uint64_t frame_id); +}; + + +} // namespace moderndbs \ No newline at end of file diff --git a/src/include/common/defer.h b/src/include/common/defer.h new file mode 100644 index 0000000..26f8600 --- /dev/null +++ b/src/include/common/defer.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace buzzdb { + +struct Defer { + /// The deferred function. + std::function fn; + + /// Constructor. + explicit Defer(std::function fn) : fn(std::move(fn)) {} + + /// Destructor. + /// Calls the deferred function. + ~Defer() { fn(); } + + /// Runs the deferred funciton. + void run() { + fn(); + fn = []() {}; + } +}; + +} // namespace buzzdb diff --git a/src/include/common/error.h b/src/include/common/error.h new file mode 100644 index 0000000..7711ad1 --- /dev/null +++ b/src/include/common/error.h @@ -0,0 +1,152 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace buzzdb { + +enum class ExceptionType { + INVALID_EXCEPTION = 0, // invalid type + + NOT_IMPLEMENTED_EXCEPTION = 1, + SCHEMA_PARSING_EXCEPTION = 2, + +}; + +class Exception : public std::runtime_error { + public: + Exception(ExceptionType exception_type) + : std::runtime_error(""), type(exception_type) { + exception_message_ = + "Exception Type :: " + ExceptionTypeToString(exception_type); + } + + Exception(std::string message) + : std::runtime_error(message), type(ExceptionType::INVALID_EXCEPTION) { + exception_message_ = "Message :: " + message; + } + + Exception(ExceptionType exception_type, std::string message) + : std::runtime_error(message), type(exception_type) { + exception_message_ = + "Exception Type :: " + ExceptionTypeToString(exception_type) + + "\nMessage :: " + message; + } + + std::string GetMessage() { return exception_message_; } + + std::string ExceptionTypeToString(ExceptionType type) { + switch (type) { + case ExceptionType::INVALID_EXCEPTION: + return "Invalid"; + case ExceptionType::NOT_IMPLEMENTED_EXCEPTION: + return "Not Implemented"; + case ExceptionType::SCHEMA_PARSING_EXCEPTION: + return "Schema Parsing"; + + default: + return "Unknown"; + } + } + + // Based on :: http://panthema.net/2008/0901-stacktrace-demangled/ + static void PrintStackTrace(FILE *out = ::stderr, + unsigned int max_frames = 63) { + ::fprintf(out, "Stack Trace:\n"); + + /// storage array for stack trace address data + void *addrlist[max_frames + 1]; + + /// retrieve current stack addresses + int addrlen = backtrace(addrlist, max_frames + 1); + + if (addrlen == 0) { + ::fprintf(out, " \n"); + return; + } + + /// resolve addresses into strings containing "filename(function+address)", + char **symbol_list = backtrace_symbols(addrlist, addrlen); + + /// allocate string which will be filled with the demangled function name + size_t func_name_size = 1024; + std::unique_ptr func_name(new char[func_name_size]); + + /// iterate over the returned symbol lines. skip the first, it is the + /// address of this function. + for (int i = 1; i < addrlen; i++) { + char *begin_name = 0, *begin_offset = 0, *end_offset = 0; + + /// find parentheses and +address offset surrounding the mangled name: + /// ./module(function+0x15c) [0x8048a6d] + for (char *p = symbol_list[i]; *p; ++p) { + if (*p == '(') + begin_name = p; + else if (*p == '+') + begin_offset = p; + else if (*p == ')' && begin_offset) { + end_offset = p; + break; + } + } + + if (begin_name && begin_offset && end_offset && + begin_name < begin_offset) { + *begin_name++ = '\0'; + *begin_offset++ = '\0'; + *end_offset = '\0'; + + /// mangled name is now in [begin_name, begin_offset) and caller + /// offset in [begin_offset, end_offset). now apply __cxa_demangle(): + int status; + char *ret = abi::__cxa_demangle(begin_name, func_name.get(), + &func_name_size, &status); + if (status == 0) { + func_name.reset(ret); // use possibly realloc()-ed string + ::fprintf(out, " %s : %s+%s\n", symbol_list[i], func_name.get(), + begin_offset); + } else { + /// demangling failed. Output function name as a C function with + /// no arguments. + ::fprintf(out, " %s : %s()+%s\n", symbol_list[i], begin_name, + begin_offset); + } + } else { + /// couldn't parse the line ? print the whole line. + ::fprintf(out, " %s\n", symbol_list[i]); + } + } + } + + friend std::ostream &operator<<(std::ostream &os, const Exception &e); + + private: + // type + ExceptionType type; + std::string exception_message_; +}; + +//===--------------------------------------------------------------------===// +// Derived classes +//===--------------------------------------------------------------------===// + +class NotImplementedException : public Exception { + public: + NotImplementedException() + : Exception(ExceptionType::NOT_IMPLEMENTED_EXCEPTION) {} +}; + +class SchemaParseException : public Exception { + SchemaParseException() = delete; + + public: + SchemaParseException(std::string msg) + : Exception(ExceptionType::SCHEMA_PARSING_EXCEPTION, msg) {} +}; + +} // namespace buzzdb diff --git a/src/include/common/macros.h b/src/include/common/macros.h new file mode 100644 index 0000000..73840ea --- /dev/null +++ b/src/include/common/macros.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace buzzdb { + +#define UNUSED_ATTRIBUTE __attribute__((unused)) + +constexpr uint64_t INVALID_PAGE_ID = std::numeric_limits::max(); + +constexpr uint64_t INVALID_FRAME_ID = std::numeric_limits::max(); + +constexpr uint64_t INVALID_NODE_ID = std::numeric_limits::max(); + +constexpr size_t REGISTER_SIZE = 16 + 1; // null delimiter + +} // namespace buzzdb diff --git a/src/include/storage/file.h b/src/include/storage/file.h new file mode 100644 index 0000000..08a5289 --- /dev/null +++ b/src/include/storage/file.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +namespace buzzdb { + +/// +/// Block-wise file API for C++. +/// +class File { + public: + /// File mode (read or write) + enum Mode { READ, WRITE }; + + virtual ~File() = default; + + /// Returns the `Mode` this file was opened with. + virtual Mode get_mode() const = 0; + + /// Returns the current size of the file in bytes. + /// Is not thread-safe w.r.t concurrent calls to `resize()`. + virtual size_t size() const = 0; + + /// Resizes the file to `new_size`. If `new_size` is smaller than `size()`, + /// the file is cut off at the end. Otherwise zero bytes are appended at + /// the end. + /// Is not thread-safe. + virtual void resize(size_t new_size) = 0; + + /// Reads a block of the file. `offset + size` must not be larger than + /// `size()`. + /// Is thread-safe w.r.t concurrent calls to `read_block()` and + /// `write_block()`. + /// @param[in] offset The offset in the file from which the block should + /// be read. + /// @param[in] size The size of the block. + /// @param[out] block A pointer to memory where the block is written to. + /// Must be able to hold at least `size` bytes. + virtual void read_block(size_t offset, size_t size, char* block) = 0; + + /// Reads a block of the file and returns it. + std::unique_ptr read_block(size_t offset, size_t size) { + auto block = std::make_unique(size); + read_block(offset, size, block.get()); + return block; + } + + /// Writes a block to the file. `offset + size` must not be larger than + /// `size()`. If you want to write past the end of the file, use + /// `resize()` first. + /// This function must not be used when the file was opened in `READ` mode. + /// Is thread-safe w.r.t concurrent calls to `read_block()` and + /// `write_block()`. + /// @param[in] block A pointer to memory that will be written to the + /// file. Must hold at least `size` bytes. + /// @param[in] offset The offset in the file at which the block should be + /// written. + /// @param[in] size The size of the block. + virtual void write_block(const char* block, size_t offset, size_t size) = 0; + + /// Opens a file with the given mode. Existing files are never overwritten. + /// @param[in] filename Path to the file. + /// @param[in] mode `Mode` that should be used to open the file. + static std::unique_ptr open_file(const char* filename, Mode mode); + + /// Opens a temporary file in `WRITE` mode. The file will be deleted + /// automatically after use. + static std::unique_ptr make_temporary_file(); +}; + +} // namespace buzzdb diff --git a/src/include/storage/test_file.h b/src/include/storage/test_file.h new file mode 100644 index 0000000..56ced62 --- /dev/null +++ b/src/include/storage/test_file.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "storage/file.h" + +namespace buzzdb { + +class TestFile : public File { + private: + Mode mode; + std::vector file_content; + + public: + explicit TestFile(Mode mode = WRITE) : mode(mode) {} + + explicit TestFile(std::vector&& file_content, Mode mode = READ) + : mode(mode), file_content(std::move(file_content)) {} + + TestFile(const TestFile&) = default; + TestFile(TestFile&&) = default; + + ~TestFile() override = default; + + TestFile& operator=(const TestFile&) = default; + TestFile& operator=(TestFile&&) = default; + + std::vector& get_content() { return file_content; } + + Mode get_mode() const override; + + size_t size() const override; + + void resize(size_t new_size) override; + + void read_block(size_t offset, size_t size, char* block); + + void write_block(const char* block, size_t offset, size_t size); +}; + +} // namespace buzzdb diff --git a/src/storage/posix_file.cc b/src/storage/posix_file.cc new file mode 100644 index 0000000..3368a24 --- /dev/null +++ b/src/storage/posix_file.cc @@ -0,0 +1,130 @@ + +#include +#include // NOLINT +#include +#include +#include +#include +#include +#include + +#include "storage/file.h" + +namespace buzzdb { + +namespace { + +[[noreturn]] void throw_errno() { + throw std::system_error{errno, std::system_category()}; +} + +} // namespace + +class PosixFile : public File { + private: + Mode mode; + int fd; + size_t cached_size; + + size_t read_size() { + struct ::stat file_stat; + if (::fstat(fd, &file_stat) < 0) { + throw_errno(); + } + return file_stat.st_size; + } + + public: + PosixFile(Mode mode, int fd, size_t size) + : mode(mode), fd(fd), cached_size(size) {} + + PosixFile(const char* filename, Mode mode) : mode(mode) { + switch (mode) { + case READ: + fd = ::open(filename, O_RDONLY | O_SYNC); + break; + case WRITE: + fd = ::open(filename, O_RDWR | O_CREAT | O_SYNC, 0666); + } + if (fd < 0) { + throw_errno(); + } + cached_size = read_size(); + } + + ~PosixFile() override { + // Don't check return value here, as we don't want a throwing + // destructor. Also, even when close() fails, the fd will always be + // freed (see man 2 close). + ::close(fd); + } + + Mode get_mode() const override { return mode; } + + size_t size() const override { return cached_size; } + + void resize(size_t new_size) override { + if (new_size == cached_size) { + return; + } + if (::ftruncate(fd, new_size) < 0) { + throw_errno(); + } + cached_size = new_size; + } + + void read_block(size_t offset, size_t size, char* block) override { + size_t total_bytes_read = 0; + while (total_bytes_read < size) { + ssize_t bytes_read = + ::pread(fd, block + total_bytes_read, size - total_bytes_read, + offset + total_bytes_read); + if (bytes_read == 0) { + // end of file, i.e. size was probably larger than the file + // size + return; + } + if (bytes_read < 0) { + throw_errno(); + } + total_bytes_read += static_cast(bytes_read); + } + } + + void write_block(const char* block, size_t offset, size_t size) override { + size_t total_bytes_written = 0; + while (total_bytes_written < size) { + ssize_t bytes_written = + ::pwrite(fd, block + total_bytes_written, size - total_bytes_written, + offset + total_bytes_written); + if (bytes_written == 0) { + // This should probably never happen. Return here to prevent + // an infinite loop. + return; + } + if (bytes_written < 0) { + throw_errno(); + } + total_bytes_written += static_cast(bytes_written); + } + } +}; + +std::unique_ptr File::open_file(const char* filename, Mode mode) { + return std::make_unique(filename, mode); +} + +std::unique_ptr File::make_temporary_file() { + char file_template[] = ".tmpfile-XXXXXX"; + int fd = ::mkstemp(file_template); + if (fd < 0) { + throw_errno(); + } + if (::unlink(file_template) < 0) { + ::close(fd); + throw_errno(); + } + return std::make_unique(File::WRITE, fd, 0); +} + +} // namespace buzzdb diff --git a/src/storage/test_file.cc b/src/storage/test_file.cc new file mode 100644 index 0000000..90cb593 --- /dev/null +++ b/src/storage/test_file.cc @@ -0,0 +1,56 @@ + +#include +#include // NOLINT +#include +#include +#include +#include +#include +#include +#include + +#include "storage/test_file.h" + +namespace buzzdb { + +class TestFileError : public std::exception { + private: + const char* message; + + public: + explicit TestFileError(const char* message) : message(message) {} + + ~TestFileError() override = default; + + const char* what() const noexcept override { return message; } +}; + +File::Mode TestFile::get_mode() const { return mode; } + +size_t TestFile::size() const { return file_content.size(); } + +void TestFile::resize(size_t new_size) { + if (mode == READ) { + throw TestFileError{"trying to resize a read only file"}; + } + file_content.resize(new_size); +} + +void TestFile::read_block(size_t offset, size_t size, char* block) { + if (offset + size > file_content.size()) { + throw TestFileError{"trying to read past end of file"}; + } + std::memcpy(block, file_content.data() + offset, size); +} + +void TestFile::write_block(const char* block, size_t offset, size_t size) { + if (mode == READ) { + throw TestFileError{"trying to write to a read only file"}; + } + if (offset + size > file_content.size()) { + throw TestFileError{"trying to write past end of file"}; + } + std::memcpy(file_content.data() + offset, block, size); +} + +} // namespace buzzdb diff --git a/test/unit/buffer/buffer_manager_test.cc b/test/unit/buffer/buffer_manager_test.cc new file mode 100644 index 0000000..ed3aedb --- /dev/null +++ b/test/unit/buffer/buffer_manager_test.cc @@ -0,0 +1,110 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "buffer/buffer_manager.h" + +namespace { + +TEST(BufferManagerTest, FixSingle) { + buzzdb::BufferManager buffer_manager{1024, 10}; + std::vector expected_values(1024 / sizeof(uint64_t), 123); + { + auto& page = buffer_manager.fix_page(1, true); + std::memcpy(page.get_data(), expected_values.data(), 1024); + buffer_manager.unfix_page(page, true); + EXPECT_EQ(std::vector{1}, buffer_manager.get_fifo_list()); + EXPECT_TRUE(buffer_manager.get_lru_list().empty()); + } + { + std::vector values(1024 / sizeof(uint64_t)); + auto& page = buffer_manager.fix_page(1, false); + std::memcpy(values.data(), page.get_data(), 1024); + buffer_manager.unfix_page(page, true); + EXPECT_TRUE(buffer_manager.get_fifo_list().empty()); + EXPECT_EQ(std::vector{1}, buffer_manager.get_lru_list()); + ASSERT_EQ(expected_values, values); + } +} + +TEST(BufferManagerTest, FixMultiple) { + buzzdb::BufferManager buffer_manager{1024, 10}; + std::vector expected_values(1024 / sizeof(uint64_t), 123); + { + for (uint64_t i = 0; i < 11; ++i) { + auto& page = buffer_manager.fix_page(i, false); + } + EXPECT_EQ(10, buffer_manager.get_fifo_list()[9]); + } +} +TEST(BufferManagerTest, PersistentRestart) { + auto buffer_manager = std::make_unique(1024, 10); + for (uint16_t segment = 0; segment < 3; ++segment) { + for (uint64_t segment_page = 0; segment_page < 10; ++segment_page) { + uint64_t page_id = (static_cast(segment) << 48) | segment_page; + auto& page = buffer_manager->fix_page(page_id, true); + uint64_t& value = *reinterpret_cast(page.get_data()); + value = segment * 10 + segment_page; + buffer_manager->unfix_page(page, true); + + } + } + // Destroy the buffer manager and create a new one. + buffer_manager = std::make_unique(1024, 10); + for (uint16_t segment = 0; segment < 3; ++segment) { + for (uint64_t segment_page = 0; segment_page < 10; ++segment_page) { + uint64_t page_id = (static_cast(segment) << 48) | segment_page; + auto& page = buffer_manager->fix_page(page_id, false); + uint64_t value = *reinterpret_cast(page.get_data()); + buffer_manager->unfix_page(page, false); + EXPECT_EQ(segment * 10 + segment_page, value); + } + } +} + +TEST(BufferManagerTest, FIFOEvict) { + buzzdb::BufferManager buffer_manager{1024, 10}; + for (uint64_t i = 1; i < 11; ++i) { + auto& page = buffer_manager.fix_page(i, false); + buffer_manager.unfix_page(page, false); + } + { + std::vector expected_fifo{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + EXPECT_EQ(expected_fifo, buffer_manager.get_fifo_list()); + EXPECT_TRUE(buffer_manager.get_lru_list().empty()); + } + { + auto& page = buffer_manager.fix_page(11, false); + buffer_manager.unfix_page(page, false); + } + { + std::vector expected_fifo{2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + EXPECT_EQ(expected_fifo, buffer_manager.get_fifo_list()); + EXPECT_TRUE(buffer_manager.get_lru_list().empty()); + } +} + +TEST(BufferManagerTest, BufferFull) { + buzzdb::BufferManager buffer_manager{1024, 10}; + std::vector pages; + pages.reserve(10); + for (uint64_t i = 1; i < 11; ++i) { + auto& page = buffer_manager.fix_page(i, false); + pages.push_back(&page); + } + EXPECT_THROW(buffer_manager.fix_page(11, false), buzzdb::buffer_full_error); + for (auto* page : pages) { + buffer_manager.unfix_page(*page, false); + } +} +} // namespace + +int main(int argc, char* argv[]) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}