Use std::to_underlying() when comparing unsigned types with enumeration values
to fix type mismatch warnings in GCC-14. This specifically addresses an issue in
utils/advanced_rpc_compressor.hh where comparing a uint8_t with 0 triggered a
'-Werror=type-limits' warning:
```
error: comparison is always false due to limited range of data type [-Werror=type-limits]
if (x < 0 || x >= static_cast<underlying>(type::COUNT))
~~^~~
```
Using std::to_underlying() provides clearer type semantics and avoids these kind
of comparison warnings. This change improves code readability while maintaining
the same behavior.
Signed-off-by: Kefu Chai <kefu.chai@scylladb.com>
Closes scylladb/scylladb#22898
367 lines
15 KiB
C++
367 lines
15 KiB
C++
/*
|
|
* Copyright (C) 2023-present ScyllaDB
|
|
*/
|
|
|
|
/*
|
|
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <seastar/core/condition-variable.hh>
|
|
#include <seastar/rpc/rpc_types.hh>
|
|
#include <utility>
|
|
#include "utils/refcounted.hh"
|
|
#include "utils/updateable_value.hh"
|
|
#include "utils/enum_option.hh"
|
|
|
|
namespace utils {
|
|
|
|
class dict_sampler;
|
|
class lz4_cstream;
|
|
class lz4_dstream;
|
|
class zstd_cstream;
|
|
class zstd_dstream;
|
|
class stream_compressor;
|
|
class stream_decompressor;
|
|
class shared_dict;
|
|
using dict_ptr = lw_shared_ptr<foreign_ptr<lw_shared_ptr<shared_dict>>>;
|
|
class control_protocol_frame;
|
|
|
|
// An enum wrapper, describing supported RPC compression algorithms.
|
|
// Always contains a valid value —- the constructors won't allow
|
|
// an invalid/unknown enum variant to be constructed.
|
|
struct compression_algorithm {
|
|
using underlying = uint8_t;
|
|
enum class type : underlying {
|
|
RAW,
|
|
LZ4,
|
|
ZSTD,
|
|
COUNT,
|
|
} _value;
|
|
// Construct from an integer.
|
|
// Used to deserialize the algorithm from the first byte of the frame.
|
|
constexpr compression_algorithm(underlying x) {
|
|
if (x < std::to_underlying(type::RAW) || x >= std::to_underlying(type::COUNT)) {
|
|
throw std::runtime_error(fmt::format("Invalid value {} for enum compression_algorithm", static_cast<int>(x)));
|
|
}
|
|
_value = static_cast<type>(x);
|
|
}
|
|
// Construct from `type`. Makes sure that `type` has a valid value.
|
|
constexpr compression_algorithm(type x) : compression_algorithm(std::to_underlying(x)) {}
|
|
|
|
// These names are used in multiple places:
|
|
// RPC negotiation, in metric labels, and config.
|
|
static constexpr std::string_view names[] = {
|
|
"raw",
|
|
"lz4",
|
|
"zstd",
|
|
};
|
|
static_assert(std::size(names) == static_cast<int>(compression_algorithm::type::COUNT));
|
|
|
|
// Implements enum_option.
|
|
static auto map() {
|
|
std::unordered_map<std::string, type> ret;
|
|
for (size_t i = 0; i < std::size(names); ++i) {
|
|
ret.insert(std::make_pair<std::string, type>(std::string(names[i]), compression_algorithm(i).get()));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
constexpr std::string_view name() const noexcept { return names[idx()]; }
|
|
constexpr underlying idx() const noexcept { return std::to_underlying(_value); }
|
|
constexpr type get() const noexcept { return _value; }
|
|
constexpr static size_t count() { return static_cast<size_t>(type::COUNT); };
|
|
bool operator<=>(const compression_algorithm &) const = default;
|
|
};
|
|
|
|
|
|
// Represents a set of compression algorithms.
|
|
// Backed by a bitset.
|
|
// Used for convenience during algorithm negotiations.
|
|
class compression_algorithm_set {
|
|
uint8_t _bitset;
|
|
static_assert(std::numeric_limits<decltype(_bitset)>::digits > compression_algorithm::count());
|
|
constexpr compression_algorithm_set(uint8_t v) noexcept : _bitset(v) {}
|
|
public:
|
|
// Returns a set containing the given algorithm and all algorithms weaker (smaller in the enum order)
|
|
// than it.
|
|
constexpr static compression_algorithm_set this_or_lighter(compression_algorithm algo) noexcept {
|
|
auto x = 1 << (algo.idx());
|
|
return {x + (x - 1)};
|
|
}
|
|
// Returns the strongest (greatest in the enum order) algorithm in the set.
|
|
constexpr compression_algorithm heaviest() const {
|
|
return {std::bit_width(_bitset) - 1};
|
|
}
|
|
// The usual set operations.
|
|
constexpr static compression_algorithm_set singleton(compression_algorithm algo) noexcept {
|
|
return {1 << algo.idx()};
|
|
}
|
|
constexpr compression_algorithm_set intersection(compression_algorithm_set o) const noexcept {
|
|
return {_bitset & o._bitset};
|
|
}
|
|
constexpr compression_algorithm_set difference(compression_algorithm_set o) const noexcept {
|
|
return {_bitset &~ o._bitset};
|
|
}
|
|
constexpr compression_algorithm_set sum(compression_algorithm_set o) const noexcept {
|
|
return {_bitset | o._bitset};
|
|
}
|
|
constexpr bool contains(compression_algorithm algo) const noexcept {
|
|
return _bitset & (1 << algo.idx());
|
|
}
|
|
constexpr bool operator==(const compression_algorithm_set&) const = default;
|
|
// Returns the contained bitset. Used for serialization.
|
|
constexpr uint8_t value() const noexcept {
|
|
return _bitset;
|
|
}
|
|
// Reconstructs the set from the output of `value()`. Used for deserialization.
|
|
constexpr static compression_algorithm_set from_value(uint8_t bitset) {
|
|
compression_algorithm_set x = bitset;
|
|
x.heaviest(); // This is a validation check. It will throw if the bitset contains some illegal/unknown bits.
|
|
return x;
|
|
}
|
|
};
|
|
|
|
using algo_config = std::vector<enum_option<compression_algorithm>>;
|
|
|
|
// See docs/dev/advanced_rpc_compression.md,
|
|
// section `Negotiation` for more information about the protocol.
|
|
struct control_protocol {
|
|
// The sender increments its protocol epoch every time it proposes to commit to a different
|
|
// algorithm.
|
|
// The epoch is echoed back by the receiver to match proposals with accepts.
|
|
uint64_t _sender_protocol_epoch = 0;
|
|
uint64_t _receiver_protocol_epoch = 0;
|
|
|
|
// To send a control frame to the peer, we set one of these flags and signal _needs_progress.
|
|
// This will cause at least one RPC message to be sent promptly. We prepend our frame to
|
|
// the next RPC message.
|
|
|
|
// These two flags are mutually exclusive.
|
|
bool _sender_has_update = false;
|
|
bool _sender_has_commit = false;
|
|
// These two flags are mutually exclusive.
|
|
bool _receiver_has_update = false;
|
|
bool _receiver_has_commit = false;
|
|
|
|
dict_ptr _sender_recent_dict = nullptr;
|
|
dict_ptr _sender_committed_dict = nullptr;
|
|
dict_ptr _sender_current_dict = nullptr;
|
|
dict_ptr _receiver_recent_dict = nullptr;
|
|
dict_ptr _receiver_committed_dict = nullptr;
|
|
dict_ptr _receiver_current_dict = nullptr;
|
|
compression_algorithm _sender_current_algo = compression_algorithm::type::RAW;
|
|
compression_algorithm _sender_committed_algo = compression_algorithm::type::RAW;
|
|
compression_algorithm_set _algos = compression_algorithm_set::singleton(compression_algorithm::type::RAW);
|
|
|
|
// When signalled, an empty message will be sent over this connection soon.
|
|
// Used to guarantee progress of algorithm negotiations.
|
|
condition_variable& _needs_progress;
|
|
public:
|
|
control_protocol(condition_variable&);
|
|
// These functions handle the control (negotiation) protocol.
|
|
std::optional<control_protocol_frame> produce_control_header();
|
|
void consume_control_header(control_protocol_frame);
|
|
void announce_dict(dict_ptr) noexcept;
|
|
void set_supported_algos(compression_algorithm_set algos) noexcept;
|
|
compression_algorithm sender_current_algorithm() const noexcept;
|
|
const shared_dict& sender_current_dict() const noexcept;
|
|
const shared_dict& receiver_current_dict() const noexcept;
|
|
};
|
|
|
|
class advanced_rpc_compressor final : public rpc::compressor {
|
|
public:
|
|
class tracker;
|
|
template <typename HighResClock, typename LowResClock> class tracker_with_clock;
|
|
private:
|
|
// Pointer/reference to the tracker, which contains stats that we need to update,
|
|
// and limits that we need to respect.
|
|
//
|
|
// The `refcounted` is just a precaution against a misuse of the APIs.
|
|
refcounted::ref<tracker> _tracker;
|
|
|
|
// Index of the compressor inside the tracker.
|
|
// Used to unregister the compressor on destruction.
|
|
size_t _idx = -1;
|
|
|
|
// State of the negotiation protocol.
|
|
control_protocol _control;
|
|
|
|
// Used by _control to send its messages to other side of the connection.
|
|
condition_variable _needs_progress;
|
|
std::function<future<>()> _send_empty_frame;
|
|
future<> _progress_fiber;
|
|
|
|
// These return global compression contexts (for non-streaming compression modes), lazily initializing them.
|
|
zstd_dstream& get_global_zstd_dstream();
|
|
zstd_cstream& get_global_zstd_cstream();
|
|
lz4_dstream& get_global_lz4_dstream();
|
|
lz4_cstream& get_global_lz4_cstream();
|
|
|
|
// Calls the appropriate get_*_cstream() function.
|
|
stream_compressor& get_compressor(compression_algorithm);
|
|
// Calls the appropriate get_*_dstream() function.
|
|
stream_decompressor& get_decompressor(compression_algorithm);
|
|
|
|
// Decides the algorithm used for the next message, based
|
|
// on the state of the negotiation and the size of the message.
|
|
compression_algorithm get_algo_for_next_msg(size_t msgsize);
|
|
|
|
// Starts a worker fiber responsible for sending _control's messages.
|
|
future<> start_progress_fiber();
|
|
public:
|
|
advanced_rpc_compressor(
|
|
tracker& fac,
|
|
std::function<future<>()> send_empty_frame
|
|
);
|
|
~advanced_rpc_compressor();
|
|
|
|
// The public interface of rpc::compressor.
|
|
rpc::snd_buf compress(size_t head_space, rpc::snd_buf data) override;
|
|
rpc::rcv_buf decompress(rpc::rcv_buf data) override;
|
|
sstring name() const override;
|
|
future<> close() noexcept override;
|
|
};
|
|
|
|
// Tracker holds one of these for every compression mode/algorithm.
|
|
// They are used for displaying metrics, and for implementing CPU/memory usage limits.
|
|
struct per_algorithm_stats {
|
|
uint64_t bytes_sent = 0;
|
|
uint64_t compressed_bytes_sent = 0;
|
|
uint64_t messages_sent = 0;
|
|
uint64_t compression_cpu_nanos = 0;
|
|
uint64_t bytes_received = 0;
|
|
uint64_t compressed_bytes_received = 0;
|
|
uint64_t messages_received = 0;
|
|
uint64_t decompression_cpu_nanos = 0;
|
|
};
|
|
|
|
// The tracker contains everything which is shared between compressor instances:
|
|
// stats, metrics, limits, reusable non-streaming compressors.
|
|
//
|
|
// Class `tracker` itself contains clock-independent functionality.
|
|
// Clock-dependent functionality is split into `tracker_with_clock`, to minimize template pollution.
|
|
// Alternatively, we could wrap clocks into some virtual interface.
|
|
//
|
|
// Tracker is referenced by all compressors, so we inherit from `refcounted` to
|
|
// prevent a misuse of the API (dangling references).
|
|
class advanced_rpc_compressor::tracker : public refcounted {
|
|
public:
|
|
using algo_config = algo_config;
|
|
struct config {
|
|
updateable_value<uint32_t> zstd_min_msg_size{0};
|
|
updateable_value<uint32_t> zstd_max_msg_size{std::numeric_limits<uint32_t>::max()};
|
|
updateable_value<float> zstd_quota_fraction{0};
|
|
updateable_value<uint32_t> zstd_quota_refresh_ms{20};
|
|
updateable_value<float> zstd_longterm_quota_fraction{1000};
|
|
updateable_value<uint32_t> zstd_longterm_quota_refresh_ms{1000};
|
|
updateable_value<algo_config> algo_config{{compression_algorithm::type::ZSTD, compression_algorithm::type::LZ4}};
|
|
bool register_metrics = false;
|
|
updateable_value<bool> checksumming{true};
|
|
};
|
|
private:
|
|
friend advanced_rpc_compressor;
|
|
|
|
config _cfg;
|
|
observer<algo_config> _algo_config_observer;
|
|
|
|
std::array<per_algorithm_stats, compression_algorithm::count()> _stats;
|
|
metrics::metric_groups _metrics;
|
|
|
|
// Compression contexts for non-streaming compression modes.
|
|
// They are shared by all compressors owned this tracker.
|
|
std::unique_ptr<zstd_cstream> _global_zstd_cstream;
|
|
std::unique_ptr<zstd_dstream> _global_zstd_dstream;
|
|
std::unique_ptr<lz4_cstream> _global_lz4_cstream;
|
|
std::unique_ptr<lz4_dstream> _global_lz4_dstream;
|
|
std::vector<advanced_rpc_compressor*> _compressors;
|
|
dict_ptr _most_recent_dict = nullptr;
|
|
|
|
dict_sampler* _dict_sampler = nullptr;
|
|
|
|
void register_metrics();
|
|
void maybe_refresh_zstd_quota(uint64_t now) noexcept;
|
|
bool cpu_limit_exceeded() const noexcept;
|
|
uint64_t get_total_nanos_spent() const noexcept;
|
|
|
|
zstd_dstream& get_global_zstd_dstream();
|
|
zstd_cstream& get_global_zstd_cstream();
|
|
lz4_dstream& get_global_lz4_dstream();
|
|
lz4_cstream& get_global_lz4_cstream();
|
|
|
|
void ingest(const rpc::snd_buf& data);
|
|
void ingest(const rpc::rcv_buf& data);
|
|
|
|
template <typename T>
|
|
requires std::same_as<T, rpc::rcv_buf> || std::same_as<T, rpc::snd_buf>
|
|
void ingest_generic(const T& data);
|
|
|
|
size_t register_compressor(advanced_rpc_compressor*);
|
|
void unregister_compressor(size_t);
|
|
public:
|
|
tracker(config);
|
|
virtual ~tracker();
|
|
|
|
// Interface of rpc::compressor::factory.
|
|
// `tracker` itself doesn't inherit from `factory` (just because this inheritance would have no users),
|
|
// but a wrapper over `tracker` can use these to implement the interface.
|
|
const sstring& supported() const;
|
|
std::unique_ptr<advanced_rpc_compressor> negotiate(sstring feature, bool is_server, std::function<future<>()> send_empty_frame);
|
|
std::span<const per_algorithm_stats, compression_algorithm::count()> get_stats() const noexcept;
|
|
|
|
void announce_dict(dict_ptr);
|
|
void attach_to_dict_sampler(dict_sampler*) noexcept;
|
|
void set_supported_algos(compression_algorithm_set algos) noexcept;
|
|
protected:
|
|
// These members are governed by `tracker_with_clock`.
|
|
//
|
|
// Why use nanos instead of Clock::duration?
|
|
// Because that would require templating `factory_base` and `advanced_rpc_compressor` on `Clock`.
|
|
// Forcing a common duration unit allows for encapsulation of clock-related details inside `tracker_with_clock`.
|
|
virtual uint64_t get_steady_nanos() const = 0;
|
|
|
|
// There are two CPU limit accounting periods: short period and long period.
|
|
// Long period is multiple seconds and is meant to limit the throughput overhead.
|
|
// Short period is a few several milliseconds and is meant to limit the latency ovehead.
|
|
// Each period has a separate quota and we fall back to cheaper compression if any of
|
|
// them is exceeded.
|
|
//
|
|
// The long quota is periodically reset by a timer.
|
|
// The short quota is periodically reset manually by the tracker, because the period is very short.
|
|
// A timer with this period could generate unnecessary noise (e.g. keep waking up an otherwise-idle reactor).
|
|
constexpr static std::chrono::nanoseconds long_period = std::chrono::seconds(10);
|
|
uint64_t _short_period_start = 0;
|
|
uint64_t _long_period_start = 0;
|
|
uint64_t _nanos_used_before_this_short_period = 0;
|
|
uint64_t _nanos_used_before_this_long_period = 0;
|
|
};
|
|
|
|
// Implements clock-dependent functionality for `tracker`.
|
|
template <typename HighResClock, typename LowResClock>
|
|
class advanced_rpc_compressor::tracker_with_clock : public advanced_rpc_compressor::tracker {
|
|
virtual uint64_t get_steady_nanos() const override {
|
|
return std::chrono::duration_cast<std::chrono::nanoseconds>(HighResClock::now().time_since_epoch()).count();
|
|
}
|
|
public:
|
|
tracker_with_clock(config c)
|
|
: advanced_rpc_compressor::tracker(std::move(c))
|
|
{}
|
|
// updateable_value must be created on the destination shard.
|
|
// Since tracker is sharded, we can't copy the tracker::config (which contains updateable_value)
|
|
// to all shards. But we can pass to all shards a function which will create the tracker::config.
|
|
tracker_with_clock(std::function<config()> f)
|
|
: tracker_with_clock(f())
|
|
{}
|
|
};
|
|
|
|
class walltime_compressor_tracker final : public utils::advanced_rpc_compressor::tracker_with_clock<std::chrono::steady_clock, lowres_clock> {
|
|
using tracker_with_clock::tracker_with_clock;
|
|
};
|
|
|
|
// Helper for setting up the lw_shared_ptr<foreign_ptr<lw_shared_ptr<utils::shared_dict>>> tree
|
|
// used by the tracker to manage the lifetime of dicts.
|
|
future<> announce_dict_to_shards(seastar::sharded<walltime_compressor_tracker>&, utils::shared_dict);
|
|
|
|
} // namespace utils
|