blob: 4fd2b77b75c7ba398df8cfed48dd417a1cba8276 [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 <gtest/gtest.h>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <memory>
#include "encoding.h"
#include "search/index_info.h"
#include "search/indexer.h"
#include "storage/compact_filter.h"
#include "storage/redis_metadata.h"
#include "storage/storage.h"
#include "time_util.h"
#include "types/redis_hash.h"
#include "types/redis_timeseries.h"
#include "types/redis_zset.h"
namespace {
class CompactHashFieldExpirationTest : public ::testing::Test {
public:
CompactHashFieldExpirationTest(const CompactHashFieldExpirationTest &) = delete;
CompactHashFieldExpirationTest &operator=(const CompactHashFieldExpirationTest &) = delete;
CompactHashFieldExpirationTest(CompactHashFieldExpirationTest &&) = delete;
CompactHashFieldExpirationTest &operator=(CompactHashFieldExpirationTest &&) = delete;
protected:
CompactHashFieldExpirationTest() {
const char *path = "compact_hash_field_expiration.conf";
unlink(path);
std::ofstream output_file(path, std::ios::out);
output_file << "hash-encoding-mode field-expiration\n";
output_file.close();
auto s = config_.Load(CLIOptions(path));
assert(s.IsOK());
config_.db_dir = "compactdb_hash_field_expiration";
config_.slot_id_encoded = false;
config_.rocks_db.compression = rocksdb::CompressionType::kNoCompression;
config_.rocks_db.write_buffer_size = 1;
config_.rocks_db.block_size = 100;
storage_ = std::make_unique<engine::Storage>(&config_);
s = storage_->Open();
assert(s.IsOK());
ctx_ = std::make_unique<engine::Context>(storage_.get());
db_ = std::make_unique<redis::Database>(storage_.get(), ns_);
hash_ = std::make_unique<redis::Hash>(storage_.get(), ns_);
}
~CompactHashFieldExpirationTest() override {
ctx_.reset();
db_.reset();
hash_.reset();
storage_.reset();
std::error_code ec;
std::filesystem::remove_all(config_.db_dir, ec);
unlink("compact_hash_field_expiration.conf");
}
HashMetadata hashMetadataOf(const std::string &key) {
HashMetadata metadata(false);
auto s = db_->GetMetadata(*ctx_, {kRedisHash}, db_->AppendNamespacePrefix(key), &metadata);
assert(s.ok());
return metadata;
}
std::string hashSubKey(const std::string &key, const std::string &field) {
HashMetadata metadata = hashMetadataOf(key);
return InternalKey(db_->AppendNamespacePrefix(key), field, metadata.version, storage_->IsSlotIdEncoded()).Encode();
}
rocksdb::Status getRawHashValue(const std::string &key, const std::string &field, std::string *value) {
return storage_->Get(*ctx_, ctx_->GetReadOptions(), hashSubKey(key, field), value);
}
rocksdb::Status getRawSubKeyValue(const std::string &sub_key, std::string *value) {
return storage_->Get(*ctx_, ctx_->GetReadOptions(), sub_key, value);
}
rocksdb::Status putRawHashValue(const std::string &key, const std::string &field, const std::string &value) {
auto batch = storage_->GetWriteBatchBase();
auto s = batch->Put(hashSubKey(key, field), value);
if (!s.ok()) return s;
return storage_->Write(*ctx_, storage_->DefaultWriteOptions(), batch->GetWriteBatch());
}
rocksdb::Status getRawMetadata(const std::string &key, std::string *value) {
return storage_->Get(*ctx_, ctx_->GetReadOptions(), storage_->GetCFHandle(ColumnFamilyID::Metadata),
db_->AppendNamespacePrefix(key), value);
}
void compactTwice() {
auto s = storage_->Compact(nullptr, nullptr, nullptr);
ASSERT_TRUE(s.ok()) << s.ToString();
s = storage_->Compact(nullptr, nullptr, nullptr);
ASSERT_TRUE(s.ok()) << s.ToString();
}
Config config_;
std::string ns_ = "test_compact_hfe";
std::unique_ptr<engine::Storage> storage_;
std::unique_ptr<engine::Context> ctx_;
std::unique_ptr<redis::Database> db_;
std::unique_ptr<redis::Hash> hash_;
};
} // namespace
TEST(Compact, Filter) {
Config config;
config.db_dir = "compactdb";
config.slot_id_encoded = false;
auto storage = std::make_unique<engine::Storage>(&config);
Status s = storage->Open();
assert(s.IsOK());
uint64_t ret = 0;
std::string ns = "test_compact";
auto hash = std::make_unique<redis::Hash>(storage.get(), ns);
std::string expired_hash_key = "expire_hash_key";
std::string live_hash_key = "live_hash_key";
engine::Context ctx(storage.get());
hash->Set(ctx, expired_hash_key, "f1", "v1", &ret);
hash->Set(ctx, expired_hash_key, "f2", "v2", &ret);
auto st = hash->Expire(ctx, expired_hash_key, 1); // expired
usleep(10000);
hash->Set(ctx, live_hash_key, "f1", "v1", &ret);
hash->Set(ctx, live_hash_key, "f2", "v2", &ret);
auto status = storage->Compact(nullptr, nullptr, nullptr);
assert(status.ok());
// Compact twice to workaround issue fixed by: https://github.com/facebook/rocksdb/pull/11468
status = storage->Compact(nullptr, nullptr, nullptr);
assert(status.ok());
rocksdb::DB *db = storage->GetDB();
rocksdb::ReadOptions read_options;
read_options.snapshot = db->GetSnapshot();
read_options.fill_cache = false;
auto new_iterator = [db, read_options, &storage](ColumnFamilyID column_family_id) {
return std::unique_ptr<rocksdb::Iterator>(db->NewIterator(read_options, storage->GetCFHandle(column_family_id)));
};
auto iter = new_iterator(ColumnFamilyID::Metadata);
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
auto [user_ns, user_key] = ExtractNamespaceKey(iter->key(), storage->IsSlotIdEncoded());
EXPECT_EQ(user_key.ToString(), live_hash_key);
}
iter = new_iterator(ColumnFamilyID::PrimarySubkey);
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
InternalKey ikey(iter->key(), storage->IsSlotIdEncoded());
EXPECT_EQ(ikey.GetKey().ToString(), live_hash_key);
}
auto zset = std::make_unique<redis::ZSet>(storage.get(), ns);
std::string expired_zset_key = "expire_zset_key";
std::vector<MemberScore> member_scores = {MemberScore{"z1", 1.1}, MemberScore{"z2", 0.4}};
zset->Add(ctx, expired_zset_key, ZAddFlags::Default(), &member_scores, &ret);
st = zset->Expire(ctx, expired_zset_key, 1); // expired
usleep(10000);
// Same as the above compact, need to compact twice here
status = storage->Compact(nullptr, nullptr, nullptr);
EXPECT_TRUE(status.ok());
status = storage->Compact(nullptr, nullptr, nullptr);
EXPECT_TRUE(status.ok());
iter = new_iterator(ColumnFamilyID::PrimarySubkey);
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
InternalKey ikey(iter->key(), storage->IsSlotIdEncoded());
EXPECT_EQ(ikey.GetKey().ToString(), live_hash_key);
}
iter = new_iterator(ColumnFamilyID::SecondarySubkey);
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
EXPECT_TRUE(false); // never reach here
}
Slice mk_with_ttl = "mk_with_ttl";
hash->Set(ctx, mk_with_ttl, "f1", "v1", &ret);
hash->Set(ctx, mk_with_ttl, "f2", "v2", &ret);
int retry = 2;
while (retry-- > 0) {
status = storage->Compact(nullptr, nullptr, nullptr);
ASSERT_TRUE(status.ok());
std::vector<FieldValue> fieldvalues;
auto get_res = hash->GetAll(ctx, mk_with_ttl, &fieldvalues);
auto s_expire = hash->Expire(ctx, mk_with_ttl, 1); // expired immediately..
if (retry == 1) {
ASSERT_TRUE(get_res.ok()); // not expired first time
ASSERT_TRUE(s_expire.ok());
} else {
ASSERT_TRUE(get_res.ok()); // expired but still return ok....
ASSERT_EQ(0, fieldvalues.size());
ASSERT_TRUE(s_expire.IsNotFound());
}
usleep(10000);
}
db->ReleaseSnapshot(read_options.snapshot);
std::error_code ec;
std::filesystem::remove_all(config.db_dir, ec);
if (ec) {
std::cout << "Encounter filesystem error: " << ec << std::endl;
}
}
TEST(Compact, KeepsLegacyHashSubkeyWithTTLLikePrefix) {
Config config;
config.db_dir = "compactdb_legacy_hash_ttl_like_prefix";
config.slot_id_encoded = false;
config.rocks_db.compression = rocksdb::CompressionType::kNoCompression;
config.rocks_db.write_buffer_size = 1;
config.rocks_db.block_size = 100;
auto storage = std::make_unique<engine::Storage>(&config);
Status s = storage->Open();
ASSERT_TRUE(s.IsOK()) << s.Msg();
std::string ns = "test_compact_legacy_hash";
engine::Context ctx(storage.get());
redis::Database db(storage.get(), ns);
redis::Hash hash(storage.get(), ns);
std::string key = "legacy_hash";
std::string field = "field";
std::string value;
PutFixed64(&value, 1);
value.append("legacy-value");
uint64_t ret = 0;
auto rs = hash.Set(ctx, key, field, value, &ret);
ASSERT_TRUE(rs.ok()) << rs.ToString();
HashMetadata metadata(false);
rs = db.GetMetadata(ctx, {kRedisHash}, db.AppendNamespacePrefix(key), &metadata);
ASSERT_TRUE(rs.ok()) << rs.ToString();
ASSERT_TRUE(metadata.IsLegacySubkeyEncoding());
std::string sub_key =
InternalKey(db.AppendNamespacePrefix(key), field, metadata.version, storage->IsSlotIdEncoded()).Encode();
engine::SubKeyFilter filter(storage.get());
EXPECT_EQ(filter.FilterBlobByKey(0, sub_key, nullptr, nullptr), rocksdb::CompactionFilter::Decision::kKeep);
auto status = storage->Compact(nullptr, nullptr, nullptr);
ASSERT_TRUE(status.ok()) << status.ToString();
status = storage->Compact(nullptr, nullptr, nullptr);
ASSERT_TRUE(status.ok()) << status.ToString();
std::string raw_value;
rs = storage->Get(ctx, ctx.GetReadOptions(), sub_key, &raw_value);
EXPECT_TRUE(rs.ok()) << rs.ToString();
EXPECT_EQ(raw_value, value);
storage.reset();
std::error_code ec;
std::filesystem::remove_all(config.db_dir, ec);
if (ec) {
std::cout << "Encounter filesystem error: " << ec << std::endl;
}
}
TEST(Compact, SearchFilter) {
Config config;
config.db_dir = "compactdb";
config.slot_id_encoded = false;
auto storage = std::make_unique<engine::Storage>(&config);
auto s = storage->Open();
assert(s.IsOK());
uint64_t ret = 0;
std::string ns = "test_compact_search";
auto hash = std::make_unique<redis::Hash>(storage.get(), ns);
redis::IndexMetadata hash_field_meta;
hash_field_meta.on_data_type = redis::IndexOnDataType::HASH;
auto hash_info = std::make_unique<kqir::IndexInfo>("hashtest", hash_field_meta, ns);
hash_info->Add(kqir::FieldInfo("f1", std::make_unique<redis::TagFieldMetadata>()));
hash_info->Add(kqir::FieldInfo("f2", std::make_unique<redis::NumericFieldMetadata>()));
redis::GlobalIndexer indexer(storage.get());
kqir::IndexMap map;
map.Insert(std::move(hash_info));
auto hash_updater = std::make_unique<redis::IndexUpdater>(map.at(ComposeNamespaceKey(ns, "hashtest", false)).get());
indexer.Add(std::move(hash_updater));
engine::Context ctx(storage.get());
std::string hash_key = "hash_key";
auto sr = indexer.Record(ctx, hash_key, ns);
ASSERT_EQ(sr.Msg(), Status::ok_msg);
auto record = *sr;
hash->Set(ctx, hash_key, "f1", "hello", &ret);
hash->Set(ctx, hash_key, "f2", "233", &ret);
auto su = indexer.Update(ctx, record);
ASSERT_TRUE(su);
auto tag_search_key = redis::SearchKey(ns, "hashtest", "f1").ConstructTagFieldData("hello", hash_key);
std::string search_value;
auto sg = storage->Get(ctx, rocksdb::ReadOptions(), storage->GetCFHandle(ColumnFamilyID::Search), tag_search_key,
&search_value);
ASSERT_TRUE(sg.ok());
auto num_search_key = redis::SearchKey(ns, "hashtest", "f2").ConstructNumericFieldData(233, hash_key);
sg = storage->Get(ctx, rocksdb::ReadOptions(), storage->GetCFHandle(ColumnFamilyID::Search), num_search_key,
&search_value);
ASSERT_TRUE(sg.ok());
auto st = hash->Expire(ctx, hash_key, 1);
ASSERT_TRUE(storage->Compact(nullptr, nullptr, nullptr).ok());
sg = storage->Get(ctx, rocksdb::ReadOptions(), storage->GetCFHandle(ColumnFamilyID::Search), tag_search_key,
&search_value);
ASSERT_TRUE(sg.IsNotFound());
sg = storage->Get(ctx, rocksdb::ReadOptions(), storage->GetCFHandle(ColumnFamilyID::Search), num_search_key,
&search_value);
ASSERT_TRUE(sg.IsNotFound());
std::error_code ec;
std::filesystem::remove_all(config.db_dir, ec);
if (ec) {
std::cout << "Encounter filesystem error: " << ec << std::endl;
}
}
TEST(Compact, IndexFilter) {
Config config;
config.db_dir = "compactdb";
config.slot_id_encoded = false;
auto storage = std::make_unique<engine::Storage>(&config);
auto s = storage->Open();
assert(s.IsOK());
std::string ns = "test_compact_index";
auto timeseries = std::make_unique<redis::TimeSeries>(storage.get(), ns);
engine::Context ctx(storage.get());
std::string ts_del_key = "ts_del_key";
std::string ts_expire_key = "ts_expire_key";
std::string ts_keep_key = "ts_keep_key";
auto create_option = redis::TSCreateOption();
create_option.labels.push_back({"flag", "temp"});
ASSERT_TRUE(timeseries->Create(ctx, ts_del_key, create_option).ok());
ASSERT_TRUE(timeseries->Create(ctx, ts_expire_key, create_option).ok());
ASSERT_TRUE(timeseries->Create(ctx, ts_keep_key, create_option).ok());
redis::TSMGetOption mget_option;
mget_option.filter.labels_equals["flag"].insert("temp");
std::vector<redis::TSMGetResult> mget_result;
ASSERT_TRUE(timeseries->MGet(ctx, mget_option, false, &mget_result).ok());
ASSERT_EQ(mget_result.size(), 3);
ASSERT_EQ(mget_result[0].name, ts_del_key);
ASSERT_EQ(mget_result[1].name, ts_expire_key);
ASSERT_EQ(mget_result[2].name, ts_keep_key);
std::string ns_del_key = ComposeNamespaceKey(ns, ts_del_key, false);
ASSERT_TRUE(
storage->Delete(ctx, storage->DefaultWriteOptions(), storage->GetCFHandle(ColumnFamilyID::Metadata), ns_del_key)
.ok());
ASSERT_TRUE(timeseries->Expire(ctx, ts_expire_key, 1).ok());
ASSERT_TRUE(storage->Compact(nullptr, nullptr, nullptr).ok());
ASSERT_TRUE(timeseries->MGet(ctx, mget_option, false, &mget_result).ok());
ASSERT_EQ(mget_result.size(), 1);
ASSERT_EQ(mget_result[0].name, ts_keep_key);
std::error_code ec;
std::filesystem::remove_all(config.db_dir, ec);
if (ec) {
std::cout << "Encounter filesystem error: " << ec << std::endl;
}
}
TEST(Compact, TSRetention) {
Config config;
config.db_dir = "compactdb_tsretention";
config.slot_id_encoded = false;
auto storage = std::make_unique<engine::Storage>(&config);
auto s = storage->Open();
assert(s.IsOK());
std::string ns = "test_compact_tsretention";
auto timeseries = std::make_unique<redis::TimeSeries>(storage.get(), ns);
engine::Context ctx(storage.get());
std::string ts_key = "ts_key";
redis::TSCreateOption create_option;
create_option.chunk_size = 3;
create_option.retention_time = 100;
ASSERT_TRUE(timeseries->Create(ctx, ts_key, create_option).ok());
rocksdb::DB *db = storage->GetDB();
rocksdb::ReadOptions read_options;
read_options.fill_cache = false;
auto get_all_chunks = [&]() {
auto iter = std::unique_ptr<rocksdb::Iterator>(
db->NewIterator(read_options, storage->GetCFHandle(ColumnFamilyID::PrimarySubkey)));
std::vector<uint64_t> chunk_ids;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
Slice slice(iter->key());
slice.remove_prefix(slice.size() - sizeof(uint64_t));
uint64_t chunk_id = 0;
GetFixed64(&slice, &chunk_id);
chunk_ids.push_back(chunk_id);
}
return chunk_ids;
};
// Add two chunk
std::vector<TSSample> samples = {{1, 1.0}, {2, 2.0}, {3, 3.0}, {4, 4.0}, {5, 5.0}, {10, 10.0}};
std::vector<TSChunk::AddResult> add_results;
ASSERT_TRUE(timeseries->MAdd(ctx, ts_key, samples, &add_results).ok());
// There should be two chunk key
auto chunk_ids = get_all_chunks();
ASSERT_EQ(chunk_ids.size(), 2);
ASSERT_EQ(chunk_ids[0], 1);
ASSERT_EQ(chunk_ids[1], 4);
// Add a sample to make last_timestamp = 110, then the first chunk is expired
samples = {{110, 110.0}};
ASSERT_TRUE(timeseries->MAdd(ctx, ts_key, samples, &add_results).ok());
ASSERT_TRUE(storage->Compact(nullptr, nullptr, nullptr).ok());
// Check the first chunk is deleted
chunk_ids = get_all_chunks();
ASSERT_EQ(chunk_ids.size(), 2);
ASSERT_EQ(chunk_ids[0], 4);
ASSERT_EQ(chunk_ids[1], 110);
// Check samples should be kept
redis::TSRangeOption range_option;
std::vector<TSSample> range_result;
ASSERT_TRUE(timeseries->Range(ctx, ts_key, range_option, &range_result).ok());
ASSERT_EQ(range_result.size(), 2);
ASSERT_EQ(range_result[0].ts, 10);
ASSERT_EQ(range_result[1].ts, 110);
std::error_code ec;
std::filesystem::remove_all(config.db_dir, ec);
if (ec) {
std::cout << "Encounter filesystem error: " << ec << std::endl;
}
}
TEST(Compact, TSDownstreamSubKey) {
Config config;
config.db_dir = "compactdb_tsdownstream";
config.slot_id_encoded = false;
auto storage = std::make_unique<engine::Storage>(&config);
auto s = storage->Open();
assert(s.IsOK());
std::string ns = "test_compact_tsdownstream";
auto timeseries = std::make_unique<redis::TimeSeries>(storage.get(), ns);
engine::Context ctx(storage.get());
rocksdb::DB *db = storage->GetDB();
rocksdb::ReadOptions read_options;
read_options.fill_cache = false;
auto get_all_ds_key = [&]() {
auto iter = std::unique_ptr<rocksdb::Iterator>(
db->NewIterator(read_options, storage->GetCFHandle(ColumnFamilyID::PrimarySubkey)));
std::vector<std::string> ds_keys;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
Slice slice(iter->key());
slice.remove_prefix(slice.size() - sizeof(uint64_t));
ds_keys.push_back(slice.ToString());
}
return ds_keys;
};
std::string ts_key = "ts_key";
std::string dst_key1 = "dst_key1";
std::string dst_key2 = "dst_key2";
redis::TSCreateOption create_option;
ASSERT_TRUE(timeseries->Create(ctx, ts_key, create_option).ok());
ASSERT_TRUE(timeseries->Create(ctx, dst_key1, create_option).ok());
ASSERT_TRUE(timeseries->Create(ctx, dst_key2, create_option).ok());
// Create two downstream rule
redis::TSAggregator agg;
agg.type = redis::TSAggregatorType::AVG;
agg.bucket_duration = 100;
auto rule_res = redis::TSCreateRuleResult::kOK;
ASSERT_TRUE(timeseries->CreateRule(ctx, ts_key, dst_key1, agg, &rule_res).ok());
ASSERT_TRUE(timeseries->CreateRule(ctx, ts_key, dst_key2, agg, &rule_res).ok());
auto ds_keys = get_all_ds_key();
ASSERT_EQ(ds_keys.size(), 2);
ASSERT_EQ(ds_keys[0], dst_key1);
ASSERT_EQ(ds_keys[1], dst_key2);
// Recreate the downstream key
ASSERT_TRUE(static_cast<redis::Database *>(timeseries.get())->Del(ctx, dst_key1).ok());
ASSERT_TRUE(timeseries->Create(ctx, dst_key1, create_option).ok());
ASSERT_TRUE(storage->Compact(nullptr, nullptr, nullptr).ok());
ds_keys = get_all_ds_key();
ASSERT_EQ(ds_keys.size(), 1);
ASSERT_EQ(ds_keys[0], dst_key2);
std::error_code ec;
std::filesystem::remove_all(config.db_dir, ec);
if (ec) {
std::cout << "Encounter filesystem error: " << ec << std::endl;
}
}