blob: e199052b69a2997d8c946c2ad39ee5d3898c81e5 [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.
*
*/
#pragma once
#include <event2/bufferevent.h>
#include <rocksdb/advanced_cache.h>
#include <rocksdb/db.h>
#include <rocksdb/options.h>
#include <rocksdb/table.h>
#include <rocksdb/utilities/backup_engine.h>
#include <rocksdb/utilities/write_batch_with_index.h>
#include <atomic>
#include <cinttypes>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <shared_mutex>
#include <string>
#include <utility>
#include <vector>
#include "common/port.h"
#include "config/config.h"
#include "lock_manager.h"
#include "observer_or_unique.h"
#include "rocksdb/write_batch.h"
#include "status.h"
#if defined(__sparc__) || defined(__arm__)
#define USE_ALIGNED_ACCESS
#endif
enum class StorageEngineType : uint16_t {
RocksDB,
};
inline constexpr StorageEngineType STORAGE_ENGINE_TYPE = StorageEngineType::KVROCKS_STORAGE_ENGINE;
const int kReplIdLength = 16;
enum DBOpenMode {
kDBOpenModeDefault,
kDBOpenModeForReadOnly,
kDBOpenModeAsSecondaryInstance,
};
enum class ColumnFamilyID : uint32_t {
PrimarySubkey = 0,
Metadata,
SecondarySubkey,
PubSub,
Propagate,
Stream,
Search,
Index,
};
constexpr uint32_t kMaxColumnFamilyID = static_cast<uint32_t>(ColumnFamilyID::Index);
namespace engine {
constexpr const char *kPropagateScriptCommand = "script";
constexpr const char *kLuaFuncSHAPrefix = "lua_f_";
constexpr const char *kLuaFuncLibPrefix = "lua_func_lib_";
constexpr const char *kLuaLibCodePrefix = "lua_lib_code_";
struct CompressionOption {
rocksdb::CompressionType type;
const std::string name;
const std::string val;
};
inline const std::vector<CompressionOption> CompressionOptions = {
{rocksdb::kNoCompression, "no", "kNoCompression"},
{rocksdb::kSnappyCompression, "snappy", "kSnappyCompression"},
{rocksdb::kZlibCompression, "zlib", "kZlibCompression"},
{rocksdb::kLZ4Compression, "lz4", "kLZ4Compression"},
{rocksdb::kZSTD, "zstd", "kZSTD"},
};
inline const std::vector<CompressionOption> WalCompressionOptions = {
{rocksdb::kNoCompression, "no", "kNoCompression"},
{rocksdb::kZSTD, "zstd", "kZSTD"},
};
struct CacheOption {
BlockCacheType type;
const std::string name;
const std::string val;
};
inline const std::vector<CacheOption> CacheOptions = {
{BlockCacheType::kCacheTypeLRU, "lru", "kCacheTypeLRU"},
{BlockCacheType::kCacheTypeHCC, "hcc", "kCacheTypeHCC"},
};
enum class StatType : uint_fast8_t {
CompactionCount,
FlushCount,
KeyspaceHits,
KeyspaceMisses,
};
struct DBStats {
alignas(CACHE_LINE_SIZE) std::atomic<uint_fast64_t> compaction_count = 0;
alignas(CACHE_LINE_SIZE) std::atomic<uint_fast64_t> flush_count = 0;
alignas(CACHE_LINE_SIZE) std::atomic<uint_fast64_t> keyspace_hits = 0;
alignas(CACHE_LINE_SIZE) std::atomic<uint_fast64_t> keyspace_misses = 0;
};
class ColumnFamilyConfig {
public:
ColumnFamilyConfig(ColumnFamilyID id, std::string_view name, bool is_minor)
: id_(id), name_(name), is_minor_(is_minor) {}
ColumnFamilyID Id() const { return id_; }
std::string_view Name() const { return name_; }
bool IsMinor() const { return is_minor_; }
private:
ColumnFamilyID id_;
std::string_view name_;
bool is_minor_;
};
constexpr const std::string_view kPrimarySubkeyColumnFamilyName = "default";
constexpr const std::string_view kMetadataColumnFamilyName = "metadata";
constexpr const std::string_view kSecondarySubkeyColumnFamilyName = "zset_score";
constexpr const std::string_view kPubSubColumnFamilyName = "pubsub";
constexpr const std::string_view kPropagateColumnFamilyName = "propagate";
constexpr const std::string_view kStreamColumnFamilyName = "stream";
constexpr const std::string_view kSearchColumnFamilyName = "search";
constexpr const std::string_view kIndexColumnFamilyName = "index";
class ColumnFamilyConfigs {
public:
/// DefaultSubkeyColumnFamily is the default column family in rocksdb.
/// In kvrocks, we use it to store the data if metadata is not enough.
static ColumnFamilyConfig PrimarySubkeyColumnFamily() {
return {ColumnFamilyID::PrimarySubkey, kPrimarySubkeyColumnFamilyName, /*is_minor=*/false};
}
/// MetadataColumnFamily stores the metadata of data-structures.
static ColumnFamilyConfig MetadataColumnFamily() {
return {ColumnFamilyID::Metadata, kMetadataColumnFamilyName, /*is_minor=*/false};
}
/// SecondarySubkeyColumnFamily stores the score of zset or other secondary subkey.
/// See https://kvrocks.apache.org/community/data-structure-on-rocksdb#zset for more details.
static ColumnFamilyConfig SecondarySubkeyColumnFamily() {
return {ColumnFamilyID::SecondarySubkey, kSecondarySubkeyColumnFamilyName,
/*is_minor=*/true};
}
/// PubSubColumnFamily stores the pubsub data.
static ColumnFamilyConfig PubSubColumnFamily() {
return {ColumnFamilyID::PubSub, kPubSubColumnFamilyName, /*is_minor=*/true};
}
static ColumnFamilyConfig PropagateColumnFamily() {
return {ColumnFamilyID::Propagate, kPropagateColumnFamilyName, /*is_minor=*/true};
}
static ColumnFamilyConfig StreamColumnFamily() {
return {ColumnFamilyID::Stream, kStreamColumnFamilyName, /*is_minor=*/true};
}
static ColumnFamilyConfig SearchColumnFamily() {
return {ColumnFamilyID::Search, kSearchColumnFamilyName, /*is_minor=*/true};
}
static ColumnFamilyConfig IndexColumnFamily() {
return {ColumnFamilyID::Index, kIndexColumnFamilyName, /*is_minor=*/true};
}
/// ListAllColumnFamilies returns all column families in kvrocks.
static const std::vector<ColumnFamilyConfig> &ListAllColumnFamilies() { return AllCfs; }
static const std::vector<ColumnFamilyConfig> &ListColumnFamiliesWithoutDefault() { return AllCfsWithoutDefault; }
static const ColumnFamilyConfig &GetColumnFamily(ColumnFamilyID id) { return AllCfs[static_cast<size_t>(id)]; }
private:
// Caution: don't change the order of column family, or the handle will be mismatched
inline const static std::vector<ColumnFamilyConfig> AllCfs = {
PrimarySubkeyColumnFamily(), MetadataColumnFamily(), SecondarySubkeyColumnFamily(), PubSubColumnFamily(),
PropagateColumnFamily(), StreamColumnFamily(), SearchColumnFamily(), IndexColumnFamily(),
};
inline const static std::vector<ColumnFamilyConfig> AllCfsWithoutDefault = {
MetadataColumnFamily(), SecondarySubkeyColumnFamily(), PubSubColumnFamily(), PropagateColumnFamily(),
StreamColumnFamily(), SearchColumnFamily(), IndexColumnFamily(),
};
};
struct Context;
class Storage {
public:
explicit Storage(Config *config);
~Storage();
void SetWriteOptions(const Config::RocksDB::WriteOptions &config);
Status Open(DBOpenMode mode = kDBOpenModeDefault);
void CloseDB();
void TrySkipBlockCacheDeallocationOnClose();
bool IsEmptyDB();
void EmptyDB();
rocksdb::BlockBasedTableOptions InitTableOptions();
void SetBlobDB(rocksdb::ColumnFamilyOptions *cf_options);
rocksdb::Options InitRocksDBOptions();
Status SetOptionForAllColumnFamilies(const std::string &key, const std::string &value);
Status SetOptionForAllColumnFamilies(const std::unordered_map<std::string, std::string> &options_map);
Status SetDBOption(const std::string &key, const std::string &value);
Status CreateColumnFamilies(const rocksdb::Options &options);
// The sequence_number will be pointed to the value of the sequence number in range of DB,
// but can't promise it's the latest sequence number. So you must check it by yourself before
// using it.
Status CreateBackup(uint64_t *sequence_number = nullptr);
void DestroyBackup();
Status RestoreFromBackup();
Status RestoreFromCheckpoint();
Status GetWALIter(rocksdb::SequenceNumber seq, std::unique_ptr<rocksdb::TransactionLogIterator> *iter);
Status ReplicaApplyWriteBatch(rocksdb::WriteBatch *batch, const rocksdb::WriteOptions &options);
Status ApplyWriteBatch(const rocksdb::WriteOptions &options, std::string &&raw_batch);
rocksdb::SequenceNumber LatestSeqNumber();
Status SyncWAL();
[[nodiscard]] rocksdb::Status Get(engine::Context &ctx, const rocksdb::ReadOptions &options,
const rocksdb::Slice &key, std::string *value);
[[nodiscard]] rocksdb::Status Get(engine::Context &ctx, const rocksdb::ReadOptions &options,
rocksdb::ColumnFamilyHandle *column_family, const rocksdb::Slice &key,
std::string *value);
[[nodiscard]] rocksdb::Status Get(engine::Context &ctx, const rocksdb::ReadOptions &options,
const rocksdb::Slice &key, rocksdb::PinnableSlice *value);
[[nodiscard]] rocksdb::Status Get(engine::Context &ctx, const rocksdb::ReadOptions &options,
rocksdb::ColumnFamilyHandle *column_family, const rocksdb::Slice &key,
rocksdb::PinnableSlice *value);
void MultiGet(engine::Context &ctx, const rocksdb::ReadOptions &options, rocksdb::ColumnFamilyHandle *column_family,
size_t num_keys, const rocksdb::Slice *keys, rocksdb::PinnableSlice *values, rocksdb::Status *statuses);
rocksdb::Iterator *NewIterator(engine::Context &ctx, const rocksdb::ReadOptions &options,
rocksdb::ColumnFamilyHandle *column_family);
rocksdb::Iterator *NewIterator(engine::Context &ctx, const rocksdb::ReadOptions &options);
const rocksdb::WriteOptions &DefaultWriteOptions() const { return default_write_opts_; }
rocksdb::ReadOptions DefaultScanOptions() const;
rocksdb::ReadOptions DefaultMultiGetOptions() const;
[[nodiscard]] rocksdb::Status Write(engine::Context &ctx, const rocksdb::WriteOptions &options,
rocksdb::WriteBatch *updates);
[[nodiscard]] rocksdb::Status Delete(engine::Context &ctx, const rocksdb::WriteOptions &options,
rocksdb::ColumnFamilyHandle *cf_handle, const rocksdb::Slice &key);
[[nodiscard]] rocksdb::Status DeleteRange(engine::Context &ctx, const rocksdb::WriteOptions &options,
rocksdb::ColumnFamilyHandle *cf_handle, Slice begin, Slice end);
[[nodiscard]] rocksdb::Status DeleteRange(engine::Context &ctx, Slice begin, Slice end);
[[nodiscard]] rocksdb::Status FlushScripts(engine::Context &ctx, const rocksdb::WriteOptions &options,
rocksdb::ColumnFamilyHandle *cf_handle);
bool WALHasNewData(rocksdb::SequenceNumber seq) { return seq <= LatestSeqNumber(); }
Status InWALBoundary(rocksdb::SequenceNumber seq);
Status WriteToPropagateCF(engine::Context &ctx, const std::string &key, const std::string &value);
[[nodiscard]] rocksdb::Status Compact(rocksdb::ColumnFamilyHandle *cf, const rocksdb::Slice *begin,
const rocksdb::Slice *end);
[[nodiscard]] rocksdb::Status FlushMemTable(rocksdb::ColumnFamilyHandle *cf_handle,
const rocksdb::FlushOptions &options);
[[nodiscard]] StatusOr<int> IngestSST(const std::string &folder,
const rocksdb::IngestExternalFileOptions &ingest_options);
void FlushBlockCache();
rocksdb::DB *GetDB();
bool IsClosing() const { return db_closing_; }
std::string GetName() const { return config_->db_name; }
/// Get the column family handle by the column family id.
rocksdb::ColumnFamilyHandle *GetCFHandle(ColumnFamilyID id);
std::vector<rocksdb::ColumnFamilyHandle *> *GetCFHandles() { return &cf_handles_; }
LockManager *GetLockManager() { return &lock_mgr_; }
void PurgeOldBackups(uint32_t num_backups_to_keep, uint32_t backup_max_keep_hours);
uint64_t GetTotalSize(const std::string &ns = kDefaultNamespace);
void SetSstFileDeleteRateBytesPerSecond(int64_t delete_rate);
void CheckDBSizeLimit();
bool ReachedDBSizeLimit() { return db_size_limit_reached_; }
void SetDBSizeLimit(bool limit) { db_size_limit_reached_ = limit; }
void SetIORateLimit(int64_t max_io_mb);
std::shared_lock<std::shared_mutex> ReadLockGuard();
std::unique_lock<std::shared_mutex> WriteLockGuard();
bool IsSlotIdEncoded() const { return config_->slot_id_encoded; }
Config *GetConfig() const { return config_; }
const DBStats *GetDBStats() const { return db_stats_.get(); }
void RecordStat(StatType type, uint64_t v);
Status BeginTxn();
Status CommitTxn();
ObserverOrUniquePtr<rocksdb::WriteBatchBase> GetWriteBatchBase();
Storage(const Storage &) = delete;
Storage &operator=(const Storage &) = delete;
int GetWriteBatchMaxBytes() const { return config_->rocks_db.write_options.write_batch_max_bytes; }
// Full replication data files manager
class ReplDataManager {
public:
static Status ValidateReplFileName(const std::string &repl_file);
// Master side
static Status GetFullReplDataInfo(Storage *storage, std::string *files);
static int OpenDataFile(Storage *storage, const std::string &rel_file, uint64_t *file_size);
static Status CleanInvalidFiles(Storage *storage, const std::string &dir, std::vector<std::string> valid_files);
struct CheckpointInfo {
// System clock time when the checkpoint was created.
std::atomic<int64_t> create_time_secs = 0;
// System clock time when the checkpoint was last accessed.
std::atomic<int64_t> access_time_secs = 0;
uint64_t latest_seq = 0;
};
// Slave side
struct MetaInfo {
int64_t timestamp;
rocksdb::SequenceNumber seq;
std::string meta_data;
// [[filename, checksum]...]
std::vector<std::pair<std::string, uint32_t>> files;
};
static Status ParseMetaAndSave(Storage *storage, rocksdb::BackupID meta_id, evbuffer *evbuf,
Storage::ReplDataManager::MetaInfo *meta);
static std::unique_ptr<rocksdb::WritableFile> NewTmpFile(Storage *storage, const std::string &dir,
const std::string &repl_file);
static Status SwapTmpFile(Storage *storage, const std::string &dir, const std::string &repl_file);
static bool FileExists(Storage *storage, const std::string &dir, const std::string &repl_file, uint32_t crc);
};
bool ExistCheckpoint();
bool ExistSyncCheckpoint();
int64_t GetCheckpointCreateTimeSecs() const { return checkpoint_info_.create_time_secs; }
void SetCheckpointAccessTimeSecs(int64_t t) { checkpoint_info_.access_time_secs = t; }
int64_t GetCheckpointAccessTimeSecs() const { return checkpoint_info_.access_time_secs; }
void SetDBInRetryableIOError(bool yes_or_no) { db_in_retryable_io_error_ = yes_or_no; }
bool IsDBInRetryableIOError() const { return db_in_retryable_io_error_; }
/// Redis PSYNC relies on a Unique Replication Sequence Id when use-rsid-psync
/// enabled.
/// ShiftReplId would generate an Id and write it to propagate cf.
Status ShiftReplId(engine::Context &ctx);
std::string GetReplIdFromWalBySeq(rocksdb::SequenceNumber seq);
std::string GetReplIdFromDbEngine();
private:
std::unique_ptr<rocksdb::DB> db_ = nullptr;
std::string replid_;
// The system clock time when the backup was created.
int64_t backup_creating_time_secs_;
std::unique_ptr<rocksdb::BackupEngine> backup_ = nullptr;
rocksdb::Env *env_;
std::shared_ptr<rocksdb::SstFileManager> sst_file_manager_;
std::shared_ptr<rocksdb::RateLimiter> rate_limiter_;
ReplDataManager::CheckpointInfo checkpoint_info_;
std::mutex checkpoint_mu_;
Config *config_ = nullptr;
std::vector<rocksdb::ColumnFamilyHandle *> cf_handles_;
LockManager lock_mgr_;
std::atomic<bool> db_size_limit_reached_{false};
std::unique_ptr<DBStats> db_stats_;
std::shared_mutex db_rw_lock_;
bool db_closing_ = true;
std::atomic<bool> db_in_retryable_io_error_{false};
// is_txn_mode_ is used to determine whether the current Storage is in transactional mode,
// .i.e, in "EXEC" command(CommandExec).
std::atomic<bool> is_txn_mode_ = false;
// txn_write_batch_ is used as the global write batch for the transaction mode,
// all writes will be grouped in this write batch when entering the transaction mode,
// then write it at once when committing.
//
// Notice: the reason why we can use the global transaction? because the EXEC is an exclusive
// command, so it won't have multi transactions to be executed at the same time.
std::unique_ptr<rocksdb::WriteBatchWithIndex> txn_write_batch_;
rocksdb::WriteOptions default_write_opts_;
// rocksdb used global block cache
std::shared_ptr<rocksdb::Cache> shared_block_cache_;
rocksdb::Status writeToDB(engine::Context &ctx, const rocksdb::WriteOptions &options, rocksdb::WriteBatch *updates);
void recordKeyspaceStat(const rocksdb::ColumnFamilyHandle *column_family, const rocksdb::Status &s);
Status applyWriteBatch(const rocksdb::WriteOptions &options, rocksdb::WriteBatch *batch);
rocksdb::Status ingestSST(rocksdb::ColumnFamilyHandle *cf_handle, const rocksdb::IngestExternalFileOptions &options,
const std::vector<std::string> &sst_file_names);
};
/// Context passes fixed snapshot and batch between APIs
///
/// Limitations: Performing a large number of writes or apply operations like DeleteRange
/// on the same Context may reduce performance.
/// Please choose to use the same Context or create a new Context based on the actual
/// situation.
///
/// Context does not provide thread safety guarantees and is generally only passed as a parameter between APIs.
struct Context {
engine::Storage *storage = nullptr;
/// batch can be nullptr if
/// 1. The Context is not in transactional mode.
/// 2. The Context is in transactional mode, but no write operation is performed.
std::unique_ptr<rocksdb::WriteBatchWithIndex> batch = nullptr;
/// txn_context_enabled is used to determine whether the current Context is in transactional mode,
/// if it is not transactional mode, then Context is equivalent to a Storage.
/// If the configuration of txn-context-enabled is no, it is false.
bool txn_context_enabled = true;
/// NoTransactionContext returns a Context with a is_txn_mode of false
static Context NoTransactionContext(engine::Storage *storage) { return Context(storage, /*txn_mode=*/false); }
/// GetReadOptions returns a default ReadOptions, and if txn_context_enabled = true,
/// then its snapshot is specified by the Context.
/// Otherwise it is the same as Storage::DefaultReadOptions().
[[nodiscard]] rocksdb::ReadOptions GetReadOptions();
/// DefaultScanOptions returns a DefaultScanOptions, and if txn_context_enabled = true,
/// then its snapshot is specified by the Context.
/// Otherwise it is the same as Storage::DefaultScanOptions().
[[nodiscard]] rocksdb::ReadOptions DefaultScanOptions();
/// DefaultMultiGetOptions returns a DefaultMultiGetOptions, and if txn_context_enabled = true,
/// then its snapshot is specified by the Context.
/// Otherwise it is the same as Storage::DefaultMultiGetOptions
[[nodiscard]] rocksdb::ReadOptions DefaultMultiGetOptions();
void RefreshLatestSnapshot();
/// TODO: Change it to defer getting the context, and the snapshot is pinned after the first read operation
explicit Context(engine::Storage *storage)
: storage(storage), txn_context_enabled(storage->GetConfig()->txn_context_enabled) {}
~Context() {
// A moved-from object doesn't have `storage`.
if (storage) {
if (snapshot_ && storage->GetDB()) {
storage->GetDB()->ReleaseSnapshot(snapshot_);
}
}
}
Context(const Context &) = delete;
Context &operator=(const Context &) = delete;
Context &operator=(Context &&ctx) noexcept {
if (this != &ctx) {
storage = ctx.storage;
snapshot_ = ctx.snapshot_;
batch = std::move(ctx.batch);
ctx.storage = nullptr;
ctx.snapshot_ = nullptr;
}
return *this;
}
Context(Context &&ctx) noexcept : storage(ctx.storage), batch(std::move(ctx.batch)), snapshot_(ctx.snapshot_) {
ctx.storage = nullptr;
ctx.snapshot_ = nullptr;
}
// GetSnapshot will create a snapshot first if it doesn't exist,
// and it's not a thread-safe operation.
const rocksdb::Snapshot *GetSnapshot() {
if (snapshot_ == nullptr) {
// Should not acquire a snapshot_ on a moved-from object.
CHECK(storage != nullptr);
snapshot_ = storage->GetDB()->GetSnapshot(); // NOLINT
}
return snapshot_;
}
private:
/// It is only used by NonTransactionContext
explicit Context(engine::Storage *storage, bool txn_mode) : storage(storage), txn_context_enabled(txn_mode) {}
/// If is_txn_mode is true, snapshot should be specified instead of nullptr when used,
/// and should be consistent with snapshot in ReadOptions to avoid ambiguity.
/// Normally it will be fixed to the latest Snapshot when the Context is constructed.
/// If is_txn_mode is false, the snapshot is nullptr.
const rocksdb::Snapshot *snapshot_ = nullptr;
};
} // namespace engine