From 51a741d076fffb2fc09ea8428bad5a84cb756037 Mon Sep 17 00:00:00 2001 From: Shinichi Umegane Date: Thu, 28 Nov 2024 19:26:10 +0900 Subject: [PATCH] Integrate wip/log-0.7 into master (#70) Key Updates: 1. Epoch Management Improvements: - Addressed race conditions in epoch handling to ensure consistent updates. - Improved handling for scenarios where epoch file writing takes longer than expected. - Enhanced logic to prevent invalid Epoch IDs caused by underflow during updates. - Modified startup behavior to treat missing Epoch files as equivalent to empty files, ensuring smoother recovery. 2. Manifest File Locking: - Added manifest file locking to prevent simultaneous database instances or conflicts caused by modifications through dblogutil. Note: - Draft documents were temporarily removed to avoid confusion in master. These documents will be finalized and included in a future merge. --- include/limestone/api/datastore.h | 13 +- include/limestone/api/log_channel.h | 2 +- src/limestone/datastore.cpp | 64 +++++-- src/limestone/datastore_format.cpp | 19 ++ src/limestone/dblog_scan.cpp | 19 +- src/limestone/dblogutil/dblogutil.cpp | 8 + src/limestone/internal.h | 7 + src/limestone/limestone_exception_helper.h | 8 +- src/limestone/log_channel.cpp | 25 ++- test/limestone/api/datastore_test.cpp | 24 ++- test/limestone/epoch/epoch_file_test.cpp | 191 +++++++++++++++++++++ test/limestone/log/rotate_test.cpp | 2 + test/limestone/utils/dblogutil_test.cpp | 30 +++- test/test_root.h | 4 +- 14 files changed, 381 insertions(+), 35 deletions(-) create mode 100644 test/limestone/epoch/epoch_file_test.cpp diff --git a/include/limestone/api/datastore.h b/include/limestone/api/datastore.h index 492292a7..1841396f 100644 --- a/include/limestone/api/datastore.h +++ b/include/limestone/api/datastore.h @@ -249,8 +249,10 @@ class datastore { protected: // for tests auto& log_channels_for_tests() const noexcept { return log_channels_; } auto epoch_id_informed_for_tests() const noexcept { return epoch_id_informed_.load(); } - auto epoch_id_recorded_for_tests() const noexcept { return epoch_id_recorded_.load(); } + auto epoch_id_recorded_for_tests() const noexcept { return epoch_id_to_be_recorded_.load(); } + auto epoch_id_switched_for_tests() const noexcept { return epoch_id_switched_.load(); } auto& files_for_tests() const noexcept { return files_; } + void rotate_epoch_file_for_tests() { rotate_epoch_file(); } private: std::vector> log_channels_; @@ -261,7 +263,8 @@ class datastore { std::atomic_uint64_t epoch_id_informed_{}; - std::atomic_uint64_t epoch_id_recorded_{}; + std::atomic_uint64_t epoch_id_to_be_recorded_{}; + std::atomic_uint64_t epoch_id_record_finished_{}; std::unique_ptr backup_{}; @@ -302,6 +305,8 @@ class datastore { std::mutex mtx_epoch_file_{}; + std::mutex mtx_epoch_persistent_callback_{}; + state state_{}; void add_file(const boost::filesystem::path& file) noexcept; @@ -327,7 +332,6 @@ class datastore { */ void create_snapshot(); - epoch_id_type last_durable_epoch_in_dir(); /** * @brief requests the data store to rotate log files @@ -342,6 +346,9 @@ class datastore { int64_t current_unix_epoch_in_millis(); std::map clear_storage; + + // File descriptor for file lock (flock) on the manifest file + int fd_for_flock_{-1}; }; } // namespace limestone::api diff --git a/include/limestone/api/log_channel.h b/include/limestone/api/log_channel.h index 492ad6d4..2a592bcf 100644 --- a/include/limestone/api/log_channel.h +++ b/include/limestone/api/log_channel.h @@ -190,7 +190,7 @@ class log_channel { std::atomic_uint64_t finished_epoch_id_{0}; - std::atomic latest_ession_epoch_id_{0}; + std::atomic latest_session_epoch_id_{0}; std::mutex session_mutex_; diff --git a/src/limestone/datastore.cpp b/src/limestone/datastore.cpp index ea68b086..21367dd0 100644 --- a/src/limestone/datastore.cpp +++ b/src/limestone/datastore.cpp @@ -68,6 +68,19 @@ datastore::datastore(configuration const& conf) : location_(conf.data_locations_ } } internal::check_and_migrate_logdir_format(location_); + + // acquire lock for manifest file + fd_for_flock_ = internal::acquire_manifest_lock(location_); + if (fd_for_flock_ == -1) { + if (errno == EWOULDBLOCK) { + std::string err_msg = "Another process is using the log directory: " + location_.string() + ". Terminate the conflicting process and restart this process."; + LOG_AND_THROW_IO_EXCEPTION(err_msg, errno); + } else { + std::string err_msg = "Failed to acquire lock for manifest in directory: " + location_.string(); + LOG_AND_THROW_IO_EXCEPTION(err_msg, errno); + } + } + add_file(compaction_catalog_path); compaction_catalog_ = std::make_unique(compaction_catalog::from_catalog_file(location_)); @@ -149,22 +162,30 @@ void datastore::switch_epoch(epoch_id_type new_epoch_id) { } epoch_id_switched_.store(neid); - update_min_epoch_id(true); + if (state_ != state::not_ready) { + update_min_epoch_id(true); + } } catch (...) { HANDLE_EXCEPTION_AND_ABORT(); } } void datastore::update_min_epoch_id(bool from_switch_epoch) { // NOLINT(readability-function-cognitive-complexity) - auto upper_limit = epoch_id_switched_.load() - 1; + auto upper_limit = epoch_id_switched_.load(); + if (upper_limit == 0) { + return; // If epoch_id_switched_ is zero, it means no epoch has been switched, so updating epoch_id_to_be_recorded_ and epoch_id_informed_ is unnecessary. + } + upper_limit--; epoch_id_type max_finished_epoch = 0; for (const auto& e : log_channels_) { - auto working_epoch = static_cast(e->current_epoch_id_.load()); - if ((working_epoch - 1) < upper_limit) { - upper_limit = working_epoch - 1; - } + auto working_epoch = e->current_epoch_id_.load(); auto finished_epoch = e->finished_epoch_id_.load(); + if (working_epoch > finished_epoch && working_epoch != UINT64_MAX) { + if ((working_epoch - 1) < upper_limit) { + upper_limit = working_epoch - 1; + } + } if (max_finished_epoch < finished_epoch) { max_finished_epoch = finished_epoch; } @@ -175,19 +196,21 @@ void datastore::update_min_epoch_id(bool from_switch_epoch) { // NOLINT(readabi if (from_switch_epoch && (to_be_epoch > static_cast(max_finished_epoch))) { to_be_epoch = static_cast(max_finished_epoch); } - auto old_epoch_id = epoch_id_recorded_.load(); + auto old_epoch_id = epoch_id_to_be_recorded_.load(); while (true) { if (old_epoch_id >= to_be_epoch) { break; } - if (epoch_id_recorded_.compare_exchange_strong(old_epoch_id, to_be_epoch)) { + if (epoch_id_to_be_recorded_.compare_exchange_strong(old_epoch_id, to_be_epoch)) { std::lock_guard lock(mtx_epoch_file_); - + if (to_be_epoch < epoch_id_to_be_recorded_.load()) { + break; + } FILE* strm = fopen(epoch_file_path_.c_str(), "a"); // NOLINT(*-owning-memory) if (!strm) { LOG_AND_THROW_IO_EXCEPTION("fopen failed", errno); } - log_entry::durable_epoch(strm, static_cast(epoch_id_recorded_.load())); + log_entry::durable_epoch(strm, static_cast(epoch_id_to_be_recorded_.load())); if (fflush(strm) != 0) { LOG_AND_THROW_IO_EXCEPTION("fflush failed", errno); } @@ -197,18 +220,29 @@ void datastore::update_min_epoch_id(bool from_switch_epoch) { // NOLINT(readabi if (fclose(strm) != 0) { // NOLINT(*-owning-memory) LOG_AND_THROW_IO_EXCEPTION("fclose failed", errno); } + epoch_id_record_finished_.store(epoch_id_to_be_recorded_.load()); break; } } + if (to_be_epoch > epoch_id_record_finished_.load()) { + return; + } // update informed_epoch_ to_be_epoch = upper_limit; + // In `informed_epoch_`, the update restriction based on the `from_switch_epoch` condition is intentionally omitted. + // Due to the interface specifications of Shirakami, it is necessary to advance the epoch even if the log channel + // is not updated. This behavior differs from `recorded_epoch_` and should be maintained as such. old_epoch_id = epoch_id_informed_.load(); while (true) { if (old_epoch_id >= to_be_epoch) { break; } if (epoch_id_informed_.compare_exchange_strong(old_epoch_id, to_be_epoch)) { + std::lock_guard lock(mtx_epoch_persistent_callback_); + if (to_be_epoch < epoch_id_informed_.load()) { + break; + } if (persistent_callback_) { persistent_callback_(to_be_epoch); } @@ -244,7 +278,15 @@ std::future datastore::shutdown() noexcept { VLOG(log_info) << "/:limestone:datastore:shutdown compaction task has been stopped."; } - return std::async(std::launch::async, []{ + if (fd_for_flock_ != -1) { + if (::close(fd_for_flock_) == -1) { + VLOG(log_error) << "Failed to close lock file descriptor: " << strerror(errno); + } else { + fd_for_flock_ = -1; + } + } + + return std::async(std::launch::async, [] { std::this_thread::sleep_for(std::chrono::microseconds(100000)); VLOG(log_info) << "/:limestone:datastore:shutdown end"; }); diff --git a/src/limestone/datastore_format.cpp b/src/limestone/datastore_format.cpp index f1d03944..f254c855 100644 --- a/src/limestone/datastore_format.cpp +++ b/src/limestone/datastore_format.cpp @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include +#include +#include #include #include @@ -170,4 +173,20 @@ void check_and_migrate_logdir_format(const boost::filesystem::path& logdir) { } } +int acquire_manifest_lock(const boost::filesystem::path& logdir) { + boost::filesystem::path manifest_path = logdir / std::string(manifest_file_name); + + int fd = ::open(manifest_path.string().c_str(), O_RDWR); // NOLINT(hicpp-vararg, cppcoreguidelines-pro-type-vararg) + if (fd == -1) { + return -1; + } + + if (::flock(fd, LOCK_EX | LOCK_NB) == -1) { + ::close(fd); + return -1; + } + VLOG_LP(log_info) << "acquired lock on manifest file: " << manifest_path.string(); + return fd; +} + } // namespace limestone::internal diff --git a/src/limestone/dblog_scan.cpp b/src/limestone/dblog_scan.cpp index 3de4f8e4..3175869d 100644 --- a/src/limestone/dblog_scan.cpp +++ b/src/limestone/dblog_scan.cpp @@ -59,18 +59,21 @@ epoch_id_type dblog_scan::last_durable_epoch_in_dir() { auto& from_dir = dblogdir_; // read main epoch file first auto main_epoch_file = from_dir / std::string(epoch_file_name); + + // If main epoch file does not exist, create an empty one if (!boost::filesystem::exists(main_epoch_file)) { - // datastore operations (ctor and rotate) ensure that the main epoch file exists. - // so it may directory called from outside of datastore - LOG_AND_THROW_EXCEPTION("epoch file does not exist: " + main_epoch_file.string()); - } - std::optional ld_epoch = last_durable_epoch(main_epoch_file); - if (ld_epoch.has_value()) { - return *ld_epoch; + std::ofstream(main_epoch_file.string()).close(); // Create an empty file + } else { + // If the file exists, attempt to get the last durable epoch + std::optional ld_epoch = last_durable_epoch(main_epoch_file); + if (ld_epoch.has_value()) { + return *ld_epoch; + } } - // main epoch file is empty, + // main epoch file is empty or does not contain a valid epoch, // read all rotated-epoch files + std::optional ld_epoch; for (const boost::filesystem::path& p : boost::filesystem::directory_iterator(from_dir)) { if (p.filename().string().rfind(epoch_file_name, 0) == 0) { // starts_with(epoch_file_name) // this is epoch file (main one or rotated) diff --git a/src/limestone/dblogutil/dblogutil.cpp b/src/limestone/dblogutil/dblogutil.cpp index 4d56b2c9..89f38260 100644 --- a/src/limestone/dblogutil/dblogutil.cpp +++ b/src/limestone/dblogutil/dblogutil.cpp @@ -289,11 +289,19 @@ int main(char *dir, subcommand mode) { // NOLINT } try { check_and_migrate_logdir_format(p); + int lock_fd = acquire_manifest_lock(p); + if (lock_fd == -1) { + LOG(ERROR) << "Another process is using the log directory: " << p + << ". Terminate the conflicting process and re-execute the command. " + << "Error: " << strerror(errno); + log_and_exit(64); + } dblog_scan ds(p); ds.set_thread_num(FLAGS_thread_num); if (mode == cmd_inspect) inspect(ds, opt_epoch); if (mode == cmd_repair) repair(ds, opt_epoch); if (mode == cmd_compaction) compaction(ds, opt_epoch); + close(lock_fd); } catch (limestone_exception& e) { LOG(ERROR) << e.what(); log_and_exit(64); diff --git a/src/limestone/internal.h b/src/limestone/internal.h index 3bd2ab9a..ceace303 100644 --- a/src/limestone/internal.h +++ b/src/limestone/internal.h @@ -54,8 +54,15 @@ void setup_initial_logdir(const boost::filesystem::path& logdir); */ int is_supported_version(const boost::filesystem::path& manifest_path, std::string& errmsg); +// Validates the manifest file in the specified log directory and performs repair or migration if necessary. void check_and_migrate_logdir_format(const boost::filesystem::path& logdir); +// Acquires an exclusive lock on the manifest file. +// Returns the file descriptor on success, or -1 on failure. +// Note: This function does not log errors or handle them internally. +// The caller must check the return value and use errno for error handling. +int acquire_manifest_lock(const boost::filesystem::path& logdir); + // from datastore_restore.cpp status purge_dir(const boost::filesystem::path& dir); diff --git a/src/limestone/limestone_exception_helper.h b/src/limestone/limestone_exception_helper.h index 76977546..d1049e8b 100644 --- a/src/limestone/limestone_exception_helper.h +++ b/src/limestone/limestone_exception_helper.h @@ -80,16 +80,16 @@ inline void handle_exception_and_abort(std::string_view func_name) { throw; } LOG_LP(FATAL) << "Fatal error in " << func_name << ": " << e.what(); - std::abort(); // Safety measure: this should never be reached due to VLOG_LP(google::FATAL) + std::abort(); // Safety measure: this should never be reached due to LOG_LP(FATAL) } catch (const std::runtime_error& e) { LOG_LP(FATAL) << "Runtime error in " << func_name << ": " << e.what(); - std::abort(); // Safety measure: this should never be reached due to VLOG_LP(google::FATAL) + std::abort(); // Safety measure: this should never be reached due to LOG_LP(FATAL) } catch (const std::exception& e) { LOG_LP(FATAL) << "Unexpected exception in " << func_name << ": " << e.what(); - std::abort(); // Safety measure: this should never be reached due to VLOG_LP(google::FATAL) + std::abort(); // Safety measure: this should never be reached due to LOG_LP(FATAL) } catch (...) { LOG_LP(FATAL) << "Unknown exception in " << func_name; - std::abort(); // Safety measure: this should never be reached due to VLOG_LP(google::FATAL) + std::abort(); // Safety measure: this should never be reached due to LOG_LP(FATAL) } } diff --git a/src/limestone/log_channel.cpp b/src/limestone/log_channel.cpp index 525982bc..55351a02 100644 --- a/src/limestone/log_channel.cpp +++ b/src/limestone/log_channel.cpp @@ -41,11 +41,26 @@ log_channel::log_channel(boost::filesystem::path location, std::size_t id, datas void log_channel::begin_session() { try { + // Synchronize `current_epoch_id_` with `epoch_id_switched_`. + // This loop is necessary to prevent inconsistencies in `current_epoch_id_` + // that could occur if `epoch_id_switched_` changes at a specific timing. + // + // Case where inconsistency occurs: + // 1. This thread (L) loads `epoch_id_switched_` and reads 10. + // 2. Another thread (S) immediately updates `epoch_id_switched_` to 11. + // 3. If the other thread (S) reads `current_epoch_id_` at this point, + // it expects `current_epoch_id_` to be consistent with the latest + // `epoch_id_switched_` value (11), but `current_epoch_id_` may still + // hold the outdated value, causing an inconsistency. + // + // This loop detects such inconsistencies and repeats until `current_epoch_id_` + // matches the latest value of `epoch_id_switched_`, ensuring consistency. do { current_epoch_id_.store(envelope_.epoch_id_switched_.load()); std::atomic_thread_fence(std::memory_order_acq_rel); } while (current_epoch_id_.load() != envelope_.epoch_id_switched_.load()); - latest_ession_epoch_id_.store(static_cast(current_epoch_id_.load())); + + latest_session_epoch_id_.store(static_cast(current_epoch_id_.load())); auto log_file = file_path(); strm_ = fopen(log_file.c_str(), "a"); // NOLINT(*-owning-memory) @@ -60,7 +75,7 @@ void log_channel::begin_session() { log_entry::begin_session(strm_, static_cast(current_epoch_id_.load())); { std::lock_guard lock(session_mutex_); - waiting_epoch_ids_.insert(latest_ession_epoch_id_); + waiting_epoch_ids_.insert(latest_session_epoch_id_); } } catch (...) { HANDLE_EXCEPTION_AND_ABORT(); @@ -76,8 +91,8 @@ void log_channel::end_session() { LOG_AND_THROW_IO_EXCEPTION("fsync failed", errno); } finished_epoch_id_.store(current_epoch_id_.load()); - current_epoch_id_.store(UINT64_MAX); envelope_.update_min_epoch_id(); + current_epoch_id_.store(UINT64_MAX); if (fclose(strm_) != 0) { // NOLINT(*-owning-memory) LOG_AND_THROW_IO_EXCEPTION("fclose failed", errno); @@ -86,7 +101,7 @@ void log_channel::end_session() { // Remove current_epoch_id_ from waiting_epoch_ids_ { std::lock_guard lock(session_mutex_); - waiting_epoch_ids_.erase(latest_ession_epoch_id_.load()); + waiting_epoch_ids_.erase(latest_session_epoch_id_.load()); // Notify waiting threads session_cv_.notify_all(); } @@ -178,7 +193,7 @@ rotation_result log_channel::do_rotate_file(epoch_id_type epoch) { envelope_.subtract_file(location_ / file_); // Create a rotation result with the current epoch ID - rotation_result result(new_name, latest_ession_epoch_id_); + rotation_result result(new_name, latest_session_epoch_id_); return result; } diff --git a/test/limestone/api/datastore_test.cpp b/test/limestone/api/datastore_test.cpp index 7fd4c72e..765723ed 100644 --- a/test/limestone/api/datastore_test.cpp +++ b/test/limestone/api/datastore_test.cpp @@ -133,10 +133,32 @@ TEST_F(datastore_test, add_persistent_callback_test) { // NOLINT } +TEST_F(datastore_test, prevent_double_start_test) { // NOLINT + if (system("rm -rf /tmp/datastore_test") != 0) { + std::cerr << "cannot remove directory" << std::endl; + } + if (system("mkdir -p /tmp/datastore_test/data_location /tmp/datastore_test/metadata_location") != 0) { + std::cerr << "cannot make directory" << std::endl; + } + std::vector data_locations{}; + data_locations.emplace_back(data_location); + boost::filesystem::path metadata_location_path{metadata_location}; + limestone::api::configuration conf(data_locations, metadata_location_path); + auto ds1 = std::make_unique(conf); + ds1->ready(); + // another process is using the log directory + ASSERT_DEATH({ + auto ds2 = std::make_unique(conf); + }, "Another process is using the log directory"); - + // Ather datastore is created after the first one is destroyed + ds1->shutdown(); + auto ds3 = std::make_unique(conf); + ds3->ready(); + ds3->shutdown(); +} } // namespace limestone::testing diff --git a/test/limestone/epoch/epoch_file_test.cpp b/test/limestone/epoch/epoch_file_test.cpp new file mode 100644 index 00000000..31b524e4 --- /dev/null +++ b/test/limestone/epoch/epoch_file_test.cpp @@ -0,0 +1,191 @@ +/* + * Copyright 2022-2024 Project Tsurugi. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include + +#include +#include + +#include "dblog_scan.h" +#include "internal.h" +#include "log_entry.h" +#include "online_compaction.h" +#include "compaction_catalog.h" + +#include "test_root.h" + +using namespace std::literals; +using namespace limestone::api; +using namespace limestone::internal; + +// Forward declare the target function for testing +namespace limestone::internal { + std::set assemble_snapshot_input_filenames( + const std::unique_ptr& compaction_catalog, + const boost::filesystem::path& location); +std::set assemble_snapshot_input_filenames( + const std::unique_ptr& compaction_catalog, + const boost::filesystem::path& location, + file_operations& file_ops); +} + + +namespace limestone::testing { + +extern void create_file(const boost::filesystem::path& path, std::string_view content); +extern const std::string_view epoch_0_str; +extern const std::string_view epoch_0x100_str; +extern std::string data_manifest(int persistent_format_version = 1); +extern const std::string_view data_normal; +extern const std::string_view data_nondurable; + +class epoch_file_test : public ::testing::Test { +public: + static constexpr const char* location = "/tmp/epoch_file_test"; + const boost::filesystem::path manifest_path = boost::filesystem::path(location) / std::string(limestone::internal::manifest_file_name); + const boost::filesystem::path compaction_catalog_path = boost::filesystem::path(location) / "compaction_catalog"; + const boost::filesystem::path epoch_file_path = boost::filesystem::path(location) / std::string(limestone::internal::epoch_file_name); + const boost::filesystem::path pwal000_file_path = boost::filesystem::path(location) / "pwal_0000"; + const std::string compacted_filename = compaction_catalog::get_compacted_filename(); + + void SetUp() { + if (boost::filesystem::exists(location)) { + boost::filesystem::permissions(location, boost::filesystem::owner_all); + } + boost::filesystem::remove_all(location); + if (!boost::filesystem::create_directory(location)) { + std::cerr << "cannot make directory" << std::endl; + } + compaction_catalog_ = std::make_unique(boost::filesystem::path(location)); + } + + void gen_datastore() { + std::vector data_locations{}; + data_locations.emplace_back(location); + boost::filesystem::path metadata_location{location}; + limestone::api::configuration conf(data_locations, metadata_location); + + datastore_ = std::make_unique(conf); + lc0_ = &datastore_->create_channel(location); + lc1_ = &datastore_->create_channel(location); + + datastore_->ready(); + } + + epoch_id_type last_durable_epoch() { + boost::filesystem::path from_dir = boost::filesystem::path(location); + std::set file_names = assemble_snapshot_input_filenames(compaction_catalog_, from_dir); + dblog_scan logscan = file_names.empty() ? dblog_scan{from_dir} : dblog_scan{from_dir, file_names}; + epoch_id_type last_durable_epoch = logscan.last_durable_epoch_in_dir(); + return last_durable_epoch; + } + + std::optional get_rotated_epoch_file() { + std::optional result; + boost::filesystem::path directory = boost::filesystem::path(location); + if (boost::filesystem::exists(directory) && boost::filesystem::is_directory(directory)) { + for (const auto& entry : boost::filesystem::directory_iterator(directory)) { + if (boost::filesystem::is_regular_file(entry) && entry.path().filename().string().rfind("epoch.", 0) == 0) { + if (result.has_value()) { + throw std::runtime_error("Multiple files starting with 'epoch.' found."); + } + result = entry.path(); + } + } + } + return result; + } + + void TearDown() { + datastore_ = nullptr; + if (boost::filesystem::exists(location)) { + boost::filesystem::permissions(location, boost::filesystem::owner_all); + } + boost::filesystem::remove_all(location); + } + +protected: + std::unique_ptr datastore_{}; + std::unique_ptr compaction_catalog_; + log_channel* lc0_{}; + log_channel* lc1_{}; +}; + + +TEST_F(epoch_file_test, last_durable_epoch) { + // ログディレクトリを初期化 + gen_datastore(); + datastore_->shutdown(); + datastore_ = nullptr; + + // Empty epoch file, No rotated epoch files + ASSERT_EQ(0, boost::filesystem::file_size(epoch_file_path)); + ASSERT_FALSE(get_rotated_epoch_file().has_value()); + EXPECT_EQ(0, last_durable_epoch()); + + // No epoch file, No rotated epoch files + boost::filesystem::remove(epoch_file_path); + ASSERT_FALSE(boost::filesystem::exists(epoch_file_path)); + ASSERT_FALSE(get_rotated_epoch_file().has_value()); + EXPECT_EQ(0, last_durable_epoch()); + + // Non-empty epoch file, No rotated epoch files + gen_datastore(); + datastore_->switch_epoch(1); + datastore_->switch_epoch(2); + lc0_->begin_session(); + lc0_->add_entry(1, "k1", "v1", {1, 0}); + lc0_->end_session(); + datastore_->switch_epoch(3); + ASSERT_TRUE(boost::filesystem::file_size(epoch_file_path) > 0); + ASSERT_FALSE(get_rotated_epoch_file().has_value()); + EXPECT_EQ(2, last_durable_epoch()); + + + // Empty epoch file, Non-empty rotated epoch files + datastore_->rotate_epoch_file(); + + ASSERT_EQ(0, boost::filesystem::file_size(epoch_file_path)); + ASSERT_TRUE(get_rotated_epoch_file().has_value()); + ASSERT_TRUE(boost::filesystem::file_size(get_rotated_epoch_file().value()) > 0); + EXPECT_EQ(2, last_durable_epoch()); + + // No epoch file, Non-empty rotated epoch files + boost::filesystem::remove(epoch_file_path); + ASSERT_FALSE(boost::filesystem::exists(epoch_file_path)); + ASSERT_TRUE(get_rotated_epoch_file().has_value()); + ASSERT_TRUE(boost::filesystem::file_size(get_rotated_epoch_file().value()) > 0); + EXPECT_EQ(2, last_durable_epoch()); + + // Non-empty epoch file, Non-empty rotated epoch files + lc0_->begin_session(); + lc0_->add_entry(1, "k1", "v2", {1, 0}); + lc0_->end_session(); + datastore_->switch_epoch(4); + + ASSERT_TRUE(boost::filesystem::file_size(epoch_file_path) > 0); + ASSERT_TRUE(boost::filesystem::file_size(get_rotated_epoch_file().value()) > 0); + EXPECT_EQ(3, last_durable_epoch()); +} + + + +} // namespace limestone::testing + + diff --git a/test/limestone/log/rotate_test.cpp b/test/limestone/log/rotate_test.cpp index 92061917..55606e5a 100644 --- a/test/limestone/log/rotate_test.cpp +++ b/test/limestone/log/rotate_test.cpp @@ -261,6 +261,7 @@ TEST_F(rotate_test, inactive_files_are_also_backed_up) { // NOLINT channel1_1.add_entry(2, "k1", "v1", {42, 4}); channel1_1.end_session(); datastore_->switch_epoch(43); + datastore_->shutdown(); } regen_datastore(); { @@ -273,6 +274,7 @@ TEST_F(rotate_test, inactive_files_are_also_backed_up) { // NOLINT channel2_0.add_entry(2, "k3", "v3", {44, 4}); channel2_0.end_session(); datastore_->switch_epoch(45); + datastore_->shutdown(); } // setup done diff --git a/test/limestone/utils/dblogutil_test.cpp b/test/limestone/utils/dblogutil_test.cpp index 6088db64..e6caeddf 100644 --- a/test/limestone/utils/dblogutil_test.cpp +++ b/test/limestone/utils/dblogutil_test.cpp @@ -53,6 +53,7 @@ int invoke(const std::string& command, std::string& out) { class dblogutil_test : public ::testing::Test { public: static constexpr const char* location = "/tmp/dblogutil_test"; +static constexpr const char* metadata_location = "/tmp/dblogutil_test/metadata"; void SetUp() { boost::filesystem::remove_all(location); @@ -565,4 +566,31 @@ TEST_F(dblogutil_test, invalid_epoch_option3) { EXPECT_TRUE(contains(out, "invalid")); } -} // namespace limestone::testing +TEST_F(dblogutil_test, execution_fails_while_active_datastore) { + // Inactive datastore + auto [rc, out] = inspect("pwal_0000", data_normal); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("\n" "status: OK"), out.npos); + + // Activate datastore + std::vector data_locations{}; + data_locations.emplace_back(location); + boost::filesystem::path metadata_location_path{metadata_location}; + limestone::api::configuration conf(data_locations, metadata_location_path); + auto ds1 = std::make_unique(conf); + ds1->ready(); + + // Attempt to run inspect while datastore is active + auto [rc_active, out_active] = inspect("pwal_0000", data_normal); + EXPECT_NE(rc_active, 0); + EXPECT_TRUE(contains(out_active, "Another process is using the log directory:")); + + // Inactive datastore + ds1->shutdown(); + ds1 = nullptr; + auto [rc_inactive, out_inacive] = inspect("pwal_0000", data_normal); + EXPECT_EQ(rc_inactive, 0); + EXPECT_NE(out_inacive.find("\n" "status: OK"), out.npos); +} + +} // namespace limestone::testing diff --git a/test/test_root.h b/test/test_root.h index 0fa8ab25..720e99dd 100644 --- a/test/test_root.h +++ b/test/test_root.h @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 Project Tsurugi. + * Copyright 2019-2024 Project Tsurugi. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ class datastore_test : public datastore { auto& log_channels() const noexcept { return log_channels_for_tests(); } auto epoch_id_informed() const noexcept { return epoch_id_informed_for_tests(); } auto epoch_id_recorded() const noexcept { return epoch_id_recorded_for_tests(); } + auto epoch_id_switched() const noexcept { return epoch_id_switched_for_tests(); } auto& files() const noexcept { return files_for_tests(); } + void rotate_epoch_file() { rotate_epoch_file_for_tests(); } }; } // namespace limestone::api