blob: 3edf8a3e435ddd35b5d6fab704fe6597ce7f9564 [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 "slot_migrate.h"
#include <memory>
#include <utility>
#include "db_util.h"
#include "event_util.h"
#include "fmt/format.h"
#include "io_util.h"
#include "storage/batch_extractor.h"
#include "storage/iterator.h"
#include "storage/redis_metadata.h"
#include "sync_migrate_context.h"
#include "thread_util.h"
#include "time_util.h"
#include "types/redis_stream_base.h"
constexpr std::string_view errFailedToSendCommands = "failed to send commands to restore a key";
constexpr std::string_view errMigrationTaskCanceled = "key migration stopped due to a task cancellation";
constexpr std::string_view errFailedToSetImportStatus = "failed to set import status on destination node";
constexpr std::string_view errUnsupportedMigrationType = "unsupported migration type";
static std::map<RedisType, std::string> type_to_cmd = {
{kRedisString, "set"}, {kRedisList, "rpush"}, {kRedisHash, "hmset"}, {kRedisSet, "sadd"},
{kRedisZSet, "zadd"}, {kRedisBitmap, "setbit"}, {kRedisSortedint, "siadd"}, {kRedisStream, "xadd"},
};
SlotMigrator::SlotMigrator(Server *srv)
: Database(srv->storage, kDefaultNamespace),
srv_(srv),
max_migration_speed_(srv->GetConfig()->migrate_speed),
max_pipeline_size_(srv->GetConfig()->pipeline_size),
seq_gap_limit_(srv->GetConfig()->sequence_gap),
migrate_batch_bytes_per_sec_(srv->GetConfig()->migrate_batch_rate_limit_mb * MiB),
migrate_batch_size_bytes_(srv->GetConfig()->migrate_batch_size_kb * KiB) {
// Let metadata_cf_handle_ be nullptr, and get them in real time to avoid accessing invalid pointer,
// because metadata_cf_handle_ and db_ will be destroyed if DB is reopened.
// [Situation]:
// 1. Start an empty slave server.
// 2. Connect to master which has amounted of data, and trigger full synchronization.
// 3. After replication, change slave to master and start slot migrate.
// 4. It will occur segment fault when using metadata_cf_handle_ to create iterator of rocksdb.
// [Reason]:
// After full synchronization, DB will be reopened, db_ and metadata_cf_handle_ will be released.
// Then, if we create rocksdb iterator with metadata_cf_handle_, it will go wrong.
// [Solution]:
// db_ and metadata_cf_handle_ will be replaced by storage_->GetDB() and storage_->GetCFHandle("metadata")
// in all functions used in migration process.
// [Note]:
// This problem may exist in all functions of Database called in slot migration process.
metadata_cf_handle_ = nullptr;
if (srv->IsSlave()) {
SetStopMigrationFlag(true);
}
}
Status SlotMigrator::PerformSlotRangeMigration(const std::string &node_id, std::string &dst_ip, int dst_port,
const SlotRange &slot_range, SyncMigrateContext *blocking_ctx) {
// TODO: concurrent migration, multiple migration jobs
// Only one slot migration job at the same time
SlotRange empty_slot_range = {-1, -1};
if (!slot_range_.compare_exchange_strong(empty_slot_range, slot_range)) {
return {Status::NotOK, "There is already a migrating job"};
}
if (slot_range.HasOverlap(forbidden_slot_range_)) {
// Have to release migrate slot set above
slot_range_ = empty_slot_range;
return {Status::NotOK, "Can't migrate slot which has been migrated"};
}
migration_state_ = MigrationState::kStarted;
auto speed = srv_->GetConfig()->migrate_speed;
auto seq_gap = srv_->GetConfig()->sequence_gap;
auto pipeline_size = srv_->GetConfig()->pipeline_size;
if (speed <= 0) {
speed = 0;
}
if (pipeline_size <= 0) {
pipeline_size = kDefaultMaxPipelineSize;
}
if (seq_gap <= 0) {
seq_gap = kDefaultSequenceGapLimit;
}
if (blocking_ctx) {
std::unique_lock<std::mutex> lock(blocking_mutex_);
blocking_context_ = blocking_ctx;
blocking_context_->Suspend();
}
dst_node_ = node_id;
// Create migration job
auto job = std::make_unique<SlotMigrationJob>(slot_range, dst_ip, dst_port, speed, pipeline_size, seq_gap);
{
std::lock_guard<std::mutex> guard(job_mutex_);
migration_job_ = std::move(job);
job_cv_.notify_one();
}
INFO("[migrate] Start migrating slot(s) {} to {}:{}", slot_range.String(), dst_ip, dst_port);
return Status::OK();
}
SlotMigrator::~SlotMigrator() {
if (thread_state_ == ThreadState::Running) {
stop_migration_ = true;
thread_state_ = ThreadState::Terminated;
job_cv_.notify_all();
if (auto s = util::ThreadJoin(t_); !s) {
WARN("Slot migrating thread operation failed: {}", s.Msg());
}
}
}
Status SlotMigrator::CreateMigrationThread() {
t_ = GET_OR_RET(util::CreateThread("slot-migrate", [this] {
thread_state_ = ThreadState::Running;
this->loop();
}));
return Status::OK();
}
void SlotMigrator::loop() {
while (true) {
{
std::unique_lock<std::mutex> ul(job_mutex_);
job_cv_.wait(ul, [&] { return isTerminated() || migration_job_; });
}
if (isTerminated()) {
clean();
return;
}
INFO("[migrate] Migrating slot(s): {}, dst_ip: {}, dst_port: {}, max_speed: {}, max_pipeline_size: {}",
migration_job_->slot_range.String(), migration_job_->dst_ip, migration_job_->dst_port,
migration_job_->max_speed, migration_job_->max_pipeline_size);
dst_ip_ = migration_job_->dst_ip;
dst_port_ = migration_job_->dst_port;
max_migration_speed_ = migration_job_->max_speed;
max_pipeline_size_ = migration_job_->max_pipeline_size;
seq_gap_limit_ = migration_job_->seq_gap_limit;
runMigrationProcess();
}
}
void SlotMigrator::runMigrationProcess() {
current_stage_ = SlotMigrationStage::kStart;
while (true) {
if (isTerminated()) {
WARN("[migrate] Will stop state machine, because the thread was terminated");
clean();
return;
}
switch (current_stage_) {
case SlotMigrationStage::kStart: {
auto s = startMigration();
if (s.IsOK()) {
INFO("[migrate] Succeed to start migrating slot(s) {}", slot_range_.load().String());
current_stage_ = SlotMigrationStage::kSnapshot;
} else {
ERROR("[migrate] Failed to start migrating slot(s) {}. Error: {}", slot_range_.load().String(), s.Msg());
current_stage_ = SlotMigrationStage::kFailed;
resumeSyncCtx(s);
}
break;
}
case SlotMigrationStage::kSnapshot: {
auto s = sendSnapshot();
if (s.IsOK()) {
current_stage_ = SlotMigrationStage::kWAL;
} else {
ERROR("[migrate] Failed to send snapshot of slot(s) {}. Error: {}", slot_range_.load().String(), s.Msg());
current_stage_ = SlotMigrationStage::kFailed;
resumeSyncCtx(s);
}
break;
}
case SlotMigrationStage::kWAL: {
auto s = syncWAL();
if (s.IsOK()) {
INFO("[migrate] Succeed to sync from WAL for slot(s) {}", slot_range_.load().String());
current_stage_ = SlotMigrationStage::kSuccess;
} else {
ERROR("[migrate] Failed to sync from WAL for slot(s) {}. Error: {}", slot_range_.load().String(), s.Msg());
current_stage_ = SlotMigrationStage::kFailed;
resumeSyncCtx(s);
}
break;
}
case SlotMigrationStage::kSuccess: {
auto s = finishSuccessfulMigration();
if (s.IsOK()) {
INFO("[migrate] Succeed to migrate slot(s) {}", slot_range_.load().String());
current_stage_ = SlotMigrationStage::kClean;
migration_state_ = MigrationState::kSuccess;
resumeSyncCtx(s);
} else {
ERROR("[migrate] Failed to finish a successful migration of slot(s) {}. Error: {}",
slot_range_.load().String(), s.Msg());
current_stage_ = SlotMigrationStage::kFailed;
resumeSyncCtx(s);
}
break;
}
case SlotMigrationStage::kFailed: {
auto s = finishFailedMigration();
if (!s.IsOK()) {
ERROR("[migrate] Failed to finish a failed migration of slot(s) {}. Error: {}", slot_range_.load().String(),
s.Msg());
}
INFO("[migrate] Failed to migrate a slot(s) {}", slot_range_.load().String());
migration_state_ = MigrationState::kFailed;
current_stage_ = SlotMigrationStage::kClean;
break;
}
case SlotMigrationStage::kClean: {
clean();
return;
}
default:
ERROR("[migrate] Unexpected state for the state machine: {}", static_cast<int>(current_stage_));
clean();
return;
}
}
}
Status SlotMigrator::startMigration() {
// Get snapshot and sequence
slot_snapshot_ = storage_->GetDB()->GetSnapshot();
if (!slot_snapshot_) {
return {Status::NotOK, "failed to create snapshot"};
}
wal_begin_seq_ = slot_snapshot_->GetSequenceNumber();
last_send_time_ = 0;
// Connect to the destination node
auto result = util::SockConnect(dst_ip_, dst_port_);
if (!result.IsOK()) {
return {Status::NotOK, fmt::format("failed to connect to the destination node: {}", result.Msg())};
}
dst_fd_.Reset(*result);
// Auth first
std::string pass = srv_->GetConfig()->requirepass;
if (!pass.empty()) {
auto s = authOnDstNode(*dst_fd_, pass);
if (!s.IsOK()) {
return s.Prefixed("failed to authenticate on destination node");
}
}
// Set destination node import status to START
auto s = setImportStatusOnDstNode(*dst_fd_, kImportStart);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSetImportStatus);
}
migration_type_ = srv_->GetConfig()->migrate_type;
// If the APPLYBATCH command is not supported on the destination,
// we will fall back to the redis-command migration type.
if (migration_type_ == MigrationType::kRawKeyValue) {
bool supported = GET_OR_RET(supportedApplyBatchCommandOnDstNode(*dst_fd_));
if (!supported) {
INFO("APPLYBATCH command is not supported, use redis command for migration");
migration_type_ = MigrationType::kRedisCommand;
}
}
INFO("[migrate] Start migrating slot(s) {}, connect destination fd {}", slot_range_.load().String(), *dst_fd_);
return Status::OK();
}
Status SlotMigrator::sendSnapshot() {
if (migration_type_ == MigrationType::kRedisCommand) {
return sendSnapshotByCmd();
} else if (migration_type_ == MigrationType::kRawKeyValue) {
return sendSnapshotByRawKV();
}
return {Status::NotOK, std::string(errUnsupportedMigrationType)};
}
Status SlotMigrator::syncWAL() {
if (migration_type_ == MigrationType::kRedisCommand) {
return syncWALByCmd();
} else if (migration_type_ == MigrationType::kRawKeyValue) {
return syncWALByRawKV();
}
return {Status::NotOK, std::string(errUnsupportedMigrationType)};
}
Status SlotMigrator::sendSnapshotByCmd() {
uint64_t migrated_key_cnt = 0;
uint64_t expired_key_cnt = 0;
uint64_t empty_key_cnt = 0;
std::string restore_cmds;
SlotRange slot_range = slot_range_;
INFO("[migrate] Start migrating snapshot of slot(s): {}", slot_range.String());
// Construct key prefix to iterate the keys belong to the target slot
std::string prefix = ComposeSlotKeyPrefix(namespace_, slot_range.start);
INFO("[migrate] Iterate keys of slot(s), key's prefix: {}", prefix);
std::string upper_bound = ComposeSlotKeyUpperBound(namespace_, slot_range.end);
rocksdb::ReadOptions read_options = storage_->DefaultScanOptions();
read_options.snapshot = slot_snapshot_;
Slice prefix_slice(prefix);
Slice upper_bound_slice(upper_bound);
read_options.iterate_lower_bound = &prefix_slice;
read_options.iterate_upper_bound = &upper_bound_slice;
rocksdb::ColumnFamilyHandle *cf_handle = storage_->GetCFHandle(ColumnFamilyID::Metadata);
auto iter = util::UniqueIterator(storage_->GetDB()->NewIterator(read_options, cf_handle));
// Seek to the beginning of keys start with 'prefix' and iterate all these keys
int current_slot = slot_range.start;
for (iter->Seek(prefix); iter->Valid(); iter->Next()) {
// The migrating task has to be stopped, if server role is changed from master to slave
// or flush command (flushdb or flushall) is executed
if (stop_migration_) {
return {Status::NotOK, std::string(errMigrationTaskCanceled)};
}
// Iteration is out of range
current_slot = ExtractSlotId(iter->key());
if (!slot_range.Contains(current_slot)) {
break;
}
// Get user key
auto [_, user_key] = ExtractNamespaceKey(iter->key(), /*slot_id_encoded=*/true);
// Add key's constructed commands to restore_cmds, send pipeline or not according to task's max_pipeline_size
auto result = migrateOneKey(user_key, iter->value(), &restore_cmds);
if (!result.IsOK()) {
return {Status::NotOK, fmt::format("failed to migrate a key {}: {}", user_key, result.Msg())};
}
if (*result == KeyMigrationResult::kMigrated) {
INFO("[migrate] The key {} successfully migrated", user_key);
migrated_key_cnt++;
} else if (*result == KeyMigrationResult::kExpired) {
INFO("[migrate] The key {} is expired", user_key);
expired_key_cnt++;
} else if (*result == KeyMigrationResult::kUnderlyingStructEmpty) {
INFO("[migrate] The key {} has no elements", user_key);
empty_key_cnt++;
} else {
ERROR("[migrate] Migrated a key {} with unexpected result: {}", user_key, static_cast<int>(*result));
return {Status::NotOK};
}
}
if (auto s = iter->status(); !s.ok()) {
auto err_str = s.ToString();
ERROR("[migrate] Failed to iterate keys of slot {}: {}", current_slot, err_str);
return {Status::NotOK, fmt::format("failed to iterate keys of slot {}: {}", current_slot, err_str)};
}
// It's necessary to send commands that are still in the pipeline since the final pipeline may not be sent
// while iterating keys because its size could be less than max_pipeline_size_
auto s = sendCmdsPipelineIfNeed(&restore_cmds, true);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
INFO(
"[migrate] Succeed to migrate slot(s) snapshot, slot(s): {}, Migrated keys: {}, Expired keys: {}, Empty keys: {}",
slot_range.String(), migrated_key_cnt, expired_key_cnt, empty_key_cnt);
return Status::OK();
}
Status SlotMigrator::syncWALByCmd() {
// Send incremental data from WAL circularly until new increment less than a certain amount
auto s = syncWalBeforeForbiddingSlot();
if (!s.IsOK()) {
return s.Prefixed("failed to sync WAL before forbidding a slot");
}
setForbiddenSlotRange(slot_range_);
// Send last incremental data
s = syncWalAfterForbiddingSlot();
if (!s.IsOK()) {
return s.Prefixed("failed to sync WAL after forbidding a slot");
}
return Status::OK();
}
Status SlotMigrator::finishSuccessfulMigration() {
if (stop_migration_) {
return {Status::NotOK, std::string(errMigrationTaskCanceled)};
}
// Set import status on the destination node to SUCCESS
auto s = setImportStatusOnDstNode(*dst_fd_, kImportSuccess);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSetImportStatus);
}
std::string dst_ip_port = dst_ip_ + ":" + std::to_string(dst_port_);
s = srv_->cluster->SetSlotRangeMigrated(slot_range_, dst_ip_port);
if (!s.IsOK()) {
return s.Prefixed(
fmt::format("failed to set slot(s) {} as migrated to {}", slot_range_.load().String(), dst_ip_port));
}
migrate_failed_slot_range_ = {-1, -1};
return Status::OK();
}
Status SlotMigrator::finishFailedMigration() {
// Stop slot will forbid writing
migrate_failed_slot_range_ = slot_range_.load();
forbidden_slot_range_ = {-1, -1};
// Set import status on the destination node to FAILED
auto s = setImportStatusOnDstNode(*dst_fd_, kImportFailed);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSetImportStatus);
}
return Status::OK();
}
void SlotMigrator::clean() {
INFO("[migrate] Clean resources of migrating slot(s) {}", slot_range_.load().String());
if (slot_snapshot_) {
storage_->GetDB()->ReleaseSnapshot(slot_snapshot_);
slot_snapshot_ = nullptr;
}
current_stage_ = SlotMigrationStage::kNone;
current_pipeline_size_ = 0;
wal_begin_seq_ = 0;
std::lock_guard<std::mutex> guard(job_mutex_);
migration_job_.reset();
dst_fd_.Reset();
slot_range_ = {-1, -1};
SetStopMigrationFlag(false);
}
Status SlotMigrator::authOnDstNode(int sock_fd, const std::string &password) {
std::string cmd = redis::ArrayOfBulkStrings({"auth", password});
auto s = util::SockSend(sock_fd, cmd);
if (!s.IsOK()) {
return s.Prefixed("failed to send AUTH command");
}
s = checkSingleResponse(sock_fd);
if (!s.IsOK()) {
return s.Prefixed("failed to check the response of AUTH command");
}
return Status::OK();
}
Status SlotMigrator::setImportStatusOnDstNode(int sock_fd, int status) {
if (sock_fd <= 0) return {Status::NotOK, "invalid socket descriptor"};
std::string cmd =
redis::ArrayOfBulkStrings({"cluster", "import", slot_range_.load().String(), std::to_string(status)});
auto s = util::SockSend(sock_fd, cmd);
if (!s.IsOK()) {
return s.Prefixed("failed to send command to the destination node");
}
s = checkSingleResponse(sock_fd);
if (!s.IsOK()) {
return s.Prefixed("failed to check the response from the destination node");
}
return Status::OK();
}
StatusOr<bool> SlotMigrator::supportedApplyBatchCommandOnDstNode(int sock_fd) {
std::string cmd = redis::ArrayOfBulkStrings({"command", "info", "applybatch"});
auto s = util::SockSend(sock_fd, cmd);
if (!s.IsOK()) {
return s.Prefixed("failed to send command info to the destination node");
}
UniqueEvbuf evbuf;
if (evbuffer_read(evbuf.get(), sock_fd, -1) <= 0) {
return Status::FromErrno("read response error");
}
UniqueEvbufReadln line(evbuf.get(), EVBUFFER_EOL_CRLF_STRICT);
if (!line) {
return Status::FromErrno("read empty response");
}
if (line[0] == '*') {
line = UniqueEvbufReadln(evbuf.get(), EVBUFFER_EOL_LF);
if (line && line[0] == '*') {
return true;
}
}
return false;
}
Status SlotMigrator::checkSingleResponse(int sock_fd) { return checkMultipleResponses(sock_fd, 1); }
// Commands | Response | Instance
// ++++++++++++++++++++++++++++++++++++++++
// set Redis::Integer :1\r\n
// hset Redis::SimpleString +OK\r\n
// sadd Redis::Integer
// zadd Redis::Integer
// siadd Redis::Integer
// setbit Redis::Integer
// expire Redis::Integer
// lpush Redis::Integer
// rpush Redis::Integer
// ltrim Redis::SimpleString -Err\r\n
// linsert Redis::Integer
// lset Redis::SimpleString
// hdel Redis::Integer
// srem Redis::Integer
// zrem Redis::Integer
// lpop Redis::NilString $-1\r\n
// or Redis::BulkString $1\r\n1\r\n
// rpop Redis::NilString
// or Redis::BulkString
// lrem Redis::Integer
// sirem Redis::Integer
// del Redis::Integer
// xadd Redis::BulkString
// bitfield Redis::Array *1\r\n:0
Status SlotMigrator::checkMultipleResponses(int sock_fd, int total) {
if (sock_fd < 0 || total <= 0) {
return {Status::NotOK, fmt::format("invalid arguments: sock_fd={}, count={}", sock_fd, total)};
}
// Set socket receive timeout first
struct timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;
setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
// Start checking response
size_t bulk_or_array_len = 0;
int cnt = 0;
parser_state_ = ParserState::ArrayLen;
UniqueEvbuf evbuf;
while (true) {
// Read response data from socket buffer to the event buffer
if (evbuffer_read(evbuf.get(), sock_fd, -1) <= 0) {
return {Status::NotOK, fmt::format("failed to read response: {}", strerror(errno))};
}
// Parse response data in event buffer
bool run = true;
while (run) {
switch (parser_state_) {
// Handle single string response
case ParserState::ArrayLen: {
UniqueEvbufReadln line(evbuf.get(), EVBUFFER_EOL_CRLF_STRICT);
if (!line) {
INFO("[migrate] Event buffer is empty, read socket again");
run = false;
break;
}
if (line[0] == '-') {
return {Status::NotOK, fmt::format("got invalid response of length {}: {}", line.length, line.get())};
} else if (line[0] == '$' || line[0] == '*') {
auto parse_result = ParseInt<uint64_t>(std::string(line.get() + 1, line.length - 1), 10);
if (!parse_result) {
return {Status::NotOK, "protocol error: expected integer value"};
}
bulk_or_array_len = *parse_result;
if (bulk_or_array_len <= 0) {
parser_state_ = ParserState::OneRspEnd;
} else if (line[0] == '$') {
parser_state_ = ParserState::BulkData;
} else {
parser_state_ = ParserState::ArrayData;
}
} else if (line[0] == '+' || line[0] == ':') {
parser_state_ = ParserState::OneRspEnd;
} else {
return {Status::NotOK, fmt::format("got unexpected response of length {}: {}", line.length, line.get())};
}
break;
}
// Handle bulk string response
case ParserState::BulkData: {
if (evbuffer_get_length(evbuf.get()) < bulk_or_array_len + 2) {
INFO("[migrate] Bulk data in event buffer is not complete, read socket again");
run = false;
break;
}
// TODO(chrisZMF): Check tail '\r\n'
evbuffer_drain(evbuf.get(), bulk_or_array_len + 2);
bulk_or_array_len = 0;
parser_state_ = ParserState::OneRspEnd;
break;
}
case ParserState::ArrayData: {
while (run && bulk_or_array_len > 0) {
evbuffer_ptr ptr = evbuffer_search_eol(evbuf.get(), nullptr, nullptr, EVBUFFER_EOL_CRLF_STRICT);
if (ptr.pos < 0) {
INFO("[migrate] Array data in event buffer is not complete, read socket again");
run = false;
break;
}
evbuffer_drain(evbuf.get(), ptr.pos + 2);
--bulk_or_array_len;
}
if (run) {
parser_state_ = ParserState::OneRspEnd;
}
break;
}
case ParserState::OneRspEnd: {
cnt++;
if (cnt >= total) {
return Status::OK();
}
parser_state_ = ParserState::ArrayLen;
break;
}
default:
break;
}
}
}
}
StatusOr<KeyMigrationResult> SlotMigrator::migrateOneKey(const rocksdb::Slice &key,
const rocksdb::Slice &encoded_metadata,
std::string *restore_cmds) {
std::string bytes = encoded_metadata.ToString();
Metadata metadata(kRedisNone, false);
if (auto s = metadata.Decode(bytes); !s.ok()) {
return {Status::NotOK, s.ToString()};
}
if (!metadata.IsEmptyableType() && metadata.size == 0) {
return KeyMigrationResult::kUnderlyingStructEmpty;
}
if (metadata.Expired()) {
return KeyMigrationResult::kExpired;
}
// Construct command according to type of the key
switch (metadata.Type()) {
case kRedisString:
case kRedisJson: {
auto s = migrateSimpleKey(key, metadata, bytes, restore_cmds);
if (!s.IsOK()) {
return s.Prefixed("failed to migrate simple key");
}
break;
}
case kRedisList:
case kRedisZSet:
case kRedisBitmap:
case kRedisHash:
case kRedisSet:
case kRedisSortedint: {
auto s = migrateComplexKey(key, metadata, restore_cmds);
if (!s.IsOK()) {
return s.Prefixed("failed to migrate complex key");
}
break;
}
case kRedisStream: {
StreamMetadata stream_md(false);
if (auto s = stream_md.Decode(bytes); !s.ok()) {
return {Status::NotOK, s.ToString()};
}
auto s = migrateStream(key, stream_md, restore_cmds);
if (!s.IsOK()) {
return s.Prefixed("failed to migrate stream key");
}
break;
}
case kRedisHyperLogLog: {
// HyperLogLog migration by cmd is not supported,
// since it's hard to restore the same key structure for HyperLogLog
// commands.
break;
}
default:
break;
}
return KeyMigrationResult::kMigrated;
}
Status SlotMigrator::migrateSimpleKey(const rocksdb::Slice &key, const Metadata &metadata, const std::string &bytes,
std::string *restore_cmds) {
if (metadata.Type() == kRedisString) {
std::vector<std::string> command = {"SET", key.ToString(), bytes.substr(Metadata::GetOffsetAfterExpire(bytes[0]))};
if (metadata.expire > 0) {
command.emplace_back("PXAT");
command.emplace_back(std::to_string(metadata.expire));
}
*restore_cmds += redis::ArrayOfBulkStrings(command);
current_pipeline_size_++;
} else if (metadata.Type() == kRedisJson) {
// kRedisJson
JsonValue json_value;
if (auto s = redis::Json::FromRawString(bytes, &json_value); !s.ok()) {
return {Status::NotOK, s.ToString()};
}
auto json_bytes = GET_OR_RET(json_value.Dump());
std::vector<std::string> command = {"JSON.SET", key.ToString(), "$", std::move(json_bytes)};
*restore_cmds += redis::ArrayOfBulkStrings(command);
current_pipeline_size_++;
if (metadata.expire > 0) {
*restore_cmds += redis::ArrayOfBulkStrings({"PEXPIREAT", key.ToString(), std::to_string(metadata.expire)});
current_pipeline_size_++;
}
} else {
return {Status::NotOK, "unsupported simple key type"};
}
// Check whether pipeline needs to be sent
// TODO(chrisZMF): Resend data if failed to send data
auto s = sendCmdsPipelineIfNeed(restore_cmds, false);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
return Status::OK();
}
Status SlotMigrator::migrateComplexKey(const rocksdb::Slice &key, const Metadata &metadata, std::string *restore_cmds) {
std::string cmd;
{
auto iter = type_to_cmd.find(metadata.Type());
if (iter != type_to_cmd.end()) {
cmd = iter->second;
} else {
if (metadata.Type() > RedisTypeNames.size()) {
return {Status::NotOK, "unknown key type: " + std::to_string(metadata.Type())};
}
return {Status::NotOK, fmt::format("unsupported complex key type: {}", metadata.TypeName())};
}
}
std::vector<std::string> user_cmd = {cmd, key.ToString()};
// Construct key prefix to iterate values of the complex type user key
std::string slot_key = AppendNamespacePrefix(key);
std::string prefix_subkey = InternalKey(slot_key, "", metadata.version, true).Encode();
rocksdb::ReadOptions read_options = storage_->DefaultScanOptions();
read_options.snapshot = slot_snapshot_;
Slice prefix_slice(prefix_subkey);
read_options.iterate_lower_bound = &prefix_slice;
// Should use th raw db iterator to avoid reading uncommitted writes in transaction mode
auto iter = util::UniqueIterator(storage_->GetDB()->NewIterator(read_options));
int item_count = 0;
for (iter->Seek(prefix_subkey); iter->Valid(); iter->Next()) {
if (stop_migration_) {
return {Status::NotOK, std::string(errMigrationTaskCanceled)};
}
if (!iter->key().starts_with(prefix_subkey)) {
break;
}
// Parse values of the complex key
// InternalKey is adopted to get complex key's value from the formatted key return by iterator of rocksdb
InternalKey inkey(iter->key(), true);
switch (metadata.Type()) {
case kRedisSet: {
user_cmd.emplace_back(inkey.GetSubKey().ToString());
break;
}
case kRedisSortedint: {
auto id = DecodeFixed64(inkey.GetSubKey().ToString().data());
user_cmd.emplace_back(std::to_string(id));
break;
}
case kRedisZSet: {
auto score = DecodeDouble(iter->value().ToString().data());
user_cmd.emplace_back(util::Float2String(score));
user_cmd.emplace_back(inkey.GetSubKey().ToString());
break;
}
case kRedisBitmap: {
auto s = migrateBitmapKey(inkey, &iter, &user_cmd, restore_cmds);
if (!s.IsOK()) {
return s.Prefixed("failed to migrate bitmap key");
}
break;
}
case kRedisHash: {
user_cmd.emplace_back(inkey.GetSubKey().ToString());
user_cmd.emplace_back(iter->value().ToString());
break;
}
case kRedisList: {
user_cmd.emplace_back(iter->value().ToString());
break;
}
case kRedisHyperLogLog: {
break;
}
default:
break;
}
// Check item count
// Exclude bitmap because it does not have hmset-like command
if (metadata.Type() != kRedisBitmap) {
item_count++;
if (item_count >= kMaxItemsInCommand) {
*restore_cmds += redis::ArrayOfBulkStrings(user_cmd);
current_pipeline_size_++;
item_count = 0;
// Have to clear saved items
user_cmd.erase(user_cmd.begin() + 2, user_cmd.end());
// Send commands if the pipeline contains enough of them
auto s = sendCmdsPipelineIfNeed(restore_cmds, false);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
}
}
}
if (auto s = iter->status(); !s.ok()) {
return {Status::NotOK,
fmt::format("failed to iterate values of the complex key {}: {}", key.ToString(), s.ToString())};
}
// Have to check the item count of the last command list
if (item_count % kMaxItemsInCommand != 0) {
*restore_cmds += redis::ArrayOfBulkStrings(user_cmd);
current_pipeline_size_++;
}
// Add TTL for complex key
if (metadata.expire > 0) {
*restore_cmds += redis::ArrayOfBulkStrings({"PEXPIREAT", key.ToString(), std::to_string(metadata.expire)});
current_pipeline_size_++;
}
// Send commands if the pipeline contains enough of them
auto s = sendCmdsPipelineIfNeed(restore_cmds, false);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
return Status::OK();
}
Status SlotMigrator::migrateStream(const Slice &key, const StreamMetadata &metadata, std::string *restore_cmds) {
rocksdb::ReadOptions read_options = storage_->DefaultScanOptions();
read_options.snapshot = slot_snapshot_;
std::string ns_key = AppendNamespacePrefix(key);
// Construct key prefix to iterate values of the stream
std::string prefix_key = InternalKey(ns_key, "", metadata.version, true).Encode();
rocksdb::Slice prefix_key_slice(prefix_key);
read_options.iterate_lower_bound = &prefix_key_slice;
// Should use th raw db iterator to avoid reading uncommitted writes in transaction mode
auto iter =
util::UniqueIterator(storage_->GetDB()->NewIterator(read_options, storage_->GetCFHandle(ColumnFamilyID::Stream)));
std::vector<std::string> user_cmd = {type_to_cmd[metadata.Type()], key.ToString()};
for (iter->Seek(prefix_key); iter->Valid(); iter->Next()) {
if (stop_migration_) {
return {Status::NotOK, std::string(errMigrationTaskCanceled)};
}
if (!iter->key().starts_with(prefix_key)) {
break;
}
auto s = WriteBatchExtractor::ExtractStreamAddCommand(true, iter->key(), iter->value(), &user_cmd);
if (!s.IsOK()) {
return s;
}
*restore_cmds += redis::ArrayOfBulkStrings(user_cmd);
current_pipeline_size_++;
user_cmd.erase(user_cmd.begin() + 2, user_cmd.end());
s = sendCmdsPipelineIfNeed(restore_cmds, false);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
}
if (auto s = iter->status(); !s.ok()) {
return {Status::NotOK,
fmt::format("failed to iterate values of the stream key {}: {}", key.ToString(), s.ToString())};
}
// commands like XTRIM and XDEL affect stream's metadata, but we use only XADD for a slot migration
// XSETID is used to adjust stream's info on the destination node according to the current values on the source
*restore_cmds += redis::ArrayOfBulkStrings({"XSETID", key.ToString(), metadata.last_generated_id.ToString(),
"ENTRIESADDED", std::to_string(metadata.entries_added), "MAXDELETEDID",
metadata.max_deleted_entry_id.ToString()});
current_pipeline_size_++;
// Add TTL
if (metadata.expire > 0) {
*restore_cmds += redis::ArrayOfBulkStrings({"PEXPIREAT", key.ToString(), std::to_string(metadata.expire)});
current_pipeline_size_++;
}
auto s = sendCmdsPipelineIfNeed(restore_cmds, false);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
return Status::OK();
}
Status SlotMigrator::migrateBitmapKey(const InternalKey &inkey, std::unique_ptr<rocksdb::Iterator> *iter,
std::vector<std::string> *user_cmd, std::string *restore_cmds) {
std::string index_str = inkey.GetSubKey().ToString();
std::string fragment = (*iter)->value().ToString();
auto parse_result = ParseInt<int>(index_str, 10);
if (!parse_result) {
return {Status::RedisParseErr, "index is not a valid integer"};
}
uint32_t index = *parse_result;
// Bitmap does not have hmset-like command
// TODO(chrisZMF): Use hmset-like command for efficiency
for (int byte_idx = 0; byte_idx < static_cast<int>(fragment.size()); byte_idx++) {
if (fragment[byte_idx] & 0xff) {
for (int bit_idx = 0; bit_idx < 8; bit_idx++) {
if (fragment[byte_idx] & (1 << bit_idx)) {
uint32_t offset = (index * 8) + (byte_idx * 8) + bit_idx;
user_cmd->emplace_back(std::to_string(offset));
user_cmd->emplace_back("1");
*restore_cmds += redis::ArrayOfBulkStrings(*user_cmd);
current_pipeline_size_++;
user_cmd->erase(user_cmd->begin() + 2, user_cmd->end());
}
}
auto s = sendCmdsPipelineIfNeed(restore_cmds, false);
if (!s.IsOK()) {
return s.Prefixed(errFailedToSendCommands);
}
}
}
return Status::OK();
}
Status SlotMigrator::sendCmdsPipelineIfNeed(std::string *commands, bool need) {
if (stop_migration_) {
return {Status::NotOK, std::string(errMigrationTaskCanceled)};
}
// Check pipeline
if (!need && current_pipeline_size_ < max_pipeline_size_) {
return Status::OK();
}
if (current_pipeline_size_ == 0) {
INFO("[migrate] No commands to send");
return Status::OK();
}
applyMigrationSpeedLimit();
auto s = util::SockSend(*dst_fd_, *commands);
if (!s.IsOK()) {
return s.Prefixed("failed to write data to a socket");
}
last_send_time_ = util::GetTimeStampUS();
s = checkMultipleResponses(*dst_fd_, current_pipeline_size_);
if (!s.IsOK()) {
return s.Prefixed("wrong response from the destination node");
}
// Clear commands and running pipeline
commands->clear();
current_pipeline_size_ = 0;
return Status::OK();
}
void SlotMigrator::setForbiddenSlotRange(const SlotRange &slot_range) {
INFO("[migrate] Setting forbidden slot(s) {}", slot_range.String());
// Block server to set forbidden slot
uint64_t during = util::GetTimeStampUS();
{
auto exclusivity = srv_->WorkExclusivityGuard();
forbidden_slot_range_ = slot_range;
}
during = util::GetTimeStampUS() - during;
INFO("[migrate] To set forbidden slot, server was blocked for {} us", during);
}
void SlotMigrator::ReleaseForbiddenSlotRange() {
INFO("[migrate] Release forbidden slot(s) {}", forbidden_slot_range_.load().String());
forbidden_slot_range_ = {-1, -1};
}
void SlotMigrator::applyMigrationSpeedLimit() const {
if (max_migration_speed_ > 0) {
uint64_t current_time = util::GetTimeStampUS();
uint64_t per_request_time = 1000000 * max_pipeline_size_ / max_migration_speed_;
if (per_request_time == 0) {
per_request_time = 1;
}
if (last_send_time_ + per_request_time > current_time) {
uint64_t during = last_send_time_ + per_request_time - current_time;
INFO("[migrate] Sleep to limit migration speed for: {}", during);
std::this_thread::sleep_for(std::chrono::microseconds(during));
}
}
}
Status SlotMigrator::generateCmdsFromBatch(rocksdb::BatchResult *batch, std::string *commands) {
// Iterate batch to get keys and construct commands for keys
WriteBatchExtractor write_batch_extractor(storage_->IsSlotIdEncoded(), slot_range_, false);
rocksdb::Status status = batch->writeBatchPtr->Iterate(&write_batch_extractor);
if (!status.ok()) {
ERROR("[migrate] Failed to parse write batch, Err: {}", status.ToString());
return {Status::NotOK};
}
// Get all constructed commands
auto resp_commands = write_batch_extractor.GetRESPCommands();
for (const auto &iter : *resp_commands) {
for (const auto &it : iter.second) {
*commands += it;
current_pipeline_size_++;
}
}
return Status::OK();
}
Status SlotMigrator::migrateIncrementData(std::unique_ptr<rocksdb::TransactionLogIterator> *iter, uint64_t end_seq) {
if (!(*iter) || !(*iter)->Valid()) {
ERROR("[migrate] WAL iterator is invalid");
return {Status::NotOK};
}
uint64_t next_seq = wal_begin_seq_ + 1;
std::string commands;
while (true) {
if (stop_migration_) {
ERROR("[migrate] Migration task end during migrating WAL data");
return {Status::NotOK};
}
auto batch = (*iter)->GetBatch();
if (batch.sequence != next_seq) {
ERROR("[migrate] WAL iterator is discrete, some seq might be lost, expected sequence: {}, but got sequence: {}",
next_seq, batch.sequence);
return {Status::NotOK};
}
// Generate commands by iterating write batch
auto s = generateCmdsFromBatch(&batch, &commands);
if (!s.IsOK()) {
ERROR("[migrate] Failed to generate commands from write batch");
return {Status::NotOK};
}
// Check whether command pipeline should be sent
s = sendCmdsPipelineIfNeed(&commands, false);
if (!s.IsOK()) {
ERROR("[migrate] Failed to send WAL commands pipeline");
return {Status::NotOK};
}
next_seq = batch.sequence + batch.writeBatchPtr->Count();
if (next_seq > end_seq) {
INFO("[migrate] Migrate incremental data an epoch OK, seq from {}, to {}", wal_begin_seq_, end_seq);
break;
}
(*iter)->Next();
if (!(*iter)->Valid()) {
ERROR("[migrate] WAL iterator is invalid, expected end seq: {}, next seq: {}", end_seq, next_seq);
return {Status::NotOK};
}
}
// Send the left data of this epoch
auto s = sendCmdsPipelineIfNeed(&commands, true);
if (!s.IsOK()) {
ERROR("[migrate] Failed to send WAL last commands in pipeline");
return {Status::NotOK};
}
return Status::OK();
}
Status SlotMigrator::syncWalBeforeForbiddingSlot() {
uint32_t count = 0;
while (count < kMaxLoopTimes) {
uint64_t latest_seq = storage_->GetDB()->GetLatestSequenceNumber();
uint64_t gap = latest_seq - wal_begin_seq_;
if (gap <= static_cast<uint64_t>(seq_gap_limit_)) {
INFO("[migrate] Incremental data sequence: {}, less than limit: {}, go to set forbidden slot", gap,
seq_gap_limit_);
break;
}
std::unique_ptr<rocksdb::TransactionLogIterator> iter = nullptr;
auto s = storage_->GetWALIter(wal_begin_seq_ + 1, &iter);
if (!s.IsOK()) {
ERROR("[migrate] Failed to generate WAL iterator before setting forbidden slot, Err: {}", s.Msg());
return {Status::NotOK};
}
// Iterate wal and migrate data
s = migrateIncrementData(&iter, latest_seq);
if (!s.IsOK()) {
ERROR("[migrate] Failed to migrate WAL data before setting forbidden slot");
return {Status::NotOK};
}
wal_begin_seq_ = latest_seq;
count++;
}
INFO("[migrate] Succeed to migrate incremental data before setting forbidden slot, end epoch: {}", count);
return Status::OK();
}
Status SlotMigrator::syncWalAfterForbiddingSlot() {
uint64_t latest_seq = storage_->GetDB()->GetLatestSequenceNumber();
// No incremental data
if (latest_seq <= wal_begin_seq_) return Status::OK();
// Get WAL iter
std::unique_ptr<rocksdb::TransactionLogIterator> iter = nullptr;
auto s = storage_->GetWALIter(wal_begin_seq_ + 1, &iter);
if (!s.IsOK()) {
ERROR("[migrate] Failed to generate WAL iterator after setting forbidden slot, Err: {}", s.Msg());
return {Status::NotOK};
}
// Send incremental data
s = migrateIncrementData(&iter, latest_seq);
if (!s.IsOK()) {
ERROR("[migrate] Failed to migrate WAL data after setting forbidden slot");
return {Status::NotOK};
}
return Status::OK();
}
void SlotMigrator::GetMigrationInfo(std::string *info) const {
info->clear();
if (!slot_range_.load().IsValid() && !forbidden_slot_range_.load().IsValid() &&
!migrate_failed_slot_range_.load().IsValid()) {
return;
}
SlotRange slot_range;
std::string task_state;
switch (migration_state_.load()) {
case MigrationState::kNone:
task_state = "none";
break;
case MigrationState::kStarted:
task_state = "start";
slot_range = slot_range_;
break;
case MigrationState::kSuccess:
task_state = "success";
slot_range = forbidden_slot_range_;
break;
case MigrationState::kFailed:
task_state = "fail";
slot_range = migrate_failed_slot_range_;
break;
default:
break;
}
*info = fmt::format("migrating_slot(s): {}\r\ndestination_node: {}\r\nmigrating_state: {}\r\n", slot_range.String(),
dst_node_, task_state);
}
void SlotMigrator::CancelSyncCtx() {
std::unique_lock<std::mutex> lock(blocking_mutex_);
blocking_context_ = nullptr;
}
void SlotMigrator::resumeSyncCtx(const Status &migrate_result) {
std::unique_lock<std::mutex> lock(blocking_mutex_);
if (blocking_context_) {
blocking_context_->Resume(migrate_result);
blocking_context_ = nullptr;
}
}
Status SlotMigrator::sendMigrationBatch(BatchSender *batch) {
// user may dynamically change some configs, apply it when send data
batch->SetMaxBytes(migrate_batch_size_bytes_);
batch->SetBytesPerSecond(migrate_batch_bytes_per_sec_);
return batch->Send();
}
Status SlotMigrator::sendSnapshotByRawKV() {
uint64_t start_ts = util::GetTimeStampMS();
auto slot_range = slot_range_.load();
INFO("[migrate] Migrating snapshot of slot(s) {} by raw key value", slot_range.String());
auto prefix = ComposeSlotKeyPrefix(namespace_, slot_range.start);
auto upper_bound = ComposeSlotKeyUpperBound(namespace_, slot_range.end);
rocksdb::ReadOptions read_options = storage_->DefaultScanOptions();
read_options.snapshot = slot_snapshot_;
rocksdb::Slice prefix_slice(prefix);
rocksdb::Slice upper_bound_slice(upper_bound);
read_options.iterate_lower_bound = &prefix_slice;
read_options.iterate_upper_bound = &upper_bound_slice;
auto no_txn_ctx = engine::Context::NoTransactionContext(storage_);
engine::DBIterator iter(no_txn_ctx, read_options);
BatchSender batch_sender(*dst_fd_, migrate_batch_size_bytes_, migrate_batch_bytes_per_sec_);
for (iter.Seek(prefix); iter.Valid(); iter.Next()) {
// Iteration is out of range
auto key_slot_id = ExtractSlotId(iter.Key());
if (!slot_range.Contains(key_slot_id)) {
break;
}
auto redis_type = iter.Type();
std::string log_data;
if (redis_type == RedisType::kRedisList) {
redis::WriteBatchLogData batch_log_data(redis_type, {std::to_string(RedisCommand::kRedisCmdRPush)});
log_data = batch_log_data.Encode();
} else {
redis::WriteBatchLogData batch_log_data(redis_type);
log_data = batch_log_data.Encode();
}
batch_sender.SetPrefixLogData(log_data);
GET_OR_RET(batch_sender.Put(storage_->GetCFHandle(ColumnFamilyID::Metadata), iter.Key(), iter.Value()));
if (batch_sender.IsFull()) {
GET_OR_RET(sendMigrationBatch(&batch_sender));
}
auto subkey_iter = iter.GetSubKeyIterator();
if (!subkey_iter) {
continue;
}
for (subkey_iter->Seek(); subkey_iter->Valid(); subkey_iter->Next()) {
GET_OR_RET(batch_sender.Put(subkey_iter->ColumnFamilyHandle(), subkey_iter->Key(), subkey_iter->Value()));
if (batch_sender.IsFull()) {
GET_OR_RET(sendMigrationBatch(&batch_sender));
}
if (redis_type == RedisType::kRedisZSet) {
InternalKey internal_key(subkey_iter->Key(), storage_->IsSlotIdEncoded());
auto score_key = subkey_iter->Value().ToString();
score_key.append(subkey_iter->UserKey().ToString());
auto score_key_bytes =
InternalKey(iter.Key(), score_key, internal_key.GetVersion(), storage_->IsSlotIdEncoded()).Encode();
GET_OR_RET(batch_sender.Put(storage_->GetCFHandle(ColumnFamilyID::SecondarySubkey), score_key_bytes, Slice()));
}
if (batch_sender.IsFull()) {
GET_OR_RET(sendMigrationBatch(&batch_sender));
}
}
}
GET_OR_RET(sendMigrationBatch(&batch_sender));
auto elapsed = util::GetTimeStampMS() - start_ts;
INFO(
"[migrate] Succeed to migrate snapshot range, slot(s): {}, elapsed: {} ms, sent: {} bytes, rate: {:.2f} kb/s, "
"batches: {}, entries: {}",
slot_range.String(), elapsed, batch_sender.GetSentBytes(), batch_sender.GetRate(start_ts),
batch_sender.GetSentBatchesNum(), batch_sender.GetEntriesNum());
return Status::OK();
}
Status SlotMigrator::syncWALByRawKV() {
uint64_t start_ts = util::GetTimeStampMS();
INFO("[migrate] Syncing WAL of slot(s) {} by raw key value", slot_range_.load().String());
BatchSender batch_sender(*dst_fd_, migrate_batch_size_bytes_, migrate_batch_bytes_per_sec_);
int epoch = 1;
uint64_t wal_incremental_seq = 0;
while (epoch <= kMaxLoopTimes) {
if (catchUpIncrementalWAL()) {
break;
}
wal_incremental_seq = storage_->GetDB()->GetLatestSequenceNumber();
auto s = migrateIncrementalDataByRawKV(wal_incremental_seq, &batch_sender);
if (!s.IsOK()) {
return {Status::NotOK, fmt::format("migrate incremental data failed, {}", s.Msg())};
}
INFO("[migrate] Migrated incremental data, epoch: {}, seq from {} to {}", epoch, wal_begin_seq_,
wal_incremental_seq);
wal_begin_seq_ = wal_incremental_seq;
epoch++;
}
setForbiddenSlotRange(slot_range_);
wal_incremental_seq = storage_->GetDB()->GetLatestSequenceNumber();
if (wal_incremental_seq > wal_begin_seq_) {
auto s = migrateIncrementalDataByRawKV(wal_incremental_seq, &batch_sender);
if (!s.IsOK()) {
return {Status::NotOK, fmt::format("migrate last incremental data failed, {}", s.Msg())};
}
INFO("[migrate] Migrated last incremental data after set forbidden slot, seq from {} to {}", wal_begin_seq_,
wal_incremental_seq);
}
auto elapsed = util::GetTimeStampMS() - start_ts;
INFO(
"[migrate] Succeed to migrate incremental data, slot(s): {}, elapsed: {} ms, "
"sent: {} bytes, rate: {:.2f} kb/s, batches: {}, entries: {}",
slot_range_.load().String(), elapsed, batch_sender.GetSentBytes(), batch_sender.GetRate(start_ts),
batch_sender.GetSentBatchesNum(), batch_sender.GetEntriesNum());
return Status::OK();
}
bool SlotMigrator::catchUpIncrementalWAL() {
uint64_t gap = storage_->GetDB()->GetLatestSequenceNumber() - wal_begin_seq_;
if (gap <= seq_gap_limit_) {
INFO("[migrate] Incremental data sequence gap: {}, less than limit: {}, set forbidden slot(s): {}", gap,
seq_gap_limit_, slot_range_.load().String());
return true;
}
return false;
}
Status SlotMigrator::migrateIncrementalDataByRawKV(uint64_t end_seq, BatchSender *batch_sender) {
engine::WALIterator wal_iter(storage_, slot_range_);
uint64_t start_seq = wal_begin_seq_ + 1;
for (wal_iter.Seek(start_seq); wal_iter.Valid(); wal_iter.Next()) {
if (wal_iter.NextSequenceNumber() > end_seq + 1) {
break;
}
auto item = wal_iter.Item();
switch (item.type) {
case engine::WALItem::Type::kTypeLogData: {
GET_OR_RET(batch_sender->PutLogData(item.key));
break;
}
case engine::WALItem::Type::kTypePut: {
if (item.column_family_id > kMaxColumnFamilyID) {
INFO("[migrate] Invalid put column family id: {}", item.column_family_id);
continue;
}
GET_OR_RET(batch_sender->Put(storage_->GetCFHandle(static_cast<ColumnFamilyID>(item.column_family_id)),
item.key, item.value));
break;
}
case engine::WALItem::Type::kTypeDelete: {
if (item.column_family_id > kMaxColumnFamilyID) {
INFO("[migrate] Invalid delete column family id: {}", item.column_family_id);
continue;
}
GET_OR_RET(
batch_sender->Delete(storage_->GetCFHandle(static_cast<ColumnFamilyID>(item.column_family_id)), item.key));
break;
}
case engine::WALItem::Type::kTypeDeleteRange: {
// Do nothing in DeleteRange due to it might cross multiple slots. It's only used in
// FLUSHDB/FLUSHALL commands for now and maybe we can disable them while migrating.
}
default:
break;
}
if (batch_sender->IsFull()) {
GET_OR_RET(sendMigrationBatch(batch_sender));
}
}
// send the remaining data
return sendMigrationBatch(batch_sender);
}