From 9b2ee7e4b2f9ebcaf49692157c99077e9de49cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=92=E4=BC=91=E6=96=AF=7ETheseus?= Date: Tue, 30 Apr 2024 12:17:20 +0800 Subject: [PATCH 01/15] Use the QUICKLIST encoding to dump the list payload to compatible with Redis >= 4 (#2277) --- src/storage/rdb.cc | 41 ++++++++++++--- src/storage/rdb.h | 1 + src/storage/rdb_ziplist.cc | 81 ++++++++++++++++++++++++++--- src/storage/rdb_ziplist.h | 12 +++++ tests/gocase/unit/dump/dump_test.go | 11 ++++ 5 files changed, 132 insertions(+), 14 deletions(-) diff --git a/src/storage/rdb.cc b/src/storage/rdb.cc index 8c5f6f11976..79c0b8c89c4 100644 --- a/src/storage/rdb.cc +++ b/src/storage/rdb.cc @@ -730,7 +730,7 @@ Status RDB::SaveObjectType(const RedisType type) { } else if (type == kRedisHash) { robj_type = RDBTypeHash; } else if (type == kRedisList) { - robj_type = RDBTypeListQuickList2; + robj_type = RDBTypeListQuickList; } else if (type == kRedisSet) { robj_type = RDBTypeSet; } else if (type == kRedisZSet) { @@ -892,12 +892,7 @@ Status RDB::SaveListObject(const std::vector &elems) { } for (const auto &elem : elems) { - status = RdbSaveLen(1 /*plain container mode */); - if (!status.IsOK()) { - return {Status::RedisExecErr, status.Msg()}; - } - - status = SaveStringObject(elem); + auto status = rdbSaveZipListObject(elem); if (!status.IsOK()) { return {Status::RedisExecErr, status.Msg()}; } @@ -1005,3 +1000,35 @@ Status RDB::rdbSaveBinaryDoubleValue(double val) { memrev64ifbe(&val); return stream_->Write((const char *)(&val), sizeof(val)); } + +Status RDB::rdbSaveZipListObject(const std::string &elem) { + // calc total ziplist size + uint prevlen = 0; + const size_t ziplist_size = zlHeaderSize + zlEndSize + elem.length() + + ZipList::ZipStorePrevEntryLength(nullptr, 0, prevlen) + + ZipList::ZipStoreEntryEncoding(nullptr, 0, elem.length()); + auto zl_string = std::string(ziplist_size, '\0'); + auto zl_ptr = reinterpret_cast(&zl_string[0]); + + // set ziplist header + ZipList::SetZipListBytes(zl_ptr, ziplist_size, (static_cast(ziplist_size))); + ZipList::SetZipListTailOffset(zl_ptr, ziplist_size, intrev32ifbe(zlHeaderSize)); + + // set ziplist entry + auto pos = ZipList::GetZipListEntryHead(zl_ptr, ziplist_size); + pos += ZipList::ZipStorePrevEntryLength(pos, ziplist_size, prevlen); + pos += ZipList::ZipStoreEntryEncoding(pos, ziplist_size, elem.length()); + assert(pos + elem.length() <= zl_ptr + ziplist_size); + memcpy(pos, elem.c_str(), elem.length()); + + // set ziplist end + ZipList::SetZipListLength(zl_ptr, ziplist_size, 1); + zl_ptr[ziplist_size - 1] = zlEnd; + + auto status = SaveStringObject(zl_string); + if (!status.IsOK()) { + return {Status::RedisExecErr, status.Msg()}; + } + + return Status::OK(); +} diff --git a/src/storage/rdb.h b/src/storage/rdb.h index 5d7f78f3740..3a08df84a40 100644 --- a/src/storage/rdb.h +++ b/src/storage/rdb.h @@ -153,4 +153,5 @@ class RDB { static bool isEmptyRedisObject(const RedisObjValue &value); static int rdbEncodeInteger(long long value, unsigned char *enc); Status rdbSaveBinaryDoubleValue(double val); + Status rdbSaveZipListObject(const std::string &elem); }; diff --git a/src/storage/rdb_ziplist.cc b/src/storage/rdb_ziplist.cc index 772226eaa47..98f764b147e 100644 --- a/src/storage/rdb_ziplist.cc +++ b/src/storage/rdb_ziplist.cc @@ -20,11 +20,9 @@ #include "rdb_ziplist.h" -#include "vendor/endianconv.h" +#include -constexpr const int zlHeaderSize = 10; -constexpr const uint8_t ZipListBigLen = 0xFE; -constexpr const uint8_t zlEnd = 0xFF; +#include "vendor/endianconv.h" constexpr const uint8_t ZIP_STR_MASK = 0xC0; constexpr const uint8_t ZIP_STR_06B = (0 << 6); @@ -52,7 +50,7 @@ StatusOr ZipList::Next() { std::string value; if ((encoding) < ZIP_STR_MASK) { // For integer type, needs to convert to uint8_t* to avoid signed extension - auto data = reinterpret_cast(input_.data()); + auto data = reinterpret_cast(input_.data()); if ((encoding) == ZIP_STR_06B) { len_bytes = 1; len = data[pos_] & 0x3F; @@ -91,7 +89,7 @@ StatusOr ZipList::Next() { } else if ((encoding) == ZIP_INT_24B) { GET_OR_RET(peekOK(3)); int32_t i32 = 0; - memcpy(reinterpret_cast(&i32) + 1, input_.data() + pos_, sizeof(int32_t) - 1); + memcpy(reinterpret_cast(&i32) + 1, input_.data() + pos_, sizeof(int32_t) - 1); memrev32ifbe(&i32); i32 >>= 8; setPreEntryLen(4); // 3byte for encoding and 1byte for the prev entry length @@ -126,7 +124,7 @@ StatusOr ZipList::Next() { StatusOr> ZipList::Entries() { GET_OR_RET(peekOK(zlHeaderSize)); // ignore 8 bytes of total bytes and tail of zip list - auto zl_len = intrev16ifbe(*reinterpret_cast(input_.data() + 8)); + auto zl_len = intrev16ifbe(*reinterpret_cast(input_.data() + 8)); pos_ += zlHeaderSize; std::vector entries; @@ -152,3 +150,72 @@ Status ZipList::peekOK(size_t n) { } uint32_t ZipList::getEncodedLengthSize(uint32_t len) { return len < ZipListBigLen ? 1 : 5; } + +uint32_t ZipList::ZipStorePrevEntryLengthLarge(unsigned char *p, size_t zl_size, unsigned int len) { + uint32_t u32 = 0; + if (p != nullptr) { + p[0] = ZipListBigLen; + u32 = len; + assert(zl_size >= 1 + sizeof(uint32_t) + zlHeaderSize); + memcpy(p + 1, &u32, sizeof(u32)); + memrev32ifbe(p + 1); + } + return 1 + sizeof(uint32_t); +} + +uint32_t ZipList::ZipStorePrevEntryLength(unsigned char *p, size_t zl_size, unsigned int len) { + if (p == nullptr) { + return (len < ZipListBigLen) ? 1 : sizeof(uint32_t) + 1; + } + if (len < ZipListBigLen) { + p[0] = len; + return 1; + } + return ZipStorePrevEntryLengthLarge(p, zl_size, len); +} + +uint32_t ZipList::ZipStoreEntryEncoding(unsigned char *p, size_t zl_size, unsigned int rawlen) { + unsigned char len = 1, buf[5]; + + /* Although encoding is given it may not be set for strings, + * so we determine it here using the raw length. */ + if (rawlen <= 0x3f) { + if (!p) return len; + buf[0] = ZIP_STR_06B | rawlen; + } else if (rawlen <= 0x3fff) { + len += 1; + if (!p) return len; + buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f); + buf[1] = rawlen & 0xff; + } else { + len += 4; + if (!p) return len; + buf[0] = ZIP_STR_32B; + buf[1] = (rawlen >> 24) & 0xff; + buf[2] = (rawlen >> 16) & 0xff; + buf[3] = (rawlen >> 8) & 0xff; + buf[4] = rawlen & 0xff; + } + assert(zl_size >= zlHeaderSize + len); + /* Store this length at p. */ + memcpy(p, buf, len); + return len; +} + +void ZipList::SetZipListBytes(unsigned char *zl, size_t zl_size, uint32_t value) { + assert(zl_size >= sizeof(uint32_t)); + memcpy(zl, &value, sizeof(uint32_t)); +} +void ZipList::SetZipListTailOffset(unsigned char *zl, size_t zl_size, uint32_t value) { + assert(zl_size >= sizeof(uint32_t) * 2); + memcpy(zl + sizeof(uint32_t), &value, sizeof(uint32_t)); +} +void ZipList::SetZipListLength(unsigned char *zl, size_t zl_size, uint16_t value) { + assert(zl_size >= sizeof(uint32_t) * 2 + sizeof(uint16_t)); + memcpy(zl + sizeof(uint32_t) * 2, &value, sizeof(uint16_t)); +} + +unsigned char *ZipList::GetZipListEntryHead(unsigned char *zl, size_t zl_size) { + assert(zl_size >= zlHeaderSize); + return ((zl) + zlHeaderSize); +} diff --git a/src/storage/rdb_ziplist.h b/src/storage/rdb_ziplist.h index e9d05fde716..8f0d99c6693 100644 --- a/src/storage/rdb_ziplist.h +++ b/src/storage/rdb_ziplist.h @@ -25,6 +25,11 @@ #include "common/status.h" +constexpr const int zlHeaderSize = 10; +constexpr const int zlEndSize = 1; +constexpr const uint8_t ZipListBigLen = 0xFE; +constexpr const uint8_t zlEnd = 0xFF; + class ZipList { public: explicit ZipList(std::string_view input) : input_(input){}; @@ -32,6 +37,13 @@ class ZipList { StatusOr Next(); StatusOr> Entries(); + static uint32_t ZipStorePrevEntryLengthLarge(unsigned char *p, size_t zl_size, unsigned int len); + static uint32_t ZipStorePrevEntryLength(unsigned char *p, size_t zl_size, unsigned int len); + static uint32_t ZipStoreEntryEncoding(unsigned char *p, size_t zl_size, unsigned int rawlen); + static void SetZipListBytes(unsigned char *zl, size_t zl_size, uint32_t value); + static void SetZipListTailOffset(unsigned char *zl, size_t zl_size, uint32_t value); + static void SetZipListLength(unsigned char *zl, size_t zl_size, uint16_t value); + static unsigned char *GetZipListEntryHead(unsigned char *zl, size_t zl_size); private: std::string_view input_; diff --git a/tests/gocase/unit/dump/dump_test.go b/tests/gocase/unit/dump/dump_test.go index faeee9a9536..bca9300d1ea 100644 --- a/tests/gocase/unit/dump/dump_test.go +++ b/tests/gocase/unit/dump/dump_test.go @@ -113,10 +113,21 @@ func TestDump_List(t *testing.T) { require.NoError(t, rdb.RPush(ctx, key, elements).Err()) serialized, err := rdb.Dump(ctx, key).Result() require.NoError(t, err) + require.Equal(t, "\x0e\x03\x15\x15\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\bkvrocks1\xff\x15\x15\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\bkvrocks2\xff\x15\x15\x00\x00\x00\n\x00\x00\x00\x01\x00\x00\bkvrocks3\xff\x06\x00u\xc7\x19h\x1da\xd0\xd8", serialized) restoredKey := fmt.Sprintf("restore_%s", key) require.NoError(t, rdb.RestoreReplace(ctx, restoredKey, 0, serialized).Err()) require.EqualValues(t, elements, rdb.LRange(ctx, restoredKey, 0, -1).Val()) + + //test special case + elements = []string{"A", " ", "", util.RandString(0, 4000, util.Alpha)} + require.NoError(t, rdb.Del(ctx, key).Err()) + require.NoError(t, rdb.RPush(ctx, key, elements).Err()) + serialized, err = rdb.Dump(ctx, key).Result() + require.NoError(t, err) + + require.NoError(t, rdb.RestoreReplace(ctx, restoredKey, 0, serialized).Err()) + require.EqualValues(t, elements, rdb.LRange(ctx, restoredKey, 0, -1).Val()) } func TestDump_Set(t *testing.T) { From a6d8386ed407f28af4ff8d91fcb358dfd652ccd7 Mon Sep 17 00:00:00 2001 From: lizhenglei <127465317+jackyyyyyssss@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:52:50 +0800 Subject: [PATCH 02/15] Fix JSON.STRLEN not_exists return null (#2285) Co-authored-by: 80597928 <673421862@qq.com> --- src/commands/cmd_json.cc | 5 +++++ tests/gocase/unit/type/json/json_test.go | 1 + 2 files changed, 6 insertions(+) diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc index e29bad83855..43421399e67 100644 --- a/src/commands/cmd_json.cc +++ b/src/commands/cmd_json.cc @@ -559,6 +559,11 @@ class CommandJsonStrLen : public Commander { Optionals results; auto s = json.StrLen(args_[1], path, &results); + if (s.IsNotFound()) { + *output = conn->NilString(); + return Status::OK(); + } + if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; *output = OptionalsToString(conn, results); diff --git a/tests/gocase/unit/type/json/json_test.go b/tests/gocase/unit/type/json/json_test.go index 51785d29844..e30635d4eb6 100644 --- a/tests/gocase/unit/type/json/json_test.go +++ b/tests/gocase/unit/type/json/json_test.go @@ -180,6 +180,7 @@ func TestJson(t *testing.T) { result2 = append(result2, int64(3), int64(5), interface{}(nil)) require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err()) require.Equal(t, rdb.Do(ctx, "JSON.STRLEN", "a", "$..a").Val(), result2) + require.ErrorIs(t, rdb.Do(ctx, "JSON.STRLEN", "not_exists", "$").Err(), redis.Nil) }) t.Run("Merge basics", func(t *testing.T) { From 76757e118052ec7a1ffa121c40276e264c215c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BA=AA=E5=8D=8E=E8=A3=95?= Date: Wed, 1 May 2024 21:56:40 +0800 Subject: [PATCH 03/15] Improve code style in cluster (#2272) Co-authored-by: Twice --- src/cluster/cluster.cc | 96 +++++++++++++++++++++--------------------- src/cluster/cluster.h | 12 +++--- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/cluster/cluster.cc b/src/cluster/cluster.cc index 8bdf9095b0e..d080ed34452 100644 --- a/src/cluster/cluster.cc +++ b/src/cluster/cluster.cc @@ -22,9 +22,11 @@ #include +#include #include #include #include +#include #include "cluster/cluster_defs.h" #include "commands/commander.h" @@ -37,11 +39,11 @@ #include "time_util.h" ClusterNode::ClusterNode(std::string id, std::string host, int port, int role, std::string master_id, - std::bitset slots) + const std::bitset &slots) : id(std::move(id)), host(std::move(host)), port(port), role(role), master_id(std::move(master_id)), slots(slots) {} Cluster::Cluster(Server *srv, std::vector binds, int port) - : srv_(srv), binds_(std::move(binds)), port_(port), size_(0), version_(-1), myself_(nullptr) { + : srv_(srv), binds_(std::move(binds)), port_(port) { for (auto &slots_node : slots_nodes_) { slots_node = nullptr; } @@ -53,10 +55,10 @@ Cluster::Cluster(Server *srv, std::vector binds, int port) // cluster data, so these commands should be executed exclusively, and ReadWriteLock // also can guarantee accessing data is safe. bool Cluster::SubCommandIsExecExclusive(const std::string &subcommand) { - for (auto v : {"setnodes", "setnodeid", "setslot", "import", "reset"}) { - if (util::EqualICase(v, subcommand)) return true; - } - return false; + std::array subcommands = {"setnodes", "setnodeid", "setslot", "import", "reset"}; + + return std::any_of(std::begin(subcommands), std::end(subcommands), + [&subcommand](const std::string &val) { return util::EqualICase(val, subcommand); }); } Status Cluster::SetNodeId(const std::string &node_id) { @@ -170,26 +172,26 @@ Status Cluster::SetClusterNodes(const std::string &nodes_str, int64_t version, b size_ = 0; // Update slots to nodes - for (const auto &n : slots_nodes) { - slots_nodes_[n.first] = nodes_[n.second]; + for (const auto &[slot, node_id] : slots_nodes) { + slots_nodes_[slot] = nodes_[node_id]; } // Update replicas info and size - for (auto &n : nodes_) { - if (n.second->role == kClusterSlave) { - if (nodes_.find(n.second->master_id) != nodes_.end()) { - nodes_[n.second->master_id]->replicas.push_back(n.first); + for (const auto &[node_id, node] : nodes_) { + if (node->role == kClusterSlave) { + if (nodes_.find(node->master_id) != nodes_.end()) { + nodes_[node->master_id]->replicas.push_back(node_id); } } - if (n.second->role == kClusterMaster && n.second->slots.count() > 0) { + if (node->role == kClusterMaster && node->slots.count() > 0) { size_++; } } if (myid_.empty() || force) { - for (auto &n : nodes_) { - if (n.second->port == port_ && util::MatchListeningIP(binds_, n.second->host)) { - myid_ = n.first; + for (const auto &[node_id, node] : nodes_) { + if (node->port == port_ && util::MatchListeningIP(binds_, node->host)) { + myid_ = node_id; break; } } @@ -210,9 +212,9 @@ Status Cluster::SetClusterNodes(const std::string &nodes_str, int64_t version, b // Clear data of migrated slots if (!migrated_slots_.empty()) { - for (auto &it : migrated_slots_) { - if (slots_nodes_[it.first] != myself_) { - auto s = srv_->slot_migrator->ClearKeysOfSlot(kDefaultNamespace, it.first); + for (const auto &[slot, _] : migrated_slots_) { + if (slots_nodes_[slot] != myself_) { + auto s = srv_->slot_migrator->ClearKeysOfSlot(kDefaultNamespace, slot); if (!s.ok()) { LOG(ERROR) << "failed to clear data of migrated slots: " << s.ToString(); } @@ -521,34 +523,32 @@ std::string Cluster::genNodesDescription() { auto now = util::GetTimeStampMS(); std::string nodes_desc; - for (const auto &item : nodes_) { - const std::shared_ptr n = item.second; - + for (const auto &[_, node] : nodes_) { std::string node_str; // ID, host, port - node_str.append(n->id + " "); - node_str.append(fmt::format("{}:{}@{} ", n->host, n->port, n->port + kClusterPortIncr)); + node_str.append(node->id + " "); + node_str.append(fmt::format("{}:{}@{} ", node->host, node->port, node->port + kClusterPortIncr)); // Flags - if (n->id == myid_) node_str.append("myself,"); - if (n->role == kClusterMaster) { + if (node->id == myid_) node_str.append("myself,"); + if (node->role == kClusterMaster) { node_str.append("master - "); } else { - node_str.append("slave " + n->master_id + " "); + node_str.append("slave " + node->master_id + " "); } // Ping sent, pong received, config epoch, link status node_str.append(fmt::format("{} {} {} connected", now - 1, now, version_)); - if (n->role == kClusterMaster) { - auto iter = slots_infos.find(n->id); - if (iter != slots_infos.end() && iter->second.size() > 0) { + if (node->role == kClusterMaster) { + auto iter = slots_infos.find(node->id); + if (iter != slots_infos.end() && !iter->second.empty()) { node_str.append(" " + iter->second); } } // Just for MYSELF node to show the importing/migrating slot - if (n->id == myid_) { + if (node->id == myid_) { if (srv_->slot_migrator) { auto migrating_slot = srv_->slot_migrator->GetMigratingSlot(); if (migrating_slot != -1) { @@ -567,10 +567,10 @@ std::string Cluster::genNodesDescription() { return nodes_desc; } -std::map Cluster::getClusterNodeSlots() const { +std::map> Cluster::getClusterNodeSlots() const { int start = -1; // node id => slots info string - std::map slots_infos; + std::map> slots_infos; std::shared_ptr n = nullptr; for (int i = 0; i <= kClusterSlots; i++) { @@ -600,30 +600,29 @@ std::map Cluster::getClusterNodeSlots() const { return slots_infos; } -std::string Cluster::genNodesInfo() { +std::string Cluster::genNodesInfo() const { auto slots_infos = getClusterNodeSlots(); std::string nodes_info; - for (const auto &item : nodes_) { - const std::shared_ptr &n = item.second; + for (const auto &[_, node] : nodes_) { std::string node_str; node_str.append("node "); // ID - node_str.append(n->id + " "); + node_str.append(node->id + " "); // Host + Port - node_str.append(fmt::format("{} {} ", n->host, n->port)); + node_str.append(fmt::format("{} {} ", node->host, node->port)); // Role - if (n->role == kClusterMaster) { + if (node->role == kClusterMaster) { node_str.append("master - "); } else { - node_str.append("slave " + n->master_id + " "); + node_str.append("slave " + node->master_id + " "); } // Slots - if (n->role == kClusterMaster) { - auto iter = slots_infos.find(n->id); - if (iter != slots_infos.end() && iter->second.size() > 0) { + if (node->role == kClusterMaster) { + auto iter = slots_infos.find(node->id); + if (iter != slots_infos.end() && !iter->second.empty()) { node_str.append(" " + iter->second); } } @@ -694,7 +693,7 @@ Status Cluster::LoadClusterNodes(const std::string &file_path) { Status Cluster::parseClusterNodes(const std::string &nodes_str, ClusterNodes *nodes, std::unordered_map *slots_nodes) { std::vector nodes_info = util::Split(nodes_str, "\n"); - if (nodes_info.size() == 0) { + if (nodes_info.empty()) { return {Status::ClusterInvalidInfo, errInvalidClusterNodeInfo}; } @@ -803,16 +802,17 @@ Status Cluster::parseClusterNodes(const std::string &nodes_str, ClusterNodes *no return Status::OK(); } -bool Cluster::IsWriteForbiddenSlot(int slot) { return srv_->slot_migrator->GetForbiddenSlot() == slot; } +bool Cluster::IsWriteForbiddenSlot(int slot) const { return srv_->slot_migrator->GetForbiddenSlot() == slot; } Status Cluster::CanExecByMySelf(const redis::CommandAttributes *attributes, const std::vector &cmd_tokens, redis::Connection *conn) { std::vector keys_indexes; - auto s = redis::CommandTable::GetKeysFromCommand(attributes, cmd_tokens, &keys_indexes); + // No keys - if (!s.IsOK()) return Status::OK(); + if (auto s = redis::CommandTable::GetKeysFromCommand(attributes, cmd_tokens, &keys_indexes); !s.IsOK()) + return Status::OK(); - if (keys_indexes.size() == 0) return Status::OK(); + if (keys_indexes.empty()) return Status::OK(); int slot = -1; for (auto i : keys_indexes) { diff --git a/src/cluster/cluster.h b/src/cluster/cluster.h index c98ea668082..335d5ef16b1 100644 --- a/src/cluster/cluster.h +++ b/src/cluster/cluster.h @@ -39,7 +39,7 @@ class ClusterNode { public: explicit ClusterNode(std::string id, std::string host, int port, int role, std::string master_id, - std::bitset slots); + const std::bitset &slots); std::string id; std::string host; int port; @@ -81,7 +81,7 @@ class Cluster { int64_t GetVersion() const { return version_; } static bool IsValidSlot(int slot) { return slot >= 0 && slot < kClusterSlots; } bool IsNotMaster(); - bool IsWriteForbiddenSlot(int slot); + bool IsWriteForbiddenSlot(int slot) const; Status CanExecByMySelf(const redis::CommandAttributes *attributes, const std::vector &cmd_tokens, redis::Connection *conn); Status SetMasterSlaveRepl(); @@ -97,16 +97,16 @@ class Cluster { private: std::string getNodeIDBySlot(int slot) const; std::string genNodesDescription(); - std::string genNodesInfo(); - std::map getClusterNodeSlots() const; + std::string genNodesInfo() const; + std::map> getClusterNodeSlots() const; SlotInfo genSlotNodeInfo(int start, int end, const std::shared_ptr &n); static Status parseClusterNodes(const std::string &nodes_str, ClusterNodes *nodes, std::unordered_map *slots_nodes); Server *srv_; std::vector binds_; int port_; - int size_; - int64_t version_; + int size_ = 0; + int64_t version_ = -1; std::string myid_; std::shared_ptr myself_; ClusterNodes nodes_; From 7bbacc894c42e7b8fada15505fff8a0779203383 Mon Sep 17 00:00:00 2001 From: Aleks Lozovyuk Date: Fri, 3 May 2024 03:41:34 +0300 Subject: [PATCH 04/15] Using ubuntu-22.04 for build docker image (#2286) --- .github/workflows/kvrocks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kvrocks.yaml b/.github/workflows/kvrocks.yaml index b7767575574..925d8ad776b 100644 --- a/.github/workflows/kvrocks.yaml +++ b/.github/workflows/kvrocks.yaml @@ -376,7 +376,7 @@ jobs: name: Check Docker image needs: [precondition, check-and-lint, check-typos] if: ${{ needs.precondition.outputs.docs_only != 'true' }} - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Get core numbers From 021c7de21cb02cdc0f74bc6c744d5317497eb76d Mon Sep 17 00:00:00 2001 From: Aleks Lozovyuk Date: Fri, 3 May 2024 15:24:40 +0300 Subject: [PATCH 05/15] Bump jsoncons to v0.175.0 (#2288) --- cmake/jsoncons.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/jsoncons.cmake b/cmake/jsoncons.cmake index 02024fa89b1..bdb5ee2d9fb 100644 --- a/cmake/jsoncons.cmake +++ b/cmake/jsoncons.cmake @@ -20,8 +20,8 @@ include_guard() include(cmake/utils.cmake) FetchContent_DeclareGitHubWithMirror(jsoncons - danielaparker/jsoncons v0.174.0 - MD5=1e620831477adbed19e85248c33cbb89 + danielaparker/jsoncons v0.175.0 + MD5=1ee4a655719dc3333b5c1fbf5a6e9321 ) FetchContent_MakeAvailableWithArgs(jsoncons From dfffffefffd1bd693eb5ae9078a46debabddb6de Mon Sep 17 00:00:00 2001 From: Twice Date: Sat, 4 May 2024 11:52:31 +0900 Subject: [PATCH 06/15] Add plan executors for KQIR via iterator model (#2281) --- src/search/executors/filter_executor.h | 129 ++++++++ .../executors/full_index_scan_executor.h | 76 +++++ src/search/executors/limit_executor.h | 57 ++++ src/search/executors/merge_executor.h | 54 ++++ src/search/executors/mock_executor.h | 57 ++++ src/search/executors/noop_executor.h | 35 +++ src/search/executors/projection_executor.h | 59 ++++ src/search/executors/sort_executor.h | 39 +++ src/search/executors/topn_sort_executor.h | 104 ++++++ src/search/index_info.h | 3 +- src/search/indexer.cc | 4 +- src/search/ir.h | 9 + src/search/ir_sema_checker.h | 2 +- src/search/plan_executor.cc | 149 +++++++++ src/search/plan_executor.h | 101 ++++++ src/search/search_encoding.h | 10 + tests/cppunit/indexer_test.cc | 20 +- tests/cppunit/ir_dot_dumper_test.cc | 16 +- tests/cppunit/ir_pass_test.cc | 14 +- tests/cppunit/ir_sema_checker_test.cc | 10 +- tests/cppunit/plan_executor_test.cc | 297 ++++++++++++++++++ 21 files changed, 1211 insertions(+), 34 deletions(-) create mode 100644 src/search/executors/filter_executor.h create mode 100644 src/search/executors/full_index_scan_executor.h create mode 100644 src/search/executors/limit_executor.h create mode 100644 src/search/executors/merge_executor.h create mode 100644 src/search/executors/mock_executor.h create mode 100644 src/search/executors/noop_executor.h create mode 100644 src/search/executors/projection_executor.h create mode 100644 src/search/executors/sort_executor.h create mode 100644 src/search/executors/topn_sort_executor.h create mode 100644 src/search/plan_executor.cc create mode 100644 src/search/plan_executor.h create mode 100644 tests/cppunit/plan_executor_test.cc diff --git a/src/search/executors/filter_executor.h b/src/search/executors/filter_executor.h new file mode 100644 index 00000000000..83d45377820 --- /dev/null +++ b/src/search/executors/filter_executor.h @@ -0,0 +1,129 @@ +/* + * 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 + +#include "parse_util.h" +#include "search/ir.h" +#include "search/plan_executor.h" +#include "search/search_encoding.h" +#include "string_util.h" + +namespace kqir { + +struct QueryExprEvaluator { + ExecutorContext *ctx; + ExecutorNode::RowType &row; + + StatusOr Transform(QueryExpr *e) const { + if (auto v = dynamic_cast(e)) { + return Visit(v); + } + if (auto v = dynamic_cast(e)) { + return Visit(v); + } + if (auto v = dynamic_cast(e)) { + return Visit(v); + } + if (auto v = dynamic_cast(e)) { + return Visit(v); + } + if (auto v = dynamic_cast(e)) { + return Visit(v); + } + + CHECK(false) << "unreachable"; + } + + StatusOr Visit(AndExpr *v) const { + for (const auto &n : v->inners) { + if (!GET_OR_RET(Transform(n.get()))) return false; + } + + return true; + } + + StatusOr Visit(OrExpr *v) const { + for (const auto &n : v->inners) { + if (GET_OR_RET(Transform(n.get()))) return true; + } + + return false; + } + + StatusOr Visit(NotExpr *v) const { return !GET_OR_RET(Transform(v->inner.get())); } + + StatusOr Visit(TagContainExpr *v) const { + auto val = GET_OR_RET(ctx->Retrieve(row, v->field->info)); + auto meta = v->field->info->MetadataAs(); + + auto split = util::Split(val, std::string(1, meta->separator)); + return std::find(split.begin(), split.end(), v->tag->val) != split.end(); + } + + StatusOr Visit(NumericCompareExpr *v) const { + auto l_str = GET_OR_RET(ctx->Retrieve(row, v->field->info)); + + // TODO: reconsider how to handle failure case here + auto l = GET_OR_RET(ParseFloat(l_str)); + auto r = v->num->val; + + switch (v->op) { + case NumericCompareExpr::EQ: + return l == r; + case NumericCompareExpr::NE: + return l != r; + case NumericCompareExpr::LT: + return l < r; + case NumericCompareExpr::LET: + return l <= r; + case NumericCompareExpr::GT: + return l > r; + case NumericCompareExpr::GET: + return l >= r; + } + } +}; + +struct FilterExecutor : ExecutorNode { + Filter *filter; + + FilterExecutor(ExecutorContext *ctx, Filter *filter) : ExecutorNode(ctx), filter(filter) {} + + StatusOr Next() override { + while (true) { + auto v = GET_OR_RET(ctx->Get(filter->source)->Next()); + + if (std::holds_alternative(v)) return end; + + QueryExprEvaluator eval{ctx, std::get(v)}; + + bool res = GET_OR_RET(eval.Transform(filter->filter_expr.get())); + + if (res) { + return v; + } + } + } +}; + +} // namespace kqir diff --git a/src/search/executors/full_index_scan_executor.h b/src/search/executors/full_index_scan_executor.h new file mode 100644 index 00000000000..0afeae0455b --- /dev/null +++ b/src/search/executors/full_index_scan_executor.h @@ -0,0 +1,76 @@ +/* + * 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 "db_util.h" +#include "search/plan_executor.h" +#include "storage/redis_db.h" +#include "storage/redis_metadata.h" +#include "storage/storage.h" + +namespace kqir { + +struct FullIndexScanExecutor : ExecutorNode { + FullIndexScan *scan; + redis::LatestSnapShot ss; + util::UniqueIterator iter{nullptr}; + const std::string *prefix_iter; + + FullIndexScanExecutor(ExecutorContext *ctx, FullIndexScan *scan) + : ExecutorNode(ctx), scan(scan), ss(ctx->storage), prefix_iter(scan->index->info->prefixes.begin()) {} + + std::string NSKey(const std::string &user_key) { + return ComposeNamespaceKey(scan->index->info->ns, user_key, ctx->storage->IsSlotIdEncoded()); + } + + StatusOr Next() override { + if (prefix_iter == scan->index->info->prefixes.end()) { + return end; + } + + auto ns_key = NSKey(*prefix_iter); + if (!iter) { + rocksdb::ReadOptions read_options = ctx->storage->DefaultScanOptions(); + read_options.snapshot = ss.GetSnapShot(); + iter = util::UniqueIterator(ctx->storage, read_options, + ctx->storage->GetCFHandle(engine::kMetadataColumnFamilyName)); + iter->Seek(ns_key); + } + + while (!iter->Valid() || !iter->key().starts_with(ns_key)) { + prefix_iter++; + if (prefix_iter == scan->index->info->prefixes.end()) { + return end; + } + + ns_key = NSKey(*prefix_iter); + iter->Seek(ns_key); + } + + auto [_, key] = ExtractNamespaceKey(iter->key(), ctx->storage->IsSlotIdEncoded()); + auto key_str = key.ToString(); + + iter->Next(); + return RowType{key_str, {}, scan->index->info}; + } +}; + +} // namespace kqir diff --git a/src/search/executors/limit_executor.h b/src/search/executors/limit_executor.h new file mode 100644 index 00000000000..8b1d4916c9f --- /dev/null +++ b/src/search/executors/limit_executor.h @@ -0,0 +1,57 @@ +/* + * 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 "search/plan_executor.h" + +namespace kqir { + +struct LimitExecutor : ExecutorNode { + Limit *limit; + size_t step = 0; + + LimitExecutor(ExecutorContext *ctx, Limit *limit) : ExecutorNode(ctx), limit(limit) {} + + StatusOr Next() override { + auto offset = limit->limit->offset; + auto count = limit->limit->count; + + if (step == count) { + return end; + } + + if (step == 0) { + while (offset--) { + auto res = GET_OR_RET(ctx->Get(limit->op)->Next()); + + if (std::holds_alternative(res)) { + return end; + } + } + } + + auto res = GET_OR_RET(ctx->Get(limit->op)->Next()); + step++; + return res; + } +}; + +} // namespace kqir diff --git a/src/search/executors/merge_executor.h b/src/search/executors/merge_executor.h new file mode 100644 index 00000000000..66b7bb85650 --- /dev/null +++ b/src/search/executors/merge_executor.h @@ -0,0 +1,54 @@ +/* + * 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 + +#include "search/plan_executor.h" + +namespace kqir { + +struct MergeExecutor : ExecutorNode { + Merge *merge; + decltype(merge->ops)::iterator iter; + + MergeExecutor(ExecutorContext *ctx, Merge *merge) : ExecutorNode(ctx), merge(merge), iter(merge->ops.begin()) {} + + StatusOr Next() override { + if (iter == merge->ops.end()) { + return end; + } + + auto v = GET_OR_RET(ctx->Get(*iter)->Next()); + while (std::holds_alternative(v)) { + iter++; + if (iter == merge->ops.end()) { + return end; + } + + v = GET_OR_RET(ctx->Get(*iter)->Next()); + } + + return v; + } +}; + +} // namespace kqir diff --git a/src/search/executors/mock_executor.h b/src/search/executors/mock_executor.h new file mode 100644 index 00000000000..f9cdf57d131 --- /dev/null +++ b/src/search/executors/mock_executor.h @@ -0,0 +1,57 @@ +/* + * 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 + +#include "search/ir_plan.h" +#include "search/plan_executor.h" + +namespace kqir { + +// this operator is only for executor-testing/debugging purpose +struct Mock : PlanOperator { + std::vector rows; + + explicit Mock(std::vector rows) : rows(std::move(rows)) {} + + std::string Dump() const override { return "mock"; } + std::string_view Name() const override { return "Mock"; } + + std::unique_ptr Clone() const override { return std::make_unique(rows); } +}; + +struct MockExecutor : ExecutorNode { + Mock *mock; + decltype(mock->rows)::iterator iter; + + MockExecutor(ExecutorContext *ctx, Mock *mock) : ExecutorNode(ctx), mock(mock), iter(mock->rows.begin()) {} + + StatusOr Next() override { + if (iter == mock->rows.end()) { + return end; + } + + return *(iter++); + } +}; + +} // namespace kqir diff --git a/src/search/executors/noop_executor.h b/src/search/executors/noop_executor.h new file mode 100644 index 00000000000..1e3685cac50 --- /dev/null +++ b/src/search/executors/noop_executor.h @@ -0,0 +1,35 @@ +/* + * 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 "search/plan_executor.h" + +namespace kqir { + +struct NoopExecutor : ExecutorNode { + Noop *noop; + + NoopExecutor(ExecutorContext *ctx, Noop *noop) : ExecutorNode(ctx), noop(noop) {} + + StatusOr Next() override { return end; } +}; + +} // namespace kqir diff --git a/src/search/executors/projection_executor.h b/src/search/executors/projection_executor.h new file mode 100644 index 00000000000..fe167334500 --- /dev/null +++ b/src/search/executors/projection_executor.h @@ -0,0 +1,59 @@ +/* + * 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 + +#include "search/plan_executor.h" + +namespace kqir { + +struct ProjectionExecutor : ExecutorNode { + Projection *proj; + + ProjectionExecutor(ExecutorContext *ctx, Projection *proj) : ExecutorNode(ctx), proj(proj) {} + + StatusOr Next() override { + auto v = GET_OR_RET(ctx->Get(proj->source)->Next()); + + if (std::holds_alternative(v)) return end; + + auto &row = std::get(v); + if (proj->select->fields.empty()) { + for (const auto &field : row.index->fields) { + GET_OR_RET(ctx->Retrieve(row, &field.second)); + } + } else { + std::map res; + + for (const auto &field : proj->select->fields) { + auto r = GET_OR_RET(ctx->Retrieve(row, field->info)); + res.emplace(field->info, std::move(r)); + } + + return RowType{row.key, res, row.index}; + } + + return v; + } +}; + +} // namespace kqir diff --git a/src/search/executors/sort_executor.h b/src/search/executors/sort_executor.h new file mode 100644 index 00000000000..ed4b205db57 --- /dev/null +++ b/src/search/executors/sort_executor.h @@ -0,0 +1,39 @@ +/* + * 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 "search/plan_executor.h" + +namespace kqir { + +struct SortExecutor : ExecutorNode { + Sort *sort; + + SortExecutor(ExecutorContext *ctx, Sort *sort) : ExecutorNode(ctx), sort(sort) {} + + StatusOr Next() override { + // most of the sort operator will be eliminated via the optimizer passes, + // so currently we don't support this operator since external sort is a little complicated + return {Status::NotSupported, "sort operator is currently not supported"}; + } +}; + +} // namespace kqir diff --git a/src/search/executors/topn_sort_executor.h b/src/search/executors/topn_sort_executor.h new file mode 100644 index 00000000000..741a968928b --- /dev/null +++ b/src/search/executors/topn_sort_executor.h @@ -0,0 +1,104 @@ +/* + * 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 +#include + +#include "parse_util.h" +#include "search/plan_executor.h" + +namespace kqir { + +struct TopNSortExecutor : ExecutorNode { + TopNSort *sort; + + struct ComparedRow { + RowType row; + double val; + + ComparedRow(RowType row, double val) : row(std::move(row)), val(val) {} + + friend bool operator<(const ComparedRow &l, const ComparedRow &r) { return l.val < r.val; } + }; + + std::vector rows; + decltype(rows)::iterator rows_iter; + bool initialized = false; + + TopNSortExecutor(ExecutorContext *ctx, TopNSort *sort) : ExecutorNode(ctx), sort(sort) {} + + StatusOr Next() override { + if (!initialized) { + auto total = sort->limit->offset + sort->limit->count; + if (total == 0) return end; + + auto v = GET_OR_RET(ctx->Get(sort->op)->Next()); + + while (!std::holds_alternative(v)) { + auto &row = std::get(v); + + auto get_order = [this](RowType &row) -> StatusOr { + auto order_str = GET_OR_RET(ctx->Retrieve(row, sort->order->field->info)); + auto order = GET_OR_RET(ParseFloat(order_str)); + return order; + }; + + if (rows.size() == total) { + std::make_heap(rows.begin(), rows.end()); + } + + if (rows.size() < total) { + auto order = GET_OR_RET(get_order(row)); + rows.emplace_back(row, order); + } else { + auto order = GET_OR_RET(get_order(row)); + + if (order < rows[0].val) { + std::pop_heap(rows.begin(), rows.end()); + rows.back() = ComparedRow{row, order}; + std::push_heap(rows.begin(), rows.end()); + } + } + + v = GET_OR_RET(ctx->Get(sort->op)->Next()); + } + + if (rows.size() <= sort->limit->offset) { + return end; + } + + std::sort(rows.begin(), rows.end()); + rows_iter = rows.begin() + static_cast(sort->limit->offset); + initialized = true; + } + + if (rows_iter == rows.end()) { + return end; + } + + auto res = rows_iter->row; + rows_iter++; + return res; + } +}; + +} // namespace kqir diff --git a/src/search/index_info.h b/src/search/index_info.h index 5b0cb7077d7..1751549d690 100644 --- a/src/search/index_info.h +++ b/src/search/index_info.h @@ -54,6 +54,7 @@ struct IndexInfo { SearchMetadata metadata; FieldMap fields; redis::SearchPrefixesMetadata prefixes; + std::string ns; IndexInfo(std::string name, SearchMetadata metadata) : name(std::move(name)), metadata(std::move(metadata)) {} @@ -64,6 +65,6 @@ struct IndexInfo { } }; -using IndexMap = std::map; +using IndexMap = std::map>; } // namespace kqir diff --git a/src/search/indexer.cc b/src/search/indexer.cc index 3e7bbf1fb46..4a4a949cc54 100644 --- a/src/search/indexer.cc +++ b/src/search/indexer.cc @@ -66,7 +66,7 @@ rocksdb::Status FieldValueRetriever::Retrieve(std::string_view field, std::strin return hash.storage_->Get(read_options, sub_key, output); } else if (std::holds_alternative(db)) { auto &value = std::get(db); - auto s = value.Get(field); + auto s = value.Get(field.front() == '$' ? field : fmt::format("$.{}", field)); if (!s.IsOK()) return rocksdb::Status::Corruption(s.Msg()); if (s->value.size() != 1) return rocksdb::Status::NotFound("json value specified by the field (json path) should exist and be unique"); @@ -231,7 +231,7 @@ Status IndexUpdater::Update(const FieldValues &original, std::string_view key, c void GlobalIndexer::Add(IndexUpdater updater) { updater.indexer = this; - for (const auto &prefix : updater.info->prefixes.prefixes) { + for (const auto &prefix : updater.info->prefixes) { prefix_map.insert(prefix, updater); } } diff --git a/src/search/ir.h b/src/search/ir.h index b841c0fa64d..be235e0dbc6 100644 --- a/src/search/ir.h +++ b/src/search/ir.h @@ -76,6 +76,14 @@ struct Node { if (casted) original.release(); return std::unique_ptr(casted); } + + template + static std::vector> List(std::unique_ptr... args) { + std::vector> result; + result.reserve(sizeof...(Args)); + (result.push_back(std::move(args)), ...); + return result; + } }; struct Ref : Node {}; @@ -379,6 +387,7 @@ struct IndexRef : Ref { const IndexInfo *info = nullptr; explicit IndexRef(std::string name) : name(std::move(name)) {} + explicit IndexRef(std::string name, const IndexInfo *info) : name(std::move(name)), info(info) {} std::string_view Name() const override { return "IndexRef"; } std::string Dump() const override { return name; } diff --git a/src/search/ir_sema_checker.h b/src/search/ir_sema_checker.h index d8982e5c6eb..170e646fb09 100644 --- a/src/search/ir_sema_checker.h +++ b/src/search/ir_sema_checker.h @@ -42,7 +42,7 @@ struct SemaChecker { if (auto v = dynamic_cast(node)) { auto index_name = v->index->name; if (auto iter = index_map.find(index_name); iter != index_map.end()) { - current_index = &iter->second; + current_index = iter->second.get(); v->index->info = current_index; GET_OR_RET(Check(v->select.get())); diff --git a/src/search/plan_executor.cc b/src/search/plan_executor.cc new file mode 100644 index 00000000000..75033f1ac25 --- /dev/null +++ b/src/search/plan_executor.cc @@ -0,0 +1,149 @@ +/* + * 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 "plan_executor.h" + +#include + +#include "search/executors/filter_executor.h" +#include "search/executors/full_index_scan_executor.h" +#include "search/executors/limit_executor.h" +#include "search/executors/merge_executor.h" +#include "search/executors/mock_executor.h" +#include "search/executors/noop_executor.h" +#include "search/executors/projection_executor.h" +#include "search/executors/sort_executor.h" +#include "search/executors/topn_sort_executor.h" +#include "search/indexer.h" +#include "search/ir_plan.h" + +namespace kqir { + +namespace details { + +struct ExecutorContextVisitor { + ExecutorContext *ctx; + + void Transform(PlanOperator *op) { + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + CHECK(false) << "unreachable"; + } + + void Visit(Limit *op) { + ctx->nodes[op] = std::make_unique(ctx, op); + Transform(op->op.get()); + } + + void Visit(Sort *op) { + ctx->nodes[op] = std::make_unique(ctx, op); + Transform(op->op.get()); + } + + void Visit(Noop *op) { ctx->nodes[op] = std::make_unique(ctx, op); } + + void Visit(Merge *op) { + ctx->nodes[op] = std::make_unique(ctx, op); + for (const auto &child : op->ops) Transform(child.get()); + } + + void Visit(Filter *op) { + ctx->nodes[op] = std::make_unique(ctx, op); + Transform(op->source.get()); + } + + void Visit(Projection *op) { + ctx->nodes[op] = std::make_unique(ctx, op); + Transform(op->source.get()); + } + + void Visit(TopNSort *op) { + ctx->nodes[op] = std::make_unique(ctx, op); + Transform(op->op.get()); + } + + void Visit(FullIndexScan *op) { ctx->nodes[op] = std::make_unique(ctx, op); } + + void Visit(Mock *op) { ctx->nodes[op] = std::make_unique(ctx, op); } +}; + +} // namespace details + +ExecutorContext::ExecutorContext(PlanOperator *op) : root(op) { + details::ExecutorContextVisitor visitor{this}; + visitor.Transform(root); +} + +ExecutorContext::ExecutorContext(PlanOperator *op, engine::Storage *storage) : root(op), storage(storage) { + details::ExecutorContextVisitor visitor{this}; + visitor.Transform(root); +} + +auto ExecutorContext::Retrieve(RowType &row, const FieldInfo *field) -> StatusOr { // NOLINT + if (auto iter = row.fields.find(field); iter != row.fields.end()) { + return iter->second; + } + + auto retriever = GET_OR_RET( + redis::FieldValueRetriever::Create(field->index->metadata.on_data_type, row.key, storage, field->index->ns)); + + std::string result; + auto s = retriever.Retrieve(field->name, &result); + if (!s.ok()) return {Status::NotOK, s.ToString()}; + + row.fields.emplace(field, result); + return result; +} + +} // namespace kqir diff --git a/src/search/plan_executor.h b/src/search/plan_executor.h new file mode 100644 index 00000000000..82d8e73e6c0 --- /dev/null +++ b/src/search/plan_executor.h @@ -0,0 +1,101 @@ +/* + * 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 + +#include "ir_plan.h" +#include "search/index_info.h" +#include "storage/storage.h" +#include "string_util.h" + +namespace kqir { + +struct ExecutorContext; + +struct ExecutorNode { + using KeyType = std::string; + using ValueType = std::string; + struct RowType { + KeyType key; + std::map fields; + const IndexInfo *index; + + bool operator==(const RowType &another) const { + return key == another.key && fields == another.fields && index == another.index; + } + + bool operator!=(const RowType &another) const { return !(*this == another); } + + // for debug purpose + friend std::ostream &operator<<(std::ostream &os, const RowType &row) { + if (row.index) { + os << row.key << "@" << row.index->name; + } else { + os << row.key; + } + return os << " {" << util::StringJoin(row.fields, [](const auto &v) { return v.first->name + ": " + v.second; }) + << "}"; + } + }; + + static constexpr inline const struct End { + } end{}; + friend constexpr bool operator==(End, End) noexcept { return true; } + friend constexpr bool operator!=(End, End) noexcept { return false; } + + using Result = std::variant; + + ExecutorContext *ctx; + explicit ExecutorNode(ExecutorContext *ctx) : ctx(ctx) {} + + virtual StatusOr Next() = 0; + virtual ~ExecutorNode() = default; +}; + +struct ExecutorContext { + std::map> nodes; + PlanOperator *root; + engine::Storage *storage; + + using Result = ExecutorNode::Result; + using RowType = ExecutorNode::RowType; + using KeyType = ExecutorNode::KeyType; + using ValueType = ExecutorNode::ValueType; + + explicit ExecutorContext(PlanOperator *op); + explicit ExecutorContext(PlanOperator *op, engine::Storage *storage); + + ExecutorNode *Get(PlanOperator *op) { + if (auto iter = nodes.find(op); iter != nodes.end()) { + return iter->second.get(); + } + + return nullptr; + } + + ExecutorNode *Get(const std::unique_ptr &op) { return Get(op.get()); } + + StatusOr Next() { return Get(root)->Next(); } + StatusOr Retrieve(RowType &row, const FieldInfo *field); +}; + +} // namespace kqir diff --git a/src/search/search_encoding.h b/src/search/search_encoding.h index 14bf2923911..24731d32190 100644 --- a/src/search/search_encoding.h +++ b/src/search/search_encoding.h @@ -45,6 +45,16 @@ inline std::string ConstructSearchPrefixesSubkey() { return {(char)SearchSubkeyT struct SearchPrefixesMetadata { std::vector prefixes; + static inline const std::string all[] = {""}; + + auto begin() const { // NOLINT + return prefixes.empty() ? std::begin(all) : prefixes.data(); + } + + auto end() const { // NOLINT + return prefixes.empty() ? std::end(all) : prefixes.data() + prefixes.size(); + } + void Encode(std::string *dst) const { for (const auto &prefix : prefixes) { PutFixed32(dst, prefix.size()); diff --git a/tests/cppunit/indexer_test.cc b/tests/cppunit/indexer_test.cc index ae5a045ef56..ced039e05f0 100644 --- a/tests/cppunit/indexer_test.cc +++ b/tests/cppunit/indexer_test.cc @@ -39,26 +39,26 @@ struct IndexerTest : TestBase { SearchMetadata hash_field_meta(false); hash_field_meta.on_data_type = SearchOnDataType::HASH; - kqir::IndexInfo hash_info("hashtest", hash_field_meta); - hash_info.Add(kqir::FieldInfo("x", std::make_unique())); - hash_info.Add(kqir::FieldInfo("y", std::make_unique())); - hash_info.prefixes.prefixes.emplace_back("idxtesthash"); + auto hash_info = std::make_unique("hashtest", hash_field_meta); + hash_info->Add(kqir::FieldInfo("x", std::make_unique())); + hash_info->Add(kqir::FieldInfo("y", std::make_unique())); + hash_info->prefixes.prefixes.emplace_back("idxtesthash"); map.emplace("hashtest", std::move(hash_info)); - redis::IndexUpdater hash_updater{&map.at("hashtest")}; + redis::IndexUpdater hash_updater{map.at("hashtest").get()}; SearchMetadata json_field_meta(false); json_field_meta.on_data_type = SearchOnDataType::JSON; - kqir::IndexInfo json_info("jsontest", json_field_meta); - json_info.Add(kqir::FieldInfo("$.x", std::make_unique())); - json_info.Add(kqir::FieldInfo("$.y", std::make_unique())); - json_info.prefixes.prefixes.emplace_back("idxtestjson"); + auto json_info = std::make_unique("jsontest", json_field_meta); + json_info->Add(kqir::FieldInfo("$.x", std::make_unique())); + json_info->Add(kqir::FieldInfo("$.y", std::make_unique())); + json_info->prefixes.prefixes.emplace_back("idxtestjson"); map.emplace("jsontest", std::move(json_info)); - redis::IndexUpdater json_updater{&map.at("jsontest")}; + redis::IndexUpdater json_updater{map.at("jsontest").get()}; indexer.Add(std::move(hash_updater)); indexer.Add(std::move(json_updater)); diff --git a/tests/cppunit/ir_dot_dumper_test.cc b/tests/cppunit/ir_dot_dumper_test.cc index 66a588d9a5c..1615b3ca031 100644 --- a/tests/cppunit/ir_dot_dumper_test.cc +++ b/tests/cppunit/ir_dot_dumper_test.cc @@ -71,14 +71,14 @@ static IndexMap MakeIndexMap() { auto f4 = FieldInfo("n2", std::make_unique()); auto f5 = FieldInfo("n3", std::make_unique()); f5.metadata->noindex = true; - auto ia = IndexInfo("ia", SearchMetadata()); - ia.Add(std::move(f1)); - ia.Add(std::move(f2)); - ia.Add(std::move(f3)); - ia.Add(std::move(f4)); - ia.Add(std::move(f5)); - - auto& name = ia.name; + auto ia = std::make_unique("ia", SearchMetadata()); + ia->Add(std::move(f1)); + ia->Add(std::move(f2)); + ia->Add(std::move(f3)); + ia->Add(std::move(f4)); + ia->Add(std::move(f5)); + + auto& name = ia->name; IndexMap res; res.emplace(name, std::move(ia)); return res; diff --git a/tests/cppunit/ir_pass_test.cc b/tests/cppunit/ir_pass_test.cc index a02dc907395..6188bb49f44 100644 --- a/tests/cppunit/ir_pass_test.cc +++ b/tests/cppunit/ir_pass_test.cc @@ -176,14 +176,14 @@ static IndexMap MakeIndexMap() { auto f4 = FieldInfo("n2", std::make_unique()); auto f5 = FieldInfo("n3", std::make_unique()); f5.metadata->noindex = true; - auto ia = IndexInfo("ia", SearchMetadata()); - ia.Add(std::move(f1)); - ia.Add(std::move(f2)); - ia.Add(std::move(f3)); - ia.Add(std::move(f4)); - ia.Add(std::move(f5)); + auto ia = std::make_unique("ia", SearchMetadata()); + ia->Add(std::move(f1)); + ia->Add(std::move(f2)); + ia->Add(std::move(f3)); + ia->Add(std::move(f4)); + ia->Add(std::move(f5)); - auto& name = ia.name; + auto& name = ia->name; IndexMap res; res.emplace(name, std::move(ia)); return res; diff --git a/tests/cppunit/ir_sema_checker_test.cc b/tests/cppunit/ir_sema_checker_test.cc index 9068a38b7c5..678a0a0fa33 100644 --- a/tests/cppunit/ir_sema_checker_test.cc +++ b/tests/cppunit/ir_sema_checker_test.cc @@ -38,12 +38,12 @@ static IndexMap MakeIndexMap() { auto f1 = FieldInfo("f1", std::make_unique()); auto f2 = FieldInfo("f2", std::make_unique()); auto f3 = FieldInfo("f3", std::make_unique()); - auto ia = IndexInfo("ia", SearchMetadata()); - ia.Add(std::move(f1)); - ia.Add(std::move(f2)); - ia.Add(std::move(f3)); + auto ia = std::make_unique("ia", SearchMetadata()); + ia->Add(std::move(f1)); + ia->Add(std::move(f2)); + ia->Add(std::move(f3)); - auto& name = ia.name; + auto& name = ia->name; IndexMap res; res.emplace(name, std::move(ia)); return res; diff --git a/tests/cppunit/plan_executor_test.cc b/tests/cppunit/plan_executor_test.cc new file mode 100644 index 00000000000..59943d19572 --- /dev/null +++ b/tests/cppunit/plan_executor_test.cc @@ -0,0 +1,297 @@ +/* + * 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 "search/plan_executor.h" + +#include + +#include + +#include "config/config.h" +#include "search/executors/mock_executor.h" +#include "search/ir.h" +#include "search/ir_plan.h" +#include "test_base.h" +#include "types/redis_json.h" + +using namespace kqir; + +static auto exe_end = ExecutorNode::Result(ExecutorNode::end); + +static IndexMap MakeIndexMap() { + auto f1 = FieldInfo("f1", std::make_unique()); + auto f2 = FieldInfo("f2", std::make_unique()); + auto f3 = FieldInfo("f3", std::make_unique()); + auto ia = std::make_unique("ia", SearchMetadata()); + ia->ns = "search_ns"; + ia->metadata.on_data_type = SearchOnDataType::JSON; + ia->prefixes.prefixes.emplace_back("test2:"); + ia->prefixes.prefixes.emplace_back("test4:"); + ia->Add(std::move(f1)); + ia->Add(std::move(f2)); + ia->Add(std::move(f3)); + + auto& name = ia->name; + IndexMap res; + res.emplace(name, std::move(ia)); + return res; +} + +static auto index_map = MakeIndexMap(); + +static auto NextRow(ExecutorContext& ctx) { + auto n = ctx.Next(); + EXPECT_EQ(n.Msg(), Status::ok_msg); + auto v = std::move(n).GetValue(); + EXPECT_EQ(v.index(), 1); + return std::get(std::move(v)); +} + +TEST(PlanExecutorTest, Mock) { + auto op = std::make_unique(std::vector{}); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + + op = std::make_unique(std::vector{{"a"}, {"b"}, {"c"}}); + + ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "c"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); +} + +static auto IndexI() -> const IndexInfo* { return index_map.at("ia").get(); } +static auto FieldI(const std::string& f) -> const FieldInfo* { return &index_map.at("ia")->fields.at(f); } + +TEST(PlanExecutorTest, TopNSort) { + std::vector data{ + {"a", {{FieldI("f3"), "4"}}, IndexI()}, {"b", {{FieldI("f3"), "2"}}, IndexI()}, + {"c", {{FieldI("f3"), "7"}}, IndexI()}, {"d", {{FieldI("f3"), "3"}}, IndexI()}, + {"e", {{FieldI("f3"), "1"}}, IndexI()}, {"f", {{FieldI("f3"), "6"}}, IndexI()}, + {"g", {{FieldI("f3"), "8"}}, IndexI()}, + }; + { + auto op = std::make_unique( + std::make_unique(data), + std::make_unique(SortByClause::ASC, std::make_unique("f3", FieldI("f3"))), + std::make_unique(0, 4)); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "e"); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "d"); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + { + auto op = std::make_unique( + std::make_unique(data), + std::make_unique(SortByClause::ASC, std::make_unique("f3", FieldI("f3"))), + std::make_unique(1, 4)); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "d"); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(NextRow(ctx).key, "f"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} + +TEST(PlanExecutorTest, Filter) { + std::vector data{ + {"a", {{FieldI("f3"), "4"}}, IndexI()}, {"b", {{FieldI("f3"), "2"}}, IndexI()}, + {"c", {{FieldI("f3"), "7"}}, IndexI()}, {"d", {{FieldI("f3"), "3"}}, IndexI()}, + {"e", {{FieldI("f3"), "1"}}, IndexI()}, {"f", {{FieldI("f3"), "6"}}, IndexI()}, + {"g", {{FieldI("f3"), "8"}}, IndexI()}, + }; + { + auto field = std::make_unique("f3", FieldI("f3")); + auto op = std::make_unique( + std::make_unique(data), + AndExpr::Create(Node::List( + std::make_unique(NumericCompareExpr::GT, field->CloneAs(), + std::make_unique(2)), + std::make_unique(NumericCompareExpr::LET, field->CloneAs(), + std::make_unique(6))))); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(NextRow(ctx).key, "d"); + ASSERT_EQ(NextRow(ctx).key, "f"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + { + auto field = std::make_unique("f3", FieldI("f3")); + auto op = std::make_unique( + std::make_unique(data), + OrExpr::Create(Node::List( + std::make_unique(NumericCompareExpr::GET, field->CloneAs(), + std::make_unique(6)), + std::make_unique(NumericCompareExpr::LT, field->CloneAs(), + std::make_unique(2))))); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "c"); + ASSERT_EQ(NextRow(ctx).key, "e"); + ASSERT_EQ(NextRow(ctx).key, "f"); + ASSERT_EQ(NextRow(ctx).key, "g"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + + data = {{"a", {{FieldI("f1"), "cpp,java"}}, IndexI()}, {"b", {{FieldI("f1"), "python,cpp,c"}}, IndexI()}, + {"c", {{FieldI("f1"), "c,perl"}}, IndexI()}, {"d", {{FieldI("f1"), "rust,python"}}, IndexI()}, + {"e", {{FieldI("f1"), "java,kotlin"}}, IndexI()}, {"f", {{FieldI("f1"), "c,rust"}}, IndexI()}, + {"g", {{FieldI("f1"), "c,cpp,java"}}, IndexI()}}; + { + auto field = std::make_unique("f1", FieldI("f1")); + auto op = std::make_unique( + std::make_unique(data), + AndExpr::Create(Node::List( + std::make_unique(field->CloneAs(), std::make_unique("c")), + std::make_unique(field->CloneAs(), std::make_unique("cpp"))))); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "g"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + { + auto field = std::make_unique("f1", FieldI("f1")); + auto op = std::make_unique( + std::make_unique(data), + OrExpr::Create(Node::List( + std::make_unique(field->CloneAs(), std::make_unique("rust")), + std::make_unique(field->CloneAs(), std::make_unique("perl"))))); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "c"); + ASSERT_EQ(NextRow(ctx).key, "d"); + ASSERT_EQ(NextRow(ctx).key, "f"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} + +TEST(PlanExecutorTest, Limit) { + std::vector data{ + {"a", {{FieldI("f3"), "4"}}, IndexI()}, {"b", {{FieldI("f3"), "2"}}, IndexI()}, + {"c", {{FieldI("f3"), "7"}}, IndexI()}, {"d", {{FieldI("f3"), "3"}}, IndexI()}, + {"e", {{FieldI("f3"), "1"}}, IndexI()}, {"f", {{FieldI("f3"), "6"}}, IndexI()}, + {"g", {{FieldI("f3"), "8"}}, IndexI()}, + }; + { + auto op = std::make_unique(std::make_unique(data), std::make_unique(1, 2)); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "c"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + { + auto field = std::make_unique("f3", FieldI("f3")); + auto op = std::make_unique(std::make_unique(data), std::make_unique(0, 3)); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "c"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} + +TEST(PlanExecutorTest, Merge) { + std::vector data1{ + {"a", {{FieldI("f3"), "4"}}, IndexI()}, + {"b", {{FieldI("f3"), "2"}}, IndexI()}, + }; + std::vector data2{{"c", {{FieldI("f3"), "7"}}, IndexI()}, + {"d", {{FieldI("f3"), "3"}}, IndexI()}, + {"e", {{FieldI("f3"), "1"}}, IndexI()}}; + { + auto op = + std::make_unique(Node::List(std::make_unique(data1), std::make_unique(data2))); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(NextRow(ctx).key, "c"); + ASSERT_EQ(NextRow(ctx).key, "d"); + ASSERT_EQ(NextRow(ctx).key, "e"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + { + auto op = std::make_unique( + Node::List(std::make_unique(decltype(data1){}), std::make_unique(data1))); + + auto ctx = ExecutorContext(op.get()); + ASSERT_EQ(NextRow(ctx).key, "a"); + ASSERT_EQ(NextRow(ctx).key, "b"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} + +class PlanExecutorTestC : public TestBase { + protected: + explicit PlanExecutorTestC() : json_(std::make_unique(storage_.get(), "search_ns")) {} + ~PlanExecutorTestC() override = default; + + void SetUp() override {} + void TearDown() override {} + + std::unique_ptr json_; +}; + +TEST_F(PlanExecutorTestC, FullIndexScan) { + json_->Set("test1:a", "$", "{}"); + json_->Set("test1:b", "$", "{}"); + json_->Set("test2:c", "$", "{\"f3\": 6}"); + json_->Set("test3:d", "$", "{}"); + json_->Set("test4:e", "$", "{\"f3\": 7}"); + json_->Set("test4:f", "$", "{\"f3\": 2}"); + json_->Set("test4:g", "$", "{\"f3\": 8}"); + json_->Set("test5:h", "$", "{}"); + json_->Set("test5:i", "$", "{}"); + json_->Set("test5:g", "$", "{}"); + + { + auto op = std::make_unique(std::make_unique("ia", IndexI())); + + auto ctx = ExecutorContext(op.get(), storage_.get()); + ASSERT_EQ(NextRow(ctx).key, "test2:c"); + ASSERT_EQ(NextRow(ctx).key, "test4:e"); + ASSERT_EQ(NextRow(ctx).key, "test4:f"); + ASSERT_EQ(NextRow(ctx).key, "test4:g"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + + { + auto op = std::make_unique( + std::make_unique(std::make_unique("ia", IndexI())), + std::make_unique(NumericCompareExpr::GT, std::make_unique("f3", FieldI("f3")), + std::make_unique(3))); + + auto ctx = ExecutorContext(op.get(), storage_.get()); + ASSERT_EQ(NextRow(ctx).key, "test2:c"); + ASSERT_EQ(NextRow(ctx).key, "test4:e"); + ASSERT_EQ(NextRow(ctx).key, "test4:g"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} From 02ff978955758adfa689062b9abdd1d4e1c300b8 Mon Sep 17 00:00:00 2001 From: Twice Date: Sat, 4 May 2024 21:08:00 +0900 Subject: [PATCH 07/15] Add plan executor for KQIR numeric/tag scan operator (#2289) --- src/common/encoding.h | 15 +++ .../executors/numeric_field_scan_executor.h | 113 +++++++++++++++++ .../executors/tag_field_scan_executor.h | 100 +++++++++++++++ src/search/plan_executor.cc | 14 +++ src/search/search_encoding.h | 15 +-- tests/cppunit/plan_executor_test.cc | 119 ++++++++++++++++++ 6 files changed, 366 insertions(+), 10 deletions(-) create mode 100644 src/search/executors/numeric_field_scan_executor.h create mode 100644 src/search/executors/tag_field_scan_executor.h diff --git a/src/common/encoding.h b/src/common/encoding.h index d497793068a..5d05e45614a 100644 --- a/src/common/encoding.h +++ b/src/common/encoding.h @@ -102,6 +102,21 @@ inline bool GetFixed16(rocksdb::Slice *input, uint16_t *value) { return GetFixed inline bool GetFixed32(rocksdb::Slice *input, uint32_t *value) { return GetFixed(input, value); } inline bool GetFixed64(rocksdb::Slice *input, uint64_t *value) { return GetFixed(input, value); } +inline void PutSizedString(std::string *dst, rocksdb::Slice value) { + PutFixed32(dst, value.size()); + dst->append(value.ToStringView()); +} + +inline bool GetSizedString(rocksdb::Slice *input, rocksdb::Slice *value) { + uint32_t size = 0; + if (!GetFixed32(input, &size)) return false; + + if (input->size() < size) return false; + *value = rocksdb::Slice(input->data(), size); + input->remove_prefix(size); + return true; +} + char *EncodeDouble(char *buf, double value); void PutDouble(std::string *dst, double value); double DecodeDouble(const char *ptr); diff --git a/src/search/executors/numeric_field_scan_executor.h b/src/search/executors/numeric_field_scan_executor.h new file mode 100644 index 00000000000..1609f433e44 --- /dev/null +++ b/src/search/executors/numeric_field_scan_executor.h @@ -0,0 +1,113 @@ +/* + * 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 + +#include "db_util.h" +#include "encoding.h" +#include "search/plan_executor.h" +#include "search/search_encoding.h" +#include "storage/redis_db.h" +#include "storage/redis_metadata.h" +#include "storage/storage.h" + +namespace kqir { + +struct NumericFieldScanExecutor : ExecutorNode { + NumericFieldScan *scan; + redis::LatestSnapShot ss; + util::UniqueIterator iter{nullptr}; + + IndexInfo *index; + std::string ns_key; + + NumericFieldScanExecutor(ExecutorContext *ctx, NumericFieldScan *scan) + : ExecutorNode(ctx), scan(scan), ss(ctx->storage), index(scan->field->info->index) { + ns_key = ComposeNamespaceKey(index->ns, index->name, ctx->storage->IsSlotIdEncoded()); + } + + std::string IndexKey(double num) { + return InternalKey(ns_key, redis::ConstructNumericFieldSubkey(scan->field->name, num, {}), index->metadata.version, + ctx->storage->IsSlotIdEncoded()) + .Encode(); + } + + bool InRangeDecode(Slice key, Slice field, double num, double *curr, Slice *user_key) { + auto ikey = InternalKey(key, ctx->storage->IsSlotIdEncoded()); + if (ikey.GetVersion() != index->metadata.version) return false; + auto subkey = ikey.GetSubKey(); + + uint8_t flag = 0; + if (!GetFixed8(&subkey, &flag)) return false; + if (flag != (uint8_t)redis::SearchSubkeyType::NUMERIC_FIELD) return false; + + Slice value; + if (!GetSizedString(&subkey, &value)) return false; + if (value != field) return false; + + if (!GetDouble(&subkey, curr)) return false; + + if (!GetSizedString(&subkey, user_key)) return false; + + return true; + } + + StatusOr Next() override { + if (!iter) { + rocksdb::ReadOptions read_options = ctx->storage->DefaultScanOptions(); + read_options.snapshot = ss.GetSnapShot(); + + iter = + util::UniqueIterator(ctx->storage, read_options, ctx->storage->GetCFHandle(engine::kSearchColumnFamilyName)); + if (scan->order == SortByClause::ASC) { + iter->Seek(IndexKey(scan->range.l)); + } else { + iter->SeekForPrev(IndexKey(IntervalSet::PrevNum(scan->range.r))); + } + } + + if (!iter->Valid()) { + return end; + } + + double curr = 0; + Slice user_key; + if (!InRangeDecode(iter->key(), scan->field->name, scan->range.r, &curr, &user_key)) { + return end; + } + + if (scan->order == SortByClause::ASC ? curr >= scan->range.r : curr < scan->range.l) { + return end; + } + + auto key_str = user_key.ToString(); + + if (scan->order == SortByClause::ASC) { + iter->Next(); + } else { + iter->Prev(); + } + return RowType{key_str, {{scan->field->info, std::to_string(curr)}}, scan->field->info->index}; + } +}; + +} // namespace kqir diff --git a/src/search/executors/tag_field_scan_executor.h b/src/search/executors/tag_field_scan_executor.h new file mode 100644 index 00000000000..a3781c11a4f --- /dev/null +++ b/src/search/executors/tag_field_scan_executor.h @@ -0,0 +1,100 @@ +/* + * 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 + +#include "db_util.h" +#include "encoding.h" +#include "search/plan_executor.h" +#include "search/search_encoding.h" +#include "storage/redis_db.h" +#include "storage/redis_metadata.h" +#include "storage/storage.h" + +namespace kqir { + +struct TagFieldScanExecutor : ExecutorNode { + TagFieldScan *scan; + redis::LatestSnapShot ss; + util::UniqueIterator iter{nullptr}; + + IndexInfo *index; + std::string ns_key; + std::string index_key; + + TagFieldScanExecutor(ExecutorContext *ctx, TagFieldScan *scan) + : ExecutorNode(ctx), scan(scan), ss(ctx->storage), index(scan->field->info->index) { + ns_key = ComposeNamespaceKey(index->ns, index->name, ctx->storage->IsSlotIdEncoded()); + index_key = InternalKey(ns_key, redis::ConstructTagFieldSubkey(scan->field->name, scan->tag, {}), + index->metadata.version, ctx->storage->IsSlotIdEncoded()) + .Encode(); + } + + bool InRangeDecode(Slice key, Slice field, Slice *user_key) { + auto ikey = InternalKey(key, ctx->storage->IsSlotIdEncoded()); + if (ikey.GetVersion() != index->metadata.version) return false; + auto subkey = ikey.GetSubKey(); + + uint8_t flag = 0; + if (!GetFixed8(&subkey, &flag)) return false; + if (flag != (uint8_t)redis::SearchSubkeyType::TAG_FIELD) return false; + + Slice value; + if (!GetSizedString(&subkey, &value)) return false; + if (value != field) return false; + + Slice tag; + if (!GetSizedString(&subkey, &tag)) return false; + if (tag != scan->tag) return false; + + if (!GetSizedString(&subkey, user_key)) return false; + + return true; + } + + StatusOr Next() override { + if (!iter) { + rocksdb::ReadOptions read_options = ctx->storage->DefaultScanOptions(); + read_options.snapshot = ss.GetSnapShot(); + + iter = + util::UniqueIterator(ctx->storage, read_options, ctx->storage->GetCFHandle(engine::kSearchColumnFamilyName)); + iter->Seek(index_key); + } + + if (!iter->Valid()) { + return end; + } + + Slice user_key; + if (!InRangeDecode(iter->key(), scan->field->name, &user_key)) { + return end; + } + + auto key_str = user_key.ToString(); + + iter->Next(); + return RowType{key_str, {}, scan->field->info->index}; + } +}; + +} // namespace kqir diff --git a/src/search/plan_executor.cc b/src/search/plan_executor.cc index 75033f1ac25..7de9f5188bd 100644 --- a/src/search/plan_executor.cc +++ b/src/search/plan_executor.cc @@ -28,8 +28,10 @@ #include "search/executors/merge_executor.h" #include "search/executors/mock_executor.h" #include "search/executors/noop_executor.h" +#include "search/executors/numeric_field_scan_executor.h" #include "search/executors/projection_executor.h" #include "search/executors/sort_executor.h" +#include "search/executors/tag_field_scan_executor.h" #include "search/executors/topn_sort_executor.h" #include "search/indexer.h" #include "search/ir_plan.h" @@ -74,6 +76,14 @@ struct ExecutorContextVisitor { return Visit(v); } + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + + if (auto v = dynamic_cast(op)) { + return Visit(v); + } + if (auto v = dynamic_cast(op)) { return Visit(v); } @@ -115,6 +125,10 @@ struct ExecutorContextVisitor { void Visit(FullIndexScan *op) { ctx->nodes[op] = std::make_unique(ctx, op); } + void Visit(NumericFieldScan *op) { ctx->nodes[op] = std::make_unique(ctx, op); } + + void Visit(TagFieldScan *op) { ctx->nodes[op] = std::make_unique(ctx, op); } + void Visit(Mock *op) { ctx->nodes[op] = std::make_unique(ctx, op); } }; diff --git a/src/search/search_encoding.h b/src/search/search_encoding.h index 24731d32190..32f244ca237 100644 --- a/src/search/search_encoding.h +++ b/src/search/search_encoding.h @@ -141,22 +141,17 @@ struct SearchNumericFieldMetadata : SearchSortableFieldMetadata {}; inline std::string ConstructTagFieldSubkey(std::string_view field_name, std::string_view tag, std::string_view key) { std::string res = {(char)SearchSubkeyType::TAG_FIELD}; - PutFixed32(&res, field_name.size()); - res.append(field_name); - PutFixed32(&res, tag.size()); - res.append(tag); - PutFixed32(&res, key.size()); - res.append(key); + PutSizedString(&res, field_name); + PutSizedString(&res, tag); + PutSizedString(&res, key); return res; } inline std::string ConstructNumericFieldSubkey(std::string_view field_name, double number, std::string_view key) { std::string res = {(char)SearchSubkeyType::NUMERIC_FIELD}; - PutFixed32(&res, field_name.size()); - res.append(field_name); + PutSizedString(&res, field_name); PutDouble(&res, number); - PutFixed32(&res, key.size()); - res.append(key); + PutSizedString(&res, key); return res; } diff --git a/tests/cppunit/plan_executor_test.cc b/tests/cppunit/plan_executor_test.cc index 59943d19572..0b225fc7a1d 100644 --- a/tests/cppunit/plan_executor_test.cc +++ b/tests/cppunit/plan_executor_test.cc @@ -25,6 +25,8 @@ #include "config/config.h" #include "search/executors/mock_executor.h" +#include "search/indexer.h" +#include "search/interval.h" #include "search/ir.h" #include "search/ir_plan.h" #include "test_base.h" @@ -295,3 +297,120 @@ TEST_F(PlanExecutorTestC, FullIndexScan) { ASSERT_EQ(ctx.Next().GetValue(), exe_end); } } + +struct ScopedUpdate { + redis::GlobalIndexer::RecordResult rr; + std::string_view key; + std::string ns; + + static auto Create(redis::GlobalIndexer& indexer, std::string_view key, const std::string& ns) { + auto s = indexer.Record(key, ns); + EXPECT_EQ(s.Msg(), Status::ok_msg); + return *s; + } + + ScopedUpdate(redis::GlobalIndexer& indexer, std::string_view key, const std::string& ns) + : rr(Create(indexer, key, ns)), key(key), ns(ns) {} + + ScopedUpdate(const ScopedUpdate&) = delete; + ScopedUpdate(ScopedUpdate&&) = delete; + ScopedUpdate& operator=(const ScopedUpdate&) = delete; + ScopedUpdate& operator=(ScopedUpdate&&) = delete; + + ~ScopedUpdate() { + auto s = redis::GlobalIndexer::Update(rr, key, ns); + EXPECT_EQ(s.Msg(), Status::ok_msg); + } +}; + +std::vector> ScopedUpdates(redis::GlobalIndexer& indexer, + const std::vector& keys, + const std::string& ns) { + std::vector> sus; + + sus.reserve(keys.size()); + for (auto key : keys) { + sus.emplace_back(std::make_unique(indexer, key, ns)); + } + + return sus; +} + +TEST_F(PlanExecutorTestC, NumericFieldScan) { + redis::GlobalIndexer indexer(storage_.get()); + indexer.Add(redis::IndexUpdater(IndexI())); + + { + auto updates = ScopedUpdates(indexer, {"test2:a", "test2:b", "test2:c", "test2:d", "test2:e", "test2:f", "test2:g"}, + "search_ns"); + json_->Set("test2:a", "$", "{\"f2\": 6}"); + json_->Set("test2:b", "$", "{\"f2\": 3}"); + json_->Set("test2:c", "$", "{\"f2\": 8}"); + json_->Set("test2:d", "$", "{\"f2\": 14}"); + json_->Set("test2:e", "$", "{\"f2\": 1}"); + json_->Set("test2:f", "$", "{\"f2\": 3}"); + json_->Set("test2:g", "$", "{\"f2\": 9}"); + } + + { + auto op = std::make_unique(std::make_unique("f2", FieldI("f2")), Interval(3, 9), + SortByClause::ASC); + + auto ctx = ExecutorContext(op.get(), storage_.get()); + ASSERT_EQ(NextRow(ctx).key, "test2:b"); + ASSERT_EQ(NextRow(ctx).key, "test2:f"); + ASSERT_EQ(NextRow(ctx).key, "test2:a"); + ASSERT_EQ(NextRow(ctx).key, "test2:c"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + + { + auto op = std::make_unique(std::make_unique("f2", FieldI("f2")), Interval(3, 9), + SortByClause::DESC); + + auto ctx = ExecutorContext(op.get(), storage_.get()); + ASSERT_EQ(NextRow(ctx).key, "test2:c"); + ASSERT_EQ(NextRow(ctx).key, "test2:a"); + ASSERT_EQ(NextRow(ctx).key, "test2:f"); + ASSERT_EQ(NextRow(ctx).key, "test2:b"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} + +TEST_F(PlanExecutorTestC, TagFieldScan) { + redis::GlobalIndexer indexer(storage_.get()); + indexer.Add(redis::IndexUpdater(IndexI())); + + { + auto updates = ScopedUpdates(indexer, {"test2:a", "test2:b", "test2:c", "test2:d", "test2:e", "test2:f", "test2:g"}, + "search_ns"); + json_->Set("test2:a", "$", "{\"f1\": \"c,cpp,java\"}"); + json_->Set("test2:b", "$", "{\"f1\": \"python,c\"}"); + json_->Set("test2:c", "$", "{\"f1\": \"java,scala\"}"); + json_->Set("test2:d", "$", "{\"f1\": \"rust,python,perl\"}"); + json_->Set("test2:e", "$", "{\"f1\": \"python,cpp\"}"); + json_->Set("test2:f", "$", "{\"f1\": \"c,cpp\"}"); + json_->Set("test2:g", "$", "{\"f1\": \"cpp,rust\"}"); + } + + { + auto op = std::make_unique(std::make_unique("f1", FieldI("f1")), "cpp"); + + auto ctx = ExecutorContext(op.get(), storage_.get()); + ASSERT_EQ(NextRow(ctx).key, "test2:a"); + ASSERT_EQ(NextRow(ctx).key, "test2:e"); + ASSERT_EQ(NextRow(ctx).key, "test2:f"); + ASSERT_EQ(NextRow(ctx).key, "test2:g"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } + + { + auto op = std::make_unique(std::make_unique("f1", FieldI("f1")), "python"); + + auto ctx = ExecutorContext(op.get(), storage_.get()); + ASSERT_EQ(NextRow(ctx).key, "test2:b"); + ASSERT_EQ(NextRow(ctx).key, "test2:d"); + ASSERT_EQ(NextRow(ctx).key, "test2:e"); + ASSERT_EQ(ctx.Next().GetValue(), exe_end); + } +} \ No newline at end of file From 82f03ca5edc6640a46a8f26d63ceb9385f30f744 Mon Sep 17 00:00:00 2001 From: Twice Date: Sun, 5 May 2024 15:03:13 +0900 Subject: [PATCH 08/15] Move logo acknowledgement to NOTICE (#2290) --- NOTICE | 4 ++++ README.md | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/NOTICE b/NOTICE index 8488d466c93..7e674d153d6 100644 --- a/NOTICE +++ b/NOTICE @@ -6,6 +6,10 @@ The Apache Software Foundation (http://www.apache.org/). ================================================================ +Thanks to designers Lingyu Tian and Shili Fan for contributing the logo of Kvrocks. + +================================================================ + This product includes a number of Dependencies with separate copyright notices and license terms. Your use of these submodules is subject to the terms and conditions of the following licenses. diff --git a/README.md b/README.md index f07c66b0ec4..582bc082e52 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,6 @@ Kvrocks has the following key features: * High Availability: Support Redis sentinel to failover when master or slave was failed. * Cluster: Centralized management but accessible via any Redis cluster client. -Thanks to designers [Lingyu Tian](https://github.com/tianlingyu1997) and Shili Fan for contributing the logo of Kvrocks. - ## Who uses Kvrocks You can find Kvrocks users at [the Users page](https://kvrocks.apache.org/users/). From 93ce7925b26d619ad81c077191ddd69a7fb55d42 Mon Sep 17 00:00:00 2001 From: mwish Date: Tue, 7 May 2024 14:01:25 +0800 Subject: [PATCH 09/15] Trying to add the unit of time in variable name (#2276) Co-authored-by: hulk --- src/cluster/replication.cc | 2 +- src/cluster/replication.h | 4 +- src/commands/cmd_replication.cc | 8 ++-- src/commands/cmd_stream.cc | 6 +-- src/common/cron.cc | 2 +- src/common/cron.h | 2 +- src/common/time_util.h | 1 + src/server/server.cc | 73 +++++++++++++++++---------------- src/server/server.h | 13 +++--- src/stats/stats.cc | 8 ++-- src/stats/stats.h | 4 +- src/storage/rdb.cc | 21 +++++----- src/storage/storage.cc | 33 +++++++-------- src/storage/storage.h | 15 ++++--- src/types/redis_stream.cc | 24 +++++------ src/types/redis_stream_base.h | 6 +-- src/types/redis_string.cc | 20 ++++----- src/types/redis_string.h | 15 +++---- 18 files changed, 132 insertions(+), 125 deletions(-) diff --git a/src/cluster/replication.cc b/src/cluster/replication.cc index 4df05a47683..57d8b9bc17f 100644 --- a/src/cluster/replication.cc +++ b/src/cluster/replication.cc @@ -201,7 +201,7 @@ void ReplicationThread::CallbacksStateMachine::ReadWriteCB(bufferevent *bev) { assert(handler_idx_ <= handlers_.size()); DLOG(INFO) << "[replication] Execute handler[" << getHandlerName(handler_idx_) << "]"; auto st = getHandlerFunc(handler_idx_)(repl_, bev); - repl_->last_io_time_.store(util::GetTimeStamp(), std::memory_order_relaxed); + repl_->last_io_time_secs_.store(util::GetTimeStamp(), std::memory_order_relaxed); switch (st) { case CBState::NEXT: ++handler_idx_; diff --git a/src/cluster/replication.h b/src/cluster/replication.h index b7f49717cc1..b223bd6a0e5 100644 --- a/src/cluster/replication.h +++ b/src/cluster/replication.h @@ -98,7 +98,7 @@ class ReplicationThread : private EventCallbackBase { Status Start(std::function &&pre_fullsync_cb, std::function &&post_fullsync_cb); void Stop(); ReplState State() { return repl_state_.load(std::memory_order_relaxed); } - time_t LastIOTime() { return last_io_time_.load(std::memory_order_relaxed); } + int64_t LastIOTimeSecs() const { return last_io_time_secs_.load(std::memory_order_relaxed); } void TimerCB(int, int16_t); @@ -155,7 +155,7 @@ class ReplicationThread : private EventCallbackBase { Server *srv_ = nullptr; engine::Storage *storage_ = nullptr; std::atomic repl_state_; - std::atomic last_io_time_ = 0; + std::atomic last_io_time_secs_ = 0; bool next_try_old_psync_ = false; bool next_try_without_announce_ip_address_ = false; diff --git a/src/commands/cmd_replication.cc b/src/commands/cmd_replication.cc index 0a86a9cc619..6beffea8c31 100644 --- a/src/commands/cmd_replication.cc +++ b/src/commands/cmd_replication.cc @@ -242,8 +242,8 @@ class CommandFetchMeta : public Commander { } else { LOG(WARNING) << "[replication] Fail to send full data file info " << ip << ", error: " << strerror(errno); } - auto now = static_cast(util::GetTimeStamp()); - srv->storage->SetCheckpointAccessTime(now); + auto now_secs = static_cast(util::GetTimeStamp()); + srv->storage->SetCheckpointAccessTimeSecs(now_secs); })); if (auto s = util::ThreadDetach(t); !s) { @@ -311,8 +311,8 @@ class CommandFetchFile : public Commander { usleep(shortest - duration); } } - auto now = static_cast(util::GetTimeStamp()); - srv->storage->SetCheckpointAccessTime(now); + auto now_secs = util::GetTimeStamp(); + srv->storage->SetCheckpointAccessTimeSecs(now_secs); srv->DecrFetchFileThread(); })); diff --git a/src/commands/cmd_stream.cc b/src/commands/cmd_stream.cc index f8736168690..a5fdf7958ab 100644 --- a/src/commands/cmd_stream.cc +++ b/src/commands/cmd_stream.cc @@ -588,7 +588,7 @@ class CommandXInfo : public Commander { } output->append(redis::MultiLen(result_vector.size())); - auto now = util::GetTimeStampMS(); + auto now_ms = util::GetTimeStampMS(); for (auto const &it : result_vector) { output->append(conn->HeaderOfMap(4)); output->append(redis::BulkString("name")); @@ -596,9 +596,9 @@ class CommandXInfo : public Commander { output->append(redis::BulkString("pending")); output->append(redis::Integer(it.second.pending_number)); output->append(redis::BulkString("idle")); - output->append(redis::Integer(now - it.second.last_idle)); + output->append(redis::Integer(now_ms - it.second.last_idle_ms)); output->append(redis::BulkString("inactive")); - output->append(redis::Integer(now - it.second.last_active)); + output->append(redis::Integer(now_ms - it.second.last_active_ms)); } return Status::OK(); diff --git a/src/common/cron.cc b/src/common/cron.cc index f4c223bf507..2c4a03bae8b 100644 --- a/src/common/cron.cc +++ b/src/common/cron.cc @@ -52,7 +52,7 @@ Status Cron::SetScheduleTime(const std::vector &args) { return Status::OK(); } -bool Cron::IsTimeMatch(tm *tm) { +bool Cron::IsTimeMatch(const tm *tm) { if (tm->tm_min == last_tm_.tm_min && tm->tm_hour == last_tm_.tm_hour && tm->tm_mday == last_tm_.tm_mday && tm->tm_mon == last_tm_.tm_mon && tm->tm_wday == last_tm_.tm_wday) { return false; diff --git a/src/common/cron.h b/src/common/cron.h index cba6d275af3..5385a0efe85 100644 --- a/src/common/cron.h +++ b/src/common/cron.h @@ -43,7 +43,7 @@ class Cron { ~Cron() = default; Status SetScheduleTime(const std::vector &args); - bool IsTimeMatch(tm *tm); + bool IsTimeMatch(const tm *tm); std::string ToString() const; bool IsEnabled() const; diff --git a/src/common/time_util.h b/src/common/time_util.h index 1c8dc7b6272..9eb6daa4266 100644 --- a/src/common/time_util.h +++ b/src/common/time_util.h @@ -24,6 +24,7 @@ namespace util { +/// Get the system timestamp in seconds, milliseconds or microseconds. template auto GetTimeStamp() { return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); diff --git a/src/server/server.cc b/src/server/server.cc index e482aefb561..7c50a00803c 100644 --- a/src/server/server.cc +++ b/src/server/server.cc @@ -52,7 +52,7 @@ #include "worker.h" Server::Server(engine::Storage *storage, Config *config) - : storage(storage), start_time_(util::GetTimeStamp()), config_(config), namespace_(storage) { + : storage(storage), start_time_secs_(util::GetTimeStamp()), config_(config), namespace_(storage) { // init commands stats here to prevent concurrent insert, and cause core auto commands = redis::CommandTable::GetOriginal(); for (const auto &iter : *commands) { @@ -179,7 +179,7 @@ Status Server::Start() { compaction_checker_thread_ = GET_OR_RET(util::CreateThread("compact-check", [this] { uint64_t counter = 0; - time_t last_compact_date = 0; + int64_t last_compact_date = 0; CompactionChecker compaction_checker{this->storage}; while (!stop_) { @@ -192,11 +192,9 @@ Status Server::Start() { if (!is_loading_ && ++counter % 600 == 0 // check every minute && config_->compaction_checker_range.Enabled()) { - auto now = static_cast(util::GetTimeStamp()); - std::tm local_time{}; - localtime_r(&now, &local_time); - if (local_time.tm_hour >= config_->compaction_checker_range.start && - local_time.tm_hour <= config_->compaction_checker_range.stop) { + auto now_hours = util::GetTimeStamp(); + if (now_hours >= config_->compaction_checker_range.start && + now_hours <= config_->compaction_checker_range.stop) { std::vector cf_names = {engine::kMetadataColumnFamilyName, engine::kSubkeyColumnFamilyName, engine::kZSetScoreColumnFamilyName, engine::kStreamColumnFamilyName}; for (const auto &cf_name : cf_names) { @@ -204,8 +202,8 @@ Status Server::Start() { } } // compact once per day - if (now != 0 && last_compact_date != now / 86400) { - last_compact_date = now / 86400; + if (now_hours != 0 && last_compact_date != now_hours / 24) { + last_compact_date = now_hours / 24; compaction_checker.CompactPropagateAndPubSubFiles(); } } @@ -344,9 +342,9 @@ void Server::CleanupExitedSlaves() { void Server::FeedMonitorConns(redis::Connection *conn, const std::vector &tokens) { if (monitor_clients_ <= 0) return; - auto now = util::GetTimeStampUS(); + auto now_us = util::GetTimeStampUS(); std::string output = - fmt::format("{}.{} [{} {}]", now / 1000000, now % 1000000, conn->GetNamespace(), conn->GetAddr()); + fmt::format("{}.{} [{} {}]", now_us / 1000000, now_us % 1000000, conn->GetNamespace(), conn->GetAddr()); for (const auto &token : tokens) { output += " \""; output += util::EscapeString(token); @@ -674,7 +672,7 @@ void Server::OnEntryAddedToStream(const std::string &ns, const std::string &key, } } -void Server::updateCachedTime() { unix_time.store(util::GetTimeStamp()); } +void Server::updateCachedTime() { unix_time_secs.store(util::GetTimeStamp()); } int Server::IncrClientNum() { total_clients_.fetch_add(1, std::memory_order_relaxed); @@ -787,13 +785,14 @@ void Server::cron() { // No replica uses this checkpoint, we can remove it. if (counter != 0 && counter % 100 == 0) { - time_t create_time = storage->GetCheckpointCreateTime(); - time_t access_time = storage->GetCheckpointAccessTime(); + int64_t create_time_secs = storage->GetCheckpointCreateTimeSecs(); + int64_t access_time_secs = storage->GetCheckpointAccessTimeSecs(); if (storage->ExistCheckpoint()) { // TODO(shooterit): support to config the alive time of checkpoint - auto now = static_cast(util::GetTimeStamp()); - if ((GetFetchFileThreadNum() == 0 && now - access_time > 30) || (now - create_time > 24 * 60 * 60)) { + int64_t now_secs = util::GetTimeStamp(); + if ((GetFetchFileThreadNum() == 0 && now_secs - access_time_secs > 30) || + (now_secs - create_time_secs > 24 * 60 * 60)) { auto s = rocksdb::DestroyDB(config_->checkpoint_dir, rocksdb::Options()); if (!s.ok()) { LOG(WARNING) << "[server] Fail to clean checkpoint, error: " << s.ToString(); @@ -963,9 +962,9 @@ void Server::GetServerInfo(std::string *info) { string_stream << "arch_bits:" << sizeof(void *) * 8 << "\r\n"; string_stream << "process_id:" << getpid() << "\r\n"; string_stream << "tcp_port:" << config_->port << "\r\n"; - int64_t now = util::GetTimeStamp(); - string_stream << "uptime_in_seconds:" << now - start_time_ << "\r\n"; - string_stream << "uptime_in_days:" << (now - start_time_) / 86400 << "\r\n"; + int64_t now_secs = util::GetTimeStamp(); + string_stream << "uptime_in_seconds:" << now_secs - start_time_secs_ << "\r\n"; + string_stream << "uptime_in_days:" << (now_secs - start_time_secs_) / 86400 << "\r\n"; *info = string_stream.str(); } @@ -1000,14 +999,14 @@ void Server::GetReplicationInfo(std::string *info) { string_stream << "# Replication\r\n"; string_stream << "role:" << (IsSlave() ? "slave" : "master") << "\r\n"; if (IsSlave()) { - time_t now = util::GetTimeStamp(); + int64_t now_secs = util::GetTimeStamp(); string_stream << "master_host:" << master_host_ << "\r\n"; string_stream << "master_port:" << master_port_ << "\r\n"; ReplState state = GetReplicationState(); string_stream << "master_link_status:" << (state == kReplConnected ? "up" : "down") << "\r\n"; string_stream << "master_sync_unrecoverable_error:" << (state == kReplError ? "yes" : "no") << "\r\n"; string_stream << "master_sync_in_progress:" << (state == kReplFetchMeta || state == kReplFetchSST) << "\r\n"; - string_stream << "master_last_io_seconds_ago:" << now - replication_thread_->LastIOTime() << "\r\n"; + string_stream << "master_last_io_seconds_ago:" << now_secs - replication_thread_->LastIOTimeSecs() << "\r\n"; string_stream << "slave_repl_offset:" << storage->LatestSeqNumber() << "\r\n"; string_stream << "slave_priority:" << config_->slave_priority << "\r\n"; } @@ -1091,15 +1090,15 @@ void Server::SetLastRandomKeyCursor(const std::string &cursor) { } int64_t Server::GetCachedUnixTime() { - if (unix_time.load() == 0) { + if (unix_time_secs.load() == 0) { updateCachedTime(); } - return unix_time.load(); + return unix_time_secs.load(); } int64_t Server::GetLastBgsaveTime() { std::lock_guard lg(db_job_mu_); - return last_bgsave_time_ == -1 ? start_time_ : last_bgsave_time_; + return last_bgsave_timestamp_secs_ == -1 ? start_time_secs_ : last_bgsave_timestamp_secs_; } void Server::GetStatsInfo(std::string *info) { @@ -1141,7 +1140,7 @@ void Server::GetCommandsStatsInfo(std::string *info) { auto latency = cmd_stat.second.latency.load(); string_stream << "cmdstat_" << cmd_stat.first << ":calls=" << calls << ",usec=" << latency - << ",usec_per_call=" << ((calls == 0) ? 0 : static_cast(latency / calls)) << "\r\n"; + << ",usec_per_call=" << static_cast(latency / calls) << "\r\n"; } *info = string_stream.str(); @@ -1195,9 +1194,10 @@ void Server::GetInfo(const std::string &ns, const std::string §ion, std::str std::lock_guard lg(db_job_mu_); string_stream << "bgsave_in_progress:" << (is_bgsave_in_progress_ ? 1 : 0) << "\r\n"; - string_stream << "last_bgsave_time:" << (last_bgsave_time_ == -1 ? start_time_ : last_bgsave_time_) << "\r\n"; + string_stream << "last_bgsave_time:" + << (last_bgsave_timestamp_secs_ == -1 ? start_time_secs_ : last_bgsave_timestamp_secs_) << "\r\n"; string_stream << "last_bgsave_status:" << last_bgsave_status_ << "\r\n"; - string_stream << "last_bgsave_time_sec:" << last_bgsave_time_sec_ << "\r\n"; + string_stream << "last_bgsave_time_sec:" << last_bgsave_duration_secs_ << "\r\n"; } if (all || section == "stats") { @@ -1249,8 +1249,9 @@ void Server::GetInfo(const std::string &ns, const std::string §ion, std::str KeyNumStats stats; GetLatestKeyNumStats(ns, &stats); - time_t last_scan_time = GetLastScanTime(ns); - tm last_scan_tm{}; + // FIXME(mwish): output still requires std::tm. + auto last_scan_time = static_cast(GetLastScanTime(ns)); + std::tm last_scan_tm{}; localtime_r(&last_scan_time, &last_scan_tm); if (section_cnt++) string_stream << "\r\n"; @@ -1393,15 +1394,15 @@ Status Server::AsyncBgSaveDB() { is_bgsave_in_progress_ = true; return task_runner_.TryPublish([this] { - auto start_bgsave_time = util::GetTimeStamp(); + auto start_bgsave_time_secs = util::GetTimeStamp(); Status s = storage->CreateBackup(); - auto stop_bgsave_time = util::GetTimeStamp(); + auto stop_bgsave_time_secs = util::GetTimeStamp(); std::lock_guard lg(db_job_mu_); is_bgsave_in_progress_ = false; - last_bgsave_time_ = start_bgsave_time; + last_bgsave_timestamp_secs_ = start_bgsave_time_secs; last_bgsave_status_ = s.IsOK() ? "ok" : "err"; - last_bgsave_time_sec_ = stop_bgsave_time - start_bgsave_time; + last_bgsave_duration_secs_ = stop_bgsave_time_secs - start_bgsave_time_secs; }); } @@ -1436,7 +1437,7 @@ Status Server::AsyncScanDBSize(const std::string &ns) { std::lock_guard lg(db_job_mu_); db_scan_infos_[ns].key_num_stats = stats; - db_scan_infos_[ns].last_scan_time = util::GetTimeStamp(); + db_scan_infos_[ns].last_scan_time_secs = util::GetTimeStamp(); db_scan_infos_[ns].is_scanning = false; }); } @@ -1529,10 +1530,10 @@ void Server::GetLatestKeyNumStats(const std::string &ns, KeyNumStats *stats) { } } -time_t Server::GetLastScanTime(const std::string &ns) { +int64_t Server::GetLastScanTime(const std::string &ns) const { auto iter = db_scan_infos_.find(ns); if (iter != db_scan_infos_.end()) { - return iter->second.last_scan_time; + return iter->second.last_scan_time_secs; } return 0; } diff --git a/src/server/server.h b/src/server/server.h index a0f0477b276..ad967c77f40 100644 --- a/src/server/server.h +++ b/src/server/server.h @@ -56,7 +56,8 @@ constexpr const char *REDIS_VERSION = "4.0.0"; struct DBScanInfo { - time_t last_scan_time = 0; + // Last scan system clock in seconds + int64_t last_scan_time_secs = 0; KeyNumStats key_num_stats; bool is_scanning = false; }; @@ -249,7 +250,7 @@ class Server { Status AsyncPurgeOldBackups(uint32_t num_backups_to_keep, uint32_t backup_max_keep_hours); Status AsyncScanDBSize(const std::string &ns); void GetLatestKeyNumStats(const std::string &ns, KeyNumStats *stats); - time_t GetLastScanTime(const std::string &ns); + int64_t GetLastScanTime(const std::string &ns) const; std::string GenerateCursorFromKeyName(const std::string &key_name, CursorType cursor_type, const char *prefix = ""); std::string GetKeyNameFromCursor(const std::string &cursor, CursorType cursor_type); @@ -294,7 +295,7 @@ class Server { Stats stats; engine::Storage *storage; std::unique_ptr cluster; - static inline std::atomic unix_time = 0; + static inline std::atomic unix_time_secs = 0; std::unique_ptr slot_migrator; std::unique_ptr slot_import; @@ -325,7 +326,7 @@ class Server { std::atomic stop_ = false; std::atomic is_loading_ = false; - int64_t start_time_; + int64_t start_time_secs_; std::mutex slaveof_mu_; std::string master_host_; uint32_t master_port_ = 0; @@ -355,9 +356,9 @@ class Server { std::mutex db_job_mu_; bool db_compacting_ = false; bool is_bgsave_in_progress_ = false; - int64_t last_bgsave_time_ = -1; + int64_t last_bgsave_timestamp_secs_ = -1; std::string last_bgsave_status_ = "ok"; - int64_t last_bgsave_time_sec_ = -1; + int64_t last_bgsave_duration_secs_ = -1; std::map db_scan_infos_; diff --git a/src/stats/stats.cc b/src/stats/stats.cc index 115fc4d9e13..ae18638b221 100644 --- a/src/stats/stats.cc +++ b/src/stats/stats.cc @@ -29,7 +29,7 @@ Stats::Stats() { for (int i = 0; i < STATS_METRIC_COUNT; i++) { InstMetric im; - im.last_sample_time = 0; + im.last_sample_time_ms = 0; im.last_sample_count = 0; im.idx = 0; for (uint64_t &sample : im.samples) { @@ -93,15 +93,15 @@ void Stats::IncrLatency(uint64_t latency, const std::string &command_name) { } void Stats::TrackInstantaneousMetric(int metric, uint64_t current_reading) { - uint64_t curr_time = util::GetTimeStampMS(); + uint64_t curr_time_ms = util::GetTimeStampMS(); std::unique_lock lock(inst_metrics_mutex); - uint64_t t = curr_time - inst_metrics[metric].last_sample_time; + uint64_t t = curr_time_ms - inst_metrics[metric].last_sample_time_ms; uint64_t ops = current_reading - inst_metrics[metric].last_sample_count; uint64_t ops_sec = t > 0 ? (ops * 1000 / t) : 0; inst_metrics[metric].samples[inst_metrics[metric].idx] = ops_sec; inst_metrics[metric].idx++; inst_metrics[metric].idx %= STATS_METRIC_SAMPLES; - inst_metrics[metric].last_sample_time = curr_time; + inst_metrics[metric].last_sample_time_ms = curr_time_ms; inst_metrics[metric].last_sample_count = current_reading; } diff --git a/src/stats/stats.h b/src/stats/stats.h index 88ab2108b09..6fdba09a194 100644 --- a/src/stats/stats.h +++ b/src/stats/stats.h @@ -49,8 +49,8 @@ struct CommandStat { }; struct InstMetric { - uint64_t last_sample_time; // Timestamp of the last sample in ms - uint64_t last_sample_count; // Count in the last sample + uint64_t last_sample_time_ms; // Timestamp of the last sample in ms + uint64_t last_sample_count; // Count in the last sample uint64_t samples[STATS_METRIC_SAMPLES]; int idx; }; diff --git a/src/storage/rdb.cc b/src/storage/rdb.cc index 79c0b8c89c4..f513caca698 100644 --- a/src/storage/rdb.cc +++ b/src/storage/rdb.cc @@ -459,11 +459,11 @@ Status RDB::saveRdbObject(int type, const std::string &key, const RedisObjValue if (type == RDBTypeString) { const auto &value = std::get(obj); redis::String string_db(storage_, ns_); - uint64_t expire = 0; + uint64_t expire_ms = 0; if (ttl_ms > 0) { - expire = ttl_ms + util::GetTimeStampMS(); + expire_ms = ttl_ms + util::GetTimeStampMS(); } - db_status = string_db.SetEX(key, value, expire); + db_status = string_db.SetEX(key, value, expire_ms); } else if (type == RDBTypeSet || type == RDBTypeSetIntSet || type == RDBTypeSetListPack) { const auto &members = std::get>(obj); redis::Set set_db(storage_, ns_); @@ -567,21 +567,20 @@ Status RDB::LoadRdb(uint32_t db_index, bool overwrite_exist_key) { return {Status::NotOK, fmt::format("Can't handle RDB format version {}", rdb_ver)}; } - uint64_t expire_time = 0; + uint64_t expire_time_ms = 0; int64_t expire_keys = 0; int64_t load_keys = 0; int64_t empty_keys_skipped = 0; - auto now = util::GetTimeStampMS(); + auto now_ms = util::GetTimeStampMS(); uint32_t db_id = 0; uint64_t skip_exist_keys = 0; while (true) { auto type = GET_OR_RET(LogWhenError(loadRdbType())); if (type == RDBOpcodeExpireTime) { - expire_time = static_cast(GET_OR_RET(LogWhenError(loadExpiredTimeSeconds()))); - expire_time *= 1000; + expire_time_ms = static_cast(GET_OR_RET(LogWhenError(loadExpiredTimeSeconds()))) * 1000; continue; } else if (type == RDBOpcodeExpireTimeMs) { - expire_time = GET_OR_RET(LogWhenError(loadExpiredTimeMilliseconds(rdb_ver))); + expire_time_ms = GET_OR_RET(LogWhenError(loadExpiredTimeMilliseconds(rdb_ver))); continue; } else if (type == RDBOpcodeFreq) { // LFU frequency: not use in kvrocks GET_OR_RET(LogWhenError(stream_->ReadByte())); // discard the value @@ -637,8 +636,8 @@ Status RDB::LoadRdb(uint32_t db_index, bool overwrite_exist_key) { LOG(WARNING) << "skipping empty key: " << key; } continue; - } else if (expire_time != 0 && - expire_time < now) { // in redis this used to feed this deletion to any connected replicas + } else if (expire_time_ms != 0 && + expire_time_ms < now_ms) { // in redis this used to feed this deletion to any connected replicas expire_keys++; continue; } @@ -655,7 +654,7 @@ Status RDB::LoadRdb(uint32_t db_index, bool overwrite_exist_key) { } } - auto ret = saveRdbObject(type, key, value, expire_time); + auto ret = saveRdbObject(type, key, value, expire_time_ms); if (!ret.IsOK()) { LOG(WARNING) << "save rdb object key " << key << " failed: " << ret.Msg(); } else { diff --git a/src/storage/storage.cc b/src/storage/storage.cc index 1759d2e7a8c..b25ae8a8c67 100644 --- a/src/storage/storage.cc +++ b/src/storage/storage.cc @@ -67,7 +67,7 @@ constexpr double kRocksdbLRUBlockCacheHighPriPoolRatio = 0.75; constexpr double kRocksdbLRURowCacheHighPriPoolRatio = 0.5; // used in creating rocksdb::HyperClockCache, set`estimated_entry_charge` to 0 means let rocksdb dynamically and -// automacally adjust the table size for the cache. +// automatically adjust the table size for the cache. constexpr size_t kRockdbHCCAutoAdjustCharge = 0; const int64_t kIORateLimitMaxMb = 1024000; @@ -75,7 +75,7 @@ const int64_t kIORateLimitMaxMb = 1024000; using rocksdb::Slice; Storage::Storage(Config *config) - : backup_creating_time_(util::GetTimeStamp()), + : backup_creating_time_secs_(util::GetTimeStamp()), env_(rocksdb::Env::Default()), config_(config), lock_mgr_(16), @@ -421,8 +421,8 @@ Status Storage::CreateBackup(uint64_t *sequence_number) { return {Status::NotOK, s.ToString()}; } - // 'backup_mu_' can guarantee 'backup_creating_time_' is thread-safe - backup_creating_time_ = static_cast(util::GetTimeStamp()); + // 'backup_mu_' can guarantee 'backup_creating_time_secs_' is thread-safe + backup_creating_time_secs_ = util::GetTimeStamp(); LOG(INFO) << "[storage] Success to create new backup"; return Status::OK(); @@ -546,7 +546,7 @@ void Storage::EmptyDB() { } void Storage::PurgeOldBackups(uint32_t num_backups_to_keep, uint32_t backup_max_keep_hours) { - time_t now = util::GetTimeStamp(); + auto now_secs = util::GetTimeStamp(); std::lock_guard lg(config_->backup_mu); std::string task_backup_dir = config_->backup_dir; @@ -555,13 +555,14 @@ void Storage::PurgeOldBackups(uint32_t num_backups_to_keep, uint32_t backup_max_ if (!s.ok()) return; // No backup is needed to keep or the backup is expired, we will clean it. - bool backup_expired = (backup_max_keep_hours != 0 && backup_creating_time_ + backup_max_keep_hours * 3600 < now); + bool backup_expired = + (backup_max_keep_hours != 0 && backup_creating_time_secs_ + backup_max_keep_hours * 3600 < now_secs); if (num_backups_to_keep == 0 || backup_expired) { s = rocksdb::DestroyDB(task_backup_dir, rocksdb::Options()); if (s.ok()) { - LOG(INFO) << "[storage] Succeeded cleaning old backup that was created at " << backup_creating_time_; + LOG(INFO) << "[storage] Succeeded cleaning old backup that was created at " << backup_creating_time_secs_; } else { - LOG(INFO) << "[storage] Failed cleaning old backup that was created at " << backup_creating_time_ + LOG(INFO) << "[storage] Failed cleaning old backup that was created at " << backup_creating_time_secs_ << ". Error: " << s.ToString(); } } @@ -975,9 +976,9 @@ Status Storage::ReplDataManager::GetFullReplDataInfo(Storage *storage, std::stri uint64_t checkpoint_latest_seq = 0; s = checkpoint->CreateCheckpoint(data_files_dir, storage->config_->rocks_db.write_buffer_size * MiB, &checkpoint_latest_seq); - auto now = static_cast(util::GetTimeStamp()); - storage->checkpoint_info_.create_time = now; - storage->checkpoint_info_.access_time = now; + auto now_secs = util::GetTimeStamp(); + storage->checkpoint_info_.create_time_secs = now_secs; + storage->checkpoint_info_.access_time_secs = now_secs; storage->checkpoint_info_.latest_seq = checkpoint_latest_seq; if (!s.ok()) { LOG(WARNING) << "[storage] Failed to create checkpoint (snapshot). Error: " << s.ToString(); @@ -987,12 +988,12 @@ Status Storage::ReplDataManager::GetFullReplDataInfo(Storage *storage, std::stri LOG(INFO) << "[storage] Create checkpoint successfully"; } else { // Replicas can share checkpoint to replication if the checkpoint existing time is less than a half of WAL TTL. - int64_t can_shared_time = storage->config_->rocks_db.wal_ttl_seconds / 2; - if (can_shared_time > 60 * 60) can_shared_time = 60 * 60; - if (can_shared_time < 10 * 60) can_shared_time = 10 * 60; + int64_t can_shared_time_secs = storage->config_->rocks_db.wal_ttl_seconds / 2; + if (can_shared_time_secs > 60 * 60) can_shared_time_secs = 60 * 60; + if (can_shared_time_secs < 10 * 60) can_shared_time_secs = 10 * 60; - auto now = static_cast(util::GetTimeStamp()); - if (now - storage->GetCheckpointCreateTime() > can_shared_time) { + auto now_secs = util::GetTimeStamp(); + if (now_secs - storage->GetCheckpointCreateTimeSecs() > can_shared_time_secs) { LOG(WARNING) << "[storage] Can't use current checkpoint, waiting next checkpoint"; return {Status::NotOK, "Can't use current checkpoint, waiting for next checkpoint"}; } diff --git a/src/storage/storage.h b/src/storage/storage.h index 208499b58fc..09151baad22 100644 --- a/src/storage/storage.h +++ b/src/storage/storage.h @@ -215,8 +215,10 @@ class Storage { 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 valid_files); struct CheckpointInfo { - std::atomic create_time = 0; - std::atomic access_time = 0; + // System clock time when the checkpoint was created. + std::atomic create_time_secs = 0; + // System clock time when the checkpoint was last accessed. + std::atomic access_time_secs = 0; uint64_t latest_seq = 0; }; @@ -238,9 +240,9 @@ class Storage { bool ExistCheckpoint(); bool ExistSyncCheckpoint(); - time_t GetCheckpointCreateTime() const { return checkpoint_info_.create_time; } - void SetCheckpointAccessTime(time_t t) { checkpoint_info_.access_time = t; } - time_t GetCheckpointAccessTime() const { return checkpoint_info_.access_time; } + 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_; } @@ -251,7 +253,8 @@ class Storage { private: std::unique_ptr db_ = nullptr; std::string replid_; - time_t backup_creating_time_; + // The system clock time when the backup was created. + int64_t backup_creating_time_secs_; std::unique_ptr backup_ = nullptr; rocksdb::Env *env_; std::shared_ptr sst_file_manager_; diff --git a/src/types/redis_stream.cc b/src/types/redis_stream.cc index 1b9444ed9ea..6c66f800543 100644 --- a/src/types/redis_stream.cc +++ b/src/types/redis_stream.cc @@ -237,8 +237,8 @@ std::string Stream::consumerNameFromInternalKey(rocksdb::Slice key) const { std::string Stream::encodeStreamConsumerMetadataValue(const StreamConsumerMetadata &consumer_metadata) { std::string dst; PutFixed64(&dst, consumer_metadata.pending_number); - PutFixed64(&dst, consumer_metadata.last_idle); - PutFixed64(&dst, consumer_metadata.last_active); + PutFixed64(&dst, consumer_metadata.last_idle_ms); + PutFixed64(&dst, consumer_metadata.last_active_ms); return dst; } @@ -246,8 +246,8 @@ StreamConsumerMetadata Stream::decodeStreamConsumerMetadataValue(const std::stri StreamConsumerMetadata consumer_metadata; rocksdb::Slice input(value); GetFixed64(&input, &consumer_metadata.pending_number); - GetFixed64(&input, &consumer_metadata.last_idle); - GetFixed64(&input, &consumer_metadata.last_active); + GetFixed64(&input, &consumer_metadata.last_idle_ms); + GetFixed64(&input, &consumer_metadata.last_active_ms); return consumer_metadata; } @@ -277,7 +277,7 @@ StreamEntryID Stream::groupAndEntryIdFromPelInternalKey(rocksdb::Slice key, std: std::string Stream::encodeStreamPelEntryValue(const StreamPelEntry &pel_entry) { std::string dst; - PutFixed64(&dst, pel_entry.last_delivery_time); + PutFixed64(&dst, pel_entry.last_delivery_time_ms); PutFixed64(&dst, pel_entry.last_delivery_count); PutFixed64(&dst, pel_entry.consumer_name.size()); dst += pel_entry.consumer_name; @@ -287,7 +287,7 @@ std::string Stream::encodeStreamPelEntryValue(const StreamPelEntry &pel_entry) { StreamPelEntry Stream::decodeStreamPelEntryValue(const std::string &value) { StreamPelEntry pel_entry; rocksdb::Slice input(value); - GetFixed64(&input, &pel_entry.last_delivery_time); + GetFixed64(&input, &pel_entry.last_delivery_time_ms); GetFixed64(&input, &pel_entry.last_delivery_count); uint64_t consumer_name_len = 0; GetFixed64(&input, &consumer_name_len); @@ -487,8 +487,8 @@ rocksdb::Status Stream::createConsumerWithoutLock(const Slice &stream_name, cons StreamConsumerMetadata consumer_metadata; auto now = util::GetTimeStampMS(); - consumer_metadata.last_idle = now; - consumer_metadata.last_active = now; + consumer_metadata.last_idle_ms = now; + consumer_metadata.last_active_ms = now; std::string consumer_key = internalKeyFromConsumerName(ns_key, metadata, group_name, consumer_name); std::string consumer_value = encodeStreamConsumerMetadataValue(consumer_metadata); std::string get_consumer_value; @@ -1153,9 +1153,9 @@ rocksdb::Status Stream::RangeWithPending(const Slice &stream_name, StreamRangeOp return s; } StreamConsumerMetadata consumer_metadata = decodeStreamConsumerMetadataValue(get_consumer_value); - auto now = util::GetTimeStampMS(); - consumer_metadata.last_idle = now; - consumer_metadata.last_active = now; + auto now_ms = util::GetTimeStampMS(); + consumer_metadata.last_idle_ms = now_ms; + consumer_metadata.last_active_ms = now_ms; if (latest) { options.start = consumergroup_metadata.last_delivered_id; @@ -1219,7 +1219,7 @@ rocksdb::Status Stream::RangeWithPending(const Slice &stream_name, StreamRangeOp } entries->emplace_back(entry_id.ToString(), std::move(values)); pel_entry.last_delivery_count += 1; - pel_entry.last_delivery_time = now; + pel_entry.last_delivery_time_ms = now_ms; batch->Put(stream_cf_handle_, iter->key(), encodeStreamPelEntryValue(pel_entry)); ++count; if (count >= options.count) break; diff --git a/src/types/redis_stream_base.h b/src/types/redis_stream_base.h index 60d54d231a2..ae90a3e503e 100644 --- a/src/types/redis_stream_base.h +++ b/src/types/redis_stream_base.h @@ -171,8 +171,8 @@ struct StreamConsumerGroupMetadata { struct StreamConsumerMetadata { uint64_t pending_number = 0; - uint64_t last_idle; - uint64_t last_active; + uint64_t last_idle_ms; + uint64_t last_active_ms; }; enum class StreamSubkeyType { @@ -183,7 +183,7 @@ enum class StreamSubkeyType { }; struct StreamPelEntry { - uint64_t last_delivery_time; + uint64_t last_delivery_time_ms; uint64_t last_delivery_count; std::string consumer_name; }; diff --git a/src/types/redis_string.cc b/src/types/redis_string.cc index e0f0c99daa5..0f0d468874c 100644 --- a/src/types/redis_string.cc +++ b/src/types/redis_string.cc @@ -255,21 +255,21 @@ rocksdb::Status String::Set(const std::string &user_key, const std::string &valu return updateRawValue(ns_key, new_raw_value); } -rocksdb::Status String::SetEX(const std::string &user_key, const std::string &value, uint64_t expire) { +rocksdb::Status String::SetEX(const std::string &user_key, const std::string &value, uint64_t expire_ms) { std::optional ret; - return Set(user_key, value, {expire, StringSetType::NONE, /*get=*/false, /*keep_ttl=*/false}, ret); + return Set(user_key, value, {expire_ms, StringSetType::NONE, /*get=*/false, /*keep_ttl=*/false}, ret); } -rocksdb::Status String::SetNX(const std::string &user_key, const std::string &value, uint64_t expire, bool *flag) { +rocksdb::Status String::SetNX(const std::string &user_key, const std::string &value, uint64_t expire_ms, bool *flag) { std::optional ret; - auto s = Set(user_key, value, {expire, StringSetType::NX, /*get=*/false, /*keep_ttl=*/false}, ret); + auto s = Set(user_key, value, {expire_ms, StringSetType::NX, /*get=*/false, /*keep_ttl=*/false}, ret); *flag = ret.has_value(); return s; } -rocksdb::Status String::SetXX(const std::string &user_key, const std::string &value, uint64_t expire, bool *flag) { +rocksdb::Status String::SetXX(const std::string &user_key, const std::string &value, uint64_t expire_ms, bool *flag) { std::optional ret; - auto s = Set(user_key, value, {expire, StringSetType::XX, /*get=*/false, /*keep_ttl=*/false}, ret); + auto s = Set(user_key, value, {expire_ms, StringSetType::XX, /*get=*/false, /*keep_ttl=*/false}, ret); *flag = ret.has_value(); return s; } @@ -384,7 +384,7 @@ rocksdb::Status String::IncrByFloat(const std::string &user_key, double incremen return updateRawValue(ns_key, raw_value); } -rocksdb::Status String::MSet(const std::vector &pairs, uint64_t expire, bool lock) { +rocksdb::Status String::MSet(const std::vector &pairs, uint64_t expire_ms, bool lock) { // Data race, key string maybe overwrite by other key while didn't lock the keys here, // to improve the set performance std::optional guard; @@ -404,7 +404,7 @@ rocksdb::Status String::MSet(const std::vector &pairs, uint64_t expi for (const auto &pair : pairs) { std::string bytes; Metadata metadata(kRedisString, false); - metadata.expire = expire; + metadata.expire = expire_ms; metadata.Encode(&bytes); bytes.append(pair.value.data(), pair.value.size()); std::string ns_key = AppendNamespacePrefix(pair.key); @@ -413,7 +413,7 @@ rocksdb::Status String::MSet(const std::vector &pairs, uint64_t expi return storage_->Write(storage_->DefaultWriteOptions(), batch->GetWriteBatch()); } -rocksdb::Status String::MSetNX(const std::vector &pairs, uint64_t expire, bool *flag) { +rocksdb::Status String::MSetNX(const std::vector &pairs, uint64_t expire_ms, bool *flag) { *flag = false; int exists = 0; @@ -435,7 +435,7 @@ rocksdb::Status String::MSetNX(const std::vector &pairs, uint64_t ex return rocksdb::Status::OK(); } - rocksdb::Status s = MSet(pairs, /*expire=*/expire, /*lock=*/false); + rocksdb::Status s = MSet(pairs, /*expire_ms=*/expire_ms, /*lock=*/false); if (!s.ok()) return s; *flag = true; diff --git a/src/types/redis_string.h b/src/types/redis_string.h index faf30259cdc..34afb0bd95b 100644 --- a/src/types/redis_string.h +++ b/src/types/redis_string.h @@ -37,6 +37,7 @@ struct StringPair { enum class StringSetType { NONE, NX, XX }; struct StringSetArgs { + // Expire time in mill seconds. uint64_t expire; StringSetType type; bool get; @@ -85,24 +86,24 @@ class String : public Database { rocksdb::Status Set(const std::string &user_key, const std::string &value); rocksdb::Status Set(const std::string &user_key, const std::string &value, StringSetArgs args, std::optional &ret); - rocksdb::Status SetEX(const std::string &user_key, const std::string &value, uint64_t expire); - rocksdb::Status SetNX(const std::string &user_key, const std::string &value, uint64_t expire, bool *flag); - rocksdb::Status SetXX(const std::string &user_key, const std::string &value, uint64_t expire, bool *flag); + rocksdb::Status SetEX(const std::string &user_key, const std::string &value, uint64_t expire_ms); + rocksdb::Status SetNX(const std::string &user_key, const std::string &value, uint64_t expire_ms, bool *flag); + rocksdb::Status SetXX(const std::string &user_key, const std::string &value, uint64_t expire_ms, bool *flag); rocksdb::Status SetRange(const std::string &user_key, size_t offset, const std::string &value, uint64_t *new_size); rocksdb::Status IncrBy(const std::string &user_key, int64_t increment, int64_t *new_value); rocksdb::Status IncrByFloat(const std::string &user_key, double increment, double *new_value); std::vector MGet(const std::vector &keys, std::vector *values); - rocksdb::Status MSet(const std::vector &pairs, uint64_t expire, bool lock = true); - rocksdb::Status MSetNX(const std::vector &pairs, uint64_t expire, bool *flag); + rocksdb::Status MSet(const std::vector &pairs, uint64_t expire_ms, bool lock = true); + rocksdb::Status MSetNX(const std::vector &pairs, uint64_t expire_ms, bool *flag); rocksdb::Status CAS(const std::string &user_key, const std::string &old_value, const std::string &new_value, - uint64_t expire, int *flag); + uint64_t expire_ms, int *flag); rocksdb::Status CAD(const std::string &user_key, const std::string &value, int *flag); rocksdb::Status LCS(const std::string &user_key1, const std::string &user_key2, StringLCSArgs args, StringLCSResult *rst); private: rocksdb::Status getValue(const std::string &ns_key, std::string *value); - rocksdb::Status getValueAndExpire(const std::string &ns_key, std::string *value, uint64_t *expire); + rocksdb::Status getValueAndExpire(const std::string &ns_key, std::string *value, uint64_t *expire_ms); std::vector getValues(const std::vector &ns_keys, std::vector *values); rocksdb::Status getRawValue(const std::string &ns_key, std::string *raw_value); std::vector getRawValues(const std::vector &keys, std::vector *raw_values); From 3b8c69fa33967bd3e3f623ffcb8f2591dd7ea340 Mon Sep 17 00:00:00 2001 From: Zhou SiLe Date: Tue, 7 May 2024 15:02:38 +0800 Subject: [PATCH 10/15] Add support of the SORT command (#2262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 纪华裕 Co-authored-by: hulk Co-authored-by: mwish --- src/commands/cmd_key.cc | 118 +++- src/storage/redis_db.cc | 212 +++++++ src/storage/redis_db.h | 63 ++ tests/gocase/unit/sort/sort_test.go | 881 ++++++++++++++++++++++++++++ 4 files changed, 1273 insertions(+), 1 deletion(-) create mode 100644 tests/gocase/unit/sort/sort_test.go diff --git a/src/commands/cmd_key.cc b/src/commands/cmd_key.cc index 589fa1ed1ae..24d8fe29c4d 100644 --- a/src/commands/cmd_key.cc +++ b/src/commands/cmd_key.cc @@ -424,6 +424,120 @@ class CommandCopy : public Commander { bool replace_ = false; }; +template +class CommandSort : public Commander { + public: + Status Parse(const std::vector &args) override { + CommandParser parser(args, 2); + while (parser.Good()) { + if (parser.EatEqICase("BY")) { + if (!sort_argument_.sortby.empty()) return {Status::InvalidArgument, "don't use multiple BY parameters"}; + sort_argument_.sortby = GET_OR_RET(parser.TakeStr()); + + if (sort_argument_.sortby.find('*') == std::string::npos) { + sort_argument_.dontsort = true; + } else { + /* TODO: + * If BY is specified with a real pattern, we can't accept it in cluster mode, + * unless we can make sure the keys formed by the pattern are in the same slot + * as the key to sort. + * If BY is specified with a real pattern, we can't accept + * it if no full ACL key access is applied for this command. */ + } + } else if (parser.EatEqICase("LIMIT")) { + sort_argument_.offset = GET_OR_RET(parser.template TakeInt()); + sort_argument_.count = GET_OR_RET(parser.template TakeInt()); + } else if (parser.EatEqICase("GET")) { + /* TODO: + * If GET is specified with a real pattern, we can't accept it in cluster mode, + * unless we can make sure the keys formed by the pattern are in the same slot + * as the key to sort. */ + sort_argument_.getpatterns.push_back(GET_OR_RET(parser.TakeStr())); + } else if (parser.EatEqICase("ASC")) { + sort_argument_.desc = false; + } else if (parser.EatEqICase("DESC")) { + sort_argument_.desc = true; + } else if (parser.EatEqICase("ALPHA")) { + sort_argument_.alpha = true; + } else if (parser.EatEqICase("STORE")) { + if constexpr (ReadOnly) { + return {Status::RedisParseErr, "SORT_RO is read-only and does not support the STORE parameter"}; + } + sort_argument_.storekey = GET_OR_RET(parser.TakeStr()); + } else { + return parser.InvalidSyntax(); + } + } + + return Status::OK(); + } + + Status Execute(Server *srv, Connection *conn, std::string *output) override { + redis::Database redis(srv->storage, conn->GetNamespace()); + RedisType type = kRedisNone; + if (auto s = redis.Type(args_[1], &type); !s.ok()) { + return {Status::RedisExecErr, s.ToString()}; + } + + if (type != RedisType::kRedisList && type != RedisType::kRedisSet && type != RedisType::kRedisZSet) { + *output = Error("WRONGTYPE Operation against a key holding the wrong kind of value"); + return Status::OK(); + } + + /* When sorting a set with no sort specified, we must sort the output + * so the result is consistent across scripting and replication. + * + * The other types (list, sorted set) will retain their native order + * even if no sort order is requested, so they remain stable across + * scripting and replication. + * + * TODO: support CLIENT_SCRIPT flag, (!storekey_.empty() || c->flags & CLIENT_SCRIPT)) */ + if (sort_argument_.dontsort && type == RedisType::kRedisSet && (!sort_argument_.storekey.empty())) { + /* Force ALPHA sorting */ + sort_argument_.dontsort = false; + sort_argument_.alpha = true; + sort_argument_.sortby = ""; + } + + std::vector> sorted_elems; + Database::SortResult res = Database::SortResult::DONE; + + if (auto s = redis.Sort(type, args_[1], sort_argument_, &sorted_elems, &res); !s.ok()) { + return {Status::RedisExecErr, s.ToString()}; + } + + switch (res) { + case Database::SortResult::UNKNOWN_TYPE: + *output = redis::Error("Unknown Type"); + break; + case Database::SortResult::DOUBLE_CONVERT_ERROR: + *output = redis::Error("One or more scores can't be converted into double"); + break; + case Database::SortResult::LIMIT_EXCEEDED: + *output = redis::Error("The number of elements to be sorted exceeds SORT_LENGTH_LIMIT = " + + std::to_string(SORT_LENGTH_LIMIT)); + break; + case Database::SortResult::DONE: + if (sort_argument_.storekey.empty()) { + std::vector output_vec; + output_vec.reserve(sorted_elems.size()); + for (const auto &elem : sorted_elems) { + output_vec.emplace_back(elem.has_value() ? redis::BulkString(elem.value()) : conn->NilString()); + } + *output = redis::Array(output_vec); + } else { + *output = Integer(sorted_elems.size()); + } + break; + } + + return Status::OK(); + } + + private: + SortArgument sort_argument_; +}; + REDIS_REGISTER_COMMANDS(MakeCmdAttr("ttl", 2, "read-only", 1, 1, 1), MakeCmdAttr("pttl", 2, "read-only", 1, 1, 1), MakeCmdAttr("type", 2, "read-only", 1, 1, 1), @@ -442,6 +556,8 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr("ttl", 2, "read-only", 1, 1, 1), MakeCmdAttr("unlink", -2, "write no-dbsize-check", 1, -1, 1), MakeCmdAttr("rename", 3, "write", 1, 2, 1), MakeCmdAttr("renamenx", 3, "write", 1, 2, 1), - MakeCmdAttr("copy", -3, "write", 1, 2, 1), ) + MakeCmdAttr("copy", -3, "write", 1, 2, 1), + MakeCmdAttr>("sort", -2, "write", 1, 1, 1), + MakeCmdAttr>("sort_ro", -2, "read-only", 1, 1, 1)) } // namespace redis diff --git a/src/storage/redis_db.cc b/src/storage/redis_db.cc index 3ff1fa0a374..13f9bd2f5c0 100644 --- a/src/storage/redis_db.cc +++ b/src/storage/redis_db.cc @@ -35,6 +35,11 @@ #include "storage/redis_metadata.h" #include "storage/storage.h" #include "time_util.h" +#include "types/redis_hash.h" +#include "types/redis_list.h" +#include "types/redis_set.h" +#include "types/redis_string.h" +#include "types/redis_zset.h" namespace redis { @@ -768,4 +773,211 @@ rocksdb::Status Database::Copy(const std::string &key, const std::string &new_ke return storage_->Write(storage_->DefaultWriteOptions(), batch->GetWriteBatch()); } +std::optional Database::lookupKeyByPattern(const std::string &pattern, const std::string &subst) { + if (pattern == "#") { + return subst; + } + + auto match_pos = pattern.find('*'); + if (match_pos == std::string::npos) { + return std::nullopt; + } + + // hash field + std::string field; + auto arrow_pos = pattern.find("->", match_pos + 1); + if (arrow_pos != std::string::npos && arrow_pos + 2 < pattern.size()) { + field = pattern.substr(arrow_pos + 2); + } + + std::string key = pattern.substr(0, match_pos + 1); + key.replace(match_pos, 1, subst); + + std::string value; + RedisType type = RedisType::kRedisNone; + if (!field.empty()) { + auto hash_db = redis::Hash(storage_, namespace_); + if (auto s = hash_db.Type(key, &type); !s.ok() || type != RedisType::kRedisHash) { + return std::nullopt; + } + + if (auto s = hash_db.Get(key, field, &value); !s.ok()) { + return std::nullopt; + } + } else { + auto string_db = redis::String(storage_, namespace_); + if (auto s = string_db.Type(key, &type); !s.ok() || type != RedisType::kRedisString) { + return std::nullopt; + } + if (auto s = string_db.Get(key, &value); !s.ok()) { + return std::nullopt; + } + } + return value; +} + +rocksdb::Status Database::Sort(RedisType type, const std::string &key, const SortArgument &args, + std::vector> *elems, SortResult *res) { + // Obtain the length of the object to sort. + const std::string ns_key = AppendNamespacePrefix(key); + Metadata metadata(type, false); + auto s = GetMetadata(GetOptions{}, {type}, ns_key, &metadata); + if (!s.ok()) return s; + + if (metadata.size > SORT_LENGTH_LIMIT) { + *res = SortResult::LIMIT_EXCEEDED; + return rocksdb::Status::OK(); + } + auto vectorlen = static_cast(metadata.size); + + // Adjust the offset and count of the limit + int offset = args.offset >= vectorlen ? 0 : std::clamp(args.offset, 0, vectorlen - 1); + int count = args.offset >= vectorlen ? 0 : std::clamp(args.count, -1, vectorlen - offset); + if (count == -1) count = vectorlen - offset; + + // Get the elements that need to be sorted + std::vector str_vec; + if (count != 0) { + if (type == RedisType::kRedisList) { + auto list_db = redis::List(storage_, namespace_); + + if (args.dontsort) { + if (args.desc) { + s = list_db.Range(key, -count - offset, -1 - offset, &str_vec); + if (!s.ok()) return s; + std::reverse(str_vec.begin(), str_vec.end()); + } else { + s = list_db.Range(key, offset, offset + count - 1, &str_vec); + if (!s.ok()) return s; + } + } else { + s = list_db.Range(key, 0, -1, &str_vec); + if (!s.ok()) return s; + } + } else if (type == RedisType::kRedisSet) { + auto set_db = redis::Set(storage_, namespace_); + s = set_db.Members(key, &str_vec); + if (!s.ok()) return s; + + if (args.dontsort) { + str_vec = std::vector(std::make_move_iterator(str_vec.begin() + offset), + std::make_move_iterator(str_vec.begin() + offset + count)); + } + } else if (type == RedisType::kRedisZSet) { + auto zset_db = redis::ZSet(storage_, namespace_); + std::vector member_scores; + + if (args.dontsort) { + RangeRankSpec spec; + spec.start = offset; + spec.stop = offset + count - 1; + spec.reversed = args.desc; + s = zset_db.RangeByRank(key, spec, &member_scores, nullptr); + if (!s.ok()) return s; + + for (auto &member_score : member_scores) { + str_vec.emplace_back(std::move(member_score.member)); + } + } else { + s = zset_db.GetAllMemberScores(key, &member_scores); + if (!s.ok()) return s; + + for (auto &member_score : member_scores) { + str_vec.emplace_back(std::move(member_score.member)); + } + } + } else { + *res = SortResult::UNKNOWN_TYPE; + return s; + } + } + + std::vector sort_vec(str_vec.size()); + for (size_t i = 0; i < str_vec.size(); ++i) { + sort_vec[i].obj = str_vec[i]; + } + + // Sort by BY, ALPHA, ASC/DESC + if (!args.dontsort) { + for (size_t i = 0; i < sort_vec.size(); ++i) { + std::string byval; + if (!args.sortby.empty()) { + auto lookup = lookupKeyByPattern(args.sortby, str_vec[i]); + if (!lookup.has_value()) continue; + byval = std::move(lookup.value()); + } else { + byval = str_vec[i]; + } + + if (args.alpha && !args.sortby.empty()) { + sort_vec[i].v = byval; + } else if (!args.alpha && !byval.empty()) { + auto double_byval = ParseFloat(byval); + if (!double_byval) { + *res = SortResult::DOUBLE_CONVERT_ERROR; + return rocksdb::Status::OK(); + } + sort_vec[i].v = *double_byval; + } + } + + std::sort(sort_vec.begin(), sort_vec.end(), [&args](const RedisSortObject &a, const RedisSortObject &b) { + return RedisSortObject::SortCompare(a, b, args); + }); + + // Gets the element specified by Limit + if (offset != 0 || count != vectorlen) { + sort_vec = std::vector(std::make_move_iterator(sort_vec.begin() + offset), + std::make_move_iterator(sort_vec.begin() + offset + count)); + } + } + + // Perform storage + for (auto &elem : sort_vec) { + if (args.getpatterns.empty()) { + elems->emplace_back(elem.obj); + } + for (const std::string &pattern : args.getpatterns) { + std::optional val = lookupKeyByPattern(pattern, elem.obj); + if (val.has_value()) { + elems->emplace_back(val.value()); + } else { + elems->emplace_back(std::nullopt); + } + } + } + + if (!args.storekey.empty()) { + std::vector store_elems; + store_elems.reserve(elems->size()); + for (const auto &e : *elems) { + store_elems.emplace_back(e.value_or("")); + } + redis::List list_db(storage_, namespace_); + s = list_db.Trim(args.storekey, -1, 0); + if (!s.ok()) return s; + uint64_t new_size = 0; + s = list_db.Push(args.storekey, std::vector(store_elems.cbegin(), store_elems.cend()), false, &new_size); + if (!s.ok()) return s; + } + + return rocksdb::Status::OK(); +} + +bool RedisSortObject::SortCompare(const RedisSortObject &a, const RedisSortObject &b, const SortArgument &args) { + if (!args.alpha) { + double score_a = std::get(a.v); + double score_b = std::get(b.v); + return !args.desc ? score_a < score_b : score_a > score_b; + } else { + if (!args.sortby.empty()) { + std::string cmp_a = std::get(a.v); + std::string cmp_b = std::get(b.v); + return !args.desc ? cmp_a < cmp_b : cmp_a > cmp_b; + } else { + return !args.desc ? a.obj < b.obj : a.obj > b.obj; + } + } +} + } // namespace redis diff --git a/src/storage/redis_db.h b/src/storage/redis_db.h index 73a5a6545c0..84579b107fd 100644 --- a/src/storage/redis_db.h +++ b/src/storage/redis_db.h @@ -21,15 +21,52 @@ #pragma once #include +#include #include #include +#include #include #include "redis_metadata.h" +#include "server/redis_reply.h" #include "storage.h" namespace redis { +/// SORT_LENGTH_LIMIT limits the number of elements to be sorted +/// to avoid using too much memory and causing system crashes. +/// TODO: Expect to expand or eliminate SORT_LENGTH_LIMIT +/// through better mechanisms such as memory restriction logic. +constexpr uint64_t SORT_LENGTH_LIMIT = 512; + +struct SortArgument { + std::string sortby; // BY + bool dontsort = false; // DONT SORT + int offset = 0; // LIMIT OFFSET + int count = -1; // LIMIT COUNT + std::vector getpatterns; // GET + bool desc = false; // ASC/DESC + bool alpha = false; // ALPHA + std::string storekey; // STORE +}; + +struct RedisSortObject { + std::string obj; + std::variant v; + + /// SortCompare is a helper function that enables `RedisSortObject` to be sorted based on `SortArgument`. + /// + /// It can assist in implementing the third parameter `Compare comp` required by `std::sort` + /// + /// \param args The basis used to compare two RedisSortObjects. + /// If `args.alpha` is false, `RedisSortObject.v` will be taken as double for comparison + /// If `args.alpha` is true and `args.sortby` is not empty, `RedisSortObject.v` will be taken as string for comparison + /// If `args.alpha` is true and `args.sortby` is empty, the comparison is by `RedisSortObject.obj`. + /// + /// \return If `desc` is false, returns true when `a < b`, otherwise returns true when `a > b` + static bool SortCompare(const RedisSortObject &a, const RedisSortObject &b, const SortArgument &args); +}; + /// Database is a wrapper of underlying storage engine, it provides /// some common operations for redis commands. class Database { @@ -107,6 +144,17 @@ class Database { enum class CopyResult { KEY_NOT_EXIST, KEY_ALREADY_EXIST, DONE }; [[nodiscard]] rocksdb::Status Copy(const std::string &key, const std::string &new_key, bool nx, bool delete_old, CopyResult *res); + enum class SortResult { UNKNOWN_TYPE, DOUBLE_CONVERT_ERROR, LIMIT_EXCEEDED, DONE }; + /// Sort sorts keys of the specified type according to SortArgument + /// + /// \param type is the type of sort key, which must be LIST, SET or ZSET + /// \param key is to be sorted + /// \param args provide the parameters to sort by + /// \param elems contain the sorted results + /// \param res represents the sorted result type. + /// When status is not ok, `res` should not been checked, otherwise it should be checked whether `res` is `DONE` + [[nodiscard]] rocksdb::Status Sort(RedisType type, const std::string &key, const SortArgument &args, + std::vector> *elems, SortResult *res); protected: engine::Storage *storage_; @@ -119,6 +167,21 @@ class Database { // Already internal keys [[nodiscard]] rocksdb::Status existsInternal(const std::vector &keys, int *ret); [[nodiscard]] rocksdb::Status typeInternal(const Slice &key, RedisType *type); + + /// lookupKeyByPattern is a helper function of `Sort` to support `GET` and `BY` fields. + /// + /// \param pattern can be the value of a `BY` or `GET` field + /// \param subst is used to replace the "*" or "#" matched in the pattern string. + /// \return Returns the value associated to the key with a name obtained using the following rules: + /// 1) The first occurrence of '*' in 'pattern' is substituted with 'subst'. + /// 2) If 'pattern' matches the "->" string, everything on the left of + /// the arrow is treated as the name of a hash field, and the part on the + /// left as the key name containing a hash. The value of the specified + /// field is returned. + /// 3) If 'pattern' equals "#", the function simply returns 'subst' itself so + /// that the SORT command can be used like: SORT key GET # to retrieve + /// the Set/List elements directly. + std::optional lookupKeyByPattern(const std::string &pattern, const std::string &subst); }; class LatestSnapShot { public: diff --git a/tests/gocase/unit/sort/sort_test.go b/tests/gocase/unit/sort/sort_test.go new file mode 100644 index 00000000000..6715ed783a0 --- /dev/null +++ b/tests/gocase/unit/sort/sort_test.go @@ -0,0 +1,881 @@ +/* + * 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. + */ + +package sort + +import ( + "context" + "fmt" + "testing" + + "github.com/redis/go-redis/v9" + + "github.com/apache/kvrocks/tests/gocase/util" + "github.com/stretchr/testify/require" +) + +func TestSortParser(t *testing.T) { + srv := util.StartServer(t, map[string]string{}) + defer srv.Close() + + ctx := context.Background() + rdb := srv.NewClient() + defer func() { require.NoError(t, rdb.Close()) }() + + t.Run("SORT Parser", func(t *testing.T) { + rdb.RPush(ctx, "bad-case-key", 5, 4, 3, 2, 1) + + _, err := rdb.Do(ctx, "Sort").Result() + require.EqualError(t, err, "ERR wrong number of arguments") + + _, err = rdb.Do(ctx, "Sort", "bad-case-key", "BadArg").Result() + require.EqualError(t, err, "ERR syntax error") + + _, err = rdb.Do(ctx, "Sort", "bad-case-key", "LIMIT").Result() + require.EqualError(t, err, "ERR no more item to parse") + + _, err = rdb.Do(ctx, "Sort", "bad-case-key", "LIMIT", 1).Result() + require.EqualError(t, err, "ERR no more item to parse") + + _, err = rdb.Do(ctx, "Sort", "bad-case-key", "LIMIT", 1, "not-number").Result() + require.EqualError(t, err, "ERR not started as an integer") + + _, err = rdb.Do(ctx, "Sort", "bad-case-key", "STORE").Result() + require.EqualError(t, err, "ERR no more item to parse") + + rdb.MSet(ctx, "rank_1", 1, "rank_2", "rank_3", 3, "rank_4", 4, "rank_5", 5) + _, err = rdb.Do(ctx, "Sort", "bad-case-key", "BY", "dontsort", "BY", "rank_*").Result() + require.EqualError(t, err, "ERR don't use multiple BY parameters") + + _, err = rdb.Do(ctx, "Sort_RO", "bad-case-key", "STORE", "store_ro_key").Result() + require.EqualError(t, err, "ERR SORT_RO is read-only and does not support the STORE parameter") + }) +} + +func TestSortLengthLimit(t *testing.T) { + srv := util.StartServer(t, map[string]string{}) + defer srv.Close() + + ctx := context.Background() + rdb := srv.NewClient() + defer func() { require.NoError(t, rdb.Close()) }() + + t.Run("SORT Length Limit", func(t *testing.T) { + for i := 0; i <= 512; i++ { + rdb.LPush(ctx, "many-list-elems-key", i) + } + _, err := rdb.Sort(ctx, "many-list-elems-key", &redis.Sort{}).Result() + require.EqualError(t, err, "The number of elements to be sorted exceeds SORT_LENGTH_LIMIT = 512") + + for i := 0; i <= 512; i++ { + rdb.SAdd(ctx, "many-set-elems-key", i) + } + _, err = rdb.Sort(ctx, "many-set-elems-key", &redis.Sort{}).Result() + require.EqualError(t, err, "The number of elements to be sorted exceeds SORT_LENGTH_LIMIT = 512") + + for i := 0; i <= 512; i++ { + rdb.ZAdd(ctx, "many-zset-elems-key", redis.Z{Score: float64(i), Member: fmt.Sprintf("%d", i)}) + } + _, err = rdb.Sort(ctx, "many-zset-elems-key", &redis.Sort{}).Result() + require.EqualError(t, err, "The number of elements to be sorted exceeds SORT_LENGTH_LIMIT = 512") + }) +} + +func TestListSort(t *testing.T) { + srv := util.StartServer(t, map[string]string{}) + defer srv.Close() + + ctx := context.Background() + rdb := srv.NewClient() + defer func() { require.NoError(t, rdb.Close()) }() + + t.Run("SORT Basic", func(t *testing.T) { + rdb.LPush(ctx, "today_cost", 30, 1.5, 10, 8) + + sortResult, err := rdb.Sort(ctx, "today_cost", &redis.Sort{}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "today_cost", &redis.Sort{Order: "ASC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "today_cost", &redis.Sort{Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult) + + sortResult, err = rdb.SortRO(ctx, "today_cost", &redis.Sort{Order: "ASC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult) + + sortResult, err = rdb.SortRO(ctx, "today_cost", &redis.Sort{Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult) + }) + + t.Run("SORT ALPHA", func(t *testing.T) { + rdb.LPush(ctx, "website", "www.reddit.com", "www.slashdot.com", "www.infoq.com") + + sortResult, err := rdb.Sort(ctx, "website", &redis.Sort{Alpha: true}).Result() + require.NoError(t, err) + require.Equal(t, []string{"www.infoq.com", "www.reddit.com", "www.slashdot.com"}, sortResult) + + _, err = rdb.Sort(ctx, "website", &redis.Sort{Alpha: false}).Result() + require.EqualError(t, err, "One or more scores can't be converted into double") + }) + + t.Run("SORT LIMIT", func(t *testing.T) { + rdb.RPush(ctx, "rank", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10) + + sortResult, err := rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0, Count: 5}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0, Count: 5, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"10", "9", "8", "7", "6"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 11, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: 2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 11}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: -1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: -2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + }) + + t.Run("SORT BY + GET", func(t *testing.T) { + rdb.LPush(ctx, "uid", 1, 2, 3, 4) + rdb.MSet(ctx, "user_name_1", "admin", "user_name_2", "jack", "user_name_3", "peter", "user_name_4", "mary") + rdb.MSet(ctx, "user_level_1", 9999, "user_level_2", 10, "user_level_3", 25, "user_level_4", 70) + + sortResult, err := rdb.Sort(ctx, "uid", &redis.Sort{By: "user_level_*"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"admin", "jack", "peter", "mary"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_level_*", Get: []string{"user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"jack", "peter", "mary", "admin"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"9999", "admin", "10", "jack", "25", "peter", "70", "mary"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"#", "user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "9999", "admin", "2", "10", "jack", "3", "25", "peter", "4", "70", "mary"}, sortResult) + + // not sorted + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4", "3", "2", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 0, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: 2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"3", "2"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: -1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"3", "2", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 0, Count: 1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: 2, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 0, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: -1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Get: []string{"#", "user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4", "70", "mary", "3", "25", "peter", "2", "10", "jack", "1", "9999", "admin"}, sortResult) + + // pattern with hash tag + rdb.HMSet(ctx, "user_info_1", "name", "admin", "level", 9999) + rdb.HMSet(ctx, "user_info_2", "name", "jack", "level", 10) + rdb.HMSet(ctx, "user_info_3", "name", "peter", "level", 25) + rdb.HMSet(ctx, "user_info_4", "name", "mary", "level", 70) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_info_*->level"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_info_*->level", Get: []string{"user_info_*->name"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"jack", "peter", "mary", "admin"}, sortResult) + + // get/by empty and nil + rdb.LPush(ctx, "uid_empty_nil", 4, 5, 6) + rdb.MSet(ctx, "user_name_5", "tom", "user_level_5", -1) + + getResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "Get", "user_name_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"mary", "tom", nil}, getResult) + byResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "By", "user_level_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"5", "6", "4"}, byResult) + + rdb.MSet(ctx, "user_name_6", "", "user_level_6", "") + + getResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "Get", "user_name_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"mary", "tom", ""}, getResult) + + byResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "By", "user_level_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"5", "6", "4"}, byResult) + }) + + t.Run("SORT STORE", func(t *testing.T) { + rdb.RPush(ctx, "numbers", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10) + + storedLen, err := rdb.Do(ctx, "Sort", "numbers", "STORE", "sorted-numbers").Result() + require.NoError(t, err) + require.Equal(t, int64(10), storedLen) + + sortResult, err := rdb.LRange(ctx, "sorted-numbers", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + rdb.LPush(ctx, "no-force-alpha-sort-key", 123, 3, 21) + storedLen, err = rdb.Do(ctx, "Sort", "no-force-alpha-sort-key", "BY", "not-exists-key", "STORE", "no-alpha-sorted").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "no-alpha-sorted", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"21", "3", "123"}, sortResult) + + // get empty and nil + rdb.LPush(ctx, "uid_get_empty_nil", 4, 5, 6) + rdb.MSet(ctx, "user_name_4", "mary", "user_level_4", 70, "user_name_5", "tom", "user_level_5", -1) + + storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil", "Get", "user_name_*", "Store", "get_empty_nil_store").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"mary", "tom", ""}, sortResult) + + rdb.MSet(ctx, "user_name_6", "", "user_level_6", "") + storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil", "Get", "user_name_*", "Store", "get_empty_nil_store").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"mary", "tom", ""}, sortResult) + }) +} + +func TestSetSort(t *testing.T) { + srv := util.StartServer(t, map[string]string{}) + defer srv.Close() + + ctx := context.Background() + rdb := srv.NewClient() + defer func() { require.NoError(t, rdb.Close()) }() + + t.Run("SORT Basic", func(t *testing.T) { + rdb.SAdd(ctx, "today_cost", 30, 1.5, 10, 8) + + sortResult, err := rdb.Sort(ctx, "today_cost", &redis.Sort{}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "today_cost", &redis.Sort{Order: "ASC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "today_cost", &redis.Sort{Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult) + + sortResult, err = rdb.SortRO(ctx, "today_cost", &redis.Sort{Order: "ASC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1.5", "8", "10", "30"}, sortResult) + + sortResult, err = rdb.SortRO(ctx, "today_cost", &redis.Sort{Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"30", "10", "8", "1.5"}, sortResult) + }) + + t.Run("SORT ALPHA", func(t *testing.T) { + rdb.SAdd(ctx, "website", "www.reddit.com", "www.slashdot.com", "www.infoq.com") + + sortResult, err := rdb.Sort(ctx, "website", &redis.Sort{Alpha: true}).Result() + require.NoError(t, err) + require.Equal(t, []string{"www.infoq.com", "www.reddit.com", "www.slashdot.com"}, sortResult) + + _, err = rdb.Sort(ctx, "website", &redis.Sort{Alpha: false}).Result() + require.EqualError(t, err, "One or more scores can't be converted into double") + }) + + t.Run("SORT LIMIT", func(t *testing.T) { + rdb.SAdd(ctx, "rank", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10) + + sortResult, err := rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0, Count: 5}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0, Count: 5, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"10", "9", "8", "7", "6"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 11, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: 2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 11}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: -1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: -2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + }) + + t.Run("SORT BY + GET", func(t *testing.T) { + rdb.SAdd(ctx, "uid", 4, 3, 2, 1) + rdb.MSet(ctx, "user_name_1", "admin", "user_name_2", "jack", "user_name_3", "peter", "user_name_4", "mary") + rdb.MSet(ctx, "user_level_1", 9999, "user_level_2", 10, "user_level_3", 25, "user_level_4", 70) + + sortResult, err := rdb.Sort(ctx, "uid", &redis.Sort{By: "user_level_*"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"admin", "jack", "peter", "mary"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_level_*", Get: []string{"user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"jack", "peter", "mary", "admin"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"9999", "admin", "10", "jack", "25", "peter", "70", "mary"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"#", "user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "9999", "admin", "2", "10", "jack", "3", "25", "peter", "4", "70", "mary"}, sortResult) + + // not sorted + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 0, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: 2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: -1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 0, Count: 1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: 2, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 0, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: -1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Get: []string{"#", "user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "9999", "admin", "2", "10", "jack", "3", "25", "peter", "4", "70", "mary"}, sortResult) + + // pattern with hash tag + rdb.HMSet(ctx, "user_info_1", "name", "admin", "level", 9999) + rdb.HMSet(ctx, "user_info_2", "name", "jack", "level", 10) + rdb.HMSet(ctx, "user_info_3", "name", "peter", "level", 25) + rdb.HMSet(ctx, "user_info_4", "name", "mary", "level", 70) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_info_*->level"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_info_*->level", Get: []string{"user_info_*->name"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"jack", "peter", "mary", "admin"}, sortResult) + + // get/by empty and nil + rdb.SAdd(ctx, "uid_empty_nil", 4, 5, 6) + rdb.MSet(ctx, "user_name_5", "tom", "user_level_5", -1) + + getResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "Get", "user_name_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"mary", "tom", nil}, getResult) + byResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "By", "user_level_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"5", "6", "4"}, byResult) + + rdb.MSet(ctx, "user_name_6", "", "user_level_6", "") + + getResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "Get", "user_name_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"mary", "tom", ""}, getResult) + + byResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "By", "user_level_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"5", "6", "4"}, byResult) + + }) + + t.Run("SORT STORE", func(t *testing.T) { + rdb.SAdd(ctx, "numbers", 1, 3, 5, 7, 9, 2, 4, 6, 8, 10) + + storedLen, err := rdb.Do(ctx, "Sort", "numbers", "STORE", "sorted-numbers").Result() + require.NoError(t, err) + require.Equal(t, int64(10), storedLen) + + sortResult, err := rdb.LRange(ctx, "sorted-numbers", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + rdb.SAdd(ctx, "force-alpha-sort-key", 123, 3, 21) + storedLen, err = rdb.Do(ctx, "Sort", "force-alpha-sort-key", "BY", "not-exists-key", "STORE", "alpha-sorted").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "alpha-sorted", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"123", "21", "3"}, sortResult) + + // get empty and nil + rdb.SAdd(ctx, "uid_get_empty_nil", 4, 5, 6) + rdb.MSet(ctx, "user_name_4", "mary", "user_level_4", 70, "user_name_5", "tom", "user_level_5", -1) + + storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil", "Get", "user_name_*", "Store", "get_empty_nil_store").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"mary", "tom", ""}, sortResult) + + rdb.MSet(ctx, "user_name_6", "", "user_level_6", "") + storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil", "Get", "user_name_*", "Store", "get_empty_nil_store").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"mary", "tom", ""}, sortResult) + }) +} + +func TestZSetSort(t *testing.T) { + srv := util.StartServer(t, map[string]string{}) + defer srv.Close() + + ctx := context.Background() + rdb := srv.NewClient() + defer func() { require.NoError(t, rdb.Close()) }() + + t.Run("SORT Basic", func(t *testing.T) { + rdb.ZAdd(ctx, "today_cost", redis.Z{Score: 30, Member: "1"}, redis.Z{Score: 1.5, Member: "2"}, redis.Z{Score: 10, Member: "3"}, redis.Z{Score: 8, Member: "4"}) + + sortResult, err := rdb.Sort(ctx, "today_cost", &redis.Sort{}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "today_cost", &redis.Sort{Order: "ASC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "today_cost", &redis.Sort{Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4", "3", "2", "1"}, sortResult) + + sortResult, err = rdb.SortRO(ctx, "today_cost", &redis.Sort{Order: "ASC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.SortRO(ctx, "today_cost", &redis.Sort{Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4", "3", "2", "1"}, sortResult) + }) + + t.Run("SORT ALPHA", func(t *testing.T) { + rdb.ZAdd(ctx, "website", redis.Z{Score: 1, Member: "www.reddit.com"}, redis.Z{Score: 2, Member: "www.slashdot.com"}, redis.Z{Score: 3, Member: "www.infoq.com"}) + + sortResult, err := rdb.Sort(ctx, "website", &redis.Sort{Alpha: true}).Result() + require.NoError(t, err) + require.Equal(t, []string{"www.infoq.com", "www.reddit.com", "www.slashdot.com"}, sortResult) + + _, err = rdb.Sort(ctx, "website", &redis.Sort{Alpha: false}).Result() + require.EqualError(t, err, "One or more scores can't be converted into double") + }) + + t.Run("SORT LIMIT", func(t *testing.T) { + rdb.ZAdd(ctx, "rank", + redis.Z{Score: 1, Member: "1"}, + redis.Z{Score: 2, Member: "3"}, + redis.Z{Score: 3, Member: "5"}, + redis.Z{Score: 4, Member: "7"}, + redis.Z{Score: 5, Member: "9"}, + redis.Z{Score: 6, Member: "2"}, + redis.Z{Score: 7, Member: "4"}, + redis.Z{Score: 8, Member: "6"}, + redis.Z{Score: 9, Member: "8"}, + redis.Z{Score: 10, Member: "10"}, + ) + + sortResult, err := rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0, Count: 5}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 0, Count: 5, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"10", "9", "8", "7", "6"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 10, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: 11, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: 2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -1, Count: 11}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: -1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "rank", &redis.Sort{Offset: -2, Count: -2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + }) + + t.Run("SORT BY + GET", func(t *testing.T) { + rdb.ZAdd(ctx, "uid", + redis.Z{Score: 1, Member: "4"}, + redis.Z{Score: 2, Member: "3"}, + redis.Z{Score: 3, Member: "2"}, + redis.Z{Score: 4, Member: "1"}) + + rdb.MSet(ctx, "user_name_1", "admin", "user_name_2", "jack", "user_name_3", "peter", "user_name_4", "mary") + rdb.MSet(ctx, "user_level_1", 9999, "user_level_2", 10, "user_level_3", 25, "user_level_4", 70) + + sortResult, err := rdb.Sort(ctx, "uid", &redis.Sort{By: "user_level_*"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"admin", "jack", "peter", "mary"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_level_*", Get: []string{"user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"jack", "peter", "mary", "admin"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"9999", "admin", "10", "jack", "25", "peter", "70", "mary"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{Get: []string{"#", "user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "9999", "admin", "2", "10", "jack", "3", "25", "peter", "4", "70", "mary"}, sortResult) + + // not sorted + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4", "3", "2", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 0, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: 2}).Result() + require.NoError(t, err) + require.Equal(t, []string{"3", "2"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 1}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 0}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: -1}).Result() + require.NoError(t, err) + require.Equal(t, []string{"3", "2", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 0, Count: 1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: 2, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 4, Count: 0, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Offset: 1, Count: -1, Order: "DESC"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "not-exists-key", Get: []string{"#", "user_level_*", "user_name_*"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"4", "70", "mary", "3", "25", "peter", "2", "10", "jack", "1", "9999", "admin"}, sortResult) + + // pattern with hash tag + rdb.HMSet(ctx, "user_info_1", "name", "admin", "level", 9999) + rdb.HMSet(ctx, "user_info_2", "name", "jack", "level", 10) + rdb.HMSet(ctx, "user_info_3", "name", "peter", "level", 25) + rdb.HMSet(ctx, "user_info_4", "name", "mary", "level", 70) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_info_*->level"}).Result() + require.NoError(t, err) + require.Equal(t, []string{"2", "3", "4", "1"}, sortResult) + + sortResult, err = rdb.Sort(ctx, "uid", &redis.Sort{By: "user_info_*->level", Get: []string{"user_info_*->name"}}).Result() + require.NoError(t, err) + require.Equal(t, []string{"jack", "peter", "mary", "admin"}, sortResult) + + // get/by empty and nil + rdb.ZAdd(ctx, "uid_empty_nil", + redis.Z{Score: 4, Member: "6"}, + redis.Z{Score: 5, Member: "5"}, + redis.Z{Score: 6, Member: "4"}) + rdb.MSet(ctx, "user_name_5", "tom", "user_level_5", -1) + + getResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "Get", "user_name_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"mary", "tom", nil}, getResult) + byResult, err := rdb.Do(ctx, "Sort", "uid_empty_nil", "By", "user_level_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"5", "6", "4"}, byResult) + + rdb.MSet(ctx, "user_name_6", "", "user_level_6", "") + + getResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "Get", "user_name_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"mary", "tom", ""}, getResult) + + byResult, err = rdb.Do(ctx, "Sort", "uid_empty_nil", "By", "user_level_*").Slice() + require.NoError(t, err) + require.Equal(t, []interface{}{"5", "6", "4"}, byResult) + }) + + t.Run("SORT STORE", func(t *testing.T) { + rdb.ZAdd(ctx, "numbers", + redis.Z{Score: 1, Member: "1"}, + redis.Z{Score: 2, Member: "3"}, + redis.Z{Score: 3, Member: "5"}, + redis.Z{Score: 4, Member: "7"}, + redis.Z{Score: 5, Member: "9"}, + redis.Z{Score: 6, Member: "2"}, + redis.Z{Score: 7, Member: "4"}, + redis.Z{Score: 8, Member: "6"}, + redis.Z{Score: 9, Member: "8"}, + redis.Z{Score: 10, Member: "10"}, + ) + + storedLen, err := rdb.Do(ctx, "Sort", "numbers", "STORE", "sorted-numbers").Result() + require.NoError(t, err) + require.Equal(t, int64(10), storedLen) + + sortResult, err := rdb.LRange(ctx, "sorted-numbers", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, sortResult) + + rdb.ZAdd(ctx, "no-force-alpha-sort-key", + redis.Z{Score: 1, Member: "123"}, + redis.Z{Score: 2, Member: "3"}, + redis.Z{Score: 3, Member: "21"}, + ) + + storedLen, err = rdb.Do(ctx, "Sort", "no-force-alpha-sort-key", "BY", "not-exists-key", "STORE", "no-alpha-sorted").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "no-alpha-sorted", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"123", "3", "21"}, sortResult) + + // get empty and nil + rdb.ZAdd(ctx, "uid_get_empty_nil", + redis.Z{Score: 4, Member: "6"}, + redis.Z{Score: 5, Member: "5"}, + redis.Z{Score: 6, Member: "4"}) + rdb.MSet(ctx, "user_name_4", "mary", "user_level_4", 70, "user_name_5", "tom", "user_level_5", -1) + + storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil", "Get", "user_name_*", "Store", "get_empty_nil_store").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"mary", "tom", ""}, sortResult) + + rdb.MSet(ctx, "user_name_6", "", "user_level_6", "") + storedLen, err = rdb.Do(ctx, "Sort", "uid_get_empty_nil", "Get", "user_name_*", "Store", "get_empty_nil_store").Result() + require.NoError(t, err) + require.Equal(t, int64(3), storedLen) + + sortResult, err = rdb.LRange(ctx, "get_empty_nil_store", 0, -1).Result() + require.NoError(t, err) + require.Equal(t, []string{"mary", "tom", ""}, sortResult) + }) +} From d0b13bd1fd7c0b5c13868dbfffc114304c74f720 Mon Sep 17 00:00:00 2001 From: hulk Date: Tue, 7 May 2024 21:48:41 +0800 Subject: [PATCH 11/15] Remove the performance benchmark result section in README.md (#2299) --- README.md | 35 ----------------------------------- assets/chart-commands.png | Bin 33058 -> 0 bytes assets/chart-threads.png | Bin 44500 -> 0 bytes assets/chart-values.png | Bin 37571 -> 0 bytes 4 files changed, 35 deletions(-) delete mode 100644 assets/chart-commands.png delete mode 100644 assets/chart-threads.png delete mode 100644 assets/chart-values.png diff --git a/README.md b/README.md index 582bc082e52..e33cdabb29a 100644 --- a/README.md +++ b/README.md @@ -183,41 +183,6 @@ Documents are hosted at the [official website](https://kvrocks.apache.org/docs/g Kvrocks community welcomes all forms of contribution and you can find out how to get involved on the [Community](https://kvrocks.apache.org/community/) and [How to Contribute](https://kvrocks.apache.org/community/contributing) pages. -## Performance - -### Hardware - -* CPU: 48 cores Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz -* Memory: 32 GiB -* NET: Intel Corporation I350 Gigabit Network Connection -* DISK: 2TB NVMe Intel SSD DC P4600 - -> Benchmark Client: multi-thread redis-benchmark(unstable branch) - -### 1. Commands QPS - -> kvrocks: workers = 16, benchmark: 8 threads/ 512 conns / 128 payload - -latency: 99.9% < 10ms - -![image](assets/chart-commands.png) - -### 2. QPS on different payloads - -> kvrocks: workers = 16, benchmark: 8 threads/ 512 conns - -latency: 99.9% < 10ms - -![image](assets/chart-values.png) - -### 3. QPS on different workers - -> kvrocks: workers = 16, benchmark: 8 threads/ 512 conns / 128 payload - -latency: 99.9% < 10ms - -![image](assets/chart-threads.png) - ## License Apache Kvrocks is licensed under the Apache License Version 2.0. See the [LICENSE](LICENSE) file for details. diff --git a/assets/chart-commands.png b/assets/chart-commands.png deleted file mode 100644 index d9a250a633f36b8c794ec9172cea695a62ef6fc3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33058 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≷w&@Xy05gN79@VthW#B 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 diff --git a/assets/chart-threads.png b/assets/chart-threads.png deleted file mode 100644 index 591263664ede930fe0bf1108c6322c06b3eb0992..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44500 zcmeFZcQjnz8$QYyz4y)-Er{NGXG9mhM~M)4A-VFuc0Xu%w4Xpu2!Xjj0c$e(CvegbG{ zJGN+OQoudbp7|a6GQb~jJz-`(XlVH4sBd(%ynGs0Q(seUEh&2sH@-&>9(Im= zfo`5aYcw>OKq=ryH%H$`kU%$AcOR)hS?J#uQozrs%luHt-zL6KWud0pdJts~Z%2qI zp9r4-R1OaUfyj6}JeD$0QTyk1;5S+56JK9XDSrNdfB?P#VLlIUCw@UmNlAVIA$}nt zUZ4f9PmsIsqd;DFAJ%_$^6!3B9DVG)ojrY>J=`IvejnL+`1#60p{NJ_=j&hN^mTsx z-zT~I{PSAC3-Y7>!Y{}t!2h4Vfm>xzSEY2logINEqxzQK@CfE{Tx`)Sz8S@W=4Trs1#C;@Y5 zj^}LUsU4XGS?ZM;Ie99_CmPNB4FM|E4Lr7r6qwtLD#3^M-FhwyI-5d*4)`oq-nsdF zTlT)XTHLCjFq-unjsz$z@?Rec(Fa!F(KH!m&; z|NSfKDohBZQ=dstZt~-Q?nbr3{(q^Jf@pj%Y59GeL92JArKMYvdx7r)-}L8<@<(*7 z4DJSAtJbhKOm-b?xMgrQbQhHVF!FFeT5R(m{$y0Sc~jeTMZU;x?|wZM+@^jeO{dMizG6H^YHMc~)uQo5N+C3~vF)O3@~n)k zE?z<|@pLmsye^nm&g{1N>Spg>FRK@fBhkk#yVt!%riPYD&Z$f<<~riu;J?4HCY6EX z)f0YuRhaRn(x~deM4o8VxSmJXo%Tzy(N?#`#zcmo4L2w8{BhMTrcC_lDa%SOUy7K? zg<@h+ml?##$qD1md{bpLud7mr`*Md~<_}WN zip@XYUwVMvU+=fA5;=dF$>Ft7TD_2Npeb5=-@KPih_m9n=t6Z%-5iSJb`iqg>FEu>sOhHHn|G7t?(fXUGU#c^R zLJqqH=7rIP*2JHDR0uzftvuZs4&VW@q8=;rH=8 z;%0-n(Q~a|p&5&ij_pT5z$?Q#Ygq^ORn~MFvIJ7nl&vwIIs#KuM*r51eXTCctIpt4 zm&+mkaMD-kF@!?Hm&6el%DqWGw^yeaw4i}(9+L+f?^Bg25nm;ag!g)Gjy2nF&lXF= zuFuwJGv<%=#o-MfUddYA-TaV`ck2FX9WvVN{8?%7r0>f4d7M&%a%gDKR!d;gmJzY@ zQKATe4ltp4Wr1C+U9%}xK2dAG1p-4`2E8R??tz^SPjC&FN+>M zjK|Bsq`@5oyvYAdMcfH*cHSJ1JO29CXHaF#Yu4hD9eQ&#+!%PY9*S$Mf9<^GD%6H6 z7Vh!&vQM|2>*1S`asD0#Pg?o#E{1?5uj#I!-+C!`@>f4g9?AH8|B&G`C425PyEU6O zG<^MD_~$~)Q-4O13g++&#XQlcIjKDw4TvW!4T@KWH4AAr4~+wWg~dq3^Z~JL)^;A? zeMPNLQ>ly$=dr;-BnA=Arj)~U^sM7^^R9x8?ZJ83uSX4Dx+6T3JG%Fod(vG-x{0QB zr1zT@lr|izt$D^@yj@z~2)bDS+{yzcIAc%ygLkqXeWWm2kWJ{3D#Qe4_1x0}c*W&2 zT+Vz*#O1inCNfJWb4u=7i%9Bc(VhnG@l2U+A!My_^!8gcF;QmK>YuxF?-7>x7!A5n z8a9II*ygdBG!gHein7OTjz|`1=vr~nErVR>TZX`I@cFE_6+*kSRn#sj%fo|7F{$-U z8aat{-byy-s}T|HS36bFo0B+Ho7jz`7kV{A!-{4&6Yo>$l^J2t_Xjt$>D1^l?;X=C z7WKetdQ(M?p}Z;u>S3^#Blx7r*y}VF!)c5I!_yFZlE?j@)R}LaykUE-?#tZyH97*w zcNa}fS2WmRU{<91%#`fyMt8jzgtx?6$$c&>oj6FVQ7tH|)hr}`V=CnxX*s#fms+dq zCZQS4p5Cb|W$O?RWz2BY7Bf)n#Wk+zJxI-=0EsCIlFFTYR0dtXJzIE+(QXHgVk@$9 z`BWTh=`$g!n0X4!jmg=P&y>LY!&k&xEEwfjFK7TKMPnF4Eb~hN@$+NEOSF)apBb`> z7C{`pBv>>UJoK+FH?wrH?6A2MvlE-K?9kdnwz5s4DleEu#y>%g^Gg-V2Ru}=4T63^zZ0b*je zqFUvS(M}~v7)vI{NY^ZeAZQ#|V)-uV!Oz&QNKKU!Xt2>UV>S`*QfY-b5S$}-W=;i^ zV|rgeLHdq8C)1kf;yZ$wet_fDJU>_tfVdh@P65#oK?nB!qFMGv)?Cx5Y z%tKwr^Q^m)mFt_|Qy3bS4R*2lVQ2G=uJetMY~=;j`b6oTa6PKoWmRe`3fJr$J}Wwo zpuHRjxz-E`6)Tr9Cm9o)B_cU+gy)rvniR`J7$%D%!ip`CgdE4>rI?$N-{&%2VsJl9 zxr?xt&?%(Wzcom1J{I!@9|J^UVDWr??Gc$9)y8&wvp#|Nlc86}H zienw8Ptd!Vt?PLxd$Q=OH9|^Gu5}}>9*Q+63yr6vuc#6O)2G*g^A=p?dvyLKD+Mi! zm7Ed@k0kBXz)5Jc{R+dz(Q2i7-U~s4iH*eXBxw-gg5kI+AFE92isY29j`y0Tur#KC zaKeaul%L^BTCKarv1{u!wC%Lm>b7-veMroPUaSzAZ~HNQP1c}v>HVwOSkEZ%eVcbO zDT%nu&P>RDkRJpS%=}QA)?e-Io~z^D$|Tj)3yJqSJ$fyzuJZKq7a63C zn!!CyNx35u-Spufe%kaTE=Z#vg%RN+SdV2aJd^AAn!TVbQ7VTWyS4MP&~4@Y4?glg z@_iaNSoFUo7~%`5K@I?GAyAnXYP1D)3XDa0#S<|qk&y|$LNCPUdkg7`*o^&P>83VWlM*bRi^<34QWk=?o;CgaYAj$f}r4semFSM{lv6CUBLfZb&F{JA~s(_`o# z7{qA^`8^@7+;XYy{UyEsEK7=o1@_9cK)pzJNx1KG#KYm(K^YA2cnz01!H4MiI{r)jU>?EK-_Mn#o5&p|nLOBn|OJ=X2FOE1wa;D;hlx#dbs$h+o z`ve7ApV5H$`nUz3bgxgV^m3?YY0V>@!x}?0+eQW~g3UELQ}Hkw9NqQh%_-dH(KRjqcudL!aahyc=KwY( zt!7NZ6nG*t7aq5irEt+%)V<<2b_U`ariwcs-13jSb0+87TZMxNh--NZQwb9%s?C}} zK}EVS8Wxe0nj&-%_NA)SW4M^vXu>n0Hj-S+Pn-*Cl#8XBxeB56(Zw?Uj^E;%@iLz} zB%FiD(hTuIvB92wQJ9L}sv)+z^}+hN2E_1y)JP<8C;u@GABmlDq};`~=trVFQ|YtS zo2;n;;&tbIhCA2*9>1p#&bQ6J*JT~#%wkJ^FN>LHEQMlz)4|C#0Vk(0FeTILdx8j_ zGgX$50lINIsq6H*t4f>(U2&9e4DlxRD0UVXH$*#|HHIK&Q2FZld-Dt%uJ-Uw61d69>bkco*C^!T82Idi!2;t#U>Scq# ztOaBqmoC3>dPNJ<4JBh=WjNg*jF~_>i3#{-X{3&3Xdu$Hz4LLkJgJGLi7#wlLYU>QONNMIb-1+7@l>H!N` zwg2WTk+GsTExUo;msS?tms^Qt?B9P}7OxNZVFoHbKgL5mG-g@H!YH(xn=lhtB2eE^ zC3|0~VQi}O#%+ry?qV*nfut`gyM%xl8B~2S`0JL-iUhuI{0JFhTvy15t`3pJG1Slz zwN7T$^Cz)YQlX2!1|jtUgwr$dK#=3{we5M7EG4?V13|l6F$Xa@Ju~Pi=dntpu%E)T zmYcF9hla6d_wPyR*VRF+#NxQ|gW+}~A5>hzHd8FnmM};lrdl0G@8;N^hOsewQB8$~ z!^PGw^+O&#xqWekHNeHwH%-0^wg;(TbVAv<5yB}WxF=jp)ir?(fpFHd2QyRMv4b}V zOiRNevaKL5oxQBn*&UcnmQHsfO9St8jk`2CHoC)7JX7$!w!VvkfDfQ;8ReN6@Bxe2 z46_Hd5vP9$cmP+(L(Hn2ET~TFc&r!`@XI^R!lk&*JM8%*Hxo}WxbTMP%DHbRX*esu z2`&W@&&dskazRZQzP7i-?tHeCF5*+}INL5O#BwH#>BZsR!kvN7yx)l>u>CL*$dVh4 zMTl2vv=sSs-%Q9k+s51+GErN66NHMOFp9o}T{6 zc?WWhNII8#fh`Vg%)R zHWB8pYX`c~$Zy@+mZ@f{a^%}u6YN8qkC=sN)Sbg_dkqxo(@bJJFUO)-F0#g zN*s6VI=TofK(Iy?v~%!Jp*fSQ;DNMP`_PZ0y`-lQPP&ox>q;1n%5pfg{_X&MQTI$I zU7f~O8ms)$dH7Dc+-WqO5#neX*gJ{w&D}t5XDuG%kjW7eNFwr!HP!ae!yYG> z=lrfH>d?pW@L=Qe?PN`1(2zygk&lc=_;gy}I|+`#XyUAQ6-{rki3jMisaUMl-QDJ& zk2;B&iM?Ao9v*)5^+;^}_5-i1wNsa+j9i=7;BI3;^wDn?(^X!Zn-78a?1N~U)~JRP zWlj=BYWEox2*<_kt~mB9<0RouiwZM#r+`xpc*uI`?X`{nN> z9@ur_>2)`1KCpm(EBfXwxL9%KRAPP6{3s0I=onaF(d^RfjM`xFf7F^WK|szKI~w?1 zOHGK*X`nhVp@|X)fF39q;DvQ2e1T?D+c?iO!Ft;Yje4Iw&;BDCvC*UF+OpH@*G>Ib zFyfX4LUZLwT_nbTnn{30)d2p|NP2qpUxBD-8raQM(>)-ff8;0B2hbs>>8RSwF5vF} ztv@|Tk0h^t@CQ!0i8^+pa%e7}RX<0c>xm|f@Z%I2fLS$;^L2tVo4hvEoM$VI z=0u*S7E$wCGssNUnYTQk6L~_iv$U(bP zD_fdZjZKf7>q+RmV=iX#utcJ`_xrcy(ZnpT9(xV?FzT|VDTE!Nwch}cPdQvnBF>d` zcsS~`Z>KG|z3pw_U0~Oic6G88SC)h|4}IO#*ws9+(A=EBe&PP%bFN=TgU;dF|M9i* z77F&VmFkzMH9i`^x&)Ldhsk`&#$l$A7#Wf>0R5S#Nvz|{H#TzW|-6 zUg1fCQeR!Gk|Sk3k6=R#p63nu{owB-hd}b1X*ePIaZF@GpTr<>pi!eodtGeYg7w2FdK8!1y0xSW#xS0Gc_NntKK?l0=AJMAAq8aYJcpP@M3~f92ANH z34l|4$uk4N7lx z3g{B-4_CUCEyQZsHFIK)lv6qa4*7tEC!GC|C(1)YW0;-Ap`8h6d|0?IGC0gG)59{I z=@RwykMakcTF)VjQocC1@mi<5U-@O|uhgDP2e{Lmzq>x5zrtbK{Ef1!Y-%_|Q1I4J zulfNF5{%Af0;_PwAIxN@%|&xZA0+7Usk%UQ`NvE2_u_Fy4=;1 z^5~DB;R)Tp9wL<`z z$Q9AA$j0I!>@-Q>Od@airZDWx4DteoUBj?}lFgJO5Y>t|Ou;`{g0@R@qsAxwWiJdlNkBpjuzeDo02JmnF0PFHBz&l z`*h>HuMC9FVb5X7CJsx+S+cJudSO~`s|c~0(|aL;OV16%RY9Nj>Br>+S7>zg%#aq2 zuwc!M#zefQb&9F(4nEENtRt-*M?gz<40V*+L?}o3VilvU!I32CGuRcB+{#D=|=xyKRiaK7h3|c56HNnmsl} z9F9>j*U$xJG&{qG(Ylx*H9fua*Bq<@q{51YLEm2}MuA^PMUvK!OC1f8V{W)L2@6ra znxzzaPo%a^#b-%_G=GD|HXezN0rq1wpqsIUWkP7R^F(mTs7Gk9_SE;eIfJEdHC5ZNOUCy2C}@HsM#zNCLCNcfsxD%#Ss=V(S;&@ zSYB_WOv?QGaayQy5o7WXCWEmtHBgKQ)5z1UYC@i;x9rP%bG@_Eyz)pKX%S;TCAm6a z$rXNSl|I=QlpA}<&s^I_(6aHguA9m@2+G@hRg;gR5=F<*Z(^p|=kwPi4`a~Cg~&aw z&-SB5z5`+bfs2f5PCc=bOB8w=t;J{Fb(Z`28ECJ=?G4hgk$hBI>rv?925g{xyU)?^1(&_0}8*ESv1wjj!iyUZk1ShqCko=(ijSbb4f#dmk zWZVp996~}sVY=0KdwTEKxNdgn#Sc)0T`b3x?@YP={N={wU)dOlIRL82pITP8dVoW~ zBqKpWlz?ME0dnExa7G$~aYi4yfZcf(o-ZYK{wz-R_sp=pRA!M?u!cx_okGAnz~BhY zQhaTvVy}q$K@}nEiJXn4IwXR7IrZ652n8tw8m)dqQ;W0HapMjVS)!MuUy}ieG&jg! z{{(}c3+aE7?2m(>8-S|(>@y4l(mC0ycV}&VaRjWsNSm_4@Zb?O$tx%LJ2pZTL;{Q5zHzcQpjT#696ZD@fYu4aDkN789Txj?(HhJ_?fl=i}426pD7ZTsOg zN=q`^`%L`mvdyY;IN2Iw)kHgqUo{JIhOg`$e}l$$imT^SKiPnsjpdzF(go=&*5w+) zubze#-M5d#Ayuab@3zg7A1dESIZSfO`&8dnY$FN4?Ts#*`f4XDg|>*-0-mv+z$Vqde)5aC|tc#(ib_u^~_n zrBTqM(q{BieIaZAO&U_-z!@;&@I>x0C$Z|8kt$@aqwp|*{EALUJ)?}Srg}pFOh^b< znYEwhK1=!s65_=IHmTIN^#a)M|H)L&Ndln6KP}_Oe>0arH+ratQdqu1!Iu93$^rno zJ^euwMD*XgJ;DD^otCQA<8|hD30(djfB8HokU9Q-PjjGLkN^@a>z@1#PG#bvQ@cy1n21_4I#7 z;2)uPjWB2i2(6&k5er#yD4~dc<|zC=pietZ*I2Z9x)oS<2e&Xp z+#Gq&=&O&$5z_ZRu2*xHyZXrrf41y7KnQ1(y}Q`(22^2G?yZSkZ>HOB2#;mIAD{nO z8l!aFE-EpYCVOUT*6v*@17xPWfuO7TW3pf?k@FSFj_mj6_Daj?vS-ip0q8p29TKp1 zwUI8L1Axbc4ilvCv=DyE(0>xvdS6Iv@0wFTkDITKJ&iDUQ1ANn5>VV>zrRmxeN-mS z20)FI{qO)I0KjLe%^m`fnUc+rt+T(HYcp7KR!S$=`UskPY#_r@RbCnml8zF>fjE z1Gh)b2)bKabDE-?%m0&6e?|&+b+0riO%idQ9@Mtays%7q&ye&HeFT%(^JslY=uv-6 z5uHjBB`3ek5|BN{oc059bR(G5mJkeVB7#=aO+W*;hVaaMZgkW%Yjd~ysgeJd1Y5Ex zGA21vmfE2fK*~96h?bq%Dr_z~=NXMilGz#y@%o?Y5rsa@y)P8p!%V}lJElj>#A*1~-M*Q1&Ne)|Dcrgi#%S6l*wx_E5z zf4C_&z?JiiMh5%+Z>K=3fd3gB1N{GQ^=~cZ|J#|+>&J0;ob>DHuN5oMsudaH8ovqG zIMyHt8p`4a;;Jla=a_eeqrxkkg4K4F(%BgrinG|J?j$dTlBD$Blaw2q-@-SM^SKVj zoC&O^p)$JR&SpbRx|qDVHU){!^V!$2z@|Ymp}(WQcsJK&=J5GV`E8!4s>@6Q(hPT% zz2dN_?I+5O^rqoF!|M|QH3N!8g?URQvaAey^ciUtV4^V+>$o|EyMg91uSBAT9332R z3IRhpB+;dBFU{bp6WKK>A3-5DggH3HQu`&x@sbbp;hO|Q{k4!>KIo4E>&Xtm+v# zQjZD1Nbn*R^2jm^cYosP)t8&4pz&ID7`k_CbF>2m9;JXP0H;w51;Qx@z|IK=cY*wD zhE>3_s7Q-hL^~0qlwprW+0XPZbl%e%oUtNR>018UuYiITZJ@-UaWPDP1^VesG~Eg)z5!1eT($S_%yd5ZK6&rr>q?y9;}><|i4J0n2{y3z@Sy`$oH= zB*Yy+DPo%C?~c;Ff&ASV5UTljOlk?XAPQn}z8p`W*F&lBh$NJp(Z z2MMeq$+Pp0uBO=LIjefwv&R#8qigZZd_bWJzJjYkXA-KGWm577gJSValauaz*!eQ5 z!e;udmpv^20xW@I0Y$Re&VWNi8uDpxB>L+v();W8#^XKziS(@MjDCg)>(goet-uAK ze24Z*QF`#9IQsUi&sC`obCnT~lopV3#y|Ze{}-!EfTw3-6u#8ua&!0LJ>LKQBl&0g zYmzUusgtXrtc!-w0uAe|z{^#OwaBxjY$o&=hUa^MFeZ?y6H)9Q1{lM;#bK1}g}|dI zOCTG345S<$%YWpxXpQk5c@_)EIJ*J6SDu>lKrz(Mb0?K)^H$dv1yr2;Bw}M`^;deh z2GoO-N^ZB503thu=r6wYD$(PFUp-0AP4!k~H$9K*m1?{p4L?%%X5!olT4WeU5{h(f{SfW;Z4mtEr4g z0+yKZ?e#e^t7&(zA4G64i;43=EF;wKR}_hN)$saWzRA0rbqyY^B6$*-g_GP?fsvq< z2dQYA2|bRt4m;_^&HO#jxHpEA@4U%wTgWT0SxmHFVmVW7r}B4dZ6@)k5J|z1s>%15 zPa%qL_&Ck8udFCWz#oF$vTRYRlTP=-wIo)EX~K2!)7f`s2Z}&k3nBx!H_ooY5;N zGldD}^)-t**c4Kp;Ge#led>r8A~tO@=lyDxwEP*4LcSKeynpq`XWitb0Gk874=*{V z%hJp5Y$})ha!06b5^8lw+pk4UxJqBe zI!JZ%i-qc+z>==Fo)=sEti`8afsK+$ej7c>Vsg}4Om+L$Pbwrr5Z>a1NBRWJaxx@^ za189YCkce^WFNE#D#~8v?Z4@p{-BNVe)oGny7QNayWfTNSH!DkZ|x#wSm4kp0Nc~t zxO{@i5DKZ~wR;Mj6M2afCdzfJ-=3q`&pi&%8zLl~C@q3mo&ru2c3x0^VZPbA2vstw zvz|dL*kiU7YIJBVsB(KDgm0~JGwZ5n2jHID0U8k3R65mDL@x1;GHR~_SON{i#c9ex zs8orW!NPWxr<4t2aOoZ-(T!GMxm4ngjluIPu3*h@(eJ6C-`J<(;B5%&2>{lN4T9Fr z0%?d63A=3>86ZxU`4dXHcW)Z2Q|{ZOW++>axt z#O<$PYJnlbUP3WhdNZNos|-ff9N?WWQ>1!2tOMK-bwlwCjr~iyAwkRQ2hCe6Athon zB8q(NDx2S;S$~uo2RvHj=Af`%0-nb$269ZYrJcFVczH8jE%W2%m{OG2y2nNu$cV6z zCRAZ$2z39}QF()KU=C|8KcvdQAQ~}ar{T1Vb>&994GhYDf=$ooXUZ^QnG)WW0%M4L@nJ$x3 znwu9JtP$pBY4OVYiH`VlNGDWnz*sUM3XEOvg>9xOjIxk$YF6(J%rQ_x6x+J+#PmY; zhwySoIFbXKV%Iy^Fv3`$_jU1AW*qiV2mRL02succp5fK1$CN(N%Q)Nj3H6cu1i%#3 z(Do>Rgp_pbh%f*cfH5n$II7@c_!xRdTtp`vu-{wmQ01c`l2n;ig4(Jdbg{CIuQ&gXmO?xslOI8f+H+p}1WAQ(Wm45$VB|bujpw zy7P#_1OaRH6o$*vs_b6v`bhn5>q)#>r_Ml5wjO#FXGf>oNP@CSfF;WQ_4*pY2!{eI zPj6{Yl2$N_L98;{uzfu5lzh<7w4CecXZr{}A-$k+Y3cx ziia+!`6y^bA8fXK_1M>_!aeu<`XTd|-7hXhB|f zE$#&)*N4eVUhzMPcA=ezOteiKJeBv~T3wj^(3Ai*stEKq+r^F`H8MUMWbM2bQ>hex zf`I*)WC@WlWZSl$EWa58R>%(ELrL(pToe$ERmhk5=KtseO$Ih|iZM!}972`KKbOw{=9Ade05*RUFit!$oVM@j#$$piI2G+x- zUP)%k4-59cPft_8R06WSn8DBGDAbEaaM&`Vx5ib0RLue(Rw6+qA5svTfh0&cKnWo~ zlSn4J_XT2FUGJdD)X$!qz~GaDbJy%z@^qbioa_gR>Wwi1b2qlk`kP1^q@f0pd%fYm z_Is)%8?FX6We@1bg=)W4BOGQ-=4j`ktd*)~pTkqSaaL^Zr8+zV!+0EazeTGqv`&oD zNAT(Epg4qrBx-Py!KN!l>W~T?wg)xuQ^^6rW2B6daLs@1gRp8xKzg2Eh0&)qT{cY^ zX5V#|u7C6A{z94%(MFGwd1uK3)!}7LDNtzw;BXAUt1tb*4=~{>cG*Au*P1^=`S)(G z5)JDPxT_rGkScp-3Ib1!H>*!4rBxqKrI}y%{2`az6ChoZnW-)KjFFJ1G2m@?i(1Sv z27l|D zZo2uxsW{N^A!p(J=~^paW==c(6lP5jd}(C5(&*FgN4qSq@X?hocxn^!+x>qcHbQvr zEKBnL;RwKM3mrqc=+jWZKmxwukG?&UDy}vGq_00c=(FyI&SsHUkp&T8R*D>%)!6V0 z`BjccA8x)KeKIHDeKN+eAQ4<+E?T*eXbgv(X{9M}1Cb#6L31}R>R86$Y{+Px{t z2LVvcT(vXVuhu-?-{2Xky6c;-#=?Uwq@jq5`v{52d@cYrXUvdp1cKa!e*rtO_917I zPZY!2H+A(Q0-#W=ia-W@7{!V(1g}pMEb)b2IyyL=u7v0d1K2)@!s8RP<6J|s;umKP zV8yCK^0yR{d|_LZ8LyQ~-tct^99K?ZeV%x!?sXIZN%|WJkTigeaA7UI^Z`F0fDhsZ z-CR8R{iyW`%TlOMGyki0s|3xr#SG#1t8m!JIM^~}R&%Y%f=)h_e;sDz?8nnXX6`Tk zNEyWSC87dC021Z1xWJ1qy-}S>nDy`X*FS=~K2?@Dl*!QKr`%q(L8=k1u5c)ohB5_B zf_-KY8KiDqsfse;<2b4G>R;am{ag_BZcz*kSK-O%%~(aLHx&3lr$bRwK&UQFK_h47 z!N7fMMI$F8WLqeph_oMui3a|B4W&^hUst)GbYu_2g$$A-OHSRiB^gLVzAz^prNOv; zI_35!Wu*J^Jw9u(yE_`9_93&pzlo(@l$0)D`05ksJ!PChPK}hJo?GM1qu~{aMv&^ zqfaK~+8VH2G&s-b`o?DDzlmILG18+QQF&{&HAm(Dc8PAhETu%cPi+7VKu5TgQ>#;Q zFH4v@cDOTsPf>4w=5#7XmbBm0WSjr#UdvRBJdgU}Iz<;@P6F6P=+D>X+B0!9L-u8} z9W5g#+oW9T>l(eM1CF_gdyUCi)i|VpKTuUGn!(9AeOsi$DQL6aJv%HZ$Q#bwqx0dV z&A)UAz_ukC%->nY7{Pz!ll#Tq?s|6i#`U$hsAjRs8-Fk^GnCp(&A|gDsyy9LLjelK zJV$?Xnm*zyc97(Gek)ULUg=+c&_lGTNz!{-vitz+O zFC+lhYOjlq663Wy_PPMQdW}3?w`-*`B-CxbQS}F1for3ZopjFQBZm?O=Bw|0r1!{y zRnWSVQ%j?0{}Z0kwvyCwK4%ZDh9q}ddDKdU1)@u~TqH35k1TxxbgM^w+h{3`54aZfeGsx03- zX%DE9mWEZO+|xF;pJGvt0~f2j%9^2Kw{pYRlQeiGGV;~EbDsk^5Mu``A=HfC%y_(2 zZH+onV-N>aiErY0EL-a4QB?T2UQ>vH)uJc^IwMc|Mvcap^k29q1i;ftd5sZvdy_wY z4X`G1vK0gnL^y9+bxF`oCsE!kq!>e#x<1&4zF?{km?XVtfn(UMtz6j=OPLR3$ z>W^YHR_t!u32~^6XjhDDs!0!@>`D3`87t#(ggfRZKyokOsRY}d02Surz*(BjovOMo zMkA?P0OP^itZF?FAx4KP6dC`d9sq;}>XG?|j^M{{Yb(u5dnP-!)ml+Xh;!NQ_X=dH zp$3{4lJ{Yc(0P^NH=vS%5cza4fwbe^)6_g@Ij@rO@>)MJ-~>r*r=#y3nbkv%o%}us zLQB+Cb=ZMwhbOGIKKGq3U$DtafV{wgoEJ2YeiSGv{TZT$+P_yJyN^S)jSwJM(Puem z(rxY&xlBb~UjM6DFqHITPyP(%j5~1X$!Kxxi=y9fOU33-%*r0YE9^fN?zn;$sR0?@ zig_c#{(Gj&$!D8I9gE7cROfs15BG&<6@+{X=kL_fAcGvp`>SQf~ry@i(^_bAjzhV zd|dW?sU+Eo?-{^Vu|VL<%Cc4v2|Lmp)&@u`)NziRN%ZRwPb!2zDMWu}F@-;~TA{O? zULV)pP+4z>?IRYLpn?vZ+?q^R&(q{Z-9p4~0w=X?v^!P;mHyBQ`&#+4(_A$%!t}B< z(_A}aKVXH+xg29cx$Qgd*1e~GhPKZ`pJ*uc;1IM- z!yR#3W%%fiGU#1Pnw)uSz&3R8H00r`)H_4T?<93UC)2k`&|j83|1HBt3@;Xrw>v=x z4V2Z~v9MMs@g_7^r1S9}eyy`9e<88gaHcOP8k2OB$iioqlvO<#8niq-&iQ?mpI6Ys z`uv2JlFey{=K=HY%Eb*H5>VIT1@m+OGLMes>#XCc=-o+~K(8COs%h0?p{sF4)g9eP zLk8S5$P=8Dgr-uZOTR&}nD$oXrF*O7opiR=hEJb#@;{u7 zb#{6G`izWBjNq$wf7Lc9`RZyuPQ-}aR>)QXX})Ro{dRLWO84Vvunbq-@TieDi!N7R z|4=`{ZS0CAp^w>Y{E;Nteb-`$-W-f&cAcfCHjD65z%KszeuWKa(8_z1T?jakS7gT^ z6X>ZB-MAWdZdHG{Q_E9n%ARo5ket-S6>UZ7*I10mPAKQp2-$D>szkSm-+$hC6_c2& zT#VHxbmXjG#80Hi^B~0f4Fjat!J+5u^0W@0JIadk%n0mS{j0xr$92S*l7koaJoykq zCL9I@btOM2?^mxi9XJE0E@RNuxD0CPx zM5(@H0w`g-Hs2tr#3FyFm1tG4XGX|j>h#Gip*wZs$}S?8g4JH14~W*Is=X`PRiZ~> z(cj3OjwJYo)P5BNsLY57fJMm4Nxgg*8|)|3;e(A|;tCnz4$v2GpKHA?T1aI<5wq&i)>daE;Y2m{W`a>%^= zSZoOZh1VycH@QbY&IRP8j*6MI&XqiatAwZALlC^Z225!FhEIU)ur(Jg4ZzhEmEG` z@9iNq)Rt3l^8po6r|Z(tKKKeiAGyt9^BI-j+hjRE1-j#~r(8c&PpgC=zH z5f4f?R?1mPSbBG1_gWRWj=ERs6@BOZH)-!`JB!s2o5eyVUu6`do>tixJW;*4Xy}LZ za(kk*I|Kpvd4X$OzueGc-j@n9j0v?n3F{id^f?aA`+gE2RaFI>p*<7v2BPpx1>Ol;(FL(?K#RA{^ z{IY%)0DevZ(QUmHvLv$x{xieB{ucm|;-%+mt(AyW?UAI40Q9e-0+R%Ja#>rPb<+xl zDPMRNP=sTQHU{M5nQ14O--!2+p8jNL=j`J<>ey8&AXNhsM0mowQucu~G73bN;k-?% z&zF-p^ruyh9F0tbXGeFWTl@;05QA3NLxI$_P$*C?-0}pgX_WK9X$dRB8@!i&>+&5d z$g7Ktp3v#6lK0wQzalGcziZm^Crf@ipHAfcq0-i5>g3R)*pX!ZBq_xdmv`Slq|0;< z&SB(!fY+uSP0Hofk?*QzML>b-!BU(j9&gAXw!&Y2PpK0rr%Q1+3O*{feM%bMVDah! zT0C*X9@MsO%|uvy41eRRX4Xl967STAVWyEfmTv{6+dKt5$Ps7g%1wbiblpKCjDAdf zRV7VRpM{J_lFfissaXq@4{+i(W=6PBB8t0SnszO+CcmXQO`)vozyWr zcAs)mjaRpwK0n|s6oMv?S6B2Z$Vh!^{@zro6@O~BrQ60SyR|(1$s3O+Tm-GTY@nOW>6GJwm5a6DxLr$rdkq zZ*Hnqs!Hb*g+^fyfL0dJO|wmZbjE?g2OU0XTzW=#h|OoXG;YL5E@b{$4LY7DB(RqAP&gT!1Xo>9IOc3jTUa@Ma(YOb|X?GUTrV5Bq3 z{r=!>-j^Eg?b#|=3mD&e0Rmnjx1!pkc}~app^{`)YBE^nYBAhtb}Gpp9}7HJ zLH}e-d3Dol*OYS0rTp%BE-C3fc3t{xy;7d0bxNz!!5t)1OR3*&rBK9{@r%;Jpa<{2 z*Df-_OrEk^y6qZfOr}>aR75(G8GuvCz>+oeA+5 z8LTByUoFWmKPz>_;!{M4evfb>WeUGfjv}Vy>3!1rwWI=3u|Hh8nwr>dg83PK5{g0PpDsEa<<$s<%~QRoK|a01N_{Bw#b7ph->`W0wZ7=~T)jEM zv&B_Ud*@5_v9LjGB+rec-q9q?WF7L0L_XNJ`O5RBo(oC)i9tqRv?>+>iSsmDQi}R)Cd;y)?=SlbU&EW${K+mR*rbt@0&Sf3KAbcDP-gA5?w*O>l4O zgK8?SdGq4J7d)-pYYR~PY|k1!jLSIDX{1q2<#dI<(RQ)R5Y;Jbi=A6)owc8OzcM|7 zZ(&Z5-Ira8trE>^1^6!$pB``jxiWc%9bf7??9+=5bz9gZ;kjNTubSj%gc}0tGf(!D z5DQ9T^p}a&LF@}2gxj;J;@{`847*6L6UJyjiGwaf9d1I7`f;HZgD!i=H3qtF)o1!8 zr`?8jvn{F}GczGd?qKYGj9o!H$8$K2bLLQ1s)CFP(Kc1g)7l4Z-;0bbd#t{DEODgH z8sld}r)#VrmfXuO1}5y5l?GZ={Vab|AC$OaisP8SFfY`k}U)coA~L%*+)6zoLiaV(xa^`{m9W1?p7{ zR12wUR-p19rVw@E9}uHet(Y4pw+mko?<%oetIiBW8`)t07fWXuR@L*hVY-C_NJux* z4bmYE(s1Y!q*IO((%s$NaOmy^DG7l?cS=eKBKdB9|M$B1eAu)1%$k|Cp66cqm9CXx zp9p+-W!vAT%Lg@oQfSq2TqHzWI3X~wC`su2XFI^>JNu=)rUF;^?UrrD_e;tCVwTs` zM$xw5WT+rQ(=-$5$JX$_0#7YX9t|Tu(&Hr`s#;_XG}QebC2+(Ii>y@RN%*7)fSL^P zLz>V_#Kg99Dlee+gBM=e>^u(`0dm}{UQUfgWHm6^Zsz4QZ;|2%xZM!&_{nGs)EoAo zZD9E$*4Cu>`>}XAv|6bPT2FbS?J!$xIdHTj4Q(A{uJPC8c%;$oDGvuxGQ^xKvCGG+&d|@Ztm3t)3Bqgvv5{)QR3YUxN8?A#TM>y8c8n3@ud_Q{ z>4xH}|6R$5J-u5ty-QRrU^n!JSP+Getz&H)6d# zSh21ON|*^v!tB?s03-@Z$75LC)HDUjAHK)EjU}NESk9po4{Df<@Ig9l0#AO?fhYbj zUX_?12T1?7seyc=`>n^}SOlEO9RLqKsg)BYfJASw(v;uGYV>h=ESUzM zL7MJ3Uamkyjtm#KDiDXzW`(~ZFUJs?92jgu4>-AW6#{2ipuA{lK^TXyJV_d!Ptq~C zUzdg)YM3XK)anssI1zEV0va{6Z4(L0w%VZx!xa%#Dk)oc>+LkPc2^`zC_CYMip{Aq z6>B4u2e-uK(eLJR7M`sN3U!A}~m@%?XEkd|~S={|OnRQu)++D4F6x2O;2-9sIK zD^GSNY^2$P8(rOF7kU2c_e|>n+^CG86!>i?-0`85F? z5bF%~qab5wm0?MY5{&A}qpA4Y{$8HO50NV}1>q|laOnfhOr!2M52HhuWaV*%e>xm0&C1Fty2>)rG57 zpc<>ikPkNVWg)zyx-1ZeZ;UW;AFy`FU+J(zLXmW^F;o4f-#yvdVB5YK&n`cE$z0De<5f=0{~mPm`$##rVJTYzGamD;jn zEuc15Q$MaOB&i(XkFDYbEvOW;-EF+5KqKy^8|K=lAZ5SNH1xmH)TW=*t^Q)z-Zh1g z>`c972!G_`= z@7CP^Dbef9!gM=|@7zhIOG5t468N9aeHytrDI~*ho!SWY^`I&?%_j9((ducpIleh3 z4`HIn8-5VmpEY337WxbZxF1T3-EymM*2j zeI%(1_sza@A(`iEhu?u$v&qN6c}%^($+kn;q{@rQv6|+Jr$X^A1?#ZZv8vJFHqp(o z3~waIp~y1y(@d*^QF+Q~n9^{EcVtvrJZ{*0?<5~x1N4Zd>E|k)ktGiKZ*^|a$iJne zsO3&u5UVpXrR6;9S+U489_xM3Naf{1&L9OF9%pRI|!@ zHLsQU=9pysPf4`p;PQlq!#tr)n|(#2U9EBZ{1SWBTuQ|Rl+yClD%pmentHa-PaSGDb$&v z*$8~D@g_YZYgUsE8FX+6Yg(K1r^i;6{bGT*wpS#Iw0y00?LJi&b7j;`wnv{<^hfKX z99(Z!rpCG#D4-j(D)m^y5kBk$=!977QjXgiN*wkXu+g?M$S#;2(Aj|Tkj}jdDf0HR zzZSrocvU4@$A}R6q=5kryjT{)i5O+wZnd`QVsMfM8o@7`3rOyMZXg(tg*(<)=TLybpTN)`-uTWTA2LUO~)Pvq$hRSPyVxjBu z`!9Zelk$tZhv=2!$YrhUg~H~O{O`wXkXR9~5B(7mj5>RFY(IxfegFOhxf6k0g4;?y zIh3z8Mb4egC}dQ?Kirjy#~q+#GaYKkq#o|z=?tV)CFm4pSg@;Yco@L$%keAGQZam# z1^WUF0e6WCWZ3{BfWU`QS;9fSp-&R!US-^+KQlX93=|ir#Z=OCs`LrA6(Wa2qdaR{ z0RxtTtZWFc^|WcIkToq=G}eV{k%0Z8bExaqcbsHQ_)l_xlcR?s`X0wuE%zOphaTWj zED!)Z{>G?cnQp=m~U`>YSsmHCRT* zdi@diUN~_R-Nh&a_eUjd+7&v<0Ex?F|B#`*=ke6(#f)(YjBw1@>$I3OYas!!G^zSL zT*yHr-GKTjfGK>he|w$RBw4LfsY?x5=uD^_hsnQYMGtKN5Q^U_ua(`d6A1N_qyj+)Rn#{~H!}!(V)2 z?}9F>ibY{VD5&Kt|8#1!=)Nfg*3fWGqCkmL5&;xrRaLY)cfKNqtlfazrj%cYxaPOo zKd(Lhxpd)}yMxH8N}a6+8(JdJ-Lh8!Qr9)arA=7C#V>u(aup zhNDRZ6e9`UccZnU8-ul?U3Zym^Li-8a1nl_d{c*>N++|fRGbnS(am%UPC6IxUb>fo z(2#aM7t3iUk#il?MURQBk-d9|lHDV-e{2jPKPnncUz@d5U8AjqyvJJLvF!Myb=*lN zl`K=wjPzD4Y@53GYj;M?$nzRqIw}8=Qo-{zYS(h6JOh7rr?urD;UM63T!P*TeSRxT zNu~EKs+Avq(H1Q&PvT4D$m71-(+wEWYK0=o%p>Ns zR2e1^Y&mO-&>GZ~CX9a0$}+(ovZLRgI=8bxF)naQ}t z@`|W>mHNL1S2^kyhn#|HxaCdL{@SzKLyyKSrUDiE8nSc}fT|o$wNfg3f9N^D3mgSd zXVn!OIG8guXbRav5UJ(;Q4%Q4G2x_iD~ggF%PEeAf{^lT``= zi`L|r;lY%zQu|=;9LozJK=r86xq}`F9Q3<%N;s|POkKha&Bi>}HKZkSy)u>`aA)R3 z-|ti#0ePNamcgH!j73!06xcX3Xb+;p>~jnGd4X3pL@5 z)Cu-XO&Vs>bcf8ws-14d5^xEY;@|%m?Rc{s_wE0xU!S_1=}E$4J*ldVmkLJB*Q>Ws z&{ZF?m7rk#0ov;7 z+%-TOMIx{OyovAA0cRW%Iv&4KB^6msGasn)MahuKIRY+K$=)qaHBJDS8^Otuy92UV z0hTA-Z*gEU)iRP*Nn?u~KYn-Pw;fQZKQq0j00MmK=ae zu@eKeIY6HMJVN<=JFLdhY!t^BMihkk@k@~w(o7lZD1i-e!z@ui+t)m{qK_#$DHOWD z$|w_J;e5{@;*T3rpYO$A%5t48l-_RX1gDIYK@r-tcopyJwmnCetGIQ#s(gzlB}=P* zMtj1V`u+I%EI5EQW>iQPdmQhe(*_dKstnyPEWbrt#96wP{~Q@vI$XVT#r0W)64M!x zR}Seizj)i;B_&oc*JC|kNpTpNW|yg2nxhzL9OWYmDz#`yKxAOE*+7Uy0UQfg+^4;$ zzWO_mY3*LUCzaZ_xFp;#MmS;|Bv)`;quK}#0VVuB;`80{$N>z|k3}KJA0aH697%?cWg*9F$KYgd*otRvGheax09lW;o(cYv z<@i2Y7Z{KZ%J_+MK5;@E=e4bVQl%B*`swP6y>IxJ&1zk+4v0cGBWwT$h9pl&Y5+j{ zlMr(I1zP2K{5R#BqHr`0nx(z%ZQ1Z)=>Y(AdjKRcarN8P#3g_;g+>m*12VjhY3Ay@ z?s>sYz%cvTPzY85=E&6Covji{RFwH`MzMUD43Bcbh4-3K>^s$GC7$MRy0MWo*g)5i z9_c(q(WI8|;LyqlOY}Di=C=qBW*@&nd%kG(kiELBY@l(q9IOby?xJ~Rhw56iv6TtK z{}nZqX&EJiLf&K=>1nvZ-UZvayXcrtS<~NgS@StO`WuJP_qRK~J|ZYG7SIp!A)Uk~ zU@!h8m9lq*Z?Iq7zo=IGXogQXRlz7|Z#v-*lWn@pCkXGj z%kfylqHw8c#oMk&E{W?De&91o2Lm3Hb&IC>#gdKG_efFF(IoUo*E+j@W@RY6l>Zzk z36d!f{jT@6TKVd<{dgC3%I#1`Qav>q$EDPQgqqjq-1WZQ%_*Jz+SYuF!n@^jvuEpn z7$f>HxX2&%SzmuoV|28A`=fqlqxZ&PGK6<4i>!S1#dUpBTE$MMZ9U?=GbqKEl~Tws z$Szz1*Vn$Apd@t(n4WT~w8vR|&9)Pn`vN%kWHPq1m-TWUX0DVXTlyeA9+4R-iy<3e zIHWk^L(1Z(CfUHYllm}2uFv-;1~(L(2@IzM8jRRCVJS5$%$kG_DI5o%Mq2j_igHMx zID_4a+WtoRzt&4^M{=|+LS3n6{wqKlBHeC5S)Lo{!K+sT$$2sTkYs}*K~rC_ks)EX zZ%wz5zM6%&C*TRVMs-Arth!O#Tk|-`UfRX*V*cS1Czig1JHa_Ml`C!nP+0~*u}BpF zRKyCNY!G9G6)kP)qf0a+LXk=M7lS$QqMAhjumy;@a4lyEe(pc&Nx>kt?RY5C4L&V9 z_==>61b_ctXRTA@Ph1&-Ns&npp3N8wn-GFzaH{sV-9Ojwl;Zq5%1%REOM;JFygPx6 z7;sNZ1Un__*tYseEo98JWRp}M^Yg=C3k1~i?SHseWz_Zf?nVJQY(j|NBd4~P%|Ad| z=r+`I2aX^Fb)8FDILVL=hId91$?X#Z9O^Oyc=boaB%=ET0GQk!3&CEOlwh+q`}6DP ziw&LGX+`uMe$Q42X+1g~9tUBsWb-2KzVGwB36<{Zm4B%-!y=@eO2q&csn=oz%0a$WMf^G7F{985ng18}6HsaCN&1O97J-Qkpph}4NnwsVZq99O zojd)}d$Vu9#?wi6lfy}iL&TaV40tR9m$ki%puNRMQ_x6~>f9c$(-G$`f0Ak|0t^;} z>*X^R!6dP7a#Vb1=Z|BGqH88;*<1lBe&SMuH7=@~Ip3HSFer=kZ7B&(%>Fq}(x+LV zuP|deEH}n)Jf3~+3xRe)Nux`tU;%Dh0qCQIyFct35@iZ1aoVymWoj#D!=O~nnU-@!<4D1v!3*`N|bXd9=F!FlvJJos_jcmdYNVnU@U1VvP-`aW}vdgg&34b17mC=_82^9>A z)pH57#MJ^d$qaUGN|}xm&~eE%Eu*r&%PVikjVtQD{}z=#(k-GiiOeQWqSuLmIzE)+ z#|c~;O`d44W#C@4c|&X*eGngUt$0bZ;fpidncruEZvn}5Xp#fX6ZDXV$!K43o*k|a z!V6y0W?RE}^_SEJbV>0LipyR@|gMisHL@Z-)OxlC~r%TN& zt~>?;U>Yo7k=vb7DkKQf$_TX_~!}2ZqJK=4icF%_4dcA44Acm@Bv^4kvJ==2=(W+KEBhaOF#?$ zG7D6HZ_l^P4rz^Na((V>jFx=b$)XqRUR`M&L?CiRO&%@OQ0kV3pD3Dxj7w?N-Xh)Z z?BNitu&kvhQKsAj&S9i73LF1i3sTF;$O2(xJZ#SKM@ zY#7pm#j)(r740nh`}wj)wsZ~&5@nd`eGFUfo)ukL#E&BgJV6}U{6&_diM!Cpow%fD zw=xwYQ3u+LB`qAo5d#Tsqs3QL;~8DCw3E8$KaE7fP&u+#5blhK5FSz=5jHX##fa+~ zz?)0nM`gC0n#K>*`ZDhA2^Ej^BLqWKMlXl6yeQgUN)JPBg6BdG>4j)5dcsi_F)kD4 zv&bSE3%EEY{`cSAv8Im85TlhPTNibg0%_Iuf&c%;waF7=fhmFN!iv$xJut?-a5(mp zI#Hi%c@&dpHyZuea(k?5rbhwlE5KwB z1Lqs};J?#kq`l($cunJdFFUbJ=@b67pEQgX)J=2kX9Qz_ba8%L_Vln@mR*AK>sX!W z2ou*XXd_mh|7zUy*F!m-lhjK!^bbOn%gdrxi~4OqtNJfK|6xWWSSucV9ZbvhU#WuOf zPngHGn7U52SlE{=G$^H$hUKInyI~;(lX3gn3tQ`I-hhsrT$^J&f zbypsO@H|zk0jk!J`yjFIaX|hL&=wEWf{?Z*;j89iw=W^4OmjhI}DWJPSAnI+;af;Yt(#QQlhM7?eU?ATqY{F)M;KiC7A7es-74o$ia%!p+{L& z!i_V=#TGg5-9)EewIEl{dmYPi0?9{9;R_qYxF88RjbjWrWc}%d7rH$CCtIhwHv8%! zMnVI$NiKuj>SojUQ8+orO{4L!OsOUT!9|jp%eBcxsU&L7q=vZOX?A4xOBGwsv?@7- zn50|fkHMm#lO*U`1n^bLzQ6~h`aoQq9t ztF0($OrHQA4FH|ZF_+6asqr?za;NA5Xf6^QJM?b}_iY0jryb8n-={W!B`$YwR!%I4 zlsl^QQ1|Ac5-+PJv|z{xvLsrk@_PXd!qG)v?k8J;cdk_rEq8WN(|>*HFWW~<9i0QT!#-4nxTQcwJ@pOWJytCOA{a(UBe=iul+iY<_tN4$b<#;n?% zkb>aqIn39u;b}j7OO46tQeU6umZev`d-#jF7tGRpIvnXaK4r_Y-$oTta<;#!TSr;|^9I7@LrMDmy7?;SvCq_S4>nCN7MJe&#Ofe$wx9 zj}@v{Jy_N-a^Q&aJ1r7YeF0{;21pR1Cg?L4w?FuW_%oEh4|1+92>-b6m~=3l8Ns=H zc~rUHWcfP=N;5}4gd1#LYa=XQV}HA7I+4yJ*TQ!~<9+uB{b$E%b7mCK9l>2d;?XLB z>7+poIU_p-W@bfe|E2lXA{+BVtuo1VxZ`50#v?kwRr5A`rZ@)O<4V<10L4(fjxP?J zA`QHN?U6~wPcNtEi+zpTs3Tfw7M?;QD_b!+lq`BEKN|{6tTL`+obeV5()zCvZ|Xbd zpst!&&~a03YK&0p))MKhFpHq1Gh+J+&+(3yh{u`S9g14mV`#0IE*%M#nXw+wXT; z;P}J^t$^m`>B)R&JRBf!&a*3nXaGBW7YMnH8<&(k)S0?_pa!WgOz(wz9EX8)YZ-t6 zu;a;UPKPOuo5A!!-kbGvRVF!@V25~tixCQ}rHV1rrlzAcm9OOs7GU(0X~c6|a(`Dp z34VW#0)ht`-H@=b-%6u&Qd1pCN*H zj+1nS?k%2r6mI%~huXeJ8GISD=kvilyQ8SFc2~J^*Os_l*Wa@)tJVXb1C77HapSM# zD$A|OB~JvOui5(6f)92x$w^1ZNmiiRv{Uiq z&mOPLP^eyoUEnEDO`w-$FbjMV1m#Dt6rzyh{W!AtPKL&Wuc^bh=v1~B0DHBktBWwc zK{dAn;K6pVsa=_0Q?VKde?0>Lwk0EIO@zD-%SrPr;=GQ-p?*B2zr^*BdSC)dh9rVB z4dS%1Obkd^^wP5~5R2zT!TO4gp#j7zHxeqj`oj?l{CWEzB#mf^)`BtLN9{KbP@%IL z(y3D{?Qdq{F3R-PKt&8xPr=1_?n#G4Na#aRh54%=hh=`h**##ZEg{Vhy|P?WW_d96AXE zI*=*?E;_5*v@{{vx>-{{X)P8m{zInpPMG^_ z;A>s=*Z^v=D)kK9c<=S^Qi7Vy+?`(U+w$ddcx`kk;*F-HSJihuAp*azph&gNB$c*J zRnLjDOpe20!hMl!y)R@!)RqoKIspV6rNS=P+mQu!Z{5M;rQTYFj@JGzQ+0SAs#Q9< zXM$`-8Sc1Cs8CwrgpGCr7`k3MlDr>MLYl-NA?q{Wd$p}VBy}0Hz0!fxR@_=7oD~)4mhK%!LvL!y6`?CPk10=pkwP4tC@4sB^xz_@@f==|RQ!DhwA zrV@9FQcLc}u47LzrF+S~e=LM*JF$)iJq`}N2+coDx3<8{Y&M*)30IfhdBa4UbE@a} zhAjDWx34hg)P`6Kew?4^R*S%5L}u}CI9@$76$V>t)B!D z=(M~Po{WgS;GTi5Ef#lw4v!L7KOj2)jt3a-2JAf#u8g(k&D$qS4VD8TuT)H4r2~+jcetT^ks$tlnkE`20Nul0BNt1-{pm*a1G9^7@Xmg*bignHWndZ#iS({r+e#c36rs0Hvv` ze}^6?L4r5(An@U$-BGv9l)fEko^<`{?zpI|On%+>*}KJ<+Wb?MUW07A+u^qK0>Bq$ zJ6&$V596Fm7x(Rs#C`_wa$vxhNw3NG9e0qZ(i9{i|27o4EZ6tRqYbEDB4et4tQ+1L zj>R0D1A46)uHRnlSOSG#pkbNFy{cQnD zbR-t}5hP`Y@G|e;5G+;LZRp;z)MD+Y2E@XN>MxEXjS)m~+IiV&B)Wm>zghLhzf}dd zKjNh)9LC@aquP|fUN?%Wh^pRH#&*544PU)hj9N0S3ZP3MOj*J~FCWI*}pHV?S!NxCF!75W*Ata<5Brr&~AQRQfTgGH20+iW8)e} z{{x#*5wYr00Y?!R_06A#<0)*#4lB)*GeOC90O853?dk3~8}9u-h7TF|ejbXzuK zgDnVI?RSVTyL6MKMNQkN10I6Rfa)eEm<9u){#;_FP_n|ozKrXUbR8Ls=r9DSqIgspPhJJ##=ie^lT0MxnO<@hIKjOu zrlOad1s}yVp1l{dVh+d`pOsPJ=#=&aqINuXd*6=%(8*l@>g<}r7M28b+fvg?G|}$r z-Kis#09k|F>l_7YH*7j^W+O3nv6_rwUUxP>N##kP+T0y_JnGt$oZHl^#2_#`&3y`u zhtrjfV3m}ua%_mJ<`kJtLSp`N-+9T#d+P|9msptvms6}U;-f z4s-pVc&z%?lpeZDE-YC^e3EeePM8N#bDow5wLVK?z8{t0XpYyucFFoad@L`WANSnr zXZdcWn7*&fy`_5igl{Z;Gj)@d97$?9G9uQ-{cD>{fFR6O{YOkG7Kbs~%M6#6l6a5w zoiUvNbvD4M>pDf`a)ef8Bn_^gcTGRq!0mifQt-=38cQJ4d(1sfd2c%)UhI^>fsaVd z-`ZjSO8agA5VJBP0yhW=^S`6c2D@K@Ak@w0pQvIA#Rf?(fjMK-UriE+=RkvVecnua zBx$O*vGYE{3O(FUk9DC6-(wp+o~0V3o-3A0Rht%o*PBEYL;zx2)1%|?@iY{Bx@-Ja z=$-QL$oFAyCPwg19N(#p^vQ((trP_i8mau_&<5$11NF+N408Yw`2yFizNCB~6ySi; zS;Q`bK-(w{WxPQR>yzNe zG?Z!O0;KrshTL%M&JloeS>O~Zv?syy7dg;61K(QZwVm|Mb@1h|!%#wIu}+1tpzDVP zLz}!6!snv^6nDRutx}$f6~P5pcb*oF4UJy8OHz6%iMr&PlMPjzMR|o<&rc2CY957E z6(pU;-zcJ+%}Eg20{0VTnVFsaOU$Wr6ppgL$2|6!b}G>#_TS?$xS-^ix=MX|+kn^6 z!_=3V8)1z$@BIlZC(Ewxw%LL&&q8RJ-eJn!kU=b_l1oS;;pOAsR*PUi zFuZ&XT~JsBG7BO3CsG@gUg@N}nNdG@8i5Vg%SmCHfB$&oiuW@q6WQ86YE zpjTy@re9YfzhB9gWK=q~dx!mM7u)Ep#7OgZZWj-VhM{)Iu;_oo$A#CgHAmu3&t%G_ zc6Can{{u<8wmsDN2}zKajXF;m{<-PyMs+DW$qF8h9JAQ^j^N>ISGQY!b(akU)aA3b zgeqK_wWK@@7Tb!T?v*7$V2E^#5|}y(IHs)azkR2r?En4!_|r#u*?FI^$=F8PCpv$N z?_c)sun>yr+_N}cn<<&v0_&zmDY)1NIU%VSQ~^k?Hm8U9j}Qop<~5c+fg0Zo|} z%RZ4tBtKF6c!x0?*<^dZ7y7}u{d@(zl5};a#%}k$w6UiGua?iUL5;E@HO;-1!8rL6 zp zt<5?nhf?%vA|j*_v+|W_{%-sZXTdjK|JitlLKyWbYYsPlO|ae-Lqr_n1av@FanHhP0l z+fA~DjNGgAXfJc050PKPWO|JvyciM6=O(&Mc-dC8S$`iH1gxm0j6se@#8%0cxQy?% z3<4lLvLYs6e+7;fCGgygjL50I3%VN`jLYY|QwxCPJK)}1?TCd%mVG?LObe){P`FuT*=Pb$2o8Ko36%BGT>)RMf}Z^eF( z#|IIu;q_?$D|HLsp_w$U}en|U4vyY^dgRl+*fX z^arNX&qoTRkA|zfnnD6K993G*d{g6YKBBLV7Cxu2KfhmHb~)i4$UTS&zPHr$JMRjZ z0_aF#KnDUx7AE!e5+oNc16Vw{PK71v7CzO4-v4l2gr1?6}`+7nsD(W{pP_+G~8Y4*yo=&-~ne zQ2ArYbGgcBfm-1Ga{0(n-bIp7+4-1LQh7hicz89d$P%KK_U0VTC1M0p&uf96;uy2@ zt_;3$mcX*pTI2L{3}c?+m<;RSI&9pf=lZ4*JrY&h|ANZnzH!t@jt1gnBrs)xri|Yr zIq5^VIwQ};YoF_@)JiHw0yf)9eXG9}u8ywhi&WtJ;klDt-U`qVtSqpb3fEZ5FR+V^ zs0kF3fk;zc{#$x81CO3%ym^g}vI zfMe5zDD=ckzTyf#;HOvze;rN6dlePqjSuFBgoS!~U!V3dsv>I*Kis*Nr?9}9Qd9|3 z+XWV4pyzjY@5Jcqm4+6q?k5%NBI=u8@vP3#R2mv-xi#rvfpsOXv&avnAq+*5#W*W%F#h zZC}cNE!W@<{e7JxXi|mUc0E=~$W#2Wfpv7jM)uv$k9}rgt%?e&?kWW9ZK`6)QO7m- z9=EvhM9*^EuAi9+l!nL!*zb`2y^h?{S#@i0ms$uSZ8f7ua#$9iv|a;uZ?JmeA_AxE zXn>FV2H~|X6HijSi9BSx9I8W&P-)B=YOBn){kf=CE2eC7xzr^iOFUKI6uaE|UD1d>6UxUJCN)g(tIol>J@}>KhGLAx+c(qq1%9(yI1wvH#|29-EAm-5T zx6&EaDBDZAZ}m~0EY)}_VW^~z!4v4osU|}yQ}x?ol1Ge9O4j1{ z%(P&=z|=$~sA<%viU^Aa=f52(0v8BKb?_e=$IDe7B%%m%nKXYEYew203RM!a2YC znY>I>2?X>Heh4i2p^VqURsrK3GoNio)ot8P(nbmsrPni<>VMG+eqD>!&@FOl!krgx|Ir@uITM*F zXK=z5f`_{#Krr@v*l3CfvF_0PXkfH45IJ&oM=Txoy&+eG=zqca76^^~R^LgQWbzDM zvhD{cWzL8hE^jU3fo3UD_EQl%;tqA+H<-1|f;n;JFcd8gerh~sC3_!@H+9Hg&21gi zK$4tt!K&;tt%mM)b;%JN%53`wmNsQ!m{o?>lt#|;d?#}WwHF3eU&u7%zBK83w>?+> zv5TiKyyHvAahQ^`sKML{Rk8wB%FEd*2KXj!54yrSNI$X=k6{}R(IdZoIW7TS4A^i> z3rC7yOPTFR5HT3l{PR}T5I#vRX15_K%zr&NLd~~2&~{HK&&Sw(rLHQXdY*(GK{Jy4 zV!i;Qdj4)jH~YA5-h7iZJ{budcNOCGkD(eA|9ItaK)&SER^-1r&o~ua*(+$Lv>Lo( zlEV1Er-=fd#$r{MWgdyu{nu(6O`%-m!|gDQsyN$>!~kB}?PWGT$zXcMSS&II`*=t( zrb`=kZvTvUt=m?W45WZVgz^4Q^!HCB7mW@v?2m;~nCicTTW<~ZW|U=gV^oCs(YB?3 z6B+eQ8mt#9ia#m6D?4}|$k41mui%+cnc~s+t^&oA=l3f6X+wXIzq}nP|03F6`7;Fp z3#LW0vRF?NnX75)_;SrIOfunld@w^N0=q#|Si>a}*$r>%6hcy=raw815tFiLw*5pE zw}cQkS1A~qW_n--`DfFKX*ij-`0?+L|l)l=RDuykMGAgJr=? zI)7o&Fl;LL?H~8gbz1k+D4LN+(!O;nj9*;yX?UricVGI##TMrPhN<=E{Ns!3_FDdB zlU4=VdMBooJLoiU7ObKOg)4$F2l!hOyu2)VXCUsPi%BqTh~lfEVu!7I)r|B9(~PG^ zVv%UE*kNaEa#v59z8X zwtt*tVmzR%*Il8zuIr(A*|^GPL@LCxgepY;u9bjn>8c29AZyXWwaM|as)}gfF)Fpl zU>5BR__{oNbOW~+G|x9qG^(QX3F&VZD$X%$OntPS6^G}xUA&cWo!HLUbM;q0;{_JXRH1^F2EdCFsW(+kWlcuW>FFi5|kqC@Ro_ZdKu6hH83Edpxm2M ze%vNg{}Gd?ZF6IVsT_(F(@#b35P16be6Bk|`cEx{Tfbs9iK~ZNhecX@~`1 zPG(45eg3+AQW+BWR;MXBKRPd4sGD~I>=O^%OMo9|2Y!5cZ+yOj#x&;C>n7}B>e;>c ztWy&y<}?MYy(aX|F@WWK&cFW{%HC$1KNi=bLnM!B)JSqz%l`ZGhM!1ViIPfNKZbqq z{r|#ixon;a(!8K9J8zq%dH|@3ozg=ab|s*#Ny?Wkp7i;4YWS2BJ?+iGldwy zI(Q;rPdQ{&6J<;~gDuJdQUK?;cyX&qvZY$Hz;5}Ja;MYSRO2pB?cIqS(Um<(Q$XLY zk~=EO63!}&b2OF4H4y%8jRRqH*e%JFc0&0*+vO#7XDsu%u2rjuj&H;x`{4g?5YkB7+b8Ba3~0%K z&~yywX|h{4Oz|1P3d$ahP#5^;rI$32j@p$Izl+Bg>$!u~?2g3K5_sJI{k=ecg9S8@n+jjETM$tBF)%lvu6N|^xfUvm*ym^RsifhAvc!;P^>41<1-4`%>q6XzQw<;R_Gl^ z&5=#$JkaX~~ zQ6>RkU<9o1(Y@hQg1fKt=1TmTf?Ol zZ!cc7;tvOXP`8I$7XVUQ{vOVR?X%x7eAt^J*=Ukcpw+?}fA^12Or5ul9UfQv5h9)O zXvcVDn_O&dfUY} zxu3ih|BJkF5;S;Wk-hbp!%3t8QBtd>qy@;7%E$joXE;SsBU6S-fxb^jG2%E5c@&i@ zX*4G*Ej4yT!_E|guzDsLU(&wIX}K&~-JI-|>cAc|5T{FU9G0!o+`>Bgdv-5o^`;mDzDn0ZhG&|(_P=?gM;j{ zEadv(^s_J$5D5Dtk%hF6E5W`Gg<&BZ)1-Zv!~{}2tWq!Q68IaH{lD72Ix4C!Y!`5V zp$Cvq5eB4(R+MfO2?Z1wq=u3X5s)6b6;K&P1Ox?@?rtd=5mCCkTROiz;?Mi7b??8I z#o~`yhckQcv(LMq_j%)i=hE}lf4Iyq#bVj5~7jA%7k!9tk@~KzWFb zimJr2s|CdVfJ%Xsaz*9h?{6Vi@5(nl*8kox9T02rKff5&u(0^Kfusf9K@G7y0Xp7g2%+2TDT+9MIYq4?9 zb??%nXsMtNyVamKu_hD8s#tCQhabkd1=l#_0^&2yvAn?U92)BnXN9rQ+w?{| zQMN#Y{!pR-4eJ6l{8hpw;)fHI?}K0+facEjb&I@fj9)}NnD#Wz=7j;i;7^x+bLXyy zH4<*B76-0*4NkslA0C-M$TRbQrEz8i*lz&5HqBgDxh9=fT6a_k%^=FF9bBTqU`@}w zI4~g2dM?w8RDSngJ>#hlB#7%X{{1GPzP)3+QADXn2=U^tG1^K`H3f z<|;4HkU&lFDO%R!<{ys-j9e3k<0XVhhWLNiNIX)o7`&F32<`asiP|@h#{EaX@R?`i z0d5s}W5Yr=rAB-Y2tlYP@os{CvO&b&dd)Y8U--HXpwBL31f)YGbz8A@OSWEXjQ72u z^7%(oN-i$eHL6jU{26ZlSqT^~n4;$Gf?-dB^%@21&^5Hl^d`1u%IERA6pQ285Mb!r{M3YT-yCiG1N2J}^m_>FTwf{(`NVNFIypfA69SDO{X1`yOwgC=yuh z_Rw=)&ut$VkUp~&2!g>Tx$d}luymoJIZF9q2hDyTn(l(w4@H;-`L9R{LCha>;13Ce zg;#<2Aql5fwv)~bVPx9k;KrPep5R-lC6mG9H`|Nrrg1#?s4IQDe$cN+)}_fMS4zQ%zdAYEk- zjj7fuA1G6J`oN-Ljpy%&03|;cHg5q!JT<%#f|9KuVcnI}4r5n_i^zi)(WXB?^?i}w zu~#4E_W_{PcLAherB{nTo&|z4i>y>;-v=pJoSygjZCC<1 zy|Ww#QvP%oFElamspG0Ij*fg5dUF!7Vahhmk#r*RQv#wtMc0VP8Q)P89JB+)Xv?-( z*4${4Y~Tb`R0qu5>CacdK~WL=exU%%ERadN8l;*ZsL?ZH>{xf~9$-n~kmkVN?Tr)n z#TYm~qQCk?=$qgc38wIaI;>{VDn51U42{Ido?8__ zXG)v~CW3L!2Cachmx3?4!)M2LK^bmVB=CWxxRSr^W&w06Ks8ri;e02&O2tW{_*{zc z(VO&~$!U_>xKwqlt5|0`; z7Yps32_7*ZPaIzh72-F)93S5d2?CpLg-<14la+-ny)xKm7(T%+DZ98=_QmFXWg*PE zBN0?6FjJLJPP&}PbRjqEuSeDE zrEG%@_up|;Ye2D>nT8E!5`~qf2wF$4&cPCiiIYi*7uWd>xeHy=+lo3>rITdbHt8lV zygu75;BO`eEcxrl!3+g#@VkbO<>mb$YW{73;UG{A-rKiRk{LaIheKnkz;z)EBng}Y zQt*$RPE(nW3P4-ZrBQ+=ec`8Nt_#{+{qRcNJmZy3VGgm!v+7SH#D7HO1DBy(YT97* zoqSMt&!<`-k=vZa;-Kg;;h-=rkLn3^LtJ~{rrDlx+P$mlcW^K7++x1%LBjMF7{@>0=>-rHeaNaOT7%p4_sH^4lAh4j%pqXe18a1MR_;J z=N&Ouu|asjnnhA@5rn~twyJ+GYno8c!Jr)*EmtJ2Ntx`?xuii_lyyJh*)cNB3cC?t zsRH*x6%>BHT5U;8MkAvF(}rE6FXPo5t`8*4qLJYo1brW7vJL5b@y(!f0h$W$wAFfb zQOTEYiBC_M&y`i$Y}ifO_6blc+3DFO&l@Dz@>&dM6_?|C#GUPq0yfwa7CzcrI!6Bb zFJjjiWJ(!f!{eS&9=RL7?sGOs9tD5;?DH4e@&&&%#UCUt z*u6-*W0gjx6j>`iE}AZQN#*06ts2rxe26pj5bH1CqX4y0sc)iV>NoLJR_ZhA77bT2 znb<;&Qz(QZJ_rSsdBy)lByt1`II4W4Wvq#gAlL7ABAW@?H`zZIe?dEjc{Hy^$DmIm8S(2+8jxae~zq9uuf^F!d}s12I|roUhIK>|P2MrE5iclBVs6b^}( z-JnpArEvTYwnQ_pw|WZj>+MP_q99pdAVZ&`!=F82oo@Vv9EHUH| zIPpEbPD;3Xh#;EX-#s1*Sh@dd&M9T+-^kcCF9%3aBP}g0T@JQvc=-8qy33-Y$;(U( z4f!}YILr>ktcMylc}2C2>9@7$zb+2CqmoGwFUh_=rdU5C4gYvjjqq5yn**OB6`JJg z?E!N#>b)jCP;B!;YU_uh3lL2vh3Em5y|?pkq`gb+`}5@s3kxlPD(p{Cp8doaDuT^2 zsjI~<4HSJWvPkeReEjYNSj4wTg<~9k0w~T<5<~nb>;Pc8_~qs0$m)cvR_!?A(K9Ex zl0fpo)SpKp$0Lm^~17CwmW>0QsZA?)1XoAWT2*v_;Dz6=6 z81imXGD~sG9(2W606~GxER*B*8F@o_yacF%=k!x%>A{kqogV_vXPz>NYyW{l06gvg zo~x<`En_OYtS^Dn9bl^Q&AmUg)!DL6z`_#=vB8?4xq;#7&ho$%pL)OWQV%%EEEG;Yy+Adt1*jn`#6iYYfBbWXNuX`QSD@T00Q9I6?Z;|IdgO;xVAj7fG+r1u)%589 z8g3A(Q^iV6Fgz5c&kOJ$Q4>9H?#>8lyGD*QJ#|}xFYA=c_L!|EXI;{aULFNE`5pgMWPiQZXnJKlQgiXq`|LLFlATrjeEwfn^d}iR)gKbNJ0lOLbytiGFPAda zx`Y=N!L(HNw#lxbi*#GieEHdEmKldPT_!-2?Br;0_~z;Q+r`1uh${W#>SKes1ac%h3ro>b`~VMyakuQ4w%UuTWDK46ur^~{?wySCXX$xdOri=Ola zqb~N?)6&7B$kWcqWS3z=`iygNC^`|X;D!(@+;~!X|aV2h*++iWe0lL!6?_;fMjMOR)OV_0CI?}z}R!eN(@VP z!c~7t!MHJxX6;=1Od?t3$Eh*UHRa0h|Is{i*OvZt?&6iqWa`*m+i%XB#!va%$sT;E zc{6d<&@&^JEOJAR5#l5EV0R85Rd0FZsFxj0yo z1=^!F1R*v7tm3!p%Z+E2_9~ZxvhG>S2TybWT1@79zzz{5(>#EHH(8IWW1nnJ-`>?_ zJ1T&uIWu=F4(U>zAlt4gv#eUaPwwf_c-4+>bl`QsUJ{(r3#D-09*wH!ycZ0KPBQ;2 z`7EbY=aE#S`>&EWBatmJySB@#5c76#QFry6^mh9dZnU{P+>xFyoCO&Q7;0`Hnc)&h ziph#c=(Q&S{Rn`sDu!KuX^1$DO6@lb`b`9P0Nk!Up%_v|Nu4oKF_-+fmv_3sgzSu4 ze>?eT*$IThM#ND`fzYb`*+ioXdj1DMFYE;*l@Z<^OU&iYzGs`t{5`-6W;=E?d&9_> zuXan@X!4Y_@x4uTw289Alno)TNI5lc0BHt3`HDyVgl3|~kV^}8Psg9TvwYk`8Q-y6 z>0Mxwyg(kR^&DkiH`8xlNHpuQfUl-maY8CR{b7t=^qcJ33q?r%WgfAzx#{V0D%&*4 zeEWv9kf~njaABZ0^h{?u!8%AKD4m1=`>a7bv|s9p~S)g+< zY?JKPGR07gYRnyt5m#MqH)O^`0;}(s_Kc*~bUol^@KnuSt4Y!!l{T{8oB2{` z$9^6fUAMTOdOn*r zz)~S2lT!DUiBWQ0%_7L247+aIzMJx~a^>+wy(aHhpDMK@URX7XJn}l9&BdF1xew(T z;g-!Cmx8R>@_Ny>YpcOz*PbUi8sVK6+DL9`wp}KKF^~PUn^#*X@%Rn!ULsKtJBH${%H$>fo5!s z4@Hnh8!mXpa&oSRN_Sox-9&v;ddmM0E!99faSMjd;Z6CXH2Jn|mv>cd9dhe;)wS=a z@O<8Jd$LL{(mthHy$unn(GM+kPt0wHqxs9PbQ-G^9#x)bbbHe|r376t)Ez$;ZNV2b z(<9o7n!9_ywBg_lc`L{|Gks}W5VJ#!x6vqbdh~dI;Vyb{v@N^&UWWaz78lT>XLD!B zsY*5%xwsIC$c+UJV&d$=uQ)Lk1os@S)vfhjR)&w?V)5-Jq$#}-LapS{5`DqZ)Lp86 zwQ5aanh+-)LlUJ>jcXvV$ECZG5@45GaTDgw{o{$)-UW|q2_(*uLJ7A7+iQnd z!YD3XZv#;`LlrWqPi!&9ES6!KuejrH4v*w1YD(Ixi4BIIwmSrjFN81BzsM;#g$#|Z&?6Vdx}07oV72ooF=mA zWMvK1@=5%x<-a#)utxTBiDt)u;Q0XhxqWXZh$6e!16ej+0p-KWtVfA!0BJ2nMv0E^fyxeXg|_nQeKkB?`Qf~iVE-?#%{&3~VhLKZ0pU9Yu8boo zJ(dl7cd^mIoJ0+Na%|p18YSp4UXNe)wtu(o)c4gbyPc4uObLRlMNq=b>r7pbCe0s& z(yZPRYCVp$SrM$=Q>X>~?W&|sq@5YHcMz*~)ERtV*(-%;^%Ac;xTrBqQ1N9TW7o&c zBA?;$Az_3nd*?_~$cJKbEqkvM?a$QTJ!&a(kY!jB_`dbr&8M2TrdEVt1f5Jj9M z#bnYUblX6P)kq$hHxWg0%KGxl7=f6>)F{U^2gM}8JfHZ`I+xj{&V1tkb?q5drNy*6(_;=i{!_JlF4$NC~e z3k&$~Y^(hsmLs|R9S$1iqdgNuZz=n)btjnATR3#xjs&BSJ&qS<8qTj?#b;3ZO}Z(c znMeH6=k~fCITN55UZg1v)WRJz>C(rY2w7P7OeXxKUFTyDV;Gw7GOE88i(u=ek%#sp z2F^{D6|(zZ@4ijM!=Sw@4}52js7B&2jN+7DjASZC;H2E_ z0a-1QUbp^)k=P-aIEcm{>Q6;<^gxIoI+nG))Tg$)I%4E0LJ`S7FE91OR9e2A42i!H z)i);mlp+hkYz-p?DA>&mA9HTck~QwJ=7&|>dj(&sJ#=jT!g9LPK(}1GTeBvfMOV5( z#IIA>#6d$5RTrJ&bQrvtsjUJ<0m)TP5RWl#4zX9n%^+*sCIq}889kE`EX z;Qn&$!%yp*kN97c?sYi%U3tN&F)4cl6(3|7x-97~mPHrLr-UEOmti{JNplf71=R}i zrs>5?>I-sw^oqKp52e zToAEAfw*>w+id<8G6U#T;agUq3s zg<$Fdq^vKKlK62|XIBUULPXCwf!4xh!%s~WE*RWrTADE3zcdua9SCbIz!A;D!P}eg z=eUqNpDA*wqv^a~!8h8)cSd}Nffj+}Lm;4#gG~Ma=w`8Lnxldrob6`(GU0NZxD^g3 z1RMfV(*pNQ;-Rmvi1$7W+G_xnGxUwk-AvgAFk@WJ40zck3$B?qLKcQAiV%muaHy3z zj#z+3Y`9br&^a`P51B6qf+4d?n!p>e1ipJ9VN_p^0T*EQ-6t2pY2&h#Q`Y_?RnaI> zYz_3eGQ99|1^DxJEV@^rB78rp+?I6vQYbG=v>oq`bt+dNp0Ymy6b2gz8Pf5H_X67L z({sFRRZPBv)5?G#q&MaC$jWo`E7#03$<^4qGW$&$Bhnj9w0GqA^$VN$UrRokRGRxz zV%^2Y745ek#3F{jwb9Cg1vQcg;OW0Bl9C3*!_PEjGOLY)?L0JpzP7x2>bDb6l$LU` zkn%Q{~)RUK+I~EsO!4blJ z-%}OCh6RtRQOALF*_K!gF80IK6dU1-tp362SM_9VH5--7R)-^W;HsduLzyJD2PC{V za^VF8^C+@Id40xDA$AO#DAPt_k~Feb;Sy*H-=16Qa#`K6!;KPLHWRK_tD*aVWo~sz z(Bo!;(yqx4nK2=b- z%#2TaGoZvi6V|HH_Bu#>CvJ^F8M=*^!WYhl4xCy1-6{L(W3rd(I5eghgtq3ELL7=i zj@1Nf&Q}#$V(G=5$Ngy^TGu&^)vg9rg90ou1Ss14!kv#74D!6;ynt-enlQHnOS%ZH zzep%=ZuZsBR9?h1{MPte6afhfQOBz`! z{9t!&?dTIo>_FhMY?gY#hMQ?-FmWv?gO7vewiBb=!J_29!q5l5QY-+A@?Dh(%~!n= zKE0MGSucot51=pbu2h7PL^p$_Rp9bpkV2#GLOH`+kHy8We<|5_9cag zi0{RC1W~gx{opDlMhpC3?;b0yP#m2LAwN2GZRHUSdQO!7-=hCh7z3D4Cs(Rs_=9UFHNAsFb?~tZ7#+ZGbMOukATD}k|1o-J!;lBW>Ho(;?V**?TX@KB S?Pfgib6fe2Qmz6j@P7agWGyQI diff --git a/assets/chart-values.png b/assets/chart-values.png deleted file mode 100644 index f7e0d708f20a3e4e151faa96cf03647b634f5f0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37571 zcmeFZbySq!7d{Gz(lCIeQbTu1cY~Dl0KyQ`Eg>Kw(k&9w2q@j11Cr7p-7VeSzjyq^ z_jB*M|J*%jmOwx#kH)w)ehmCgX)3R-jDX-thk)=Q00H3|cZWt z0)imWhR89sK}{I=0L@We*BJo;10Vh$5g|F180cNsN)zG&QC1Q(b+BVIe&b+b#^!G4 z2waVTAnYy({Ay?BVod37XKU{)=q>{ObA=%AJN#{SFy)_1Tx>+Z5am~tQVws;D0$dk zuyKGz(J3h@h2Oq07gU#){ku8vmk8L>#l=yOo&DXrcWm#t*&NLLOL!*BHOzrV)mVrBln zJJ~z`JuP5@?C?+6IoUYa|Lq%SDhz*DQ1z{q8E|KK|Dv42f3Ez$y!ZEggxTRY|Ic9l zn(3c+fvJk33$y>bY@+B&ZS=Yb2;v9|(h{2Ph`VVhsn0v(A2RTI1u&42Vvs{pl|es@ zDmZ^2n;w$%Pm5A?rPe?4I=`}w@b*RFS1JpYdqFp2^!6#S5;h(ZqNF71dTQnUfqt_? zEl+=tZn2w=P|Giioc9d{g#|4lLU*HuI;DEWM{iz&DUtqtVaa}V^&kHhCM}Nq?+XtR zjl~=H-#?|l`k~1i@o>D-qW@nzJjgY6$OqKFU*eDOKebvLeJxP=UoSLer(_^R+`nIb zPv|K9##_a;(*9}l0kw(%2^Zv-L-mUIV~JLw_PE=(GTWy``>7`Dh0M=fwQ5<53SnoO zI=;(CZRg+?s+sTmL3#Ao--_WqXv%$!e^_D|wsduId5t{r$nP?i*=tzmT~h;_rVcD; zHPB95hp*eFAox~&JL|R~XZyOcbHFI4e|=ZfaO$nGo1NgDvCQUg zp0FE}`q6M+Iq`Dm)rXq8g|?j)=k6#n4Xi8JcxG=ec&zzN&u2|lMiwUK;VAgfYeS@e#V@4;HyqGd7P-n&o{VMvilr`yDj;l=tYx@d_D^~Mh*A9 zU8QiFcb(MDRY_y&L;=6K*{iL^SKe1`uXxFqLh@%FT13$DOMUNl+>U$bXNh2kk8ehzecrSOVO5S~%jF)Yo?P#g z&SDGg8a-TPJXitK&q#Cmr6AjkN*FK`M!;;{wC}n_PY?_V^}I}NNg#c8p;V-#Dth(J z4tDk1t&7Cs+r2ZE$oumbC22OYUuDzOe9Jwlqgg|3eD|s+nMj_7F<-wfT72G@YqFjt zcJ+3K2oaaT8R_0MlLiS~MvhkJN;fX*dwYj?lji#^<)*rc_c-Ca+%sCcl$ zE=_u~4QDpuElB9u44e4RP}siZX7~uEJ?vHv<%qUT6~AF*FG<63)T?te_WZ5{Jd1A5 z?dZ27vTIh%a04a zDYl^!Fs>ZCON66g^Ngn+aWdj#agq^^qBkLI$o~4)yA%2j)lREjv=r>pFEN8V1vnOCdK`rpv=o5Q*1R zKKJ(*ExF*=)S+DM!8o-o_h+TQCt7-aJ_T>y{=o4u3Tag4o$h#oMH+`Z(Z}9md+Eg4 zA0|dBRqg0M60c)u)&1N#;%t9@78G@}TNzz1!-2M7HJU>kblA98GpjBt`Q?~C-93x! z?N2IRt6$&NsO4_{iUb%5ut(rwRAkUA{n^W`fhCSW9$AzP6ZcZve1Bb zK{N^RHj`6ekIkt19LKJ8L=j(g@xhGW%{c{o-^?zmn1OIFt&Hn`km6&Xj_by}+@qiD z5IE(&-j;Ij!KA>Dj<6TpZ4n8ubuOO)jCJ3It`dLlW6wmn&;6Pr|(B zH28}8f)%j?iF*koIZj)|6H-L{B>TCfe?Y7t_fA=jQX&TxR=3l04D^-ow~&Yhuaf~9 zSQ*>$*Z>FVR#D|(1z_qgbe%z5*^EPD3v;q-~;iMpz{g98>9127g9z?XF z3fNu4Wf#qdDRBnMPX&r=RG{O87_C2@?Y!*IXPFPQ zp>PeVj;0dLs7`#oL4wqIfxav|e+l7i3OFlM8N%D;nd>*?nxSk%0* zdE@+f*w2&14-27pOjsT&>B0h6O}H;VP9FF^+`RGo_Bu5~F!RqOsDVki)xIX0112#A z7A$l8bg0%Ajf(rjKlSOykHUCTTFawPajZL@oKdn&sXHwD3wL03=cf>zaqNb)s4GuY zs$Y|cs=U@X<#uiITWH(LFcvPM5XD3xck0091w3IdM0>;{fIc)+s7R-S)Rey*(#yT# zbsfDbjD3j7XmVD-O4U{~V_kR(=4ZL-r~GCB(Qufno!M50p@xX}(Et&fNB=j^c7gO) zT@Ymif;O=v;nIn}`;zfc_%gkDTTJkQ8o&RB(4bUG2N0~Ug8AEnOuM6$E6*6@nnO@} zSHfKvy)VA&x`y~Ct1I7q8#+z%!TU^gz(VdFYFCAuVfw5)+%6k)0*rTubJW9gyXWAm zh(yPtuSQ;o7j*|bavxH{PKN@;FKjltS(5!G-r)@b6hC?$Y0wtgqod=`JyxFw(UZ{8 zP`x7g7~m=@ep1Db1b&qcKslzNo039%X(Ztek5}9*?|Nvz&>(j7S5e#_2KOkC zQU$nE+CS|c1fuBLj(?(rQ2<8>>sesg5|^aya^gUdL`N4Y1v#}eqQ|8jE+SbAOIBv^ zX78kgNZ?%u3ge>canitr*u#lfkDfnhEgAiWWP*O1My3TL+M?d3MT02RSacAB%AbNn z>Klz$`e7iL!p%0cQRZi$O-VgnpmD2!i2Cb|u4fLZ*ql3fnH`@fH%$FL5*?MH?uqAx zQb!AOp}lAmp`>{@{?bB|{`%1#HA{K2u@<#GaV0*kD~rB#81E>`AM>V#n|IS*!)6y9 zND{#}v#=BG zgdE`cH7f1hg23ZhX@sS|ryq;T$VD~My{>zby*vD~*t}BZF%4GwI=O^HFnT8@hGwoa zm`dEGZb#%kx`W+_?WvcYWgy6@!%3ZTVQyQG7>f)$awL@k5fqc|YdQ(_zfH&2&b?K{ z3q_@tD>gTYF%k+5WPLQ>z@qHLo<$Lm(m|Ut87>uX}L|PfRtNO2biu=!_YNer0Da9l?1vL}a9PQ#Ecx8(I3R7m(`2A2d(K^Wplr*KJVD zu^9EUyy$o=8LsP#byrrR;;0#%2@zw|pFi5(w)(jms8mN6bW=8in}x{yG@)eswjX_H z=%Tn`he_DbfNY#>s5aC44=M&cfaryu&WsXTz~L1Di0w`{M;MQO5V$V(h8EMZn-9nqlTlkoi87dC$85EXJo#rAk(032jo}0Is|VEQ&B^nCyQrZ zTF4lh8SlaKFmZekkc1&^TB{?6;ffOn@c+c2AY?8y?L92G}~N65VYfg56{5BAlYpN75K$^tOBcP^DW zoD+O{YP<1-*E)BFw!^9)_4Ez(Q?nnNS|!$bJm2tKnd66v)>g?qn2>xuo7Og-S9WR+ z^d7bVCjDC(SOAYjinEeH(2`#Su{cQN?sU{@#5e%48|_n1R-1_Dsl3h&Jk?OAaOnrH!$Q;8 zkYTgu^f!grT^s04a`^;>H@(wBmQ46A;5@1vym$1imfu`=I2pz;WQfrpuMn?Ku?t79 zj(t%L=_4|9r-~Qb{OZrtXpd_LuyJX``|cy zlMBN)`G)tT79STP`P8?Y{l+0WmDEJ(mqY0ykuqUW?=!6w4r58Zl|U}kT);{F!WO%A z)NgRH!h0@Uhbs}1;@%2%9^g?Jc1%U_g2cOD0zv!6$_|Djdf9q^UfVJa;Cj=$>$A$M zlMQ-5Ss_Rs2r~rSK}JZmohkZD)%SXoW)2R`-_Q!~mbI&QoR-C{cVcW%bx0a@B&@2H zysG*nht3;#!&te(y4^=1MJ!9^w0CvOPY)Hl3cL$(E)Y`j86%kBnh7-fmOL2AH4_Hl zA0og(9ycsD%+OJSz>jdx&SxEFWrz&kKw{+~>5y-UTmXz+#a!=nG-TR!Flca5XpEEF zJQN`0{Y|`;9WDCO1Ras@?V7yMVe6yV_z2;dS@+e*X=N$@u^;6iEynDXV*Ofsg@{kV zWk#2!4OHSY`*Zb`1L-2OT~TBy0l>50Pn-$cNf!0?Z14#f;hnWL)wi!Vv))>b7E?hA z%IxJd?V9?4_~>&Wi!^?>TqLPkhWEMd_Ku3sUL4E2RHc@(f@UDM;k6w8>O$@#?=;b_X-?*p#aM@29lVVq+yDes7T|4ilco77P z5xrWAb(sh7zv?!%Fheeoy1s%TudNxvE(~=Rej=B>E(dY0grRVk3ON6!QETI%lne-{ z4v>i;Vtbu#r0zK-Mkmy*}SrdFGFRV@h5Ju`eTxxr}HRG%-0|>rrr7 zmLiy-@yY|yqCS=$`qFZ{{)VUL;)~!h&f5S)*W*^P^*9ank>&Q_*&g}~+lx78Ri^Ox ztxrLad?ew~dEbf50*Uy0Ir5(HKiL zL*nF7DT;J4Q$B-AQ-ftb*{tdv@|Yr8;;o?D5Sc_tUon?9aaFgUt-hC?ckEEzgQp(A zo?;%C5pAy#+AWh3B(mpdt8nXMt(^)aGECgYa7sqsQU?4%5!@e~?M-&_J@ErEU|w)4 z$*xHBKe`^%P*oh89Qh(QV*N@r_n>Ha*`onl_T2rU)!c zV5Ga~W8h_qw`BaVnH^2HaaQ)GJTjA(e2#|v5&|`;%Wn`R#Mi$B8G9R}MqFoMwtsE{ zkzhJ=z6r4u2RUM5C@V8==a*o9M8+UCVeFt>0fm#=4HK~_b9}{|d_LJnNF|k(#y@Bw z&B*;s0ri(xmTfALT}S;0^te45t0V?6BALs~=Hp&w1+V)h6ft=u_MoTPA|=r$`Co`` zI83LG0A5A8Qn;K@_RcB228gxmisGV>_4fy^>ygqQP`GdwfOzym*R-TzyWmAc@DSd; z1Vq_!3ERS^q0Z2)UE7*sb* zHS*-Hl~hWUU0O|a#2z*2_UKmBj%sedVW(v6AVB;FQ}9$fSmr0{Z8kqPMi6JYN=#nk z;S7Hl2F5r*IiwV^`mj*JqWL;(sq?B+mLPXERo7FVlkF4l#kdj_`tuo*Lv?Dq!0Q&T@JH8 zFN+TH?a@7cPSkWzxXjfTxi*fD)z+RC?2;NR9umyk&EWqtoQ-e~O zb-<#MQGjs!+lgWf<6nlKQ2J}HU*=!Z)8fa3{J43zm-efa8zga@hSR!94%TY!ZP1&mjrP80G!SZWP)sU<`m z|F^gRzsvt)R|9%R>$#~L7dDMW$-RWX348`84Kxg4sptQ@A*Jfqk;jC6CS^WE>=Oj0 z9-(bdyH7L*O*2ly(qc>fE8;TKy(BqeW%+}Pymf<4Z zo(BKYsTl1lsr(~@!Y4|zKrB=j(?!_@oV{XrNd;^Q9z1~fZD+R7e6f&n3_%YFH0i+@ zkZ`WEdCps#oWiWGQM+xoP)I#>4FfO=}R=GxKf=U$5R(FT(fDfIa{6 zG+FEPP-8cX`*$ePUwM%tYcy3*i@M3*v#=fm7%O;Yr8AtF$U){-_;gt6d_<1o4?0`z zj;>8{fK3BTn#s<1p-%73g&8aDI~UAROAjS~JyVU$-0-_ijv0Zn{5-d#|F8t&)3?zrAMK{8=O5TGSU} z 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 Date: Fri, 10 May 2024 11:56:18 +0900 Subject: [PATCH 12/15] Add README for KQIR (#2304) --- assets/KQIR.png | Bin 0 -> 65729 bytes src/search/README.md | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 assets/KQIR.png create mode 100644 src/search/README.md diff --git a/assets/KQIR.png b/assets/KQIR.png new file mode 100644 index 0000000000000000000000000000000000000000..e0a605d928a72f0842d9ec6fc1ede0a5d2fd940d GIT binary patch literal 65729 zcmeFZS6Ecbwm+%}l9gnGAd;FSSrHHsiK0M z#+ftch{1nv2+o6_+$X461OJ?L(2$oolh<}-_RJZ^Gm3K351;EVBq6`+H^lTI6?8DM z6wcJqX=c<+X_r2J^0Q2(y2Q=rPB5!foydAAt^T1+mRte76(->kZbO*#JG`9D7OOrd z&8Ow{B?6ZB*3*A0nblr@$RYFas2!F@z@$;mblnD~@gd_H(#h62ImupDFnJAiw#DGYV#CC)3r0?b1L za84f0(~cK^`b!12&lL%w1rN?(WZ0RBzDJ&n?bcp|kTf*IGq^Nbf#GLS{$rOvq)MGU zn%MCy+vzV+g#3AIEDKapMeV^sI1VRca|x43Q$5{Tjoo7=FaLMi|G!=Bx4b(uAHqNF zFIDGP{dvXpDe$W6(ev`ZdE;(bXn5< zP^P8-B7N(yDx6`IT|HU1AuMKh8GY=@vmPjUVANx7_L0U>;`v-wW?2C&soG^ZZ>e%R z`M!zdM$+xC(?3&wTKYb^{hG-@kT8k5ArM6|f$o^0jQ$JGcM#nCyp~Y^u}_Rs6I?_U&EAAI&Oa-a_ZHaQuG;T5^y}&S=~$V+D9HQ@(>=;4C$uu%heBT zy{&{Vj0G~Z^*m{cHBB{$=CRXVl;@}w#K?%s!c|TS&s$zgCD;Ike;y`+WX6cOogFcQ zl4N5?`Tr=%rAQL+=u6L5FDkIbZ=1a=>ghq3SGPAe*KeNkBUbxjOo0$9F)J(4A1jo9 z9=p~0w(=FNeR3r})uM^cA{HJF0y7m?pfBk!yqb zLD)OYYEs6QueR`M|Onr;VDN&j$XHj3L( zl|nYrV>D1=E1Vs(UWOd4o102R=8ZoYzJEKVdos>Izsb3R_)oHMVJ|(UwP+dTRebAC zx-v{$7uB-r^}R+Hnu|8UGJ}4@rK930Yqup1H@ci^D$H9q?K0I_HXyzYVu}s;te`*YSV}w`%C&xQ?yQB%8d3gs#7{Vo#w-(_A%&YMGMHgsCIztJK9K zIftas2rG^zI~YTy7;RXo~UqI{T|lovGvLQj)rIP zxK({FgqoD-GWyR>0#t0ud^IaF9$tn?$?-e3N?2H>Zg*W} zlRWa%u|~n+x}?a2K4Deb;TLwo2#d!?&T;&SEjyHEuS11Lo|`OAE@TA1n8E+iCKL6R zKlQ0Gm0Gi4usL|@+Z`eC;?%aX_3gSt8#}TVr0Y?FwWVu-j99&Sk;U5Z!^ZNgR{WxM za>~&*&3;Z27hLy4)uYXEE4}NN^wlnyIh77K%%FFukKgZ2A0MRjRzF$7yO2=P+CMto zUi;0?$-3=7ipCNfQ;``j*Xs6&8*($9&m)Uybi()zcS$X2*5YfW`JEDalgJN>w%zVI zrm3^_nUbq3>^G2gyHm0I^v32De@1^fd!e|vYE;d8dsW}o6RPU?Pz=nod)MjNG*u;p zMf+VpxZbPpW;2dkY(dR#IvgEes+Z54z|(6S1-{YRp^ocrjt!Esx3Y++ushiS4t zIP;157kaLax4jn;s<#k99{$$x+|70o8S2k%M_4Zzy{Aa#+BUm&StFAN@l;23Gu;2_ z5_g5^mn7DUgA){e8Dez2TSK@#lGi%7ToFIodHJsoPk!YhleFfsB6(py5=@P(|IBdtoHSBz+xKfov*w3f zyLO+d7BMa&mG4#cMZe|tv%V=tJVumptOK*HPwhu&$33h)SZhrjq0GOvSJJjEyUgup zW1$Qi$_wR>dOqjspT&lFrWgb_)x%h)B;l2>r@x8M7aO2h5)oYF-q}P}FP1fqK3bh5 zsVpX2zg@0pzct8ee@I^zpM)35)RdACrtbeUU91Hz=BAhb%cjbttHDTZ6e>D{UjLDQ zBV{*79mxx_atxMYb!@XnX(!Y_m4Lup`sU=W0YYH^G=3&XZCePwh&Np(2D0WHDrP97 zyx)=4bd0EKi1n$cEv1^@;TDD$B^ha3{c^UopsJW`U~bgvS)9gK4MrM?({GicWde_(^DZwR5DpQuNdw^PySZ8O@3ot4Gr zn_$;HiT@}g`C?6jCCok1ay$9VBtK^@qwtK{r<(&!U{TzHwTDfm-Te$v+`G&Jj}|AJpS_2vkyW$ls!lIgn>wnl$qXLGKf{;|Ccd2LyRUP3Rkpd12k|U8dP~c* z6ywuu^J;&k(xzJW-3vMmouc*<9qPGF5B4}8GjmpV<@?WL4NNOc#E%N$$3Gq!{aId? zs@>>R*73Y^TgB4&Pc@G-ec;OCUDYz>!7AtK17U9e!qE*MT=Zgn9~!dAO} zcobh12ht9sCV*1>gN7yj$m)%n#56)`&1Z}+yQzj&j)i?k&A*&2v$_^tkZ z_|AC5cois8i`q6nwNgyKsLKtw|B7Akm)NQdsAfyeP1xFrdgSs=WyF?EOpmwhP>dus z1^#5xt$%a%=jeXi>pN=C@t+LinJYGXJjv73TdlPIa6uNY5|6T!^gyUm|D zCBxW|RCbi-Svu~7biCc?t2%p*CeGweDc5ECcW>?l|ElqO$KfmRWD5d7E&`bPmC%lR zj3;LhUe%c>R|-Z%9}%~FQF|$6cUNu{ms%13t+*c}h5+&C-A*)>_VNE&LkIw4{7rtX zX_RvQWf5F67OA2LVFbbQ+mc$|n>sH;)8b8y;O!spQyS_%`tXWZB-`|h{q@VL(XV{T z3YJK6CN1cal=FKeT29ech8zHb&ykVbBtL0L&*SEZowIr|z&6#HT^eqGiX=J70oohU zffIkdao6|k+4;85kCV%SD>p0`KU_J1i-tgVF(&LKW{ejdByvUH(%^>Rd5#~WM~gd)SJv{>gM%OK1en-3f50COw5JW|8ZK6tJ(G66u7yh(Y_}P)p!4wn-!we zys?Xnh=R1>oG%30F6VpJN=VClta#K1FZ`8G#su1Y_5v3gpQjqoqx?N2I++{t zsE0H>&M&=fBlcH51Th(k+ag*+EV_o~Be=g0f4yOma`K1$kMlQ)UYF7T_3CeA1y}KN zCmTvEmYft|Q%hrSN`7J!tl{myW={azu(r9OLNu-1lPwX;4)8M?4qw)8?Gh4*)6ezz z>v1Z1`gERV|3m;=9u=!WaMoXvV{rq3Vz?TNE)f2D)c- z)q0y)c2}l)FUq!dvu6&#z-->?g`$`VEd!6zs@aT0&127-cD{ zuh69Q%5KmRKnC6D-}6VY?Qh>_KgQ<4iv4W&fc)tzKS$u^P*;MAu+j6=G>RL}AKQJD zL0FTs(aQS;^yxb6u;bor`QkBe9!fvJbx>5dVav6#U@4J`ptd9hTKN>-v$v47_tk? zJSczMM%yCKKi`7qVzl)8qsfA_-u(=(J`;@UJL5XaB-_Va>%0XE_@cygP_Q4|5@&Ru#8L zrv)Vo?T*>{6=M!Pz_MnuJTl(z)-uprmh%(iIN#?Qd|JIta(hAf{ny>0KVf_&bX5i| z3MW2%?i9PFwV%EcAx2y$aKsz9UtO)R$ac8IT;jz!sMkDF=iyd6&R7jUk~bal++WOH zoWV7IC?)EvAyMtT_^IvfElRU6r_3ije4p+=^f}r39E958Iz4OAKeQhTfMB}p%Rhfh z<25N!Q_L8(0haN@004X$)g*;Y5$;`wEOPc1qCab9UR2I);oLPMP>FbX?1LwOUW`xAu*gD&tj`;~n2bqWa<$ml zHye5%>UJ)9zs(>h61|&0=4=M>GTn*mdvocxah0I*5_6p`t~paKQ2|C#OL5dDv*XMZ zC$)nvW|a3x_S~DD3!t<8i0{oJAC@yBHSwL$DtMgSojb@FA9CkiX!$b=Oi#b z$6GZS)uwmE$j2EYyrBx#t_ZQO=g)z1BmJsI*C~-7YD}3*lkbacr#* z`*j?5yk?;3wG-%7A)3+exUB3M%QN+$nBRuuoWJ?bzT{dfcvFXOMK0l+ia?E z1pk+yh*UyQgE-ghyKRi$8n?Df$V%Yukv&6GQZ9XZ75Xers--_8f?bqn6%5zFuFB4Av9C;D`}> zFb5SqV1g>Wh)B)!e}q60MuI~~eut7E+$q4N+B8s_Z9eb<{imh6{iSOvI0tF<>acnf zInP^XJ001xL7p^s4~?ybU)@xMsijENV&qjZ4?5Joi4ZuFDO{GljKA(jjgh&!^mI}2 zU0NG!Lu_=FYIBa(Q`aj0KV&qaQWc+;^cxwucI;JVR>r=T+Iq!!=@pS(CAV*is%I#1 zB(+qAP3)JA{v_mkTHT5Pgp~_P)_n4S;cs#hZ*Q=te0$c;+aAFiqPSH$ZCY;(RNYXB zjLTkHt5FMylaXSZx+t+@xCw|(+r3h^pz4)@QQ@wjv==vHZE2% z8A|{D{^mya58+X3CQGu*P&xZ{SHkNKkm;Vr<9bIEx;{Af01U`#I80IGDhtzZ@(9!S zi@h^v@y-&c_?|@Td&Q@a%P2g0b|o{mdm82u#>z^1qNU%ZgZ8X(hc5WgkR1`(3w^jp za+W}joWtWvNf{rJhEa?HjL3mtN|3U^^Evc_LAgd(|A3v-#)(9c(Ts%Nq~rTQ9TOsq z6?7)toimEehKDN|&{7aG#++(?cw`0o6H1q(niM36q>`<1Ff!L=a6_d*>M=s8XY%YW z{@=}ZPSYc+7Y~g4pKxL7uhdk$6w;Qg!EQVI;!UC-I5NR(CsI-V{2^ae6Kyv6bfxBEOw1;1n$|5vGNvns6r|28H3Rh2Z!Py+bRmh?~PO z0##wQvn1D)>2_3nKLepPI(0{cNGDcFK%xmGN7#z?IASq^u3fuXs3}cr4~WUqXI+`Hr1qp8 zDeei)og`H^d``OX7pysXyO2?+UaF#nueTU^Dm>y(W^|R(I>!?!#vb*;O%j#6au;!S zvhZ`7-$S3jBb5Eo5>9-l@|?*4!C&z;rnFMF7H4+U#o_c+$`Z5Xip%AJJ{+s+cF;4P znfu*@x-x@bl18$=u|uJV|AKi5oT6aq8I%^vMG-V|lsWnuIjUr%aFwu4JL_6SsW*|4 zI~LhDa^FPrA}@@o6!GKp@xCKvbvn;kmiEiW-eH00btyeD!53~-mDLAI!`!d+K6YwL zHuUIlOj-YEREHDy`~18ofbn5ckt3D=T=i8Il#o2u;bkmT#bWh4Qh0#Ja&eS zR+kg}&Ug;c;unVK+K`+5=zBbJB^ejxf9tv3S)0@BwVIv6ewZ8OpWY<3P)wtPoULiH zmGq=duDEGp_2MffjsCg6aNgb+^gdh4#=c7l(Lyv8l;ld-z+?-C8=uEDKBUt5`Po!% zgYjjG>*rV8mf(f@rp2#5-7UHoEykbm`sL==L}os%VKyPre5nV2c>xN`zV)!kG_hjW zXS~e5e*k(AvcIGWDegJYa@#r5ifG+;mBsqX>tZEXqjW&5E-ts1B!Bw3pO4~9nY1P* zyL2>iCO_~ae-uiDp^iD^Nl-0nN6-G!zV02}X7;@beKe2O zPZF_vznAE>jufima`tCSCshthEF4URa>S(E0kel41186zgkCa|dm>28DhW>9BNr)5 zh{H+06Yq2<&fOL?>~p)*DM(e?JsEWiSp&W3aCYn@R$&i{`MjZJo3~<^>>~J`nR{SN zT{&>u2#UyK+?e}`rl};7h7HZQ8lt`e^G8$2qbHj{hC%Cf6e`hb4Dc6a!*Y3{WTz(e zm}qPQ$v)EjL#IacK+v5)`$k!;@NMt0n-SP#$D>e~C27oa!?4CZ9P=QJ6Kp1DMWoN} zEB>EHB+2qK-3XsZ4F`l=uejiGI_Mjw&wJBoZj}^BfAOj=ss^155TPd8+ z8$|5rc+CUCf$9>XtTB|{s;q}aRhF@*sc=(J=ze1uNYIwf1jX64h4-8(aFoCMLdV-k zDW&P_Plofvhzq#{Ic=I}${MoNp#9L`-PZRT3T$iNuREWh{op|04kdJ?&<0yXr*}9) z%MsHwXIy+PrZZrqAd5^DJJ=7T8OBJQLeE#Zy@OKM;N+jxbygdA?MrXfClr>}q@B5z zb{!+b6!tmPjxf#fyh+dEUXs7IC8teLw_@!Pc2kw3(|-lAlFeuKxfVKW@PSdk8t=)B zg?5O?fQBUk<*y;`tLF=?1?YxwD5_ujT?B zvFFjvS+@?Z+hGin`g+d&W{IZ$LX~^~=wV^upoRp7>}v~Dbga7lARs8}b>v=h+#qcJ zGdXvj;g)@x=<}7|ur9Ua+lb*8(@C0a;k@inWlEr>S^w_2(IMXrR!wUdh<)9!g-7}l zb-F-bo1NcN{c&l0U$f3_tKa;CNmjY{-Amkurly?>->_+g{~JVfh1q6Cl`;KB^P}>t zNN9J?<1Dh#tBY^`vc!oLHCpJLty@-tK|&n8m~&3mRDx9rb8(z9SRcKRfDlPj6d%Kz zhmFrJM&U(nw#!~b#A}6yLTg4$cXj{!V|HQ`$-;1TxhPO=>ABR7+LFZPO(RTG<0Djs zy+ns0rRTl$u1`$AxX{f2hHa`M%<0h;9f9* zc&ZGm4^EqbCE;JV^6?N7AzeF}s`YG3t2p!O>9gu=!Z;b`$NOAUUl;z&QN|f=^FfP| zFZJz?Vs7yL+yv-TPjX&k3+m|iN#kfl=nnB-3Xi%wlomf*{nOA>S3jz^F<#$4Z_GPX z`b>4&ha=g~Fy!_*b7B#%TL_04-sqo|rEOrFr~f+iBke z)$L$2nc25EMKJnQbBja^??abBPkS9rXCLoM9-C**LMMci zkRcA90*vi9T&09PqVw{^Co4s!bW)GS751Sb%ViV+dlNk(!sPtX}9*G~{x?)ztV{#MQD~W#?UFXF3xcB7l z1RPneS#-5HhmFgJDPxZo_v{x>hC7(zxOaZ$%{=?L3~BvUX?47a8KoijQ6-J6MXm$o zjz4ZLiX-$M>rrV1>P0~zOowT!lT4Id!k{=ial|850f zJKg4C#ItRBUH9CO+-JS-)KjSD)wzxz&!+gi?R17^e73fxQ-n3Lh1D?=q>+?V&Ix&k zl(LrT$E_#uQnbG0p$uO6ULl6&&hH^%Kck`9mfJFEw7a7yC34w`(g&g@_J&6fhr6>M z>@RTdG(9oxUIW&IJR5s3scWXs&ApTSbICg#jhu(!!`o5S71tK_M-b2ClhTr_`NrDb zg~Zq|e^ICXS~2%(F|{t-17J++ovgPI}Ocr7A?nHBpBM*|)bWug`s5gZu0zU#E!d2N9}WnlbzBiay)R>- zV#&(KZ+ZRtHHQ58H572Y(m8?u+Dz z%n;Hpv6Q=-@`AE}q&s@2^-*tUHK$EM$p=RS6z-?B9&qBWtDO35R_Q?Ar-W_I>@d%u0ajw9fiu-_)L zz^p~LN7E=iT(R>9P8@R4+v3bAE1_J(|604Wq$o|h*qXZN=#^AXxXJt%PhFfH3ofJ; zThChK>j;adEQt7k6c{qy$vgsrDH)7G&|ts*@wJmXgT;Twl+^jxpe_q`(cSdS4jAu5 z1f3i`rk+uo+*0*#IM($)fn#{%V<|VWp2#mRfn)-`QSV$R7df;(=fLtPhkNzUs? zz6c-oJP^EXM-%?}xf;6IaQEubSjUIdi$IuTyrU0_SBekLCLULGkk<{L7;|_I%qc)Y zeL~@y@dL2n4Z#JeknnQC6w+aP<5c%h-9hO~4C*ZJu~3Wl;gkuhXVGFWH}#JxO0%!! zet&B^W7rftGp;1}f1iYBByC>%LJ1t?v%2z=_TTd-@9bR^o^lXNbAoKn>z5Xt*NmZ| zb{4Qby$#p{3;wI#0!UfftJi5e|K>B_(_lyGh-8*l#^!11)_vY2EZ#}i3H6h}U3tLp z*v`!8N>i>nk27qq{k^r&-!c6S#gtFy4EIC3S1B5Eyi_i+Paq7ZAA6Tqn-fIm02(Pj z80kfte;+9?#z&e_RYgv*P)NWrNXuBsvt8qNi(F)BsBl94|@Kv@35A*FXXO_HSj_dGQBNCAYZ?djec{%>KuVPiqKA6!?kwe~qi7v*1!a{O04vDb(*mRB1@Kq#C>kjAZRb=%0;D;!?dC(3uJg&Ct z&6@-KUc*4#SAJ)h{XKbeBn~I#2Adg_M5VU!NcMYYPegMf__-TUo(u?#d9*&7fH8#k@Cu;*>tJm#%}B*+$4 z0hCGwW?!Dc;~{`I9^Ju|>~rzYCu6kc^hgUmj*kvEN8#Jl0#3;|dd5?)y+ z!Dj!HL@Ue19}``&e$aUH}0+Qy@TELP24R=>T@}l>^hfa*%U^< zX{sl2BmmznPyF5L1xe~vz=JGIZ)Z<=m|>!P-42#y+?9x|>rdeJQ)~cc++F20& zj0iDdHald|U0crB${)X`80s}-IXupE5y_bnZQzndd95L&?<_W2w;&G!B0Rfi&7Qxk zr?S~pnz71Z!@h*$b{S<}U10W<4aUczttySNhGk`V*|WUvoZUooWK|XXMyiCCJPNP8 zh~erW;j%#muEIns`)!IzYv8wH8VwX<8f%k@=N2qAIO>TdD_OiNh*!D!71$s44gbS0 z1H1#dld8dHiMb)FFx=)UG2*o(75mBK$?>+~@?k_ox7#s=J?K*<6(h@;hMfm(y3xnm z$8~$1adjoLGNmf-hY_AGQw9{YitqzWZOV#5<+HUVwtX!dr5e4auwxNH|b^V+lYXyM1lXJx)UfhWv(8ds+AeHuT6MK?qbx1 z=u?R{fm1L@BGLgMYT>KW_M@%G`}xtEzAciE;i8ioMJq&6Q7b&Ep8JR8OuM@M2n7#! zudS(U(6%~CE1Z_rP^r(Zk*%&*N@9g^I=?*PW<$`tHT5PsFpmdeU7|68NDQ*0pO=}WWHGlEfu2lM&=ci|J#j@TsBbC!Fz<@ z|Lz$9<_$|8q!Ra_H7g?Cmzui${CptL;d}7@!v=M#%qRjPAIVceid~UhtN-vNXhXS~ z@eO5abYbN2GrAlGec-w2o%nap)oBi~LJCjmZ%E+*LCC|TYXJ)R)THu&8CVM*wx<3)`2<~8&<$NUIeqzA!Mc1Zt>G+$gFuzThE4-@v&VJ! zX6FI=ZcF}?_4&So$D(Ljb;)-0UoIpdXz6XKkGXk9+Yss3H@2L-KeAN+ORND}tM9hDv2X z=`(han}~l65)BHp0CCuCQHV9cfNTyCbVWe|5?H?1t^c}wtYkYYUoU1OJ?3?~k;hxe z{F_bCN@?M=KBQBCX%K37Q2~h!Y-t``;3eV`9B{t(SCDbfDpDjr{}Vj7+Qg7A0XCy1 zgU1Frrk?DbTQeLnb2jAtE`hds5l9A%_J7WG09L7sdIl=qp403a8l1T$d%2@~z$Zh~ z9soQ{Bu31ZA~-!!#=r*{tMi{9uD9b-p%_xDH;dEL)T?-w`oMovn@mWd^sUrTf z>_)~7;YFpmXPY9Pi(<1BSOe=ZJCb|y#~)qC{HZ@-snjRB_88XgCI+v!>4;O`Nz@&e zEG0#l(k6+s4YOg=tWIlJ71zI}C=gP0CoF+_;BT-rd)A>*_YJ*#VS~W`B{TR5Jt>T(x75*pKK^YB3#{0Mx-37 zoJp_dxm|95&-fe_&1)MLtjm}B&Z(_2x)kKlKWjlID^=YbGw%X39uV9X1&Y+vQcij~ z(!}H|iFBS~8T7|bUXq{QjC%^qK*06()k25xI~(G2CE1KDqt>)>u2!{7S0Jg`X|BnV zV;yg#-AkOUpxhK~7l=8wtiKymQ+?2Qc4s}voLk;1fUQ7@HYaDj@|M!Na6HD6W8BnAWpmq&31p1Xz^nM^xzB0*MS4ZVR$ z3G7{~tp!BXL|UK)mH+1naI5davdljZrN-toUKLRkOywOXg6vAsb+5mO*$h)@wjtq{f>ExmGMaD{k1$rpIYQ7jEWj*IKxojQ=>puDUH0&UxcseVW5JnLY$= zbUp07R0$khqu0lJ;l#2_C~gtIr+YXApzab&^;8GLiEAV88(n~|RQ0&#PX72(UScGH zn*fMdd5lU`;+ac|tc>wgRKZi5=2pZdlZY4O#a zpadN2JR4E|i-YC8T>DERkOCs}ktBbPu^m+L$PjP?^&ZQAy@4u9_>GO2nomC`{0c^`9yr;C z5HpTUc#Qlaz(V|mTnlSNmi0beKCP#xF-$R)=C2$zjaWa%UUT^q!k5R(0sJw)mu@u= zMocU!L*yIZy+HJjca^^v2Cg|PiS2;iS`<33)aHwnBDuL+M@zQ&pzEwqufZb4c*grV z&JwB0(K7otX9jU!L)`|);S1ZlAc z>noNMdut8Al>g&fx4?4?{2*66R1;5XaUDpH?p@R?iT$4pAe4+`Ie&Qpux60#Xgeq7oe$sPh@kqr=~706}1Er&7R{b74(=>slPWTUJq!0mG%| z9Bl#dJEsU^es|w&?` z4UkJ$ZE4~lEnN(dAwL}WuS#V6TjH=HIQR)Jv88_l7AJoTDgkCJfqVA}%U#7Yk~i6j zmE1oWL)P&N^rC6kvU%Wyq$y}-sUj&sZj@Bsg`vkttTQEqj^Ej1kmjD`NlckssI-7&eI=hjxR9M4_}@^`QyBONjOefu%uhUQ_#~Y z0O#4Yh*b`bi>~TQyQ!LO0@+LwqBE>+dNL#8Z8wf$CjVByweXXbkpv&9#9YN}jV@uw zX%t^9Bn%~8k&v@@%W9WqCX@~S;gBBaKmabIow^XRZ#1U^%altH)qN&beT_L-$BsV@ zIpiE5{_eUsetr$uAJRsVSGs& zCp$ol>l9!G{y;`{2+vr^BEj|0b5^y5YsT}N=T-fI5C*&kiKc=vZsb8w2I@E77kLBn z=~X-A%|Q8nJOjQP08ivBWh1-caOG1S!(3r!|Hgp}Hv62t;Zo3c*(yeLa$>oBz6Q;? zE%uiwz#uQL`r@VntYw`L_F9S&8`Pi@DzUrIHq#zKME+7*s_OS0Xt0Si$(Y0tuvPd$ z{_@b|T-Iq#bSKk#1TK)^^8qgh*f8fSH^3QpA(Hu88pz^dW?D2E3zj=((hjj;15ECv zzkOi;6rl4xuGn3sJ^cr3qX4dn4Ce9?SF6n-2jCylbABT#ifyC@+<-$|fAcL);W+R~ zhlwqH1ZQ@+;_%mtlDfm-6z4KP?@lFh z8PMOdH=X<{V0}ea9$G)rZno*4bhZ^nT@HVn?KcOk^8aQBQ&l)LY%M&mQE0rs56yv9 zC_iWpc+o1(vNWl!sS-Od_kC-#_|*}%H?Ets-YbaxQ&TiX6&#LVLviMF3wL9oRWHE$dalVlyYi7h{bn!VQK z+b@bBcYtPg_gcBr9^|0lDf{*;C@Tt@i9p#_ZZR)n#bePKb~G}&@7683Z+>h&DO)Z} z)%bEfli>k{WV$Cd0XQzSnWr2dOlSXepSrPmA=%y3=(oJ&-u=zmWsm$39#j&ZmlkH! zEZY3Bb1Xy>wk+PO0h0e_u^YtE8h~q`DZVeSI>ewvi(oHrWv9zc0ZY*eNwvmRwEDmI3xtnyyfd{K5+ke z-nT({%U4fw

CSrmH6Sc}tTmn#vvtlC0e`<;{_RTSnBP^g+1A2HFXv}SUa ze(`9zdaqTf3a-m+#Ng8Yh-uIj0mr74084X&%Jki={KE$vNXUsvMdRGBP}F`WXxUtm zQ45fG*m@e~B;DyqlEs6VDK2hzly)5^GWp&HE(5TFLFIjBM$FKiE8{7Mgza-T_7oSo zdK58ktJLieFKaTmuSh4A=m7&vBFeu^IMV-TUlkzs+@qZrIHv2r->c)%%iAnAL?exTUEx6->hYovd6&JohyF=MIo#IuuorDLtg7EpdG&W)* z59)Jc9oLg??kY7%gcNQDSZ0CS5MsFYqtgy4`??-O3KF*VfT4_7W_=NHyloK0we921 zkY8Q=JFEo2fd`wb4JZi^1zaJ>l3Jz%tc+SX>wnQ7D5%JTdOg|D6JqB)YMQ#p!#)qR zcJ%%9+&Zzc&TSioB^UZWM9`|ITN1c|U3~r`$~nftv^OD#dpd{57w30V~=A?;mu9#iK>o79HEZdBDGo2U)nd``yIrV z=@j5fZcZ{(H{X<#?1xjP*rdx8k@3HITx4jG@8$e(3i6Q=9jDUvEG z09|0jk(85S!O|}vH0LbNSPG9ekt;xUOcATBOTfwrdLm=%G_~8_G{7snF;4$A-ZYhc z9I(ExJs(*s>4<4rJsJhx5HrP&V&1N}<*&IKZ~RgKZt{Hn_CU~MP}8z2oCGu3jy0x0 zl(^;lE`~buAp^4&FM?LI6r0SDSusHAl_&;j)UwOsz$4&0v`+tuvD_O=%iF4E(6mR} z)3tCIaoy57K+M06q1I8~e|x_z0Na)fXopjgV%z?%KDoH=lKy(>sM>S7}IjGjsPX>ijk8VElJrlMkr%mKAkq4cunFZ9zI(I~?Q()>7K(_ZH z^uP2vXLY8~1ai$TCH)*27eDzD7%fT%ftznXm$UMX?Pqw1@#)uSm$O@IJvHz0pTBop z)oUCOEK8SiWef?Uj!+76d^L=qk;tjOdD0icc8#U*?oix!RTv?Zt z&qy|)_7Sx}2QAq9l_^oG83Z8)3*45EwaunR!xOdhBy}m)(B-UFyr;VoZ$rbuH+38? z`5=9(iwk%Wec%e;a2fCq+g9a|p;-IPz^zPqlYPCX<+U$9@gOd*M>cm~*2ZQ%QiUNk z`xEvKz}EQ52v#2lura+%DQon85~+;xV^4nm-t}mAIvX%}m7X0cgG~^kxOJN_LL78&pqDCILH;#oSx7|kcGgCuVz&=ooFpbdT>xB2uuP0b65y@cZRKl>Y$`sb&R=J3%%Dpr+&Pl zo~_JVnWjcD!E$`4jNVZ02Hz8OT!I?C01I0hKh`_RlfZMBU01dv3>0CJ$d|W*%$cx3?^yM+F zJunE)wq}TX&I24gH4hYq&K2M*=#lrH@Jy>AwHU^4{MvN$Ul}DVawl?}%u!W>P7Z`D zPmUclB`xiBbL0i!i7_UPdYiA7QY?@(heo7+AFfO^~#gYDiCs? zB#TG0Vq@X;-mH{NvVoO2Pcw|cjEl>sr6Az1YZWK!^<25o#I#&pO%H;nY8QU_k%mG2 zGvSAXv{`TqXAr_VIAWTBg%3Pw7>y z_41FmiT?q@KmzD}JVG#Y|3twiW`ho-fYASoy*H1Cy8ZsgZ$e~GQAsFUSwj@ER`y6` zi9xc)n2?cmEFqO8dt|9B$-d2Gk1W|m)`_X?#@Kge{H~YQ`~CfV-@ku;-^cHt@BP<( z-{!Sk=XI`go%1~BJde}84f7w3>U$J@f1pMPh--Va*HMnR@}@mOxyU@|pwjh|fQ$G7*(QGs{!z^^#w8Z#R62%?0R24<_pSLO@GnaMN|ZMH@*&w?b6dnSu*QV!)4Z4f z&}VcbRa(%`gfSfgM~#yFo%ev;_=`|YN!il?=r_05!g=W(PVU{uUO3$LXO$2gfRODESr07Q?E%>R3uiZ zC_?Z|S7MA}Ua)xFNKU~uh98wcM;^U5Y3zmmE~dM1674`r~8BZkxNX8+~uf&rX%3sqTr;{ zx(dm;EnPb4$ibh=HqQL(1}#rKg^v0CSRQl4EApQyW*NV-r`dY~UMaY8o94&!W8WWV z|9OKxiV5+&*e?u`l=wH7M|h}#WG{K0()P9PBV4Pou;|2pjy@AC3F?-G9-yFL#Wbw^I`kh8}wm?e|`nV&We6T>+_&!im1KXLc4jcb!%N zzgfu;@Eu$izh6eOwIGk~DTARV=#TwbzQ#iCXz=ZN%GQn5y%P#W%-=2D8aE@2&!Bd~ zcdrrWcZoIl3fftb_|z-Z;1&HoQAY?p4LWui`tTFlUm^4eMu2ivK06Oq8F#-az<~hzJzZi_ z^|j;X8TX^tZ{j#)PKmKPqNJ3@cWrd(EtSbak!4|N9+t9@x%)nL! zKB?QgQ7SiAbDi0?Jt{YHck)NMTn{gJFbsG50<;hdK-_x>e~b?5+3k7)`iewN4h8KQ z1$weQ{Qxh=B#;9Buqj`s483X;^xhZZZi_*eUm@qArJ&mTXz?tdzE$+0ARf!w zFg@tsm-#@E<2CQ4sFX4P<;r0#Jh+UKiOa~)H_GlLbdN&t3J}*4^u9sd;O2A&X;2fd zp(I$;<%*O>6(B4SdWoH~={^MAF&$hj(7>`+^TZVjUhSB|UGN(~SGIs=2{gBYj5|{ars*VLm3fT%=qxyLTShM*Wg|qgM%XIJgXiFuP5b+k{1Nuu$(s3%FTp0( zi2iBEp~uBv^fQTCGx0dbz-!c{`-*Pw8GG;XewJn|p=7c<+ex6=Kj`4!OS0Daxg?eU zKb8~`<4c$3m7#8a=gu9b4X_L(eA9g+DH$#57>9kL) zm6=FY0&t0bi`tS`cS$kfu-3Ph;1z5{5p9!<-Ll5IvB?X#xR)cR` zd+Cp!ebh)D^OF>euZv-v#&BD)g}HW5#LQt#*2cTDY54w6h|n%oJX~%`%@x4Ozw!_I zx1a{c833T?00s*fR1g7mcfQ-YI&)m^!M)XaP#RF`0Tmjzic~m6#Vlb!!Ev5t&EH+9 zh38%-7j%_x!KLUP!fzCI&!N`mNibKyw;1{sqdf=}kqqDO(O&HbRJJ*zX(i$H%lV|4 ze0_*2Xxw5G6rKznYDynGZr=%&?>03aHZZxS&Q1*Xv>ro1)z0qkbY@*T?As#~FIf>`y$Xm3DW)PGg-1u0{zo8fuAB(Lzkm+Y5k%R{~jP*G@nso&14 zm^&k`&g8<&ur@a=i&xK{k3}DGs*WroxjfrCo3-- z+p4u=^BvVtd!>BK>Mu%Ks4~bSGjlpV77@O{)cBkIvv*?Q!DHFGC3(WPIn44)pgvcK zH^XEYBxPbYlxF2Y3nsjklk|g6 zcc_o-$@zts?k(eAyS08l*sa})Ry#CX&rYeng(zV|n;w{6qWDz{Af5KYt4`umwb8eF zj80MNb4$=qS2v%)CrM%s0XW0wb^e`G+VW6h!Hux)TysiJX58T&D5S?X+eKBuXs%67 zpn0?o9@?!|jLPehkCWSE8C@7qvo#ZtHo1UYtAePwE_F{kMeSrXXK!N}Q5zp_R+fcS zE1X8$ncdFKN&<_$*WWxixltxX?HMO_-q_?MYwV|b)v4BuQ3Y;PXn0@4N2tF0A#Od` zY~=d|Yn$7rvZNVU>`~lYmFqESAG5!p?+9Hs$%*sW{pepC!IH{t6%1!&Qz@U@%)K+o z>QoKr+8oPU$5;=KnI_D3y8?H`?&2emtZ08>)iFp=*viANPsj7|%FBp;0OKO)4K0UQOPCaW?6Q zbW~7CYN0RL3N;z*P@#d+NBMw3FG524=&?ut_*iO6~tUEd7fT~rWRJE5n6O`qEn z%r;@IiFJDYdiV&Emo`#hr%rT7*%c&Yds{Wf1`%7Hai|hBC}b81>ebvC3~C!w zksvj&ey5=~I#M7qu(mGOxWITkviHk}V8;G^st^1I??x8R!LKA>-sEKCqQ~C#Z40_z z?F5~U)iN{PC9ftp+3dEe;dMMxzv``e(_dp{+e3!RUu&K$hOahrIN6oY&gjp;hS3Csf6vwv*bR0U!?=q>|g0$5{`fkLfEKLRjCH+&TG|gE(HFLtWKxv zufFsl?l-J|uErcuON^vkmzEB3ar!PQueLkRr&p&9(wZRQ_m(hEvUi5<5Cc#PjBgRB zZESXP`G&T9QMMCQnur-j8ejb8&@DQirr^{h)ZdVcyRutDo|NEt)=$TBzNR=dc)rN9=AkxqpCRyG~r_ z;~9-KE^zX`XDFgt^fi*=!ee1l^zOy2sn@*-Vjsc^|ElqkU?gR_r^^KuyAgqgy4TEb z8{J#0eKWU3D&HAT+V?&ibDX1bZt#!*5gxwnGV z4cji=m|f)W_(lwncVpEH=PrxMzUNw~2AP5~+%)?99R{k-cYa=3T`M>d_ z&GuF}c~pu)o7kv0tS?o_y3ei$^gXBEvjLpu^CQ(0oD$37|D5A^vdka#O7O+XMZof& zZTV8W=Cezb`*W>aS@Kg>R(?u+u|EDFjWO560ox|R%v;r82jZOhY4MSn8P%#S6%+-h zZ|cF;e4_XMfs;pxLIzDquOd zp+KD0F}N%W;>DTKaD}Os>V& zoYR)CTYjURpK>47Q`s(1Df&%XJ5flI;JmRfl}B=k22)lc!XG@jOcZwAjq~q^k>IkU zxCv#jdtDpCyRW$Q4J+i0@-YIP>$xQJg^YT4IC||p!W?3h^ z<@?TB1OC%k5FM;R@euuu3}K@M{$tj$lEs(g&wsnL?)Xg zM=4$7dp6fbC_gNxaNAi*3PwRIIv9j@o3@piJ6Jx{d~XZtn$q4^9L?C-scCd!jM8xDs)me3~xiF?u zVihRT5VCxCnasU0DSRlB(yT^7oem^fENcn!!2W2dWZ8Vx7oR;=`bosj#LcF5UM1-%AgEB;suDCB7}yw0Y~PWkg$nbl z-B^XTsBi#79qIs4Whm%CKc>MVFv@67Qanf2bC#K$X+x7-u8Hu{{A>i0LuHAL%TigC zCHG#WC~Hs+%lCsjd3V&rLm2s!;i~#xMDa2QtkDAQ>)6Q0RZs!k*JMbX_nBY!L6j;B z-+5TGZ$?zSpt$RJ0G>6oF7Qwsbr6biG=rdO_yOZm#^JK-7@~P>xbT%LLS+e;BN>YW zjN5{fkyK?(TnLrOfnFc*s*!=KCF-mtpVWSkoqoLX-a|Pem;3o4H&_8@OU_=@`0@dl z8xeu1-|6soJq{RXx8#1U><4!6E4;P4Hgex#;d9n?W1q7RY_WUsLGVP8kNJ~JaT;-% zuCcDNB_8L-|9hA&XI~Q|qJtO0BW+jjw*0?<6F&rO^nV5XzZdXF#TJ_c3MnZmsDRUYiEWL86fuKg$}`q`NbL2tUp$?%{XMozfk zm#O?4drO5nj-ZYla{w64{-yAHP@19>MGpTU=%0EIJj#K;RD2K9^ZWj@)yWSW^@ZVo zlp3IY;PM9Mf@Dk3O+*eLraPQ})c6Nh2+Ha&Hlt%RDPsY;C;vxre}ILDRNwztu+aZq zFp$^(cVAdg-iYbJ4-E1{+XBL8IT5NuwReZ;TE}op-{$WfkvNevFRxm{h;{q zq}gVSSr%!hbN2=5xzm}}cS0`vo+IwBX8+9V^&DN>1A~#|S#R$iZ1x{dyS^__e40Xo zVQZz2`v*0^ak4E>3vPZg_>Q{xH`ZhQgV`}Q!xKOs0jW7a6txiKy!*Z98+C6lQj`R= zeODp_jqvFfdZqU=$$;VVleJ|>oG)6TRK!O6 zhJ8w@X{g+v%jm9D!4w1iX_z`GW%u`ZfU5U-PjLW!d_66EM~&eKD87(x&Y7>WJLj7{`;2&56Fj~)uJAi?inR4Eq!|qn+h1&fU-T?(;YY>PSISq$g3aSqWEh7PJvSNf3SV zm30c$tK>r)JtEz<<3^YZ01yWm&%b|ZC`U%%Dt@5*4;YJwe*icdX&xOh0%Yu)LBI;W z`S+r2#AXQOXFvutnf~_4GqT|K9V1+HkPyIynXXrVThyLJU)L_Tkd^oKut>m_KYD|*#4^0$`Sfk(9-rF73TSFr1{uc zg{dIS#@=0}M$E>O;Tm)efn5}@$g+apxE zw?6^puz__PEANxHuR7+wcZg4Gr;er04<9-6!)6PD&E|g!+H7`gb|p(WSishw0W!`Y zfAag6Ldm|!2qQUxf3*C4UFAT9KgFe|LVzgW>jOAOZv1=E5=T~%WQXa1J_m_0#{d=) zIU58M6(j_3VUG;|Zwo!G{a$7&cYM{6=`bt!S5nTn@X!UxYH70X`O@NN4rwW}cMe@v z&6Ts~0ctv=1UXE5J9*?*a)uH#r_B`*59mXsx;J(JWSxCAN#-WZrTyHov#+B!e-EAm zRDo~V5tV0+CIQBNa~SFeEOVKZYL;9JjAAO7h(wci~|jvi9>g8V~<-JHWimvetBB4rQm+`?~a4WHaNS@ zuZ~ZR$g1d;A<}1{t7iYwRiGo2N1mGSi*TSDC@Q7vpB}a>_)>#Hxy}w0-qhofTO(i% zbWUh$M2lNI)=vU&`P4NEuE5D$u;scge#`iS0)y#a>^ zGrNE|!aP}MV^mA4NRIkZ2zBqCHIe&IYhsFx&jU@I{9xg(Ro!!LbD+S?{o+f~&ZL5w zHxX}^zmr65^epo5>tB%6OiUKUB@YcfxLdqOynZ((+7-2Kpq5Ai1=VZXvb3ZYLqh}o zknv>#0_7P6mygS+6+iyo@+2Cfj=nW;C3m>UpD>mTh+Jb=q2kR*2~=r(j1+vVF@~j& z(`Vy$RpMCWB?Y4hn#h{gulaczbgdfrOc4LhH1ntA@Jv`P6@%=;t>hHnjYa%-m{4p| zAtwMUSM!EZ8;rXzp*lGOSu4_vJ=ReP;7`~GfNoJ+Hqb||w803A!zSm8#l{t}NC%Uu zb$0o!hC7svP0DO0yER2~6Skx>^WiTRo7i@rFK&NCKQLws*6vK14J!$icB*QASNhx3 zTK(J9{t%XEl`x*>-PTkWkWA7J3DNrK1~j9a&CyMo zmzIP}t2)`wVdTV8T8)(5PJ>$n&>4n>*Ld*bWcw~4&y@r`l|pL>P4e+uBpQd#M+vF< zPc_BJF&^ZEMuwfyug`RBY6)KlWA+>#UsgEl>NFEJP)cA08J#)A9=)eQ=bPf!!W}~- zktbvaF^;?gsA|&(-vSTIPAmN4C*fG7j^jf^XK!)_y-Mt-6;!0q?_omcz?fiu)eB&& zl7xu`sQX!VFBW(C7ANMHwno*BE1`);p-uy9JEg1G>ld8wUc`Z6G@-r!Hn^QDd|tbGweUMyK(h=^O~#=ruCdh#bGa(_5TzO6!~AH0Fa@mF7jZFS5P?2V5RaicpCdMLhhh9W*TRyj~9s znTnSyhe<1Zi%@^6`GWX>kspTtUY5(D4Z>-g-h4LsDDBBkY)5)4j))E-B-R!3;~&gD z&du`1_3oB71aTU4TcaXt@~-v_zeB@Ln}wwaNv<}1JE@CXv_9ig44Lp-VgEjG5yCn> zsd-71{2y>4u{D8{mHpaUJFn%gCd*oLM@hIfpV!i@tI>qH)f$noDQ)+sK?a~smR7#_ zUZQceOOwowAG&q!9xZPA-oErs{>f(y&`F#bGMOj-t#JQyMbv7xSuR8&D#JVn00|n5 zsNf5&ir4l&l?s4R#DHyFSoBzV zE1L?+%wmUrctUU+i01k6+AS#F$u> zF6O}Qe+K>t;;&7z=P_j3MerF#D2-O&h>pTn!%j6bKfjks6F$EYb%sFMhIXEtpNkLF zLhtmNm$5LtuCXtmGk8-zYk%uoBu_e=|Ky6!94%@{=?aU19-pe5+hE`Q{i<_ZJ74!J z*lMxXjk*q5zC3+_p2!ZNM?w!fi>!k?P+3ZCVFt~j6nkeVtWaiU9Y##iWKA=R^1*Sa z(*jf@GVmV1W^wj6&fXQc2XVFkCL5`Aw7bivtEGP}vv0g)&92ps(9=>3<~^CKvls~i z_b}y-7=-aiG#AaB$$kx$fk9l$+BP@)wcpk$H79}QixJOkK-GwYaX{>h&|nnp?3;7* z-g82>kuiC--H0eGM?`ZkMB-#?mN@(kjCB)S{Cr)f*G=(DeRmCKTAg44|M@d zUDhyV`^)4^#eT<3O|Q{8=0{p?>SiF_Xp_+pE;2F<%GZ-9-S#(Wf2_OITbd0pp=}Yb z*vmmC^|2KyB#M^sVsuTg8|$Um5*TS0G%%S~t)!00(!vV6);;m0>Ug}bq)`D~zBH-VXP zVzFLleYya@^lftrjnUmOZX}^ydT^I(c4cWnONiw&p@)dfXSEXzY-Tz9Va{QF9OtHs zjM1ZaMy6h_a*wZ2)LGUxlq6EOio`s*cma8;LV1Zb4dih@c2%|uezCKCVpHP^hVk{Y zvc)YSj}^iqS>iU{N%y@Qb;OSdjABPQl@4q_1#s{5Yh?px24WRX4_?Hy=H_D(C|e10 zV;wU6L}+?hpCF{xoH+Z%n6R_WATrX|a2vOHg^kQz({5C!LhUsOHfH|4NDyqS%K!S?EV?hVwoeqnx!M0%>GXc4{-Lq$79qfvokc_dsN>R)@eQWZ-4OUQ;!PY z13?s9^~;;fli5)B2MEcA zM@PLj;{9_8n=_kcDV-@h%_%o_2{lBF%(aM4KrlYDOXA-hOSu7U_%k7P@=PD0C{pLe z`D<_ohcVj>f>1)vuG{Y+H?9cm5TD>NpW%e@qECe)KehVi} zg|l`Z`Uf!0G)Xio%bUG2LiL$dOvT%iz-T(Yp#LEn8dcnEim+5w(uV}^FSly7H_LlW_?TQM=R?Jmjh^~tt}7=G zk!$S5dh?& zjaI2H1uD@%_CXH+;KU)mIvZ8U4w1z;rnq{>@pTT>0N8e`|1a2fFVfPC>t|;=OvS6~ zQE7;k7SMXw8KyYBXi)B5O#A2=yCuOaSB`KpR6qF~`fuCUr({-MIpX_9OaSbT zZA4)udu(JrMf;n7_bmFR!+TTa-A}s|Mrh5O-CL^B5t`;~eTPc9OHGRJjwNJE7wCtI zRD8L_9&ioA`jBcE345e_KpG$DNPgyFLCl6~ZJ^0B;X21IshYRCw(_`nJkN56SJ-4F zPGa7@y=`;xe8KG8j`)3iQ@$1#?Y#Iu;IeXO9=o|QtC86H0yuOrYlT= zuv9YY{<$M#v6iA_^ujxG3Gw??K0^c!WA`Bv!Na|p)aw|W4@}wyjRwF?D=<;f zBTPpl4^DYh+V+c{);hp>+%K=mtRFA^&iCUNF8J5Nwe5#S^`^mUdIXZ{NoltbLS4)X_<7A`^Ty(Qf2b-tk z{?}94pPHhGt}Y-Vp@~Zb<2ypxr4vB$Sv*K~CBQ0>H+MNbhs#}CO{%>HD?xkx1Q;oo z7;R6OA1rQqalxfg$f&5kHG0f*;cM+Hx$qMg#Q`19n;FlQsZZ(`5H&<}ncKYSDh9M) zbvfXwhrg7$*eq>tt>W8zvklsZO6`r|nnY>%kmM$@B}OntxGZNF-=8nq!a0<|H#)R9 zNh!_s3@Nuip6@ipXBt}^VrYDj3M;R=B^gS(z-sAzyl@;oT;8j;Tj4NRl*Ko+)dhHc zu5Q7n?Ey1@baBoA`r%mrT0X*kXj*w_HmVQfBX4DwsU&2}l3w{(F)lzJ?XyaQNm5Hu zK$&{~Q)E$lnBt@wm9bbctl>xPuNf!uoQ40YT@V-sd(R*N|1cJ1p zv-^2xtG^~W`kbbankm-N?Q)&)br~DC@!Fxt8d^>xt2YYcE`$#Dz~wDWd-6OmeBRCb z{Q!Ik+!{H;cE6k6T;9W>!yQ-*!hrGOm;J(Pw}VzNNO2NqxoV!tt`O(4(lFZv5I{Ve znOC6`?>oEh`Me2qu4GAWv|3H2b^EbpP|Bb)gSkK5eb=GrPB1*B;8{(p%JGU@j!hXi z&SnvCd2y4i*D5%wk>1_htX=}o_~Lz$U2b+_kOcWMQ_xDve_t)O@#XTQmd!|i_PiX#!&X&REzy*+}OK==-K)FErF)@Ucy~%$>l0A z_I4Ql(o3Dy59;IE0QPiESQ|^TBVD~nzdR30^HUsLmzZg_1vxE>3G;aaL*UH6Zt*f( zkQNp``ISAI?lsubokL>Rdl5r`h&^y%tx()=1k_We)kGsTaHkm+E&YXwU%>1Jvvem; zuXov<-N8D>ex-~CUv|*RMKb11VOwjTGQ3u6$^=2VoynUoH_UoV;W4<`c!})lLQ>xv zqQ7-{JbjqKtU`X8iB>*ZjYUgQcB(?()I0x;_rdFIv~ztNZc7CD+irmYbjovxrINQk z*0qOei{|yIGK7uMM>ajNYiV1L1KLk*;@8Hcv+z&{@Kl?!H+#3_wJttZo^bCOs|ICa zn}6f=+`I-&B2{;u*#f;r&^4ARkDc_lILSiY`QowN^!&(}Tbr2SzGbNSKpM5~hBBa` zccn|su-FwTSD4K^ z_dtvwcdLy)jbMgb^5T@)ie>wddKz)MLY|-~w4r-aTa1j5I`8a0*RudhFQ>EMYRH-? z$-=L5g7k-J*QR3~t|g!;)%8`rv|eq&2LvRwY2I4*b?Gr5&+~s5oTF0jKOFQ~ii>K$ znZJZYb-+^utaNm+Uu2q#YfpJAZ>%>#D{B1`f|fg9*gw;o-L8Y$y1pz*YN!$0odzw0 zGqdq>pPkIcxa_lI^>2E!7SWs+;Z=smM0RxXJfnzSe)FtXN!x>@jN_joa0Jd4}I5&8L(t{ ztDGQhH@~{Vjx=gn`sP`PmZKfa12HE-V;CkT6?WZS<=Zay>!KuvOZM}_R}a99Y&|QY zgeO;~dwOSrU>ENhyl1A>QT2Gck?5)uQzV;hx+H5+8+_2tpXUCBsqFWtiK7zpgQP8r z!C^C*`4)dqH1}AWzc(?(^Tey_`z9z2UP68f$Grys9aDw4Pb;5OFx-?l)-y*h**;x- zTxPRf*NQGg$oH*GKA%mdZjKOefn-D9Vqo6j~@P_2a_f>yP?6h2CMG zSi@EJU|{K+pK)nt@ooN}ZTirh>eo?so^f=6AYlF`qV^unY`P)q=w6I_upRc~N7ew5Wao4+;om^Pm zA7%p%{CQlt;hU7GB3!eb2YY1F*9yjv1oX^1@3d|D1b2Pbv}!W2qg)0aEne=&v-L)1!;`NUxE6BMT0==3qWK?^9#I8OF$uiR#k>7Bfb#42GJ1%9hGqAeVP!d`>UhhAz zlAyO}0Pdr8@{bNzRhlbWs%a?7ntJU&io4SAS(GmyK~yscQ)Jk^+foO7o~kNDQBmx6v}Cl?EuxuBqXK?m<|#dj{K8H)}PE&Ol#uN&kRtjTC%1kt;?(Kz-XU!dCPzcNWIrVW$O@itYv76KcA_ znq%-~B2y}&d|;uNlkH2Xeca$Xr_t*TgE)gc6T7ez7mrzqWN2d?hsEPW&8}{dU83K; zLY8o<7@1Vsn)q7XQe90ETT?y0DWA3|tiyf{GV4>Nh7VSzo(&e>2`=*?di3_?8eXm>td>@uPw4il>&8^p-z;IQqOT zMb5q33$$KLfPr`UEgLNc)#66H4*<&9o^a0o?NSg9v$d>(wiT_GsA>$U>+%eL_CYiO|hiZJ=c# zG$|Tet3*X%)DkNR-z`iFaSmx8op*J=Da^2U3-~IPmnHF$Ve}SaCHVKlPUcysNMA61b%6??Y)wK zOpfp{?y>~WskecTOOQ$B6^ z-C>(yIy~L%9SCA#`n-KlTBs}y`lyhMMX-RYrA~tch7aEmJP^4Ho-_fNx2|ldP8X-u zxNJ*I$HCn}2#_~38^&hQDSyv>zG|Jdw`a(1dwaOTEqlLSw@4xP_`upN`RcR1u~&gE z&P04@%#m4;r6{4^Xe^#rIR?z%45IZkWW%Zs(;~@*ql}IU%xs(B>t4zi^zM%bG-C$) z;_0Z6VlKPJXsO=)Qe;9p+rxf(e9PL{h#3|aH?h*NB6!h0lc;^U|J-FC$n`BUPMtkr z)^FMqb*^xHt^xRyN}mohM_%P)1^lguFUE3+F4TGjE|-tOnKj8) zS`UI8@|gthe80elNHo01S@}+D0q#Xvb zIN$k;uamA~X{%Rz(D>`7hz5W73-2P!UEG1-EiQ4mxeJ&JO|Z#jbU^COGhBjO)aFrZ zVdck)h2K=a`pz)hp9eE8IddE338vPzg9O~#=Rq2Kori7E1gYk}jPkCx`3|2A?kAAL zlT+hrNub4b_vT@K9eOLB*N5qgwAHD2M5yWf6x+t`CVOUgEaqyR5IW(QyO|+d=EsVL;9r8d^zFl3y-wDXg7G$-&-1C?*E7QTX zh%tFauyAQzhP}cWX4ViIa*cksaUg{s!HJjCk!|kLCa+$EOmNT-6Ws4v_*JSqNv-wk zxNJdb2<1_=FW%V}*>>j3%(RjHFdu|-8033dzZ7AejXiB#?claGvR-WiijMQO(ToGQ zoQs>N!N}DHQJ=zN0(ejFVX|9mr?y`_k4R0KglJH1<_Znqbx316ZJ>|AlC+E>buJ%L zJwZ(u=G*8io^NPt%x&wT8L{HFyTQ48XL&hg*Q@BDF3$Rbz12&*3OizLxG}%+2pGD+ z27i9n*r*_zxn`q5^lh$&c5o&h`zGN?N$#aK7B65snjE!~<@+vVRE-f>$#5IX-fE-9 z$x?y9ehhswF8nH@-OpdMbYDXR=a;K3^s)Np`i>&@{lqC&FW@hAXxp&nbVX#Et~vun znPWjF;m{VJ?z7gNU@bAunh7rP#_@>jbZKHArkkwB1fl~v<&~};qpm-=z+q`Y*ceNh z9C;fj8*UgO{zZazj+S@U)Mqf(uJhI)|MuKQfC~ z0yplJ@S-WQ)%)vh46+)G#*6AND!&7yU58!I8C*JD&v~0oVqd>cTiQ$F?2-d>jeb`I2 zIT*P?>9`X3(R&!`vPsFYc24I(jGtOL)DH24!Mx%)cy**$jLU)QSNxJwgy)ol9%GIQ zMJN4gKTaa~w9k;gT$IZ`adt818kMTDNb zu{Ah*Rwsa$((vsL&Y* zK~NN~;64LagClI-%HADXKQ*JXhHu@SlZUN`YOYq*X(Lqb-c=>``eo^}j@bd?l4*9_ z1{EjcaJac)byJn9!x~)GMEAhFmjHVNYJl zkM+%_c#rDHCP~@gw(PL0rx|~`Z6Flsjt6etk@H7cnU55qg1<=By-V?qLTcTOQ#EmY z;)_OkxBlD-z0gkZ`w1WdkE>p}K-M!3gm+&1pC3;x!N$Js7+Bzs_)r7M-jsogbb>ow ziVVJstdcVWc;Z4SK8oSDx0qZjFhKdbBlUNDaD_vF!@-MTccOqf0F7ULV#n^jYS39S zdKox6DRIEjQKs4SsQ{@{rr5)e?SLQQ!y@a9^O$^;;H>^?jNsc(#SV(&l;{xzx;p932iuy}(f z^!|YHN-k{wE0zFmeOx0zR%_ok7d(0&L6jXsGdB`v6HD&sRmgpRYfMoBA)h}^(=6cG z^)UQ==#+EUDOUgC!3yBfPs)BA`zL)EecZJ%NWt$osTbPJ&SDGe)Tc{nY_~D^@y(^4 z|AcG*gfJqae95JF@fpQw^HG-#>EO^KC9r%mE$qka8jlPP(V_9ptxBM$TYrI7S(RWA z?Q=k+77xLl-<_An)Lj5%YOWvo4fwzlOXICzV1|Tsb0lxB71VWS4H)(&0Js$;34Ska zkZ*bp_@YUzYjV#WE;4Y+n-muG16oMOz8oWoO)S7b+iC;$w>Lat%jPl0IG%%WSR=H< z^Q_&8zB_=*QNlulkP+w1JiQ{3KZE6v?0 zgQPR);niwvW_YppSRSmR4WMQL*<4cT!;>9J5{#VkFLp31HN2wQyBe}yztsA}-)cRi zgiBw_F=V=JF1s*r_2MNVK)3b&#YIn~_=5Tfs5Y`XOo#ls-DV?SY%Av4;6Ki@FX`|h zxNFSP4B-HS41WN!a)Uv<+O;n)B_GzUq)2TGdF=`7s%-WC1kuhE1%;y$Y586Nf&LtP zvT*dVAgcx~#d=523gVF=*dK{kE2Q_lsJ}q(V^0(6{r@Rq83z=LKA%{Q8&IpstgCOw z2pYT%DbZe@E$&RXo_ST+gM7XYk$B??yzJ8Z&nW}AO7|Xzfr6Js_9?o(DG7t+`KDFn z9tU{)!FYf>L;AwATq}TS^*Ol9Rs9<#g=XJ}Y@_hv7nMha9c_BLehal!VA8A*hg1~ZD-9VxN99@CU};0quU`Skdn zaY}!Q#7WDC84ozax#KLNq$oAu{e6!I5**hNf{uYud$NN&bdFZUtN5}&{Nw>8GWz)F-m(k z=n*9V28LD>8$OWskdZ%_0%v!b-3C0jH|tcW z%$UzOTi;GFw7qp~eyhcQo{Ponbp#nX-RiWFiR|4j%1QuPP;7BrV=VA0TL(J@x){lHEtK zy!VXMBWlxqthsLp4b(P@T&$QpJ$s$ceTi4_=-!a#Ok>oV4aypL&YaKLOdr(l4NGIm zj_?@pbDi!^kIFRD>tJe7?jLoZpjRiSJ`Z83?0MEiu)l`;MoD}+SS;V@N!D2&9J+Uy zYRq#Yhd_4CcoVrKvKrIs7*I5BSj(N=xaJqS?f+D&P?t?XDoJjZnM&KMq`}|1&1C9Monf8P2^DJFQXH2*dH}${ZT=Mw!YAv&_Tjf% z8q?rgc8>jOM$AUt6i(cokghok73T$9ezJY2L;iVWFYWe?yJ|M9<%dwU(x;Ac>7If> zd*5^TNkP^$`W}7Ogbp+H6BS7}_@?nrWY%-tM|4K~kOzg}fwSfPQ8Mfd%xcSN2wT^Ur{#34{4t*EcfYvE6K%W`euO z&P(scD?ngq!)iUwhJ)BELILBOtM%{i+2pAFRqM(y9cdn=?Ybk0>uZn(%pjI~lH4TwUA+M$T4*qvdUTQ%Ie~^v%_g z`Rd2DwmbaUZ@?6anY{M3eT2lj`dW_qAye1f{JGZU!?fb+Za9IJqKurNTzX85KARE$sXi@I2P0t2M2QMBs@jH67+*EtX8I1^zF#^C9S5FgxBvI|1mztCv{#aVcUf{9!fA;DjO5%uM58>U zJhVcgvDx3Bhme8m{heX{mpl8uhJu1(39{iOt*xr$n)1*!kW;t?_U5<*;0q22k$efE z@s9#^5o@AV$em3FapI}Vp-pOv5MJ3+jcS@26~UC@Ny2+8vj~j&odf^NsnU^6RwXzb z;{;i)lC{G@%{eI{@;16TXRdQ-t@Ni@ovu$9rXrU#*wf09r- zXOB=yKc=fXsXH~gV*0dXbg)$*ge&5aFA^m>?nIxc7(yD9zUtj$=lSNZ><^T1es|Uq z^fW{=y>rFK4ua$T=30BAbUW~<_>uBE3o6Cl+enwm4i1;`mczw&ni%qNkvF21GoeZ? z#Vy76kenZg0M&E*gK9H6(jC2`^WH7u=*2gR|2zs*G}mZ`d~UgPYdt!OiIci_NqQ}L zj6A=s=AN@1q4ey1&I3|A!sm`WXi4@o=1E3{A1M6FA^w&ITwUsXh{~^y!g`o9>n#NS zd;ZBVmbTJ5G9A65ohnSa#N)M%#YxRqZ|Ax164%>73cu<+bEj#%S~L)a9RFLs24%$< zUnqq@bO92K_#^w@cai^S`0U=+irq4(ChgcMMy+*1FwH5%mHJ}oGY9#&fW@eeDxlDQ zsYd;PWr5)4-Q&)_rez4;Y4y)LLD;-v5Cw(FGp@6wv1XQHkUZ89fJo*?SVL?OH+Zzv z6a|w@x3?c^XG}4(EQDUi-5(Or{A+K3+*Sa)3KjK{JJnmg`3Vs0@p=;iNpOTHrDt z9UMk;XCDaXRIp}`+^-OMAA(`svRyw@kvmb8Oo~>QLjJAKAOCh}oKpG`NI~m{D+nHg zIx65b5#b@t*a$Dt?}KrNruFtrg^ULAU66KaJ^CCjI4pf@5?Gm&^uJ?C(~8QkRL^dI zrWm3+YMLZ7Q!oV|1qoBPgA=q*Nh5JL6nbKo^7COpkSl)+(sqByA<>mv{C| z5lir&ErPMQ-Ipf)?vv#d9l(8;IV@%N`94-OKL8A9pNP&jiT6nP<|N^}(k+Grjmj^5@B6;Wc^v zhtiJrVM31cueFQK~#G;d_S_+hTEOaz*Z4&B9B^gEo)RyWRBWFCXGb_1cdy zg7Grg>V8h(MWTE&rXTe;@pKYq9;~L#oJQvl7fA`%AC<=?cNJ8`G@SIRpIMpj#%LPwK(@B(`LFld4PKrP@m-wK zCe8}ppA&Sw_7u#y&Fx-a4zP`j*0on8f7Le_r!Dxp+}q-NNyEXPC}Qt#7{q@m;#v*& z{zZlWOrEBDv*O2lGuf) zeQ;!Pn*g4y{(ZF8b!)c4vz)Q)BV36b$}aP8gG=Q{XoMV^p%UCYXg?*#F0&rY%^odq z+J$Pi27u1fzYh!o4JE4w;CTf*4Hh?m$shQe$^RDbx~w@lGg?Y4K&7r2kabT7xBb`5 zmu`@y*X?EtaHuH?&nE6-(SWSHGy06LF-d&3c=C|L+Ge6nm2MO?BSkjK4EY6`M@>ew zV;mZt&Iq${y<%bvR{Xohtk_;Lah)EIc|wbrp0!D8Q-iWIz(b?aQBfVqUu=y9QX=QM zPo9lt_!svp^b2AojrKR^2Fa75JkA4Z3rjI--z2WmtDt~ku*?P^7UF$Q<50V%7cTd>At9{YWi{QnLVcPPyM;G@N1JjrgP_YsQ-le~IfD0D0$#J<6zROpvMy|CRYc=t22= zR7WETNR9?SwWf4VzaSQiw^32JoHTkoSaAAz|3Ov(*^%f)gy-_N-#WKls3QaMw6BPH z>^@%*0LkAYRu^zph;*O~~JLzI~tEAYhq_fi38*I%# z*Q0v%X`k*cD|n8@T*`s!ldQHy_zR;ul{&v>IV*;dYnupr7AvG8b+x$yh?YD1-~64` zRkkub05F^Dp81EmD1?;-3Z9;7xgsB8F0Z&Lybb~tmQOgVH3nV(KL!0~Q z7k>ruF`J_)vBNEXHGJ?F5$^F_^=Er2E`H+?6kW0=C)zn|jOEbHJ>zPNj7sc*S1!;# zS{p2Tkx^5;39;jL8i(RGkUN#tws{rE?*8&P0Y0o6GA`Tgp#sBe^xwjgKoY?vd$jCn z?CIWJTESRl@>z_}+MPO5U%rf#H80Cykp5VXejVg!V$EQjsTUc@+z?hPH}a6s%$$qr zAzIHtX=IzBb7-(&Re>%}I+#PwVP&)Ex_N&2}=8Qp#mV#8?CfV$vNlP$=isyq;Wq4PKrpsuD zpFaQ77a7MrzrX?HLl|`??C`wTh6q3o?ZC5Y`F+3%{rHkNR5gh^R zv22KAdtkC4VlVB0oT?8{nXqF%Fdwgw<@eNnHu~>ARo&p5&PjyN#v9P-lgit_SU|*2 zxxeihNFs$BUeF2@p@}gm&LYHJo_uUPP-InL_djK9N7H3l>*r+TF7MfQp!K# z)7-zEF_6L;Tb`I&*Ch}n zZ>QISAaGX~@M-R*j)6)Dtr-KcuNWq02h1_CJu@Yz)oO3W!9sAq5mR#v=gtyp3jdR{ zLssa4a8fHic^@!G=epYZwoz6UE;e&cf41lF|Mu@7IC-z%kimW3;|XpxowurZfid|t zL{1BoOCF?yd&&L971}uEDfwU!_t0+gc{8k@iTG)cVVyOmw)i!XghFbS9X-}3~Ja7o_|oeGFbBZnR<*++(j;hl`nfifX; z3p2|U6d;R$0@WGXzY?iHe{_^oP^I`nIqPse0*UuN20KNw3 zQqHdF&KE<8LE{$Q)vHZd1bbr7;7;D)tDn*Nhv{=$+w`NB0gPBsarR&&?XAy{i}ORn zMRD)DqY1Gmy#NX$@b#aIAyAV54)3#gtY2fs3-`+aP;}(^1g{MXn;575>2gv}liB@9 zaK(+FQ)i)n5jE8cP-93US%a4*QN9-TGCW%;}T238n>89y5Cta<)50N zN@mp9$$8q~d+aaCQeX7{`;7OkJ>bAMUqPp{4B-h9%lEE2?I%1nzKHn<-buY1 z*aKk*F(29+OEBE@9Z0^jym@n`kp}ZcNcf*VuqDO|Nj$>I5e^-gvuO3ls0sS`oby$k zF$KtUA3BsdE72wj;CKK1x{w`7vQhnp(CraI0=Lq>wKzlsh26|Psb+Qz*Pth z0|0;oI*z)4%S$ddZk53)ev`B7bO4O{>4NwMV0_+x7Uci+f~bDx1CldoLQkfAgxq)X zwE2--}~&;XK5cL$Yt}$SYvi)L zjXsWki`;dn*(gp%xs5anME$iXx?$oKl%4T^w;PthMtM| zd(gMkAowl^Gp0x4)2U(jvX9w`(jdHq+RlEjs$~;N)HMPQ0f(t;1>sp` zB{%CLjCyYlkPh@xjC_=FPKsg|dlciV70>laRb^f+PTVlB9S>u$t|^|HBU@mOGX1RP zNr`yaiMF#w9f=&|TmiNnWvf^}X)6Kmq7*?}q26I@hg18|L54Ah($tupa&7kOP4Lee z#AnA6u%FONR(wLNaDwzcuI?lW@t?(HQ_9=0C7I{8W81!g`O_UONI zUG1@jb6+)KB%x|&41a<7qJ*f7&z$-@Wd<{kOA$YT92e4KMR$_ddt>9MZuv#!mI0&l zLK%;}SaW0TY#YAsON)4JRXb?0pQJMS-}ip@K5@?Wx0 z2qy{HT6dRP^wK%bcU>+qYmY4QBhQNE8a?TrJPZZq(}b6erXGgyBh(1Pz*gN;>;y`YwRCR z5aWo=q*D+=eJ7Cy$F4J>f?beee%H7heT^d}T>>eBgfM)SL{1VD3|vyWcJr+d1rabY ze1I*fukMpvs{1)y4GuX}@^Pqa@%Nf=g=zncE@-mJU0miWh zoXZR#i|79uo&DV*C|m;0*Pq{pK%tE^Xk|{S@T?f`UmdRS z{b<&gO636hGw2$=*yDTU0k{D2p|^9&~tCJ3-(y|I_lF zIhTa7j@bb_t77t2&`qUjWlPQSQ51jLPDL9WbTyUj6}?0%g*qUhLf=(-5S4mpHt)>t z;&gX;DHyNf!ny|Ur1S8aAcFE>)5TpQGo^KC;rQqvNiSKjbECa~jPU2e zlaK37Ry!+RF0zvPRMo%w2X;_Z$=dAVXO=lFJLn1aUWctaGiz&yv+s0C#nA#sQta{p zX-T;EOoQi%Y8<_q;N~)EJS$+Tt?MHg_$^1T^z*sKoq6XR(wZN0{<_N$w#6=CUGj&Z zPyoMkBo0(JRO!lO=ogPZnrJ{L0?>hKGE@0K*W(+C2b>5;4wrUI9w zQEo-}UTICYmwvpKccsOpWs@}Ju|dsDj~<}`wgqLA0{Lcvn6=S3=`3geF1pu+M4H3m zJ6PVy=KVLM#_8Q$*jSumB--{YWna{i%1;C^6Y~c=XM_bTJ{%oz$6-t!6bV(WI8)KQer@+-`*JXx+fw5HWQVQOY;=k!PDjUU{y~Y~tKYVlgmElO}QE zCtsMBWs*n@lT^SQ*k0M;D#2#4(s(%SP>|Sgy+Yg$?Lh|8(M?ksZVfg|mNLDG66Gavrqo(b~<_4saux75BND5~G zK9@YOWBDLK!9C}HmONe)Or$wpR@rmRhgTl-%=tX-H=ak`tr#OrEj1mD&CWVH*?uv` z18M{0Bh_aXgi89HA2r%mh?Y!^+6#Zs70(w{6d4g1=!IvD2FyA>YFy}&>R*=7SAY75 zu`=Yc+1b{+&5aCalI~mp0btJ!r~RfaIdNUSLaicpqMcB_Z*B)KC+nA0k2S<`*GzjY zWF3Y6i1wW3`I(#VdcJc90vYNipT8VK(M(|?Ul86zz(8+3SJu$$ut5L#Jw`6vVp6$r?MzN_ z>TBnfS)kp=8(>q|zKPB2Y5+(FvrET-Sib-xvUOk2)+*h8coxL~35n?sTY>Dde4b%GIXg?}7Yud0ROzd>9-L%Yf zX!bZX@C7PQ!Q8vy;Ko4tnXipGwg>k_Z!HK-jmxfgIh`9JDmiSp)e;gZ2Avg*t2?=z13|YBy;u}chrsJ4WNB_RTE^p2 z(cirRn|(yN&}kM%gtJ9b#DtCDCKlZUfNcrEPh59cT5SVz&1h3W;~*=ymj9E^b(}!F zW~rQ!Cc9oj2AJErL@qHV%KZs2Cn|(ic-Dg3jaAYR=B3L3!Rn>C<8&HFJCaDiUu(gO zE?B)04uF$`pYg-s)(qSp&~G;WOn(alBeAvQ{9>d|uSsS){mqgKIcmG6g*RSkEymP* zfMXH~BTfLEk%r)vGj==mhA$BVAnYYZzf8ht5B$ef7*Rg_{K;9B?^4ZD3f0l43ESdX z?B>S6794h>@k+u29inC_L(#i+r*}LC*9GF8STeHU(txLG{M#e?M+EzWI(NLkKi$3YW#~Q0@fNg+vwp3$7%BLH2(!8j7qFXgOc<5T9xP9Roj$u^a)ki1 z(;p1VxF+X+psxB_z|N7jFTHkUb%V0QDnMLxJy@&g1_9&^6G-Q^EM^Hr5K#?t1X+L6 z4N3qyLT(AsC@6k;FuJ`m3?-h4Kvi`(U%R9g3L(qjRtIyPaos>ett*m6k#)&NA7nd+ z>R!f9Kz#?a!#pUwt?=GzZvWCZ4VBbdMyvUtwD8RpD>Vpi3bPV+J>AVr0&X!eWv@$s z9RbRq?K`0Tf;H-gD)>+8JIh>}ZO!9l{gmLmC%JAiz~X84vhM@j%`+vF!0Mr;avIEm?ohwz00})PAsgay%&rg zk7sZJ^brg$%j&~N+`N_)4RgPFP7@i%R~Iwvo8#RnfK+G8#UmrIPB0%Y0QAz>xZ z?^~$h3c}V~znYf%v)NOR)+UeF)NGi;@-qU(Uw$tHA{sXuwwv+`&Lg-(NE%L04GKDQ zCI|*)VppI^abHoarmR(8JDvr$3S&*^>S#U&Be@DvHfvN4WsSxvpQb0SdjI}<_hB`l zNl^<68B{r#gT{k^Qm8XsMw_*Iy?N4U1Gq)Ou%vs-wf2+s3hyeTeu9YiA-O|Np=~d~ zGAx4qm6HS>{z#voyPo~Awj2dFtRMRa^`~xk;wCr>UJ3(&z?f2|FnKMHsQ`*%89Acw z#J8@$!_hlRz5wDX}$x?#_G+ zG8CQPNr1XPmQ{l5G%Sz|8dJ{>y+H~j+^_f^Az$gr2s4(Ge}k5L zUUw3uQC9Z|JWSDp9Cu^E4(=<_jV*lKgtlAtC*`w-1; zS_cP z$2?vckr7)tWdM9+&)Qm$&kL0pA+H@A&J{-;0fLxH`6d6i+Lq)*>#$5WSG&4#lw1uL z=$x??k3>OzB@~b85uXzL1E^DX<=l7in9+3+-(SF|fM-gtTUx4e=hjTIMml0cU2U7a zP>(DF!YK6{a)YrAmVxq}M``2KxENj}?#w(AU_Q8$RbIjPy}UpRVKR@sAb^-E2XuiW zTRBzw?hq_ zrir-6o=x_FzJBL3e{U6Kssr*HI?|%=ciud$>L`#JdC0({gGEc8GcfMu ztLCB=+s`Ub>HO$T7&-mgM0J2u79H*J`E$!s+%r&v{(OmwEtn% zX!5ks;vfjqFED;@fIZFwg<^ruSdOi;hk%=VrK+MgS(q}q_e!|isWWaGJC*qRBk5Oj zv5LGEx{YJ0+F=!@D4$rd;1*+*5SS-a#0F~x`^Us^T@p@T~D!&zeHb2tAsWQFhWD!$+ z$f<17f9H;iV(;3_+a0q=14^Kku?L22CJr08cOP!XHSj_GvV!&#eAEIq-1x+Fe30JC zX8RTHVJ^+8dimRRVEsR2BNMy9NtNfHh@-( zdoE`c)Ad#rXkqLVwVrs|YQm0}fHv4XG>={MSDHR!?iZi!Y-%+!as4{5CFp;#2F){HrO`wuO@aP24|vD#*wHVP4g*gy3HY_@$`I35NZUMo zl1}9LedS}|Xl(%|q)A9vwJMMSw5so=C=}|HSwFbEp6f5XRQqIFYDR$BRcHK>N_jht zm`CMk_3RM)@K7d|c*|_})4MN&@q+MO%s+Q;v`VNc%~|%=LObS)jA5rg1VNAk9+fl0 zJ6Q&3K>=+}gSpG_9{PEJLRkUsNcJa4%Y=1SB}F8M^%$A~qd~rU(z($qesU-SuLUyM z^Pq(#YVajW!M}UMEn^8ZQhv8AS+I+`{+b4Y!j+JCV$Ce~wB2;?=$3PWB19Pr_L$sWOx zk5`=|ffA#$ZUl$oMP=^|_sN@v2cyHVxrzqCAUs3~h}{x)W*G!bBzMJv^tDM4eV5L} zaq8!4_$K0}+Yvoe=yc^;9;npK1P8Y7V_=2=i6tW@TQ^Dbuw2-Ade5EFgwf1AZOP}E zsP`(!*mqc({nkC-Gs2`yB`WZ4XU^49I$4i>R0Y;(QA0+R8kqAyUm08n;^#tZ{(*R( zsofq)PGro?M|iF|wCDN?GXiaKmlwUjm674q@`QM(c~h(QV=n;m=or-pdk)26oa9Ky ziICg;>l)X8O2ePAGv!Ti=RF%0f^xa#skU^oskNQfavE!tEJs{ z;TVXEfyQhNTTwFlAuzzt6hdA@dg>O3bQ((E?!Nb0>x?51ZmT?yJlc3-8xRf=v zbj{a`TOM)%Doqa%^yd8^q}O>(4lhLsx(I)nSm{!}RYk~fo@7y(B#Jh{L z+V^sAS6A;{@9_k0*(^&^#TXuh$$|s@m3;lQt`S#T@CR1ynr`l#S1Zg5N&PW~-w+Xu zx9{dyz_ropf32!U$%pIK%JWxmv|i2Rt3r=&uW_(nmmmQ>$B!@%+@ESRkM}~=;eq!_ z{*m&U+>BdPg1`a5ws(E6B&Y(8cX_trW0B03z3Z$S&k5q}uX_b=VIPtZJtdV{|B7~z zDZoXV$H{14Z|Hce9!4v?bLrf{jV_wfgWnqH4=eQ78L^A$njI>}Wvh19d!brzxqe4c ziuH94iSBve6653aq_ek93O706@I?wa`B3P1RIPo#qjE4WRnR8^&tM4+YubygrfjnQ zK0ff18lh!Gt?FXmB{py#cyyNACZe(V@WbPkE{FEvm=(>Vbc@aE0dTe-S|P6@lgV7D_80&ruJf^uFLBfpLOAcx6p;?}YHqbAGjBQYo8?)ga%zr*YBjp~UfI0dE_wH=w$Q$oQJ; zOW?C}^6sLq*vHzECEV__WBlw8kIzX#$iwqsr zG_JgPw;wZIkJ^S5rt;3SYvg|@KU+nXpnN5j|O9^W8+iX5_x@-N>dkCXz7^Cfr$x zO`y`=UG21TL(;h7n)ZXA%Xx)~pjoB!;g#zvpd3MQ+;@{G(EsP&m%rbI5mrr|JdocH zjFj7Y+SMV`AT8jj8T4j9N2<@&XL_Cr_QgA1h5fGm_krXj8l$;2MdPydT2#``(-csfF zkH+XRedciC01OQGjk!GJS9JP4Xy*(r21u8Z&Sb~eN}GKj6qwGw)%(f5*~HgY?x|q! z2=w4gJ{Y@l*{geISc{#u}vikyM@mS2lA&$J#8^zzW<0H2hdV;oSEK3X&?I-=cYR zknA#%vwWq4qo79XEj8lZ-Kc;6ya4vJa?7=q;~c8i3J*dk#hXjPk*=VXEHbFP#^edm zgt5lq(hc#bI5*hR2tX@V-C!U|cop45e0=B%VMp!EKxBGO9e`)Gx2U z3nEG7iMd=PVhP{?Yf@ZqDTjPz9uu9p z@7-Y^3ZQ=79l)f?r65VTBntX{zIFttc&=K1fqJTdgUcNZzI{g5o4t5`8aAxY0OQ-A zBO@~TQt&)8_oUQeL3AbReL)qz2;$2iF%A&cc6@k=zSsYwT$Zb+UPdwP&BU6GRjn=R zysa*&w7`y~*Kc*@Kw6;uD%G5*7bGls@^wl!K)a!z(d?I{r-K)$Z_2)b>8|gj0GrQs zyN?E_TVPvh!acyG@z6Q#U*FW}T@E7I_4Gb7hd%KxF=2d}G=CxfWFIgvYZatpRjD`p(?;hhW3a9Q6m`DmKteB*E-EvJ~o9(}N zYq&+KpE@i{xzV(xF+avX3q|0N@RC-@A>(!p0yM*9_h!T|bk406SsWIK+_FfAjk?z?wfW&KD-5Us>M36_`$1Z~J8>7@hTAN9 z)yTG8rsWh|3=R+_Vz}?o1Lyk)Z@WxTBc;k!_)*OHM=3BA%Od_ZcO%(~*JtyU;V+-` zKLc@n*rVJxl}lYxM@Al=D@po7mO9f>BX+mF?*oT_&Q23Ppvf>+FX`pj%e(aT$W%V( zR`v&Wa|owdJcoHEt$Ywk2n2wcX@iG9Uy0|lE;zLN8hp=n1Y(fOA7vOPrNR&U!LXZ2 zEp>5X<4FVN{g$0x?_k75dAoF2=4{GtNEIh+V zK7qvN({qB1E9oeua9cUrUk1S&H=ku#$>&9cP=in`H0SmYZId*sl_X)`LVJ8`;e5T5 znwm|@;$rR<(_loi+S4~5Dfb~@{8<8)CmTCaa&RxIJSb~8%Q?4^Hb;rj4p`ZR?Bzg< zAiVv$`fX)gza*8HKAsdsHRio3+ToNJ+LouFQD?G+EU?>Y76ANn~zcaH;d_Y zqHmIJ25<@$!A(A=P>|{Dbj#b?OH08QTP{JTWc=!s-Gc)0^G*jtsEQ|9N3rQKIjtXP zoK(!uAi{&;a~Ff5C^Q-J4cRfc3nFh3IM@VyfarXljTb;=zgRfr>K$&M4KT&O$7LpRp1bORRkpwjp?84ZX zQ)~Q>zW_W1mC}CIhY%$7_K$lq!2`T?V(ivy&5h6BC&%VO$-;ETsrMygNADr`9Y$Cy z`C6%Z40w8Ci-I?m$4>L07@%G#=+vN2H+sWWG_N+m>B}41H0`|Kh<)kT^FL@**}kEV z=xy{79X5?dIYmP+&m(l(g#8i-nE4Y%Y7BK!G`XAa?U-)8dd`Ty5RM9mf%20CJwcxf z6AVR?J%D%tx$k@JUr7ZCU!2xtu8@*!%FdS)7qqV&&zjmg)NS<$-w~^I2JOgOpYiGA zA-u!8f}YgD@Pef#=j<8Vb^PHB&P~Z%+7TZXhSNv;s>HWmYinuAS^1%PzUSXayy3~U z?|V+IIw^9;GX#LNi^eMBq`D7^T({+$tdY^az@I2zgCg?<<=fetR)_^tkgB5qp*N$1yrR zD>KKX-1^FDVGow9Qwq$QrEZr;i!eajW0AEUxWZ1L%pVt2zo)K20y-!)KJtzPoj{2Z zFyec2f;GJNDT%fRiL}j&*w2}40o5o-Rc(F*VLGS~peId9q;&uHdXzd#3j}?smA!IE zW%Wt@@gB$P0#Dif4=%k+ugA#iD!Ex)C<$lZqz&xeUEa*z=Pt8N1w4U~8b1k*D^RaG zApw{SfDD8rkTlGYHBKu-L2XZ3UWi`x)mr>9E?RIK{qtXBt85UTC{T~(G8A(%|Evjb)5aww-BxV=0ts{h9%Xkh1v@J;4eat$8Tl>JKq!do(bRS5 zmc|nAmR<{U{b!6+==KkQga;86EfCIbXYbg`gH}N?lh5PT%^-KeBJ_e*Ry^#u#YfE-1dfTX6et zf_XTUR*+JP&eDlu3w^MbmA0lrS7cqR$dX1i^*O{V!W@w+H+Bn#*&!IKrpH`vyGiBg z#7mH2Na>A)Q{(IdLO9@C0GNaRrc($y0dQ?R%9n(|h@B`6@yDU5KMv3SD^NtOYj)?U zo%z^kwJ(^gAH~=H*X0P1U z4WMGy`BO4*e5VK7>hI4g$ykG`gy3*i)1(euz#;#0hYoy42s;rBDLQK05eF2;zBVGx z{A=M2ot@{7p+f2W>i3dBe{F{3$(s2Yx54qk45u+*WXo>Xrqun2Nopa zQ`0qEsXxm{uwChe0$87|J&fmIZWZcL&(}gULY3kUCd27tpk-_91?03f>;=h5dDb3J z{DCtN?3W;cf%?WeAR)e0)$7>1PJb^cyO2p z`-FFTJs8(_EfUN1UeovBT(Uhcx;y5Wwc_Qp@AO>-;nfoX!Cy2_11=EA08T%^j_462 zv^Zb!(A!pm^*v}JuKqX6Rulbk(P%JpT>cG@Um=EkpfsG<+z6N^dF`1>7tL#e51yd_ z67y)X-ZN@V!MRLIgV{!`{i^G@{1RZ}!RC@44!>>q0zfGHHv+*Vml!SB4xExU4?u1V zBlPXhR?)04VQSh&%!2D{gh231adkT;gvK`U%bnpPK9wYgd0$XAv8XjB!g2ibl-w07 zgS_+5;VO~PMlk4xkJ)Dc4T*N3yZ9~s<92FUiP{yKevNsCmXy^W|MA{u{+l!27Ur~2iNU0 z=@0F@9Z*wj-8(VfCq`5gm?(%{*B$`ioUD)%&(1Qor#y1@jn` z&|~7gFL)d(z7V;Xg%7`sONrHbbB#+}rBVz;GWSC4uWF z>d4@{I&J>lgn5D5%+9FAZx15s4|YWA1>#-k{hjx^;EO9^VxT|S0OGqab9%44A9h9`SGQM%Sl_Z}=mUWB(7B=uB(AtyvPoY?>k z5~~cH9IxG7cY*1Nm)0RVQ0mLq>_sDAtTA!R$T7hjTnmJqQ6vjhR-t#`BW$=;*rxVW zP+a2mCr~PGS3u_7?i_6Eb%2VB!-dWn-xz?qz1Fy4<&{oKWFsA1+rq^z-8}Bc=(1puPUP(XEU(}+$_f-Q@_*Y0E^yDU0+wXsep8V?Y z;}ad89>BPB5X{y}{oBIiB$|@*!!8A}a0k99i}mjdooGH%^SFpM=uqop1A zUtUn9;YL4i`~I@(F8{}M{Lh=uZ+D>!u7dfoA!@^huY$v}`{8rhRP-*_xqy?<55Bwn z?J7FTi2Bks?*kovi74 zw3dXRT{?roxE>Y4+hhz2z*n6BMjpjOQ<%x0x!~L&?*SUI2@rkgVV=fAL-$y&z2N6X zI5R*G$Lc6?<1W1?-@dZV8ssRrPGG{sbRr*K`g&X*8I9nQiL1n*+qTJN#Xez_{TzQ5 z?@nzogEANk{ygqSMD_1LD4x;A9I|c;*ifONA&~Qw;xyEWtv5{B1SF<>GA!pg2@Sm2 zleOV^bH4@vtH{3p#9Ls`?~rO)^H1=y?#Fv=&6=zgnLPsN`nKu&`lT)$F*`P$4iz4; zbyWkXHZhyB=yZKCC*-2<7*8}r6ZMK?McgLsYfuFOpcSGk5!x!rEc%{#!8P4&+xaxX z26b*uGcPAJ;gG3*;Dsmy$J^XDCDTy{KKn0A1X!@79B7AD5MF|{O)2?pp1_=AAPX5{APg?Z_%I+4N zFrU)S2E0x86(n@*dJs|Mpot&Lsjs8F#|21L#?Kl$jasCEU?dZb^ZJ;TS;HX&-<;`W zLSqkvxK*o3QKJ(9FSQ85PRcjH?H=nB#%gjn;VJXEuZ0<1;9hh_k2dgd(#o<(gHs4M)*G|GftxS*a z6EF{wDd!#o>GcG7XH6YQ*3=&qisw||&&>+%8YS157@yds)}Me{dcGLnGw`tO$Xly7 zJ^}eUHv0|B5_a>Gu#;cnxx9Kw0&u<-zuL!o7dJ%@R!ioA3wi|j2O(lIX2|_aF3%j^ z6Mrrb<4px=(rais9Vh^lws`F-3}BAo?RD<+Q$R&5WOTB8uKnmqBs*-jVSM*p&02+2$7tvZ?{PeEsV~JPdes+-KAP4*Dhb6``__sDt^`xrt{;wgh@pyys?zVVc87jOeV3$&7ETjL zHEU4q7s_{{oEL()^RNsFVO4wj8z4Psu;_AWw`c()_i)!Lk7?`K)$1mfI-=<63fK2? z*F)iE5h%BsAn}Y%SB7j|Qo8NPo+$R=B}zhcAvrv%Moine0qX{sfpT=>ly0l?&Eg{f z6PW>?82IT)#a-=#{)-%f-(zEpSH=~ZMDV{nWDlaYqjz^aLERYIX z9r}n`epM6s-nESe@y>aLJA5hat&ZwCHdjTR5*5A$E0hZ2;}xx;q7P^`V2{i=gjrTp{C z1>EL0`r}V}%|{$BFpG(X7S)_^^F z<=Wm;l(F{REFg+WaA9mGU?!)Q1N}|Zex#}32l+GxQLqfqDnh<6Q{{4B9_QefrRV(Wk4;s zYeAKRuFX>!bW8@LX(*H5e(5x;Jgo|e&p8Blie+|YE)?M2`ctxN20g~ybm&We$p~{igf4ArueT75Em1I; z+=NSkSvLd{Mu01YW8&+MgX0LOeqH{dN~gqa12pr`jiN}sTp0ZB_%A&EQ%+z8Kr;b( zAef_r_7)%oexg;{I&DU8rGkEo8vFmnZ`nv&zlPY~b-G;H4xOT3XY8oyplAhkET@0` zlgS5$ZeRUG1^Cwhe-5~n9ZCo=VKRwKxl@Mq7x14XM6^Wmx&4%Y#b5tC*zJ@kInM^Y zKo`B}EDb++fe+&vuBx4Q?R>NTr^>*->wqZgx^GG^cdCHR}VC*(1w zvKcbye|e7+*fj$dn5uBYN(jxVeEkaITK;2Oa&KKfmA2`VN4FF}JpX6B*K*Xo324Be zBCW6C)kEl%EPGyh|MaexFufD1xa8(i3@o5`{NeJZT5+1q=?mV#fm%=05wcYz_gT@Td++ zt6#IC$>5)`z*aHA&XTn98J`DTqM(6<{zaJORZQnW7<8fW^9RZyXXViZtV7A${&z5Y zJVl7Ukbai$3DxO^0hE?_m~{?CUueUl;_j}1xiG~fdldbw+z#m1jF`mXYv96MxItvG ze5!5o1pTX(R~h<i zG#raRLvRI@2LQgG4%WY|qwxbyO6yJ>W(|*6RJt+etP5-`DUsOLN~_;5Hqk?{31s z&~>6$sZSrhO6zyn_+Loe!Fd*N+1T_P`Vu#3pI@Yiz9gf}_at)iK0Vwl`W*q#M*G&I z&%kef=Nv&?b;7>Oxj! Date: Sat, 11 May 2024 14:24:03 +0900 Subject: [PATCH 13/15] Fix minica version in CI workflow to prevent TLS test failure (#2307) --- .github/workflows/kvrocks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/kvrocks.yaml b/.github/workflows/kvrocks.yaml index 925d8ad776b..affa0a385fa 100644 --- a/.github/workflows/kvrocks.yaml +++ b/.github/workflows/kvrocks.yaml @@ -292,7 +292,7 @@ jobs: GOCASE_RUN_ARGS="" if [[ -n "${{ matrix.with_openssl }}" ]] && [[ "${{ matrix.os }}" == ubuntu* ]]; then git clone https://github.com/jsha/minica - cd minica && go build && cd .. + cd minica && git checkout 96a5c93723cf3d34b50b3e723a9f05cd3765bc67 && go build && cd .. ./minica/minica --domains localhost cp localhost/cert.pem tests/gocase/tls/cert/server.crt cp localhost/key.pem tests/gocase/tls/cert/server.key From b2347f31ef39f982067f41cd0310f7c802cc787a Mon Sep 17 00:00:00 2001 From: raffertyyu Date: Sat, 11 May 2024 14:25:12 +0800 Subject: [PATCH 14/15] Fix compiler warnings in KQIR and RDB (#2303) Co-authored-by: Twice --- src/search/executors/filter_executor.h | 3 +++ src/storage/rdb_ziplist.cc | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/search/executors/filter_executor.h b/src/search/executors/filter_executor.h index 83d45377820..6820ae586eb 100644 --- a/src/search/executors/filter_executor.h +++ b/src/search/executors/filter_executor.h @@ -100,6 +100,9 @@ struct QueryExprEvaluator { return l > r; case NumericCompareExpr::GET: return l >= r; + default: + CHECK(false) << "unreachable"; + __builtin_unreachable(); } } }; diff --git a/src/storage/rdb_ziplist.cc b/src/storage/rdb_ziplist.cc index 98f764b147e..b51dc8ddadc 100644 --- a/src/storage/rdb_ziplist.cc +++ b/src/storage/rdb_ziplist.cc @@ -196,7 +196,7 @@ uint32_t ZipList::ZipStoreEntryEncoding(unsigned char *p, size_t zl_size, unsign buf[3] = (rawlen >> 8) & 0xff; buf[4] = rawlen & 0xff; } - assert(zl_size >= zlHeaderSize + len); + assert(zl_size >= static_cast(zlHeaderSize) + len); /* Store this length at p. */ memcpy(p, buf, len); return len; From 1e23484fbd88a7169859e66f398dd695fdf36128 Mon Sep 17 00:00:00 2001 From: detached <54363053+13015517713@users.noreply.github.com> Date: Sat, 11 May 2024 15:58:26 +0800 Subject: [PATCH 15/15] Optimize the implementation of IntervalSet intersection (#2300) Co-authored-by: mwish Co-authored-by: Twice --- src/cluster/redis_slot.cc | 2 -- src/search/interval.h | 27 ++++++++++++++++++++++++--- tests/cppunit/interval_test.cc | 26 +++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/cluster/redis_slot.cc b/src/cluster/redis_slot.cc index 5934fd2d601..991b5d863e7 100644 --- a/src/cluster/redis_slot.cc +++ b/src/cluster/redis_slot.cc @@ -20,8 +20,6 @@ #include "redis_slot.h" -#include - #include #include #include diff --git a/src/search/interval.h b/src/search/interval.h index 5ce90a45282..efe462b4074 100644 --- a/src/search/interval.h +++ b/src/search/interval.h @@ -128,11 +128,32 @@ struct IntervalSet { } friend IntervalSet operator&(const IntervalSet &l, const IntervalSet &r) { - if (l.IsEmpty() || r.IsEmpty()) { - return IntervalSet(); + IntervalSet result; + + if (l.intervals.empty() || r.intervals.empty()) { + return result; + } + + auto it_l = l.intervals.begin(); + auto it_r = r.intervals.begin(); + + while (it_l != l.intervals.end() && it_r != r.intervals.end()) { + // Find overlap between current intervals + double start = std::max(it_l->first, it_r->first); + double end = std::min(it_l->second, it_r->second); + + if (start <= end) { + result.intervals.emplace_back(start, end); + } + + if (it_l->second < it_r->second) { + ++it_l; + } else { + ++it_r; + } } - return ~(~l | ~r); + return result; } friend IntervalSet operator|(const IntervalSet &l, const IntervalSet &r) { diff --git a/tests/cppunit/interval_test.cc b/tests/cppunit/interval_test.cc index 5090c4c8801..bffd5d630c2 100644 --- a/tests/cppunit/interval_test.cc +++ b/tests/cppunit/interval_test.cc @@ -48,6 +48,14 @@ TEST(IntervalSet, Simple) { (IntervalSet::DataType{{IntervalSet::minf, 1}, {4, IntervalSet::inf}})); ASSERT_EQ((IntervalSet(NumericCompareExpr::GET, 4) | IntervalSet(NumericCompareExpr::NE, 1)).intervals, (IntervalSet::DataType{{IntervalSet::minf, 1}, {IntervalSet::NextNum(1), IntervalSet::inf}})); + + ASSERT_TRUE((IntervalSet(Interval(1, 2)) & IntervalSet(Interval(3, 4))).IsEmpty()); + ASSERT_EQ((IntervalSet(Interval(1, 2)) & IntervalSet(Interval(2, 4))).intervals, (IntervalSet::DataType{{2, 2}})); + ASSERT_EQ((IntervalSet(Interval(1, 3)) & IntervalSet(Interval(2, 4))).intervals, (IntervalSet::DataType{{2, 3}})); + ASSERT_EQ((IntervalSet(Interval(3, 8)) & (IntervalSet(Interval(1, 4)) | IntervalSet(Interval(5, 7)))).intervals, + (IntervalSet::DataType{{3, 4}, {5, 7}})); + ASSERT_EQ((IntervalSet(Interval(3, 8)) & (IntervalSet(Interval(1, 4)) | IntervalSet(Interval(9, 11)))).intervals, + (IntervalSet::DataType{{3, 4}})); ASSERT_EQ((IntervalSet(NumericCompareExpr::GET, 1) & IntervalSet(NumericCompareExpr::LT, 4)).intervals, (IntervalSet::DataType{{1, 4}})); ASSERT_EQ((IntervalSet(NumericCompareExpr::GET, 1) & IntervalSet(NumericCompareExpr::NE, 4)).intervals, @@ -60,9 +68,21 @@ TEST(IntervalSet, Simple) { IntervalSet({2, 5}) | IntervalSet({7, 8})); ASSERT_EQ(~IntervalSet({2, 8}), IntervalSet({IntervalSet::minf, 2}) | IntervalSet({8, IntervalSet::inf})); - for (auto i = 0; i < 1000; ++i) { - auto gen = [] { return static_cast(rand()) / 100; }; - auto geni = [&gen] { return IntervalSet({gen(), gen()}); }; + for (auto i = 0; i < 2000; ++i) { + auto gen = [] { return static_cast(std::rand()) / 100; }; + auto geni = [&gen] { + auto r = std::rand() % 50; + if (r == 0) { + return IntervalSet(NumericCompareExpr::GET, gen()); + } else if (r == 1) { + return IntervalSet(NumericCompareExpr::LT, gen()); + } else if (r == 2) { + return IntervalSet(NumericCompareExpr::NE, gen()); + } else { + return IntervalSet({gen(), gen()}); + } + }; + auto l = geni(), r = geni(); for (int j = 0; j < i % 10; ++j) { l = l | geni();