| /* |
| * 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 <algorithm> |
| #include <limits> |
| #include <memory> |
| #include <stdexcept> |
| |
| #include "command_parser.h" |
| #include "commander.h" |
| #include "error_constants.h" |
| #include "event_util.h" |
| #include "server/server.h" |
| #include "status.h" |
| #include "time_util.h" |
| #include "types/redis_stream.h" |
| |
| namespace redis { |
| |
| namespace { |
| // for XRead and XReadGroup stream range parse. |
| CommandKeyRange ParseStreamReadRange(const std::vector<std::string> &args, uint32_t start_offset) { |
| // assert here we must have a stream in args since it has been parsed. |
| auto stream_keyword_iter = std::find_if(std::next(args.cbegin(), start_offset), args.cend(), |
| [](const std::string &arg) { return util::EqualICase(arg, "streams"); }); |
| int stream_pos = static_cast<int>(std::distance(args.cbegin(), stream_keyword_iter)); |
| int stream_size = static_cast<int>(args.size() - stream_pos) / 2; |
| |
| CommandKeyRange range; |
| range.first_key = stream_pos + 1; |
| range.key_step = 1; |
| range.last_key = range.first_key + stream_size - 1; |
| return range; |
| } |
| } // namespace |
| |
| void AddStreamEntriesToResponse(std::string *output, const std::vector<StreamEntry> &entries) { |
| output->append(redis::MultiLen(entries.size())); |
| for (const auto &entry : entries) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(entry.key)); |
| output->append(redis::ArrayOfBulkStrings(entry.values)); |
| } |
| } |
| |
| class CommandXAck : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| stream_name_ = args[1]; |
| group_name_ = args[2]; |
| StreamEntryID tmp_id; |
| for (size_t i = 3; i < args.size(); ++i) { |
| auto s = ParseStreamEntryID(args[i], &tmp_id); |
| if (!s.IsOK()) return s; |
| entry_ids_.emplace_back(tmp_id); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| uint64_t acknowledged = 0; |
| |
| auto s = stream_db.DeletePelEntries(ctx, stream_name_, group_name_, entry_ids_, &acknowledged); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| *output = redis::Integer(acknowledged); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string stream_name_; |
| std::string group_name_; |
| std::vector<StreamEntryID> entry_ids_; |
| }; |
| |
| class CommandXAdd : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| bool entry_id_found = false; |
| stream_name_ = args[1]; |
| |
| for (size_t i = 2; i < args.size();) { |
| auto val = entry_id_found ? args[i] : util::ToLower(args[i]); |
| |
| if (val == "nomkstream" && !entry_id_found) { |
| nomkstream_ = true; |
| ++i; |
| continue; |
| } |
| |
| if (val == "maxlen" && !entry_id_found) { |
| if (i + 1 >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| size_t max_len_idx = 0; |
| bool modifier_found = false; |
| if (args[i + 1] == "=" || args[i + 1] == "~") { |
| max_len_idx = i + 2; |
| modifier_found = true; |
| } else { |
| max_len_idx = i + 1; |
| } |
| |
| if (max_len_idx >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| auto parse_result = ParseInt<uint64_t>(args[max_len_idx], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| max_len_ = *parse_result; |
| with_max_len_ = true; |
| |
| i += modifier_found ? 3 : 2; |
| continue; |
| } |
| |
| if (val == "minid" && !entry_id_found) { |
| if (i + 1 >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| size_t min_id_idx = 0; |
| bool modifier_found = false; |
| if (args[i + 1] == "=" || args[i + 1] == "~") { |
| min_id_idx = i + 2; |
| modifier_found = true; |
| } else { |
| min_id_idx = i + 1; |
| } |
| |
| if (min_id_idx >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| auto s = ParseStreamEntryID(args[min_id_idx], &min_id_); |
| if (!s.IsOK()) return s; |
| |
| with_min_id_ = true; |
| i += modifier_found ? 3 : 2; |
| continue; |
| } |
| |
| if (val == "limit" && !entry_id_found) { |
| return {Status::RedisParseErr, errLimitOptionNotAllowed}; |
| } |
| |
| if (!entry_id_found) { |
| auto result = ParseNextStreamEntryIDStrategy(val); |
| if (!result.IsOK()) return result; |
| |
| next_id_strategy_ = std::move(*result); |
| |
| entry_id_found = true; |
| ++i; |
| continue; |
| } |
| |
| name_value_pairs_.push_back(val); |
| ++i; |
| } |
| |
| if (name_value_pairs_.empty() || name_value_pairs_.size() % 2 != 0) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::StreamAddOptions options; |
| options.nomkstream = nomkstream_; |
| if (with_max_len_) { |
| options.trim_options.strategy = StreamTrimStrategy::MaxLen; |
| options.trim_options.max_len = max_len_; |
| } |
| if (with_min_id_) { |
| options.trim_options.strategy = StreamTrimStrategy::MinID; |
| options.trim_options.min_id = min_id_; |
| } |
| options.next_id_strategy = std::move(next_id_strategy_); |
| |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| StreamEntryID entry_id; |
| |
| auto s = stream_db.Add(ctx, stream_name_, options, name_value_pairs_, &entry_id); |
| if (!s.ok() && !s.IsNotFound()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (s.IsNotFound() && nomkstream_) { |
| *output = conn->NilString(); |
| return Status::OK(); |
| } |
| |
| *output = redis::BulkString(entry_id.ToString()); |
| |
| srv->OnEntryAddedToStream(conn->GetNamespace(), stream_name_, entry_id); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string stream_name_; |
| uint64_t max_len_ = 0; |
| redis::StreamEntryID min_id_; |
| std::unique_ptr<redis::NextStreamEntryIDGenerationStrategy> next_id_strategy_; |
| std::vector<std::string> name_value_pairs_; |
| bool nomkstream_ = false; |
| bool with_max_len_ = false; |
| bool with_min_id_ = false; |
| }; |
| |
| class CommandXDel : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| for (size_t i = 2; i < args.size(); ++i) { |
| redis::StreamEntryID id; |
| auto s = ParseStreamEntryID(args[i], &id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| |
| ids_.push_back(id); |
| } |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| uint64_t deleted = 0; |
| |
| auto s = stream_db.DeleteEntries(ctx, args_[1], ids_, &deleted); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(deleted); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::vector<redis::StreamEntryID> ids_; |
| }; |
| |
| class CommandXClaim : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| if (args.size() < 6) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| CommandParser parser(args, 1); |
| stream_name_ = GET_OR_RET(parser.TakeStr()); |
| group_name_ = GET_OR_RET(parser.TakeStr()); |
| consumer_name_ = GET_OR_RET(parser.TakeStr()); |
| auto parse_result = parser.TakeInt<int64_t>(); |
| if (!parse_result.IsOK()) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| min_idle_time_ms_ = parse_result.GetValue(); |
| if (min_idle_time_ms_ < 0) { |
| min_idle_time_ms_ = 0; |
| } |
| |
| while (parser.Good() && !isOption(parser.RawPeek())) { |
| auto raw_id = GET_OR_RET(parser.TakeStr()); |
| redis::StreamEntryID id; |
| auto s = ParseStreamEntryID(raw_id, &id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| entry_ids_.emplace_back(id); |
| } |
| |
| while (parser.Good()) { |
| if (parser.EatEqICase("idle")) { |
| auto parse_result = parser.TakeInt<int64_t>(); |
| if (!parse_result.IsOK()) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| if (parse_result.GetValue() < 0) { |
| return {Status::RedisParseErr, "IDLE for XCLAIM must be non-negative"}; |
| } |
| stream_claim_options_.idle_time_ms = parse_result.GetValue(); |
| } else if (parser.EatEqICase("time")) { |
| auto parse_result = parser.TakeInt<int64_t>(); |
| if (!parse_result.IsOK()) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| if (parse_result.GetValue() < 0) { |
| return {Status::RedisParseErr, "TIME for XCLAIM must be non-negative"}; |
| } |
| stream_claim_options_.with_time = true; |
| stream_claim_options_.last_delivery_time_ms = parse_result.GetValue(); |
| } else if (parser.EatEqICase("retrycount")) { |
| auto parse_result = parser.TakeInt<int64_t>(); |
| if (!parse_result.IsOK()) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| if (parse_result.GetValue() < 0) { |
| return {Status::RedisParseErr, "RETRYCOUNT for XCLAIM must be non-negative"}; |
| } |
| stream_claim_options_.with_retry_count = true; |
| stream_claim_options_.last_delivery_count = parse_result.GetValue(); |
| } else if (parser.EatEqICase("force")) { |
| stream_claim_options_.force = true; |
| } else if (parser.EatEqICase("justid")) { |
| stream_claim_options_.just_id = true; |
| } else if (parser.EatEqICase("lastid")) { |
| auto last_id = GET_OR_RET(parser.TakeStr()); |
| auto s = ParseStreamEntryID(last_id, &stream_claim_options_.last_delivered_id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| } else { |
| return parser.InvalidSyntax(); |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| StreamClaimResult result; |
| |
| auto s = stream_db.ClaimPelEntries(ctx, stream_name_, group_name_, consumer_name_, min_idle_time_ms_, entry_ids_, |
| stream_claim_options_, &result); |
| if (!s.ok()) { |
| if (s.IsNotFound()) { |
| return {Status::RedisNoGroup, "No such key '" + stream_name_ + "' or consumer group '" + group_name_ + "'"}; |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (!stream_claim_options_.just_id) { |
| AddStreamEntriesToResponse(output, result.entries); |
| } else { |
| output->append(redis::MultiLen(result.ids.size())); |
| for (const auto &id : result.ids) { |
| output->append(redis::BulkString(id)); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string stream_name_; |
| std::string group_name_; |
| std::string consumer_name_; |
| uint64_t min_idle_time_ms_; |
| std::vector<StreamEntryID> entry_ids_; |
| StreamClaimOptions stream_claim_options_; |
| |
| bool static isOption(const std::string &arg) { |
| static const std::unordered_set<std::string> options = {"idle", "time", "retrycount", "force", "justid", "lastid"}; |
| return options.find(util::ToLower(arg)) != options.end(); |
| } |
| }; |
| |
| class CommandAutoClaim : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| key_name_ = GET_OR_RET(parser.TakeStr()); |
| group_name_ = GET_OR_RET(parser.TakeStr()); |
| consumer_name_ = GET_OR_RET(parser.TakeStr()); |
| if (auto parse_status = parser.TakeInt<uint64_t>(); !parse_status.IsOK()) { |
| return {Status::RedisParseErr, "Invalid min-idle-time argument for XAUTOCLAIM"}; |
| } else { |
| options_.min_idle_time_ms = parse_status.GetValue(); |
| } |
| |
| auto start_str = GET_OR_RET(parser.TakeStr()); |
| if (!start_str.empty() && start_str.front() == '(') { |
| options_.exclude_start = true; |
| start_str = start_str.substr(1); |
| } |
| if (!options_.exclude_start && start_str == "-") { |
| options_.start_id = StreamEntryID::Minimum(); |
| } else { |
| auto parse_status = ParseRangeStart(start_str, &options_.start_id); |
| if (!parse_status.IsOK()) { |
| return parse_status; |
| } |
| } |
| |
| if (parser.EatEqICase("count")) { |
| uint64_t count = GET_OR_RET(parser.TakeInt<uint64_t>()); |
| constexpr uint64_t min_count = 1; |
| uint64_t max_count = std::numeric_limits<int64_t>::max() / |
| (std::max(static_cast<uint64_t>(sizeof(StreamEntryID)), options_.attempts_factors)); |
| if (count < min_count || count > max_count) { |
| return {Status::RedisParseErr, "COUNT must be > 0"}; |
| } |
| options_.count = count; |
| } |
| |
| if (parser.Good() && parser.EatEqICase("justid")) { |
| options_.just_id = true; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| StreamAutoClaimResult result; |
| |
| auto s = stream_db.AutoClaim(ctx, key_name_, group_name_, consumer_name_, options_, &result); |
| if (!s.ok()) { |
| if (s.IsNotFound()) { |
| return {Status::RedisNoGroup, "No such key '" + key_name_ + "' or consumer group '" + group_name_ + "'"}; |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| return sendResults(conn, result, output); |
| } |
| |
| private: |
| Status sendResults([[maybe_unused]] Connection *conn, const StreamAutoClaimResult &result, |
| std::string *output) const { |
| output->append(redis::MultiLen(3)); |
| output->append(redis::BulkString(result.next_claim_id)); |
| if (options_.just_id) { |
| output->append(redis::MultiLen(result.entries.size())); |
| for (const auto &item : result.entries) { |
| output->append(redis::BulkString(item.key)); |
| } |
| } else { |
| AddStreamEntriesToResponse(output, result.entries); |
| } |
| |
| output->append(redis::MultiLen(result.deleted_ids.size())); |
| for (const auto &item : result.deleted_ids) { |
| output->append(redis::BulkString(item)); |
| } |
| |
| return Status::OK(); |
| } |
| |
| std::string key_name_; |
| std::string group_name_; |
| std::string consumer_name_; |
| StreamAutoClaimOptions options_; |
| }; |
| |
| class CommandXGroup : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| subcommand_ = util::ToLower(GET_OR_RET(parser.TakeStr())); |
| stream_name_ = GET_OR_RET(parser.TakeStr()); |
| group_name_ = GET_OR_RET(parser.TakeStr()); |
| |
| if (subcommand_ == "create") { |
| if (args.size() < 5 || args.size() > 8) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| xgroup_create_options_.last_id = GET_OR_RET(parser.TakeStr()); |
| |
| while (parser.Good()) { |
| if (parser.EatEqICase("mkstream")) { |
| xgroup_create_options_.mkstream = true; |
| } else if (parser.EatEqICase("entriesread")) { |
| auto parse_result = parser.TakeInt<int64_t>(); |
| if (!parse_result.IsOK()) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| if (parse_result.GetValue() < 0 && parse_result.GetValue() != -1) { |
| return {Status::RedisParseErr, "value for ENTRIESREAD must be positive or -1"}; |
| } |
| xgroup_create_options_.entries_read = parse_result.GetValue(); |
| } else { |
| return parser.InvalidSyntax(); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "destroy") { |
| if (args.size() != 4) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "createconsumer" || subcommand_ == "delconsumer") { |
| if (args.size() != 5) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| consumer_name_ = GET_OR_RET(parser.TakeStr()); |
| |
| return Status::OK(); |
| } |
| |
| if (subcommand_ == "setid") { |
| if (args.size() != 5 && args.size() != 7) { |
| return {Status::RedisParseErr, errWrongNumOfArguments}; |
| } |
| |
| xgroup_create_options_.last_id = GET_OR_RET(parser.TakeStr()); |
| if (args.size() == 7) { |
| if (parser.EatEqICase("entriesread")) { |
| auto parse_result = parser.TakeInt<int64_t>(); |
| if (!parse_result.IsOK()) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| if (parse_result.GetValue() < 0 && parse_result.GetValue() != -1) { |
| return {Status::RedisParseErr, "value for ENTRIESREAD must be positive or -1"}; |
| } |
| xgroup_create_options_.entries_read = parse_result.GetValue(); |
| } else { |
| return parser.InvalidSyntax(); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| return {Status::RedisParseErr, "unknown subcommand"}; |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| if (subcommand_ == "create") { |
| auto s = stream_db.CreateGroup(ctx, stream_name_, xgroup_create_options_, group_name_); |
| if (!s.ok()) { |
| if (s.IsBusy()) { |
| return {Status::RedisBusyGroup, "consumer group name '" + group_name_ + "' already exists"}; |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::RESP_OK; |
| } |
| |
| if (subcommand_ == "destroy") { |
| bool destroyed = false; |
| auto s = stream_db.DestroyGroup(ctx, stream_name_, group_name_, &destroyed); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(destroyed ? 1 : 0); |
| } |
| |
| if (subcommand_ == "createconsumer") { |
| int created_number = 0; |
| auto s = stream_db.CreateConsumer(ctx, stream_name_, group_name_, consumer_name_, &created_number); |
| if (!s.ok()) { |
| if (s.IsNotFound()) { |
| return {Status::RedisNoGroup, "No such consumer group " + group_name_ + " for key name " + stream_name_}; |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(created_number); |
| } |
| |
| if (subcommand_ == "delconsumer") { |
| uint64_t deleted_pel = 0; |
| auto s = stream_db.DestroyConsumer(ctx, stream_name_, group_name_, consumer_name_, deleted_pel); |
| if (!s.ok()) { |
| if (s.IsNotFound()) { |
| return {Status::RedisNoGroup, "No such consumer group " + group_name_ + " for key name " + stream_name_}; |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(deleted_pel); |
| } |
| |
| if (subcommand_ == "setid") { |
| auto s = stream_db.GroupSetId(ctx, stream_name_, group_name_, xgroup_create_options_); |
| if (!s.ok()) { |
| if (s.IsNotFound()) { |
| return {Status::RedisNoGroup, "No such consumer group " + group_name_ + " for key name " + stream_name_}; |
| } |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::RESP_OK; |
| } |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string subcommand_; |
| std::string stream_name_; |
| std::string group_name_; |
| std::string consumer_name_; |
| StreamXGroupCreateOptions xgroup_create_options_; |
| }; |
| |
| class CommandXLen : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| if (args.size() > 2) { |
| redis::StreamEntryID id; |
| auto s = redis::ParseStreamEntryID(args[2], &id); |
| if (!s.IsOK()) return s; |
| |
| options_.with_entry_id = true; |
| options_.entry_id = id; |
| |
| if (args.size() > 3) { |
| if (args[3] == "-") { |
| options_.to_first = true; |
| } else if (args[3] != "+") { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| uint64_t len = 0; |
| |
| auto s = stream_db.Len(ctx, args_[1], options_, &len); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(len); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| redis::StreamLenOptions options_; |
| }; |
| |
| class CommandXInfo : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| auto val = util::ToLower(args[1]); |
| if (val == "stream" && args.size() >= 3) { |
| subcommand_ = "stream"; |
| |
| if (args.size() > 3 && util::ToLower(args[3]) == "full") { |
| full_ = true; |
| } |
| |
| if (args.size() > 5 && util::ToLower(args[4]) == "count") { |
| auto parse_result = ParseInt<uint64_t>(args[5], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| count_ = *parse_result; |
| } |
| } else if (val == "groups" && args.size() == 3) { |
| subcommand_ = "groups"; |
| } else if (val == "consumers" && args.size() == 4) { |
| subcommand_ = "consumers"; |
| } else { |
| return {Status::RedisParseErr, errUnknownSubcommandOrWrongArguments}; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (subcommand_ == "stream") { |
| return getStreamInfo(ctx, srv, conn, output); |
| } |
| |
| if (subcommand_ == "groups") { |
| return getGroupInfo(ctx, srv, conn, output); |
| } |
| |
| if (subcommand_ == "consumers") { |
| return getConsumerInfo(ctx, srv, conn, output); |
| } |
| return Status::OK(); |
| } |
| |
| private: |
| uint64_t count_ = 10; // default Redis value |
| std::string subcommand_; |
| bool full_ = false; |
| |
| Status getStreamInfo(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| redis::StreamInfo info; |
| |
| auto s = stream_db.GetStreamInfo(ctx, args_[2], full_, count_, &info); |
| if (!s.ok() && !s.IsNotFound()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (s.IsNotFound()) { |
| return {Status::RedisExecErr, errNoSuchKey}; |
| } |
| |
| if (!full_) { |
| output->append(conn->HeaderOfMap(7)); |
| } else { |
| output->append(conn->HeaderOfMap(6)); |
| } |
| output->append(redis::BulkString("length")); |
| output->append(redis::Integer(info.size)); |
| output->append(redis::BulkString("last-generated-id")); |
| output->append(redis::BulkString(info.last_generated_id.ToString())); |
| output->append(redis::BulkString("max-deleted-entry-id")); |
| output->append(redis::BulkString(info.max_deleted_entry_id.ToString())); |
| output->append(redis::BulkString("entries-added")); |
| output->append(redis::Integer(info.entries_added)); |
| output->append(redis::BulkString("recorded-first-entry-id")); |
| output->append(redis::BulkString(info.recorded_first_entry_id.ToString())); |
| if (!full_) { |
| output->append(redis::BulkString("first-entry")); |
| if (info.first_entry) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(info.first_entry->key)); |
| output->append(conn->MultiBulkString(info.first_entry->values)); |
| } else { |
| output->append(conn->NilString()); |
| } |
| output->append(redis::BulkString("last-entry")); |
| if (info.last_entry) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(info.last_entry->key)); |
| output->append(conn->MultiBulkString(info.last_entry->values)); |
| } else { |
| output->append(conn->NilString()); |
| } |
| } else { |
| output->append(redis::BulkString("entries")); |
| AddStreamEntriesToResponse(output, info.entries); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status getGroupInfo(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| std::vector<std::pair<std::string, StreamConsumerGroupMetadata>> result_vector; |
| |
| auto s = stream_db.GetGroupInfo(ctx, args_[2], result_vector); |
| if (!s.ok() && !s.IsNotFound()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (s.IsNotFound()) { |
| return {Status::RedisExecErr, errNoSuchKey}; |
| } |
| |
| output->append(redis::MultiLen(result_vector.size())); |
| for (auto const &it : result_vector) { |
| output->append(conn->HeaderOfMap(6)); |
| output->append(redis::BulkString("name")); |
| output->append(redis::BulkString(it.first)); |
| output->append(redis::BulkString("consumers")); |
| output->append(redis::Integer(it.second.consumer_number)); |
| output->append(redis::BulkString("pending")); |
| output->append(redis::Integer(it.second.pending_number)); |
| output->append(redis::BulkString("last-delivered-id")); |
| output->append(redis::BulkString(it.second.last_delivered_id.ToString())); |
| output->append(redis::BulkString("entries-read")); |
| if (it.second.entries_read == -1) { |
| output->append(conn->NilString()); |
| } else { |
| output->append(redis::Integer(it.second.entries_read)); |
| } |
| output->append(redis::BulkString("lag")); |
| if (it.second.lag == UINT64_MAX) { |
| output->append(conn->NilString()); |
| } else { |
| output->append(redis::Integer(it.second.lag)); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status getConsumerInfo(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| std::vector<std::pair<std::string, redis::StreamConsumerMetadata>> result_vector; |
| |
| auto s = stream_db.GetConsumerInfo(ctx, args_[2], args_[3], result_vector); |
| |
| if (!s.ok() && !s.IsNotFound()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (s.IsNotFound()) { |
| return {Status::RedisExecErr, errNoSuchKey}; |
| } |
| |
| output->append(redis::MultiLen(result_vector.size())); |
| auto now_ms = util::GetTimeStampMS(); |
| for (auto const &it : result_vector) { |
| output->append(conn->HeaderOfMap(4)); |
| output->append(redis::BulkString("name")); |
| output->append(redis::BulkString(it.first)); |
| output->append(redis::BulkString("pending")); |
| output->append(redis::Integer(it.second.pending_number)); |
| output->append(redis::BulkString("idle")); |
| output->append(redis::Integer(now_ms - it.second.last_attempted_interaction_ms)); |
| output->append(redis::BulkString("inactive")); |
| output->append(redis::Integer(now_ms - it.second.last_successful_interaction_ms)); |
| } |
| |
| return Status::OK(); |
| } |
| }; |
| |
| class CommandXPending : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| CommandParser parser(args, 1); |
| stream_name_ = GET_OR_RET(parser.TakeStr()); |
| group_name_ = GET_OR_RET(parser.TakeStr()); |
| if (parser.EatEqICase("idle")) { |
| options_.idle_time = GET_OR_RET(parser.TakeInt<uint64_t>()); |
| options_.with_time = true; |
| } |
| |
| if (parser.Good()) { |
| const std::string start_str = GET_OR_RET(parser.TakeStr()); |
| const std::string end_str = GET_OR_RET(parser.TakeStr()); |
| |
| // Extended XPENDING uses the same ID range rules as XRANGE (see Redis documentation). |
| if (start_str == "-") { |
| options_.start_id = StreamEntryID::Minimum(); |
| options_.exclude_start = false; |
| } else if (!start_str.empty() && start_str[0] == '(') { |
| options_.exclude_start = true; |
| auto s = ParseRangeStart(start_str.substr(1), &options_.start_id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| } else if (start_str == "+") { |
| options_.start_id = StreamEntryID::Maximum(); |
| options_.exclude_start = false; |
| } else { |
| auto s = ParseRangeStart(start_str, &options_.start_id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| options_.exclude_start = false; |
| } |
| |
| if (end_str == "+") { |
| options_.end_id = StreamEntryID::Maximum(); |
| options_.exclude_end = false; |
| } else if (!end_str.empty() && end_str[0] == '(') { |
| options_.exclude_end = true; |
| auto s = ParseRangeEnd(end_str.substr(1), &options_.end_id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| } else if (end_str == "-") { |
| options_.end_id = StreamEntryID::Minimum(); |
| options_.exclude_end = false; |
| } else { |
| auto s = ParseRangeEnd(end_str, &options_.end_id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| options_.exclude_end = false; |
| } |
| |
| options_.count = GET_OR_RET(parser.TakeInt<uint64_t>()); |
| options_.with_count = true; |
| if (parser.Good()) { |
| options_.consumer = GET_OR_RET(parser.TakeStr()); |
| options_.with_consumer = true; |
| } |
| } |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| std::vector<std::pair<std::string, int>> pending_infos; |
| StreamGetPendingEntryResult results; |
| options_.stream_name = stream_name_; |
| options_.group_name = group_name_; |
| std::vector<StreamNACK> ext_results; |
| |
| auto s = stream_db.GetPendingEntries(ctx, options_, results, ext_results); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| if (options_.with_count) { |
| return SendExtResults(conn, output, ext_results); |
| } |
| return SendResults(conn, output, results); |
| } |
| |
| static Status SendResults([[maybe_unused]] Connection *conn, std::string *output, |
| StreamGetPendingEntryResult &results) { |
| // NOTE: In the case that our stream has no pending elements, Redis will |
| // return NilString for the first and last entry IDs, and a nil array |
| // for the consumer infos. Make this a special case to maintain consistency |
| // with Redis. |
| if (results.pending_number == 0) { |
| output->append(redis::MultiLen(4)); |
| output->append(redis::Integer(0)); |
| output->append(conn->NilString()); |
| output->append(conn->NilString()); |
| output->append(conn->NilArray()); |
| } else { |
| output->append(redis::MultiLen(4)); |
| output->append(redis::Integer(results.pending_number)); |
| output->append(redis::BulkString(results.first_entry_id.ToString())); |
| output->append(redis::BulkString(results.last_entry_id.ToString())); |
| output->append(redis::MultiLen(results.consumer_infos.size())); |
| for (const auto &entry : results.consumer_infos) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(entry.first)); |
| output->append(redis::BulkString(std::to_string(entry.second))); |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| static Status SendExtResults([[maybe_unused]] Connection *conn, std::string *output, |
| std::vector<StreamNACK> &ext_results) { |
| auto now = util::GetTimeStampMS(); |
| output->append(redis::MultiLen(ext_results.size())); |
| for (const auto &entry : ext_results) { |
| output->append(redis::MultiLen(4)); |
| output->append(redis::BulkString(entry.id.ToString())); |
| output->append(redis::BulkString(entry.pel_entry.consumer_name)); |
| output->append(redis::Integer(now - entry.pel_entry.last_delivery_time_ms)); |
| output->append(redis::Integer(entry.pel_entry.last_delivery_count)); |
| } |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string group_name_; |
| std::string stream_name_; |
| std::string consumer_name_; |
| StreamPendingOptions options_; |
| }; |
| |
| class CommandXRange : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| stream_name_ = args[1]; |
| |
| if (args[2] == "-") { |
| start_ = redis::StreamEntryID::Minimum(); |
| } else if (args[2][0] == '(') { |
| exclude_start_ = true; |
| auto s = ParseRangeStart(args[2].substr(1), &start_); |
| if (!s.IsOK()) return s; |
| } else if (args[2] == "+") { |
| start_ = redis::StreamEntryID::Maximum(); |
| } else { |
| auto s = ParseRangeStart(args[2], &start_); |
| if (!s.IsOK()) return s; |
| } |
| |
| if (args[3] == "+") { |
| end_ = redis::StreamEntryID::Maximum(); |
| } else if (args[3][0] == '(') { |
| exclude_end_ = true; |
| auto s = ParseRangeEnd(args[3].substr(1), &end_); |
| if (!s.IsOK()) return s; |
| } else if (args[3] == "-") { |
| end_ = redis::StreamEntryID::Minimum(); |
| } else { |
| auto s = ParseRangeEnd(args[3], &end_); |
| if (!s.IsOK()) return s; |
| } |
| |
| if (args.size() >= 5 && util::ToLower(args[4]) == "count") { |
| if (args.size() != 6) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| with_count_ = true; |
| |
| auto parse_result = ParseInt<uint64_t>(args[5], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| count_ = *parse_result; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (with_count_ && count_ == 0) { |
| *output = conn->NilString(); |
| return Status::OK(); |
| } |
| |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| redis::StreamRangeOptions options; |
| options.reverse = false; |
| options.start = start_; |
| options.end = end_; |
| options.with_count = with_count_; |
| options.count = count_; |
| options.exclude_start = exclude_start_; |
| options.exclude_end = exclude_end_; |
| |
| std::vector<StreamEntry> result; |
| |
| auto s = stream_db.Range(ctx, stream_name_, options, &result); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| AddStreamEntriesToResponse(output, result); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string stream_name_; |
| StreamEntryID start_; |
| StreamEntryID end_; |
| uint64_t count_ = 0; |
| bool exclude_start_ = false; |
| bool exclude_end_ = false; |
| bool with_count_ = false; |
| }; |
| |
| class CommandXRevRange : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| stream_name_ = args[1]; |
| |
| if (args[2] == "+") { |
| start_ = redis::StreamEntryID::Maximum(); |
| } else if (args[2][0] == '(') { |
| exclude_start_ = true; |
| auto s = ParseRangeEnd(args[2].substr(1), &start_); |
| if (!s.IsOK()) return s; |
| } else if (args[2] == "-") { |
| start_ = redis::StreamEntryID::Minimum(); |
| } else { |
| auto s = ParseRangeEnd(args[2], &start_); |
| if (!s.IsOK()) return s; |
| } |
| |
| if (args[3] == "-") { |
| end_ = redis::StreamEntryID::Minimum(); |
| } else if (args[3][0] == '(') { |
| exclude_end_ = true; |
| auto s = ParseRangeStart(args[3].substr(1), &end_); |
| if (!s.IsOK()) return s; |
| } else if (args[3] == "+") { |
| end_ = redis::StreamEntryID::Maximum(); |
| } else { |
| auto s = ParseRangeStart(args[3], &end_); |
| if (!s.IsOK()) return s; |
| } |
| |
| if (args.size() >= 5 && util::ToLower(args[4]) == "count") { |
| if (args.size() != 6) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| with_count_ = true; |
| |
| auto parse_result = ParseInt<uint64_t>(args[5]); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| count_ = *parse_result; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| if (with_count_ && count_ == 0) { |
| *output = conn->NilString(); |
| return Status::OK(); |
| } |
| |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| redis::StreamRangeOptions options; |
| options.reverse = true; |
| options.start = start_; |
| options.end = end_; |
| options.with_count = with_count_; |
| options.count = count_; |
| options.exclude_start = exclude_start_; |
| options.exclude_end = exclude_end_; |
| |
| std::vector<StreamEntry> result; |
| |
| auto s = stream_db.Range(ctx, stream_name_, options, &result); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| AddStreamEntriesToResponse(output, result); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string stream_name_; |
| StreamEntryID start_; |
| StreamEntryID end_; |
| uint64_t count_ = 0; |
| bool exclude_start_ = false; |
| bool exclude_end_ = false; |
| bool with_count_ = false; |
| }; |
| |
| class CommandXRead : public Commander, |
| private EvbufCallbackBase<CommandXRead, false>, |
| private EventCallbackBase<CommandXRead> { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| size_t streams_word_idx = 0; |
| |
| for (size_t i = 1; i < args.size();) { |
| auto arg = util::ToLower(args[i]); |
| |
| if (arg == "streams") { |
| streams_word_idx = i; |
| break; |
| } |
| |
| if (arg == "count") { |
| if (i + 1 >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| with_count_ = true; |
| |
| auto parse_result = ParseInt<uint64_t>(args[i + 1], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| count_ = *parse_result; |
| i += 2; |
| continue; |
| } |
| |
| if (arg == "block") { |
| if (i + 1 >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| block_ = true; |
| |
| auto parse_result = ParseInt<int64_t>(args[i + 1], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| if (*parse_result < 0) { |
| return {Status::RedisParseErr, errTimeoutIsNegative}; |
| } |
| |
| block_timeout_ = *parse_result; |
| i += 2; |
| continue; |
| } |
| |
| ++i; |
| } |
| |
| if (streams_word_idx == 0) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| if ((args.size() - streams_word_idx - 1) % 2 != 0) { |
| return {Status::RedisParseErr, errUnbalancedStreamList}; |
| } |
| |
| size_t number_of_streams = (args.size() - streams_word_idx - 1) / 2; |
| |
| for (size_t i = streams_word_idx + 1; i <= streams_word_idx + number_of_streams; ++i) { |
| streams_.push_back(args[i]); |
| const auto &id_str = args[i + number_of_streams]; |
| bool get_latest = id_str == "$"; |
| latest_marks_.push_back(get_latest); |
| StreamEntryID id; |
| if (!get_latest) { |
| auto s = ParseStreamEntryID(id_str, &id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| } |
| ids_.push_back(id); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| std::vector<redis::StreamReadResult> results; |
| |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| if (latest_marks_[i]) { |
| continue; |
| } |
| |
| redis::StreamRangeOptions options; |
| options.reverse = false; |
| options.start = ids_[i]; |
| options.end = StreamEntryID::Maximum(); |
| options.with_count = with_count_; |
| options.count = count_; |
| options.exclude_start = true; |
| options.exclude_end = false; |
| |
| std::vector<StreamEntry> result; |
| auto s = stream_db.Range(ctx, streams_[i], options, &result); |
| if (!s.ok() && !s.IsNotFound()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (result.size() > 0) { |
| results.emplace_back(streams_[i], result); |
| } |
| } |
| |
| if (block_ && results.empty()) { |
| if (conn->IsInExec()) { |
| *output = conn->NilArray(); |
| return Status::OK(); // No blocking in multi-exec |
| } |
| |
| return BlockingRead(srv, conn, &stream_db); |
| } |
| |
| if (!block_ && results.empty()) { |
| *output = conn->NilArray(); |
| return Status::OK(); |
| } |
| |
| return SendResults(output, results); |
| } |
| |
| static Status SendResults(std::string *output, const std::vector<StreamReadResult> &results) { |
| output->append(redis::MultiLen(results.size())); |
| |
| for (const auto &result : results) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(result.name)); |
| |
| AddStreamEntriesToResponse(output, result.entries); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status BlockingRead(Server *srv, Connection *conn, redis::Stream *stream_db) { |
| if (!with_count_) { |
| with_count_ = true; |
| count_ = blocked_default_count_; |
| } |
| |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| if (latest_marks_[i]) { |
| StreamEntryID last_generated_id; |
| engine::Context ctx(srv->storage); |
| auto s = stream_db->GetLastGeneratedID(ctx, streams_[i], &last_generated_id); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| ids_[i] = last_generated_id; |
| } |
| } |
| |
| srv_ = srv; |
| conn_ = conn; |
| |
| srv_->BlockOnStreams(streams_, ids_, conn_); |
| |
| auto bev = conn->GetBufferEvent(); |
| SetCB(bev); |
| |
| if (block_timeout_ > 0) { |
| timer_.reset(NewTimer(bufferevent_get_base(bev))); |
| timeval tm; |
| if (block_timeout_ > 1000) { |
| tm.tv_sec = block_timeout_ / 1000; |
| tm.tv_usec = (block_timeout_ % 1000) * 1000; |
| } else { |
| tm.tv_sec = 0; |
| tm.tv_usec = block_timeout_ * 1000; |
| } |
| |
| evtimer_add(timer_.get(), &tm); |
| } |
| |
| return {Status::BlockingCmd}; |
| } |
| |
| void OnWrite(bufferevent *bev) { |
| if (timer_ != nullptr) { |
| timer_.reset(); |
| } |
| |
| unblockAll(); |
| conn_->SetCB(bev); |
| bufferevent_enable(bev, EV_READ); |
| |
| redis::Stream stream_db(srv_->storage, conn_->GetNamespace()); |
| |
| std::vector<StreamReadResult> results; |
| engine::Context ctx(srv_->storage); |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| redis::StreamRangeOptions options; |
| options.reverse = false; |
| options.start = ids_[i]; |
| options.end = StreamEntryID::Maximum(); |
| options.with_count = with_count_; |
| options.count = count_; |
| options.exclude_start = true; |
| options.exclude_end = false; |
| |
| std::vector<StreamEntry> result; |
| auto s = stream_db.Range(ctx, streams_[i], options, &result); |
| if (!s.ok() && !s.IsNotFound()) { |
| conn_->Reply(redis::Error({Status::NotOK, s.ToString()})); |
| return; |
| } |
| |
| if (result.size() > 0) { |
| results.emplace_back(streams_[i], result); |
| } |
| } |
| |
| if (results.empty()) { |
| conn_->Reply(conn_->NilArray()); |
| return; |
| } |
| |
| SendReply(results); |
| } |
| |
| void SendReply(const std::vector<StreamReadResult> &results) { |
| std::string output; |
| |
| output.append(redis::MultiLen(results.size())); |
| |
| for (const auto &result : results) { |
| output.append(redis::MultiLen(2)); |
| output.append(redis::BulkString(result.name)); |
| |
| AddStreamEntriesToResponse(&output, result.entries); |
| } |
| |
| conn_->Reply(output); |
| } |
| |
| void OnEvent(bufferevent *bev, int16_t events) { |
| if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { |
| if (timer_ != nullptr) { |
| timer_.reset(); |
| } |
| unblockAll(); |
| } |
| conn_->OnEvent(bev, events); |
| } |
| |
| void TimerCB(int, [[maybe_unused]] int16_t events) { |
| conn_->Reply(conn_->NilString()); |
| |
| timer_.reset(); |
| |
| unblockAll(); |
| |
| auto bev = conn_->GetBufferEvent(); |
| conn_->SetCB(bev); |
| bufferevent_enable(bev, EV_READ); |
| } |
| |
| static const inline CommandKeyRangeGen keyRangeGen = [](const std::vector<std::string> &args) { |
| return ParseStreamReadRange(args, 0); |
| }; |
| |
| private: |
| std::vector<std::string> streams_; |
| std::vector<StreamEntryID> ids_; |
| std::vector<bool> latest_marks_; |
| Server *srv_ = nullptr; |
| Connection *conn_ = nullptr; |
| UniqueEvent timer_; |
| uint64_t count_ = 0; |
| int64_t block_timeout_ = 0; |
| int blocked_default_count_ = 1000; |
| bool with_count_ = false; |
| bool block_ = false; |
| |
| void unblockAll() { srv_->UnblockOnStreams(streams_, conn_); } |
| }; |
| |
| class CommandXReadGroup : public Commander, |
| private EvbufCallbackBase<CommandXReadGroup, false>, |
| private EventCallbackBase<CommandXReadGroup> { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| size_t streams_word_idx = 0; |
| if (util::ToLower(args[1]) != "group") { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| group_name_ = args[2]; |
| consumer_name_ = args[3]; |
| |
| for (size_t i = 4; i < args.size();) { |
| auto arg = util::ToLower(args[i]); |
| |
| if (arg == "streams") { |
| streams_word_idx = i; |
| break; |
| } |
| |
| if (arg == "count") { |
| if (i + 1 >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| with_count_ = true; |
| |
| auto parse_result = ParseInt<uint64_t>(args[i + 1], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| count_ = *parse_result; |
| i += 2; |
| continue; |
| } |
| |
| if (arg == "block") { |
| if (i + 1 >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| block_ = true; |
| |
| auto parse_result = ParseInt<int64_t>(args[i + 1], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| if (*parse_result < 0) { |
| return {Status::RedisParseErr, errTimeoutIsNegative}; |
| } |
| |
| block_timeout_ = *parse_result; |
| i += 2; |
| continue; |
| } |
| |
| if (arg == "noack") { |
| noack_ = true; |
| } |
| |
| ++i; |
| } |
| |
| if (streams_word_idx == 0) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| if ((args.size() - streams_word_idx - 1) % 2 != 0) { |
| return {Status::RedisParseErr, errUnbalancedStreamList}; |
| } |
| |
| size_t number_of_streams = (args.size() - streams_word_idx - 1) / 2; |
| |
| for (size_t i = streams_word_idx + 1; i <= streams_word_idx + number_of_streams; ++i) { |
| streams_.push_back(args[i]); |
| const auto &id_str = args[i + number_of_streams]; |
| bool get_latest = id_str == ">"; |
| latest_marks_.push_back(get_latest); |
| if (!get_latest) { |
| block_ = false; |
| } |
| StreamEntryID id; |
| if (!get_latest) { |
| auto s = ParseStreamEntryID(id_str, &id); |
| if (!s.IsOK()) { |
| return s; |
| } |
| } |
| ids_.push_back(id); |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| std::vector<redis::StreamReadResult> results; |
| |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| redis::StreamRangeOptions options; |
| options.reverse = false; |
| options.start = ids_[i]; |
| options.end = StreamEntryID::Maximum(); |
| options.with_count = with_count_; |
| options.count = count_; |
| options.exclude_start = true; |
| options.exclude_end = false; |
| |
| std::vector<StreamEntry> result; |
| auto s = stream_db.RangeWithPending(ctx, streams_[i], options, &result, group_name_, consumer_name_, noack_, |
| latest_marks_[i]); |
| if (!s.ok() && !s.IsNotFound()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| if (result.size() > 0) { |
| results.emplace_back(streams_[i], result); |
| } |
| } |
| |
| if (block_ && results.empty()) { |
| if (conn->IsInExec()) { |
| output->append(redis::MultiLen(streams_.size())); |
| for (const auto &stream_name : streams_) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(stream_name)); |
| output->append(redis::MultiLen(0)); |
| } |
| return Status::OK(); // No blocking in multi-exec |
| } |
| |
| return BlockingRead(srv, conn, &stream_db); |
| } |
| |
| if (!block_ && results.empty()) { |
| output->append(redis::MultiLen(streams_.size())); |
| for (const auto &stream_name : streams_) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(stream_name)); |
| output->append(redis::MultiLen(0)); |
| } |
| return Status::OK(); |
| } |
| |
| return SendResults(conn, output, results); |
| } |
| |
| Status SendResults(Connection *conn, std::string *output, const std::vector<StreamReadResult> &results) { |
| output->append(redis::MultiLen(results.size())); |
| int id = 0; |
| for (const auto &result : results) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(result.name)); |
| output->append(redis::MultiLen(result.entries.size())); |
| for (const auto &entry : result.entries) { |
| output->append(redis::MultiLen(2)); |
| output->append(redis::BulkString(entry.key)); |
| if (entry.values.size() == 0 && !latest_marks_[id]) { |
| output->append(conn->NilString()); |
| continue; |
| } |
| output->append(conn->MultiBulkString(entry.values)); |
| } |
| ++id; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status BlockingRead(Server *srv, Connection *conn, [[maybe_unused]] redis::Stream *stream_db) { |
| if (!with_count_) { |
| with_count_ = true; |
| count_ = blocked_default_count_; |
| } |
| |
| srv_ = srv; |
| conn_ = conn; |
| |
| srv_->BlockOnStreams(streams_, ids_, conn_); |
| |
| auto bev = conn->GetBufferEvent(); |
| SetCB(bev); |
| |
| if (block_timeout_ > 0) { |
| timer_.reset(NewTimer(bufferevent_get_base(bev))); |
| timeval tm; |
| if (block_timeout_ > 1000) { |
| tm.tv_sec = block_timeout_ / 1000; |
| tm.tv_usec = (block_timeout_ % 1000) * 1000; |
| } else { |
| tm.tv_sec = 0; |
| tm.tv_usec = block_timeout_ * 1000; |
| } |
| |
| evtimer_add(timer_.get(), &tm); |
| } |
| |
| return {Status::BlockingCmd}; |
| } |
| |
| void OnWrite(bufferevent *bev) { |
| if (timer_ != nullptr) { |
| timer_.reset(); |
| } |
| |
| unblockAll(); |
| conn_->SetCB(bev); |
| bufferevent_enable(bev, EV_READ); |
| |
| redis::Stream stream_db(srv_->storage, conn_->GetNamespace()); |
| |
| std::vector<StreamReadResult> results; |
| |
| std::vector<std::string> lock_keys; |
| lock_keys.reserve(streams_.size()); |
| for (auto &stream_name : streams_) { |
| auto ns_key = stream_db.AppendNamespacePrefix(stream_name); |
| lock_keys.emplace_back(std::move(ns_key)); |
| } |
| MultiLockGuard guard(srv_->storage->GetLockManager(), lock_keys); |
| engine::Context ctx(srv_->storage); |
| for (size_t i = 0; i < streams_.size(); ++i) { |
| redis::StreamRangeOptions options; |
| options.reverse = false; |
| options.start = ids_[i]; |
| options.end = StreamEntryID::Maximum(); |
| options.with_count = with_count_; |
| options.count = count_; |
| options.exclude_start = true; |
| options.exclude_end = false; |
| |
| std::vector<StreamEntry> result; |
| auto s = stream_db.RangeWithPending(ctx, streams_[i], options, &result, group_name_, consumer_name_, noack_, |
| latest_marks_[i]); |
| if (!s.ok() && !s.IsNotFound()) { |
| conn_->Reply(redis::Error({Status::NotOK, s.ToString()})); |
| return; |
| } |
| |
| if (result.size() > 0) { |
| results.emplace_back(streams_[i], result); |
| } |
| } |
| |
| if (results.empty()) { |
| conn_->Reply(redis::MultiLen(-1)); |
| return; |
| } |
| |
| SendReply(results); |
| } |
| |
| void SendReply(const std::vector<StreamReadResult> &results) { |
| std::string output; |
| |
| output.append(redis::MultiLen(results.size())); |
| |
| for (const auto &result : results) { |
| output.append(redis::MultiLen(2)); |
| output.append(redis::BulkString(result.name)); |
| AddStreamEntriesToResponse(&output, result.entries); |
| } |
| |
| conn_->Reply(output); |
| } |
| |
| void OnEvent(bufferevent *bev, int16_t events) { |
| if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { |
| if (timer_ != nullptr) { |
| timer_.reset(); |
| } |
| unblockAll(); |
| } |
| conn_->OnEvent(bev, events); |
| } |
| |
| void TimerCB(int, [[maybe_unused]] int16_t events) { |
| conn_->Reply(conn_->NilString()); |
| |
| timer_.reset(); |
| |
| unblockAll(); |
| |
| auto bev = conn_->GetBufferEvent(); |
| conn_->SetCB(bev); |
| bufferevent_enable(bev, EV_READ); |
| } |
| |
| static const inline CommandKeyRangeGen keyRangeGen = [](const std::vector<std::string> &args) { |
| return ParseStreamReadRange(args, 4); |
| }; |
| |
| private: |
| std::vector<std::string> streams_; |
| std::vector<StreamEntryID> ids_; |
| std::vector<bool> latest_marks_; |
| std::string group_name_; |
| std::string consumer_name_; |
| bool noack_ = false; |
| |
| Server *srv_ = nullptr; |
| Connection *conn_ = nullptr; |
| UniqueEvent timer_; |
| uint64_t count_ = 0; |
| int64_t block_timeout_ = 0; |
| int blocked_default_count_ = 1000; |
| bool with_count_ = false; |
| bool block_ = false; |
| |
| void unblockAll() { srv_->UnblockOnStreams(streams_, conn_); } |
| }; |
| |
| class CommandXTrim : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| bool modifier_found = false; |
| |
| auto trim_strategy = util::ToLower(args[2]); |
| if (trim_strategy == "maxlen") { |
| strategy_ = StreamTrimStrategy::MaxLen; |
| |
| size_t max_len_idx = 0; |
| if (args[3] != "=" && args[3] != "~") { |
| max_len_idx = 3; |
| } else { |
| max_len_idx = 4; |
| modifier_found = true; |
| } |
| |
| if (max_len_idx >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| auto parse_result = ParseInt<uint64_t>(args[max_len_idx], 10); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| max_len_ = *parse_result; |
| } else if (trim_strategy == "minid") { |
| strategy_ = StreamTrimStrategy::MinID; |
| |
| size_t min_id_idx = 0; |
| if (args[3] != "=" && args[3] != "~") { |
| min_id_idx = 3; |
| } else { |
| min_id_idx = 4; |
| modifier_found = true; |
| } |
| |
| if (min_id_idx >= args.size()) { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| auto s = ParseStreamEntryID(args[min_id_idx], &min_id_); |
| if (!s.IsOK()) { |
| return s; |
| } |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| |
| bool limit_option_found = false; |
| if (modifier_found) { |
| if (args.size() > 6 && util::ToLower(args[5]) == "limit") { |
| limit_option_found = true; |
| } |
| } else { |
| if (args.size() > 5 && util::ToLower(args[4]) == "limit") { |
| limit_option_found = true; |
| } |
| } |
| |
| if (limit_option_found) { |
| return {Status::RedisParseErr, errLimitOptionNotAllowed}; |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| StreamTrimOptions options; |
| options.strategy = strategy_; |
| options.max_len = max_len_; |
| options.min_id = min_id_; |
| |
| uint64_t removed = 0; |
| |
| auto s = stream_db.Trim(ctx, args_[1], options, &removed); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::Integer(removed); |
| |
| return Status::OK(); |
| } |
| |
| private: |
| uint64_t max_len_ = 0; |
| StreamEntryID min_id_; |
| StreamTrimStrategy strategy_ = StreamTrimStrategy::None; |
| }; |
| |
| class CommandXSetId : public Commander { |
| public: |
| Status Parse(const std::vector<std::string> &args) override { |
| stream_name_ = args[1]; |
| |
| auto s = redis::ParseStreamEntryID(args[2], &last_id_); |
| if (!s.IsOK()) return s; |
| |
| if (args.size() == 3) { |
| return Status::OK(); |
| } |
| |
| for (size_t i = 3; i < args.size(); /* manual increment */) { |
| if (util::EqualICase(args[i], "entriesadded") && i + 1 < args.size()) { |
| auto parse_result = ParseInt<uint64_t>(args[i + 1]); |
| if (!parse_result) { |
| return {Status::RedisParseErr, errValueNotInteger}; |
| } |
| |
| entries_added_ = *parse_result; |
| i += 2; |
| } else if (util::EqualICase(args[i], "maxdeletedid") && i + 1 < args.size()) { |
| StreamEntryID id; |
| s = redis::ParseStreamEntryID(args[i + 1], &id); |
| if (!s.IsOK()) return s; |
| |
| max_deleted_id_ = std::make_optional<StreamEntryID>(id.ms, id.seq); |
| i += 2; |
| } else { |
| return {Status::RedisParseErr, errInvalidSyntax}; |
| } |
| } |
| |
| return Status::OK(); |
| } |
| |
| Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override { |
| redis::Stream stream_db(srv->storage, conn->GetNamespace()); |
| |
| auto s = stream_db.SetId(ctx, stream_name_, last_id_, entries_added_, max_deleted_id_); |
| if (!s.ok()) { |
| return {Status::RedisExecErr, s.ToString()}; |
| } |
| |
| *output = redis::RESP_OK; |
| |
| return Status::OK(); |
| } |
| |
| private: |
| std::string stream_name_; |
| StreamEntryID last_id_; |
| std::optional<StreamEntryID> max_deleted_id_; |
| std::optional<uint64_t> entries_added_; |
| }; |
| |
| REDIS_REGISTER_COMMANDS(Stream, MakeCmdAttr<CommandXAck>("xack", -4, "write no-dbsize-check", 1, 1, 1), |
| MakeCmdAttr<CommandXAdd>("xadd", -5, "write", 1, 1, 1), |
| MakeCmdAttr<CommandXDel>("xdel", -3, "write no-dbsize-check", 1, 1, 1), |
| MakeCmdAttr<CommandXClaim>("xclaim", -6, "write", 1, 1, 1), |
| MakeCmdAttr<CommandAutoClaim>("xautoclaim", -6, "write", 1, 1, 1), |
| MakeCmdAttr<CommandXGroup>("xgroup", -4, "write", 2, 2, 1), |
| MakeCmdAttr<CommandXLen>("xlen", -2, "read-only", 1, 1, 1), |
| MakeCmdAttr<CommandXInfo>("xinfo", -2, "read-only", NO_KEY), |
| MakeCmdAttr<CommandXPending>("xpending", -3, "read-only", 1, 1, 1), |
| MakeCmdAttr<CommandXRange>("xrange", -4, "read-only", 1, 1, 1), |
| MakeCmdAttr<CommandXRevRange>("xrevrange", -4, "read-only", 1, 1, 1), |
| MakeCmdAttr<CommandXRead>("xread", -4, "read-only blocking", CommandXRead::keyRangeGen), |
| MakeCmdAttr<CommandXReadGroup>("xreadgroup", -7, "write blocking", |
| CommandXReadGroup::keyRangeGen), |
| MakeCmdAttr<CommandXTrim>("xtrim", -4, "write no-dbsize-check", 1, 1, 1), |
| MakeCmdAttr<CommandXSetId>("xsetid", -3, "write", 1, 1, 1)) |
| |
| } // namespace redis |