Skip to content

Commit c74e159

Browse files
committed
refactor(compaction): centralize the max_blob_id high-water mark in the catalog
- update_catalog_file now keeps max_blob_id monotonically non-decreasing, so the invariant "blob IDs are never reused" lives in one place - online compaction drops its now-redundant explicit std::max - offline compaction (tglogutil) carries over the existing catalog and its backup into the work directory, then loads+updates it like online compaction does, preserving the high-water mark through the shared update_catalog_file - check the error_code (other than ENOENT) when probing/copying the catalog files, and abort with a clear message when the carried-over catalog is unrecoverable, instead of silently losing the high-water mark - add compaction_catalog::get_catalog_backup_filename() - tests: update_catalog_file never lowers max_blob_id; offline compaction preserves the high-water mark and recovers the catalog from its backup
1 parent 226b96d commit c74e159

6 files changed

Lines changed: 154 additions & 5 deletions

File tree

src/limestone/compaction_catalog.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
#include <glog/logging.h>
18+
#include <algorithm>
1819
#include <fstream>
1920
#include <stdexcept>
2021
#include <sstream>
@@ -180,9 +181,12 @@ void compaction_catalog::parse_catalog_entry(const std::string& line, bool& max_
180181

181182
// Method to update the compaction catalog
182183
void compaction_catalog::update_catalog_file(epoch_id_type max_epoch_id, blob_id_type max_blob_id, const std::set<compacted_file_info>& compacted_files, const std::set<std::string>& detached_pwals) {
183-
// Update internal state
184+
// Update internal state. The maximum blob ID is a monotonically non-decreasing
185+
// high-water mark: blob IDs must never be reused, so it must not drop below the
186+
// value already recorded even when the caller passes a smaller value (e.g. a
187+
// compaction that observed only the blobs still referenced by live entries).
184188
max_epoch_id_ = max_epoch_id;
185-
max_blob_id_ = max_blob_id;
189+
max_blob_id_ = std::max(max_blob_id, max_blob_id_);
186190
compacted_files_ = compacted_files;
187191
detached_pwals_ = detached_pwals;
188192

src/limestone/compaction_catalog.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,15 @@ class compaction_catalog {
137137
* This method updates the catalog with new compacted files, detached PWALs, the maximum epoch ID,
138138
* and the maximum blob ID, then writes the updated catalog to a file.
139139
*
140+
* The maximum blob ID is monotonically non-decreasing: the recorded value never
141+
* drops below the value the catalog already holds, even if @p max_blob_id is
142+
* smaller. Blob IDs must never be reused, so a compaction (online or offline)
143+
* that observes only the blobs still referenced by live entries must not lower
144+
* this high-water mark.
145+
*
140146
* @param max_epoch_id The maximum epoch ID to be recorded in the catalog.
141-
* @param max_blob_id The maximum blob ID to be recorded in the catalog.
147+
* @param max_blob_id The candidate maximum blob ID; the recorded value is the
148+
* maximum of this and the current value (see above).
142149
* @param compacted_files Set of compacted files to be included in the catalog.
143150
* @param detached_pwals Set of detached PWALs to be included in the catalog.
144151
*/
@@ -180,6 +187,13 @@ class compaction_catalog {
180187
*/
181188
[[nodiscard]] static inline std::string get_catalog_filename() { return COMPACTION_CATALOG_FILENAME; }
182189

190+
/**
191+
* @brief Returns the filename of the compaction catalog backup.
192+
*
193+
* @return The filename of the compaction catalog backup.
194+
*/
195+
[[nodiscard]] static inline std::string get_catalog_backup_filename() { return COMPACTION_CATALOG_BACKUP_FILENAME; }
196+
183197
/**
184198
* @brief Retrieves the name of the compaction temporary directory.
185199
*

src/limestone/datastore.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,9 +1014,11 @@ void datastore::compact_with_online() {
10141014

10151015

10161016
// update compaction catalog
1017+
// update_catalog_file keeps max_blob_id monotonically non-decreasing, so the
1018+
// high-water mark recorded by previous compactions is preserved even though
1019+
// max_blob_id here reflects only the freshly compacted files.
10171020
compacted_file_info compacted_file_info{compacted_file.filename().string(), 1};
10181021
detached_pwals.erase(compacted_file.filename().string());
1019-
max_blob_id = std::max(max_blob_id, compaction_catalog_->get_max_blob_id());
10201022
compaction_catalog_->update_catalog_file(result.get_epoch_id(), max_blob_id, {compacted_file_info}, detached_pwals);
10211023
add_file(compacted_file);
10221024

src/limestone/dblogutil/dblogutil.cpp

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,51 @@ void compaction(dblog_scan &ds, std::optional<epoch_id_type> epoch) {
252252
// Without this, a subsequent startup treats the directory as if no compaction
253253
// had been performed, and remove entries are dropped from the snapshot,
254254
// resurrecting deleted records (see tsurugi-issues #1498).
255+
//
256+
// tmp will replace from_dir, so carry over the existing catalog (and its backup)
257+
// before updating it, exactly like online compaction updates the live catalog.
258+
// This preserves the high-water marks recorded by previous compactions: in
259+
// particular update_catalog_file keeps max_blob_id monotonically non-decreasing,
260+
// so the freshly computed max_blob_id (which reflects only blobs still referenced
261+
// by live entries) never lowers it and blob IDs are never reused. If from_dir has
262+
// no catalog (a directory that was never compacted), the empty catalog created by
263+
// setup_initial_logdir(tmp) is used instead.
264+
auto copy_catalog_file = [&](const std::string& filename) {
265+
boost::filesystem::path src = from_dir / filename;
266+
boost::system::error_code ec;
267+
bool present = boost::filesystem::exists(src, ec);
268+
// A non-existent file is the normal case (boost reports it via ec as ENOENT).
269+
// Any other error (permission, I/O, ...) must not be silently ignored: skipping
270+
// the copy would drop the catalog carry-over and let later processing run on an
271+
// inconsistent state.
272+
if (ec && ec != boost::system::errc::no_such_file_or_directory) {
273+
LOG_AND_THROW_IO_EXCEPTION("failed to check existence of compaction catalog file: " + src.string(), ec);
274+
}
275+
if (present) {
276+
boost::filesystem::copy_file(src, tmp / filename, boost::filesystem::copy_options::overwrite_existing, ec);
277+
if (ec) {
278+
LOG_AND_THROW_IO_EXCEPTION("failed to copy compaction catalog file: " + src.string(), ec);
279+
}
280+
}
281+
};
282+
copy_catalog_file(compaction_catalog::get_catalog_filename());
283+
copy_catalog_file(compaction_catalog::get_catalog_backup_filename());
284+
255285
VLOG_LP(log_info) << "updating compaction catalog in " << tmp;
256-
compaction_catalog catalog{tmp};
286+
// tmp always has a catalog here (setup_initial_logdir created an empty one, possibly
287+
// overwritten by the carry-over above), so loading only fails when the carried-over
288+
// catalog and its backup are both unreadable. In that case abort with a clear message
289+
// rather than silently proceeding, which would lose the blob-id high-water mark.
290+
compaction_catalog catalog = [&]() {
291+
try {
292+
return compaction_catalog::from_catalog_file(tmp);
293+
} catch (const limestone_exception& ex) {
294+
LOG_AND_THROW_EXCEPTION(
295+
"the existing compaction catalog in " + from_dir.string() +
296+
" is unreadable and could not be recovered; offline compaction was aborted to"
297+
" avoid losing the blob-id high-water mark (cause: " + ex.what() + ")");
298+
}
299+
}();
257300
compacted_file_info compacted_file{compaction_catalog::get_compacted_filename(), 1};
258301
catalog.update_catalog_file(ld_epoch, max_blob_id, {compacted_file}, {});
259302

test/limestone/compaction/compaction_catalog_test.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,31 @@ TEST_F(compaction_catalog_test, restore_from_backup_exceptions) {
873873
limestone_io_exception);
874874
}
875875

876+
// Verifies that max_blob_id is a monotonically non-decreasing high-water mark:
877+
// update_catalog_file must never lower it (blob IDs must never be reused), while
878+
// max_epoch_id is set directly.
879+
TEST_F(compaction_catalog_test, max_blob_id_is_monotonically_non_decreasing) {
880+
compaction_catalog catalog(test_dir);
881+
882+
catalog.update_catalog_file(10, 1000, {}, {});
883+
EXPECT_EQ(catalog.get_max_blob_id(), 1000);
884+
EXPECT_EQ(catalog.get_max_epoch_id(), 10);
885+
886+
// A smaller value must not lower the high-water mark.
887+
catalog.update_catalog_file(20, 500, {}, {});
888+
EXPECT_EQ(catalog.get_max_blob_id(), 1000);
889+
EXPECT_EQ(catalog.get_max_epoch_id(), 20); // epoch is set directly, not maxed
890+
891+
// A larger value updates it.
892+
catalog.update_catalog_file(30, 2000, {}, {});
893+
EXPECT_EQ(catalog.get_max_blob_id(), 2000);
894+
895+
// The preserved value survives a reload from the catalog file.
896+
compaction_catalog reloaded = compaction_catalog::from_catalog_file(test_dir);
897+
EXPECT_EQ(reloaded.get_max_blob_id(), 2000);
898+
EXPECT_EQ(reloaded.get_max_epoch_id(), 30);
899+
}
900+
876901

877902

878903
} // namespace limestone::testing

test/limestone/compaction/offline_compaction_test.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <cerrno>
1919
#include <cstdio>
2020
#include <cstring>
21+
#include <fstream>
2122
#include <map>
2223
#include <set>
2324
#include <sstream>
@@ -191,4 +192,64 @@ TEST_F(offline_compaction_test, detects_inconsistent_compaction_catalog_at_start
191192
}
192193
}
193194

195+
// Offline compaction must preserve the max_blob_id high-water mark recorded in the
196+
// existing catalog, even though the freshly computed value (no blobs here) is lower.
197+
// Lowering it could lead to blob-id reuse.
198+
TEST_F(offline_compaction_test, offline_compaction_preserves_blob_id_high_water_mark) {
199+
gen_datastore();
200+
datastore_->switch_epoch(1);
201+
lc0_->begin_session();
202+
lc0_->add_entry(1, "key1", "value1", {1, 0}); // no blob -> computed max_blob_id is 0
203+
lc0_->end_session();
204+
datastore_->switch_epoch(2);
205+
datastore_->shutdown();
206+
datastore_ = nullptr;
207+
208+
// Simulate a high-water mark left by earlier blob allocations / online compactions.
209+
{
210+
compaction_catalog catalog = compaction_catalog::from_catalog_file(location);
211+
catalog.update_catalog_file(catalog.get_max_epoch_id(), 9999, {}, {});
212+
}
213+
214+
run_offline_compaction();
215+
216+
// The high-water mark must survive offline compaction (not lowered to the computed 0).
217+
compaction_catalog catalog = compaction_catalog::from_catalog_file(location);
218+
EXPECT_EQ(catalog.get_max_blob_id(), 9999);
219+
}
220+
221+
// Offline compaction must recover the existing catalog from its backup when the main
222+
// catalog file is unreadable, and still preserve the high-water mark.
223+
TEST_F(offline_compaction_test, offline_compaction_recovers_catalog_from_backup) {
224+
gen_datastore();
225+
datastore_->switch_epoch(1);
226+
lc0_->begin_session();
227+
lc0_->add_entry(1, "key1", "value1", {1, 0});
228+
lc0_->end_session();
229+
datastore_->switch_epoch(2);
230+
datastore_->shutdown();
231+
datastore_ = nullptr;
232+
233+
{
234+
compaction_catalog catalog = compaction_catalog::from_catalog_file(location);
235+
catalog.update_catalog_file(catalog.get_max_epoch_id(), 7777, {}, {});
236+
}
237+
238+
// Leave the valid content only in the backup, then corrupt the main catalog file.
239+
boost::filesystem::path dir{location};
240+
boost::filesystem::path main_catalog = dir / compaction_catalog::get_catalog_filename();
241+
boost::filesystem::path backup_catalog = dir / compaction_catalog::get_catalog_backup_filename();
242+
boost::filesystem::copy_file(main_catalog, backup_catalog, boost::filesystem::copy_options::overwrite_existing);
243+
{
244+
std::ofstream ofs(main_catalog.string(), std::ios::trunc);
245+
ofs << "GARBAGE";
246+
}
247+
248+
run_offline_compaction();
249+
250+
// The catalog must have been recovered from the backup and the high-water mark kept.
251+
compaction_catalog catalog = compaction_catalog::from_catalog_file(location);
252+
EXPECT_EQ(catalog.get_max_blob_id(), 7777);
253+
}
254+
194255
} // namespace limestone::testing

0 commit comments

Comments
 (0)