mirror of
https://github.com/ceph/ceph
synced 2026-08-02 23:20:36 +00:00
osd: Add ECOmapJournal class and relocate OmapUpdateType enum class
The ECOmapJournal will be used to store omap updates (in ec pools with optimisations enabled) which have not yet been committed to the object store. Added unit tests for this class. Promoted OmapUpdateType to osd_types.h so that it can be shared to multiple files without circular dependencies. Signed-off-by: Matty Williams <Matty.Williams@ibm.com>
This commit is contained in:
parent
82dbf20104
commit
7289e4d2ec
@ -45,6 +45,7 @@ ECBackend::ECBackend(pg_shard_t whoami,
|
||||
DoutPrefixProvider &dpp,
|
||||
ECListener &eclistener)
|
||||
: PGBackend{whoami, coll, shard_services, store_index, dpp},
|
||||
ECCommon(dpp),
|
||||
ec_impl{create_ec_impl(ec_profile)},
|
||||
sinfo(ec_impl, &(eclistener.get_pool()), stripe_width),
|
||||
fast_read{fast_read},
|
||||
|
||||
@ -53,6 +53,8 @@ set(osd_srcs
|
||||
ECInject.cc
|
||||
ECInject.h
|
||||
Coroutines.h
|
||||
ECOmapJournal.cc
|
||||
ECOmapJournal.h
|
||||
${CMAKE_SOURCE_DIR}/src/common/TrackedOp.cc
|
||||
${CMAKE_SOURCE_DIR}/src/mgr/OSDPerfMetricTypes.cc
|
||||
${osd_cyg_functions_src}
|
||||
|
||||
@ -75,7 +75,8 @@ ECBackend::ECBackend(
|
||||
uint64_t stripe_width,
|
||||
ECSwitch *s,
|
||||
ECExtentCache::LRU &ec_extent_cache_lru)
|
||||
: parent(pg), cct(cct), switcher(s),
|
||||
: ECCommon(*pg->get_dpp()),
|
||||
parent(pg), cct(cct), switcher(s),
|
||||
#ifdef WITH_CRIMSON
|
||||
read_pipeline(cct, ec_impl, this->sinfo, get_parent()->get_eclistener(), *this),
|
||||
#else
|
||||
|
||||
@ -51,6 +51,10 @@ struct PGLog;
|
||||
struct RecoveryMessages;
|
||||
|
||||
struct ECCommon {
|
||||
ECOmapJournal ec_omap_journal;
|
||||
|
||||
explicit ECCommon(const DoutPrefixProvider& dpp) : ec_omap_journal(dpp) {}
|
||||
|
||||
struct ec_extent_t {
|
||||
int err;
|
||||
extent_map emap;
|
||||
|
||||
580
src/osd/ECOmapJournal.cc
Normal file
580
src/osd/ECOmapJournal.cc
Normal file
@ -0,0 +1,580 @@
|
||||
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
|
||||
// vim: ts=8 sw=2 sts=2 expandtab
|
||||
|
||||
/*
|
||||
* Ceph - scalable distributed file system
|
||||
*
|
||||
* Copyright (C) 2025 IBM
|
||||
*
|
||||
* This is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License version 2.1, as published by the Free Software
|
||||
* Foundation. See file COPYING.
|
||||
*/
|
||||
|
||||
#include "ECOmapJournal.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
ECOmapJournalEntry::ECOmapJournalEntry(
|
||||
const eversion_t version, const bool clear_omap, std::optional<ceph::buffer::list> omap_header,
|
||||
std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> omap_updates)
|
||||
: version(version), clear_omap(clear_omap),
|
||||
omap_header(std::move(omap_header)), omap_updates(std::move(omap_updates)) {}
|
||||
|
||||
void ECOmapValue::update_value(const eversion_t new_version, std::optional<ceph::buffer::list> new_value) {
|
||||
this->version = new_version;
|
||||
this->value = std::move(new_value);
|
||||
}
|
||||
|
||||
void ECOmapRemovedRanges::add_range(const std::string& start, const std::optional<std::string>& end) {
|
||||
std::string new_start = start;
|
||||
std::optional<std::string> new_end = end;
|
||||
auto it = ranges.begin();
|
||||
bool inserted = false;
|
||||
while (it != ranges.end()) {
|
||||
// Current range is to the left of new range
|
||||
if (it->second && *it->second < new_start) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
// Current range is to the right of new range
|
||||
if (new_end && *new_end < it->first) {
|
||||
ranges.insert(it, {new_start, new_end});
|
||||
inserted = true;
|
||||
break;
|
||||
}
|
||||
// Ranges overlap, merge them
|
||||
if (it->first < new_start) {
|
||||
new_start = it->first;
|
||||
}
|
||||
if (!it->second) {
|
||||
new_end = std::nullopt;
|
||||
} else if (new_end && *it->second > *new_end) {
|
||||
new_end = it->second;
|
||||
}
|
||||
it = ranges.erase(it);
|
||||
}
|
||||
if (!inserted) {
|
||||
ranges.emplace_back(new_start, new_end);
|
||||
}
|
||||
}
|
||||
|
||||
void ECOmapRemovedRanges::clear_omap() {
|
||||
ranges.clear();
|
||||
ranges.emplace_back("", std::nullopt);
|
||||
}
|
||||
|
||||
void ECOmapHeader::update_header(const eversion_t new_version,
|
||||
std::optional<ceph::buffer::list> new_header) {
|
||||
this->version = new_version;
|
||||
this->header = std::move(new_header);
|
||||
}
|
||||
|
||||
|
||||
void ECOmapJournal::add_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry) {
|
||||
ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
|
||||
<< " version=" << entry.version
|
||||
<< " clear_omap=" << entry.clear_omap
|
||||
<< " header_size=" << (entry.omap_header ? entry.omap_header->length() : 0)
|
||||
<< dendl;
|
||||
entries[hoid].push_back(entry);
|
||||
}
|
||||
|
||||
bool ECOmapJournal::remove_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry) {
|
||||
// Attempt to remove entry from unprocessed entries
|
||||
if (const auto it_map = entries.find(hoid);
|
||||
it_map != entries.end()) {
|
||||
auto &entry_list = it_map->second;
|
||||
for (auto it = entry_list.begin(); it != entry_list.end(); ++it) {
|
||||
if (it->version == entry.version) {
|
||||
ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
|
||||
<< " version=" << entry.version << " found_unprocessed=true" << dendl;
|
||||
entry_list.erase(it);
|
||||
if (const auto header_it = header_map.find(hoid);
|
||||
header_it != header_map.end() &&
|
||||
header_it->second.version == entry.version) {
|
||||
header_map.erase(header_it);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
|
||||
<< " version=" << entry.version << " found_unprocessed=false" << dendl;
|
||||
|
||||
// Attempt to remove entry from processed entries
|
||||
return remove_processed_entry(hoid, entry);
|
||||
}
|
||||
|
||||
bool ECOmapJournal::remove_entry_by_version(const hobject_t &hoid, const eversion_t version) {
|
||||
// Attempt to remove entry from unprocessed entries
|
||||
if (const auto it_map = entries.find(hoid);
|
||||
it_map != entries.end()) {
|
||||
auto &entry_list = it_map->second;
|
||||
for (auto it = entry_list.begin(); it != entry_list.end(); ++it) {
|
||||
if (it->version == version) {
|
||||
entry_list.erase(it);
|
||||
if (const auto header_it = header_map.find(hoid);
|
||||
header_it != header_map.end() &&
|
||||
header_it->second.version == version) {
|
||||
header_map.erase(header_it);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to remove entry from processed entries
|
||||
return remove_processed_entry_by_version(hoid, version);
|
||||
}
|
||||
|
||||
void ECOmapJournal::clear(const hobject_t &hoid) {
|
||||
entries.erase(hoid);
|
||||
key_map.erase(hoid);
|
||||
removed_ranges_map.erase(hoid);
|
||||
header_map.erase(hoid);
|
||||
object_state_map.erase(hoid);
|
||||
}
|
||||
|
||||
void ECOmapJournal::clear_all() {
|
||||
entries.clear();
|
||||
key_map.clear();
|
||||
removed_ranges_map.clear();
|
||||
header_map.clear();
|
||||
object_state_map.clear();
|
||||
}
|
||||
|
||||
std::size_t ECOmapJournal::entries_size(const hobject_t &hoid) const {
|
||||
if (const auto entries_it = entries.find(hoid);
|
||||
entries_it != entries.end()) {
|
||||
return entries_it->second.size();
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
bool ECOmapJournal::has_unprocessed_entries(const hobject_t &hoid) const {
|
||||
return entries.contains(hoid);
|
||||
}
|
||||
|
||||
bool ECOmapJournal::has_omap_updates(const hobject_t &hoid) const {
|
||||
// Check unprocessed entries
|
||||
if (const auto entries_it = entries.find(hoid);
|
||||
entries_it != entries.end() && !entries_it->second.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check processed key map
|
||||
if (const auto key_it = key_map.find(hoid);
|
||||
key_it != key_map.end() && !key_it->second.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check removed ranges
|
||||
if (const auto ranges_it = removed_ranges_map.find(hoid);
|
||||
ranges_it != removed_ranges_map.end() && !ranges_it->second.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check header updates
|
||||
if (header_map.find(hoid) != header_map.end()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to get specific object's entries, if not present, creates an empty list
|
||||
std::list<ECOmapJournalEntry>& ECOmapJournal::get_entries(const hobject_t &hoid) {
|
||||
return entries[hoid];
|
||||
}
|
||||
|
||||
std::list<ECOmapJournalEntry> ECOmapJournal::snapshot_entries(const hobject_t &hoid) const {
|
||||
if (const auto it = entries.find(hoid);
|
||||
it != entries.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ECOmapJournal::const_iterator ECOmapJournal::begin_entries(const hobject_t &hoid) const {
|
||||
return entries.at(hoid).begin();
|
||||
}
|
||||
|
||||
ECOmapJournal::const_iterator ECOmapJournal::end_entries(const hobject_t &hoid) const {
|
||||
return entries.at(hoid).end();
|
||||
}
|
||||
|
||||
std::optional<ceph::buffer::list> ECOmapJournal::get_updated_header(const hobject_t &hoid) {
|
||||
process_entries(hoid);
|
||||
if (!header_map.contains(hoid)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return header_map[hoid].header;
|
||||
}
|
||||
|
||||
std::tuple<ECOmapJournal::UpdateMapType, ECOmapJournal::RangeMapType>
|
||||
ECOmapJournal::get_value_updates(const hobject_t &hoid) {
|
||||
process_entries(hoid);
|
||||
return {get_key_map(hoid), get_removed_ranges(hoid)};
|
||||
}
|
||||
|
||||
void ECOmapJournal::process_entries(const hobject_t &hoid) {
|
||||
auto entry_list = get_entries(hoid);
|
||||
ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
|
||||
<< " processing " << entry_list.size() << " entries" << dendl;
|
||||
if (!has_unprocessed_entries(hoid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto entry_iter = begin_entries(hoid);
|
||||
entry_iter != end_entries(hoid); ++entry_iter) {
|
||||
ECOmapRemovedRanges removed_ranges(entry_iter->version);
|
||||
|
||||
// Clear omap if specified
|
||||
if (entry_iter->clear_omap) {
|
||||
// Mark all keys as removed
|
||||
for (auto [_, value] : key_map[hoid]) {
|
||||
value.update_value(entry_iter->version, std::nullopt);
|
||||
}
|
||||
|
||||
// Mark entire range as removed
|
||||
removed_ranges.clear_omap();
|
||||
|
||||
if (!entry_iter->omap_header) {
|
||||
// Set the header to an empty bufferlist
|
||||
bufferlist bl;
|
||||
if (!header_map.contains(hoid)) {
|
||||
header_map[hoid] = ECOmapHeader(entry_iter->version, bl);
|
||||
} else {
|
||||
header_map[hoid].update_header(entry_iter->version, bl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update header if present
|
||||
if (entry_iter->omap_header) {
|
||||
if (!header_map.contains(hoid)) {
|
||||
header_map[hoid] = ECOmapHeader(entry_iter->version, entry_iter->omap_header);
|
||||
} else {
|
||||
header_map[hoid].update_header(entry_iter->version, entry_iter->omap_header);
|
||||
}
|
||||
}
|
||||
|
||||
// Process key updates
|
||||
auto &obj_map = key_map[hoid];
|
||||
for (const auto & [type, update] : entry_iter->omap_updates) {
|
||||
auto iter = update.cbegin();
|
||||
switch (type) {
|
||||
case OmapUpdateType::Insert: {
|
||||
std::map<std::string, ceph::buffer::list> vals;
|
||||
decode(vals, iter);
|
||||
// Insert key value pairs into update_map
|
||||
for (auto it = vals.begin(); it != vals.end(); ++it) {
|
||||
const auto &key = it->first;
|
||||
const auto &val = it->second;
|
||||
|
||||
// Check if key already exists in key map
|
||||
auto entry_it = obj_map.find(key);
|
||||
if (entry_it != obj_map.end()) {
|
||||
// Update existing value
|
||||
entry_it->second.update_value(entry_iter->version, val);
|
||||
} else {
|
||||
// Insert new value
|
||||
obj_map.emplace(key, ECOmapValue(entry_iter->version, val));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OmapUpdateType::Remove: {
|
||||
std::set<std::string> keys;
|
||||
decode(keys, iter);
|
||||
// Mark keys in key_map as removed
|
||||
for (const auto &key : keys) {
|
||||
// Check if key already exists in key map
|
||||
if (auto entry_it = obj_map.find(key);
|
||||
entry_it != obj_map.end()) {
|
||||
// Update existing value to null
|
||||
entry_it->second.update_value(entry_iter->version, std::nullopt);
|
||||
} else {
|
||||
// Insert new null value
|
||||
obj_map.emplace(key, ECOmapValue(entry_iter->version, std::nullopt));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OmapUpdateType::RemoveRange: {
|
||||
std::string key_begin, key_end;
|
||||
decode(key_begin, iter);
|
||||
decode(key_end, iter);
|
||||
|
||||
// Add removed range
|
||||
std::string start = key_begin;
|
||||
std::optional<std::string> end = key_end;
|
||||
removed_ranges.add_range(start, end);
|
||||
|
||||
// Mark keys in key_map as removed that fall within the removed range
|
||||
auto map_it = obj_map.lower_bound(key_begin);
|
||||
while (map_it != obj_map.end()) {
|
||||
if (map_it->first >= key_end) {
|
||||
break;
|
||||
}
|
||||
map_it->second.update_value(entry_iter->version, std::nullopt);
|
||||
++map_it;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ceph_abort_msg("Unknown OmapUpdateType");
|
||||
}
|
||||
}
|
||||
if (!removed_ranges.ranges.empty()) {
|
||||
removed_ranges_map[hoid].emplace_back(removed_ranges);
|
||||
}
|
||||
}
|
||||
entries.erase(hoid);
|
||||
}
|
||||
|
||||
bool ECOmapJournal::remove_processed_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry) {
|
||||
// Remove the header if version matches
|
||||
if (const auto header_it = header_map.find(hoid);
|
||||
header_it != header_map.end() && header_it->second.version == entry.version) {
|
||||
header_map.erase(header_it);
|
||||
}
|
||||
|
||||
// Remove key updates if version matches
|
||||
auto &obj_map = key_map[hoid];
|
||||
for (const auto & [type, update] : entry.omap_updates) {
|
||||
auto iter = update.cbegin();
|
||||
switch (type) {
|
||||
case OmapUpdateType::Insert: {
|
||||
std::map<std::string, ceph::buffer::list> vals;
|
||||
decode(vals, iter);
|
||||
for (auto val_it = vals.begin(); val_it != vals.end(); ++val_it) {
|
||||
const auto &key = val_it->first;
|
||||
auto key_it = obj_map.find(key);
|
||||
if (key_it != obj_map.end() &&
|
||||
key_it->second.version == entry.version) {
|
||||
obj_map.erase(key);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OmapUpdateType::Remove: {
|
||||
std::set<std::string> keys;
|
||||
decode(keys, iter);
|
||||
for (const auto &key : keys) {
|
||||
if (auto it = obj_map.find(key);
|
||||
it != obj_map.end() &&
|
||||
it->second.version == entry.version) {
|
||||
obj_map.erase(key);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OmapUpdateType::RemoveRange: {
|
||||
std::string key_begin, key_end;
|
||||
decode(key_begin, iter);
|
||||
decode(key_end, iter);
|
||||
auto map_it = obj_map.lower_bound(key_begin);
|
||||
while (map_it != obj_map.end()) {
|
||||
if (map_it->first >= key_end) {
|
||||
break;
|
||||
}
|
||||
if (map_it->second.version == entry.version) {
|
||||
map_it = obj_map.erase(map_it);
|
||||
} else {
|
||||
++map_it;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removed ranges if version matches
|
||||
if (const auto removed_ranges_it = removed_ranges_map.find(hoid);
|
||||
removed_ranges_it != removed_ranges_map.end()) {
|
||||
auto &removed_ranges_list = removed_ranges_it->second;
|
||||
for (auto rr_it = removed_ranges_list.begin(); rr_it != removed_ranges_list.end(); ++rr_it) {
|
||||
if (rr_it->version == entry.version) {
|
||||
removed_ranges_list.erase(rr_it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ECOmapJournal::remove_processed_entry_by_version(const hobject_t &hoid, const eversion_t version) {
|
||||
// Remove the header if version matches
|
||||
if (const auto header_it = header_map.find(hoid);
|
||||
header_it != header_map.end() && header_it->second.version == version) {
|
||||
header_map.erase(header_it);
|
||||
}
|
||||
|
||||
// Remove key updates if version matches
|
||||
auto key_map_it = key_map.find(hoid);
|
||||
if (key_map_it != key_map.end()) {
|
||||
for (auto it = key_map_it->second.begin(); it != key_map_it->second.end(); ) {
|
||||
if (it->second.version == version) {
|
||||
it = key_map_it->second.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removed ranges if version matches
|
||||
auto removed_ranges_it = removed_ranges_map.find(hoid);
|
||||
if (removed_ranges_it != removed_ranges_map.end()) {
|
||||
auto &removed_ranges_list = removed_ranges_it->second;
|
||||
for (auto rr_it = removed_ranges_list.begin(); rr_it != removed_ranges_list.end(); ++rr_it) {
|
||||
if (rr_it->version == version) {
|
||||
removed_ranges_list.erase(rr_it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ECOmapJournal::UpdateMapType ECOmapJournal::get_key_map(const hobject_t &hoid) const {
|
||||
if (const auto it = key_map.find(hoid); it != key_map.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
ECOmapJournal::RangeMapType ECOmapJournal::get_removed_ranges(const hobject_t &hoid) const {
|
||||
// Merge all removed ranges for the object
|
||||
RangeMapType merged_ranges;
|
||||
if (const auto it = removed_ranges_map.find(hoid);
|
||||
it != removed_ranges_map.end()) {
|
||||
for (const auto &rr : it->second) {
|
||||
for (const auto & [range_first, range_second] : rr.ranges) {
|
||||
// Add range to merged_ranges, merging overlapping ranges
|
||||
std::string start = range_first;
|
||||
std::optional<std::string> end = range_second;
|
||||
|
||||
// Find the range that starts after the current start
|
||||
auto map_it = merged_ranges.upper_bound(start);
|
||||
if (map_it != merged_ranges.begin()) {
|
||||
// Merge range to the left, if they overlap
|
||||
if (const auto prev = std::prev(map_it);
|
||||
!prev->second || *prev->second >= start) {
|
||||
start = prev->first;
|
||||
if (!end) {
|
||||
// end is already open ended so cannot be extended
|
||||
} else if (!prev->second) {
|
||||
end = std::nullopt;
|
||||
} else if (*prev->second > *end) {
|
||||
end = *prev->second;
|
||||
}
|
||||
merged_ranges.erase(prev);
|
||||
}
|
||||
}
|
||||
// Merge ranges to the right, if they overlap
|
||||
while (map_it != merged_ranges.end() &&
|
||||
(!end || map_it->first <= *end)) {
|
||||
if (!end) {
|
||||
// end is already open ended so cannot be extended
|
||||
} else if (!map_it->second) {
|
||||
end = std::nullopt;
|
||||
} else if (*map_it->second > *end) {
|
||||
end = map_it->second;
|
||||
}
|
||||
map_it = merged_ranges.erase(map_it);
|
||||
}
|
||||
merged_ranges.emplace_hint(map_it, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged_ranges;
|
||||
}
|
||||
|
||||
void ECOmapJournal::append_delete(
|
||||
const hobject_t &hoid,
|
||||
const version_t version,
|
||||
const bool lost_delete) {
|
||||
entries.erase(hoid);
|
||||
key_map.erase(hoid);
|
||||
removed_ranges_map.erase(hoid);
|
||||
header_map.erase(hoid);
|
||||
|
||||
auto [it, inserted] = object_state_map.try_emplace(hoid, std::map<version_t, bool>{});
|
||||
it->second.insert({version, lost_delete});
|
||||
|
||||
size_t total_versions = it->second.size();
|
||||
|
||||
ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
|
||||
<< " version=" << version
|
||||
<< " whiteout=" << lost_delete
|
||||
<< " total_versions=" << total_versions
|
||||
<< dendl;
|
||||
}
|
||||
|
||||
void ECOmapJournal::append_create(const hobject_t &hoid) {
|
||||
entries.erase(hoid);
|
||||
key_map.erase(hoid);
|
||||
removed_ranges_map.erase(hoid);
|
||||
header_map.erase(hoid);
|
||||
}
|
||||
|
||||
void ECOmapJournal::append_whiteout(const hobject_t &hoid) {
|
||||
entries.erase(hoid);
|
||||
key_map.erase(hoid);
|
||||
removed_ranges_map.erase(hoid);
|
||||
header_map.erase(hoid);
|
||||
}
|
||||
|
||||
void ECOmapJournal::trim_delete(const hobject_t &hoid, const version_t version) {
|
||||
// Capture whiteout value before erasing
|
||||
bool whiteout = false;
|
||||
if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
|
||||
if (const auto it2 = it->second.find(version); it2 != it->second.end()) {
|
||||
whiteout = it2->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
|
||||
std::map<version_t,bool>& versions = it->second;
|
||||
if (const auto it2 = versions.find(version); it2 != versions.end()) {
|
||||
versions.erase(it2);
|
||||
}
|
||||
if (versions.empty()) {
|
||||
object_state_map.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the total number of versions for this object after the operation
|
||||
size_t total_versions = 0;
|
||||
if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
|
||||
total_versions = it->second.size();
|
||||
}
|
||||
|
||||
ldpp_dout(&dpp, 20) << __func__ << ": hoid=" << hoid
|
||||
<< " version=" << version
|
||||
<< " whiteout=" << whiteout
|
||||
<< " total_versions=" << total_versions
|
||||
<< dendl;
|
||||
}
|
||||
|
||||
std::pair<gen_t, bool> ECOmapJournal::get_generation(const hobject_t &hoid) const {
|
||||
if (const auto it = object_state_map.find(hoid); it != object_state_map.end()) {
|
||||
if (const auto& versions = it->second; !versions.empty()) {
|
||||
const auto& [gen, lost] = *versions.begin();
|
||||
return {gen, lost};
|
||||
}
|
||||
}
|
||||
return {static_cast<gen_t>(ghobject_t::NO_GEN), false};
|
||||
}
|
||||
156
src/osd/ECOmapJournal.h
Normal file
156
src/osd/ECOmapJournal.h
Normal file
@ -0,0 +1,156 @@
|
||||
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:nil -*-
|
||||
// vim: ts=8 sw=2 sts=2 expandtab
|
||||
|
||||
/*
|
||||
* Ceph - scalable distributed file system
|
||||
*
|
||||
* Copyright (C) 2025 IBM
|
||||
*
|
||||
* This is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License version 2.1, as published by the Free Software
|
||||
* Foundation. See file COPYING.
|
||||
*/
|
||||
|
||||
/*
|
||||
* ECOmapJournal
|
||||
*
|
||||
* An in-memory journal to maintain OMAP consistency for Erasure Coded pools.
|
||||
*
|
||||
* Background:
|
||||
* Unlike Replicated pools which use a "roll-forward" recovery model, EC pools rely on a
|
||||
* "roll-back" mechanism for interrupted updates. To support efficient rollback without
|
||||
* incurring expensive read-before-write operations to generate rollback info, EC OMAP
|
||||
* updates are initially recorded only in the PG Log. They are applied to the underlying
|
||||
* ObjectStore only after the transaction is fully committed on all shards.
|
||||
*
|
||||
* The Problem:
|
||||
* This deferred application creates a latency window where the authoritative state exists
|
||||
* in the PG Log but the ObjectStore contains stale data. Reads served during this window
|
||||
* would return incorrect results.
|
||||
*
|
||||
* The Solution:
|
||||
* An ECOmapJournal tracks these "log-only" updates in memory. When an OMAP read occurs,
|
||||
* the backend fetches the base state from the ObjectStore and supplements it with the
|
||||
* updates stored in this journal. This ensures clients always receive the most up-to-date
|
||||
* result, merging the persistent state with the in-flight log state.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "include/buffer.h"
|
||||
|
||||
#include "osd_types.h"
|
||||
|
||||
struct eversion_t;
|
||||
|
||||
class ECOmapJournalEntry {
|
||||
public:
|
||||
eversion_t version;
|
||||
bool clear_omap;
|
||||
std::optional<ceph::buffer::list> omap_header;
|
||||
std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> omap_updates;
|
||||
|
||||
ECOmapJournalEntry(
|
||||
eversion_t version,
|
||||
bool clear_omap,
|
||||
std::optional<ceph::buffer::list> omap_header,
|
||||
std::vector<std::pair<OmapUpdateType, ceph::buffer::list>> omap_updates);
|
||||
};
|
||||
|
||||
class ECOmapValue {
|
||||
public:
|
||||
eversion_t version;
|
||||
std::optional<ceph::buffer::list> value;
|
||||
|
||||
ECOmapValue(
|
||||
const eversion_t version,
|
||||
const std::optional<ceph::buffer::list> &value)
|
||||
: version(version), value(value) {}
|
||||
|
||||
void update_value(eversion_t new_version, std::optional<ceph::buffer::list> new_value);
|
||||
};
|
||||
|
||||
class ECOmapRemovedRanges {
|
||||
public:
|
||||
eversion_t version;
|
||||
std::list<std::pair<std::string, std::optional<std::string>>> ranges;
|
||||
|
||||
explicit ECOmapRemovedRanges(const eversion_t version) : version(version) {}
|
||||
ECOmapRemovedRanges(
|
||||
const eversion_t version, std::list<std::pair<std::string,
|
||||
std::optional<std::string>>> ranges)
|
||||
: version(version), ranges(std::move(ranges)) {}
|
||||
|
||||
void add_range(const std::string& start, const std::optional<std::string>& end);
|
||||
void clear_omap();
|
||||
};
|
||||
|
||||
class ECOmapHeader {
|
||||
public:
|
||||
eversion_t version = eversion_t();
|
||||
std::optional<ceph::buffer::list> header = std::nullopt;
|
||||
|
||||
ECOmapHeader(const eversion_t version, std::optional<ceph::buffer::list> header)
|
||||
: version(version), header(std::move(header)) {}
|
||||
ECOmapHeader() = default;
|
||||
|
||||
void update_header(eversion_t new_version, std::optional<ceph::buffer::list> new_header);
|
||||
};
|
||||
|
||||
class ECOmapJournal {
|
||||
using UpdateMapType = std::map<std::string, ECOmapValue>;
|
||||
using RangeMapType = std::map<std::string, std::optional<std::string>>;
|
||||
using const_iterator = std::list<ECOmapJournalEntry>::const_iterator;
|
||||
private:
|
||||
// Unprocessed journal entries
|
||||
std::map<hobject_t, std::list<ECOmapJournalEntry>> entries;
|
||||
|
||||
// Processed journal entries
|
||||
std::map<hobject_t, std::map<std::string, ECOmapValue>> key_map;
|
||||
std::map<hobject_t, std::list<ECOmapRemovedRanges>> removed_ranges_map;
|
||||
std::map<hobject_t, ECOmapHeader> header_map;
|
||||
|
||||
// Contains the set of versions and lost object booleans corresponding to
|
||||
// outstanding deletes for that ob
|
||||
std::map<hobject_t, std::map<version_t, bool>> object_state_map;
|
||||
|
||||
const DoutPrefixProvider& dpp;
|
||||
|
||||
// Function to get specific object's unprocessed entries
|
||||
std::list<ECOmapJournalEntry>& get_entries(const hobject_t &hoid);
|
||||
std::list<ECOmapJournalEntry> snapshot_entries(const hobject_t &hoid) const;
|
||||
|
||||
void process_entries(const hobject_t &hoid);
|
||||
bool remove_processed_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry);
|
||||
bool remove_processed_entry_by_version(const hobject_t &hoid, const eversion_t version);
|
||||
UpdateMapType get_key_map(const hobject_t &hoid) const;
|
||||
RangeMapType get_removed_ranges(const hobject_t &hoid) const;
|
||||
|
||||
public:
|
||||
explicit ECOmapJournal(const DoutPrefixProvider& dpp_) : dpp(dpp_) {}
|
||||
|
||||
void add_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry);
|
||||
bool remove_entry(const hobject_t &hoid, const ECOmapJournalEntry &entry);
|
||||
bool remove_entry_by_version(const hobject_t &hoid, const eversion_t version);
|
||||
void clear(const hobject_t &hoid);
|
||||
void clear_all();
|
||||
[[nodiscard]] std::size_t entries_size(const hobject_t &hoid) const;
|
||||
[[nodiscard]] bool has_unprocessed_entries(const hobject_t &hoid) const;
|
||||
[[nodiscard]] bool has_omap_updates(const hobject_t &hoid) const;
|
||||
std::tuple<UpdateMapType, RangeMapType> get_value_updates(const hobject_t &hoid);
|
||||
std::optional<ceph::buffer::list> get_updated_header(const hobject_t &hoid);
|
||||
void append_delete(const hobject_t &hoid, const version_t version, const bool lost_delete);
|
||||
void append_create(const hobject_t &hoid);
|
||||
void append_whiteout(const hobject_t &hoid);
|
||||
void trim_delete(const hobject_t &hoid, const version_t version);
|
||||
std::pair<gen_t, bool> get_generation(const hobject_t &hoid) const;
|
||||
|
||||
[[nodiscard]] const_iterator begin_entries(const hobject_t &hoid) const;
|
||||
[[nodiscard]] const_iterator end_entries(const hobject_t &hoid) const;
|
||||
};
|
||||
@ -137,7 +137,6 @@ public:
|
||||
|
||||
std::map<std::string, std::optional<ceph::buffer::list> > attr_updates;
|
||||
|
||||
enum class OmapUpdateType {Remove, Insert, RemoveRange};
|
||||
std::vector<std::pair<OmapUpdateType, ceph::buffer::list> > omap_updates;
|
||||
|
||||
std::optional<ceph::buffer::list> omap_header;
|
||||
@ -455,7 +454,7 @@ public:
|
||||
auto &op = get_object_op_for_modify(hoid);
|
||||
op.omap_updates.emplace_back(
|
||||
std::make_pair(
|
||||
ObjectOperation::OmapUpdateType::Insert,
|
||||
OmapUpdateType::Insert,
|
||||
keys_bl));
|
||||
}
|
||||
void omap_setkeys(
|
||||
@ -474,7 +473,7 @@ public:
|
||||
auto &op = get_object_op_for_modify(hoid);
|
||||
op.omap_updates.emplace_back(
|
||||
std::make_pair(
|
||||
ObjectOperation::OmapUpdateType::Remove,
|
||||
OmapUpdateType::Remove,
|
||||
keys_bl));
|
||||
}
|
||||
void omap_rmkeys(
|
||||
@ -493,7 +492,7 @@ public:
|
||||
auto &op = get_object_op_for_modify(hoid);
|
||||
op.omap_updates.emplace_back(
|
||||
std::make_pair(
|
||||
ObjectOperation::OmapUpdateType::RemoveRange,
|
||||
OmapUpdateType::RemoveRange,
|
||||
range_bl));
|
||||
}
|
||||
void omap_rmkeyrange(
|
||||
|
||||
@ -456,15 +456,14 @@ void generate_transaction(
|
||||
t->omap_setheader(coll, goid, *(op.omap_header));
|
||||
|
||||
for (auto &&up: op.omap_updates) {
|
||||
using UpdateType = PGTransaction::ObjectOperation::OmapUpdateType;
|
||||
switch (up.first) {
|
||||
case UpdateType::Remove:
|
||||
case OmapUpdateType::Remove:
|
||||
t->omap_rmkeys(coll, goid, up.second);
|
||||
break;
|
||||
case UpdateType::Insert:
|
||||
case OmapUpdateType::Insert:
|
||||
t->omap_setkeys(coll, goid, up.second);
|
||||
break;
|
||||
case UpdateType::RemoveRange:
|
||||
case OmapUpdateType::RemoveRange:
|
||||
t->omap_rmkeyrange(coll, goid, up.second);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -145,6 +145,8 @@ typedef interval_set<
|
||||
using shard_id_set = bitset_set<128, shard_id_t>;
|
||||
WRITE_CLASS_DENC(shard_id_set)
|
||||
|
||||
enum class OmapUpdateType : uint8_t {Remove, Insert, RemoveRange};
|
||||
|
||||
/**
|
||||
* osd request identifier
|
||||
*
|
||||
|
||||
@ -215,10 +215,18 @@ target_link_libraries(unittest_mclock_scheduler
|
||||
global osd dmclock os
|
||||
)
|
||||
|
||||
# unittest ECOmapJournal
|
||||
add_executable(unittest_ec_omap_journal
|
||||
test_ec_omap_journal.cc
|
||||
)
|
||||
add_ceph_unittest(unittest_ec_omap_journal)
|
||||
target_link_libraries(unittest_ec_omap_journal osd global ${BLKID_LIBRARIES})
|
||||
|
||||
# osd_unittests: custom target that builds and runs all OSD unit tests
|
||||
# Not including unittest_osdmap, as it is slow. It is tested elsewhere.
|
||||
set(OSD_UNITTESTS
|
||||
unittest_backend_basics
|
||||
unittest_ec_omap_journal
|
||||
unittest_ec_transaction
|
||||
unittest_ec_transaction_l
|
||||
unittest_ecbackend
|
||||
|
||||
1427
src/test/osd/test_ec_omap_journal.cc
Normal file
1427
src/test/osd/test_ec_omap_journal.cc
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user