blob: 9f15ed1e495d66e7a354281879f1129ac3afd146 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
#include "commander.h"
#include "error_constants.h"
#include "event_util.h"
#include "io_util.h"
#include "scope_exit.h"
#include "server/redis_reply.h"
#include "server/server.h"
#include "thread_util.h"
#include "time_util.h"
#include "unique_fd.h"
namespace redis {
class CommandPSync : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
size_t seq_arg = 1;
if (args.size() == 3) {
seq_arg = 2;
new_psync_ = true;
}
auto parse_result = ParseInt<uint64_t>(args[seq_arg], 10);
if (!parse_result) {
return {Status::RedisParseErr, "value is not an unsigned long long or out of range"};
}
next_repl_seq_ = static_cast<rocksdb::SequenceNumber>(*parse_result);
if (new_psync_) {
assert(args.size() == 3);
replica_replid_ = args[1];
if (replica_replid_.size() != kReplIdLength) {
return {Status::RedisParseErr, "Wrong replication id length"};
}
}
return Commander::Parse(args);
}
Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
INFO(
"Slave {}, listening port: {}, announce ip: {} asks for synchronization "
"with next sequence: {}, replication id: {}, and local sequence: {}",
conn->GetAddr(), conn->GetListeningPort(), conn->GetAnnounceIP(), next_repl_seq_,
(replica_replid_.length() ? replica_replid_ : "not supported"), srv->storage->LatestSeqNumber());
bool need_full_sync = false;
// Check replication id of the last sequence log
if (new_psync_ && srv->GetConfig()->use_rsid_psync) {
std::string replid_in_wal = srv->storage->GetReplIdFromWalBySeq(next_repl_seq_ - 1);
INFO("Replication id in WAL: {}", replid_in_wal);
// We check replication id only when WAL has this sequence, since there may be no WAL,
// Or WAL may have nothing when starting from db of old version kvrocks.
if (replid_in_wal.length() == kReplIdLength && replid_in_wal != replica_replid_) {
*output = "wrong replication id of the last log";
need_full_sync = true;
}
}
// Check Log sequence
if (!need_full_sync && !checkWALBoundary(srv->storage, next_repl_seq_).IsOK()) {
*output = "sequence out of range, please use fullsync";
need_full_sync = true;
}
if (need_full_sync) {
srv->stats.IncrPSyncErrCount();
return {Status::RedisExecErr, *output};
}
// Server would spawn a new thread to sync the batch, and connection would
// be taken over, so should never trigger any event in worker thread.
conn->Detach();
conn->EnableFlag(redis::Connection::kSlave);
auto s = util::SockSetBlocking(conn->GetFD(), 1);
if (!s.IsOK()) {
conn->EnableFlag(redis::Connection::kCloseAsync);
return s.Prefixed("failed to set blocking mode on socket");
}
srv->stats.IncrPSyncOKCount();
s = srv->AddSlave(conn, next_repl_seq_);
if (!s.IsOK()) {
std::string err = redis::Error(s);
s = util::SockSend(conn->GetFD(), err, conn->GetBufferEvent());
if (!s.IsOK()) {
WARN("failed to send error message to the replica: {}", s.Msg());
}
conn->EnableFlag(redis::Connection::kCloseAsync);
WARN("Failed to add replica: {} to start incremental syncing", conn->GetAddr());
} else {
INFO("New replica: {} was added, start incremental syncing", conn->GetAddr());
}
return s;
}
private:
rocksdb::SequenceNumber next_repl_seq_ = 0;
bool new_psync_ = false;
std::string replica_replid_;
// Return OK if the seq is in the range of the current WAL
static Status checkWALBoundary(engine::Storage *storage, rocksdb::SequenceNumber seq) {
if (seq == storage->LatestSeqNumber() + 1) {
return Status::OK();
}
// Upper bound
if (seq > storage->LatestSeqNumber() + 1) {
return {Status::NotOK};
}
// Lower bound
std::unique_ptr<rocksdb::TransactionLogIterator> iter;
auto s = storage->GetWALIter(seq, &iter);
if (s.IsOK() && iter->Valid()) {
auto batch = iter->GetBatch();
if (seq != batch.sequence) {
if (seq > batch.sequence) {
ERROR("checkWALBoundary with sequence: {}, but GetWALIter return older sequence: {}", seq, batch.sequence);
}
return {Status::NotOK};
}
return Status::OK();
}
return {Status::NotOK};
}
};
class CommandReplConf : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
if (args.size() % 2 == 0) {
return {Status::RedisParseErr, errWrongNumOfArguments};
}
for (size_t i = 1; i < args.size(); i += 2) {
Status s = ParseParam(util::ToLower(args[i]), args[i + 1]);
if (!s.IsOK()) {
return s;
}
}
return Commander::Parse(args);
}
Status ParseParam(const std::string &option, const std::string &value) {
if (option == "listening-port") {
auto parse_result = ParseInt<int>(value, NumericRange<int>{1, PORT_LIMIT - 1}, 10);
if (!parse_result) {
return {Status::RedisParseErr, "listening-port should be number or out of range"};
}
port_ = *parse_result;
} else if (option == "ip-address") {
if (value == "") {
return {Status::RedisParseErr, "ip-address should not be empty"};
}
ip_address_ = value;
} else {
return {Status::RedisParseErr, errUnknownOption};
}
return Status::OK();
}
Status Execute([[maybe_unused]] engine::Context &ctx, [[maybe_unused]] Server *srv, Connection *conn,
std::string *output) override {
if (port_ != 0) {
conn->SetListeningPort(port_);
}
if (!ip_address_.empty()) {
conn->SetAnnounceIP(ip_address_);
}
*output = redis::RESP_OK;
return Status::OK();
}
private:
int port_ = 0;
std::string ip_address_;
};
class CommandFetchMeta : public Commander {
public:
Status Parse([[maybe_unused]] const std::vector<std::string> &args) override { return Status::OK(); }
Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn,
[[maybe_unused]] std::string *output) override {
int repl_fd = conn->GetFD();
std::string ip = conn->GetAnnounceIP();
auto s = util::SockSetBlocking(repl_fd, 1);
if (!s.IsOK()) {
return s.Prefixed("failed to set blocking mode on socket");
}
conn->NeedNotFreeBufferEvent();
conn->EnableFlag(redis::Connection::kCloseAsync);
srv->stats.IncrFullSyncCount();
// Feed-replica-meta thread
auto t = GET_OR_RET(util::CreateThread("feed-repl-info", [srv, repl_fd, ip, bev = conn->GetBufferEvent()] {
srv->IncrFetchFileThread();
auto exit = MakeScopeExit([srv, bev] {
bufferevent_free(bev);
srv->DecrFetchFileThread();
});
std::string files;
auto s = engine::Storage::ReplDataManager::GetFullReplDataInfo(srv->storage, &files);
if (!s.IsOK()) {
WARN("[replication] Failed to get full data file info: {}", s.Msg());
s = util::SockSend(repl_fd, redis::Error({Status::RedisErrorNoPrefix, "can't create db checkpoint"}), bev);
if (!s.IsOK()) {
WARN("[replication] Failed to send error response: {}", s.Msg());
}
return;
}
// Send full data file info
if (auto s = util::SockSend(repl_fd, files + CRLF, bev)) {
INFO("[replication] Succeed sending full data file info to {}", ip);
} else {
WARN("[replication] Fail to send full data file info {}, error: {}", ip, s.Msg());
}
auto now_secs = static_cast<time_t>(util::GetTimeStamp());
srv->storage->SetCheckpointAccessTimeSecs(now_secs);
}));
if (auto s = util::ThreadDetach(t); !s) {
return s;
}
return Status::OK();
}
};
class CommandFetchFile : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
files_str_ = args[1];
return Status::OK();
}
Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn,
[[maybe_unused]] std::string *output) override {
std::vector<std::string> files = util::Split(files_str_, ",");
int repl_fd = conn->GetFD();
std::string ip = conn->GetAnnounceIP();
auto s = util::SockSetBlocking(repl_fd, 1);
if (!s.IsOK()) {
return s.Prefixed("failed to set blocking mode on socket");
}
conn->NeedNotFreeBufferEvent(); // Feed-replica-file thread will close the replica bufferevent
conn->EnableFlag(redis::Connection::kCloseAsync);
auto t = GET_OR_RET(util::CreateThread("feed-repl-file", [srv, repl_fd, ip, files, bev = conn->GetBufferEvent()]() {
auto exit = MakeScopeExit([bev] { bufferevent_free(bev); });
srv->IncrFetchFileThread();
for (const auto &file : files) {
if (srv->IsStopped()) break;
uint64_t file_size = 0, max_replication_bytes = 0;
if (srv->GetConfig()->max_replication_mb > 0 && srv->GetFetchFileThreadNum() != 0) {
max_replication_bytes = (srv->GetConfig()->max_replication_mb * MiB) / srv->GetFetchFileThreadNum();
}
auto start = std::chrono::high_resolution_clock::now();
auto fd = UniqueFD(engine::Storage::ReplDataManager::OpenDataFile(srv->storage, file, &file_size));
if (!fd) break;
// Send file size and content
auto s = util::SockSend(repl_fd, std::to_string(file_size) + CRLF, bev);
if (s) {
s = util::SockSendFile(repl_fd, *fd, file_size, bev);
}
if (s) {
INFO("[replication] Succeed sending file {} to {}", file, ip);
} else {
WARN("[replication] Fail to send file {} to {}, error: {}", file, ip, s.Msg());
break;
}
fd.Close();
// Sleep if the speed of sending file is more than replication speed limit
auto end = std::chrono::high_resolution_clock::now();
uint64_t duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
if (max_replication_bytes > 0) {
auto shortest = static_cast<uint64_t>(static_cast<double>(file_size) /
static_cast<double>(max_replication_bytes) * (1000 * 1000));
if (duration < shortest) {
INFO("[replication] Need to sleep {} ms since of sending files too quickly", (shortest - duration) / 1000);
usleep(shortest - duration);
}
}
}
auto now_secs = util::GetTimeStamp<std::chrono::seconds>();
srv->storage->SetCheckpointAccessTimeSecs(now_secs);
srv->DecrFetchFileThread();
}));
if (auto s = util::ThreadDetach(t); !s) {
return s;
}
return Status::OK();
}
private:
std::string files_str_;
};
class CommandDBName : public Commander {
public:
Status Parse([[maybe_unused]] const std::vector<std::string> &args) override { return Status::OK(); }
Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn,
[[maybe_unused]] std::string *output) override {
conn->Reply(srv->storage->GetName() + CRLF);
return Status::OK();
}
};
class CommandWait : public Commander,
private EventCallbackBase<CommandWait>,
private EvbufCallbackBase<CommandWait, false> {
public:
Status Parse(const std::vector<std::string> &args) override {
auto num_replicas_result = ParseInt<int64_t>(args[1], 10);
if (!num_replicas_result || *num_replicas_result <= 0) {
return {Status::RedisParseErr, "numreplicas should be a positive integer"};
}
num_replicas_ = *num_replicas_result;
auto timeout_result = ParseInt<int64_t>(args[2], 10);
if (!timeout_result || *timeout_result < 0) {
return {Status::RedisParseErr, "timeout should be a non-negative integer"};
}
timeout_ = *timeout_result * 1000;
return Commander::Parse(args);
}
Status Execute([[maybe_unused]] engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
// Only master can execute WAIT command
if (srv->IsSlave()) {
return {Status::RedisExecErr, "WAIT command can only be executed on master"};
}
// Get current sequence number
auto current_seq = srv->storage->LatestSeqNumber();
// Check if we already have enough replicas at the current sequence
size_t reached_replicas = srv->GetReplicasReachedSequence(current_seq);
// If we already have enough replicas, return immediately
if (reached_replicas >= num_replicas_) {
*output = redis::Integer(reached_replicas);
return Status::OK();
}
conn_ = conn;
srv_ = srv;
// Block the connection and wait for replicas to catch up
srv->BlockOnWait(conn, current_seq, num_replicas_);
// set callback to use the callbacks defined in this class
SetCB(conn->GetBufferEvent());
// Disable read event so the connection will not process any other commands
// Disable write event so the connection will not send response
bufferevent_disable(conn->GetBufferEvent(), EV_READ | EV_WRITE);
if (timeout_ > 0) {
initTimer(current_seq, timeout_);
}
// The connection will be woken up by WakeupWaitConnections when enough replicas
// have reached the target sequence
return {Status::BlockingCmd};
}
void OnWrite(bufferevent *bev) {
// Before unblocking, we must confirm that the wait condition has actually been met or that a timeout occurred.
// Ideally, write callback is called after WAIT response is sent, but it may be called for previous commands.
// so we need to check if the connection is still waiting.
// For example, considering the following scenario:
// 1. SET k1 v1
// 2. WAIT 1 0
// 3. SET k1 v2
// After WAIT 1 0 is executed, the connection is blocked, and the write callback is called for SET k1 v1.
size_t reached_replicas = srv_->GetReplicasReachedSequence(target_seq_);
bool wait_condition_met = (reached_replicas >= num_replicas_);
// The timer is reset in the TimerCB before the connection is woken up.
// If the timer is null, it means we were woken up by a timeout.
bool timed_out = (timeout_ > 0 && timer_ == nullptr);
if (!wait_condition_met && !timed_out) {
return; // This is a premature write, so we do nothing and keep the connection blocked.
}
if (timer_ != nullptr) {
timer_.reset();
}
conn_->SetCB(bev);
bufferevent_enable(bev, EV_READ);
// We need to manually trigger the read event since we will stop processing commands
// in connection after the blocking command, so there may have some commands to be processed.
// Related issue: https://github.com/apache/kvrocks/issues/831
bufferevent_trigger(bev, EV_READ, BEV_TRIG_IGNORE_WATERMARKS);
}
void TimerCB(int, int16_t) {
timer_.reset();
// Wake up the connection upon timeout.
// WakeupWaitConnection will hold the lock of the connection during the execution,
// holding the lock is necessary to avoid race condition that timeout and replication ack happen at the same time.
srv_->WakeupWaitConnection(conn_, target_seq_);
}
void OnEvent(bufferevent *bev, int16_t events) {
if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) {
if (timer_ != nullptr) {
timer_.reset();
}
}
conn_->OnEvent(bev, events);
}
private:
// variables used for timeout only
int64_t timeout_ = 0; // microseconds
UniqueEvent timer_;
rocksdb::SequenceNumber target_seq_ = 0;
// variables used for all cases
Server *srv_ = nullptr;
uint64_t num_replicas_ = 0;
Connection *conn_ = nullptr;
void initTimer(rocksdb::SequenceNumber target_seq, int64_t timeout) {
target_seq_ = target_seq;
// init timer
auto bev = conn_->GetBufferEvent();
timer_.reset(NewTimer(bufferevent_get_base(bev)));
int64_t timeout_second = timeout / 1000 / 1000;
int64_t timeout_microsecond = timeout % (1000 * 1000);
timeval tm = {timeout_second, static_cast<int>(timeout_microsecond)};
evtimer_add(timer_.get(), &tm);
}
};
REDIS_REGISTER_COMMANDS(Replication, MakeCmdAttr<CommandReplConf>("replconf", -3, "read-only no-script", NO_KEY),
MakeCmdAttr<CommandPSync>("psync", -2, "read-only no-multi no-script admin", NO_KEY),
MakeCmdAttr<CommandFetchMeta>("_fetch_meta", 1, "read-only no-multi no-script admin", NO_KEY),
MakeCmdAttr<CommandFetchFile>("_fetch_file", 2, "read-only no-multi no-script admin", NO_KEY),
MakeCmdAttr<CommandDBName>("_db_name", 1, "read-only no-multi", NO_KEY),
MakeCmdAttr<CommandWait>("wait", 3, "read-only no-multi no-script blocking", NO_KEY), )
} // namespace redis