osd: Introduce optimized EC

Signed-off-by: Alex Ainscow <aainscow@uk.ibm.com>
This commit is contained in:
Alex Ainscow 2025-04-03 14:47:28 +01:00
parent df60540889
commit 9e2841ab16
20 changed files with 9113 additions and 4191 deletions

View File

@ -48,7 +48,7 @@ set(osd_srcs
ECUtilL.cc
ECCommon.cc
ECBackend.cc
ExtentCache.cc
ECExtentCache.cc
ECTransaction.cc
ECUtil.cc
ECInject.cc

File diff suppressed because it is too large Load Diff

View File

@ -14,22 +14,23 @@
#pragma once
#include <boost/intrusive/set.hpp>
#include <boost/intrusive/list.hpp>
#include <boost/intrusive/set.hpp>
#include "ECCommon.h"
#include "ECExtentCache.h"
#include "ECListener.h"
#include "ECTypes.h"
#include "ECUtil.h"
#include "OSD.h"
#include "PGBackend.h"
#include "erasure-code/ErasureCodeInterface.h"
#include "ECUtil.h"
#include "ECTransaction.h"
#include "ExtentCache.h"
#include "ECListener.h"
#include "include/buffer.h"
#include "osd/scrubber/scrub_backend.h"
/* This file is soon going to be replaced (before next release), so we are going
* to simply ignore all deprecated warnings.
* */
IGNORE_DEPRECATED
//forward declaration
struct ECSubWrite;
@ -39,63 +40,61 @@ struct ECSubReadReply;
class ECSwitch;
struct RecoveryMessages;
class ECSwitch;
class ECBackend : public ECCommon {
public:
PGBackend::RecoveryHandle *open_recovery_op();
public:
PGBackend::RecoveryHandle *open_recovery_op();
void run_recovery_op(
PGBackend::RecoveryHandle *h,
int priority
);
int priority
);
int recover_object(
const hobject_t &hoid,
eversion_t v,
ObjectContextRef head,
ObjectContextRef obc,
const hobject_t &hoid,
eversion_t v,
ObjectContextRef head,
ObjectContextRef obc,
PGBackend::RecoveryHandle *h
);
);
bool _handle_message(
OpRequestRef op
);
bool can_handle_while_inactive(
OpRequestRef op
);
bool _handle_message(OpRequestRef op);
bool can_handle_while_inactive(OpRequestRef op);
friend struct SubWriteApplied;
friend struct SubWriteCommitted;
void sub_write_committed(
ceph_tid_t tid,
eversion_t version,
eversion_t last_complete,
const ZTracer::Trace &trace);
ceph_tid_t tid,
eversion_t version,
eversion_t last_complete,
const ZTracer::Trace &trace
);
void handle_sub_write(
pg_shard_t from,
OpRequestRef msg,
ECSubWrite &op,
const ZTracer::Trace &trace,
ECListener& eclistener
pg_shard_t from,
OpRequestRef msg,
ECSubWrite &op,
const ZTracer::Trace &trace,
ECListener &eclistener
) override;
void handle_sub_read(
pg_shard_t from,
const ECSubRead &op,
ECSubReadReply *reply,
const ZTracer::Trace &trace
pg_shard_t from,
const ECSubRead &op,
ECSubReadReply *reply,
const ZTracer::Trace &trace
);
void handle_sub_write_reply(
pg_shard_t from,
const ECSubWriteReply &op,
const ZTracer::Trace &trace
pg_shard_t from,
const ECSubWriteReply &op,
const ZTracer::Trace &trace
);
void handle_sub_read_reply(
pg_shard_t from,
ECSubReadReply &op,
const ZTracer::Trace &trace
pg_shard_t from,
ECSubReadReply &op,
const ZTracer::Trace &trace
);
/// @see ReadOp below
void check_recovery_sources(const OSDMapRef& osdmap);
void check_recovery_sources(const OSDMapRef &osdmap);
void on_change();
void clear_recovery_state();
@ -107,26 +106,27 @@ public:
}
void submit_transaction(
const hobject_t &hoid,
const object_stat_sum_t &delta_stats,
const eversion_t &at_version,
PGTransactionUPtr &&t,
const eversion_t &trim_to,
const eversion_t &pg_committed_to,
std::vector<pg_log_entry_t>&& log_entries,
std::optional<pg_hit_set_history_t> &hset_history,
Context *on_all_commit,
ceph_tid_t tid,
osd_reqid_t reqid,
OpRequestRef op
const hobject_t &hoid,
const object_stat_sum_t &delta_stats,
const eversion_t &at_version,
PGTransactionUPtr &&t,
const eversion_t &trim_to,
const eversion_t &pg_committed_to,
std::vector<pg_log_entry_t> &&log_entries,
std::optional<pg_hit_set_history_t> &hset_history,
Context *on_all_commit,
ceph_tid_t tid,
osd_reqid_t reqid,
OpRequestRef op
);
int objects_read_sync(
const hobject_t &hoid,
uint64_t off,
uint64_t len,
uint32_t op_flags,
ceph::buffer::list *bl);
const hobject_t &hoid,
uint64_t off,
uint64_t len,
uint32_t op_flags,
ceph::buffer::list *bl
);
/**
* Async read mechanism
@ -147,19 +147,35 @@ public:
* check_recovery_sources.
*/
void objects_read_and_reconstruct(
const std::map<hobject_t, std::list<ec_align_t>> &reads,
bool fast_read,
GenContextURef<ECCommon::ec_extents_t &&> &&func) override;
const std::map<hobject_t, std::list<ec_align_t>> &reads,
bool fast_read,
uint64_t object_size,
GenContextURef<ECCommon::ec_extents_t&&> &&func
) override;
/**
* Async read mechanism for read-modify-write (RMW) code paths. Here wthe
* client already knows the set of shard reads that are required, so these
* can be passed in directly. The "fast_read" mechanism is not needed.
*
* Otherwise this is the same as objects_read_and_reconstruct.
*/
void objects_read_and_reconstruct_for_rmw(
std::map<hobject_t, read_request_t> &&reads,
GenContextURef<ECCommon::ec_extents_t&&> &&func
) override;
void objects_read_async(
const hobject_t &hoid,
uint64_t object_size,
const std::list<std::pair<ec_align_t,
std::pair<ceph::buffer::list*, Context*>>> &to_read,
Context *on_complete,
bool fast_read = false);
const hobject_t &hoid,
uint64_t object_size,
const std::list<std::pair<ec_align_t,
std::pair<ceph::buffer::list*, Context*>>> &
to_read,
Context *on_complete,
bool fast_read = false
);
private:
private:
friend struct ECRecoveryHandle;
void kick_reads();
@ -194,159 +210,185 @@ private:
* Transaction, and reads in a RecoveryMessages object which is passed
* among the recovery methods.
*/
public:
public:
struct RecoveryBackend {
CephContext* cct;
CephContext *cct;
const coll_t &coll;
ceph::ErasureCodeInterfaceRef ec_impl;
const ECUtil::stripe_info_t& sinfo;
ReadPipeline& read_pipeline;
UnstableHashInfoRegistry& unstable_hashinfo_registry;
const ECUtil::stripe_info_t &sinfo;
ReadPipeline &read_pipeline;
UnstableHashInfoRegistry &unstable_hashinfo_registry;
// TODO: lay an interface down here
ECListener* parent;
ECBackend* ecbackend;
ECListener *parent;
ECBackend *ecbackend;
ECListener *get_parent() const { return parent; }
const OSDMapRef& get_osdmap() const { return get_parent()->pgb_get_osdmap(); }
epoch_t get_osdmap_epoch() const { return get_parent()->pgb_get_osdmap_epoch(); }
const OSDMapRef &get_osdmap() const {
return get_parent()->pgb_get_osdmap();
}
epoch_t get_osdmap_epoch() const {
return get_parent()->pgb_get_osdmap_epoch();
}
const pg_info_t &get_info() { return get_parent()->get_info(); }
void add_temp_obj(const hobject_t &oid) { get_parent()->add_temp_obj(oid); }
void clear_temp_obj(const hobject_t &oid) { get_parent()->clear_temp_obj(oid); }
RecoveryBackend(CephContext* cct,
const coll_t &coll,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t& sinfo,
ReadPipeline& read_pipeline,
UnstableHashInfoRegistry& unstable_hashinfo_registry,
ECListener* parent,
ECBackend* ecbackend);
struct RecoveryOp {
hobject_t hoid;
eversion_t v;
std::set<pg_shard_t> missing_on;
std::set<shard_id_t> missing_on_shards;
void clear_temp_obj(const hobject_t &oid) {
get_parent()->clear_temp_obj(oid);
}
ObjectRecoveryInfo recovery_info;
ObjectRecoveryProgress recovery_progress;
RecoveryBackend(CephContext *cct,
const coll_t &coll,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t &sinfo,
ReadPipeline &read_pipeline,
UnstableHashInfoRegistry &unstable_hashinfo_registry,
ECListener *parent,
ECBackend *ecbackend);
enum state_t { IDLE, READING, WRITING, COMPLETE } state;
struct RecoveryOp {
hobject_t hoid;
eversion_t v;
std::set<pg_shard_t> missing_on;
std::set<shard_id_t> missing_on_shards;
static const char* tostr(state_t state) {
switch (state) {
case RecoveryOp::IDLE:
return "IDLE";
case RecoveryOp::READING:
return "READING";
case RecoveryOp::WRITING:
return "WRITING";
case RecoveryOp::COMPLETE:
return "COMPLETE";
default:
ceph_abort();
return "";
ObjectRecoveryInfo recovery_info;
ObjectRecoveryProgress recovery_progress;
enum state_t { IDLE, READING, WRITING, COMPLETE } state;
static const char *tostr(state_t state) {
switch (state) {
case RecoveryOp::IDLE:
return "IDLE";
case RecoveryOp::READING:
return "READING";
case RecoveryOp::WRITING:
return "WRITING";
case RecoveryOp::COMPLETE:
return "COMPLETE";
default:
ceph_abort();
return "";
}
}
// must be filled if state == WRITING
std::optional<ECUtil::shard_extent_map_t> returned_data;
std::map<std::string, ceph::buffer::list, std::less<>> xattrs;
ECUtil::HashInfoRef hinfo;
ObjectContextRef obc;
std::set<pg_shard_t> waiting_on_pushes;
void dump(ceph::Formatter *f) const;
RecoveryOp() : state(IDLE) {}
void print(std::ostream &os) const {
os << "RecoveryOp("
<< "hoid=" << hoid
<< " v=" << v
<< " missing_on=" << missing_on
<< " missing_on_shards=" << missing_on_shards
<< " recovery_info=" << recovery_info
<< " recovery_progress=" << recovery_progress
<< " obc refcount=" << obc.use_count()
<< " state=" << ECBackend::RecoveryBackend::RecoveryOp::tostr(state)
<< " waiting_on_pushes=" << waiting_on_pushes
<< ")";
}
};
std::map<hobject_t, RecoveryOp> recovery_ops;
uint64_t get_recovery_chunk_size() const {
return round_up_to(cct->_conf->osd_recovery_max_chunk,
sinfo.get_stripe_width());
}
// must be filled if state == WRITING
std::map<int, ceph::buffer::list> returned_data;
std::map<std::string, ceph::buffer::list, std::less<>> xattrs;
ECUtil::HashInfoRef hinfo;
ObjectContextRef obc;
std::set<pg_shard_t> waiting_on_pushes;
virtual ~RecoveryBackend() = default;
virtual void commit_txn_send_replies(
ceph::os::Transaction &&txn,
std::map<int, MOSDPGPushReply*> replies) = 0;
void dispatch_recovery_messages(RecoveryMessages &m, int priority);
// valid in state READING
std::pair<uint64_t, uint64_t> extent_requested;
void dump(ceph::Formatter *f) const;
RecoveryOp() : state(IDLE) {}
PGBackend::RecoveryHandle *open_recovery_op();
void run_recovery_op(
struct ECRecoveryHandle &h,
int priority);
int recover_object(
const hobject_t &hoid,
eversion_t v,
ObjectContextRef head,
ObjectContextRef obc,
PGBackend::RecoveryHandle *h);
void continue_recovery_op(
RecoveryBackend::RecoveryOp &op,
RecoveryMessages *m);
void handle_recovery_read_complete(
const hobject_t &hoid,
ECUtil::shard_extent_map_t &&buffers_read,
std::optional<std::map<std::string, ceph::buffer::list, std::less<>>>
attrs,
const ECUtil::shard_extent_set_t &want_to_read,
RecoveryMessages *m);
void handle_recovery_push(
const PushOp &op,
RecoveryMessages *m,
bool is_repair);
void handle_recovery_push_reply(
const PushReplyOp &op,
pg_shard_t from,
RecoveryMessages *m);
friend struct RecoveryMessages;
void _failed_push(const hobject_t &hoid, ECCommon::read_result_t &res);
};
friend ostream &operator<<(ostream &lhs, const RecoveryOp &rhs);
std::map<hobject_t, RecoveryOp> recovery_ops;
uint64_t get_recovery_chunk_size() const {
return round_up_to(cct->_conf->osd_recovery_max_chunk,
sinfo.get_stripe_width());
}
virtual ~RecoveryBackend() = default;
virtual void commit_txn_send_replies(
ceph::os::Transaction&& txn,
std::map<int, MOSDPGPushReply*> replies) = 0;
void dispatch_recovery_messages(RecoveryMessages &m, int priority);
PGBackend::RecoveryHandle *open_recovery_op();
void run_recovery_op(
struct ECRecoveryHandle &h,
int priority);
int recover_object(
const hobject_t &hoid,
eversion_t v,
ObjectContextRef head,
ObjectContextRef obc,
PGBackend::RecoveryHandle *h);
void continue_recovery_op(
RecoveryBackend::RecoveryOp &op,
RecoveryMessages *m);
void handle_recovery_read_complete(
const hobject_t &hoid,
boost::tuple<uint64_t, uint64_t, std::map<pg_shard_t, ceph::buffer::list> > &to_read,
std::optional<std::map<std::string, ceph::buffer::list, std::less<>> > attrs,
RecoveryMessages *m);
void handle_recovery_push(
const PushOp &op,
RecoveryMessages *m,
bool is_repair);
void handle_recovery_push_reply(
const PushReplyOp &op,
pg_shard_t from,
RecoveryMessages *m);
friend struct RecoveryMessages;
int get_ec_data_chunk_count() const {
return ec_impl->get_data_chunk_count();
}
void _failed_push(const hobject_t &hoid, ECCommon::read_result_t &res);
};
struct ECRecoveryBackend : RecoveryBackend {
ECRecoveryBackend(CephContext* cct,
const coll_t &coll,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t& sinfo,
ReadPipeline& read_pipeline,
UnstableHashInfoRegistry& unstable_hashinfo_registry,
PGBackend::Listener* parent,
ECBackend* ecbackend)
: RecoveryBackend(cct, coll, std::move(ec_impl), sinfo, read_pipeline, unstable_hashinfo_registry, parent->get_eclistener(), ecbackend),
parent(parent) {
}
ECRecoveryBackend(CephContext *cct,
const coll_t &coll,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t &sinfo,
ReadPipeline &read_pipeline,
UnstableHashInfoRegistry &unstable_hashinfo_registry,
PGBackend::Listener *parent,
ECBackend *ecbackend)
: RecoveryBackend(cct, coll, std::move(ec_impl), sinfo, read_pipeline,
unstable_hashinfo_registry, parent->get_eclistener(),
ecbackend),
parent(parent) {}
void commit_txn_send_replies(
ceph::os::Transaction&& txn,
std::map<int, MOSDPGPushReply*> replies) override;
ceph::os::Transaction &&txn,
std::map<int, MOSDPGPushReply*> replies) override;
PGBackend::Listener *get_parent() const { return parent; }
private:
private:
PGBackend::Listener *parent;
};
friend ostream &operator<<(ostream &lhs, const RecoveryBackend::RecoveryOp &rhs);
friend ostream &operator<<(ostream &lhs,
const RecoveryBackend::RecoveryOp &rhs
);
friend struct RecoveryMessages;
friend struct OnRecoveryReadComplete;
friend struct RecoveryReadCompleter;
void handle_recovery_push(
const PushOp &op,
RecoveryMessages *m,
bool is_repair);
const PushOp &op,
RecoveryMessages *m,
bool is_repair
);
public:
PGBackend::Listener *parent;
CephContext *cct;
ECSwitch *switcher;
struct ReadPipeline read_pipeline;
struct RMWPipeline rmw_pipeline;
struct ECRecoveryBackend recovery_backend;
PGBackend::Listener *parent;
CephContext *cct;
ECSwitch *switcher;
ReadPipeline read_pipeline;
RMWPipeline rmw_pipeline;
ECRecoveryBackend recovery_backend;
ceph::ErasureCodeInterfaceRef ec_impl;
@ -355,46 +397,55 @@ public:
/**
* ECRecPred
*
* Determines the whether _have is sufficient to recover an object
* Determines whether _have is sufficient to recover an object
*/
class ECRecPred : public IsPGRecoverablePredicate {
std::set<int> want;
shard_id_set want;
const ECUtil::stripe_info_t *sinfo;
ceph::ErasureCodeInterfaceRef ec_impl;
public:
explicit ECRecPred(ceph::ErasureCodeInterfaceRef ec_impl) : ec_impl(ec_impl) {
for (unsigned i = 0; i < ec_impl->get_chunk_count(); ++i) {
want.insert(i);
}
}
bool operator()(const std::set<pg_shard_t> &_have) const override {
std::set<int> have;
for (std::set<pg_shard_t>::const_iterator i = _have.begin();
i != _have.end();
++i) {
have.insert(static_cast<int>(i->shard));
}
std::map<int, std::vector<std::pair<int, int>>> min;
return ec_impl->minimum_to_decode(want, have, &min) == 0;
public:
explicit ECRecPred(const ECUtil::stripe_info_t *sinfo,
ceph::ErasureCodeInterfaceRef ec_impl) :
sinfo(sinfo), ec_impl(ec_impl) {
want.insert_range(shard_id_t(0), sinfo->get_k_plus_m());
}
bool operator()(const std::set<pg_shard_t> &_have) const override {
shard_id_set have;
for (pg_shard_t p: _have) {
have.insert(p.shard);
}
std::unique_ptr<shard_id_map<std::vector<std::pair<int, int>>>>
min_sub_chunks = nullptr;
if (sinfo->supports_sub_chunks()) {
min_sub_chunks = std::make_unique<shard_id_map<std::vector<std::pair<
int, int>>>>(sinfo->get_k_plus_m());
}
shard_id_set min;
return ec_impl->minimum_to_decode(want, have, min, min_sub_chunks.get())
== 0;
}
};
std::unique_ptr<ECRecPred> get_is_recoverable_predicate() const {
return std::make_unique<ECRecPred>(ec_impl);
return std::make_unique<ECRecPred>(&sinfo, ec_impl);
}
unsigned get_ec_data_chunk_count() const {
return ec_impl->get_data_chunk_count();
unsigned get_ec_data_chunk_count() const {
return sinfo.get_k();
}
int get_ec_stripe_chunk_size() const {
return sinfo.get_chunk_size();
}
uint64_t object_size_to_shard_size(const uint64_t size,
shard_id_t shard) const {
if (size == std::numeric_limits<uint64_t>::max()) {
return size;
}
return sinfo.logical_to_next_chunk_offset(size);
uint64_t object_size_to_shard_size(const uint64_t size, shard_id_t shard
) const {
return sinfo.object_size_to_shard_size(size, shard);
}
/**
* ECReadPred
*
@ -403,16 +454,21 @@ public:
class ECReadPred : public IsPGReadablePredicate {
pg_shard_t whoami;
ECRecPred rec_pred;
public:
public:
ECReadPred(
pg_shard_t whoami,
ceph::ErasureCodeInterfaceRef ec_impl) : whoami(whoami), rec_pred(ec_impl) {}
pg_shard_t whoami,
const ECUtil::stripe_info_t *sinfo,
ceph::ErasureCodeInterfaceRef ec_impl) : whoami(whoami), rec_pred(sinfo, ec_impl) {}
bool operator()(const std::set<pg_shard_t> &_have) const override {
return _have.count(whoami) && rec_pred(_have);
}
};
std::unique_ptr<ECReadPred> get_is_readable_predicate(pg_shard_t whoami) const {
return std::make_unique<ECReadPred>(whoami, ec_impl);
std::unique_ptr<ECReadPred>
get_is_readable_predicate(pg_shard_t whoami) const {
return std::make_unique<ECReadPred>(whoami, &sinfo, ec_impl);
}
const ECUtil::stripe_info_t sinfo;
@ -424,34 +480,41 @@ public:
int,
std::map<std::string, ceph::bufferlist, std::less<>>,
size_t
> get_attrs_n_size_from_disk(const hobject_t& hoid);
> get_attrs_n_size_from_disk(const hobject_t &hoid);
public:
int object_stat(const hobject_t &hoid, struct stat* st);
ECUtil::HashInfoRef get_hinfo_from_disk(hobject_t oid);
std::optional<object_info_t> get_object_info_from_obc(
ObjectContextRef &obc_map
);
public:
int object_stat(const hobject_t &hoid, struct stat *st);
ECBackend(
PGBackend::Listener *pg,
CephContext *cct,
ceph::ErasureCodeInterfaceRef ec_impl,
uint64_t stripe_width,
ECSwitch *s,
ECExtentCache::LRU &ignored);
PGBackend::Listener *pg,
CephContext *cct,
ceph::ErasureCodeInterfaceRef ec_impl,
uint64_t stripe_width,
ECSwitch *s,
ECExtentCache::LRU &ec_extent_cache_lru
);
int objects_get_attrs(
const hobject_t &hoid,
std::map<std::string, ceph::buffer::list, std::less<>> *out);
const hobject_t &hoid,
std::map<std::string, ceph::buffer::list, std::less<>> *out
);
bool auto_repair_supported() const { return true; }
int be_deep_scrub(
const hobject_t &poid,
ScrubMap &map,
ScrubMapBuilder &pos,
ScrubMap::object &o);
const hobject_t &poid,
ScrubMap &map,
ScrubMapBuilder &pos,
ScrubMap::object &o
);
uint64_t be_get_ondisk_size(uint64_t logical_size, shard_id_t ignored) const {
return sinfo.logical_to_next_chunk_offset(logical_size);
uint64_t be_get_ondisk_size(uint64_t logical_size, shard_id_t shard_id
) const {
return object_size_to_shard_size(logical_size, shard_id);
}
};
ostream &operator<<(ostream &lhs, const ECBackend::RMWPipeline::pipeline_state_t &rhs);
END_IGNORE_DEPRECATED

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@
#include "ECUtil.h"
#include "ECTypes.h"
#if WITH_CRIMSON
#include "ExtentCache.h"
#include "crimson/osd/object_context.h"
#include "os/Transaction.h"
#include "osd/OSDMap.h"
@ -32,7 +31,7 @@ struct ECTransaction {
struct WritePlan {
bool invalidates_cache = false; // Yes, both are possible
std::map<hobject_t,extent_set> to_read;
std::map<hobject_t,extent_set> will_write; // superset of to_read
std::map<hobject_t,extent_set> will_write;
std::map<hobject_t,ECUtil::HashInfoRef> hash_infos;
};
@ -45,8 +44,9 @@ typedef crimson::osd::ObjectContextRef ObjectContextRef;
#endif
#include "ECTransaction.h"
#include "ExtentCache.h"
#include "ECExtentCache.h"
#include "ECListener.h"
#include "common/dout.h"
//forward declaration
struct ECSubWrite;
@ -56,36 +56,86 @@ struct ECCommon {
struct ec_extent_t {
int err;
extent_map emap;
ECUtil::shard_extent_map_t shard_extent_map;
void print(std::ostream &os) const {
os << err << "," << emap;
}
};
friend std::ostream &operator<<(std::ostream &lhs, const ec_extent_t &rhs);
using ec_extents_t = std::map<hobject_t, ec_extent_t>;
virtual ~ECCommon() = default;
virtual void handle_sub_write(
pg_shard_t from,
OpRequestRef msg,
ECSubWrite &op,
const ZTracer::Trace &trace,
ECListener& eclistener
) = 0;
pg_shard_t from,
OpRequestRef msg,
ECSubWrite &op,
const ZTracer::Trace &trace,
ECListener &eclistener) = 0;
virtual void objects_read_and_reconstruct(
const std::map<hobject_t, std::list<ec_align_t>> &reads,
bool fast_read,
GenContextURef<ec_extents_t &&> &&func) = 0;
const std::map<hobject_t, std::list<ec_align_t>> &reads,
bool fast_read,
uint64_t object_size,
GenContextURef<ec_extents_t&&> &&func) = 0;
struct shard_read_t {
extent_set extents;
std::optional<std::vector<std::pair<int, int>>> subchunk;
pg_shard_t pg_shard;
bool operator==(const shard_read_t &other) const;
void print(std::ostream &os) const {
os << "shard_read_t(extents=[" << extents << "]"
<< ", subchunk=" << subchunk
<< ", pg_shard=" << pg_shard
<< ")";
}
};
struct read_request_t {
const std::list<ec_align_t> to_read;
std::map<pg_shard_t, std::vector<std::pair<int, int>>> need;
bool want_attrs;
const uint32_t flags = 0;
const ECUtil::shard_extent_set_t shard_want_to_read;
shard_id_map<shard_read_t> shard_reads;
bool want_attrs = false;
uint64_t object_size;
read_request_t(
const std::list<ec_align_t> &to_read,
const std::map<pg_shard_t, std::vector<std::pair<int, int>>> &need,
bool want_attrs)
: to_read(to_read), need(need), want_attrs(want_attrs) {}
const std::list<ec_align_t> &to_read,
const ECUtil::shard_extent_set_t &shard_want_to_read,
bool want_attrs, uint64_t object_size) :
to_read(to_read),
flags(to_read.front().flags),
shard_want_to_read(shard_want_to_read),
shard_reads(shard_want_to_read.get_max_shards()),
want_attrs(want_attrs),
object_size(object_size) {}
read_request_t(const ECUtil::shard_extent_set_t &shard_want_to_read,
bool want_attrs, uint64_t object_size) :
shard_want_to_read(shard_want_to_read),
shard_reads(shard_want_to_read.get_max_shards()),
want_attrs(want_attrs),
object_size(object_size) {}
bool operator==(const read_request_t &other) const;
void print(std::ostream &os) const {
os << "read_request_t(to_read=[" << to_read << "]"
<< ", flags=" << flags
<< ", shard_want_to_read=" << shard_want_to_read
<< ", shard_reads=" << shard_reads
<< ", want_attrs=" << want_attrs
<< ")";
}
};
friend std::ostream &operator<<(std::ostream &lhs, const read_request_t &rhs);
virtual void objects_read_and_reconstruct_for_rmw(
std::map<hobject_t, read_request_t> &&to_read,
GenContextURef<ec_extents_t&&> &&func) = 0;
struct ReadOp;
/**
* Low level async read mechanism
@ -111,19 +161,30 @@ struct ECCommon {
struct read_result_t {
int r;
std::map<pg_shard_t, int> errors;
std::optional<std::map<std::string, ceph::buffer::list, std::less<>> > attrs;
std::list<
boost::tuple<
uint64_t, uint64_t, std::map<pg_shard_t, ceph::buffer::list> > > returned;
read_result_t() : r(0) {}
std::optional<std::map<std::string, ceph::buffer::list, std::less<>>> attrs;
ECUtil::shard_extent_map_t buffers_read;
ECUtil::shard_extent_set_t processed_read_requests;
read_result_t(const ECUtil::stripe_info_t *sinfo) :
r(0), buffers_read(sinfo),
processed_read_requests(sinfo->get_k_plus_m()) {}
void print(std::ostream &os) const {
os << "read_result_t(r=" << r << ", errors=" << errors;
if (attrs) {
os << ", attrs=" << *(attrs);
} else {
os << ", noattrs";
}
os << ", buffers_read=" << buffers_read << ")";
}
};
struct ReadCompleter {
virtual void finish_single_request(
const hobject_t &hoid,
read_result_t &res,
std::list<ec_align_t> to_read,
std::set<int> wanted_to_read) = 0;
const hobject_t &hoid,
read_result_t &&res,
ECCommon::read_request_t &req) = 0;
virtual void finish(int priority) && = 0;
@ -131,26 +192,35 @@ struct ECCommon {
};
friend struct CallClientContexts;
struct ClientAsyncReadStatus {
unsigned objects_to_read;
GenContextURef<ec_extents_t &&> func;
GenContextURef<ec_extents_t&&> func;
ec_extents_t results;
explicit ClientAsyncReadStatus(
unsigned objects_to_read,
GenContextURef<ec_extents_t &&> &&func)
unsigned objects_to_read,
GenContextURef<ec_extents_t&&> &&func)
: objects_to_read(objects_to_read), func(std::move(func)) {}
void complete_object(
const hobject_t &hoid,
int err,
extent_map &&buffers) {
const hobject_t &hoid,
int err,
extent_map &&buffers,
ECUtil::shard_extent_map_t &&shard_extent_map) {
ceph_assert(objects_to_read);
--objects_to_read;
ceph_assert(!results.count(hoid));
results.emplace(hoid, ec_extent_t{err, std::move(buffers)});
ceph_assert(!results.contains(hoid));
results.emplace(hoid, ec_extent_t{
err, std::move(buffers),
std::move(shard_extent_map)
});
}
bool is_complete() const {
return objects_to_read == 0;
}
void run() {
func.release()->complete(std::move(results));
}
@ -171,113 +241,125 @@ struct ECCommon {
ZTracer::Trace trace;
std::map<hobject_t, std::set<int>> want_to_read;
std::map<hobject_t, read_request_t> to_read;
std::map<hobject_t, read_result_t> complete;
std::map<hobject_t, std::set<pg_shard_t>> obj_to_source;
std::map<pg_shard_t, std::set<hobject_t> > source_to_obj;
std::map<pg_shard_t, std::set<hobject_t>> source_to_obj;
void dump(ceph::Formatter *f) const;
std::set<pg_shard_t> in_progress;
std::list<ECUtil::log_entry_t> debug_log;
ReadOp(
int priority,
ceph_tid_t tid,
bool do_redundant_reads,
bool for_recovery,
std::unique_ptr<ReadCompleter> _on_complete,
OpRequestRef op,
std::map<hobject_t, std::set<int>> &&_want_to_read,
std::map<hobject_t, read_request_t> &&_to_read)
int priority,
ceph_tid_t tid,
bool do_redundant_reads,
bool for_recovery,
std::unique_ptr<ReadCompleter> _on_complete,
std::map<hobject_t, read_request_t> &&_to_read)
: priority(priority),
tid(tid),
op(op),
do_redundant_reads(do_redundant_reads),
for_recovery(for_recovery),
on_complete(std::move(_on_complete)),
want_to_read(std::move(_want_to_read)),
to_read(std::move(_to_read)) {
for (auto &&hpair: to_read) {
auto &returned = complete[hpair.first].returned;
for (auto &&extent: hpair.second.to_read) {
returned.push_back(
boost::make_tuple(
extent.offset,
extent.size,
std::map<pg_shard_t, ceph::buffer::list>()));
}
}
}
to_read(std::move(_to_read)) {}
ReadOp() = delete;
ReadOp(const ReadOp &) = delete; // due to on_complete being unique_ptr
ReadOp(ReadOp &&) = default;
void print(std::ostream &os) const {
os << "ReadOp(tid=" << tid;
#ifndef WITH_CRIMSON
if (op && op->get_req()) {
os << ", op=";
op->get_req()->print(os);
}
#endif
os << ", to_read=" << to_read << ", complete=" << complete
<< ", priority=" << priority << ", obj_to_source=" << obj_to_source
<< ", source_to_obj=" << source_to_obj << ", in_progress=" <<
in_progress
<< ", debug_log=" << debug_log << ")";
}
};
struct ReadPipeline {
void objects_read_and_reconstruct(
const std::map<hobject_t, std::list<ec_align_t>> &reads,
bool fast_read,
GenContextURef<ec_extents_t &&> &&func);
const std::map<hobject_t, std::list<ec_align_t>> &reads,
bool fast_read,
uint64_t object_size,
GenContextURef<ec_extents_t&&> &&func);
void objects_read_and_reconstruct_for_rmw(
std::map<hobject_t, read_request_t> &&to_read,
GenContextURef<ECCommon::ec_extents_t&&> &&func);
template <class F, class G>
void filter_read_op(
const OSDMapRef& osdmap,
ReadOp &op,
F&& on_erase,
G&& on_schedule_recovery);
const OSDMapRef &osdmap,
ReadOp &op,
F &&on_erase,
G &&on_schedule_recovery);
template <class F, class G>
void check_recovery_sources(
const OSDMapRef& osdmap,
F&& on_erase,
G&& on_schedule_recovery);
const OSDMapRef &osdmap,
F &&on_erase,
G &&on_schedule_recovery);
void complete_read_op(ReadOp &rop);
void complete_read_op(ReadOp &&rop);
void start_read_op(
int priority,
std::map<hobject_t, std::set<int>> &want_to_read,
std::map<hobject_t, read_request_t> &to_read,
OpRequestRef op,
bool do_redundant_reads,
bool for_recovery,
std::unique_ptr<ReadCompleter> on_complete);
int priority,
std::map<hobject_t, read_request_t> &to_read,
bool do_redundant_reads,
bool for_recovery,
std::unique_ptr<ReadCompleter> on_complete);
void do_read_op(ReadOp &rop);
int send_all_remaining_reads(
const hobject_t &hoid,
ReadOp &rop);
const hobject_t &hoid,
ReadOp &rop);
void on_change();
void kick_reads();
std::map<ceph_tid_t, ReadOp> tid_to_read_map;
std::map<pg_shard_t, std::set<ceph_tid_t> > shard_to_read_map;
std::map<pg_shard_t, std::set<ceph_tid_t>> shard_to_read_map;
std::list<ClientAsyncReadStatus> in_progress_client_reads;
CephContext* cct;
CephContext *cct;
ceph::ErasureCodeInterfaceRef ec_impl;
const ECUtil::stripe_info_t& sinfo;
const ECUtil::stripe_info_t &sinfo;
// TODO: lay an interface down here
ECListener* parent;
ECListener *parent;
ECListener *get_parent() const { return parent; }
const OSDMapRef& get_osdmap() const { return get_parent()->pgb_get_osdmap(); }
epoch_t get_osdmap_epoch() const { return get_parent()->pgb_get_osdmap_epoch(); }
const pg_info_t &get_info() { return get_parent()->get_info(); }
ReadPipeline(CephContext* cct,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t& sinfo,
ECListener* parent)
const OSDMapRef &get_osdmap() const {
return get_parent()->pgb_get_osdmap();
}
epoch_t get_osdmap_epoch() const {
return get_parent()->pgb_get_osdmap_epoch();
}
const pg_info_t &get_info() const { return get_parent()->get_info(); }
ReadPipeline(CephContext *cct,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t &sinfo,
ECListener *parent)
: cct(cct),
ec_impl(std::move(ec_impl)),
sinfo(sinfo),
parent(parent) {
}
parent(parent) {}
/**
* While get_want_to_read_shards creates a want_to_read based on the EC
@ -289,47 +371,42 @@ struct ECCommon {
*
*/
void get_min_want_to_read_shards(
uint64_t offset, ///< [in]
uint64_t length, ///< [in]
std::set<int> *want_to_read ///< [out]
);
static void get_min_want_to_read_shards(
const uint64_t offset,
const uint64_t length,
const ECUtil::stripe_info_t& sinfo,
std::set<int> *want_to_read);
const ec_align_t &to_read, ///< [in]
ECUtil::shard_extent_set_t &want_shard_reads); ///< [out]
int get_remaining_shards(
const hobject_t &hoid,
const std::set<int> &avail,
const std::set<int> &want,
const read_result_t &result,
std::map<pg_shard_t, std::vector<std::pair<int, int>>> *to_read,
bool for_recovery);
const hobject_t &hoid,
read_result_t &read_result,
read_request_t &read_request,
bool for_recovery,
bool fast_read);
void get_all_avail_shards(
const hobject_t &hoid,
const std::set<pg_shard_t> &error_shards,
std::set<int> &have,
std::map<shard_id_t, pg_shard_t> &shards,
bool for_recovery);
const hobject_t &hoid,
shard_id_set &have,
shard_id_map<pg_shard_t> &shards,
bool for_recovery,
const std::optional<std::set<pg_shard_t>> &error_shards = std::nullopt);
std::pair<const shard_id_set, const shard_id_set> get_readable_writable_shard_id_sets();
friend std::ostream &operator<<(std::ostream &lhs, const ReadOp &rhs);
friend struct FinishReadOp;
void get_want_to_read_shards(std::set<int> *want_to_read) const;
void get_want_to_read_shards(
const std::list<ec_align_t> &to_read,
ECUtil::shard_extent_set_t &want_shard_reads);
/// Returns to_read replicas sufficient to reconstruct want
int get_min_avail_to_read_shards(
const hobject_t &hoid, ///< [in] object
const std::set<int> &want, ///< [in] desired shards
bool for_recovery, ///< [in] true if we may use non-acting replicas
bool do_redundant_reads, ///< [in] true if we want to issue redundant reads to reduce latency
std::map<pg_shard_t, std::vector<std::pair<int, int>>> *to_read ///< [out] shards, corresponding subchunks to read
const hobject_t &hoid, ///< [in] object
bool for_recovery, ///< [in] true if we may use non-acting replicas
bool do_redundant_reads,
///< [in] true if we want to issue redundant reads to reduce latency
read_request_t &read_request,
///< [out] shard_reads, corresponding subchunks / other sub reads to read
const std::optional<std::set<pg_shard_t>> &error_shards = std::nullopt
//< [in] Shards where reads have failed (optional)
); ///< @return error code, 0 on success
void schedule_recovery_work();
};
/**
@ -346,7 +423,7 @@ struct ECCommon {
* on the writing std::list.
*/
struct RMWPipeline {
struct RMWPipeline : ECExtentCache::BackendReadListener {
struct Op : boost::intrusive::list_base_hook<> {
/// From submit_transaction caller, describes operation
hobject_t hoid;
@ -374,171 +451,176 @@ struct ECCommon {
/// Ancillary also provided from submit_transaction caller
std::map<hobject_t, ObjectContextRef> obc_map;
/// see call_write_ordered
std::list<std::function<void(void)> > on_write;
/// Generated internally
std::set<hobject_t> temp_added;
std::set<hobject_t> temp_cleared;
ECTransaction::WritePlan plan;
bool requires_rmw() const { return !plan.to_read.empty(); }
bool invalidates_cache() const { return plan.invalidates_cache; }
bool requires_rmw() const { return !plan.want_read; }
// must be true if requires_rmw(), must be false if invalidates_cache()
bool using_cache = true;
/// In progress read state;
std::map<hobject_t,extent_set> pending_read; // subset already being read
std::map<hobject_t,extent_set> remote_read; // subset we must read
std::map<hobject_t,extent_map> remote_read_result;
bool read_in_progress() const {
return !remote_read.empty() && remote_read_result.empty();
}
int pending_cache_ops = 0;
std::map<hobject_t, ECUtil::shard_extent_map_t> remote_shard_extent_map;
/// In progress write state.
std::set<pg_shard_t> pending_commit;
// we need pending_apply for pre-mimic peers so that we don't issue a
// read on a remote shard before it has applied a previous write. We can
// remove this after nautilus.
std::set<pg_shard_t> pending_apply;
int pending_commits = 0;
bool write_in_progress() const {
return !pending_commit.empty() || !pending_apply.empty();
return pending_commits != 0;
}
/// optional, may be null, for tracking purposes
OpRequestRef client_op;
/// pin for cache
ExtentCache::write_pin pin;
std::list<ECExtentCache::OpRef> cache_ops;
RMWPipeline *pipeline;
Op() : tid(), plan(), pipeline(nullptr) {}
/// Callbacks
Context *on_all_commit = nullptr;
virtual ~Op() {
delete on_all_commit;
}
virtual void generate_transactions(
ceph::ErasureCodeInterfaceRef &ecimpl,
pg_t pgid,
const ECUtil::stripe_info_t &sinfo,
std::map<hobject_t,extent_map> *written,
std::map<shard_id_t, ceph::os::Transaction> *transactions,
DoutPrefixProvider *dpp,
const ceph_release_t require_osd_release = ceph_release_t::unknown) = 0;
ceph::ErasureCodeInterfaceRef &ec_impl,
pg_t pgid,
const ECUtil::stripe_info_t &sinfo,
std::map<hobject_t, ECUtil::shard_extent_map_t> *written,
shard_id_map<ceph::os::Transaction> *transactions,
DoutPrefixProvider *dpp,
const OSDMapRef &osdmap) = 0;
virtual bool skip_transaction(
std::set<shard_id_t> &pending_roll_forward,
shard_id_t shard,
ceph::os::Transaction &transaction) = 0;
void cache_ready(const hobject_t &oid, const ECUtil::shard_extent_map_t &result) {
if (!result.empty()) {
remote_shard_extent_map.insert(std::pair(oid, result));
}
if (!--pending_cache_ops) {
pipeline->cache_ready(*this);
}
}
void print(std::ostream &os) const {
os << "Op(" << hoid << " v=" << version << " tt=" << trim_to
<< " tid=" << tid << " reqid=" << reqid;
#ifndef WITH_CRIMSON
if (client_op && client_op->get_req()) {
os << " client_op=";
client_op->get_req()->print(os);
}
#endif
os << " pg_committed_to=" << pg_committed_to
<< " temp_added=" << temp_added
<< " temp_cleared=" << temp_cleared
<< " remote_read_result=" << remote_shard_extent_map
<< " pending_commits=" << pending_commits
<< " plan.to_read=" << plan
<< ")";
}
};
using OpRef = std::unique_ptr<Op>;
using op_list = boost::intrusive::list<Op>;
friend std::ostream &operator<<(std::ostream &lhs, const Op &rhs);
ExtentCache cache;
void backend_read(hobject_t oid, ECUtil::shard_extent_set_t const &request,
uint64_t object_size) override {
std::map<hobject_t, read_request_t> to_read;
to_read.emplace(oid, read_request_t(request, false, object_size));
objects_read_async_no_cache(
std::move(to_read),
[this](ec_extents_t &&results) {
for (auto &&[oid, result]: results) {
extent_cache.read_done(oid, std::move(result.shard_extent_map));
}
});
}
using OpRef = std::shared_ptr<Op>;
std::map<ceph_tid_t, OpRef> tid_to_op_map; /// Owns Op structure
/**
* We model the possible rmw states as a std::set of waitlists.
* All writes at this time complete in order, so a write blocked
* at waiting_state blocks all writes behind it as well (same for
* other states).
*
* Future work: We can break this up into a per-object pipeline
* (almost). First, provide an ordering token to submit_transaction
* and require that all operations within a single transaction take
* place on a subset of hobject_t space partitioned by that token
* (the hashid seem about right to me -- even works for temp objects
* if you recall that a temp object created for object head foo will
* only ever be referenced by other transactions on foo and aren't
* reused). Next, factor this part into a class and maintain one per
* ordering token. Next, fixup PrimaryLogPG's repop queue to be
* partitioned by ordering token. Finally, refactor the op pipeline
* so that the log entries passed into submit_transaction aren't
* versioned. We can't assign versions to them until we actually
* submit the operation. That's probably going to be the hard part.
*/
class pipeline_state_t {
enum {
CACHE_VALID = 0,
CACHE_INVALID = 1
} pipeline_state = CACHE_VALID;
public:
bool caching_enabled() const {
return pipeline_state == CACHE_VALID;
}
bool cache_invalid() const {
return !caching_enabled();
}
void invalidate() {
pipeline_state = CACHE_INVALID;
}
void clear() {
pipeline_state = CACHE_VALID;
}
friend std::ostream &operator<<(std::ostream &lhs, const pipeline_state_t &rhs);
} pipeline_state;
std::map<hobject_t, eversion_t> oid_to_version;
op_list waiting_state; /// writes waiting on pipe_state
op_list waiting_reads; /// writes waiting on partial stripe reads
op_list waiting_commit; /// writes waiting on initial commit
std::list<OpRef> waiting_commit;
eversion_t completed_to;
eversion_t committed_to;
void start_rmw(OpRef op);
bool try_state_to_reads();
bool try_reads_to_commit();
bool try_finish_rmw();
void check_ops();
void cache_ready(Op &op);
void try_finish_rmw();
void finish_rmw(OpRef const &op);
void on_change();
void on_change2();
void call_write_ordered(std::function<void(void)> &&cb);
CephContext* cct;
CephContext *cct;
ECListener *get_parent() const { return parent; }
const OSDMapRef& get_osdmap() const { return get_parent()->pgb_get_osdmap(); }
epoch_t get_osdmap_epoch() const { return get_parent()->pgb_get_osdmap_epoch(); }
const pg_info_t &get_info() { return get_parent()->get_info(); }
const OSDMapRef &get_osdmap() const {
return get_parent()->pgb_get_osdmap();
}
epoch_t get_osdmap_epoch() const {
return get_parent()->pgb_get_osdmap_epoch();
}
const pg_info_t &get_info() const { return get_parent()->get_info(); }
template <typename Func>
void objects_read_async_no_cache(
const std::map<hobject_t,extent_set> &to_read,
Func &&on_complete
) {
std::map<hobject_t, std::list<ec_align_t>> _to_read;
for (auto &&hpair: to_read) {
auto &l = _to_read[hpair.first];
for (auto extent: hpair.second) {
l.emplace_back(ec_align_t{extent.first, extent.second, 0});
}
}
ec_backend.objects_read_and_reconstruct(
_to_read,
false,
std::map<hobject_t, read_request_t> &&to_read,
Func &&on_complete) {
ec_backend.objects_read_and_reconstruct_for_rmw(
std::move(to_read),
make_gen_lambda_context<
ECCommon::ec_extents_t &&, Func>(
std::forward<Func>(on_complete)));
ECCommon::ec_extents_t&&, Func>(
std::forward<Func>(on_complete)));
}
void handle_sub_write(
pg_shard_t from,
OpRequestRef msg,
ECSubWrite &op,
const ZTracer::Trace &trace
) {
ec_backend.handle_sub_write(from, std::move(msg), op, trace, *get_parent());
pg_shard_t from,
OpRequestRef msg,
ECSubWrite &op,
const ZTracer::Trace &trace) const {
ec_backend.handle_sub_write(from, std::move(msg), op, trace,
*get_parent());
}
// end of iface
ceph::ErasureCodeInterfaceRef ec_impl;
const ECUtil::stripe_info_t& sinfo;
ECListener* parent;
ECCommon& ec_backend;
// Set of shards that will need a dummy transaction for the final
// roll forward
std::set<shard_id_t> pending_roll_forward;
RMWPipeline(CephContext* cct,
ceph::ErasureCodeInterfaceRef ec_impl;
const ECUtil::stripe_info_t &sinfo;
ECListener *parent;
ECCommon &ec_backend;
ECExtentCache extent_cache;
uint64_t ec_pdw_write_mode;
RMWPipeline(CephContext *cct,
ceph::ErasureCodeInterfaceRef ec_impl,
const ECUtil::stripe_info_t& sinfo,
ECListener* parent,
ECCommon& ec_backend)
const ECUtil::stripe_info_t &sinfo,
ECListener *parent,
ECCommon &ec_backend,
ECExtentCache::LRU &ec_extent_cache_lru)
: cct(cct),
ec_impl(std::move(ec_impl)),
sinfo(sinfo),
parent(parent),
ec_backend(ec_backend) {
}
ec_backend(ec_backend),
extent_cache(*this, ec_extent_cache_lru, sinfo, cct),
ec_pdw_write_mode(cct->_conf.get_val<uint64_t>("ec_pdw_write_mode")) {}
};
class UnstableHashInfoRegistry {
@ -547,47 +629,35 @@ struct ECCommon {
/// If modified, ensure that the ref is held until the update is applied
SharedPtrRegistry<hobject_t, ECUtil::HashInfo> registry;
public:
public:
UnstableHashInfoRegistry(
CephContext *cct,
ceph::ErasureCodeInterfaceRef ec_impl)
CephContext *cct,
ceph::ErasureCodeInterfaceRef ec_impl)
: cct(cct),
ec_impl(std::move(ec_impl)) {}
ec_impl(std::move(ec_impl)) {}
ECUtil::HashInfoRef maybe_put_hash_info(
const hobject_t &hoid,
ECUtil::HashInfo &&hinfo);
const hobject_t &hoid,
ECUtil::HashInfo &&hinfo);
ECUtil::HashInfoRef get_hash_info(
const hobject_t &hoid,
bool create,
const std::map<std::string, ceph::buffer::list, std::less<>>& attr,
uint64_t size);
const hobject_t &hoid,
bool create,
const std::map<std::string, ceph::buffer::list, std::less<>> &attrs,
uint64_t size);
};
};
std::ostream &operator<<(std::ostream &lhs,
const ECCommon::RMWPipeline::pipeline_state_t &rhs);
std::ostream &operator<<(std::ostream &lhs,
const ECCommon::read_request_t &rhs);
std::ostream &operator<<(std::ostream &lhs,
const ECCommon::read_result_t &rhs);
std::ostream &operator<<(std::ostream &lhs,
const ECCommon::ReadOp &rhs);
std::ostream &operator<<(std::ostream &lhs,
const ECCommon::RMWPipeline::Op &rhs);
template <class F, class G>
void ECCommon::ReadPipeline::check_recovery_sources(
const OSDMapRef& osdmap,
F&& on_erase,
G&& on_schedule_recovery)
{
const OSDMapRef &osdmap,
F &&on_erase,
G &&on_schedule_recovery
) {
std::set<ceph_tid_t> tids_to_filter;
for (std::map<pg_shard_t, std::set<ceph_tid_t> >::iterator
for (std::map<pg_shard_t, std::set<ceph_tid_t>>::iterator
i = shard_to_read_map.begin();
i != shard_to_read_map.end();
) {
i != shard_to_read_map.end();) {
if (osdmap->is_down(i->first.osd)) {
tids_to_filter.insert(i->second.begin(), i->second.end());
shard_to_read_map.erase(i++);
@ -606,53 +676,45 @@ void ECCommon::ReadPipeline::check_recovery_sources(
template <class F, class G>
void ECCommon::ReadPipeline::filter_read_op(
const OSDMapRef& osdmap,
ReadOp &op,
F&& on_erase,
G&& on_schedule_recovery)
{
const OSDMapRef &osdmap,
ReadOp &op,
F &&on_erase,
G &&on_schedule_recovery
) {
std::set<hobject_t> to_cancel;
for (std::map<pg_shard_t, std::set<hobject_t> >::iterator i = op.source_to_obj.begin();
i != op.source_to_obj.end();
++i) {
if (osdmap->is_down(i->first.osd)) {
to_cancel.insert(i->second.begin(), i->second.end());
op.in_progress.erase(i->first);
continue;
for (auto &&[pg_shard, hoid_set] : op.source_to_obj) {
if (osdmap->is_down(pg_shard.osd)) {
to_cancel.insert(hoid_set.begin(), hoid_set.end());
op.in_progress.erase(pg_shard);
}
}
if (to_cancel.empty())
return;
for (std::map<pg_shard_t, std::set<hobject_t> >::iterator i = op.source_to_obj.begin();
i != op.source_to_obj.end();
) {
for (std::set<hobject_t>::iterator j = i->second.begin();
j != i->second.end();
) {
if (to_cancel.count(*j))
i->second.erase(j++);
else
++j;
for (auto iter = op.source_to_obj.begin();
iter != op.source_to_obj.end();) {
auto &[pg_shard, hoid_set] = *iter;
for (auto &hoid : hoid_set) {
if (to_cancel.contains(hoid)) {
hoid_set.erase(hoid);
}
}
if (i->second.empty()) {
op.source_to_obj.erase(i++);
if (hoid_set.empty()) {
op.source_to_obj.erase(iter++);
} else {
ceph_assert(!osdmap->is_down(i->first.osd));
++i;
ceph_assert(!osdmap->is_down(pg_shard.osd));
++iter;
}
}
for (std::set<hobject_t>::iterator i = to_cancel.begin();
i != to_cancel.end();
++i) {
get_parent()->cancel_pull(*i);
for (auto hoid : to_cancel) {
get_parent()->cancel_pull(hoid);
ceph_assert(op.to_read.count(*i));
op.to_read.erase(*i);
op.complete.erase(*i);
on_erase(*i);
ceph_assert(op.to_read.contains(hoid));
op.to_read.erase(hoid);
op.complete.erase(hoid);
on_erase(hoid);
}
if (op.in_progress.empty()) {
@ -675,8 +737,14 @@ void ECCommon::ReadPipeline::filter_read_op(
}
}
template <> struct fmt::formatter<ECCommon::RMWPipeline::pipeline_state_t> : fmt::ostream_formatter {};
template <> struct fmt::formatter<ECCommon::read_request_t> : fmt::ostream_formatter {};
template <> struct fmt::formatter<ECCommon::read_result_t> : fmt::ostream_formatter {};
template <> struct fmt::formatter<ECCommon::ReadOp> : fmt::ostream_formatter {};
template <> struct fmt::formatter<ECCommon::RMWPipeline::Op> : fmt::ostream_formatter {};
template <>
struct fmt::formatter<ECCommon::read_request_t> : fmt::ostream_formatter {};
template <>
struct fmt::formatter<ECCommon::read_result_t> : fmt::ostream_formatter {};
template <>
struct fmt::formatter<ECCommon::ReadOp> : fmt::ostream_formatter {};
template <>
struct fmt::formatter<ECCommon::RMWPipeline::Op> : fmt::ostream_formatter {};

480
src/osd/ECExtentCache.cc Normal file
View File

@ -0,0 +1,480 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
#include "ECExtentCache.h"
#include "ECUtil.h"
#include <ranges>
using namespace std;
using namespace ECUtil;
void ECExtentCache::Object::request(OpRef &op) {
/* After a cache invalidation, we allow through a single cache-invalidating
* IO.
*/
if (op->invalidates_cache) {
if (cache_invalidated) {
op->invalidates_cache = false;
} else {
cache_invalidate_expected = true;
}
}
cache_invalidated = false;
extent_set eset = op->get_pin_eset(line_size);
for (auto &&[start, len] : eset) {
for (uint64_t to_pin = start; to_pin < start + len; to_pin += line_size) {
LineRef l;
if (!lines.contains(to_pin)) {
l = make_shared<Line>(*this, to_pin);
if (!l->cache->empty()) {
l->cache->to_shard_extent_set(do_not_read);
}
lines.emplace(to_pin, weak_ptr(l));
} else {
l = lines.at(to_pin).lock();
}
op->lines.emplace_back(l);
}
}
bool read_required = false;
/* Deal with reads if there are any.
* If any cache invalidation ops have been added, there is no point adding any
* reads as they are all going to be thrown away before any of the
* post-invalidate ops are honoured.
*/
if (op->reads && !cache_invalidate_expected) {
for (auto &&[shard, eset] : *(op->reads)) {
extent_set request = eset;
if (do_not_read.contains(shard)) {
request.subtract(do_not_read.at(shard));
}
if (!request.empty()) {
requesting[shard].union_of(request);
read_required = true;
requesting_ops.emplace_back(op);
}
}
}
/* Calculate the range of the object which no longer need to be written. This
* will include:
* - Any reads being issued by this IO.
* - Any writes being issued (these will be cached)
* - any unwritten regions in an append - these can assumed to be zero.
*/
if (read_required) {
do_not_read.insert(requesting);
}
do_not_read.insert(op->writes);
if (op->projected_size > projected_size) {
/* This write is growing the size of the object. This essentially counts
* as a write (although the cache will not get populated). Future reads
* to this area will be skipped, but this makes them essentially zero
* reads.
*/
shard_extent_set_t obj_hole(pg.sinfo.get_k_plus_m());
shard_extent_set_t read_mask(pg.sinfo.get_k_plus_m());
pg.sinfo.ro_size_to_read_mask(op->projected_size, obj_hole);
pg.sinfo.ro_size_to_read_mask(projected_size, read_mask);
obj_hole.subtract(read_mask);
do_not_read.insert(obj_hole);
} else if (op->projected_size < projected_size) {
// Invalidate the object's cache when we see any object reduce in size.
op->invalidates_cache = true;
}
projected_size = op->projected_size;
if (read_required) send_reads();
else op->read_done = true;
}
void ECExtentCache::Object::send_reads() {
if (reading || requesting.empty())
return; // Read busy
reading_ops.swap(requesting_ops);
pg.backend_read.backend_read(oid, requesting, current_size);
requesting.clear();
reading = true;
}
void ECExtentCache::Object::read_done(shard_extent_map_t const &buffers) {
reading = false;
for (auto &&op : reading_ops) {
op->read_done = true;
}
reading_ops.clear();
insert(buffers);
}
uint64_t ECExtentCache::Object::line_align(uint64_t x) const {
return x - (x % line_size);
}
void ECExtentCache::Object::insert(shard_extent_map_t const &buffers) const {
if (buffers.empty()) return;
/* The following gets quite inefficient for writes which write to the start
* and the end of a very large object, since we iterated over the middle.
* This seems like a strange use case, so currently this is not being
* optimised.
*/
for (uint64_t slice_start = line_align(buffers.get_start_offset());
slice_start < buffers.get_end_offset();
slice_start += line_size) {
shard_extent_map_t slice = buffers.slice_map(slice_start, line_size);
if (!slice.empty()) {
LineRef l = lines.at(slice_start).lock();
/* The line should have been created already! */
l->cache->insert(slice);
uint64_t old_size = l->size;
l->size = l->cache->size();
ceph_assert(l->size >= old_size);
update_mempool(0, l->size - old_size);
}
}
}
void ECExtentCache::Object::write_done(shard_extent_map_t const &buffers,
uint64_t new_size) {
insert(buffers);
current_size = new_size;
}
void ECExtentCache::Object::unpin(Op &op) const {
op.lines.clear();
delete_maybe();
}
void ECExtentCache::Object::delete_maybe() const {
if (lines.empty() && active_ios == 0) {
pg.objects.erase(oid);
}
}
void check_seset_empty_for_range(shard_extent_set_t s, uint64_t off,
uint64_t len) {
for (auto &[shard, eset] : s) {
ceph_assert(!eset.intersects(off, len));
}
}
void ECExtentCache::Object::erase_line(uint64_t offset) {
check_seset_empty_for_range(requesting, offset, line_size);
do_not_read.erase_stripe(offset, line_size);
lines.erase(offset);
delete_maybe();
}
void ECExtentCache::Object::invalidate(const OpRef &invalidating_op) {
for (auto &l : std::views::values(lines)) {
auto line = l.lock();
line->cache->clear();
update_mempool(0, -line->size);
line->size = 0;
}
/* Remove all entries from the LRU */
pg.lru.remove_object(oid);
ceph_assert(!reading);
do_not_read.clear();
requesting.clear();
requesting_ops.clear();
reading_ops.clear();
/* Current size should reflect the actual size of the object, which was set
* by the previous write. We are going to replay all the writes now, so set
* the projected size to that of this op.
*/
projected_size = invalidating_op->projected_size;
// Cache can now be replayed and invalidate teh cache!
invalidating_op->invalidates_cache = false;
cache_invalidated = true;
cache_invalidate_expected = false;
/* We now need to reply all outstanding ops, so as to regenerate the read */
for (auto &op : pg.waiting_ops) {
if (op->object.oid == oid) {
op->read_done = false;
request(op);
}
}
}
void ECExtentCache::cache_maybe_ready() {
while (!waiting_ops.empty()) {
OpRef op = waiting_ops.front();
if (op->invalidates_cache) {
/* We must wait for any outstanding reads to complete. The cache replans
* all reads as part of invalidate. If an in-flight read completes after
* the invalidate, it will potentially corrupt it, leading to data
* corruption at the host.
*/
if (op->object.reading) {
return;
}
op->object.invalidate(op);
ceph_assert(!op->invalidates_cache);
}
/* If reads_done finds all reads complete it will call the completion
* callback. Typically, this will cause the client to execute the
* transaction and pop the front of waiting_ops. So we abort if either
* reads are not ready, or the client chooses not to complete the op
*/
if (!op->complete_if_reads_cached(op)) {
return;
}
waiting_ops.pop_front();
}
}
ECExtentCache::OpRef ECExtentCache::prepare(GenContextURef<OpRef&> &&ctx,
hobject_t const &oid,
std::optional<shard_extent_set_t>
const &to_read,
shard_extent_set_t const &write,
uint64_t orig_size,
uint64_t projected_size,
bool invalidates_cache) {
auto object_iter = objects.find(oid);
if (object_iter == objects.end()) {
auto p = objects.emplace(oid, Object(*this, oid, orig_size));
object_iter = p.first;
}
OpRef op = std::make_shared<Op>(
std::move(ctx), object_iter->second, to_read, write, projected_size,
invalidates_cache);
return op;
}
void ECExtentCache::read_done(hobject_t const &oid,
shard_extent_map_t const &update) {
objects.at(oid).read_done(update);
cache_maybe_ready();
objects.at(oid).send_reads();
}
void ECExtentCache::write_done(OpRef const &op,
shard_extent_map_t const &update) {
op->write_done(std::move(update));
}
uint64_t ECExtentCache::get_projected_size(hobject_t const &oid) const {
return objects.at(oid).get_projected_size();
}
bool ECExtentCache::contains_object(hobject_t const &oid) const {
return objects.contains(oid);
}
ECExtentCache::Op::~Op() {
ceph_assert(object.active_ios > 0);
object.active_ios--;
ceph_assert(object.pg.active_ios > 0);
object.pg.active_ios--;
object.unpin(*this);
}
/* ECExtent cache cleanup on occurs in two parts. The first performs cleanup
* of the ops currently managed by the extent cache. At this point, however
* the cache will be waiting for other parts of EC to clean up (for example
* any outstanding reads). on_change2() executes once all of this cleanup has
* occurred.
*/
void ECExtentCache::on_change() {
for (auto &&o : std::views::values(objects)) {
o.reading_ops.clear();
o.requesting_ops.clear();
o.requesting.clear();
}
for (auto &&op : waiting_ops) {
op->cancel();
}
waiting_ops.clear();
}
/* This must be run toward the end of EC on_change handling. It asserts that
* any object which is automatically self-destructs when idle has done so.
* Additionally, it discards the entire LRU cache. This must be done after all
* in-flight reads/writes have completed, or we risk attempting to insert data
* into the cache after it has been cleared.
*
* Note that the LRU will end up being called multiple times. With some
* additional code complexity this could be fixed for a small (probably
* insignificant) performance improvement.
*/
void ECExtentCache::on_change2() const {
lru.discard();
/* If this assert fires in a unit test, make sure that all ops have completed
* and cleared any extent cache ops they contain */
ceph_assert(objects.empty());
ceph_assert(active_ios == 0);
ceph_assert(idle());
}
void ECExtentCache::execute(list<OpRef> &op_list) {
for (auto &op : op_list) {
op->object.request(op);
}
waiting_ops.insert(waiting_ops.end(), op_list.begin(), op_list.end());
counter++;
cache_maybe_ready();
}
bool ECExtentCache::idle() const {
return active_ios == 0;
}
uint32_t ECExtentCache::get_and_reset_counter() {
uint32_t ret = counter;
counter = 0;
return ret;
}
list<ECExtentCache::LRU::Key>::iterator ECExtentCache::LRU::erase(
const list<Key>::iterator &it,
bool do_update_mempool) {
uint64_t size_change = map.at(*it).second->size();
if (do_update_mempool) {
update_mempool(-1, 0 - size_change);
}
size -= size_change;
map.erase(*it);
return lru.erase(it);
}
void ECExtentCache::LRU::add(const Line &line) {
if (line.size == 0) {
update_mempool(-1, 0);
return;
}
const Key k(line.offset, line.object.oid);
shared_ptr<shard_extent_map_t> cache = line.cache;
mutex.lock();
ceph_assert(!map.contains(k));
auto i = lru.insert(lru.end(), k);
auto j = make_pair(std::move(i), std::move(cache));
map.insert(std::pair(std::move(k), std::move(j)));
size += line.size; // This is already accounted for in mempool.
free_maybe();
mutex.unlock();
}
shared_ptr<shard_extent_map_t> ECExtentCache::LRU::find(
const hobject_t &oid, uint64_t offset) {
Key k(offset, oid);
shared_ptr<shard_extent_map_t> cache = nullptr;
mutex.lock();
if (map.contains(k)) {
auto &&[lru_iter, c] = map.at(k);
cache = c;
auto it = lru_iter; // Intentional copy.
erase(it, false);
}
mutex.unlock();
return cache;
}
void ECExtentCache::LRU::remove_object(const hobject_t &oid) {
mutex.lock();
for (auto it = lru.begin(); it != lru.end();) {
if (it->oid == oid) {
it = erase(it, true);
} else {
++it;
}
}
mutex.unlock();
}
void ECExtentCache::LRU::free_maybe() {
while (max_size < size) {
auto it = lru.begin();
erase(it, true);
}
}
void ECExtentCache::LRU::discard() {
mutex.lock();
lru.clear();
update_mempool(0 - map.size(), 0 - size);
map.clear();
size = 0;
mutex.unlock();
}
const extent_set ECExtentCache::Op::get_pin_eset(uint64_t alignment) const {
extent_set eset = writes.get_extent_superset();
if (reads) {
reads->get_extent_superset(eset);
}
eset.align(alignment);
return eset;
}
ECExtentCache::Op::Op(GenContextURef<OpRef&> &&cache_ready_cb,
Object &object,
std::optional<shard_extent_set_t> const &to_read,
shard_extent_set_t const &write,
uint64_t projected_size,
bool invalidates_cache) :
object(object),
reads(to_read),
writes(write),
result(&object.pg.sinfo),
invalidates_cache(invalidates_cache),
projected_size(projected_size),
cache_ready_cb(std::move(cache_ready_cb)) {
object.active_ios++;
object.pg.active_ios++;
}
shard_extent_map_t ECExtentCache::Object::get_cache(
std::optional<shard_extent_set_t> const &set) const {
if (!set) {
return shard_extent_map_t(&pg.sinfo);
}
shard_id_map<extent_map> res(pg.sinfo.get_k_plus_m());
for (auto &&[shard, eset] : *set) {
for (auto [off, len] : eset) {
for (uint64_t slice_start = line_align(off);
slice_start < off + len;
slice_start += line_size) {
uint64_t offset = max(slice_start, off);
uint64_t length = min(slice_start + line_size, off + len) - offset;
// This line must exist, as it was created when the op was created.
LineRef l = lines.at(slice_start).lock();
if (l->cache->contains_shard(shard)) {
extent_map m = l->cache->get_extent_map(shard).intersect(
offset, length);
if (!m.empty()) {
if (!res.contains(shard)) res.emplace(shard, std::move(m));
else res.at(shard).insert(m);
}
}
}
}
}
return shard_extent_map_t(&pg.sinfo, std::move(res));
}

View File

@ -1,10 +1,383 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
/* EC "extent" cache. This extent cache attempts to improve performance,
* particularly for small sequential writes, by caching the results of recent
* reads and writes.
*
* The cache has two parts: The main cache which is active while an IO is
* outstanding to an object and an "LRU" which stashes recent IO according to
* a least-recently-used scheme.
*
* The cache keeps all caches indexed by shard, shard_offset. That is it
* independently tracks caches for each shard of an EC. It will keep a cache
* even for shards which are currently offline or missing, since the cache
* is formed from the result of reads and writes, which are required to always
* calculate missing shards.
*
* The cache allows for a single read to be outstanding per PG at a time. If
* multiple writes are received while a read is active, the next read will
* contain all necessary reads, so as to catch up. Early on in development, a
* more parallel read mechanism was explored but was found to have no benefit.
*
* This cache will never re-order IO.
*
* The LRU
*
* The LRU is a per-OSD-shard (not to be confused with an EC shard). Since the
* OSD-shard can have multiple threads, the LRU must have a mutex. This should
* not be required for crimson-based pools, since each osd shard has a single
* reactor. Some effort has been made to limit the frequency that this mutex is
* taken.
*
* The LRU has a maximum size (defined in the constructor) and will keep its
* usage below this amount.
*
* Cache Lines
*
* The LRU tracks extents of recent writes with cache Lines. These are
* simple-to-track ranges of offsets across all shards. Each line represents 32K
* of address space on each shard.
*
* A cache line can be owned by:
* - No-one (i.e. it is not instantiated)
* - Object - an IO is inflight for this cache line
* - LRU - A recent IO touched this cache line.
*
* This simple ownership model means that the locking required for the LRU does
* not leak out into the wider extent cache and allows for the entire cache
* to be built from reference-counters.
*
* Client API
*
* The client has a number of required interactions:
* 1. prepare(...). This creates a cache op. All cache ops required for a single
* parent op must be prepared before any are executed.
* 2. execute(...). Execute an IO. This gives the cache permission to perform
* the IO. This function can (and frequently does) call back
* re-entrantly, so the caller must be aware that this can
* happen.
*
* The client must provide a mechanism for the extent cache to read. It does
* this by extending the ECExtentCache::BackendRead class.
*
* Once a read is complete, the client must call cache.read_done().
*
* When the cache is ready, it will call back the lambda passed to execute.
* The client is expected to populate the write data, including any parity
* data, by calling the cache.write_done() method.
*
* Finally, there is an on_change() and on_change2() methods. The first of these
* instructs the extent cache to discard any ops it has queued. The second
* simply asserts that the cache is now idle, this is to ensure that the calling
* code has performed the required clean up to clear the extent cache.
*/
#pragma once
// Temporary stubs
#include "ECUtil.h"
#include "include/Context.h"
class ECExtentCache {
class Address;
class Line;
class Object;
typedef std::shared_ptr<Line> LineRef;
typedef std::list<LineRef>::iterator LineIter;
public:
class LRU;
class Op;
typedef std::shared_ptr<Op> OpRef;
struct BackendReadListener {
virtual void backend_read(hobject_t oid,
ECUtil::shard_extent_set_t const &request,
uint64_t object_size) = 0;
virtual ~BackendReadListener() = default;
};
static void update_mempool(int items, int64_t bytes) {
mempool::get_pool(mempool::pool_index_t(mempool::mempool_ec_extent_cache)).
adjust_count(items, bytes);
}
class LRU {
public:
LRU(uint64_t) {}
class Key {
public:
uint64_t offset;
hobject_t oid;
Key(uint64_t offset, const hobject_t &oid) : offset(offset), oid(oid) {};
friend bool operator==(const Key &lhs, const Key &rhs) {
return lhs.offset == rhs.offset
&& lhs.oid == rhs.oid;
}
friend bool operator!=(const Key &lhs, const Key &rhs) {
return !(lhs == rhs);
}
};
struct KeyHash {
std::size_t operator()(const Key &obj) const {
std::size_t seed = 0x625610ED;
seed ^= (seed << 6) + (seed >> 2) + 0x1E665363 + static_cast<
std::size_t>(obj.offset);
seed ^= (seed << 6) + (seed >> 2) + 0x51343C80 + obj.oid.get_hash();
return seed;
}
};
private:
friend class Object;
friend class ECExtentCache;
std::unordered_map<Key, std::pair<
std::list<Key>::iterator, std::shared_ptr<
ECUtil::shard_extent_map_t>>, KeyHash> map;
std::list<Key> lru;
uint64_t max_size = 0;
uint64_t size = 0;
ceph::mutex mutex = ceph::make_mutex("ECExtentCache::LRU");
void free_maybe();
void discard();
void add(const Line &line);
void erase(const Key &k);
std::list<Key>::iterator erase(const std::list<Key>::iterator &it,
bool update_mempool);
std::shared_ptr<ECUtil::shard_extent_map_t> find(
const hobject_t &oid, uint64_t offset);
void remove_object(const hobject_t &oid);
public:
explicit LRU(uint64_t max_size) : map(), max_size(max_size) {}
};
};
class Op {
friend class Object;
friend class ECExtentCache;
Object &object;
std::optional<ECUtil::shard_extent_set_t> const reads;
ECUtil::shard_extent_set_t const writes;
ECUtil::shard_extent_map_t result;
bool complete = false;
bool invalidates_cache = false;
bool reading = false;
bool read_done = false;
uint64_t projected_size = 0;
GenContextURef<OpRef&> cache_ready_cb;
std::list<LineRef> lines;
// List of callbacks to be executed on write completion (not commit)
std::list<std::function<void(void)>> on_write;
const extent_set get_pin_eset(uint64_t alignment) const;
public:
explicit Op(
GenContextURef<OpRef&> &&cache_ready_cb,
Object &object,
std::optional<ECUtil::shard_extent_set_t> const &to_read,
ECUtil::shard_extent_set_t const &write,
uint64_t projected_size,
bool invalidates_cache);
~Op();
void cancel() { delete cache_ready_cb.release(); }
const ECUtil::shard_extent_set_t &get_writes() const { return writes; }
const Object &get_object() const { return object; }
const hobject_t &get_hoid() const { return object.oid; }
const ECUtil::shard_extent_map_t &get_result() { return result; }
void add_on_write(std::function<void(void)> &&cb) {
on_write.emplace_back(std::move(cb));
}
bool complete_if_reads_cached(OpRef &op_ref) {
if (!read_done) {
return false;
}
result = object.get_cache(reads);
complete = true;
cache_ready_cb.release()->complete(op_ref);
return true;
}
void write_done(ECUtil::shard_extent_map_t const &update) const {
object.write_done(update, projected_size);
for (auto &cb: on_write) {
cb();
}
}
};
#define MIN_LINE_SIZE (32UL*1024UL)
private:
class Object {
friend class Op;
friend class LRU;
friend class Line;
friend class ECExtentCache;
ECExtentCache &pg;
ECUtil::shard_extent_set_t requesting;
ECUtil::shard_extent_set_t do_not_read;
std::list<OpRef> reading_ops;
std::list<OpRef> requesting_ops;
// Map of the byte-offset of the start of the line to the line.
std::map<uint64_t, std::weak_ptr<Line>> lines;
int active_ios = 0;
uint64_t current_size = 0;
uint64_t projected_size = 0;
uint64_t line_size = 0;
bool reading = false;
bool cache_invalidated = false;
bool cache_invalidate_expected = false;
void request(OpRef &op);
void send_reads();
void unpin(Op &op) const;
void delete_maybe() const;
void erase_line(uint64_t offset);
void invalidate(const OpRef &invalidating_op);
public:
hobject_t oid;
Object(ECExtentCache &pg, hobject_t const &oid, uint64_t size) :
pg(pg),
requesting(pg.sinfo.get_k_plus_m()),
do_not_read(pg.sinfo.get_k_plus_m()),
current_size(size),
projected_size(size),
oid(oid) {
line_size = std::max(MIN_LINE_SIZE, pg.sinfo.get_chunk_size());
}
void insert(ECUtil::shard_extent_map_t const &buffers) const;
void write_done(ECUtil::shard_extent_map_t const &buffers, uint64_t new_size);
void read_done(ECUtil::shard_extent_map_t const &result);
[[nodiscard]] uint64_t get_projected_size() const { return projected_size; }
ECUtil::shard_extent_map_t get_cache(
std::optional<ECUtil::shard_extent_set_t> const &set) const;
uint64_t line_align(uint64_t line) const;
};
class Line {
public:
uint64_t offset;
uint64_t size;
std::shared_ptr<ECUtil::shard_extent_map_t> cache;
Object &object;
Line(Object &object,
uint64_t offset) :
offset(offset),
object(object) {
std::shared_ptr<ECUtil::shard_extent_map_t> c = object.pg.lru.find(
object.oid, offset);
if (c == nullptr) {
cache = std::make_shared<ECUtil::shard_extent_map_t>(&object.pg.sinfo);
size = 0;
/* We are creating an empty cache line */
update_mempool(1, 0);
} else {
cache = c;
size = c->size();
}
}
~Line() {
object.pg.lru.add(*this);
object.erase_line(offset);
}
friend bool operator==(const Line &lhs, const Line &rhs) {
return lhs.offset == rhs.offset
&& lhs.object.oid == rhs.object.oid;
}
friend bool operator!=(const Line &lhs, const Line &rhs) {
return !(lhs == rhs);
}
};
std::map<hobject_t, Object> objects;
BackendReadListener &backend_read;
LRU &lru;
const ECUtil::stripe_info_t &sinfo;
std::list<OpRef> waiting_ops;
void cache_maybe_ready();
uint32_t counter = 0;
uint32_t active_ios = 0;
CephContext *cct;
OpRef prepare(GenContextURef<OpRef&> &&ctx,
hobject_t const &oid,
std::optional<ECUtil::shard_extent_set_t> const &to_read,
ECUtil::shard_extent_set_t const &write,
uint64_t orig_size,
uint64_t projected_size,
bool invalidates_cache);
public:
~ECExtentCache() {
// This should really only be needed in failed tests, as the PG should
// clear up any IO before it gets destructed. However, here we make sure
// to clean up any outstanding IO.
on_change();
on_change2();
}
explicit ECExtentCache(BackendReadListener &backend_read,
LRU &lru, const ECUtil::stripe_info_t &sinfo,
CephContext *cct
) :
backend_read(backend_read),
lru(lru),
sinfo(sinfo),
cct(cct) {}
// Insert some data into the cache.
void read_done(hobject_t const &oid, ECUtil::shard_extent_map_t const &update);
void write_done(OpRef const &op, ECUtil::shard_extent_map_t const &update);
void on_change();
void on_change2() const;
[[nodiscard]] bool contains_object(hobject_t const &oid) const;
[[nodiscard]] uint64_t get_projected_size(hobject_t const &oid) const;
template <typename CacheReadyCb>
OpRef prepare(hobject_t const &oid,
std::optional<ECUtil::shard_extent_set_t> const &to_read,
ECUtil::shard_extent_set_t const &write,
uint64_t orig_size,
uint64_t projected_size,
bool invalidates_cache,
CacheReadyCb &&ready_cb) {
GenContextURef<OpRef&> ctx =
make_gen_lambda_context<OpRef&, CacheReadyCb>(
std::forward<CacheReadyCb>(ready_cb));
return prepare(std::move(ctx), oid, to_read, write, orig_size,
projected_size, invalidates_cache);
}
void execute(std::list<OpRef> &op_list);
[[nodiscard]] bool idle() const;
uint32_t get_and_reset_counter();
void add_on_write(std::function<void(void)> &&cb) const {
if (waiting_ops.empty()) {
cb();
} else {
waiting_ops.back()->add_on_write(std::move(cb));
}
}
}; // ECExtentCaches

File diff suppressed because it is too large Load Diff

View File

@ -12,184 +12,133 @@
*
*/
#ifndef ECTRANSACTION_H
#define ECTRANSACTION_H
#pragma once
#include "common/dout.h"
#include "ECUtil.h"
#include "ExtentCache.h"
#include "erasure-code/ErasureCodeInterface.h"
#include "os/Transaction.h"
#include "PGTransaction.h"
namespace ECTransaction {
struct WritePlan {
bool invalidates_cache = false; // Yes, both are possible
std::map<hobject_t,extent_set> to_read;
std::map<hobject_t,extent_set> will_write; // superset of to_read
class WritePlanObj {
public:
const hobject_t hoid;
std::optional<ECUtil::shard_extent_set_t> to_read;
ECUtil::shard_extent_set_t will_write;
const ECUtil::HashInfoRef hinfo;
const ECUtil::HashInfoRef shinfo;
const uint64_t orig_size;
uint64_t projected_size;
bool invalidates_cache;
bool do_parity_delta_write = false;
std::map<hobject_t,ECUtil::HashInfoRef> hash_infos;
};
WritePlanObj(
const hobject_t &hoid,
const PGTransaction::ObjectOperation &op,
const ECUtil::stripe_info_t &sinfo,
const shard_id_set readable_shards,
const shard_id_set writable_shards,
const bool object_in_cache,
uint64_t orig_size,
const std::optional<object_info_t> &oi,
const std::optional<object_info_t> &soi,
const ECUtil::HashInfoRef &&hinfo,
const ECUtil::HashInfoRef &&shinfo,
const unsigned pdw_write_mode);
template <typename F>
WritePlan get_write_plan(
const ECUtil::stripe_info_t &sinfo,
PGTransaction& t,
F &&get_hinfo,
DoutPrefixProvider *dpp) {
WritePlan plan;
t.safe_create_traverse(
[&](std::pair<const hobject_t, PGTransaction::ObjectOperation> &i) {
const auto& [obj, op] = i;
ECUtil::HashInfoRef hinfo = get_hinfo(obj);
plan.hash_infos[obj] = hinfo;
uint64_t projected_size =
hinfo->get_projected_total_logical_size(sinfo);
if (op.deletes_first()) {
ldpp_dout(dpp, 20) << __func__ << ": delete, setting projected size"
<< " to 0" << dendl;
projected_size = 0;
}
hobject_t source;
if (op.has_source(&source)) {
// typically clone or mv
plan.invalidates_cache = true;
ECUtil::HashInfoRef shinfo = get_hinfo(source);
projected_size = shinfo->get_projected_total_logical_size(sinfo);
plan.hash_infos[source] = shinfo;
}
auto &will_write = plan.will_write[obj];
if (op.truncate &&
op.truncate->first < projected_size) {
if (!(sinfo.logical_offset_is_stripe_aligned(
op.truncate->first))) {
plan.to_read[obj].union_insert(
sinfo.logical_to_prev_stripe_offset(op.truncate->first),
sinfo.get_stripe_width());
ldpp_dout(dpp, 20) << __func__ << ": unaligned truncate" << dendl;
will_write.union_insert(
sinfo.logical_to_prev_stripe_offset(op.truncate->first),
sinfo.get_stripe_width());
}
projected_size = sinfo.logical_to_next_stripe_offset(
op.truncate->first);
}
extent_set raw_write_set;
for (auto &&extent: op.buffer_updates) {
using BufferUpdate = PGTransaction::ObjectOperation::BufferUpdate;
if (boost::get<BufferUpdate::CloneRange>(&(extent.get_val()))) {
ceph_assert(
0 ==
"CloneRange is not allowed, do_op should have returned ENOTSUPP");
}
raw_write_set.insert(extent.get_off(), extent.get_len());
}
auto orig_size = projected_size;
for (auto extent = raw_write_set.begin();
extent != raw_write_set.end();
++extent) {
uint64_t head_start =
sinfo.logical_to_prev_stripe_offset(extent.get_start());
uint64_t head_finish =
sinfo.logical_to_next_stripe_offset(extent.get_start());
if (head_start > projected_size) {
head_start = projected_size;
}
if (head_start != head_finish &&
head_start < orig_size) {
ceph_assert(head_finish <= orig_size);
ceph_assert(head_finish - head_start == sinfo.get_stripe_width());
ldpp_dout(dpp, 20) << __func__ << ": reading partial head stripe "
<< head_start << "~" << sinfo.get_stripe_width()
<< dendl;
plan.to_read[obj].union_insert(
head_start, sinfo.get_stripe_width());
}
uint64_t tail_start =
sinfo.logical_to_prev_stripe_offset(
extent.get_start() + extent.get_len());
uint64_t tail_finish =
sinfo.logical_to_next_stripe_offset(
extent.get_start() + extent.get_len());
if (tail_start != tail_finish &&
(head_start == head_finish || tail_start != head_start) &&
tail_start < orig_size) {
ceph_assert(tail_finish <= orig_size);
ceph_assert(tail_finish - tail_start == sinfo.get_stripe_width());
ldpp_dout(dpp, 20) << __func__ << ": reading partial tail stripe "
<< tail_start << "~" << sinfo.get_stripe_width()
<< dendl;
plan.to_read[obj].union_insert(
tail_start, sinfo.get_stripe_width());
}
if (head_start != tail_finish) {
ceph_assert(
sinfo.logical_offset_is_stripe_aligned(
tail_finish - head_start)
);
will_write.union_insert(
head_start, tail_finish - head_start);
if (tail_finish > projected_size)
projected_size = tail_finish;
} else {
ceph_assert(tail_finish <= projected_size);
}
}
if (op.truncate && op.truncate->second > projected_size) {
uint64_t truncating_to =
sinfo.logical_to_next_stripe_offset(op.truncate->second);
ldpp_dout(dpp, 20) << __func__ << ": truncating out to "
<< truncating_to
<< dendl;
will_write.union_insert(projected_size,
truncating_to - projected_size);
projected_size = truncating_to;
}
ldpp_dout(dpp, 20) << __func__ << ": " << obj
<< " projected size "
<< projected_size
<< dendl;
hinfo->set_projected_total_logical_size(
sinfo,
projected_size);
/* validate post conditions:
* to_read should have an entry for `obj` if it isn't empty
* and if we are reading from `obj`, we can't be renaming or
* cloning it */
ceph_assert(plan.to_read.count(obj) == 0 ||
(!plan.to_read.at(obj).empty() &&
!i.second.has_source()));
});
return plan;
void print(std::ostream &os) const {
os << "to_read: " << to_read
<< " will_write: " << will_write
<< " hinfo: " << hinfo
<< " shinfo: " << shinfo
<< " orig_size: " << orig_size
<< " projected_size: " << projected_size
<< " invalidates_cache: " << invalidates_cache
<< " do_pdw: " << do_parity_delta_write;
}
};
void generate_transactions(
PGTransaction* _t,
struct WritePlan {
bool want_read;
std::list<WritePlanObj> plans;
void print(std::ostream &os) const {
os << " { plans : ";
bool first = true;
for (auto && p : plans) {
if (first) {
first = false;
} else {
os << ", ";
}
os << p;
}
os << "}";
}
};
class Generate {
PGTransaction &t;
const ErasureCodeInterfaceRef &ec_impl;
const pg_t &pgid;
const ECUtil::stripe_info_t &sinfo;
shard_id_map<ceph::os::Transaction> &transactions;
DoutPrefixProvider *dpp;
const OSDMapRef &osdmap;
pg_log_entry_t *entry;
const hobject_t &oid;
PGTransaction::ObjectOperation& op;
ObjectContextRef obc;
std::map<std::string, std::optional<bufferlist>> xattr_rollback;
const WritePlanObj &plan;
std::optional<ECUtil::shard_extent_map_t> read_sem;
ECUtil::shard_extent_map_t to_write;
std::vector<std::pair<uint64_t, uint64_t>> rollback_extents;
std::vector<shard_id_set> rollback_shards;
uint32_t fadvise_flags = 0;
void all_shards_written();
void shard_written(const shard_id_t shard);
void shards_written(const shard_id_set &shards);
void delete_first();
void zero_truncate_to_delete();
void process_init();
void encode_and_write();
void truncate();
void overlay_writes();
void appends_and_clone_ranges();
void written_and_present_shards();
void attr_updates();
void handle_deletes();
public:
Generate(PGTransaction &t,
ErasureCodeInterfaceRef &ec_impl, pg_t &pgid,
const ECUtil::stripe_info_t &sinfo,
const std::map<hobject_t, ECUtil::shard_extent_map_t> &partial_extents,
std::map<hobject_t, ECUtil::shard_extent_map_t> *written_map,
shard_id_map<ceph::os::Transaction> &transactions,
const OSDMapRef &osdmap,
const hobject_t &oid, PGTransaction::ObjectOperation &op,
WritePlanObj &plan,
DoutPrefixProvider *dpp,
pg_log_entry_t *entry);
};
void generate_transactions(
PGTransaction *_t,
WritePlan &plan,
ceph::ErasureCodeInterfaceRef &ecimpl,
ceph::ErasureCodeInterfaceRef &ec_impl,
pg_t pgid,
const ECUtil::stripe_info_t &sinfo,
const std::map<hobject_t,extent_map> &partial_extents,
const std::map<hobject_t, ECUtil::shard_extent_map_t> &partial_extents,
std::vector<pg_log_entry_t> &entries,
std::map<hobject_t,extent_map> *written,
std::map<shard_id_t, ceph::os::Transaction> *transactions,
std::map<hobject_t, ECUtil::shard_extent_map_t> *written_map,
shard_id_map<ceph::os::Transaction> *transactions,
std::set<hobject_t> *temp_added,
std::set<hobject_t> *temp_removed,
DoutPrefixProvider *dpp,
const ceph_release_t require_osd_release = ceph_release_t::unknown);
};
#endif
const OSDMapRef &osdmap
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,245 +0,0 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
/*
* Ceph - scalable distributed file system
*
* Copyright (C) 2016 Red Hat
*
* 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 "ExtentCache.h"
using std::ostream;
using ceph::bufferlist;
void ExtentCache::extent::_link_pin_state(pin_state &pin_state)
{
ceph_assert(parent_extent_set);
ceph_assert(!parent_pin_state);
parent_pin_state = &pin_state;
pin_state.pin_list.push_back(*this);
}
void ExtentCache::extent::_unlink_pin_state()
{
ceph_assert(parent_extent_set);
ceph_assert(parent_pin_state);
auto liter = pin_state::list::s_iterator_to(*this);
parent_pin_state->pin_list.erase(liter);
parent_pin_state = nullptr;
}
void ExtentCache::extent::unlink()
{
ceph_assert(parent_extent_set);
ceph_assert(parent_pin_state);
_unlink_pin_state();
// remove from extent set
{
auto siter = object_extent_set::set::s_iterator_to(*this);
auto &set = object_extent_set::set::container_from_iterator(siter);
ceph_assert(&set == &(parent_extent_set->extent_set));
set.erase(siter);
}
parent_extent_set = nullptr;
ceph_assert(!parent_pin_state);
}
void ExtentCache::extent::link(
object_extent_set &extent_set,
pin_state &pin_state)
{
ceph_assert(!parent_extent_set);
parent_extent_set = &extent_set;
extent_set.extent_set.insert(*this);
_link_pin_state(pin_state);
}
void ExtentCache::extent::move(
pin_state &to)
{
_unlink_pin_state();
_link_pin_state(to);
}
void ExtentCache::remove_and_destroy_if_empty(object_extent_set &eset)
{
if (eset.extent_set.empty()) {
auto siter = cache_set::s_iterator_to(eset);
auto &set = cache_set::container_from_iterator(siter);
ceph_assert(&set == &per_object_caches);
// per_object_caches owns eset
per_object_caches.erase(eset);
delete &eset;
}
}
ExtentCache::object_extent_set &ExtentCache::get_or_create(
const hobject_t &oid)
{
cache_set::insert_commit_data data;
auto p = per_object_caches.insert_check(oid, Cmp(), data);
if (p.second) {
auto *eset = new object_extent_set(oid);
per_object_caches.insert_commit(*eset, data);
return *eset;
} else {
return *(p.first);
}
}
ExtentCache::object_extent_set *ExtentCache::get_if_exists(
const hobject_t &oid)
{
cache_set::insert_commit_data data;
auto p = per_object_caches.insert_check(oid, Cmp(), data);
if (p.second) {
return nullptr;
} else {
return &*(p.first);
}
}
std::pair<
ExtentCache::object_extent_set::set::iterator,
ExtentCache::object_extent_set::set::iterator
> ExtentCache::object_extent_set::get_containing_range(
uint64_t off, uint64_t len)
{
// fst is first iterator with end after off (may be end)
auto fst = extent_set.upper_bound(off, uint_cmp());
if (fst != extent_set.begin())
--fst;
if (fst != extent_set.end() && off >= (fst->offset + fst->get_length()))
++fst;
// lst is first iterator with start >= off + len (may be end)
auto lst = extent_set.lower_bound(off + len, uint_cmp());
return std::make_pair(fst, lst);
}
extent_set ExtentCache::reserve_extents_for_rmw(
const hobject_t &oid,
write_pin &pin,
const extent_set &to_write,
const extent_set &to_read)
{
if (to_write.empty() && to_read.empty()) {
return extent_set();
}
extent_set must_read;
auto &eset = get_or_create(oid);
extent_set missing;
for (auto &&res: to_write) {
eset.traverse_update(
pin,
res.first,
res.second,
[&](uint64_t off, uint64_t len,
extent *ext, object_extent_set::update_action *action) {
action->action = object_extent_set::update_action::UPDATE_PIN;
if (!ext) {
missing.insert(off, len);
}
});
}
must_read.intersection_of(
to_read,
missing);
return must_read;
}
extent_map ExtentCache::get_remaining_extents_for_rmw(
const hobject_t &oid,
write_pin &pin,
const extent_set &to_get)
{
if (to_get.empty()) {
return extent_map();
}
extent_map ret;
auto &eset = get_or_create(oid);
for (auto &&res: to_get) {
bufferlist bl;
uint64_t cur = res.first;
eset.traverse_update(
pin,
res.first,
res.second,
[&](uint64_t off, uint64_t len,
extent *ext, object_extent_set::update_action *action) {
ceph_assert(off == cur);
cur = off + len;
action->action = object_extent_set::update_action::NONE;
ceph_assert(ext && ext->bl && ext->pinned_by_write());
bl.substr_of(
*(ext->bl),
off - ext->offset,
len);
ret.insert(off, len, bl);
});
}
return ret;
}
void ExtentCache::present_rmw_update(
const hobject_t &oid,
write_pin &pin,
const extent_map &extents)
{
if (extents.empty()) {
return;
}
auto &eset = get_or_create(oid);
for (auto &&res: extents) {
eset.traverse_update(
pin,
res.get_off(),
res.get_len(),
[&](uint64_t off, uint64_t len,
extent *ext, object_extent_set::update_action *action) {
action->action = object_extent_set::update_action::NONE;
ceph_assert(ext && ext->pinned_by_write());
action->bl = bufferlist();
action->bl->substr_of(
res.get_val(),
off - res.get_off(),
len);
});
}
}
ostream &ExtentCache::print(ostream &out) const
{
out << "ExtentCache(" << std::endl;
for (auto esiter = per_object_caches.begin();
esiter != per_object_caches.end();
++esiter) {
out << " Extents(" << esiter->oid << ")[" << std::endl;
for (auto exiter = esiter->extent_set.begin();
exiter != esiter->extent_set.end();
++exiter) {
out << " Extent(" << exiter->offset
<< "~" << exiter->get_length()
<< ":" << exiter->pin_tid()
<< ")" << std::endl;
}
}
return out << ")" << std::endl;
}
ostream &operator<<(ostream &lhs, const ExtentCache &cache)
{
return cache.print(lhs);
}

View File

@ -1,486 +0,0 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
/*
* Ceph - scalable distributed file system
*
* Copyright (C) 2016 Red Hat
*
* 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.
*
*/
#pragma once
#include <map>
#include <list>
#include <vector>
#include <utility>
#include <optional>
#include <boost/intrusive/set.hpp>
#include <boost/intrusive/list.hpp>
#include "include/interval_set.h"
#include "common/interval_map.h"
#include "include/buffer.h"
#include "common/hobject.h"
/**
ExtentCache
The main purpose of this cache is to ensure that we can pipeline
overlapping partial overwrites.
To that end we need to ensure that an extent pinned for an operation is
live until that operation completes. However, a particular extent
might be pinned by multiple operations (several pipelined writes
on the same object).
1) When we complete an operation, we only look at extents owned only
by that operation.
2) Per-extent overhead is fixed size.
2) Per-operation metadata is fixed size.
This is simple enough to realize with two main structures:
- extent: contains a pointer to the pin owning it and intrusive list
pointers to other extents owned by the same pin
- pin_state: contains the list head for extents owned by it
This works as long as we only need to remember one "owner" for
each extent. To make this work, we'll need to leverage some
invariants guaranteed by higher layers:
1) Writes on a particular object must be ordered
2) A particular object will have outstanding reads or writes, but not
both (note that you can have a read while a write is committed, but
not applied).
Our strategy therefore will be to have whichever in-progress op will
finish "last" be the owner of a particular extent. For now, we won't
cache reads, so 2) simply means that we can assume that reads and
recovery operations imply no unstable extents on the object in
question.
Write: WaitRead -> WaitCommit -> Complete
Invariant 1) above actually indicates that we can't have writes
bypassing the WaitRead state while there are writes waiting on
Reads. Thus, the set of operations pinning a particular extent
must always complete in order or arrival.
This suggests that a particular extent may be in only the following
states:
0) Empty (not in the map at all)
1) Write Pending N
- Some write with reqid <= N is currently fetching the data for
this extent
- The extent must persist until Write reqid N completes
- All ops pinning this extent are writes in the WaitRead state of
the Write pipeline (there must be an in progress write, so no
reads can be in progress).
2) Write Pinned N:
- This extent has data corresponding to some reqid M <= N
- The extent must persist until Write reqid N commits
- All ops pinning this extent are writes in some Write
state (all are possible). Reads are not possible
in this state (or the others) due to 2).
All of the above suggests that there are 3 things users can
ask of the cache corresponding to the 3 Write pipelines
states.
*/
/// If someone wants these types, but not ExtentCache, move to another file
struct bl_split_merge {
ceph::buffer::list split(
uint64_t offset,
uint64_t length,
ceph::buffer::list &bl) const {
ceph::buffer::list out;
out.substr_of(bl, offset, length);
return out;
}
bool can_merge(const ceph::buffer::list &left, const ceph::buffer::list &right) const {
return true;
}
ceph::buffer::list merge(ceph::buffer::list &&left, ceph::buffer::list &&right) const {
ceph::buffer::list bl{std::move(left)};
bl.claim_append(right);
return bl;
}
uint64_t length(const ceph::buffer::list &b) const { return b.length(); }
};
using extent_set = interval_set<uint64_t>;
using extent_map = interval_map<uint64_t, ceph::buffer::list, bl_split_merge>;
class ExtentCache {
struct object_extent_set;
struct pin_state;
private:
struct extent {
object_extent_set *parent_extent_set = nullptr;
pin_state *parent_pin_state = nullptr;
boost::intrusive::set_member_hook<> extent_set_member;
boost::intrusive::list_member_hook<> pin_list_member;
uint64_t offset;
uint64_t length;
std::optional<ceph::buffer::list> bl;
uint64_t get_length() const {
return length;
}
bool is_pending() const {
return bl == std::nullopt;
}
bool pinned_by_write() const {
ceph_assert(parent_pin_state);
return parent_pin_state->is_write();
}
uint64_t pin_tid() const {
ceph_assert(parent_pin_state);
return parent_pin_state->tid;
}
extent(uint64_t offset, ceph::buffer::list _bl)
: offset(offset), length(_bl.length()), bl(_bl) {}
extent(uint64_t offset, uint64_t length)
: offset(offset), length(length) {}
bool operator<(const extent &rhs) const {
return offset < rhs.offset;
}
private:
// can briefly violate the two link invariant, used in unlink() and move()
void _link_pin_state(pin_state &pin_state);
void _unlink_pin_state();
public:
void unlink();
void link(object_extent_set &parent_extent_set, pin_state &pin_state);
void move(pin_state &to);
};
struct object_extent_set : boost::intrusive::set_base_hook<> {
hobject_t oid;
explicit object_extent_set(const hobject_t &oid) : oid(oid) {}
using set_member_options = boost::intrusive::member_hook<
extent,
boost::intrusive::set_member_hook<>,
&extent::extent_set_member>;
using set = boost::intrusive::set<extent, set_member_options>;
set extent_set;
bool operator<(const object_extent_set &rhs) const {
return oid < rhs.oid;
}
struct uint_cmp {
bool operator()(uint64_t lhs, const extent &rhs) const {
return lhs < rhs.offset;
}
bool operator()(const extent &lhs, uint64_t rhs) const {
return lhs.offset < rhs;
}
};
std::pair<set::iterator, set::iterator> get_containing_range(
uint64_t offset, uint64_t length);
void erase(uint64_t offset, uint64_t length);
struct update_action {
enum type {
NONE,
UPDATE_PIN
};
type action = NONE;
std::optional<ceph::buffer::list> bl;
};
template <typename F>
void traverse_update(
pin_state &pin,
uint64_t offset,
uint64_t length,
F &&f) {
auto range = get_containing_range(offset, length);
if (range.first == range.second || range.first->offset > offset) {
uint64_t extlen = range.first == range.second ?
length : range.first->offset - offset;
update_action action;
f(offset, extlen, nullptr, &action);
ceph_assert(!action.bl || action.bl->length() == extlen);
if (action.action == update_action::UPDATE_PIN) {
extent *ext = action.bl ?
new extent(offset, *action.bl) :
new extent(offset, extlen);
ext->link(*this, pin);
} else {
ceph_assert(!action.bl);
}
}
for (auto p = range.first; p != range.second;) {
extent *ext = &*p;
++p;
uint64_t extoff = std::max(ext->offset, offset);
uint64_t extlen = std::min(
ext->length - (extoff - ext->offset),
offset + length - extoff);
update_action action;
f(extoff, extlen, ext, &action);
ceph_assert(!action.bl || action.bl->length() == extlen);
extent *final_extent = nullptr;
if (action.action == update_action::NONE) {
final_extent = ext;
} else {
pin_state *ps = ext->parent_pin_state;
ext->unlink();
if ((ext->offset < offset) &&
(ext->offset + ext->get_length() > offset)) {
extent *head = nullptr;
if (ext->bl) {
ceph::buffer::list bl;
bl.substr_of(
*(ext->bl),
0,
offset - ext->offset);
head = new extent(ext->offset, bl);
} else {
head = new extent(
ext->offset, offset - ext->offset);
}
head->link(*this, *ps);
}
if ((ext->offset + ext->length > offset + length) &&
(offset + length > ext->offset)) {
uint64_t nlen =
(ext->offset + ext->get_length()) - (offset + length);
extent *tail = nullptr;
if (ext->bl) {
ceph::buffer::list bl;
bl.substr_of(
*(ext->bl),
ext->get_length() - nlen,
nlen);
tail = new extent(offset + length, bl);
} else {
tail = new extent(offset + length, nlen);
}
tail->link(*this, *ps);
}
if (action.action == update_action::UPDATE_PIN) {
if (ext->bl) {
ceph::buffer::list bl;
bl.substr_of(
*(ext->bl),
extoff - ext->offset,
extlen);
final_extent = new ExtentCache::extent(
extoff,
bl);
} else {
final_extent = new ExtentCache::extent(
extoff, extlen);
}
final_extent->link(*this, pin);
}
delete ext;
}
if (action.bl) {
ceph_assert(final_extent);
ceph_assert(final_extent->length == action.bl->length());
final_extent->bl = *(action.bl);
}
uint64_t next_off = p == range.second ?
offset + length : p->offset;
if (extoff + extlen < next_off) {
uint64_t tailoff = extoff + extlen;
uint64_t taillen = next_off - tailoff;
update_action action;
f(tailoff, taillen, nullptr, &action);
ceph_assert(!action.bl || action.bl->length() == taillen);
if (action.action == update_action::UPDATE_PIN) {
extent *ext = action.bl ?
new extent(tailoff, *action.bl) :
new extent(tailoff, taillen);
ext->link(*this, pin);
} else {
ceph_assert(!action.bl);
}
}
}
}
};
struct Cmp {
bool operator()(const hobject_t &oid, const object_extent_set &rhs) const {
return oid < rhs.oid;
}
bool operator()(const object_extent_set &lhs, const hobject_t &oid) const {
return lhs.oid < oid;
}
};
object_extent_set &get_or_create(const hobject_t &oid);
object_extent_set *get_if_exists(const hobject_t &oid);
void remove_and_destroy_if_empty(object_extent_set &set);
using cache_set = boost::intrusive::set<object_extent_set>;
cache_set per_object_caches;
uint64_t next_write_tid = 1;
uint64_t next_read_tid = 1;
struct pin_state {
uint64_t tid = 0;
enum pin_type_t {
NONE,
WRITE,
};
pin_type_t pin_type = NONE;
bool is_write() const { return pin_type == WRITE; }
pin_state(const pin_state &other) = delete;
pin_state &operator=(const pin_state &other) = delete;
pin_state(pin_state &&other) = delete;
pin_state() = default;
using list_member_options = boost::intrusive::member_hook<
extent,
boost::intrusive::list_member_hook<>,
&extent::pin_list_member>;
using list = boost::intrusive::list<extent, boost::intrusive::constant_time_size<false>, list_member_options>;
list pin_list;
~pin_state() {
ceph_assert(pin_list.empty());
ceph_assert(tid == 0);
ceph_assert(pin_type == NONE);
}
void _open(uint64_t in_tid, pin_type_t in_type) {
ceph_assert(pin_type == NONE);
ceph_assert(in_tid > 0);
tid = in_tid;
pin_type = in_type;
}
};
void release_pin(pin_state &p) {
for (auto iter = p.pin_list.begin(); iter != p.pin_list.end(); ) {
std::unique_ptr<extent> extent(&*iter); // we now own this
iter++; // unlink will invalidate
ceph_assert(extent->parent_extent_set);
auto &eset = *(extent->parent_extent_set);
extent->unlink();
remove_and_destroy_if_empty(eset);
}
p.tid = 0;
p.pin_type = pin_state::NONE;
}
public:
class write_pin : private pin_state {
friend class ExtentCache;
private:
void open(uint64_t in_tid) {
_open(in_tid, pin_state::WRITE);
}
public:
write_pin() : pin_state() {}
};
void open_write_pin(write_pin &pin) {
pin.open(next_write_tid++);
}
/**
* Reserves extents required for rmw, and learn
* which need to be read
*
* Pins all extents in to_write. Returns subset of to_read not
* currently present in the cache. Caller must obtain those
* extents before calling get_remaining_extents_for_rmw.
*
* Transition table:
* - Empty -> Write Pending pin.reqid
* - Write Pending N -> Write Pending pin.reqid
* - Write Pinned N -> Write Pinned pin.reqid
*
* @param oid [in] object undergoing rmw
* @param pin [in,out] pin to use (obtained from create_write_pin)
* @param to_write [in] extents which will be written
* @param to_read [in] extents to read prior to write (must be subset
* of to_write)
* @return subset of to_read which isn't already present or pending
*/
extent_set reserve_extents_for_rmw(
const hobject_t &oid,
write_pin &pin,
const extent_set &to_write,
const extent_set &to_read);
/**
* Gets extents required for rmw not returned from
* reserve_extents_for_rmw
*
* Requested extents (to_get) must be the set to_read \ the set
* returned from reserve_extents_for_rmw. No transition table,
* all extents at this point must be present and already pinned
* for this pin by reserve_extents_for_rmw.
*
* @param oid [in] object
* @param pin [in,out] pin associated with this IO
* @param to_get [in] extents to get (see above for restrictions)
* @return map of buffers from to_get
*/
extent_map get_remaining_extents_for_rmw(
const hobject_t &oid,
write_pin &pin,
const extent_set &to_get);
/**
* Updates the cache to reflect the rmw write
*
* All presented extents must already have been specified in
* reserve_extents_for_rmw under to_write.
*
* Transition table:
* - Empty -> invalid, must call reserve_extents_for_rmw first
* - Write Pending N -> Write Pinned N, update buffer
* (assert N >= pin.reqid)
* - Write Pinned N -> Update buffer (assert N >= pin.reqid)
*
* @param oid [in] object
* @param pin [in,out] pin associated with this IO
* @param extents [in] map of buffers to update
* @return void
*/
void present_rmw_update(
const hobject_t &oid,
write_pin &pin,
const extent_map &extents);
/**
* Release all buffers pinned by pin
*/
void release_write_pin(
write_pin &pin) {
release_pin(pin);
}
std::ostream &print(std::ostream &out) const;
};
std::ostream &operator <<(std::ostream &lhs, const ExtentCache &cache);

View File

@ -17,6 +17,7 @@
#include <errno.h>
#include <stdlib.h>
#include "erasure-code/ErasureCodePlugin.h"
#include "log/Log.h"
#include "global/global_context.h"
@ -62,6 +63,80 @@ TEST(ErasureCodePlugin, factory)
}
}
bufferptr create_bufferptr(uint64_t value) {
bufferlist bl;
bl.append_zero(4096);
memcpy(bl.c_str(), &value, sizeof(value));
return bl.begin().get_current_ptr();
}
TEST(ErasureCodePlugin, parity_delta_write) {
ErasureCodePluginRegistry &instance = ErasureCodePluginRegistry::instance();
ErasureCodeInterfaceRef erasure_code;
ErasureCodeProfile profile;
profile["technique"] = "reed_sol_van";
profile["k"] = "5";
int k=5;
profile["m"] = "3";
int m=3;
EXPECT_EQ(0, instance.factory("jerasure",
g_conf().get_val<std::string>("erasure_code_dir"),
profile,
&erasure_code, &cerr));
shard_id_map<bufferptr> data(8);
shard_id_map<bufferptr> coding(8);
shard_id_map<bufferptr> coding2(8);
shard_id_map<bufferptr> decode_in(8);
shard_id_map<bufferptr> decode_out(8);
uint32_t seeds[] = {100, 101, 102, 103, 104};
uint32_t overwrite3 = 1032;
for (shard_id_t s; s < k; ++s) {
data[s] = create_bufferptr(seeds[int(s)]);
}
for (shard_id_t s(k); s < k + m; ++s) {
coding[s] = create_bufferptr(-1);
coding2[s] = create_bufferptr(-1);
}
// Do a normal encode.
erasure_code->encode_chunks(data, coding);
shard_id_map<bufferptr> delta(8);
delta[shard_id_t(3)] = create_bufferptr(-1);
bufferptr overwrite_bp = create_bufferptr(overwrite3);
erasure_code->encode_delta(data[shard_id_t(3)], overwrite_bp, &delta[shard_id_t(3)]);
erasure_code->apply_delta(delta, coding);
data[shard_id_t(3)] = overwrite_bp;
erasure_code->encode_chunks(data, coding2);
for (shard_id_t s(k); s < k + m; ++s) {
ASSERT_EQ(*(uint32_t*)coding[s].c_str(), *(uint32_t*)coding2[s].c_str());
}
data.erase(shard_id_t(4));
data.emplace(shard_id_t(4), (char*)malloc(4096), 4096);
shard_id_set want;
want.insert_range(shard_id_t(0), 5);
decode_in[shard_id_t(0)] = data[shard_id_t(0)];
decode_in[shard_id_t(1)] = data[shard_id_t(1)];
decode_in[shard_id_t(2)] = data[shard_id_t(2)];
decode_in[shard_id_t(3)] = data[shard_id_t(3)];
decode_out[shard_id_t(4)] = data[shard_id_t(4)];
decode_in[shard_id_t(6)] = coding[shard_id_t(6)];
ASSERT_EQ(0, erasure_code->decode_chunks(want, decode_in, decode_out));
seeds[3] = overwrite3;
for (shard_id_t s(0); s < k; ++s) {
ASSERT_EQ(seeds[int(s)], *(uint32_t*)data[s].c_str());
}
}
/*
* Local Variables:
* compile-command: "cd ../.. ; make -j4 &&

View File

@ -76,6 +76,14 @@ add_executable(unittest_ecbackend
add_ceph_unittest(unittest_ecbackend)
target_link_libraries(unittest_ecbackend osd global)
# unittest_ecutil
add_executable(unittest_ecutil
TestECUtil.cc
$<TARGET_OBJECTS:unit-main>
)
add_ceph_unittest(unittest_ecutil)
target_link_libraries(unittest_ecutil osd global)
# unittest_osdscrub
add_executable(unittest_osdscrub
TestOSDScrub.cc

File diff suppressed because it is too large Load Diff

1034
src/test/osd/TestECUtil.cc Normal file

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
#include "osd/PGTransaction.h"
#include "osd/ECTransaction.h"
#include "common/debug.h"
#include "osd/ECBackend.h"
#include "test/unit.cc"
@ -27,99 +28,344 @@ struct mydpp : public DoutPrefixProvider {
#define dout_context g_ceph_context
TEST(ectransaction, two_writes_separated)
struct ECTestOp : ECCommon::RMWPipeline::Op {
PGTransactionUPtr t;
};
TEST(ectransaction, two_writes_separated_append)
{
hobject_t h;
PGTransactionUPtr t(new PGTransaction);
PGTransaction::ObjectOperation op;
bufferlist a, b;
t->create(h);
a.append_zero(565760);
t->write(h, 0, a.length(), a, 0);
op.buffer_updates.insert(0, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
b.append_zero(2437120);
t->write(h, 669856, b.length(), b, 0);
op.buffer_updates.insert(669856, b.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{b, 0});
ECUtil::stripe_info_t sinfo(2, 2, 8192);
auto plan = ECTransaction::get_write_plan(
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 2, 8192, &pool);
shard_id_set shards;
shards.insert_range(shard_id_t(), 4);
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
*t,
[&](const hobject_t &i) {
ECUtil::HashInfoRef ref(new ECUtil::HashInfo(1));
return ref;
},
&dpp);
generic_derr << "to_read " << plan.to_read << dendl;
generic_derr << "will_write " << plan.will_write << dendl;
shards,
shards,
false,
0,
std::nullopt,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
ASSERT_EQ(0u, plan.to_read.size());
ASSERT_EQ(1u, plan.will_write.size());
generic_derr << "plan " << plan << dendl;
ASSERT_FALSE(plan.to_read);
ASSERT_EQ(4u, plan.will_write.shard_count());
}
TEST(ectransaction, two_writes_nearby)
TEST(ectransaction, two_writes_separated_misaligned_overwrite)
{
hobject_t h;
PGTransactionUPtr t(new PGTransaction);
PGTransaction::ObjectOperation op;
bufferlist a, b;
t->create(h);
// two nearby writes, both partly touching the same 8192-byte stripe
ECUtil::stripe_info_t sinfo(2, 2, 8192);
a.append_zero(565760);
t->write(h, 0, a.length(), a, 0);
op.buffer_updates.insert(0, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
b.append_zero(2437120);
t->write(h, 569856, b.length(), b, 0);
op.buffer_updates.insert(669856, b.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{b, 0});
auto plan = ECTransaction::get_write_plan(
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 2, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 3112960;
shard_id_set shards;
shards.insert_range(shard_id_t(), 4);
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
*t,
[&](const hobject_t &i) {
ECUtil::HashInfoRef ref(new ECUtil::HashInfo(1));
return ref;
},
&dpp);
generic_derr << "to_read " << plan.to_read << dendl;
generic_derr << "will_write " << plan.will_write << dendl;
shards,
shards,
false,
oi.size,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
ASSERT_EQ(0u, plan.to_read.size());
ASSERT_EQ(1u, plan.will_write.size());
generic_derr << "plan " << plan << dendl;
ASSERT_EQ(2u, (*plan.to_read).shard_count());
ASSERT_EQ(4u, plan.will_write.shard_count());
}
TEST(ectransaction, many_writes)
// Test writing to an object at an offset which is beyond the end of the
// current object.
TEST(ectransaction, partial_write)
{
hobject_t h;
PGTransactionUPtr t(new PGTransaction);
bufferlist a, b;
a.append_zero(512);
b.append_zero(4096);
t->create(h);
PGTransaction::ObjectOperation op;
bufferlist a;
ECUtil::stripe_info_t sinfo(2, 2, 8192);
// write 2801664~512
// write 2802176~512
// write 2802688~512
// write 2803200~512
t->write(h, 2801664, a.length(), a, 0);
t->write(h, 2802176, a.length(), a, 0);
t->write(h, 2802688, a.length(), a, 0);
t->write(h, 2803200, a.length(), a, 0);
// Start by writing 8 bytes to the start of an object.
a.append_zero(8);
op.buffer_updates.insert(0, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
// write 2805760~4096
// write 2809856~4096
// write 2813952~4096
t->write(h, 2805760, b.length(), b, 0);
t->write(h, 2809856, b.length(), b, 0);
t->write(h, 2813952, b.length(), b, 0);
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 8;
shard_id_set shards;
shards.insert_range(shard_id_t(), 3);
auto plan = ECTransaction::get_write_plan(
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
*t,
[&](const hobject_t &i) {
ECUtil::HashInfoRef ref(new ECUtil::HashInfo(1));
return ref;
},
&dpp);
generic_derr << "to_read " << plan.to_read << dendl;
generic_derr << "will_write " << plan.will_write << dendl;
shards,
shards,
false,
0,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
ASSERT_EQ(0u, plan.to_read.size());
ASSERT_EQ(1u, plan.will_write.size());
generic_derr << "plan " << plan << dendl;
// The object is empty, so we should have no reads and an 4k write.
ASSERT_FALSE(plan.to_read);
extent_set ref_write;
ref_write.insert(0, 4096);
ASSERT_EQ(2u, plan.will_write.shard_count());
ASSERT_EQ(ref_write, plan.will_write.at(shard_id_t(0)));
ASSERT_EQ(ref_write, plan.will_write.at(shard_id_t(2)));
}
TEST(ectransaction, overlapping_write_non_aligned)
{
hobject_t h;
PGTransaction::ObjectOperation op;
bufferlist a;
// Start by writing 8 bytes to the start of an object.
a.append_zero(8);
op.buffer_updates.insert(0, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 8;
shard_id_set shards;
shards.insert_range(shard_id_t(), 4);
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
shards,
shards,
false,
8,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
generic_derr << "plan " << plan << dendl;
// There should be no overlap of this read.
ASSERT_EQ(1u, (*plan.to_read).shard_count());
extent_set ref;
ref.insert(0, 4096);
ASSERT_EQ(2u, plan.will_write.shard_count());
ASSERT_EQ(1u, (*plan.to_read).shard_count());
ASSERT_EQ(ref, plan.will_write.at(shard_id_t(0)));
ASSERT_EQ(ref, plan.will_write.at(shard_id_t(2)));
}
TEST(ectransaction, test_appending_write_non_aligned)
{
hobject_t h;
PGTransaction::ObjectOperation op;
bufferlist a;
// Start by writing 8 bytes to the start of an object.
a.append_zero(4096);
op.buffer_updates.insert(3*4096, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 4*4096;
shard_id_set shards;
shards.insert_range(shard_id_t(), 4);
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
shards,
shards,
false,
8,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
generic_derr << "plan " << plan << dendl;
// We are growing an option from zero with a hole.
ASSERT_FALSE(plan.to_read);
// The writes will cover not cover the zero parts
ECUtil::shard_extent_set_t ref_write(sinfo.get_k_plus_m());
ref_write[shard_id_t(1)].insert(4096, 4096);
ref_write[shard_id_t(2)].insert(4096, 4096);
ASSERT_EQ(ref_write, plan.will_write);
}
TEST(ectransaction, append_with_large_hole)
{
hobject_t h;
PGTransaction::ObjectOperation op;
bufferlist a;
// We have a 4k write quite a way after the current limit of a 4k object
a.append_zero(4096);
op.buffer_updates.insert(24*4096, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 25*4096;
shard_id_set shards;
shards.insert_range(shard_id_t(), 4);
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
shards,
shards,
false,
4096,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
generic_derr << "plan " << plan << dendl;
// Should not require any reads.
ASSERT_FALSE(plan.to_read);
// The writes will cover the new zero parts.
ECUtil::shard_extent_set_t ref_write(sinfo.get_k_plus_m());
ref_write[shard_id_t(0)].insert(12*4096, 4096);
ref_write[shard_id_t(2)].insert(12*4096, 4096);
ASSERT_EQ(ref_write, plan.will_write);
}
TEST(ectransaction, test_append_not_page_aligned_with_large_hole)
{
hobject_t h;
PGTransaction::ObjectOperation op;
bufferlist a;
// We have a 4k write quite a way after the current limit of a 4k object
a.append_zero(2048);
op.buffer_updates.insert(24*4096 + 1024, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 25*4096;
shard_id_set shards;
shards.insert_range(shard_id_t(), 3);
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
shards,
shards,
false,
4096,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
generic_derr << "plan " << plan << dendl;
// No reads (because not yet written)
ASSERT_FALSE(plan.to_read);
// Writes should grow to 4k
ECUtil::shard_extent_set_t ref_write(sinfo.get_k_plus_m());
ref_write[shard_id_t(0)].insert(12*4096, 4096);
ref_write[shard_id_t(2)].insert(12*4096, 4096);
ASSERT_EQ(ref_write, plan.will_write);
}
TEST(ectransaction, test_overwrite_with_missing)
{
hobject_t h;
PGTransaction::ObjectOperation op, op2;
bufferlist a;
// We have a 4k write quite a way after the current limit of a 4k object
a.append_zero(14*1024);
op.buffer_updates.insert(0, a.length(), PGTransaction::ObjectOperation::BufferUpdate::Write{a, 0});
pg_pool_t pool;
pool.set_flag(pg_pool_t::FLAG_EC_OPTIMIZATIONS);
ECUtil::stripe_info_t sinfo(2, 1, 8192, &pool, std::vector<shard_id_t>(0));
object_info_t oi;
oi.size = 42*1024;
shard_id_set shards;
shards.insert(shard_id_t(0));
shards.insert(shard_id_t(1));
ECTransaction::WritePlanObj plan(
h,
op,
sinfo,
shards,
shards,
false,
42*1024,
oi,
std::nullopt,
ECUtil::HashInfoRef(new ECUtil::HashInfo(1)),
nullptr,
0);
generic_derr << "plan " << plan << dendl;
// No reads (because not yet written)
ASSERT_TRUE(plan.to_read);
ECUtil::shard_extent_set_t ref_read(sinfo.get_k_plus_m());
ref_read[shard_id_t(1)].insert(4096, 4096);
ASSERT_EQ(ref_read, plan.to_read);
// Writes should grow to 4k
ECUtil::shard_extent_set_t ref_write(sinfo.get_k_plus_m());
ref_write[shard_id_t(0)].insert(0, 8192);
ref_write[shard_id_t(1)].insert(0, 8192);
ASSERT_EQ(ref_write, plan.will_write);
}

View File

@ -14,269 +14,682 @@
#include <gtest/gtest.h>
#include "osd/ExtentCache.h"
#include <iostream>
#include "osd/ECExtentCache.h"
using namespace std;
using namespace ECUtil;
extent_map imap_from_vector(vector<pair<uint64_t, uint64_t> > &&in)
shard_extent_map_t imap_from_vector(vector<vector<pair<uint64_t, uint64_t>>> &&in, stripe_info_t const *sinfo)
{
extent_map out;
for (auto &&tup: in) {
bufferlist bl;
bl.append_zero(tup.second);
out.insert(tup.first, bl.length(), bl);
shard_extent_map_t out(sinfo);
for (int shard = 0; shard < (int)in.size(); shard++) {
for (auto &&tup: in[shard]) {
bufferlist bl;
bl.append_zero(tup.second);
out.insert_in_shard(shard_id_t(shard), tup.first, bl);
}
}
return out;
}
extent_map imap_from_iset(const extent_set &set)
shard_extent_map_t imap_from_iset(const shard_extent_set_t &sset, stripe_info_t *sinfo)
{
extent_map out;
for (auto &&iter: set) {
bufferlist bl;
bl.append_zero(iter.second);
out.insert(iter.first, iter.second, bl);
shard_extent_map_t out(sinfo);
for (auto &&[shard, set]: sset) {
for (auto &&iter: set) {
bufferlist bl;
bl.append_zero(iter.second);
out.insert_in_shard(shard, iter.first, bl);
}
}
return out;
}
extent_set iset_from_vector(vector<pair<uint64_t, uint64_t> > &&in)
shard_extent_set_t iset_from_vector(vector<vector<pair<uint64_t, uint64_t>>> &&in, const stripe_info_t *sinfo)
{
extent_set out;
for (auto &&tup: in) {
out.insert(tup.first, tup.second);
shard_extent_set_t out(sinfo->get_k_plus_m());
for (int shard = 0; shard < (int)in.size(); shard++) {
for (auto &&tup: in[shard]) {
out[shard_id_t(shard)].insert(tup.first, tup.second);
}
}
return out;
}
TEST(extentcache, simple_write)
struct Client : public ECExtentCache::BackendReadListener
{
hobject_t oid;
hobject_t oid = hobject_t().make_temp_hobject("My first object");
stripe_info_t sinfo;
ECExtentCache::LRU lru;
ECExtentCache cache;
optional<shard_extent_set_t> active_reads;
list<shard_extent_map_t> results;
ExtentCache c;
ExtentCache::write_pin pin;
c.open_write_pin(pin);
Client(uint64_t chunk_size, int k, int m, uint64_t cache_size) :
sinfo(k, m, k*chunk_size, vector<shard_id_t>(0)),
lru(cache_size), cache(*this, lru, sinfo, g_ceph_context) {};
auto to_read = iset_from_vector(
{{0, 2}, {8, 2}, {20, 2}});
auto to_write = iset_from_vector(
{{0, 10}, {20, 4}});
auto must_read = c.reserve_extents_for_rmw(
oid, pin, to_write, to_read);
ASSERT_EQ(
must_read,
to_read);
void backend_read(hobject_t _oid, const shard_extent_set_t& request,
uint64_t object_size) override {
ceph_assert(oid == _oid);
active_reads = request;
}
c.print(std::cerr);
void cache_ready(const hobject_t& _oid, const shard_extent_map_t& _result)
{
ceph_assert(oid == _oid);
results.emplace_back(_result);
}
auto got = imap_from_iset(must_read);
auto pending_read = to_read;
pending_read.subtract(must_read);
void complete_read()
{
auto reads_done = imap_from_iset(*active_reads, &sinfo);
active_reads.reset(); // set before done, as may be called back.
cache.read_done(oid, std::move(reads_done));
}
auto pending = c.get_remaining_extents_for_rmw(
oid,
pin,
pending_read);
ASSERT_TRUE(pending.empty());
void complete_write(ECExtentCache::OpRef &op)
{
shard_extent_map_t emap = imap_from_iset(op->get_writes(), &sinfo);
//Fill in the parity. Parity correctness does not matter to the cache.
emap.insert_parity_buffers();
results.clear();
cache.write_done(op, std::move(emap));
}
auto write_map = imap_from_iset(to_write);
c.present_rmw_update(
oid,
pin,
write_map);
void cache_execute(ECExtentCache::OpRef &op)
{
list<ECExtentCache::OpRef> l;
l.emplace_back(op);
cache.execute(l);
}
c.release_write_pin(pin);
const stripe_info_t *get_stripe_info() const { return &sinfo; }
};
TEST(ECExtentCache, double_write_done)
{
Client cl(32, 2, 1, 64);
auto to_write = iset_from_vector({{{0, 10}}, {{0, 10}}}, cl.get_stripe_info());
optional op = cl.cache.prepare(cl.oid, nullopt, to_write, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op);
cl.complete_write(*op);
}
TEST(extentcache, write_write_overlap)
TEST(ECExtentCache, simple_write)
{
hobject_t oid;
Client cl(32, 2, 1, 64);
{
auto to_read = iset_from_vector( {{{0, 2}}, {{0, 2}}}, cl.get_stripe_info());
auto to_write = iset_from_vector({{{0, 10}}, {{0, 10}}}, cl.get_stripe_info());
ExtentCache c;
ExtentCache::write_pin pin;
c.open_write_pin(pin);
/* OpRef request(hobject_t const &oid,
std::optional<std::shard_extent_set_t> const &to_read,
std::shard_extent_set_t const &write,
uint64_t orig_size,
uint64_t projected_size,
CacheReadyCb &&ready_cb)
*/
// start write 1
auto to_read = iset_from_vector(
{{0, 2}, {8, 2}, {20, 2}});
auto to_write = iset_from_vector(
{{0, 10}, {20, 4}});
auto must_read = c.reserve_extents_for_rmw(
oid, pin, to_write, to_read);
ASSERT_EQ(
must_read,
to_read);
optional op = cl.cache.prepare(cl.oid, to_read, to_write, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op);
ASSERT_EQ(to_read, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
cl.complete_read();
c.print(std::cerr);
ASSERT_FALSE(cl.active_reads);
ASSERT_EQ(1, cl.results.size());
ASSERT_EQ(to_read, cl.results.front().get_extent_set());
cl.complete_write(*op);
// start write 2
ExtentCache::write_pin pin2;
c.open_write_pin(pin2);
auto to_read2 = iset_from_vector(
{{2, 4}, {10, 4}, {18, 4}});
auto to_write2 = iset_from_vector(
{{2, 12}, {18, 12}});
auto must_read2 = c.reserve_extents_for_rmw(
oid, pin2, to_write2, to_read2);
ASSERT_EQ(
must_read2,
iset_from_vector({{10, 4}, {18, 2}}));
ASSERT_FALSE(cl.active_reads);
ASSERT_TRUE(cl.results.empty());
op.reset();
}
c.print(std::cerr);
// Repeating the same read should complete without a backend read..
{
auto to_read = iset_from_vector( {{{0, 2}}, {{0, 2}}}, cl.get_stripe_info());
auto to_write = iset_from_vector({{{0, 10}}, {{0, 10}}}, cl.get_stripe_info());
optional op = cl.cache.prepare(cl.oid, to_read, to_write, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op);
ASSERT_FALSE(cl.active_reads);
ASSERT_FALSE(cl.results.empty());
ASSERT_EQ(1, cl.results.size());
ASSERT_EQ(to_read, cl.results.front().get_extent_set());
cl.complete_write(*op);
op.reset();
}
// complete read for write 1 and start commit
auto got = imap_from_iset(must_read);
auto pending_read = to_read;
pending_read.subtract(must_read);
auto pending = c.get_remaining_extents_for_rmw(
oid,
pin,
pending_read);
ASSERT_TRUE(pending.empty());
// Perform a read overlapping with the previous write, but not hte previous read.
// This should not result in any backend reads, since the cache can be honoured
// from the previous write.
{
auto to_read = iset_from_vector( {{{2, 2}}, {{2, 2}}}, cl.get_stripe_info());
auto to_write = iset_from_vector({{{0, 10}}, {{0, 10}}}, cl.get_stripe_info());
optional op = cl.cache.prepare(cl.oid, to_read, to_write, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op);
auto write_map = imap_from_iset(to_write);
c.present_rmw_update(
oid,
pin,
write_map);
c.print(std::cerr);
// complete read for write 2 and start commit
auto pending_read2 = to_read2;
pending_read2.subtract(must_read2);
auto pending2 = c.get_remaining_extents_for_rmw(
oid,
pin2,
pending_read2);
ASSERT_EQ(
pending2,
imap_from_iset(pending_read2));
auto write_map2 = imap_from_iset(to_write2);
c.present_rmw_update(
oid,
pin2,
write_map2);
c.print(std::cerr);
c.release_write_pin(pin);
c.print(std::cerr);
c.release_write_pin(pin2);
// SHould have remained in LRU!
ASSERT_FALSE(cl.active_reads);
ASSERT_EQ(1, cl.results.size());
ASSERT_EQ(to_read, cl.results.front().get_extent_set());
cl.complete_write(*op);
op.reset();
}
}
TEST(extentcache, write_write_overlap2)
{
hobject_t oid;
TEST(ECExtentCache, sequential_appends) {
Client cl(32, 2, 1, 32);
ExtentCache c;
ExtentCache::write_pin pin;
c.open_write_pin(pin);
auto to_write1 = iset_from_vector({{{0, 10}}}, cl.get_stripe_info());
// start write 1
auto to_read = extent_set();
auto to_write = iset_from_vector(
{{659456, 4096}});
auto must_read = c.reserve_extents_for_rmw(
oid, pin, to_write, to_read);
ASSERT_EQ(
must_read,
to_read);
// The first write...
optional op1 = cl.cache.prepare(cl.oid, nullopt, to_write1, 0, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
c.print(std::cerr);
// Write should have been honoured immediately.
ASSERT_FALSE(cl.results.empty());
auto to_write2 = iset_from_vector({{{10, 10}}}, cl.get_stripe_info());
cl.complete_write(*op1);
ASSERT_TRUE(cl.results.empty());
// start write 2
ExtentCache::write_pin pin2;
c.open_write_pin(pin2);
auto to_read2 = extent_set();
auto to_write2 = iset_from_vector(
{{663552, 4096}});
auto must_read2 = c.reserve_extents_for_rmw(
oid, pin2, to_write2, to_read2);
ASSERT_EQ(
must_read2,
to_read2);
// The first write...
optional op2 = cl.cache.prepare(cl.oid, nullopt, to_write1, 10, 20, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op2);
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op2);
// start write 3
ExtentCache::write_pin pin3;
c.open_write_pin(pin3);
auto to_read3 = iset_from_vector({{659456, 8192}});
auto to_write3 = iset_from_vector({{659456, 8192}});
auto must_read3 = c.reserve_extents_for_rmw(
oid, pin3, to_write3, to_read3);
ASSERT_EQ(
must_read3,
extent_set());
c.print(std::cerr);
// complete read for write 1 and start commit
auto got = imap_from_iset(must_read);
auto pending_read = to_read;
pending_read.subtract(must_read);
auto pending = c.get_remaining_extents_for_rmw(
oid,
pin,
pending_read);
ASSERT_TRUE(pending.empty());
auto write_map = imap_from_iset(to_write);
c.present_rmw_update(
oid,
pin,
write_map);
c.print(std::cerr);
// complete read for write 2 and start commit
auto pending_read2 = to_read2;
pending_read2.subtract(must_read2);
auto pending2 = c.get_remaining_extents_for_rmw(
oid,
pin2,
pending_read2);
ASSERT_EQ(
pending2,
imap_from_iset(pending_read2));
auto write_map2 = imap_from_iset(to_write2);
c.present_rmw_update(
oid,
pin2,
write_map2);
// complete read for write 2 and start commit
auto pending_read3 = to_read3;
pending_read3.subtract(must_read3);
auto pending3 = c.get_remaining_extents_for_rmw(
oid,
pin3,
pending_read3);
ASSERT_EQ(
pending3,
imap_from_iset(pending_read3));
auto write_map3 = imap_from_iset(to_write3);
c.present_rmw_update(
oid,
pin3,
write_map3);
c.print(std::cerr);
c.release_write_pin(pin);
c.print(std::cerr);
c.release_write_pin(pin2);
c.print(std::cerr);
c.release_write_pin(pin3);
}
TEST(ECExtentCache, multiple_writes)
{
Client cl(32, 2, 1, 32);
auto to_read1 = iset_from_vector( {{{0, 2}}}, cl.get_stripe_info());
auto to_write1 = iset_from_vector({{{0, 10}}}, cl.get_stripe_info());
// This should drive a request for this IO, which we do not yet honour.
optional op1 = cl.cache.prepare(cl.oid, to_read1, to_write1, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Perform another request. We should not see any change in the read requests.
auto to_read2 = iset_from_vector( {{{8, 4}}}, cl.get_stripe_info());
auto to_write2 = iset_from_vector({{{10, 10}}}, cl.get_stripe_info());
optional op2 = cl.cache.prepare(cl.oid, to_read2, to_write2, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op2);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Perform another request, this to check that reads are coalesced.
auto to_read3 = iset_from_vector( {{{32, 6}}}, cl.get_stripe_info());
auto to_write3 = iset_from_vector({}, cl.get_stripe_info());
optional op3 = cl.cache.prepare(cl.oid, to_read3, to_write3, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op3);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Finally op4, with no reads.
auto to_write4 = iset_from_vector({{{20, 10}}}, cl.get_stripe_info());
optional op4 = cl.cache.prepare(cl.oid, nullopt, to_write4, 10, 10, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op4);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Completing the first read will allow the first write and start a batched read.
// Note that the cache must not read what was written in op 1.
cl.complete_read();
auto expected_read = iset_from_vector({{{10,2}, {32,6}}}, cl.get_stripe_info());
ASSERT_EQ(expected_read, cl.active_reads);
ASSERT_EQ(1, cl.results.size());
ASSERT_EQ(to_read1, cl.results.front().get_extent_set());
cl.complete_write(*op1);
// The next write requires some more reads, so should not occur.
ASSERT_TRUE(cl.results.empty());
// All reads complete, this should allow for op2 to be ready.
cl.complete_read();
ASSERT_FALSE(cl.active_reads);
ASSERT_EQ(3, cl.results.size());
auto result = cl.results.begin();
ASSERT_EQ(to_read2, result++->get_extent_set());
ASSERT_EQ(to_read3, result++->get_extent_set());
ASSERT_TRUE(result++->empty());
cl.complete_write(*op2);
cl.complete_write(*op3);
cl.complete_write(*op4);
op1.reset();
op2.reset();
op3.reset();
op4.reset();
}
int dummies;
struct Dummy
{
Dummy() {dummies++;}
~Dummy() {dummies--;}
};
TEST(ECExtentCache, on_change)
{
Client cl(32, 2, 1, 64);
auto to_read1 = iset_from_vector( {{{0, 2}}}, cl.get_stripe_info());
auto to_write1 = iset_from_vector({{{0, 10}}}, cl.get_stripe_info());
optional<ECExtentCache::OpRef> op;
optional<shared_ptr<Dummy>> dummy;
dummy.emplace(make_shared<Dummy>());
ceph_assert(dummies == 1);
{
shared_ptr<Dummy> d = *dummy;
/* Here we generate an op that we never expect to be completed. Note that
* some static code analysis tools suggest deleting d here. DO NOT DO THIS
* as we are relying on side effects from the destruction of d in this test.
*/
op.emplace(cl.cache.prepare(cl.oid, to_read1, to_write1, 10, 10, false,
[d](ECExtentCache::OpRef &ignored)
{
ceph_abort("Should be cancelled");
}));
}
cl.cache_execute(*op);
/* We now have the following graph of objects:
* cache -- op -- lambda -- d
* dummy --/
*/
ASSERT_EQ(1, dummies);
/* Executing the on_change will "cancel" this cache op. This will cause it
* to release the lambda, reducing us down to dummy -- d
*/
cl.cache.on_change();
ASSERT_EQ(1, dummies);
/* This emulates the rmw pipeline clearing outstanding IO. We now have no
* references to d, so we should have destructed the object.
* */
dummy.reset();
ASSERT_EQ(0, dummies);
/* Keeping the op alive here is emulating the dummy keeping a record of the
* cache op. It will also be destroyed at this point by rmw pipeline.
*/
ASSERT_FALSE(cl.cache.idle());
op.reset();
ASSERT_TRUE(cl.cache.idle());
// The cache has its own asserts, which we should honour.
cl.cache.on_change2();
}
TEST(ECExtentCache, multiple_misaligned_writes)
{
Client cl(256*1024, 2, 1, 1024*1024);
// IO 1 is really a 6k write. The write is inflated to 8k, but the second 4k is
// partial, so we read the second 4k to RMW
auto to_read1 = iset_from_vector( {{{4*1024, 4*1024}}}, cl.get_stripe_info());
auto to_write1 = iset_from_vector({{{0, 8*1024}}}, cl.get_stripe_info());
// IO 2 is the next 8k write, starting at 6k. So we have a 12k write, reading the
// first and last pages. The first part of this read should be in the cache.
auto to_read2 = iset_from_vector( {{{4*1024, 4*1024}, {12*4096, 4*4096}}}, cl.get_stripe_info());
auto to_read2_exec = iset_from_vector( {{{12*4096, 4*4096}}}, cl.get_stripe_info());
auto to_write2 = iset_from_vector({{{4*1024, 12*1024}}}, cl.get_stripe_info());
// IO 3 is the next misaligned 4k, very similar to IO 3.
auto to_read3 = iset_from_vector( {{{12*1024, 4*1024}, {20*4096, 4*4096}}}, cl.get_stripe_info());
auto to_read3_exec = iset_from_vector( {{{20*4096, 4*4096}}}, cl.get_stripe_info());
auto to_write3 = iset_from_vector({{{12*1024, 12*1024}}}, cl.get_stripe_info());
//Perform the first write, which should result in a read.
optional op1 = cl.cache.prepare(cl.oid, to_read1, to_write1, 22*1024, 22*1024, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Submit the second IO.
optional op2 = cl.cache.prepare(cl.oid, to_read2, to_write2, 22*1024, 22*1024, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op2);
// We should still be executing read 1.
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Allow the read to complete. We should now have op1 done...
cl.complete_read();
ASSERT_EQ(to_read2_exec, cl.active_reads);
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op1);
// And move on to op3
optional op3 = cl.cache.prepare(cl.oid, to_read3, to_write3, 22*1024, 22*1024, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op3);
// We should still be executing read 1.
ASSERT_EQ(to_read2_exec, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Allow the read to complete. We should now have op2 done...
cl.complete_read();
ASSERT_EQ(to_read3_exec, cl.active_reads);
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op2);
ASSERT_EQ(to_read3_exec, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
cl.complete_read();
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op3);
}
TEST(ECExtentCache, multiple_misaligned_writes2)
{
Client cl(256*1024, 2, 1, 1024*1024);
// IO 1 is really a 6k write. The write is inflated to 8k, but the second 4k is
// partial, so we read the second 4k to RMW
auto to_read1 = iset_from_vector( {{{4*1024, 4*1024}}}, cl.get_stripe_info());
auto to_write1 = iset_from_vector({{{0, 8*1024}}}, cl.get_stripe_info());
// IO 2 is the next 8k write, starting at 6k. So we have a 12k write, reading the
// first and last pages. The first part of this read should be in the cache.
auto to_read2 = iset_from_vector( {{{4*1024, 4*1024}, {12*1024, 4*1024}}}, cl.get_stripe_info());
auto to_read2_exec = iset_from_vector( {{{12*1024, 4*1024}}}, cl.get_stripe_info());
auto to_write2 = iset_from_vector({{{4*1024, 12*1024}}}, cl.get_stripe_info());
// IO 3 is the next misaligned 4k, very similar to IO 3.
auto to_read3 = iset_from_vector( {{{12*1024, 4*1024}, {20*1024, 4*1024}}}, cl.get_stripe_info());
auto to_read3_exec = iset_from_vector( {{{20*1024, 4*1024}}}, cl.get_stripe_info());
auto to_write3 = iset_from_vector({{{12*1024, 12*1024}}}, cl.get_stripe_info());
//Perform the first write, which should result in a read.
optional op1 = cl.cache.prepare(cl.oid, to_read1, to_write1, 22*1024, 22*1024, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Submit the second IO.
optional op2 = cl.cache.prepare(cl.oid, to_read2, to_write2, 22*1024, 22*1024, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op2);
// We should still be executing read 1.
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Allow the read to complete. We should now have op1 done...
cl.complete_read();
ASSERT_EQ(to_read2_exec, cl.active_reads);
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op1);
// And move on to op3
optional op3 = cl.cache.prepare(cl.oid, to_read3, to_write3, 22*1024, 22*1024, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op3);
// We should still be executing read 1.
ASSERT_EQ(to_read2_exec, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
// Allow the read to complete. We should now have op2 done...
cl.complete_read();
ASSERT_EQ(to_read3_exec, cl.active_reads);
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op2);
ASSERT_EQ(to_read3_exec, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
cl.complete_read();
ASSERT_FALSE(cl.results.empty());
cl.complete_write(*op3);
}
TEST(ECExtentCache, test_invalidate)
{
Client cl(256*1024, 2, 1, 1024*1024);
/* First attempt a write which does not do any reads */
{
auto to_read1 = iset_from_vector( {{{0, 4096}}}, cl.get_stripe_info());
auto to_write1 = iset_from_vector({{{0, 4096}}}, cl.get_stripe_info());
optional op1 = cl.cache.prepare(cl.oid, to_read1, to_write1, 4096, 4096, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
ASSERT_EQ(to_read1, cl.active_reads);
ASSERT_TRUE(cl.results.empty());
/* Now perform an invalidating cache write */
optional op2 = cl.cache.prepare(cl.oid, nullopt, shard_extent_set_t(cl.sinfo.get_k_plus_m()), 4*1024, 0, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op2);
cl.complete_read();
ASSERT_EQ(2, cl.results.size());
auto result = cl.results.begin();
ASSERT_FALSE(result++->empty());
ASSERT_TRUE(result++->empty());
cl.complete_write(*op1);
ASSERT_FALSE(cl.active_reads);
cl.complete_write(*op2);
cl.cache.on_change();
op1.reset();
op2.reset();
cl.cache.on_change2();
ASSERT_TRUE(cl.cache.idle());
}
/* Second test, modifies, deletes, creates, then modifies. */
{
auto to_read1 = iset_from_vector( {{{0, 8192}}}, cl.get_stripe_info());
auto to_write1 = iset_from_vector({{{0, 8192}}}, cl.get_stripe_info());
auto to_write2 = iset_from_vector({{{4096, 4096}}}, cl.get_stripe_info());
auto to_read3 = iset_from_vector( {{{0, 4096}}}, cl.get_stripe_info());
auto to_write3 = iset_from_vector({{{0, 4096}}}, cl.get_stripe_info());
optional op1 = cl.cache.prepare(cl.oid, to_read1, to_write1, 8192, 8192, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
optional op2 = cl.cache.prepare(cl.oid, nullopt, shard_extent_set_t(cl.sinfo.get_k_plus_m()), 4*1024, 0, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
optional op3 = cl.cache.prepare(cl.oid, nullopt, to_write2, 0, 8192, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
optional op4 = cl.cache.prepare(cl.oid, to_read3, to_write3, 8192, 8192, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
cl.cache_execute(*op2);
cl.cache_execute(*op3);
cl.cache_execute(*op4);
/* The first result must actually read. */
cl.complete_read();
ASSERT_EQ(4, cl.results.size());
auto result = cl.results.begin();
ASSERT_FALSE(result++->empty());
ASSERT_TRUE(result++->empty());
ASSERT_TRUE(result++->empty());
ASSERT_TRUE(result++->empty());
cl.complete_write(*op1);
cl.complete_write(*op2);
cl.complete_write(*op3);
cl.complete_write(*op4);
cl.cache.on_change();
op1.reset();
op2.reset();
op3.reset();
op4.reset();
cl.cache.on_change2();
ASSERT_TRUE(cl.cache.idle());
}
}
TEST(ECExtentCache, test_invalidate_lru)
{
uint64_t c = 4096;
int k = 4;
int m = 2;
Client cl(c, k, m, 1024*c);
/* Populate the cache LRU and then invalidate the cache. */
{
uint64_t bs = 3767;
auto io1 = iset_from_vector({{{align_page_prev(35*bs), align_page_next(36*bs) - align_page_prev(35*bs)}}}, cl.get_stripe_info());
io1[shard_id_t(k)].insert(io1.get_extent_superset());
io1[shard_id_t(k+1)].insert(io1.get_extent_superset());
auto io2 = iset_from_vector({{{align_page_prev(18*bs), align_page_next(19*bs) - align_page_prev(18*bs)}}}, cl.get_stripe_info());
io2[shard_id_t(k)].insert(io1.get_extent_superset());
io2[shard_id_t(k+1)].insert(io1.get_extent_superset());
// io 3 is the truncate
auto io3 = shard_extent_set_t(cl.sinfo.get_k_plus_m());
auto io4 = iset_from_vector({{{align_page_prev(30*bs), align_page_next(31*bs) - align_page_prev(30*bs)}}}, cl.get_stripe_info());
io3[shard_id_t(k)].insert(io1.get_extent_superset());
io3[shard_id_t(k+1)].insert(io1.get_extent_superset());
auto io5 = iset_from_vector({{{align_page_prev(18*bs), align_page_next(19*bs) - align_page_prev(18*bs)}}}, cl.get_stripe_info());
io4[shard_id_t(k)].insert(io1.get_extent_superset());
io4[shard_id_t(k+1)].insert(io1.get_extent_superset());
optional op1 = cl.cache.prepare(cl.oid, nullopt, io1, 0, align_page_next(36*bs), false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op1);
ASSERT_FALSE(cl.active_reads);
cl.complete_write(*op1);
op1.reset();
optional op2 = cl.cache.prepare(cl.oid, io2, io2, align_page_next(36*bs), align_page_next(36*bs), false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op2);
// We have active reads because the object was discarded fro the cache
// and has forgotten about all the zero reads.
ASSERT_TRUE(cl.active_reads);
cl.complete_read();
cl.complete_write(*op2);
op2.reset();
optional op3 = cl.cache.prepare(cl.oid, nullopt, io3, align_page_next(36*bs), 0, false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op3);
ASSERT_FALSE(cl.active_reads);
cl.complete_write(*op3);
op3.reset();
optional op4 = cl.cache.prepare(cl.oid, nullopt, io4, 0, align_page_next(30*bs), false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op4);
ASSERT_FALSE(cl.active_reads);
cl.complete_write(*op4);
op4.reset();
optional op5 = cl.cache.prepare(cl.oid, io5, io5, align_page_next(30*bs), align_page_next(30*bs), false,
[&cl](ECExtentCache::OpRef &op)
{
cl.cache_ready(op->get_hoid(), op->get_result());
});
cl.cache_execute(*op5);
ASSERT_TRUE(cl.active_reads);
cl.complete_write(*op5);
op5.reset();
}
}

View File

@ -6,7 +6,6 @@
#include "common/ceph_argparse.h"
#include "common/config_proxy.h"
#include "common/errno.h"
#include "erasure-code/ErasureCode.h"
#include "erasure-code/ErasureCodePlugin.h"
#include "global/global_context.h"
#include "global/global_init.h"
@ -96,7 +95,7 @@ int ec_init(const std::string &profile_str,
uint64_t stripe_size = atoi(profile["k"].c_str());
ceph_assert(stripe_size > 0);
uint64_t stripe_width = stripe_size * stripe_unit;
sinfo->reset(new ECUtil::stripe_info_t(*ec_impl, stripe_width));
sinfo->reset(new ECUtil::stripe_info_t(*ec_impl, nullptr, stripe_width));
return 0;
}
@ -196,37 +195,36 @@ int do_encode(const std::vector<const char*> &args) {
return r;
}
std::set<int> want;
ECUtil::shard_extent_map_t encoded_data(sinfo.get());
std::vector<std::string> shards;
boost::split(shards, args[2], boost::is_any_of(","));
for (auto &shard : shards) {
want.insert(atoi(shard.c_str()));
}
ceph::bufferlist decoded_data;
ceph::bufferlist input_data;
std::string fname = args[3];
std::string error;
r = decoded_data.read_file(fname.c_str(), &error);
r = input_data.read_file(fname.c_str(), &error);
if (r < 0) {
std::cerr << "failed to read " << fname << ": " << error << std::endl;
return 1;
}
uint64_t stripe_width = sinfo->get_stripe_width();
if (decoded_data.length() % stripe_width != 0) {
uint64_t pad = stripe_width - decoded_data.length() % stripe_width;
decoded_data.append_zero(pad);
if (input_data.length() % stripe_width != 0) {
uint64_t pad = stripe_width - input_data.length() % stripe_width;
input_data.append_zero(pad);
}
std::map<int, ceph::bufferlist> encoded_data;
r = ECUtil::encode(*sinfo, ec_impl, decoded_data, want, &encoded_data);
sinfo->ro_range_to_shard_extent_map(0, input_data.length(), input_data, encoded_data);
r = encoded_data.encode(ec_impl, nullptr, encoded_data.get_ro_end());
if (r < 0) {
std::cerr << "failed to encode: " << cpp_strerror(r) << std::endl;
return 1;
}
for (auto &[shard, bl] : encoded_data) {
for (auto &[shard, _] : encoded_data.get_extent_maps()) {
std::string name = fname + "." + stringify(shard);
bufferlist bl;
encoded_data.get_shard_first_buffer(shard, bl);
r = bl.write_file(name.c_str());
if (r < 0) {
std::cerr << "failed to write " << name << ": " << cpp_strerror(r)
@ -247,40 +245,41 @@ int do_decode(const std::vector<const char*> &args) {
ceph::ErasureCodeInterfaceRef ec_impl;
std::unique_ptr<ECUtil::stripe_info_t> sinfo;
int r = ec_init(args[0], args[1], &ec_impl, &sinfo);
if (r < 0) {
if (r) {
return r;
}
std::map<int, ceph::bufferlist> encoded_data;
ECUtil::shard_extent_map_t encoded_data(sinfo.get());
std::vector<std::string> shards;
boost::split(shards, args[2], boost::is_any_of(","));
for (auto &shard : shards) {
encoded_data[atoi(shard.c_str())] = {};
}
ceph::bufferlist decoded_data;
std::string fname = args[3];
std::set<int> want_to_read;
const auto chunk_mapping = ec_impl->get_chunk_mapping();
for (auto &[shard, bl] : encoded_data) {
std::string name = fname + "." + stringify(shard);
for (auto &shard_str : shards) {
std::string name = fname + "." + shard_str;
std::string error;
bufferlist bl;
r = bl.read_file(name.c_str(), &error);
if (r < 0) {
std::cerr << "failed to read " << name << ": " << error << std::endl;
return 1;
}
auto chunk = static_cast<ssize_t>(chunk_mapping.size()) > shard ?
chunk_mapping[shard] : shard_id_t(shard);
want_to_read.insert(static_cast<int>(chunk));
shard_id_t shard = sinfo->get_shard(raw_shard_id_t(atoi(shard_str.c_str())));
encoded_data.insert_in_shard(shard, 0, bl);
}
r = ECUtil::decode(*sinfo, ec_impl, want_to_read, encoded_data, &decoded_data);
ECUtil::shard_extent_set_t wanted(sinfo->get_k_plus_m());
sinfo->ro_range_to_shard_extent_set(encoded_data.get_ro_start(),
encoded_data.get_ro_end() - encoded_data.get_ro_start(), wanted);
r = encoded_data.decode(ec_impl, wanted, encoded_data.get_ro_end());
if (r < 0) {
std::cerr << "failed to decode: " << cpp_strerror(r) << std::endl;
return 1;
}
bufferlist decoded_data = encoded_data.get_ro_buffer();
r = decoded_data.write_file(fname.c_str());
if (r < 0) {
std::cerr << "failed to write " << fname << ": " << cpp_strerror(r)