diff --git a/include/daemon/backend/data/pmdk/pool.hpp b/include/daemon/backend/data/pmdk/pool.hpp new file mode 100644 index 0000000000000000000000000000000000000000..96a256cc062db84a97cf96d8377ec722d716dfd2 --- /dev/null +++ b/include/daemon/backend/data/pmdk/pool.hpp @@ -0,0 +1,139 @@ +/* + Copyright 2018-2020, Barcelona Supercomputing Center (BSC), Spain + Copyright 2015-2020, Johannes Gutenberg Universitaet Mainz, Germany + + This software was partially supported by the + EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). + + This software was partially supported by the + ADA-FS project under the SPPEXA project funded by the DFG. + + SPDX-License-Identifier: MIT +*/ + +#ifndef GEKKOFS_DAEMON_DATA_PMDK_POOL_HPP +#define GEKKOFS_DAEMON_DATA_PMDK_POOL_HPP + +#include +#include + +namespace fs = boost::filesystem; + +namespace pmdk { + +/** + * This class implements a RAII-enabled non-volatile memory pool based on + * Intel's PMDK C library. + */ +class pool { + +public: + /** + * Create a pool of non-volatile memory of a fixed `size` under + * `parent_dir`. Give it a random name. + * + * @param[in] parent_dir the parent directory where the memory pool backing + * file will be stored + * @param[in] size the maximum capacity in bytes of the pool + * @returns the newly created pool + */ + pool(const fs::path& parent_dir, std::size_t size) noexcept; + + pool(pool&& rhs) = default; + pool(const pool& other) = delete; + pool& operator=(pool&& rhs) = default; + pool& operator=(const pool& other) = delete; + + /** + * Destroy an existing pool. + */ + ~pool(); + + /** + * Returns the path to a pool's data storage in the file system. + * + * @returns If the pool was correctly created, the function returns a + * `boost::fylesystem::path` with the path to the file backing the + * pool in the file system. Otherwise, `boost::filesystem::path{}` + * is returned. + */ + fs::path path() const noexcept; + + /** + * Returns the address to a pool's data region in non-volatile memory. + * + * @returns If the pool was correctly created, the function returns a + * void pointer to the non-volatile memory region containing the + * pool's data. Otherwise, `nullptr` is returned. + */ + void* data() const noexcept; + + /** + * Returns the size of a pool's data region in non-volatile memory. + * + * @returns If the pool was correctly created, the function returns the + * size of the non-volatile memory region containing + * the pool's data. Otherwise, `0` is returned. + */ + std::size_t size() const noexcept; + + /** + * Returns whether a pool's data is actually stored in non-volatile + * memory. + * + * @returns The function returns `true` if the pool was correctly created, + * and the pool's data is stored in NVM. Otherwise, `false` is + * returned. + */ + bool is_persistent() const noexcept; + + /** + * + * Check whether a pool is valid. + * + * @returns `true` if the pool was correctly created. Otherwise it + * returns `false`. + */ + explicit operator bool() const noexcept; + + /** + * + * Check whether a pool is invalid. + * + * @returns `true` if the pool was not correctly created. Otherwise it + * returns `false`. + */ + bool operator!() const noexcept; + + /** + * + * Check whether a pool is valid. + * + * @returns `true` if the pool was correctly created. Otherwise it + * returns `false`. + */ + bool valid() const noexcept; + +private: + /// Logger instance + std::shared_ptr log_; + + /// Path to the pool file in the file system + fs::path path_; + + /// Address of the pool's non-volatile memory region + void* data_ = nullptr; + + /// Size of the pool's non-volatile memory region + std::size_t size_ = 0; + + /// Flag set if the pool file is actually stored in non-volatile memory + bool is_persistent_ = false; + + /// Flag set if the pool was created correctly + bool is_valid_ = false; +}; + +} // namespace pmdk + +#endif // GEKKOFS_DAEMON_DATA_PMDK_POOL_HPP diff --git a/src/daemon/backend/data/CMakeLists.txt b/src/daemon/backend/data/CMakeLists.txt index 9535714be84359270aadad00d4b833bf89380c5d..033bb4eb45992d0a22cec7f15fea54bafe827736 100644 --- a/src/daemon/backend/data/CMakeLists.txt +++ b/src/daemon/backend/data/CMakeLists.txt @@ -1,3 +1,6 @@ +# PMDK backend +add_subdirectory(pmdk) + add_library(storage STATIC) target_sources(storage @@ -16,9 +19,11 @@ target_link_libraries(storage PRIVATE spdlog Boost::filesystem + pmdk_storage -ldl ) #target_include_directories(storage # PRIVATE # ) + diff --git a/src/daemon/backend/data/pmdk/CMakeLists.txt b/src/daemon/backend/data/pmdk/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..657ee98b37cf6c22f1540c256eab160e55c9e457 --- /dev/null +++ b/src/daemon/backend/data/pmdk/CMakeLists.txt @@ -0,0 +1,29 @@ +## +# Copyright 2018-2020, Barcelona Supercomputing Center (BSC), Spain +# Copyright 2015-2020, Johannes Gutenberg Universitaet Mainz, Germany +# +# This software was partially supported by the +# EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). +# +# This software was partially supported by the +# ADA-FS project under the SPPEXA project funded by the DFG. +# +# SPDX-License-Identifier: MIT +## + +add_library(pmdk_storage + STATIC +) + +target_sources(pmdk_storage + PRIVATE + pool.cpp + PUBLIC + ${INCLUDE_DIR}/daemon/backend/data/pmdk/pool.hpp +) + +target_link_libraries(pmdk_storage + PUBLIC + PMDK::pmem + spdlog +) diff --git a/src/daemon/backend/data/pmdk/pool.cpp b/src/daemon/backend/data/pmdk/pool.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d78236c0c0021d05fe3ea2f6bd26022f2b5918da --- /dev/null +++ b/src/daemon/backend/data/pmdk/pool.cpp @@ -0,0 +1,217 @@ +/* + Copyright 2018-2020, Barcelona Supercomputing Center (BSC), Spain + Copyright 2015-2020, Johannes Gutenberg Universitaet Mainz, Germany + + This software was partially supported by the + EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). + + This software was partially supported by the + ADA-FS project under the SPPEXA project funded by the DFG. + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include +#include +#include + +#include +#include + +namespace fs = boost::filesystem; + +namespace { + +/** + * Generate a random filename for a non-volatile memory pool using the Mersenne + * Twister PRNG. Note that the function does not verify whether the path + * returned actually exists or can be created. Neither does it verifies that + * `subdir` exists or that the user actually has permissions to create + * + * @param[in] subdir an optional parent directory to prepend to the generated + * path (Default: "") + * @returns a randomly generated path + */ +fs::path +generate_pool_path(const fs::path& subdir = "") { + + static boost::mt19937 rng; + using RngType = decltype(rng); + + const auto uuid = boost::uuids::basic_random_generator(rng)(); + + return subdir / boost::uuids::to_string(uuid); +} + +} // namespace + +namespace pmdk { + +/** + * Create a pool of non-volatile memory of a fixed `size` under + * `parent_dir`. Give it a random name. + * + * @param[in] parent_dir the parent directory where the memory pool backing + * file will be stored + * @param[in] size the maximum capacity in bytes of the pool + * @returns the newly created pool + * + * @warning The pool construction may fail for a number of reasons, but + * the constructor will not throw (though the error itself will be logged). + * Boolean operator overloads are provided so that it is simple to check + * if a pool has been correctly constructed: + * + * .. code-block:: cpp + * + * pool p{"/foo/bar", 42000}; + * + * if(!p) { + * abort("pool is invalid!") + * } + */ +pool::pool(const fs::path& parent_dir, std::size_t size) noexcept { + + // get logger instance and set it for data module and chunk storage + auto log_ = spdlog::get(GKFS_DATA_MOD->LOGGER_NAME); + assert(log_); + + if(!fs::exists(parent_dir)) { + log_->error("Error creating PMDK pool: parent directory {} " + "does not exist", parent_dir.string()); + return; + } + + if(size <= 0) { + log_->error("Error creating PMDK pool: invalid pool size {}", size); + return; + } + + fs::path pool_path = ::generate_pool_path(parent_dir); + void* pool_address = nullptr; + std::size_t pool_length = 0; + int is_pmem = 0; + + // if the pool already exists in the host file system, it might be + // a stale file or we might have a collision. Either way, abort the + // pool creation. + if(fs::exists(pool_path)) { + log_->warn("PMDK pool file {} already exists (possible collision or " + "stale file)", pool_path.string()); + return; + + } + + pool_address = ::pmem_map_file(pool_path.c_str(), size, + PMEM_FILE_CREATE | PMEM_FILE_EXCL | PMEM_FILE_SPARSE, + 0666, &pool_length, &is_pmem); + + if(pool_address == nullptr) { + log_->critical("Error creating PMDK pool file {}: {}", + pool_path.string(), ::strerror(errno)); + return; + } + + path_ = pool_path; + data_ = pool_address; + size_ = pool_length; + is_persistent_ = is_pmem; + is_valid_ = true; +} + +/** + * Destroy an existing pool. + */ +pool::~pool() { + if(data_ != nullptr) { + ::pmem_unmap(data_, size_); + } +} + +/** + * Returns the path to a pool's data storage in the file system. + * + * @returns If the pool was correctly created, the function returns a + * `boost::fylesystem::path` with the path to the file backing the + * pool in the file system. Otherwise, `boost::filesystem::path{}` + * is returned. + */ +fs::path +pool::path() const noexcept { + return path_; +} + +/** + * Returns the address to a pool's data region in non-volatile memory. + * + * @returns If the pool was correctly created, the function returns a + * void pointer to the non-volatile memory region containing the + * pool's data. Otherwise, `nullptr` is returned. + */ +void* +pool::data() const noexcept { + return data_; +} + +/** + * Returns the size of a pool's data region in non-volatile memory. + * + * @returns If the pool was correctly created, the function returns the + * size of the non-volatile memory region containing + * the pool's data. Otherwise, `0` is returned. + */ +std::size_t +pool::size() const noexcept { + return size_; +} + +/** + * Returns whether a pool's data is actually stored in non-volatile + * memory. + * + * @returns The function returns `true` if the pool was correctly created, + * and the pool's data is stored in NVM. Otherwise, `false` is + * returned. + */ +bool +pool::is_persistent() const noexcept { + return is_persistent_; +} + +/** + * + * Check whether a pool is valid. + * + * @returns `true` if the pool was correctly created. Otherwise it + * returns `false`. + */ +pool::operator bool() const noexcept { + return valid(); +} + +/** + * + * Check whether a pool is invalid. + * + * @returns `true` if the pool was not correctly created. Otherwise it + * returns `false`. + */ +bool +pool::operator!() const noexcept { + return !valid(); +} + +/** + * + * Check whether a pool is valid. + * + * @returns `true` if the pool was correctly created. Otherwise it + * returns `false`. + */ +bool +pool::valid() const noexcept { + return is_valid_; +} + +} // namespace pmdk diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 08a6f139e615e4d63da65b4f6cc21ceb82aeba13..226876d7190f7a8b41fdb66ea95a439bc4bd49ef 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -34,11 +34,14 @@ target_link_libraries(catch2_main add_executable(tests test_example_00.cpp test_example_01.cpp + test_storage_pmdk.cpp ) target_link_libraries(tests catch2_main fmt::fmt + pmdk_storage + Boost::filesystem ) # Catch2's contrib folder includes some helper functions diff --git a/tests/unit/test_storage_pmdk.cpp b/tests/unit/test_storage_pmdk.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d1e13eececa4b7c3dba15e170d184b27eb742290 --- /dev/null +++ b/tests/unit/test_storage_pmdk.cpp @@ -0,0 +1,175 @@ +/* + Copyright 2018-2020, Barcelona Supercomputing Center (BSC), Spain + Copyright 2015-2020, Johannes Gutenberg Universitaet Mainz, Germany + + This software was partially supported by the + EC H2020 funded project NEXTGenIO (Project ID: 671951, www.nextgenio.eu). + + This software was partially supported by the + ADA-FS project under the SPPEXA project funded by the DFG. + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include +#include + +namespace fs = boost::filesystem; + + +// FIXME: temporary solution to have correct loggers for tests +#include + +#include +#include + + +namespace { + +void +patch_gkfs_loggers(const std::vector& loggers_name, + spdlog::level::level_enum level, const std::string& path) { + + /* Create common sink */ + auto file_sink = std::make_shared(path); + + /* Create and configure loggers */ + auto loggers = std::list>(); + for (const auto& name: loggers_name) { + + if(spdlog::get(name)) { + continue; + } + + auto logger = std::make_shared(name, file_sink); + logger->flush_on(spdlog::level::trace); + loggers.push_back(logger); + } + + /* register loggers */ + for (const auto& logger: loggers) { + spdlog::register_logger(logger); + } + + // set logger format + spdlog::set_pattern("[%C-%m-%d %H:%M:%S.%f] %P [%L][%n] %v"); + + spdlog::set_level(level); +} + +} // namespace + +// end of temporary solution + +namespace { + +fs::path +create_temporary_directory() { + auto tmpdir = fs::temp_directory_path() / fs::unique_path(); + + boost::system::error_code ec; + + if(!fs::create_directory(tmpdir, ec)) { + throw std::runtime_error(ec.message()); + } + + return tmpdir; +} + +} // namespace + + +SCENARIO( "PMDK pools can be created", "[pmdk_storage_pool]" ) { + + + auto logger_names = std::vector{ + "main", + "MetadataDB", + "DataModule", + }; + + ::patch_gkfs_loggers(logger_names, spdlog::level::debug, "/dev/stdout"); + + GIVEN( "A valid parent directory " ) { + + WHEN( "Size is a positive integer" ) { + + std::size_t pool_size = 420; + auto tmpdir = create_temporary_directory(); + + pmdk::pool p(tmpdir, pool_size); + + THEN( "The pool object is valid " ) { + REQUIRE(p); + REQUIRE(p.valid()); + REQUIRE(p.size() == pool_size); + REQUIRE(p.data() != nullptr); + } + + AND_THEN( "The mapping file exists and has the correct size" ) { + REQUIRE(fs::exists(p.path())); + REQUIRE(fs::file_size(p.path()) == pool_size); + } + } + + WHEN( "Size is zero" ) { + + auto tmpdir = create_temporary_directory(); + + pmdk::pool p(tmpdir, 0); + + THEN( "The pool object is invalid " ) { + REQUIRE(!p); + REQUIRE(!p.valid()); + + AND_THEN( "Internal members are default initialized" ) { + REQUIRE(p.path().empty()); + REQUIRE(p.data() == nullptr); + REQUIRE(p.size() == 0); + } + } + } + + WHEN( "Size is negative" ) { + + auto tmpdir = create_temporary_directory(); + + pmdk::pool p(tmpdir, -1); + + THEN( "The pool object is invalid " ) { + REQUIRE(!p); + REQUIRE(!p.valid()); + + AND_THEN( "Internal members are default initialized" ) { + REQUIRE(p.path().empty()); + REQUIRE(p.data() == nullptr); + REQUIRE(p.size() == 0); + } + } + } + } + + GIVEN( "An invalid parent directory " ) { + + WHEN( "Parent directory does not exist" ) { + + std::size_t pool_size = 420; + auto tmpdir = create_temporary_directory(); + + pmdk::pool p("/foo/bar/", pool_size); + + THEN( "The pool object is invalid " ) { + REQUIRE(!p); + REQUIRE(!p.valid()); + + AND_THEN( "Internal members are default initialized" ) { + REQUIRE(p.path().empty()); + REQUIRE(p.data() == nullptr); + REQUIRE(p.size() == 0); + } + } + } + } +}