Skip to content

Commit abbb8f7

Browse files
committed
fix(compaction): reject a non-empty or missing --working-dir before destructive steps
- the working directory is consumed by the command (renamed onto dblogdir on a real run, removed on a dry run), so a mistaken --working-dir pointing at a directory that holds data would destroy the user's files - require an existing empty directory and exit with status 64 otherwise, before any destructive step, consistent with the cross-filesystem check - add regression tests for the non-empty and non-existent cases, verifying the log directory (and the working directory) are left intact - document the requirement in the --working-dir description and the exit-status 64 list
1 parent f42a4b0 commit abbb8f7

3 files changed

Lines changed: 87 additions & 2 deletions

File tree

docs/tglogutil-compaction-man.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Options:
2525
* `--thread-num=<number>`
2626
* Number (default `1`) of concurrent processing thread of reading log files
2727
* `--working-dir=</path/to/working-dir>`
28-
* Directory for storing temporary files (default is a uniquely named directory next to `dblogdir`)
28+
* Directory used as the working directory itself (not a parent under which one is created). By default a uniquely named directory is created next to `dblogdir`.
29+
* Must be an existing empty directory dedicated to this command. Its contents are consumed by the command: on a real run the directory is renamed onto `dblogdir` at the end, and on a dry run it is removed on completion. If the path does not exist or is not empty, the command stops before any destructive operation and exits with status 64. Do not point it at a directory whose contents you want to keep.
2930
* Must be on the same filesystem as `dblogdir`. If it is on a different filesystem, the command stops before any destructive operation and exits with status 64.
3031
* `--verbose=<bool>`
3132
* Verbose mode (default `false`)
@@ -48,6 +49,7 @@ Options:
4849
* Specified a directory that is not the transaction log directory
4950
* Specified a transaction log directory of unsupported format version
5051
* `epoch` file does not exist
52+
* `working-dir` does not exist or is not an empty directory
5153
* `working-dir` is on a different filesystem than `dblogdir`
5254
* files in `dblogdir` are damaged
5355

src/limestone/dblogutil/dblogutil.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,21 @@ void compaction(dblog_scan &ds) {
369369
boost::filesystem::path tmp;
370370
if (!FLAGS_working_dir.empty()) {
371371
tmp = FLAGS_working_dir;
372-
// TODO: check, error if exist and non-empty
372+
// The working directory is consumed by the command: on a real run it is
373+
// renamed onto from_dir, and on a dry run it is removed. Require an existing
374+
// empty directory so that a mistaken --working-dir never destroys the user's
375+
// files. A non-existent path or a non-empty directory is rejected before any
376+
// destructive step.
377+
boost::system::error_code ec;
378+
if (!boost::filesystem::is_directory(tmp, ec)) {
379+
LOG(ERROR) << "working directory must be an existing directory: "
380+
<< "working-directory=" << tmp;
381+
log_and_exit(64);
382+
}
383+
if (!boost::filesystem::is_empty(tmp, ec)) {
384+
LOG(ERROR) << "working directory must be empty: working-directory=" << tmp;
385+
log_and_exit(64);
386+
}
373387
} else {
374388
tmp = make_work_dir_next_to(from_dir);
375389
}

test/limestone/compaction/offline_compaction_test.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,75 @@ TEST_F(offline_compaction_test, offline_compaction_ignores_malformed_epoch_optio
600600
EXPECT_EQ(kv_list[0].second, "va");
601601
}
602602

603+
// --working-dir is consumed by the command (renamed onto dblogdir on a real run,
604+
// removed on a dry run), so pointing it at a non-empty directory would destroy the
605+
// user's data. Such a directory must be rejected up front, before any destructive
606+
// step, leaving the original log directory intact.
607+
TEST_F(offline_compaction_test, offline_compaction_rejects_non_empty_working_dir) {
608+
gen_datastore();
609+
datastore_->switch_epoch(1);
610+
lc0_->begin_session();
611+
lc0_->add_entry(1, "A", "va", {1, 0});
612+
lc0_->end_session();
613+
datastore_->switch_epoch(2);
614+
datastore_->shutdown();
615+
datastore_ = nullptr;
616+
617+
std::map<std::string, std::string> before = snapshot_tree(location);
618+
619+
// A non-empty working directory on the same filesystem as the log directory.
620+
boost::filesystem::path working_dir =
621+
boost::filesystem::path(test_root_) / "non_empty_wd";
622+
boost::filesystem::create_directories(working_dir);
623+
{
624+
std::ofstream ofs((working_dir / "precious.txt").string());
625+
ofs << "do not delete me";
626+
}
627+
628+
std::string out;
629+
std::string command = std::string(util_command) + " compaction --force --working_dir=" +
630+
working_dir.string() + " " + std::string(location) + " 2>&1";
631+
int rc = invoke(command, out);
632+
EXPECT_NE(rc, 0) << "compaction should fail on a non-empty working directory";
633+
EXPECT_NE(out.find("working directory must be empty"), std::string::npos)
634+
<< "tglogutil output:\n" << out;
635+
636+
// The rejection must leave both the log directory and the working directory intact.
637+
EXPECT_EQ(snapshot_tree(location), before);
638+
EXPECT_TRUE(boost::filesystem::exists(working_dir / "precious.txt"));
639+
640+
boost::filesystem::remove_all(working_dir);
641+
}
642+
643+
// --working-dir must be an existing empty directory. A non-existent path is rejected
644+
// up front, leaving the original log directory intact.
645+
TEST_F(offline_compaction_test, offline_compaction_rejects_non_existent_working_dir) {
646+
gen_datastore();
647+
datastore_->switch_epoch(1);
648+
lc0_->begin_session();
649+
lc0_->add_entry(1, "A", "va", {1, 0});
650+
lc0_->end_session();
651+
datastore_->switch_epoch(2);
652+
datastore_->shutdown();
653+
datastore_ = nullptr;
654+
655+
std::map<std::string, std::string> before = snapshot_tree(location);
656+
657+
boost::filesystem::path working_dir =
658+
boost::filesystem::path(test_root_) / "missing_wd";
659+
ASSERT_FALSE(boost::filesystem::exists(working_dir));
660+
661+
std::string out;
662+
std::string command = std::string(util_command) + " compaction --force --working_dir=" +
663+
working_dir.string() + " " + std::string(location) + " 2>&1";
664+
int rc = invoke(command, out);
665+
EXPECT_NE(rc, 0) << "compaction should fail on a non-existent working directory";
666+
EXPECT_NE(out.find("working directory must be an existing directory"), std::string::npos)
667+
<< "tglogutil output:\n" << out;
668+
669+
EXPECT_EQ(snapshot_tree(location), before);
670+
}
671+
603672
// With --make_backup, the blob directory must be copied (not moved) so that both the
604673
// compacted log directory and the backup directory keep the blob data.
605674
TEST_F(offline_compaction_test, offline_compaction_with_backup_copies_blob_directory) {

0 commit comments

Comments
 (0)