diff --git a/CODEOWNERS b/CODEOWNERS index 37e376e77e79..4a7d6aa7cafe 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -16,6 +16,8 @@ extensions/filters/common/original_src @snowp @klarose # dubbo_proxy extension /*/extensions/filters/network/dubbo_proxy @zyfjeff @lizan +# rocketmq_proxy extension +/*/extensions/filters/network/rocketmq_proxy @aaron-ai @lizhanhui @lizan # thrift_proxy extension /*/extensions/filters/network/thrift_proxy @zuercher @brian-pane # compressor used by http compression filters diff --git a/api/BUILD b/api/BUILD index 97a8554bc520..d52653ebc4e6 100644 --- a/api/BUILD +++ b/api/BUILD @@ -214,6 +214,7 @@ proto_library( "//envoy/extensions/filters/network/ratelimit/v3:pkg", "//envoy/extensions/filters/network/rbac/v3:pkg", "//envoy/extensions/filters/network/redis_proxy/v3:pkg", + "//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3alpha:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", diff --git a/api/envoy/extensions/filters/network/rocketmq_proxy/v3/BUILD b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/BUILD new file mode 100644 index 000000000000..e6bc5699efc4 --- /dev/null +++ b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "//envoy/config/route/v3:pkg", + "//envoy/type/matcher/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/rocketmq_proxy/v3/README.md b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/README.md new file mode 100644 index 000000000000..3bd849bc2530 --- /dev/null +++ b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/README.md @@ -0,0 +1 @@ +Protocol buffer definitions for the Rocketmq proxy. \ No newline at end of file diff --git a/api/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.proto b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.proto new file mode 100644 index 000000000000..ee77ab909592 --- /dev/null +++ b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.rocketmq_proxy.v3; + +import "envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto"; + +import "google/protobuf/any.proto"; +import "google/protobuf/duration.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.rocketmq_proxy.v3"; +option java_outer_classname = "RocketmqProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: RocketMQ Proxy] +// RocketMQ Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.rocketmq_proxy] + +message RocketmqProxy { + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // The route table for the connection manager is specified in this property. + RouteConfiguration route_config = 2; + + // The largest duration transient object expected to live, more than 10s is recommended. + google.protobuf.Duration transient_object_life_span = 3; + + // If develop_mode is enabled, this proxy plugin may work without dedicated traffic intercepting + // facility without considering backward compatibility of exiting RocketMQ client SDK. + bool develop_mode = 4; +} diff --git a/api/envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto new file mode 100644 index 000000000000..5fe5d33ffacf --- /dev/null +++ b/api/envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto @@ -0,0 +1,55 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.rocketmq_proxy.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/config/route/v3/route_components.proto"; +import "envoy/type/matcher/v3/string.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.rocketmq_proxy.v3"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Rocketmq Proxy Route Configuration] +// Rocketmq Proxy :ref:`configuration overview `. + +message RouteConfiguration { + // The name of the route configuration. + string name = 1; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 2; +} + +message Route { + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + // The name of the topic. + type.matcher.v3.StringMatcher topic = 1 [(validate.rules).message = {required: true}]; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated config.route.v3.HeaderMatcher headers = 2; +} + +message RouteAction { + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Optional endpoint metadata match criteria used by the subset load balancer. + config.core.v3.Metadata metadata_match = 2; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index bbb683d8bd08..f1a0d2440e14 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -96,6 +96,7 @@ proto_library( "//envoy/extensions/filters/network/ratelimit/v3:pkg", "//envoy/extensions/filters/network/rbac/v3:pkg", "//envoy/extensions/filters/network/redis_proxy/v3:pkg", + "//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3alpha:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index 65511250f84b..4c29a385acad 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -23,6 +23,7 @@ filters. rate_limit_filter rbac_filter redis_proxy_filter + rocketmq_proxy_filter tcp_proxy_filter thrift_proxy_filter sni_cluster_filter diff --git a/docs/root/configuration/listeners/network_filters/rocketmq_proxy_filter.rst b/docs/root/configuration/listeners/network_filters/rocketmq_proxy_filter.rst new file mode 100644 index 000000000000..50033efc899c --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/rocketmq_proxy_filter.rst @@ -0,0 +1,76 @@ +.. _config_network_filters_rocketmq_proxy: + +RocketMQ proxy +============== + +Apache RocketMQ is a distributed messaging system, which is composed of four types of roles: producer, consumer, name +server and broker server. The former two are embedded into user application in form of SDK; whilst the latter are +standalone servers. + +A message in RocketMQ carries a topic as its destination and optionally one or more tags as application specific labels. + +Producers are used to send messages to brokers according to their topics. Similar to many distributed systems, +producers need to know how to connect to these serving brokers. To achieve this goal, RocketMQ provides name server +clusters for producers to lookup. Namely, when producers attempts to send messages with a new topic, it first +tries to lookup the addresses(called route info) of brokers that serve the topic from name servers. Once producers +get the route info of the topic, they actively cache them in memory and renew them periodically thereafter. This +mechanism, though simple, effectively keeps service availability high without demanding availability of name server +service. + +Brokers provides messaging service to end users. In addition to various messaging services, they also periodically +report health status and route info of topics currently served to name servers. + +Major role of the name server is to serve querying of route info for a topic. Additionally, it also purges route info +entries once the belonging brokers fail to report their health info for a configured period of time. This ensures +clients almost always connect to brokers that are online and ready to serve. + +Consumers are used by application to pull message from brokers. They perform similar heartbeats to maintain alive +status. RocketMQ brokers support two message-fetch approaches: long-pulling and pop. + +Using the first approach, consumers have to implement load-balancing algorithm. The pop approach, in the perspective of +consumers, is stateless. + +Envoy RocketMQ filter proxies requests and responses between producers/consumer and brokers. Various statistical items +are collected to enhance observability. + +At present, pop-based message fetching is implemented. Long-pulling will be implemented in the next pull request. + +.. _config_network_filters_rocketmq_proxy_stats: + +Statistics +---------- + +Every configured rocketmq proxy filter has statistics rooted at *rocketmq..* with the +following statistics: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + request, Counter, Total requests + request_decoding_error, Counter, Total decoding error requests + request_decoding_success, Counter, Total decoding success requests + response, Counter, Total responses + response_decoding_error, Counter, Total decoding error responses + response_decoding_success, Counter, Total decoding success responses + response_error, Counter, Total error responses + response_success, Counter, Total success responses + heartbeat, Counter, Total heartbeat requests + unregister, Counter, Total unregister requests + get_topic_route, Counter, Total getting topic route requests + send_message_v1, Counter, Total sending message v1 requests + send_message_v2, Counter, Total sending message v2 requests + pop_message, Counter, Total poping message requests + ack_message, Counter, Total acking message requests + get_consumer_list, Counter, Total getting consumer list requests + maintenance_failure, Counter, Total maintenance failure + request_active, Gauge, Total active requests + send_message_v1_active, Gauge, Total active sending message v1 requests + send_message_v2_active, Gauge, Total active sending message v2 requests + pop_message_active, Gauge, Total active poping message active requests + get_topic_route_active, Gauge, Total active geting topic route requests + send_message_pending, Gauge, Total pending sending message requests + pop_message_pending, Gauge, Total pending poping message requests + get_topic_route_pending, Gauge, Total pending geting topic route requests + total_pending, Gauge, Total pending requests + request_time_ms, Histogram, Request time in milliseconds \ No newline at end of file diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 7dafeef3d4a2..f587abbb4aaa 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -19,6 +19,7 @@ Changes `envoy.reloadable_features.new_http2_connection_pool_behavior`. * logger: added :ref:`--log-format-prefix-with-location ` command line option to prefix '%v' with file path and line number. * network filters: added a :ref:`postgres proxy filter `. +* network filters: added a :ref:`rocketmq proxy filter `. * request_id: added to :ref:`always_set_request_id_in_response setting ` to set :ref:`x-request-id ` header in response even if tracing is not forced. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index 6aafa3e75588..15ac05d10ced 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -77,6 +77,7 @@ proto_library( "//envoy/config/filter/network/rate_limit/v2:pkg", "//envoy/config/filter/network/rbac/v2:pkg", "//envoy/config/filter/network/redis_proxy/v2:pkg", + "//envoy/config/filter/network/rocketmq_proxy/v3:pkg", "//envoy/config/filter/network/sni_cluster/v2:pkg", "//envoy/config/filter/network/tcp_proxy/v2:pkg", "//envoy/config/filter/network/thrift_proxy/v2alpha1:pkg", diff --git a/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/BUILD new file mode 100644 index 000000000000..e6bc5699efc4 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/core/v3:pkg", + "//envoy/config/route/v3:pkg", + "//envoy/type/matcher/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.proto b/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.proto new file mode 100644 index 000000000000..ee77ab909592 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.rocketmq_proxy.v3; + +import "envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto"; + +import "google/protobuf/any.proto"; +import "google/protobuf/duration.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.rocketmq_proxy.v3"; +option java_outer_classname = "RocketmqProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: RocketMQ Proxy] +// RocketMQ Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.rocketmq_proxy] + +message RocketmqProxy { + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // The route table for the connection manager is specified in this property. + RouteConfiguration route_config = 2; + + // The largest duration transient object expected to live, more than 10s is recommended. + google.protobuf.Duration transient_object_life_span = 3; + + // If develop_mode is enabled, this proxy plugin may work without dedicated traffic intercepting + // facility without considering backward compatibility of exiting RocketMQ client SDK. + bool develop_mode = 4; +} diff --git a/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto b/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto new file mode 100644 index 000000000000..5fe5d33ffacf --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/rocketmq_proxy/v3/route.proto @@ -0,0 +1,55 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.rocketmq_proxy.v3; + +import "envoy/config/core/v3/base.proto"; +import "envoy/config/route/v3/route_components.proto"; +import "envoy/type/matcher/v3/string.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.rocketmq_proxy.v3"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Rocketmq Proxy Route Configuration] +// Rocketmq Proxy :ref:`configuration overview `. + +message RouteConfiguration { + // The name of the route configuration. + string name = 1; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 2; +} + +message Route { + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + // The name of the topic. + type.matcher.v3.StringMatcher topic = 1 [(validate.rules).message = {required: true}]; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated config.route.v3.HeaderMatcher headers = 2; +} + +message RouteAction { + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Optional endpoint metadata match criteria used by the subset load balancer. + config.core.v3.Metadata metadata_match = 2; +} diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 30b44628076d..384564d7c620 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -34,6 +34,7 @@ namespace Logger { FUNCTION(conn_handler) \ FUNCTION(decompression) \ FUNCTION(dubbo) \ + FUNCTION(rocketmq) \ FUNCTION(file) \ FUNCTION(filter) \ FUNCTION(forward_proxy) \ diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 1bb72dfb7e1d..49f603e2697c 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -96,6 +96,7 @@ EXTENSIONS = { "envoy.filters.network.ratelimit": "//source/extensions/filters/network/ratelimit:config", "envoy.filters.network.rbac": "//source/extensions/filters/network/rbac:config", "envoy.filters.network.redis_proxy": "//source/extensions/filters/network/redis_proxy:config", + "envoy.filters.network.rocketmq_proxy": "//source/extensions/filters/network/rocketmq_proxy:config", "envoy.filters.network.tcp_proxy": "//source/extensions/filters/network/tcp_proxy:config", "envoy.filters.network.thrift_proxy": "//source/extensions/filters/network/thrift_proxy:config", "envoy.filters.network.sni_cluster": "//source/extensions/filters/network/sni_cluster:config", diff --git a/source/extensions/filters/network/rocketmq_proxy/BUILD b/source/extensions/filters/network/rocketmq_proxy/BUILD new file mode 100644 index 000000000000..65c4f18be827 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/BUILD @@ -0,0 +1,148 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "well_known_names", + hdrs = ["well_known_names.h"], + deps = ["//source/common/singleton:const_singleton"], +) + +envoy_cc_library( + name = "stats_interface", + hdrs = ["stats.h"], + deps = [ + "//include/envoy/stats:stats_interface", + "//include/envoy/stats:stats_macros", + ], +) + +envoy_cc_library( + name = "rocketmq_interface", + hdrs = [ + "topic_route.h", + ], + deps = [ + "//source/common/protobuf:utility_lib", + ], +) + +envoy_cc_library( + name = "rocketmq_lib", + srcs = [ + "topic_route.cc", + ], + deps = [ + ":rocketmq_interface", + ], +) + +envoy_cc_library( + name = "protocol_interface", + hdrs = ["protocol.h"], + deps = [ + ":metadata_lib", + "//source/common/buffer:buffer_lib", + "//source/common/protobuf:utility_lib", + ], +) + +envoy_cc_library( + name = "protocol_lib", + srcs = ["protocol.cc"], + deps = [ + ":protocol_interface", + ":well_known_names", + "//source/common/common:enum_to_int", + ], +) + +envoy_cc_library( + name = "codec_lib", + srcs = [ + "codec.cc", + ], + hdrs = [ + "codec.h", + ], + deps = [ + ":protocol_lib", + "//include/envoy/network:filter_interface", + "//source/common/protobuf:utility_lib", + ], +) + +envoy_cc_library( + name = "conn_manager_lib", + srcs = [ + "active_message.cc", + "conn_manager.cc", + ], + hdrs = [ + "active_message.h", + "conn_manager.h", + ], + deps = [ + ":codec_lib", + ":protocol_lib", + ":rocketmq_lib", + ":stats_interface", + ":well_known_names", + "//include/envoy/buffer:buffer_interface", + "//include/envoy/event:dispatcher_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/tcp:conn_pool_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:assert_lib", + "//source/common/common:empty_string", + "//source/common/common:enum_to_int", + "//source/common/common:linked_object", + "//source/common/protobuf:utility_lib", + "//source/common/stats:timespan_lib", + "//source/common/upstream:load_balancer_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/rocketmq_proxy/router:router_interface", + "@envoy_api//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = [ + "config.cc", + ], + hdrs = [ + "config.h", + ], + security_posture = "requires_trusted_downstream_and_upstream", + status = "alpha", + deps = [ + ":conn_manager_lib", + "//include/envoy/registry", + "//include/envoy/server:filter_config_interface", + "//source/common/common:logger_lib", + "//source/common/common:minimal_logger_lib", + "//source/common/config:utility_lib", + "//source/extensions/filters/network/common:factory_base_lib", + "//source/extensions/filters/network/rocketmq_proxy/router:route_matcher", + "//source/extensions/filters/network/rocketmq_proxy/router:router_lib", + "@envoy_api//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "metadata_lib", + hdrs = ["metadata.h"], + external_deps = ["abseil_optional"], + deps = [ + "//source/common/http:header_map_lib", + ], +) diff --git a/source/extensions/filters/network/rocketmq_proxy/active_message.cc b/source/extensions/filters/network/rocketmq_proxy/active_message.cc new file mode 100644 index 000000000000..c9e3bd14c2c3 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/active_message.cc @@ -0,0 +1,333 @@ +#include "extensions/filters/network/rocketmq_proxy/active_message.h" + +#include "envoy/upstream/cluster_manager.h" + +#include "common/common/empty_string.h" +#include "common/common/enum_to_int.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/topic_route.h" +#include "extensions/filters/network/rocketmq_proxy/well_known_names.h" +#include "extensions/filters/network/well_known_names.h" + +#include "absl/strings/match.h" + +using Envoy::Tcp::ConnectionPool::ConnectionDataPtr; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +ActiveMessage::ActiveMessage(ConnectionManager& conn_manager, RemotingCommandPtr&& request) + : connection_manager_(conn_manager), request_(std::move(request)) { + metadata_ = std::make_shared(); + MetadataHelper::parseRequest(request_, metadata_); + updateActiveRequestStats(); +} + +ActiveMessage::~ActiveMessage() { updateActiveRequestStats(false); } + +void ActiveMessage::createFilterChain() { router_ = connection_manager_.config().createRouter(); } + +void ActiveMessage::sendRequestToUpstream() { + if (!router_) { + createFilterChain(); + } + router_->sendRequestToUpstream(*this); +} + +Router::RouteConstSharedPtr ActiveMessage::route() { + if (cached_route_) { + return cached_route_.value(); + } + const std::string& topic_name = metadata_->topicName(); + ENVOY_LOG(trace, "fetch route for topic: {}", topic_name); + Router::RouteConstSharedPtr route = connection_manager_.config().routerConfig().route(*metadata_); + cached_route_ = route; + return cached_route_.value(); +} + +void ActiveMessage::onError(absl::string_view error_message) { + connection_manager_.onError(request_, error_message); +} + +const RemotingCommandPtr& ActiveMessage::downstreamRequest() const { return request_; } + +void ActiveMessage::fillAckMessageDirective(Buffer::Instance& buffer, const std::string& group, + const std::string& topic, + const AckMessageDirective& directive) { + int32_t cursor = 0; + const int32_t buffer_length = buffer.length(); + while (cursor < buffer_length) { + auto frame_length = buffer.peekBEInt(cursor); + std::string decoded_topic = Decoder::decodeTopic(buffer, cursor); + ENVOY_LOG(trace, "Process a message: consumer group: {}, topic: {}, messageId: {}", + decoded_topic, group, Decoder::decodeMsgId(buffer, cursor)); + if (!absl::StartsWith(decoded_topic, RetryTopicPrefix) && decoded_topic != topic) { + ENVOY_LOG(warn, + "Decoded topic from pop-response does not equal to request. Decoded topic: " + "{}, request topic: {}, message ID: {}", + decoded_topic, topic, Decoder::decodeMsgId(buffer, cursor)); + } + + /* + * Sometimes, client SDK may used -1 for queue-id in the pop request so that broker servers + * are allowed to lookup all queues it serves. So we need to use the actual queue Id from + * response body. + */ + int32_t queue_id = Decoder::decodeQueueId(buffer, cursor); + int64_t queue_offset = Decoder::decodeQueueOffset(buffer, cursor); + + std::string key = fmt::format("{}-{}-{}-{}", group, decoded_topic, queue_id, queue_offset); + connection_manager_.insertAckDirective(key, directive); + ENVOY_LOG( + debug, + "Insert an ack directive. Consumer group: {}, topic: {}, queue Id: {}, queue offset: {}", + group, topic, queue_id, queue_offset); + cursor += frame_length; + } +} + +void ActiveMessage::sendResponseToDownstream() { + if (request_->code() == enumToSignedInt(RequestCode::PopMessage)) { + // Fill ack message directive + auto pop_header = request_->typedCustomHeader(); + AckMessageDirective directive(pop_header->targetBrokerName(), pop_header->targetBrokerId(), + connection_manager_.timeSource().monotonicTime()); + ENVOY_LOG(trace, "Receive pop response from broker name: {}, broker ID: {}", + pop_header->targetBrokerName(), pop_header->targetBrokerId()); + fillAckMessageDirective(response_->body(), pop_header->consumerGroup(), pop_header->topic(), + directive); + } + + // If acknowledgment of the message is successful, we need to erase the ack directive from + // manager. + if (request_->code() == enumToSignedInt(RequestCode::AckMessage) && + response_->code() == enumToSignedInt(ResponseCode::Success)) { + auto ack_header = request_->typedCustomHeader(); + connection_manager_.eraseAckDirective(ack_header->directiveKey()); + } + + if (response_) { + response_->opaque(request_->opaque()); + connection_manager_.sendResponseToDownstream(response_); + } +} + +void ActiveMessage::fillBrokerData(std::vector& list, const std::string& cluster, + const std::string& broker_name, int64_t broker_id, + const std::string& address) { + bool found = false; + for (auto& entry : list) { + if (entry.cluster() == cluster && entry.brokerName() == broker_name) { + found = true; + if (entry.brokerAddresses().find(broker_id) != entry.brokerAddresses().end()) { + ENVOY_LOG(warn, "Duplicate broker_id found. Broker ID: {}, address: {}", broker_id, + address); + continue; + } else { + entry.brokerAddresses()[broker_id] = address; + } + } + } + + if (!found) { + std::unordered_map addresses; + addresses.emplace(broker_id, address); + + list.emplace_back(BrokerData(cluster, broker_name, std::move(addresses))); + } +} + +void ActiveMessage::onQueryTopicRoute() { + std::string cluster_name; + ASSERT(metadata_->hasTopicName()); + const std::string& topic_name = metadata_->topicName(); + Upstream::ThreadLocalCluster* cluster = nullptr; + Router::RouteConstSharedPtr route = this->route(); + if (route) { + cluster_name = route->routeEntry()->clusterName(); + Upstream::ClusterManager& cluster_manager = connection_manager_.config().clusterManager(); + cluster = cluster_manager.get(cluster_name); + } + if (cluster) { + ENVOY_LOG(trace, "Enovy has an operating cluster {} for topic {}", cluster_name, topic_name); + std::vector queue_data_list; + std::vector broker_data_list; + for (auto& host_set : cluster->prioritySet().hostSetsPerPriority()) { + if (host_set->hosts().empty()) { + continue; + } + for (const auto& host : host_set->hosts()) { + std::string broker_address = host->address()->asString(); + auto& filter_metadata = host->metadata()->filter_metadata(); + const auto filter_it = filter_metadata.find(NetworkFilterNames::get().RocketmqProxy); + ASSERT(filter_it != filter_metadata.end()); + const auto& metadata_fields = filter_it->second.fields(); + ASSERT(metadata_fields.contains(RocketmqConstants::get().BrokerName)); + std::string broker_name = + metadata_fields.at(RocketmqConstants::get().BrokerName).string_value(); + ASSERT(metadata_fields.contains(RocketmqConstants::get().ClusterName)); + std::string broker_cluster_name = + metadata_fields.at(RocketmqConstants::get().ClusterName).string_value(); + // Proto3 will ignore the field if the value is zero. + int32_t read_queue_num = 0; + if (metadata_fields.contains(RocketmqConstants::get().ReadQueueNum)) { + read_queue_num = static_cast( + metadata_fields.at(RocketmqConstants::get().WriteQueueNum).number_value()); + } + int32_t write_queue_num = 0; + if (metadata_fields.contains(RocketmqConstants::get().WriteQueueNum)) { + write_queue_num = static_cast( + metadata_fields.at(RocketmqConstants::get().WriteQueueNum).number_value()); + } + int32_t perm = 0; + if (metadata_fields.contains(RocketmqConstants::get().Perm)) { + perm = static_cast( + metadata_fields.at(RocketmqConstants::get().Perm).number_value()); + } + int32_t broker_id = 0; + if (metadata_fields.contains(RocketmqConstants::get().BrokerId)) { + broker_id = static_cast( + metadata_fields.at(RocketmqConstants::get().BrokerId).number_value()); + } + queue_data_list.emplace_back(QueueData(broker_name, read_queue_num, write_queue_num, perm)); + if (connection_manager_.config().developMode()) { + ENVOY_LOG(trace, "Develop mode, return proxy address to replace all broker addresses so " + "that L4 network rewrite is not required"); + fillBrokerData(broker_data_list, broker_cluster_name, broker_name, broker_id, + connection_manager_.config().proxyAddress()); + } else { + fillBrokerData(broker_data_list, broker_cluster_name, broker_name, broker_id, + broker_address); + } + } + } + ENVOY_LOG(trace, "Prepare TopicRouteData for {} OK", topic_name); + TopicRouteData topic_route_data(std::move(queue_data_list), std::move(broker_data_list)); + ProtobufWkt::Struct data_struct; + topic_route_data.encode(data_struct); + std::string json = MessageUtil::getJsonStringFromMessage(data_struct); + ENVOY_LOG(trace, "Serialize TopicRouteData for {} OK:\n{}", cluster_name, json); + RemotingCommandPtr response = std::make_unique( + static_cast(ResponseCode::Success), downstreamRequest()->version(), + downstreamRequest()->opaque()); + response->markAsResponse(); + response->body().add(json); + connection_manager_.sendResponseToDownstream(response); + } else { + onError("Cluster is not available"); + ENVOY_LOG(warn, "Cluster for topic {} is not available", topic_name); + } + onReset(); +} + +void ActiveMessage::onReset() { connection_manager_.deferredDelete(*this); } + +bool ActiveMessage::onUpstreamData(Envoy::Buffer::Instance& data, bool end_stream, + ConnectionDataPtr& conn_data) { + bool underflow = false; + bool has_error = false; + response_ = Decoder::decode(data, underflow, has_error, downstreamRequest()->code()); + if (underflow && !end_stream) { + ENVOY_LOG(trace, "Wait for more data from upstream"); + return false; + } + + if (enumToSignedInt(RequestCode::PopMessage) == request_->code() && router_ != nullptr) { + recordPopRouteInfo(router_->upstreamHost()); + } + + connection_manager_.stats().response_.inc(); + if (!has_error) { + connection_manager_.stats().response_decoding_success_.inc(); + // Relay response to downstream + sendResponseToDownstream(); + } else { + ENVOY_LOG(error, "Failed to decode response for opaque: {}, close immediately.", + downstreamRequest()->opaque()); + onError("Failed to decode response from upstream"); + connection_manager_.stats().response_decoding_error_.inc(); + conn_data->connection().close(Network::ConnectionCloseType::NoFlush); + } + + if (end_stream) { + conn_data->connection().close(Network::ConnectionCloseType::NoFlush); + } + return true; +} + +void ActiveMessage::recordPopRouteInfo(Upstream::HostDescriptionConstSharedPtr host_description) { + if (host_description) { + auto host_metadata = host_description->metadata(); + auto filter_metadata = host_metadata->filter_metadata(); + const auto filter_it = filter_metadata.find(NetworkFilterNames::get().RocketmqProxy); + ASSERT(filter_it != filter_metadata.end()); + const auto& metadata_fields = filter_it->second.fields(); + ASSERT(metadata_fields.contains(RocketmqConstants::get().BrokerName)); + std::string broker_name = + metadata_fields.at(RocketmqConstants::get().BrokerName).string_value(); + // Proto3 will ignore the field if the value is zero. + int32_t broker_id = 0; + if (metadata_fields.contains(RocketmqConstants::get().BrokerId)) { + broker_id = static_cast( + metadata_fields.at(RocketmqConstants::get().BrokerId).number_value()); + } + // Tag the request with upstream host metadata: broker-name, broker-id + auto custom_header = request_->typedCustomHeader(); + custom_header->targetBrokerName(broker_name); + custom_header->targetBrokerId(broker_id); + } +} + +void ActiveMessage::updateActiveRequestStats(bool is_inc) { + if (is_inc) { + connection_manager_.stats().request_active_.inc(); + } else { + connection_manager_.stats().request_active_.dec(); + } + auto code = static_cast(request_->code()); + switch (code) { + case RequestCode::PopMessage: { + if (is_inc) { + connection_manager_.stats().pop_message_active_.inc(); + } else { + connection_manager_.stats().pop_message_active_.dec(); + } + break; + } + case RequestCode::SendMessage: { + if (is_inc) { + connection_manager_.stats().send_message_v1_active_.inc(); + } else { + connection_manager_.stats().send_message_v1_active_.dec(); + } + break; + } + case RequestCode::SendMessageV2: { + if (is_inc) { + connection_manager_.stats().send_message_v2_active_.inc(); + } else { + connection_manager_.stats().send_message_v2_active_.dec(); + } + break; + } + case RequestCode::GetRouteInfoByTopic: { + if (is_inc) { + connection_manager_.stats().get_topic_route_active_.inc(); + } else { + connection_manager_.stats().get_topic_route_active_.dec(); + } + break; + } + default: + break; + } +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/rocketmq_proxy/active_message.h b/source/extensions/filters/network/rocketmq_proxy/active_message.h new file mode 100644 index 000000000000..566907d40dd6 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/active_message.h @@ -0,0 +1,105 @@ +#pragma once + +#include "envoy/event/deferred_deletable.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/stats/timespan.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/linked_object.h" +#include "common/common/logger.h" + +#include "extensions/filters/network/rocketmq_proxy/codec.h" +#include "extensions/filters/network/rocketmq_proxy/protocol.h" +#include "extensions/filters/network/rocketmq_proxy/router/router.h" +#include "extensions/filters/network/rocketmq_proxy/topic_route.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class ConnectionManager; + +/** + * ActiveMessage represents an in-flight request from downstream that has not yet received response + * from upstream. + */ +class ActiveMessage : public LinkedObject, + public Event::DeferredDeletable, + Logger::Loggable { +public: + ActiveMessage(ConnectionManager& conn_manager, RemotingCommandPtr&& request); + + ~ActiveMessage() override; + + /** + * Set up filter-chain according to configuration from bootstrap config file and dynamic + * configuration items from Pilot. + */ + void createFilterChain(); + + /** + * Relay requests from downstream to upstream cluster. If the target cluster is absent at the + * moment, it triggers cluster discovery service request and mark awaitCluster as true. + * ClusterUpdateCallback will process requests marked await-cluster once the target cluster is + * in place. + */ + void sendRequestToUpstream(); + + const RemotingCommandPtr& downstreamRequest() const; + + /** + * Parse pop response and insert ack route directive such that ack requests will be forwarded to + * the same broker host from which messages are popped. + * @param buffer Pop response body. + * @param group Consumer group name. + * @param topic Topic from which messages are popped + * @param directive ack route directive + */ + virtual void fillAckMessageDirective(Buffer::Instance& buffer, const std::string& group, + const std::string& topic, + const AckMessageDirective& directive); + + virtual void sendResponseToDownstream(); + + void onQueryTopicRoute(); + + virtual void onError(absl::string_view error_message); + + ConnectionManager& connectionManager() { return connection_manager_; } + + virtual void onReset(); + + bool onUpstreamData(Buffer::Instance& data, bool end_stream, + Tcp::ConnectionPool::ConnectionDataPtr& conn_data); + + virtual MessageMetadataSharedPtr metadata() const { return metadata_; } + + virtual Router::RouteConstSharedPtr route(); + + void recordPopRouteInfo(Upstream::HostDescriptionConstSharedPtr host_description); + + static void fillBrokerData(std::vector& list, const std::string& cluster, + const std::string& broker_name, int64_t broker_id, + const std::string& address); + +private: + ConnectionManager& connection_manager_; + RemotingCommandPtr request_; + RemotingCommandPtr response_; + MessageMetadataSharedPtr metadata_; + Router::RouterPtr router_; + absl::optional cached_route_; + + void updateActiveRequestStats(bool is_inc = true); +}; + +using ActiveMessagePtr = std::unique_ptr; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/codec.cc b/source/extensions/filters/network/rocketmq_proxy/codec.cc new file mode 100644 index 000000000000..628fc302f99d --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/codec.cc @@ -0,0 +1,408 @@ +#include "extensions/filters/network/rocketmq_proxy/codec.h" + +#include + +#include "common/common/assert.h" +#include "common/common/empty_string.h" +#include "common/common/enum_to_int.h" +#include "common/common/logger.h" + +#include "extensions/filters/network/rocketmq_proxy/protocol.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +RemotingCommandPtr Decoder::decode(Buffer::Instance& buffer, bool& underflow, bool& has_error, + int request_code) { + // Verify there is at least some bits, which stores frame length and header length + if (buffer.length() <= MIN_FRAME_SIZE) { + underflow = true; + return nullptr; + } + + auto frame_length = buffer.peekBEInt(); + + if (frame_length > MAX_FRAME_SIZE) { + has_error = true; + return nullptr; + } + + if (buffer.length() < frame_length) { + underflow = true; + return nullptr; + } + buffer.drain(FRAME_LENGTH_FIELD_SIZE); + + auto mark = buffer.peekBEInt(); + uint32_t header_length = adjustHeaderLength(mark); + ASSERT(frame_length > header_length); + buffer.drain(FRAME_HEADER_LENGTH_FIELD_SIZE); + + uint32_t body_length = frame_length - 4 - header_length; + + ENVOY_LOG(debug, + "Request/Response Frame Meta: Frame Length = {}, Header Length = {}, Body Length = {}", + frame_length, header_length, body_length); + + Buffer::OwnedImpl header_buffer; + header_buffer.move(buffer, header_length); + std::string header_json = header_buffer.toString(); + ENVOY_LOG(trace, "Request/Response Header JSON: {}", header_json); + + int32_t code, version, opaque; + uint32_t flag; + if (isJsonHeader(mark)) { + ProtobufWkt::Struct header_struct; + + // Parse header JSON text + try { + MessageUtil::loadFromJson(header_json, header_struct); + } catch (std::exception& e) { + has_error = true; + ENVOY_LOG(error, "Failed to parse header JSON: {}. Error message: {}", header_json, e.what()); + return nullptr; + } + + const auto& filed_value_pair = header_struct.fields(); + if (!filed_value_pair.contains("code")) { + ENVOY_LOG(error, "Malformed frame: 'code' field is missing. Header JSON: {}", header_json); + has_error = true; + return nullptr; + } + code = filed_value_pair.at("code").number_value(); + if (!filed_value_pair.contains("version")) { + ENVOY_LOG(error, "Malformed frame: 'version' field is missing. Header JSON: {}", header_json); + has_error = true; + return nullptr; + } + version = filed_value_pair.at("version").number_value(); + if (!filed_value_pair.contains("opaque")) { + ENVOY_LOG(error, "Malformed frame: 'opaque' field is missing. Header JSON: {}", header_json); + has_error = true; + return nullptr; + } + opaque = filed_value_pair.at("opaque").number_value(); + if (!filed_value_pair.contains("flag")) { + ENVOY_LOG(error, "Malformed frame: 'flag' field is missing. Header JSON: {}", header_json); + has_error = true; + return nullptr; + } + flag = filed_value_pair.at("flag").number_value(); + RemotingCommandPtr cmd = std::make_unique(code, version, opaque); + cmd->flag(flag); + if (filed_value_pair.contains("language")) { + cmd->language(filed_value_pair.at("language").string_value()); + } + + if (filed_value_pair.contains("serializeTypeCurrentRPC")) { + cmd->serializeTypeCurrentRPC(filed_value_pair.at("serializeTypeCurrentRPC").string_value()); + } + + cmd->body_.move(buffer, body_length); + + if (RemotingCommand::isResponse(flag)) { + if (filed_value_pair.contains("remark")) { + cmd->remark(filed_value_pair.at("remark").string_value()); + } + cmd->custom_header_ = decodeResponseExtHeader(static_cast(code), header_struct, + static_cast(request_code)); + } else { + cmd->custom_header_ = decodeExtHeader(static_cast(code), header_struct); + } + return cmd; + } else { + ENVOY_LOG(warn, "Unsupported header serialization type"); + has_error = true; + return nullptr; + } +} + +bool Decoder::isComplete(Buffer::Instance& buffer, int32_t cursor) { + if (buffer.length() - cursor < 4) { + // buffer is definitely incomplete. + return false; + } + + auto total_size = buffer.peekBEInt(cursor); + return buffer.length() - cursor >= static_cast(total_size); +} + +std::string Decoder::decodeTopic(Buffer::Instance& buffer, int32_t cursor) { + if (!isComplete(buffer, cursor)) { + return EMPTY_STRING; + } + + auto magic_code = buffer.peekBEInt(cursor + 4); + + MessageVersion message_version = V1; + if (enumToSignedInt(MessageVersion::V1) == magic_code) { + message_version = V1; + } else if (enumToSignedInt(MessageVersion::V2) == magic_code) { + message_version = V2; + } + + int32_t offset = 4 /* total size */ + + 4 /* magic code */ + + 4 /* body CRC */ + + 4 /* queue Id */ + + 4 /* flag */ + + 8 /* queue offset */ + + 8 /* physical offset */ + + 4 /* sys flag */ + + 8 /* born timestamp */ + + 4 /* born host */ + + 4 /* born host port */ + + 8 /* store timestamp */ + + 4 /* store host */ + + 4 /* store host port */ + + 4 /* re-consume times */ + + 8 /* transaction offset */ + ; + auto body_size = buffer.peekBEInt(cursor + offset); + offset += 4 /* body size */ + + body_size /* body */; + int32_t topic_length; + std::string topic; + switch (message_version) { + case V1: { + topic_length = buffer.peekBEInt(cursor + offset); + topic.reserve(topic_length); + topic.resize(topic_length); + buffer.copyOut(cursor + offset + sizeof(int8_t), topic_length, &topic[0]); + break; + } + case V2: { + topic_length = buffer.peekBEInt(cursor + offset); + topic.reserve(topic_length); + topic.resize(topic_length); + buffer.copyOut(cursor + offset + sizeof(int16_t), topic_length, &topic[0]); + break; + } + } + return topic; +} + +int32_t Decoder::decodeQueueId(Buffer::Instance& buffer, int32_t cursor) { + if (!isComplete(buffer, cursor)) { + return -1; + } + + int32_t offset = 4 /* total size */ + + 4 /* magic code */ + + 4 /* body CRC */; + + return buffer.peekBEInt(cursor + offset); +} + +int64_t Decoder::decodeQueueOffset(Buffer::Instance& buffer, int32_t cursor) { + if (!isComplete(buffer, cursor)) { + return -1; + } + + int32_t offset = 4 /* total size */ + + 4 /* magic code */ + + 4 /* body CRC */ + + 4 /* queue Id */ + + 4 /* flag */; + return buffer.peekBEInt(cursor + offset); +} + +std::string Decoder::decodeMsgId(Buffer::Instance& buffer, int32_t cursor) { + if (!isComplete(buffer, cursor)) { + return EMPTY_STRING; + } + + int32_t offset = 4 /* total size */ + + 4 /* magic code */ + + 4 /* body CRC */ + + 4 /* queue Id */ + + 4 /* flag */ + + 8 /* queue offset */; + auto physical_offset = buffer.peekBEInt(cursor + offset); + offset += 8 /* physical offset */ + + 4 /* sys flag */ + + 8 /* born timestamp */ + + 4 /* born host */ + + 4 /* born host port */ + + 8 /* store timestamp */ + ; + + Buffer::OwnedImpl msg_id_buffer; + msg_id_buffer.writeBEInt(buffer.peekBEInt(cursor + offset)); + msg_id_buffer.writeBEInt(physical_offset); + std::string msg_id; + msg_id.reserve(32); + for (uint64_t i = 0; i < msg_id_buffer.length(); i++) { + auto c = msg_id_buffer.peekBEInt(); + msg_id.append(1, static_cast(c >> 4U)); + msg_id.append(1, static_cast(c & 0xFU)); + } + return msg_id; +} + +CommandCustomHeaderPtr Decoder::decodeExtHeader(RequestCode code, + ProtobufWkt::Struct& header_struct) { + const auto& filed_value_pair = header_struct.fields(); + switch (code) { + case RequestCode::SendMessage: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto send_msg_ext_header = new SendMessageRequestHeader(); + send_msg_ext_header->version_ = SendMessageRequestVersion::V1; + send_msg_ext_header->decode(ext_fields); + return send_msg_ext_header; + } + case RequestCode::SendMessageV2: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto send_msg_ext_header = new SendMessageRequestHeader(); + send_msg_ext_header->version_ = SendMessageRequestVersion::V2; + send_msg_ext_header->decode(ext_fields); + return send_msg_ext_header; + } + + case RequestCode::GetRouteInfoByTopic: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto get_route_info_request_header = new GetRouteInfoRequestHeader(); + get_route_info_request_header->decode(ext_fields); + return get_route_info_request_header; + } + + case RequestCode::UnregisterClient: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto unregister_client_request_header = new UnregisterClientRequestHeader(); + unregister_client_request_header->decode(ext_fields); + return unregister_client_request_header; + } + + case RequestCode::GetConsumerListByGroup: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto get_consumer_list_by_group_request_header = new GetConsumerListByGroupRequestHeader(); + get_consumer_list_by_group_request_header->decode(ext_fields); + return get_consumer_list_by_group_request_header; + } + + case RequestCode::PopMessage: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto pop_message_request_header = new PopMessageRequestHeader(); + pop_message_request_header->decode(ext_fields); + return pop_message_request_header; + } + + case RequestCode::AckMessage: { + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + auto ack_message_request_header = new AckMessageRequestHeader(); + ack_message_request_header->decode(ext_fields); + return ack_message_request_header; + } + + case RequestCode::HeartBeat: { + // Heartbeat does not have an extended header. + return nullptr; + } + + default: + ENVOY_LOG(warn, "Unsupported request code: {}", static_cast(code)); + return nullptr; + } +} + +CommandCustomHeaderPtr Decoder::decodeResponseExtHeader(ResponseCode response_code, + ProtobufWkt::Struct& header_struct, + RequestCode request_code) { + // No need to decode a failed response. + if (response_code != ResponseCode::Success && response_code != ResponseCode::SlaveNotAvailable) { + return nullptr; + } + const auto& filed_value_pair = header_struct.fields(); + switch (request_code) { + case RequestCode::SendMessage: + case RequestCode::SendMessageV2: { + auto send_message_response_header = new SendMessageResponseHeader(); + ASSERT(filed_value_pair.contains("extFields")); + auto& ext_fields = filed_value_pair.at("extFields"); + send_message_response_header->decode(ext_fields); + return send_message_response_header; + } + + case RequestCode::PopMessage: { + auto pop_message_response_header = new PopMessageResponseHeader(); + ASSERT(filed_value_pair.contains("extFields")); + const auto& ext_fields = filed_value_pair.at("extFields"); + pop_message_response_header->decode(ext_fields); + return pop_message_response_header; + } + default: + return nullptr; + } +} + +void Encoder::encode(const RemotingCommandPtr& command, Buffer::Instance& data) { + + ProtobufWkt::Struct command_struct; + auto* fields = command_struct.mutable_fields(); + + ProtobufWkt::Value code_v; + code_v.set_number_value(command->code_); + (*fields)["code"] = code_v; + + ProtobufWkt::Value language_v; + language_v.set_string_value(command->language()); + (*fields)["language"] = language_v; + + ProtobufWkt::Value version_v; + version_v.set_number_value(command->version_); + (*fields)["version"] = version_v; + + ProtobufWkt::Value opaque_v; + opaque_v.set_number_value(command->opaque_); + (*fields)["opaque"] = opaque_v; + + ProtobufWkt::Value flag_v; + flag_v.set_number_value(command->flag_); + (*fields)["flag"] = flag_v; + + if (!command->remark_.empty()) { + ProtobufWkt::Value remark_v; + remark_v.set_string_value(command->remark_); + (*fields)["remark"] = remark_v; + } + + ProtobufWkt::Value serialization_type_v; + serialization_type_v.set_string_value(command->serializeTypeCurrentRPC()); + (*fields)["serializeTypeCurrentRPC"] = serialization_type_v; + + if (command->custom_header_) { + ProtobufWkt::Value ext_fields_v; + command->custom_header_->encode(ext_fields_v); + (*fields)["extFields"] = ext_fields_v; + } + + std::string json = MessageUtil::getJsonStringFromMessage(command_struct); + + int32_t frame_length = 4; + int32_t header_length = json.size(); + frame_length += header_length; + frame_length += command->bodyLength(); + + data.writeBEInt(frame_length); + data.writeBEInt(header_length); + data.add(json); + + // add body + if (command->bodyLength() > 0) { + data.add(command->body()); + } +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/codec.h b/source/extensions/filters/network/rocketmq_proxy/codec.h new file mode 100644 index 000000000000..e22502f48b34 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/codec.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include + +#include "envoy/common/platform.h" +#include "envoy/network/filter.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/logger.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/rocketmq_proxy/protocol.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +enum MessageVersion : uint32_t { + V1 = (0xAABBCCDDU ^ 1880681586U) + 8U, + V2 = (0xAABBCCDDU ^ 1880681586U) + 4U +}; + +class Decoder : Logger::Loggable { +public: + Decoder() = default; + + ~Decoder() = default; + + /** + * @param buffer Data buffer to decode. + * @param underflow Indicate if buffer contains enough data in terms of protocol frame. + * @param has_error Indicate if the decoding is successful or not. + * @param request_code Corresponding request code if applies. + * @return Decoded remote command. + */ + static RemotingCommandPtr decode(Buffer::Instance& buffer, bool& underflow, bool& has_error, + int request_code = 0); + + static std::string decodeTopic(Buffer::Instance& buffer, int32_t cursor); + + static int32_t decodeQueueId(Buffer::Instance& buffer, int32_t cursor); + + static int64_t decodeQueueOffset(Buffer::Instance& buffer, int32_t cursor); + + static std::string decodeMsgId(Buffer::Instance& buffer, int32_t cursor); + + static constexpr uint32_t MIN_FRAME_SIZE = 8; + + static constexpr uint32_t MAX_FRAME_SIZE = 4 * 1024 * 1024; + + static constexpr uint32_t FRAME_LENGTH_FIELD_SIZE = 4; + + static constexpr uint32_t FRAME_HEADER_LENGTH_FIELD_SIZE = 4; + +private: + static uint32_t adjustHeaderLength(uint32_t len) { return len & 0xFFFFFFu; } + + static bool isJsonHeader(uint32_t len) { return (len >> 24u) == 0; } + + static CommandCustomHeaderPtr decodeExtHeader(RequestCode code, + ProtobufWkt::Struct& header_struct); + + static CommandCustomHeaderPtr decodeResponseExtHeader(ResponseCode response_code, + ProtobufWkt::Struct& header_struct, + RequestCode request_code); + + static bool isComplete(Buffer::Instance& buffer, int32_t cursor); +}; + +class Encoder { +public: + static void encode(const RemotingCommandPtr& command, Buffer::Instance& buffer); +}; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/config.cc b/source/extensions/filters/network/rocketmq_proxy/config.cc new file mode 100644 index 000000000000..02f8da69c41f --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/config.cc @@ -0,0 +1,65 @@ +#include "extensions/filters/network/rocketmq_proxy/config.h" + +#include + +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.h" +#include "envoy/registry/registry.h" +#include "envoy/server/filter_config.h" + +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/stats.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +namespace rocketmq_config = envoy::extensions::filters::network::rocketmq_proxy::v3; + +Network::FilterFactoryCb RocketmqProxyFilterConfigFactory::createFilterFactoryFromProtoTyped( + const rocketmq_config::RocketmqProxy& proto_config, + Server::Configuration::FactoryContext& context) { + std::shared_ptr filter_config = std::make_shared(proto_config, context); + return [filter_config, &context](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter( + std::make_shared(*filter_config, context.dispatcher().timeSource())); + }; +} + +REGISTER_FACTORY(RocketmqProxyFilterConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory); + +ConfigImpl::ConfigImpl(const RocketmqProxyConfig& config, + Server::Configuration::FactoryContext& context) + : context_(context), stats_prefix_(fmt::format("rocketmq.{}.", config.stat_prefix())), + stats_(RocketmqFilterStats::generateStats(stats_prefix_, context_.scope())), + route_matcher_(new Router::RouteMatcher(config.route_config())), + develop_mode_(config.develop_mode()), + transient_object_life_span_(PROTOBUF_GET_MS_OR_DEFAULT(config, transient_object_life_span, + TransientObjectLifeSpan)) {} + +std::string ConfigImpl::proxyAddress() { + const LocalInfo::LocalInfo& localInfo = context_.getServerFactoryContext().localInfo(); + Network::Address::InstanceConstSharedPtr address = localInfo.address(); + if (address->type() == Network::Address::Type::Ip) { + const std::string& ip = address->ip()->addressAsString(); + std::string proxyAddr{ip}; + if (address->ip()->port()) { + return proxyAddr.append(":").append(std::to_string(address->ip()->port())); + } else { + ENVOY_LOG(trace, "Local info does not have port specified, defaulting to 10000"); + return proxyAddr.append(":10000"); + } + } + return address->asString(); +} + +Router::RouteConstSharedPtr ConfigImpl::route(const MessageMetadata& metadata) const { + return route_matcher_->route(metadata); +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/config.h b/source/extensions/filters/network/rocketmq_proxy/config.h new file mode 100644 index 000000000000..df5cbe7c9711 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/config.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.validate.h" + +#include "extensions/filters/network/common/factory_base.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/router/route_matcher.h" +#include "extensions/filters/network/rocketmq_proxy/router/router_impl.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class RocketmqProxyFilterConfigFactory + : public Common::FactoryBase< + envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy> { +public: + RocketmqProxyFilterConfigFactory() : FactoryBase(NetworkFilterNames::get().RocketmqProxy, true) {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +class ConfigImpl : public Config, public Router::Config, Logger::Loggable { +public: + using RocketmqProxyConfig = + envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy; + + ConfigImpl(const RocketmqProxyConfig& config, Server::Configuration::FactoryContext& context); + ~ConfigImpl() override = default; + + // Config + RocketmqFilterStats& stats() override { return stats_; } + Upstream::ClusterManager& clusterManager() override { return context_.clusterManager(); } + Router::RouterPtr createRouter() override { + return std::make_unique(context_.clusterManager()); + } + bool developMode() const override { return develop_mode_; } + + std::chrono::milliseconds transientObjectLifeSpan() const override { + return transient_object_life_span_; + } + + std::string proxyAddress() override; + Router::Config& routerConfig() override { return *this; } + + // Router::Config + Router::RouteConstSharedPtr route(const MessageMetadata& metadata) const override; + +private: + Server::Configuration::FactoryContext& context_; + const std::string stats_prefix_; + RocketmqFilterStats stats_; + Router::RouteMatcherPtr route_matcher_; + const bool develop_mode_; + std::chrono::milliseconds transient_object_life_span_; + + static constexpr uint64_t TransientObjectLifeSpan = 30 * 1000; +}; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/conn_manager.cc b/source/extensions/filters/network/rocketmq_proxy/conn_manager.cc new file mode 100644 index 000000000000..a613998d53a0 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/conn_manager.cc @@ -0,0 +1,376 @@ +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" + +#include "envoy/buffer/buffer.h" +#include "envoy/network/connection.h" + +#include "common/common/enum_to_int.h" +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +ConsumerGroupMember::ConsumerGroupMember(absl::string_view client_id, + ConnectionManager& conn_manager) + : client_id_(client_id.data(), client_id.size()), connection_manager_(&conn_manager), + last_(connection_manager_->time_source_.monotonicTime()) {} + +void ConsumerGroupMember::refresh() { last_ = connection_manager_->time_source_.monotonicTime(); } + +bool ConsumerGroupMember::expired() const { + auto duration = connection_manager_->time_source_.monotonicTime() - last_; + return std::chrono::duration_cast(duration).count() > + connection_manager_->config().transientObjectLifeSpan().count(); +} + +ConnectionManager::ConnectionManager(Config& config, TimeSource& time_source) + : config_(config), time_source_(time_source), stats_(config.stats()) {} + +Envoy::Network::FilterStatus ConnectionManager::onData(Envoy::Buffer::Instance& data, + bool end_stream) { + ENVOY_CONN_LOG(trace, "rocketmq_proxy: received {} bytes.", read_callbacks_->connection(), + data.length()); + request_buffer_.move(data); + dispatch(); + if (end_stream) { + resetAllActiveMessages("Connection to downstream is closed"); + read_callbacks_->connection().close(Envoy::Network::ConnectionCloseType::FlushWrite); + } + return Network::FilterStatus::StopIteration; +} + +void ConnectionManager::dispatch() { + if (request_buffer_.length() < Decoder::MIN_FRAME_SIZE) { + ENVOY_CONN_LOG(warn, "rocketmq_proxy: request buffer length is less than min frame size: {}", + read_callbacks_->connection(), request_buffer_.length()); + return; + } + + bool underflow = false; + bool has_decode_error = false; + while (!underflow) { + RemotingCommandPtr request = Decoder::decode(request_buffer_, underflow, has_decode_error); + if (underflow) { + // Wait for more data + break; + } + stats_.request_.inc(); + + // Decode error, we need to close connection immediately. + if (has_decode_error) { + ENVOY_CONN_LOG(error, "Failed to decode request, close connection immediately", + read_callbacks_->connection()); + stats_.request_decoding_error_.inc(); + resetAllActiveMessages("Failed to decode data from downstream. Close connection immediately"); + read_callbacks_->connection().close(Envoy::Network::ConnectionCloseType::FlushWrite); + return; + } else { + stats_.request_decoding_success_.inc(); + } + + switch (static_cast(request->code())) { + case RequestCode::GetRouteInfoByTopic: { + ENVOY_CONN_LOG(trace, "GetTopicRoute request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onGetTopicRoute(std::move(request)); + } break; + + case RequestCode::UnregisterClient: { + ENVOY_CONN_LOG(trace, "process unregister client request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onUnregisterClient(std::move(request)); + } break; + + case RequestCode::SendMessage: { + ENVOY_CONN_LOG(trace, "SendMessage request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onSendMessage(std::move(request)); + stats_.send_message_v1_.inc(); + } break; + + case RequestCode::SendMessageV2: { + ENVOY_CONN_LOG(trace, "SendMessage request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onSendMessage(std::move(request)); + stats_.send_message_v2_.inc(); + } break; + + case RequestCode::GetConsumerListByGroup: { + ENVOY_CONN_LOG(trace, "GetConsumerListByGroup request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onGetConsumerListByGroup(std::move(request)); + } break; + + case RequestCode::PopMessage: { + ENVOY_CONN_LOG(trace, "PopMessage request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onPopMessage(std::move(request)); + stats_.pop_message_.inc(); + } break; + + case RequestCode::AckMessage: { + ENVOY_CONN_LOG(trace, "AckMessage request, code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + onAckMessage(std::move(request)); + stats_.ack_message_.inc(); + } break; + + case RequestCode::HeartBeat: { + ENVOY_CONN_LOG(trace, "Heartbeat request, opaque: {}", read_callbacks_->connection(), + request->opaque()); + onHeartbeat(std::move(request)); + } break; + + default: { + ENVOY_CONN_LOG(warn, "Request code {} not supported yet", read_callbacks_->connection(), + request->code()); + std::string error_msg("Request not supported"); + onError(request, error_msg); + } break; + } + } +} + +void ConnectionManager::purgeDirectiveTable() { + auto current = time_source_.monotonicTime(); + for (auto it = ack_directive_table_.begin(); it != ack_directive_table_.end();) { + auto duration = current - it->second.creation_time_; + if (std::chrono::duration_cast(duration).count() > + config_.transientObjectLifeSpan().count()) { + ack_directive_table_.erase(it++); + } else { + it++; + } + } +} + +void ConnectionManager::sendResponseToDownstream(RemotingCommandPtr& response) { + Buffer::OwnedImpl buffer; + Encoder::encode(response, buffer); + if (read_callbacks_->connection().state() == Network::Connection::State::Open) { + ENVOY_CONN_LOG(trace, "Write response to downstream. Opaque: {}", read_callbacks_->connection(), + response->opaque()); + read_callbacks_->connection().write(buffer, false); + } else { + ENVOY_CONN_LOG(error, "Send response to downstream failed as connection is no longer open", + read_callbacks_->connection()); + } +} + +void ConnectionManager::onGetTopicRoute(RemotingCommandPtr request) { + createActiveMessage(request).onQueryTopicRoute(); + stats_.get_topic_route_.inc(); +} + +void ConnectionManager::onHeartbeat(RemotingCommandPtr request) { + const std::string& body = request->body().toString(); + + purgeDirectiveTable(); + + ProtobufWkt::Struct body_struct; + try { + MessageUtil::loadFromJson(body, body_struct); + } catch (std::exception& e) { + ENVOY_LOG(warn, "Failed to decode heartbeat body. Error message: {}", e.what()); + return; + } + + HeartbeatData heartbeatData; + if (!heartbeatData.decode(body_struct)) { + ENVOY_LOG(warn, "Failed to decode heartbeat data"); + return; + } + + for (const auto& group : heartbeatData.consumerGroups()) { + addOrUpdateGroupMember(group, heartbeatData.clientId()); + } + + RemotingCommandPtr response = std::make_unique(); + response->code(enumToSignedInt(ResponseCode::Success)); + response->opaque(request->opaque()); + response->remark("Heartbeat OK"); + response->markAsResponse(); + sendResponseToDownstream(response); + stats_.heartbeat_.inc(); +} + +void ConnectionManager::addOrUpdateGroupMember(absl::string_view group, + absl::string_view client_id) { + ENVOY_LOG(trace, "#addOrUpdateGroupMember. Group: {}, client ID: {}", group, client_id); + auto search = group_members_.find(std::string(group.data(), group.length())); + if (search == group_members_.end()) { + std::vector members; + members.emplace_back(ConsumerGroupMember(client_id, *this)); + group_members_.emplace(std::string(group.data(), group.size()), members); + } else { + std::vector& members = search->second; + for (auto it = members.begin(); it != members.end();) { + if (it->clientId() == client_id) { + it->refresh(); + ++it; + } else if (it->expired()) { + it = members.erase(it); + } else { + ++it; + } + } + if (members.empty()) { + group_members_.erase(search); + } + } +} + +void ConnectionManager::onUnregisterClient(RemotingCommandPtr request) { + auto header = request->typedCustomHeader(); + ASSERT(header != nullptr); + ASSERT(!header->clientId().empty()); + ENVOY_LOG(trace, "Unregister client ID: {}, producer group: {}, consumer group: {}", + header->clientId(), header->producerGroup(), header->consumerGroup()); + + if (!header->consumerGroup().empty()) { + auto search = group_members_.find(header->consumerGroup()); + if (search != group_members_.end()) { + std::vector& members = search->second; + for (auto it = members.begin(); it != members.end();) { + if (it->clientId() == header->clientId()) { + it = members.erase(it); + } else if (it->expired()) { + it = members.erase(it); + } else { + ++it; + } + } + if (members.empty()) { + group_members_.erase(search); + } + } + } + + RemotingCommandPtr response = std::make_unique( + enumToSignedInt(ResponseCode::Success), request->version(), request->opaque()); + response->markAsResponse(); + response->remark("Envoy unregister client OK."); + sendResponseToDownstream(response); + stats_.unregister_.inc(); +} + +void ConnectionManager::onError(RemotingCommandPtr& request, absl::string_view error_msg) { + Buffer::OwnedImpl buffer; + RemotingCommandPtr response = std::make_unique(); + response->markAsResponse(); + response->opaque(request->opaque()); + response->code(enumToSignedInt(ResponseCode::SystemError)); + response->remark(error_msg); + sendResponseToDownstream(response); +} + +void ConnectionManager::onSendMessage(RemotingCommandPtr request) { + ENVOY_CONN_LOG(trace, "#onSendMessage, opaque: {}", read_callbacks_->connection(), + request->opaque()); + auto header = request->typedCustomHeader(); + header->queueId(-1); + createActiveMessage(request).sendRequestToUpstream(); +} + +void ConnectionManager::onGetConsumerListByGroup(RemotingCommandPtr request) { + auto requestExtHeader = request->typedCustomHeader(); + + ASSERT(requestExtHeader != nullptr); + ASSERT(!requestExtHeader->consumerGroup().empty()); + + ENVOY_LOG(trace, "#onGetConsumerListByGroup, consumer group: {}", + requestExtHeader->consumerGroup()); + + auto search = group_members_.find(requestExtHeader->consumerGroup()); + GetConsumerListByGroupResponseBody getConsumerListByGroupResponseBody; + if (search != group_members_.end()) { + std::vector& members = search->second; + std::sort(members.begin(), members.end()); + for (const auto& member : members) { + getConsumerListByGroupResponseBody.add(member.clientId()); + } + } else { + ENVOY_LOG(warn, "There is no consumer belongs to consumer_group: {}", + requestExtHeader->consumerGroup()); + } + ProtobufWkt::Struct body_struct; + + getConsumerListByGroupResponseBody.encode(body_struct); + + RemotingCommandPtr response = std::make_unique( + enumToSignedInt(ResponseCode::Success), request->version(), request->opaque()); + response->markAsResponse(); + std::string json = MessageUtil::getJsonStringFromMessage(body_struct); + response->body().add(json); + ENVOY_LOG(trace, "GetConsumerListByGroup respond with body: {}", json); + + sendResponseToDownstream(response); + stats_.get_consumer_list_.inc(); +} + +void ConnectionManager::onPopMessage(RemotingCommandPtr request) { + auto header = request->typedCustomHeader(); + ASSERT(header != nullptr); + ENVOY_LOG(trace, "#onPopMessage. Consumer group: {}, topic: {}", header->consumerGroup(), + header->topic()); + createActiveMessage(request).sendRequestToUpstream(); +} + +void ConnectionManager::onAckMessage(RemotingCommandPtr request) { + auto header = request->typedCustomHeader(); + ASSERT(header != nullptr); + ENVOY_LOG( + trace, + "#onAckMessage. Consumer group: {}, topic: {}, queue Id: {}, offset: {}, extra-info: {}", + header->consumerGroup(), header->topic(), header->queueId(), header->offset(), + header->extraInfo()); + + // Fill the target broker_name and broker_id routing directive + auto it = ack_directive_table_.find(header->directiveKey()); + if (it == ack_directive_table_.end()) { + ENVOY_LOG(warn, "There was no previous ack directive available, which is unexpected"); + onError(request, "No ack directive is found"); + return; + } + header->targetBrokerName(it->second.broker_name_); + header->targetBrokerId(it->second.broker_id_); + + createActiveMessage(request).sendRequestToUpstream(); +} + +ActiveMessage& ConnectionManager::createActiveMessage(RemotingCommandPtr& request) { + ENVOY_CONN_LOG(trace, "ConnectionManager#createActiveMessage. Code: {}, opaque: {}", + read_callbacks_->connection(), request->code(), request->opaque()); + ActiveMessagePtr active_message = std::make_unique(*this, std::move(request)); + active_message->moveIntoList(std::move(active_message), active_message_list_); + return **active_message_list_.begin(); +} + +void ConnectionManager::deferredDelete(ActiveMessage& active_message) { + read_callbacks_->connection().dispatcher().deferredDelete( + active_message.removeFromList(active_message_list_)); +} + +void ConnectionManager::resetAllActiveMessages(absl::string_view error_msg) { + while (!active_message_list_.empty()) { + ENVOY_CONN_LOG(warn, "Reset pending request {} due to error: {}", read_callbacks_->connection(), + active_message_list_.front()->downstreamRequest()->opaque(), error_msg); + active_message_list_.front()->onReset(); + stats_.response_error_.inc(); + } +} + +Envoy::Network::FilterStatus ConnectionManager::onNewConnection() { + return Network::FilterStatus::Continue; +} + +void ConnectionManager::initializeReadFilterCallbacks( + Envoy::Network::ReadFilterCallbacks& callbacks) { + read_callbacks_ = &callbacks; +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/rocketmq_proxy/conn_manager.h b/source/extensions/filters/network/rocketmq_proxy/conn_manager.h new file mode 100644 index 000000000000..e69237b6cae7 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/conn_manager.h @@ -0,0 +1,215 @@ +#pragma once + +#include + +#include "envoy/common/time.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.validate.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" +#include "envoy/stats/stats_macros.h" +#include "envoy/stats/timespan.h" +#include "envoy/upstream/thread_local_cluster.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/logger.h" + +#include "extensions/filters/network/rocketmq_proxy/active_message.h" +#include "extensions/filters/network/rocketmq_proxy/codec.h" +#include "extensions/filters/network/rocketmq_proxy/stats.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class Config { +public: + virtual ~Config() = default; + + virtual RocketmqFilterStats& stats() PURE; + + virtual Upstream::ClusterManager& clusterManager() PURE; + + virtual Router::RouterPtr createRouter() PURE; + + /** + * Indicate whether this proxy is running in develop mode. Once set true, this proxy plugin may + * work without dedicated traffic intercepting facility without considering backward + * compatibility. + * @return true when in development mode; false otherwise. + */ + virtual bool developMode() const PURE; + + virtual std::string proxyAddress() PURE; + + virtual Router::Config& routerConfig() PURE; + + virtual std::chrono::milliseconds transientObjectLifeSpan() const PURE; +}; + +class ConnectionManager; + +/** + * This class is to ensure legacy RocketMQ SDK works. Heartbeat between client SDK and envoy is not + * necessary any more and should be removed once the lite SDK is in-place. + */ +class ConsumerGroupMember { +public: + ConsumerGroupMember(absl::string_view client_id, ConnectionManager& conn_manager); + + bool operator==(const ConsumerGroupMember& other) const { return client_id_ == other.client_id_; } + + bool operator<(const ConsumerGroupMember& other) const { return client_id_ < other.client_id_; } + + void refresh(); + + bool expired() const; + + absl::string_view clientId() const { return client_id_; } + + void setLastForTest(MonotonicTime tp) { last_ = tp; } + +private: + std::string client_id_; + ConnectionManager* connection_manager_; + MonotonicTime last_; +}; + +class ConnectionManager : public Network::ReadFilter, Logger::Loggable { +public: + ConnectionManager(Config& config, TimeSource& time_source); + + ~ConnectionManager() override = default; + + /** + * Called when data is read on the connection. + * @param data supplies the read data which may be modified. + * @param end_stream supplies whether this is the last byte on the connection. This will only + * be set if the connection has half-close semantics enabled. + * @return status used by the filter manager to manage further filter iteration. + */ + Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; + + /** + * Called when a connection is first established. Filters should do one time long term processing + * that needs to be done when a connection is established. Filter chain iteration can be stopped + * if needed. + * @return status used by the filter manager to manage further filter iteration. + */ + Network::FilterStatus onNewConnection() override; + + /** + * Initializes the read filter callbacks used to interact with the filter manager. It will be + * called by the filter manager a single time when the filter is first registered. Thus, any + * construction that requires the backing connection should take place in the context of this + * function. + * + * IMPORTANT: No outbound networking or complex processing should be done in this function. + * That should be done in the context of onNewConnection() if needed. + * + * @param callbacks supplies the callbacks. + */ + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks&) override; + + /** + * Send response to downstream either when envoy proxy has received result from upstream hosts or + * the proxy itself may serve the request. + * @param response Response to write to downstream with identical opaque number. + */ + void sendResponseToDownstream(RemotingCommandPtr& response); + + void onGetTopicRoute(RemotingCommandPtr request); + + /** + * Called when downstream sends heartbeat requests. + * @param request heartbeat request from downstream + */ + void onHeartbeat(RemotingCommandPtr request); + + void addOrUpdateGroupMember(absl::string_view group, absl::string_view client_id); + + void onUnregisterClient(RemotingCommandPtr request); + + void onError(RemotingCommandPtr& request, absl::string_view error_msg); + + void onSendMessage(RemotingCommandPtr request); + + void onGetConsumerListByGroup(RemotingCommandPtr request); + + void onPopMessage(RemotingCommandPtr request); + + void onAckMessage(RemotingCommandPtr request); + + ActiveMessage& createActiveMessage(RemotingCommandPtr& request); + + void deferredDelete(ActiveMessage& active_message); + + void resetAllActiveMessages(absl::string_view error_msg); + + Config& config() { return config_; } + + RocketmqFilterStats& stats() { return stats_; } + + absl::flat_hash_map>& groupMembersForTest() { + return group_members_; + } + + std::list& activeMessageList() { return active_message_list_; } + + void insertAckDirective(const std::string& key, const AckMessageDirective& directive) { + ack_directive_table_.insert(std::make_pair(key, directive)); + } + + void eraseAckDirective(const std::string& key) { + auto it = ack_directive_table_.find(key); + if (it != ack_directive_table_.end()) { + ack_directive_table_.erase(it); + } + } + + TimeSource& timeSource() const { return time_source_; } + + const absl::flat_hash_map& getAckDirectiveTableForTest() const { + return ack_directive_table_; + } + + friend class ConsumerGroupMember; + +private: + /** + * Dispatch incoming requests from downstream to run through filter chains. + */ + void dispatch(); + + /** + * Invoked by heartbeat to purge deprecated ack_directive entries. + */ + void purgeDirectiveTable(); + + Network::ReadFilterCallbacks* read_callbacks_{}; + Buffer::OwnedImpl request_buffer_; + + Config& config_; + TimeSource& time_source_; + RocketmqFilterStats& stats_; + + std::list active_message_list_; + + absl::flat_hash_map> group_members_; + + /** + * Message unique key to message acknowledge directive mapping. + * Acknowledge requests first consult this table to determine which host in the cluster to go. + */ + absl::flat_hash_map ack_directive_table_; +}; +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/metadata.h b/source/extensions/filters/network/rocketmq_proxy/metadata.h new file mode 100644 index 000000000000..8fca6ab7811a --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/metadata.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "common/http/header_map_impl.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class MessageMetadata { +public: + MessageMetadata() = default; + + void setOneWay(bool oneway) { is_oneway_ = oneway; } + bool isOneWay() const { return is_oneway_; } + + bool hasTopicName() const { return topic_name_.has_value(); } + const std::string& topicName() const { return topic_name_.value(); } + void setTopicName(const std::string& topic_name) { topic_name_ = topic_name; } + + /** + * @return HeaderMap of current headers + */ + const Http::HeaderMap& headers() const { return headers_; } + Http::HeaderMap& headers() { return headers_; } + +private: + bool is_oneway_{false}; + absl::optional topic_name_{}; + + Http::HeaderMapImpl headers_; +}; + +using MessageMetadataSharedPtr = std::shared_ptr; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/protocol.cc b/source/extensions/filters/network/rocketmq_proxy/protocol.cc new file mode 100644 index 000000000000..e16481cc453f --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/protocol.cc @@ -0,0 +1,749 @@ +#include "extensions/filters/network/rocketmq_proxy/protocol.h" + +#include "common/common/assert.h" +#include "common/common/enum_to_int.h" + +#include "extensions/filters/network/rocketmq_proxy/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +void SendMessageRequestHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + switch (version_) { + case SendMessageRequestVersion::V1: { + ProtobufWkt::Value producer_group_v; + producer_group_v.set_string_value(producer_group_); + members["producerGroup"] = producer_group_v; + + ProtobufWkt::Value topic_v; + topic_v.set_string_value(topic_.c_str(), topic_.length()); + members["topic"] = topic_v; + + ProtobufWkt::Value default_topic_v; + default_topic_v.set_string_value(default_topic_); + members["defaultTopic"] = default_topic_v; + + ProtobufWkt::Value default_topic_queue_number_v; + default_topic_queue_number_v.set_number_value(default_topic_queue_number_); + members["defaultTopicQueueNums"] = default_topic_queue_number_v; + + ProtobufWkt::Value queue_id_v; + queue_id_v.set_number_value(queue_id_); + members["queueId"] = queue_id_v; + + ProtobufWkt::Value sys_flag_v; + sys_flag_v.set_number_value(sys_flag_); + members["sysFlag"] = sys_flag_v; + + ProtobufWkt::Value born_timestamp_v; + born_timestamp_v.set_number_value(born_timestamp_); + members["bornTimestamp"] = born_timestamp_v; + + ProtobufWkt::Value flag_v; + flag_v.set_number_value(flag_); + members["flag"] = flag_v; + + if (!properties_.empty()) { + ProtobufWkt::Value properties_v; + properties_v.set_string_value(properties_.c_str(), properties_.length()); + members["properties"] = properties_v; + } + + if (reconsume_time_ > 0) { + ProtobufWkt::Value reconsume_times_v; + reconsume_times_v.set_number_value(reconsume_time_); + members["reconsumeTimes"] = reconsume_times_v; + } + + if (unit_mode_) { + ProtobufWkt::Value unit_mode_v; + unit_mode_v.set_bool_value(unit_mode_); + members["unitMode"] = unit_mode_v; + } + + if (batch_) { + ProtobufWkt::Value batch_v; + batch_v.set_bool_value(batch_); + members["batch"] = batch_v; + } + + if (max_reconsume_time_ > 0) { + ProtobufWkt::Value max_reconsume_time_v; + max_reconsume_time_v.set_number_value(max_reconsume_time_); + members["maxReconsumeTimes"] = max_reconsume_time_v; + } + break; + } + case SendMessageRequestVersion::V2: { + ProtobufWkt::Value producer_group_v; + producer_group_v.set_string_value(producer_group_.c_str(), producer_group_.length()); + members["a"] = producer_group_v; + + ProtobufWkt::Value topic_v; + topic_v.set_string_value(topic_.c_str(), topic_.length()); + members["b"] = topic_v; + + ProtobufWkt::Value default_topic_v; + default_topic_v.set_string_value(default_topic_.c_str(), default_topic_.length()); + members["c"] = default_topic_v; + + ProtobufWkt::Value default_topic_queue_number_v; + default_topic_queue_number_v.set_number_value(default_topic_queue_number_); + members["d"] = default_topic_queue_number_v; + + ProtobufWkt::Value queue_id_v; + queue_id_v.set_number_value(queue_id_); + members["e"] = queue_id_v; + + ProtobufWkt::Value sys_flag_v; + sys_flag_v.set_number_value(sys_flag_); + members["f"] = sys_flag_v; + + ProtobufWkt::Value born_timestamp_v; + born_timestamp_v.set_number_value(born_timestamp_); + members["g"] = born_timestamp_v; + + ProtobufWkt::Value flag_v; + flag_v.set_number_value(flag_); + members["h"] = flag_v; + + if (!properties_.empty()) { + ProtobufWkt::Value properties_v; + properties_v.set_string_value(properties_.c_str(), properties_.length()); + members["i"] = properties_v; + } + + if (reconsume_time_ > 0) { + ProtobufWkt::Value reconsume_times_v; + reconsume_times_v.set_number_value(reconsume_time_); + members["j"] = reconsume_times_v; + } + + if (unit_mode_) { + ProtobufWkt::Value unit_mode_v; + unit_mode_v.set_bool_value(unit_mode_); + members["k"] = unit_mode_v; + } + + if (batch_) { + ProtobufWkt::Value batch_v; + batch_v.set_bool_value(batch_); + members["m"] = batch_v; + } + + if (max_reconsume_time_ > 0) { + ProtobufWkt::Value max_reconsume_time_v; + max_reconsume_time_v.set_number_value(max_reconsume_time_); + members["l"] = max_reconsume_time_v; + } + break; + } + default: + break; + } +} + +void SendMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + switch (version_) { + case SendMessageRequestVersion::V1: { + ASSERT(members.contains("producerGroup")); + ASSERT(members.contains("topic")); + ASSERT(members.contains("defaultTopic")); + ASSERT(members.contains("defaultTopicQueueNums")); + ASSERT(members.contains("queueId")); + ASSERT(members.contains("sysFlag")); + ASSERT(members.contains("bornTimestamp")); + ASSERT(members.contains("flag")); + + producer_group_ = members.at("producerGroup").string_value(); + topic_ = members.at("topic").string_value(); + default_topic_ = members.at("defaultTopic").string_value(); + + if (members.at("defaultTopicQueueNums").kind_case() == ProtobufWkt::Value::kNumberValue) { + default_topic_queue_number_ = members.at("defaultTopicQueueNums").number_value(); + } else { + default_topic_queue_number_ = std::stoi(members.at("defaultTopicQueueNums").string_value()); + } + + if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + queue_id_ = members.at("queueId").number_value(); + } else { + queue_id_ = std::stoi(members.at("queueId").string_value()); + } + + if (members.at("sysFlag").kind_case() == ProtobufWkt::Value::kNumberValue) { + sys_flag_ = static_cast(members.at("sysFlag").number_value()); + } else { + sys_flag_ = std::stoi(members.at("sysFlag").string_value()); + } + + if (members.at("bornTimestamp").kind_case() == ProtobufWkt::Value::kNumberValue) { + born_timestamp_ = static_cast(members.at("bornTimestamp").number_value()); + } else { + born_timestamp_ = std::stoll(members.at("bornTimestamp").string_value()); + } + + if (members.at("flag").kind_case() == ProtobufWkt::Value::kNumberValue) { + flag_ = static_cast(members.at("flag").number_value()); + } else { + flag_ = std::stoi(members.at("flag").string_value()); + } + + if (members.contains("properties")) { + properties_ = members.at("properties").string_value(); + } + + if (members.contains("reconsumeTimes")) { + if (members.at("reconsumeTimes").kind_case() == ProtobufWkt::Value::kNumberValue) { + reconsume_time_ = members.at("reconsumeTimes").number_value(); + } else { + reconsume_time_ = std::stoi(members.at("reconsumeTimes").string_value()); + } + } + + if (members.contains("unitMode")) { + if (members.at("unitMode").kind_case() == ProtobufWkt::Value::kBoolValue) { + unit_mode_ = members.at("unitMode").bool_value(); + } else { + unit_mode_ = (members.at("unitMode").string_value() == std::string("true")); + } + } + + if (members.contains("batch")) { + if (members.at("batch").kind_case() == ProtobufWkt::Value::kBoolValue) { + batch_ = members.at("batch").bool_value(); + } else { + batch_ = (members.at("batch").string_value() == std::string("true")); + } + } + + if (members.contains("maxReconsumeTimes")) { + if (members.at("maxReconsumeTimes").kind_case() == ProtobufWkt::Value::kNumberValue) { + max_reconsume_time_ = static_cast(members.at("maxReconsumeTimes").number_value()); + } else { + max_reconsume_time_ = std::stoi(members.at("maxReconsumeTimes").string_value()); + } + } + break; + } + + case SendMessageRequestVersion::V2: { + ASSERT(members.contains("a")); + ASSERT(members.contains("b")); + ASSERT(members.contains("c")); + ASSERT(members.contains("d")); + ASSERT(members.contains("e")); + ASSERT(members.contains("f")); + ASSERT(members.contains("g")); + ASSERT(members.contains("h")); + + producer_group_ = members.at("a").string_value(); + topic_ = members.at("b").string_value(); + default_topic_ = members.at("c").string_value(); + + if (members.at("d").kind_case() == ProtobufWkt::Value::kNumberValue) { + default_topic_queue_number_ = members.at("d").number_value(); + } else { + default_topic_queue_number_ = std::stoi(members.at("d").string_value()); + } + + if (members.at("e").kind_case() == ProtobufWkt::Value::kNumberValue) { + queue_id_ = members.at("e").number_value(); + } else { + queue_id_ = std::stoi(members.at("e").string_value()); + } + + if (members.at("f").kind_case() == ProtobufWkt::Value::kNumberValue) { + sys_flag_ = static_cast(members.at("f").number_value()); + } else { + sys_flag_ = std::stoi(members.at("f").string_value()); + } + + if (members.at("g").kind_case() == ProtobufWkt::Value::kNumberValue) { + born_timestamp_ = static_cast(members.at("g").number_value()); + } else { + born_timestamp_ = std::stoll(members.at("g").string_value()); + } + + if (members.at("h").kind_case() == ProtobufWkt::Value::kNumberValue) { + flag_ = static_cast(members.at("h").number_value()); + } else { + flag_ = std::stoi(members.at("h").string_value()); + } + + if (members.contains("i")) { + properties_ = members.at("i").string_value(); + } + + if (members.contains("j")) { + if (members.at("j").kind_case() == ProtobufWkt::Value::kNumberValue) { + reconsume_time_ = members.at("j").number_value(); + } else { + reconsume_time_ = std::stoi(members.at("j").string_value()); + } + } + + if (members.contains("k")) { + if (members.at("k").kind_case() == ProtobufWkt::Value::kBoolValue) { + unit_mode_ = members.at("k").bool_value(); + } else { + unit_mode_ = (members.at("k").string_value() == std::string("true")); + } + } + + if (members.contains("m")) { + if (members.at("m").kind_case() == ProtobufWkt::Value::kBoolValue) { + batch_ = members.at("m").bool_value(); + } else { + batch_ = (members.at("m").string_value() == std::string("true")); + } + } + + if (members.contains("l")) { + if (members.at("l").kind_case() == ProtobufWkt::Value::kNumberValue) { + max_reconsume_time_ = members.at("l").number_value(); + } else { + max_reconsume_time_ = std::stoi(members.at("l").string_value()); + } + } + break; + } + default: + ENVOY_LOG(error, "Unknown SendMessageRequestVersion: {}", static_cast(version_)); + break; + } +} + +void SendMessageResponseHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ASSERT(!msg_id_.empty()); + ProtobufWkt::Value msg_id_v; + msg_id_v.set_string_value(msg_id_.c_str(), msg_id_.length()); + members["msgId"] = msg_id_v; + + ASSERT(queue_id_ >= 0); + ProtobufWkt::Value queue_id_v; + queue_id_v.set_number_value(queue_id_); + members["queueId"] = queue_id_v; + + ASSERT(queue_offset_ >= 0); + ProtobufWkt::Value queue_offset_v; + queue_offset_v.set_number_value(queue_offset_); + members["queueOffset"] = queue_offset_v; + + if (!transaction_id_.empty()) { + ProtobufWkt::Value transaction_id_v; + transaction_id_v.set_string_value(transaction_id_.c_str(), transaction_id_.length()); + members["transactionId"] = transaction_id_v; + } +} + +void SendMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("msgId")); + ASSERT(members.contains("queueId")); + ASSERT(members.contains("queueOffset")); + + msg_id_ = members.at("msgId").string_value(); + + if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + queue_id_ = members.at("queueId").number_value(); + } else { + queue_id_ = std::stoi(members.at("queueId").string_value()); + } + + if (members.at("queueOffset").kind_case() == ProtobufWkt::Value::kNumberValue) { + queue_offset_ = members.at("queueOffset").number_value(); + } else { + queue_offset_ = std::stoll(members.at("queueOffset").string_value()); + } + + if (members.contains("transactionId")) { + transaction_id_ = members.at("transactionId").string_value(); + } +} + +void GetRouteInfoRequestHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ProtobufWkt::Value topic_v; + topic_v.set_string_value(topic_.c_str(), topic_.length()); + members["topic"] = topic_v; +} + +void GetRouteInfoRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("topic")); + topic_ = members.at("topic").string_value(); +} + +void PopMessageRequestHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ASSERT(!consumer_group_.empty()); + ProtobufWkt::Value consumer_group_v; + consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); + members["consumerGroup"] = consumer_group_v; + + ASSERT(!topic_.empty()); + ProtobufWkt::Value topicNode; + topicNode.set_string_value(topic_.c_str(), topic_.length()); + members["topic"] = topicNode; + + ProtobufWkt::Value queue_id_v; + queue_id_v.set_number_value(queue_id_); + members["queueId"] = queue_id_v; + + ProtobufWkt::Value max_msg_nums_v; + max_msg_nums_v.set_number_value(max_msg_nums_); + members["maxMsgNums"] = max_msg_nums_v; + + ProtobufWkt::Value invisible_time_v; + invisible_time_v.set_number_value(invisible_time_); + members["invisibleTime"] = invisible_time_v; + + ProtobufWkt::Value poll_time_v; + poll_time_v.set_number_value(poll_time_); + members["pollTime"] = poll_time_v; + + ProtobufWkt::Value born_time_v; + born_time_v.set_number_value(born_time_); + members["bornTime"] = born_time_v; + + ProtobufWkt::Value init_mode_v; + init_mode_v.set_number_value(init_mode_); + members["initMode"] = init_mode_v; + + if (!exp_type_.empty()) { + ProtobufWkt::Value exp_type_v; + exp_type_v.set_string_value(exp_type_.c_str(), exp_type_.size()); + members["expType"] = exp_type_v; + } + + if (!exp_.empty()) { + ProtobufWkt::Value exp_v; + exp_v.set_string_value(exp_.c_str(), exp_.size()); + members["exp"] = exp_v; + } +} + +void PopMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("consumerGroup")); + ASSERT(members.contains("topic")); + ASSERT(members.contains("queueId")); + ASSERT(members.contains("maxMsgNums")); + ASSERT(members.contains("invisibleTime")); + ASSERT(members.contains("pollTime")); + ASSERT(members.contains("bornTime")); + ASSERT(members.contains("initMode")); + + consumer_group_ = members.at("consumerGroup").string_value(); + topic_ = members.at("topic").string_value(); + + if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + queue_id_ = members.at("queueId").number_value(); + } else { + queue_id_ = std::stoi(members.at("queueId").string_value()); + } + + if (members.at("maxMsgNums").kind_case() == ProtobufWkt::Value::kNumberValue) { + max_msg_nums_ = members.at("maxMsgNums").number_value(); + } else { + max_msg_nums_ = std::stoi(members.at("maxMsgNums").string_value()); + } + + if (members.at("invisibleTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + invisible_time_ = members.at("invisibleTime").number_value(); + } else { + invisible_time_ = std::stoll(members.at("invisibleTime").string_value()); + } + + if (members.at("pollTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + poll_time_ = members.at("pollTime").number_value(); + } else { + poll_time_ = std::stoll(members.at("pollTime").string_value()); + } + + if (members.at("bornTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + born_time_ = members.at("bornTime").number_value(); + } else { + born_time_ = std::stoll(members.at("bornTime").string_value()); + } + + if (members.at("initMode").kind_case() == ProtobufWkt::Value::kNumberValue) { + init_mode_ = members.at("initMode").number_value(); + } else { + init_mode_ = std::stol(members.at("initMode").string_value()); + } + + if (members.contains("expType")) { + exp_type_ = members.at("expType").string_value(); + } + + if (members.contains("exp")) { + exp_ = members.at("exp").string_value(); + } +} + +void PopMessageResponseHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ProtobufWkt::Value pop_time_v; + pop_time_v.set_number_value(pop_time_); + members["popTime"] = pop_time_v; + + ProtobufWkt::Value invisible_time_v; + invisible_time_v.set_number_value(invisible_time_); + members["invisibleTime"] = invisible_time_v; + + ProtobufWkt::Value revive_qid_v; + revive_qid_v.set_number_value(revive_qid_); + members["reviveQid"] = revive_qid_v; + + ProtobufWkt::Value rest_num_v; + rest_num_v.set_number_value(rest_num_); + members["restNum"] = rest_num_v; + + if (!start_offset_info_.empty()) { + ProtobufWkt::Value start_offset_info_v; + start_offset_info_v.set_string_value(start_offset_info_.c_str(), start_offset_info_.size()); + members["startOffsetInfo"] = start_offset_info_v; + } + + if (!msg_off_set_info_.empty()) { + ProtobufWkt::Value msg_offset_info_v; + msg_offset_info_v.set_string_value(msg_off_set_info_.c_str(), msg_off_set_info_.size()); + members["msgOffsetInfo"] = msg_offset_info_v; + } + + if (!order_count_info_.empty()) { + ProtobufWkt::Value order_count_info_v; + order_count_info_v.set_string_value(order_count_info_.c_str(), order_count_info_.size()); + members["orderCountInfo"] = order_count_info_v; + } +} + +void PopMessageResponseHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("popTime")); + ASSERT(members.contains("invisibleTime")); + ASSERT(members.contains("reviveQid")); + ASSERT(members.contains("restNum")); + + if (members.at("popTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + pop_time_ = members.at("popTime").number_value(); + } else { + pop_time_ = std::stoull(members.at("popTime").string_value()); + } + + if (members.at("invisibleTime").kind_case() == ProtobufWkt::Value::kNumberValue) { + invisible_time_ = members.at("invisibleTime").number_value(); + } else { + invisible_time_ = std::stoull(members.at("invisibleTime").string_value()); + } + + if (members.at("reviveQid").kind_case() == ProtobufWkt::Value::kNumberValue) { + revive_qid_ = members.at("reviveQid").number_value(); + } else { + revive_qid_ = std::stoul(members.at("reviveQid").string_value()); + } + + if (members.at("restNum").kind_case() == ProtobufWkt::Value::kNumberValue) { + rest_num_ = members.at("restNum").number_value(); + } else { + rest_num_ = std::stoull(members.at("restNum").string_value()); + } + + if (members.contains("startOffsetInfo")) { + start_offset_info_ = members.at("startOffsetInfo").string_value(); + } + + if (members.contains("msgOffsetInfo")) { + msg_off_set_info_ = members.at("msgOffsetInfo").string_value(); + } + + if (members.contains("orderCountInfo")) { + order_count_info_ = members.at("orderCountInfo").string_value(); + } +} + +void AckMessageRequestHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ASSERT(!consumer_group_.empty()); + ProtobufWkt::Value consumer_group_v; + consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); + members["consumerGroup"] = consumer_group_v; + + ASSERT(!topic_.empty()); + ProtobufWkt::Value topic_v; + topic_v.set_string_value(topic_.c_str(), topic_.size()); + members["topic"] = topic_v; + + ASSERT(queue_id_ >= 0); + ProtobufWkt::Value queue_id_v; + queue_id_v.set_number_value(queue_id_); + members["queueId"] = queue_id_v; + + ASSERT(!extra_info_.empty()); + ProtobufWkt::Value extra_info_v; + extra_info_v.set_string_value(extra_info_.c_str(), extra_info_.size()); + members["extraInfo"] = extra_info_v; + + ASSERT(offset_ >= 0); + ProtobufWkt::Value offset_v; + offset_v.set_number_value(offset_); + members["offset"] = offset_v; +} + +void AckMessageRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("consumerGroup")); + ASSERT(members.contains("topic")); + ASSERT(members.contains("queueId")); + ASSERT(members.contains("extraInfo")); + ASSERT(members.contains("offset")); + + consumer_group_ = members.at("consumerGroup").string_value(); + + topic_ = members.at("topic").string_value(); + + if (members.at("queueId").kind_case() == ProtobufWkt::Value::kNumberValue) { + queue_id_ = members.at("queueId").number_value(); + } else { + queue_id_ = std::stoi(members.at("queueId").string_value()); + } + + extra_info_ = members.at("extraInfo").string_value(); + + if (members.at("offset").kind_case() == ProtobufWkt::Value::kNumberValue) { + offset_ = members.at("offset").number_value(); + } else { + offset_ = std::stoll(members.at("offset").string_value()); + } +} + +void UnregisterClientRequestHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ASSERT(!client_id_.empty()); + ProtobufWkt::Value client_id_v; + client_id_v.set_string_value(client_id_.c_str(), client_id_.size()); + members["clientID"] = client_id_v; + + ASSERT(!producer_group_.empty() || !consumer_group_.empty()); + if (!producer_group_.empty()) { + ProtobufWkt::Value producer_group_v; + producer_group_v.set_string_value(producer_group_.c_str(), producer_group_.size()); + members["producerGroup"] = producer_group_v; + } + + if (!consumer_group_.empty()) { + ProtobufWkt::Value consumer_group_v; + consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); + members["consumerGroup"] = consumer_group_v; + } +} + +void UnregisterClientRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("clientID")); + ASSERT(members.contains("producerGroup") || members.contains("consumerGroup")); + + client_id_ = members.at("clientID").string_value(); + + if (members.contains("consumerGroup")) { + consumer_group_ = members.at("consumerGroup").string_value(); + } + + if (members.contains("producerGroup")) { + producer_group_ = members.at("producerGroup").string_value(); + } +} + +void GetConsumerListByGroupResponseBody::encode(ProtobufWkt::Struct& root) { + auto& members = *(root.mutable_fields()); + + ProtobufWkt::Value consumer_id_list_v; + auto member_list = consumer_id_list_v.mutable_list_value(); + for (const auto& consumerId : consumer_id_list_) { + auto consumer_id_v = new ProtobufWkt::Value; + consumer_id_v->set_string_value(consumerId.c_str(), consumerId.size()); + member_list->mutable_values()->AddAllocated(consumer_id_v); + } + members["consumerIdList"] = consumer_id_list_v; +} + +bool HeartbeatData::decode(ProtobufWkt::Struct& doc) { + const auto& members = doc.fields(); + if (!members.contains("clientID")) { + return false; + } + + client_id_ = members.at("clientID").string_value(); + + if (members.contains("consumerDataSet")) { + auto& consumer_data_list = members.at("consumerDataSet").list_value().values(); + for (const auto& it : consumer_data_list) { + if (it.struct_value().fields().contains("groupName")) { + consumer_groups_.push_back(it.struct_value().fields().at("groupName").string_value()); + } + } + } + return true; +} + +void HeartbeatData::encode(ProtobufWkt::Struct& root) { + auto& members = *(root.mutable_fields()); + + ProtobufWkt::Value client_id_v; + client_id_v.set_string_value(client_id_.c_str(), client_id_.size()); + members["clientID"] = client_id_v; +} + +void GetConsumerListByGroupRequestHeader::encode(ProtobufWkt::Value& root) { + auto& members = *(root.mutable_struct_value()->mutable_fields()); + + ProtobufWkt::Value consumer_group_v; + consumer_group_v.set_string_value(consumer_group_.c_str(), consumer_group_.size()); + members["consumerGroup"] = consumer_group_v; +} + +void GetConsumerListByGroupRequestHeader::decode(const ProtobufWkt::Value& ext_fields) { + const auto& members = ext_fields.struct_value().fields(); + ASSERT(members.contains("consumerGroup")); + + consumer_group_ = members.at("consumerGroup").string_value(); +} + +void MetadataHelper::parseRequest(RemotingCommandPtr& request, MessageMetadataSharedPtr metadata) { + metadata->setOneWay(request->isOneWay()); + CommandCustomHeader* custom_header = request->customHeader(); + + auto route_command_custom_header = request->typedCustomHeader(); + if (route_command_custom_header != nullptr) { + metadata->setTopicName(route_command_custom_header->topic()); + } + + const uint64_t code = request->code(); + metadata->headers().addCopy(Http::LowerCaseString("code"), code); + + if (enumToInt(RequestCode::AckMessage) == code) { + metadata->headers().addCopy(Http::LowerCaseString(RocketmqConstants::get().BrokerName), + custom_header->targetBrokerName()); + metadata->headers().addCopy(Http::LowerCaseString(RocketmqConstants::get().BrokerId), + custom_header->targetBrokerId()); + } +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/protocol.h b/source/extensions/filters/network/rocketmq_proxy/protocol.h new file mode 100644 index 000000000000..aa9c213bbc89 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/protocol.h @@ -0,0 +1,672 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/common/time.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/logger.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/filters/network/rocketmq_proxy/metadata.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +/** + * Retry topic prefix + */ +constexpr absl::string_view RetryTopicPrefix = "%RETRY%"; + +/** + * RocketMQ supports two versions of sending message protocol. These two versions are identical in + * terms of functionality. But they do differ in encoding scheme. See SendMessageRequestHeader + * encode/decode functions for specific differences. + */ +enum class SendMessageRequestVersion : uint32_t { + V1 = 0, + V2 = 1, + // Only for test purpose + V3 = 2, +}; + +/** + * Command custom header are used in combination with RemotingCommand::code, to provide further + * instructions and data for the operation defined by the protocol. + * In addition to the shared encode/decode functions, this class also defines target-broker-name and + * target-broker-id fields, which are helpful if the associated remoting command should be delivered + * to specific host according to the semantics of the previous command. + */ +class CommandCustomHeader { +public: + CommandCustomHeader() = default; + + virtual ~CommandCustomHeader() = default; + + virtual void encode(ProtobufWkt::Value& root) PURE; + + virtual void decode(const ProtobufWkt::Value& ext_fields) PURE; + + const std::string& targetBrokerName() const { return target_broker_name_; } + + void targetBrokerName(absl::string_view broker_name) { + target_broker_name_ = std::string(broker_name.data(), broker_name.length()); + } + + int32_t targetBrokerId() const { return target_broker_id_; } + + void targetBrokerId(int32_t broker_id) { target_broker_id_ = broker_id; } + +protected: + /** + * If this field is not empty, RDS will employ this field and target-broker-id to direct the + * associated request to a subset of the chosen cluster. + */ + std::string target_broker_name_; + + /** + * Used along with target-broker-name field. + */ + int32_t target_broker_id_; +}; + +using CommandCustomHeaderPtr = CommandCustomHeader*; + +/** + * This class extends from CommandCustomHeader, adding a commonly used field by various custom + * command headers which participate the process of request routing. + */ +class RoutingCommandCustomHeader : public CommandCustomHeader { +public: + virtual const std::string& topic() const { return topic_; } + + virtual void topic(absl::string_view t) { topic_ = std::string(t.data(), t.size()); } + +protected: + std::string topic_; +}; + +/** + * This class defines basic request/response forms used by RocketMQ among all its components. + */ +class RemotingCommand { +public: + RemotingCommand() : RemotingCommand(0, 0, 0) {} + + RemotingCommand(int code, int version, int opaque) + : code_(code), version_(version), opaque_(opaque), flag_(0) {} + + ~RemotingCommand() { delete custom_header_; } + + int32_t code() const { return code_; } + + void code(int code) { code_ = code; } + + const std::string& language() const { return language_; } + + void language(absl::string_view lang) { language_ = std::string(lang.data(), lang.size()); } + + int32_t version() const { return version_; } + + void opaque(int opaque) { opaque_ = opaque; } + + int32_t opaque() const { return opaque_; } + + uint32_t flag() const { return flag_; } + + void flag(uint32_t f) { flag_ = f; } + + void customHeader(CommandCustomHeaderPtr custom_header) { custom_header_ = custom_header; } + + CommandCustomHeaderPtr customHeader() const { return custom_header_; } + + template T* typedCustomHeader() { + if (!custom_header_) { + return nullptr; + } + + return dynamic_cast(custom_header_); + } + + uint32_t bodyLength() const { return body_.length(); } + + Buffer::Instance& body() { return body_; } + + const std::string& remark() const { return remark_; } + + void remark(absl::string_view remark) { remark_ = std::string(remark.data(), remark.length()); } + + const std::string& serializeTypeCurrentRPC() const { return serialize_type_current_rpc_; } + + void serializeTypeCurrentRPC(absl::string_view serialization_type) { + serialize_type_current_rpc_ = std::string(serialization_type.data(), serialization_type.size()); + } + + bool isOneWay() const { + uint32_t marker = 1u << SHIFT_ONEWAY; + return (flag_ & marker) == marker; + } + + void markAsResponse() { flag_ |= (1u << SHIFT_RPC); } + + void markAsOneway() { flag_ |= (1u << SHIFT_ONEWAY); } + + static bool isResponse(uint32_t flag) { return (flag & (1u << SHIFT_RPC)) == (1u << SHIFT_RPC); } + +private: + /** + * Action code of this command. Possible values are defined in RequestCode enumeration. + */ + int32_t code_; + + /** + * Language used by the client. + */ + std::string language_{"CPP"}; + + /** + * Version of the client SDK. + */ + int32_t version_; + + /** + * Request ID. If the RPC is request-response form, this field is used to establish the + * association. + */ + int32_t opaque_; + + /** + * Bit-wise flag indicating RPC type, including whether it is one-way or request-response; + * a request or response command. + */ + uint32_t flag_; + + /** + * Remark is used to deliver text message in addition to code. Urgent scenarios may use this field + * to transfer diagnostic message to the counterparts when a full-fledged response is impossible. + */ + std::string remark_; + + /** + * Indicate how the custom command header is serialized. + */ + std::string serialize_type_current_rpc_{"JSON"}; + + /** + * The custom command header works with command code to provide additional protocol + * implementation. + * Generally speaking, each code has pair of request/response custom command header. + */ + CommandCustomHeaderPtr custom_header_{nullptr}; + + /** + * The command body, in form of binary. + */ + Buffer::OwnedImpl body_; + + static constexpr uint32_t SHIFT_RPC = 0; + + static constexpr uint32_t SHIFT_ONEWAY = 1; + + friend class Encoder; + friend class Decoder; +}; + +using RemotingCommandPtr = std::unique_ptr; + +/** + * Command codes used when sending requests. Meaning of each field is self-explanatory. + */ +enum class RequestCode : uint32_t { + SendMessage = 10, + HeartBeat = 34, + UnregisterClient = 35, + GetConsumerListByGroup = 38, + PopMessage = 50, + AckMessage = 51, + GetRouteInfoByTopic = 105, + SendMessageV2 = 310, + // Only for test purpose + Unsupported = 999, +}; + +/** + * Command code used when sending responses. Meaning of each enum is self-explanatory. + */ +enum class ResponseCode : uint32_t { + Success = 0, + SystemError = 1, + SystemBusy = 2, + RequestCodeNotSupported = 3, + SlaveNotAvailable = 11, +}; + +/** + * Custom command header for sending messages. + */ +class SendMessageRequestHeader : public RoutingCommandCustomHeader, + Logger::Loggable { +public: + ~SendMessageRequestHeader() override = default; + + int32_t queueId() const { return queue_id_; } + + /** + * TODO(lizhanhui): Remove this write API after adding queue-id-aware route logic + * @param queue_id target queue Id. + */ + void queueId(int32_t queue_id) { queue_id_ = queue_id; } + + void producerGroup(std::string producer_group) { producer_group_ = std::move(producer_group); } + + void encode(ProtobufWkt::Value& root) override; + + void decode(const ProtobufWkt::Value& ext_fields) override; + + const std::string& producerGroup() const { return producer_group_; } + + const std::string& defaultTopic() const { return default_topic_; } + + int32_t defaultTopicQueueNumber() const { return default_topic_queue_number_; } + + int32_t sysFlag() const { return sys_flag_; } + + int32_t flag() const { return flag_; } + + int64_t bornTimestamp() const { return born_timestamp_; } + + const std::string& properties() const { return properties_; } + + int32_t reconsumeTimes() const { return reconsume_time_; } + + bool unitMode() const { return unit_mode_; } + + bool batch() const { return batch_; } + + int32_t maxReconsumeTimes() const { return max_reconsume_time_; } + + void properties(absl::string_view props) { + properties_ = std::string(props.data(), props.size()); + } + + void reconsumeTimes(int32_t reconsume_times) { reconsume_time_ = reconsume_times; } + + void unitMode(bool unit_mode) { unit_mode_ = unit_mode; } + + void batch(bool batch) { batch_ = batch; } + + void maxReconsumeTimes(int32_t max_reconsume_times) { max_reconsume_time_ = max_reconsume_times; } + + void version(SendMessageRequestVersion version) { version_ = version; } + + SendMessageRequestVersion version() const { return version_; } + +private: + std::string producer_group_; + std::string default_topic_; + int32_t default_topic_queue_number_{0}; + int32_t queue_id_{-1}; + int32_t sys_flag_{0}; + int64_t born_timestamp_{0}; + int32_t flag_{0}; + std::string properties_; + int32_t reconsume_time_{0}; + bool unit_mode_{false}; + bool batch_{false}; + int32_t max_reconsume_time_{0}; + SendMessageRequestVersion version_{SendMessageRequestVersion::V1}; + + friend class Decoder; +}; + +/** + * Custom command header to respond to a send-message-request. + */ +class SendMessageResponseHeader : public CommandCustomHeader { +public: + SendMessageResponseHeader() = default; + + SendMessageResponseHeader(std::string msg_id, int32_t queue_id, int64_t queue_offset, + std::string transaction_id) + : msg_id_(std::move(msg_id)), queue_id_(queue_id), queue_offset_(queue_offset), + transaction_id_(std::move(transaction_id)) {} + + void encode(ProtobufWkt::Value& root) override; + + void decode(const ProtobufWkt::Value& ext_fields) override; + + const std::string& msgId() const { return msg_id_; } + + int32_t queueId() const { return queue_id_; } + + int64_t queueOffset() const { return queue_offset_; } + + const std::string& transactionId() const { return transaction_id_; } + + // This function is for testing only. + void msgIdForTest(absl::string_view msg_id) { + msg_id_ = std::string(msg_id.data(), msg_id.size()); + } + + void queueId(int32_t queue_id) { queue_id_ = queue_id; } + + void queueOffset(int64_t queue_offset) { queue_offset_ = queue_offset; } + + void transactionId(absl::string_view transaction_id) { + transaction_id_ = std::string(transaction_id.data(), transaction_id.size()); + } + +private: + std::string msg_id_; + int32_t queue_id_{0}; + int64_t queue_offset_{0}; + std::string transaction_id_; +}; + +/** + * Classic RocketMQ needs to known addresses of each broker to work with. To resolve the addresses, + * client SDK uses this command header to query name servers. + * + * This header is kept for compatible purpose only. + */ +class GetRouteInfoRequestHeader : public RoutingCommandCustomHeader { +public: + void encode(ProtobufWkt::Value& root) override; + + void decode(const ProtobufWkt::Value& ext_fields) override; +}; + +/** + * When a client wishes to consume messages stored in brokers, it sends a pop command to brokers. + * Brokers would send a batch of messages to the client. At the same time, the broker keeps the + * batch invisible for a configured period of time, waiting for acknowledgments from the client. + * + * If the client manages to consume the messages within promised time interval and sends ack command + * back to the broker, the broker will mark the acknowledged ones as consumed. Otherwise, the + * previously sent messages are visible again and would be consumable for other client instances. + * + * Through this approach, we achieves stateless message-pulling, comparing to classic offset-based + * consuming progress management. This models brings about some extra workload to broker side, but + * it fits Envoy well. + */ +class PopMessageRequestHeader : public RoutingCommandCustomHeader { +public: + friend class Decoder; + + void encode(ProtobufWkt::Value& root) override; + + void decode(const ProtobufWkt::Value& ext_fields) override; + + const std::string& consumerGroup() const { return consumer_group_; } + + void consumerGroup(absl::string_view consumer_group) { + consumer_group_ = std::string(consumer_group.data(), consumer_group.size()); + } + + int32_t queueId() const { return queue_id_; } + + void queueId(int32_t queue_id) { queue_id_ = queue_id; } + + int32_t maxMsgNum() const { return max_msg_nums_; } + + void maxMsgNum(int32_t max_msg_num) { max_msg_nums_ = max_msg_num; } + + int64_t invisibleTime() const { return invisible_time_; } + + void invisibleTime(int64_t invisible_time) { invisible_time_ = invisible_time; } + + int64_t pollTime() const { return poll_time_; } + + void pollTime(int64_t poll_time) { poll_time_ = poll_time; } + + int64_t bornTime() const { return born_time_; } + + void bornTime(int64_t born_time) { born_time_ = born_time; } + + int32_t initMode() const { return init_mode_; } + + void initMode(int32_t init_mode) { init_mode_ = init_mode; } + + const std::string& expType() const { return exp_type_; } + + void expType(absl::string_view exp_type) { + exp_type_ = std::string(exp_type.data(), exp_type.size()); + } + + const std::string& exp() const { return exp_; } + + void exp(absl::string_view exp) { exp_ = std::string(exp.data(), exp.size()); } + +private: + std::string consumer_group_; + int32_t queue_id_{-1}; + int32_t max_msg_nums_{32}; + int64_t invisible_time_{0}; + int64_t poll_time_{0}; + int64_t born_time_{0}; + int32_t init_mode_{0}; + std::string exp_type_; + std::string exp_; + bool order_{false}; +}; + +/** + * The pop response command header. See pop request header for how-things-work explanation. + */ +class PopMessageResponseHeader : public CommandCustomHeader { +public: + void decode(const ProtobufWkt::Value& ext_fields) override; + + void encode(ProtobufWkt::Value& root) override; + + // This function is for testing only. + int64_t popTimeForTest() const { return pop_time_; } + + void popTime(int64_t pop_time) { pop_time_ = pop_time; } + + int64_t invisibleTime() const { return invisible_time_; } + + void invisibleTime(int64_t invisible_time) { invisible_time_ = invisible_time; } + + int32_t reviveQid() const { return revive_qid_; } + + void reviveQid(int32_t revive_qid) { revive_qid_ = revive_qid; } + + int64_t restNum() const { return rest_num_; } + + void restNum(int64_t rest_num) { rest_num_ = rest_num; } + + const std::string& startOffsetInfo() const { return start_offset_info_; } + + void startOffsetInfo(absl::string_view start_offset_info) { + start_offset_info_ = std::string(start_offset_info.data(), start_offset_info.size()); + } + + const std::string& msgOffsetInfo() const { return msg_off_set_info_; } + + void msgOffsetInfo(absl::string_view msg_offset_info) { + msg_off_set_info_ = std::string(msg_offset_info.data(), msg_offset_info.size()); + } + + const std::string& orderCountInfo() const { return order_count_info_; } + + void orderCountInfo(absl::string_view order_count_info) { + order_count_info_ = std::string(order_count_info.data(), order_count_info.size()); + } + +private: + int64_t pop_time_{0}; + int64_t invisible_time_{0}; + int32_t revive_qid_{0}; + int64_t rest_num_{0}; + std::string start_offset_info_; + std::string msg_off_set_info_; + std::string order_count_info_; +}; + +/** + * This command is used by the client to acknowledge message(s) that has been successfully consumed. + * Once the broker received this request, the associated message will formally marked as consumed. + * + * Note: the ack request has to be sent the exactly same broker where messages are popped from. + */ +class AckMessageRequestHeader : public RoutingCommandCustomHeader { +public: + void decode(const ProtobufWkt::Value& ext_fields) override; + + void encode(ProtobufWkt::Value& root) override; + + absl::string_view consumerGroup() const { return consumer_group_; } + + int64_t offset() const { return offset_; } + + void consumerGroup(absl::string_view consumer_group) { + consumer_group_ = std::string(consumer_group.data(), consumer_group.size()); + } + + int32_t queueId() const { return queue_id_; } + void queueId(int32_t queue_id) { queue_id_ = queue_id; } + + absl::string_view extraInfo() const { return extra_info_; } + void extraInfo(absl::string_view extra_info) { + extra_info_ = std::string(extra_info.data(), extra_info.size()); + } + + void offset(int64_t offset) { offset_ = offset; } + + const std::string& directiveKey() { + if (key_.empty()) { + key_ = fmt::format("{}-{}-{}-{}", consumer_group_, topic_, queue_id_, offset_); + } + return key_; + } + +private: + std::string consumer_group_; + int32_t queue_id_{0}; + std::string extra_info_; + int64_t offset_{0}; + std::string key_; +}; + +/** + * When a client shuts down gracefully, it notifies broker(now envoy) this event. + */ +class UnregisterClientRequestHeader : public CommandCustomHeader { +public: + void encode(ProtobufWkt::Value& root) override; + + void decode(const ProtobufWkt::Value& ext_fields) override; + + void clientId(absl::string_view client_id) { + client_id_ = std::string(client_id.data(), client_id.length()); + } + + const std::string& clientId() const { return client_id_; } + + void producerGroup(absl::string_view producer_group) { + producer_group_ = std::string(producer_group.data(), producer_group.length()); + } + + const std::string& producerGroup() const { return producer_group_; } + + void consumerGroup(absl::string_view consumer_group) { + consumer_group_ = std::string(consumer_group.data(), consumer_group.length()); + } + + const std::string& consumerGroup() const { return consumer_group_; } + +private: + std::string client_id_; + std::string producer_group_; + std::string consumer_group_; +}; + +/** + * Classic SDK clients use client-side load balancing. This header is kept for compatibility. + */ +class GetConsumerListByGroupRequestHeader : public CommandCustomHeader { +public: + void encode(ProtobufWkt::Value& root) override; + + void decode(const ProtobufWkt::Value& ext_fields) override; + + void consumerGroup(absl::string_view consumer_group) { + consumer_group_ = std::string(consumer_group.data(), consumer_group.length()); + } + + const std::string& consumerGroup() const { return consumer_group_; } + +private: + std::string consumer_group_; +}; + +/** + * The response body. + */ +class GetConsumerListByGroupResponseBody { +public: + void encode(ProtobufWkt::Struct& root); + + void add(absl::string_view consumer_id) { + consumer_id_list_.emplace_back(std::string(consumer_id.data(), consumer_id.length())); + } + +private: + std::vector consumer_id_list_; +}; + +/** + * Client periodically sends heartbeat to servers to maintain alive status. + */ +class HeartbeatData : public Logger::Loggable { +public: + bool decode(ProtobufWkt::Struct& doc); + + const std::string& clientId() const { return client_id_; } + + const std::vector& consumerGroups() const { return consumer_groups_; } + + void encode(ProtobufWkt::Struct& root); + + void clientId(absl::string_view client_id) { + client_id_ = std::string(client_id.data(), client_id.size()); + } + +private: + std::string client_id_; + std::vector consumer_groups_; +}; + +class MetadataHelper { +public: + MetadataHelper() = delete; + + static void parseRequest(RemotingCommandPtr& request, MessageMetadataSharedPtr metadata); +}; + +/** + * Directive to ensure entailing ack requests are routed to the same broker host where pop + * requests are made. + */ +struct AckMessageDirective { + + AckMessageDirective(absl::string_view broker_name, int32_t broker_id, MonotonicTime create_time) + : broker_name_(broker_name.data(), broker_name.length()), broker_id_(broker_id), + creation_time_(create_time) {} + + std::string broker_name_; + int32_t broker_id_; + MonotonicTime creation_time_; +}; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/router/BUILD b/source/extensions/filters/network/rocketmq_proxy/router/BUILD new file mode 100644 index 000000000000..19227abff64a --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/router/BUILD @@ -0,0 +1,50 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "router_interface", + hdrs = ["router.h"], + deps = [ + "//include/envoy/tcp:conn_pool_interface", + "//include/envoy/upstream:load_balancer_interface", + "//source/common/upstream:load_balancer_lib", + ], +) + +envoy_cc_library( + name = "router_lib", + srcs = ["router_impl.cc"], + hdrs = ["router_impl.h"], + deps = [ + ":router_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//include/envoy/upstream:thread_local_cluster_interface", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/rocketmq_proxy:conn_manager_lib", + ], +) + +envoy_cc_library( + name = "route_matcher", + srcs = ["route_matcher.cc"], + hdrs = ["route_matcher.h"], + deps = [ + ":router_interface", + "//include/envoy/config:typed_config_interface", + "//include/envoy/server:filter_config_interface", + "//source/common/common:logger_lib", + "//source/common/common:matchers_lib", + "//source/common/http:header_utility_lib", + "//source/common/router:metadatamatchcriteria_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/rocketmq_proxy:metadata_lib", + "@envoy_api//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/network/rocketmq_proxy/router/route_matcher.cc b/source/extensions/filters/network/rocketmq_proxy/router/route_matcher.cc new file mode 100644 index 000000000000..e99d6c249ebb --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/router/route_matcher.cc @@ -0,0 +1,73 @@ +#include "extensions/filters/network/rocketmq_proxy/router/route_matcher.h" + +#include "common/router/metadatamatchcriteria_impl.h" + +#include "extensions/filters/network/rocketmq_proxy/metadata.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { +namespace Router { + +RouteEntryImpl::RouteEntryImpl( + const envoy::extensions::filters::network::rocketmq_proxy::v3::Route& route) + : topic_name_(route.match().topic()), cluster_name_(route.route().cluster()), + config_headers_(Http::HeaderUtility::buildHeaderDataVector(route.match().headers())) { + + if (route.route().has_metadata_match()) { + const auto filter_it = route.route().metadata_match().filter_metadata().find( + Envoy::Config::MetadataFilters::get().ENVOY_LB); + if (filter_it != route.route().metadata_match().filter_metadata().end()) { + metadata_match_criteria_ = + std::make_unique(filter_it->second); + } + } +} + +const std::string& RouteEntryImpl::clusterName() const { return cluster_name_; } + +const RouteEntry* RouteEntryImpl::routeEntry() const { return this; } + +RouteConstSharedPtr RouteEntryImpl::matches(const MessageMetadata& metadata) const { + if (headersMatch(metadata.headers())) { + const std::string& topic_name = metadata.topicName(); + if (topic_name_.match(topic_name)) { + return shared_from_this(); + } + } + return nullptr; +} + +bool RouteEntryImpl::headersMatch(const Http::HeaderMap& headers) const { + ENVOY_LOG(debug, "rocketmq route matcher: headers size {}, metadata headers size {}", + config_headers_.size(), headers.size()); + return Http::HeaderUtility::matchHeaders(headers, config_headers_); +} + +RouteMatcher::RouteMatcher(const RouteConfig& config) { + for (const auto& route : config.routes()) { + routes_.emplace_back(std::make_shared(route)); + } + ENVOY_LOG(debug, "rocketmq route matcher: routes list size {}", routes_.size()); +} + +RouteConstSharedPtr RouteMatcher::route(const MessageMetadata& metadata) const { + const std::string& topic_name = metadata.topicName(); + for (const auto& route : routes_) { + RouteConstSharedPtr route_entry = route->matches(metadata); + if (nullptr != route_entry) { + ENVOY_LOG(debug, "rocketmq route matcher: find cluster success for topic: {}", topic_name); + return route_entry; + } + } + ENVOY_LOG(debug, "rocketmq route matcher: find cluster failed for topic: {}", topic_name); + return nullptr; +} + +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/router/route_matcher.h b/source/extensions/filters/network/rocketmq_proxy/router/route_matcher.h new file mode 100644 index 000000000000..8cd4c533a541 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/router/route_matcher.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +#include "envoy/config/typed_config.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/route.pb.h" +#include "envoy/server/filter_config.h" + +#include "common/common/logger.h" +#include "common/common/matchers.h" +#include "common/http/header_utility.h" + +#include "extensions/filters/network/rocketmq_proxy/router/router.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class MessageMetadata; + +namespace Router { + +class RouteEntryImpl : public RouteEntry, + public Route, + public std::enable_shared_from_this, + public Logger::Loggable { +public: + RouteEntryImpl(const envoy::extensions::filters::network::rocketmq_proxy::v3::Route& route); + ~RouteEntryImpl() override = default; + + // Router::RouteEntry + const std::string& clusterName() const override; + const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() const override { + return metadata_match_criteria_.get(); + } + + // Router::Route + const RouteEntry* routeEntry() const override; + + RouteConstSharedPtr matches(const MessageMetadata& metadata) const; + +private: + bool headersMatch(const Http::HeaderMap& headers) const; + + const Matchers::StringMatcherImpl topic_name_; + const std::string cluster_name_; + const std::vector config_headers_; + Envoy::Router::MetadataMatchCriteriaConstPtr metadata_match_criteria_; +}; + +using RouteEntryImplConstSharedPtr = std::shared_ptr; + +class RouteMatcher : public Logger::Loggable { +public: + using RouteConfig = envoy::extensions::filters::network::rocketmq_proxy::v3::RouteConfiguration; + RouteMatcher(const RouteConfig& config); + + RouteConstSharedPtr route(const MessageMetadata& metadata) const; + +private: + std::vector routes_; +}; + +using RouteMatcherPtr = std::unique_ptr; + +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/router/router.h b/source/extensions/filters/network/rocketmq_proxy/router/router.h new file mode 100644 index 000000000000..6067a7295fc6 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/router/router.h @@ -0,0 +1,85 @@ +#pragma once + +#include "envoy/tcp/conn_pool.h" + +#include "common/upstream/load_balancer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class ActiveMessage; +class MessageMetadata; + +namespace Router { + +/** + * RouteEntry is an individual resolved route entry. + */ +class RouteEntry { +public: + virtual ~RouteEntry() = default; + + /** + * @return const std::string& the upstream cluster that owns the route. + */ + virtual const std::string& clusterName() const PURE; + + /** + * @return MetadataMatchCriteria* the metadata that a subset load balancer should match when + * selecting an upstream host + */ + virtual const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() const PURE; +}; + +/** + * Route holds the RouteEntry for a request. + */ +class Route { +public: + virtual ~Route() = default; + + /** + * @return the route entry or nullptr if there is no matching route for the request. + */ + virtual const RouteEntry* routeEntry() const PURE; +}; + +using RouteConstSharedPtr = std::shared_ptr; +using RouteSharedPtr = std::shared_ptr; + +/** + * The router configuration. + */ +class Config { +public: + virtual ~Config() = default; + + virtual RouteConstSharedPtr route(const MessageMetadata& metadata) const PURE; +}; + +class Router : public Tcp::ConnectionPool::UpstreamCallbacks, + public Upstream::LoadBalancerContextBase { + +public: + virtual void sendRequestToUpstream(ActiveMessage& active_message) PURE; + + /** + * Release resources associated with this router. + */ + virtual void reset() PURE; + + /** + * Return host description that is eventually connected. + * @return upstream host if a connection has been established; nullptr otherwise. + */ + virtual Upstream::HostDescriptionConstSharedPtr upstreamHost() PURE; +}; + +using RouterPtr = std::unique_ptr; +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/router/router_impl.cc b/source/extensions/filters/network/rocketmq_proxy/router/router_impl.cc new file mode 100644 index 000000000000..425eeec687c2 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/router/router_impl.cc @@ -0,0 +1,218 @@ +#include "extensions/filters/network/rocketmq_proxy/router/router_impl.h" + +#include "common/common/enum_to_int.h" + +#include "extensions/filters/network/rocketmq_proxy/active_message.h" +#include "extensions/filters/network/rocketmq_proxy/codec.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/protocol.h" +#include "extensions/filters/network/rocketmq_proxy/well_known_names.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { +namespace Router { + +RouterImpl::RouterImpl(Envoy::Upstream::ClusterManager& cluster_manager) + : cluster_manager_(cluster_manager), handle_(nullptr), active_message_(nullptr) {} + +RouterImpl::~RouterImpl() { + if (handle_) { + handle_->cancel(Tcp::ConnectionPool::CancelPolicy::Default); + } +} + +Upstream::HostDescriptionConstSharedPtr RouterImpl::upstreamHost() { return upstream_host_; } + +void RouterImpl::onAboveWriteBufferHighWatermark() { + ENVOY_LOG(trace, "Above write buffer high watermark"); +} + +void RouterImpl::onBelowWriteBufferLowWatermark() { + ENVOY_LOG(trace, "Below write buffer low watermark"); +} + +void RouterImpl::onEvent(Network::ConnectionEvent event) { + switch (event) { + case Network::ConnectionEvent::RemoteClose: { + ENVOY_LOG(error, "Connection to upstream: {} is closed by remote peer", + upstream_host_->address()->asString()); + // Send local reply to downstream + active_message_->onError("Connection to upstream is closed by remote peer"); + break; + } + case Network::ConnectionEvent::LocalClose: { + ENVOY_LOG(error, "Connection to upstream: {} has been closed", + upstream_host_->address()->asString()); + // Send local reply to downstream + active_message_->onError("Connection to upstream has been closed"); + break; + } + default: + // Ignore other events for now + ENVOY_LOG(trace, "Ignore event type"); + return; + } + active_message_->onReset(); +} + +const Envoy::Router::MetadataMatchCriteria* RouterImpl::metadataMatchCriteria() { + if (route_entry_) { + return route_entry_->metadataMatchCriteria(); + } + return nullptr; +} + +void RouterImpl::onUpstreamData(Buffer::Instance& data, bool end_stream) { + ENVOY_LOG(trace, "Received some data from upstream: {} bytes, end_stream: {}", data.length(), + end_stream); + if (active_message_->onUpstreamData(data, end_stream, connection_data_)) { + reset(); + } +} + +void RouterImpl::sendRequestToUpstream(ActiveMessage& active_message) { + active_message_ = &active_message; + int opaque = active_message_->downstreamRequest()->opaque(); + ASSERT(active_message_->metadata()->hasTopicName()); + std::string topic_name = active_message_->metadata()->topicName(); + + RouteConstSharedPtr route = active_message.route(); + if (!route) { + active_message.onError("No route for current request."); + ENVOY_LOG(warn, "Can not find route for topic {}", topic_name); + reset(); + return; + } + + route_entry_ = route->routeEntry(); + const std::string cluster_name = route_entry_->clusterName(); + Upstream::ThreadLocalCluster* cluster = cluster_manager_.get(cluster_name); + if (!cluster) { + active_message.onError("Cluster does not exist."); + ENVOY_LOG(warn, "Cluster for {} is not available", cluster_name); + reset(); + return; + } + + cluster_info_ = cluster->info(); + if (cluster_info_->maintenanceMode()) { + ENVOY_LOG(warn, "Cluster {} is under maintenance. Opaque: {}", cluster_name, opaque); + active_message.onError("Cluster under maintenance."); + active_message.connectionManager().stats().maintenance_failure_.inc(); + reset(); + return; + } + + Tcp::ConnectionPool::Instance* conn_pool = cluster_manager_.tcpConnPoolForCluster( + cluster_name, Upstream::ResourcePriority::Default, this); + if (!conn_pool) { + ENVOY_LOG(warn, "No host available for cluster {}. Opaque: {}", cluster_name, opaque); + active_message.onError("No host available"); + reset(); + return; + } + + upstream_request_ = std::make_unique(*this); + Tcp::ConnectionPool::Cancellable* cancellable = conn_pool->newConnection(*upstream_request_); + if (cancellable) { + handle_ = cancellable; + ENVOY_LOG(trace, "No connection is available for now. Create a cancellable handle. Opaque: {}", + opaque); + } else { + /* + * UpstreamRequest#onPoolReady or #onPoolFailure should have been invoked. + */ + ENVOY_LOG(trace, + "One connection is picked up from connection pool, callback should have been " + "executed. Opaque: {}", + opaque); + } +} + +RouterImpl::UpstreamRequest::UpstreamRequest(RouterImpl& router) : router_(router) {} + +void RouterImpl::UpstreamRequest::onPoolReady(Tcp::ConnectionPool::ConnectionDataPtr&& conn, + Upstream::HostDescriptionConstSharedPtr host) { + router_.connection_data_ = std::move(conn); + router_.upstream_host_ = host; + router_.connection_data_->addUpstreamCallbacks(router_); + if (router_.handle_) { + ENVOY_LOG(trace, "#onPoolReady, reset cancellable handle to nullptr"); + router_.handle_ = nullptr; + } + ENVOY_LOG(debug, "Current chosen host address: {}", host->address()->asString()); + // TODO(lizhanhui): we may optimize out encoding in case we there is no protocol translation. + Buffer::OwnedImpl buffer; + Encoder::encode(router_.active_message_->downstreamRequest(), buffer); + router_.connection_data_->connection().write(buffer, false); + ENVOY_LOG(trace, "Write data to upstream OK. Opaque: {}", + router_.active_message_->downstreamRequest()->opaque()); + + if (router_.active_message_->metadata()->isOneWay()) { + ENVOY_LOG(trace, + "Reset ActiveMessage since data is written and the downstream request is one-way. " + "Opaque: {}", + router_.active_message_->downstreamRequest()->opaque()); + + // For one-way ack-message requests, we need erase previously stored ack-directive. + if (enumToSignedInt(RequestCode::AckMessage) == + router_.active_message_->downstreamRequest()->code()) { + auto ack_header = router_.active_message_->downstreamRequest() + ->typedCustomHeader(); + router_.active_message_->connectionManager().eraseAckDirective(ack_header->directiveKey()); + } + + router_.reset(); + } +} + +void RouterImpl::UpstreamRequest::onPoolFailure(Tcp::ConnectionPool::PoolFailureReason reason, + Upstream::HostDescriptionConstSharedPtr host) { + if (router_.handle_) { + ENVOY_LOG(trace, "#onPoolFailure, reset cancellable handle to nullptr"); + router_.handle_ = nullptr; + } + switch (reason) { + case Tcp::ConnectionPool::PoolFailureReason::Overflow: { + ENVOY_LOG(error, "Unable to acquire a connection to send request to upstream"); + router_.active_message_->onError("overflow"); + } break; + + case Tcp::ConnectionPool::PoolFailureReason::RemoteConnectionFailure: { + ENVOY_LOG(error, "Failed to make request to upstream due to remote connection error. Host {}", + host->address()->asString()); + router_.active_message_->onError("remote connection failure"); + } break; + + case Tcp::ConnectionPool::PoolFailureReason::LocalConnectionFailure: { + ENVOY_LOG(error, "Failed to make request to upstream due to local connection error. Host: {}", + host->address()->asString()); + router_.active_message_->onError("local connection failure"); + } break; + + case Tcp::ConnectionPool::PoolFailureReason::Timeout: { + ENVOY_LOG(error, "Failed to make request to upstream due to timeout. Host: {}", + host->address()->asString()); + router_.active_message_->onError("timeout"); + } break; + } + + // Release resources allocated to this request. + router_.reset(); +} + +void RouterImpl::reset() { + active_message_->onReset(); + if (connection_data_) { + connection_data_.reset(nullptr); + } +} + +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/router/router_impl.h b/source/extensions/filters/network/rocketmq_proxy/router/router_impl.h new file mode 100644 index 000000000000..b3eca29e1e67 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/router/router_impl.h @@ -0,0 +1,75 @@ +#pragma once + +#include "envoy/tcp/conn_pool.h" +#include "envoy/upstream/cluster_manager.h" +#include "envoy/upstream/thread_local_cluster.h" + +#include "common/common/logger.h" +#include "common/upstream/load_balancer_impl.h" + +#include "extensions/filters/network/rocketmq_proxy/router/router.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { +namespace Router { + +class RouterImpl : public Router, public Logger::Loggable { +public: + explicit RouterImpl(Upstream::ClusterManager& cluster_manager); + + ~RouterImpl() override; + + // Tcp::ConnectionPool::UpstreamCallbacks + void onUpstreamData(Buffer::Instance& data, bool end_stream) override; + void onAboveWriteBufferHighWatermark() override; + void onBelowWriteBufferLowWatermark() override; + void onEvent(Network::ConnectionEvent event) override; + + // Upstream::LoadBalancerContextBase + const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() override; + + void sendRequestToUpstream(ActiveMessage& active_message) override; + + void reset() override; + + Upstream::HostDescriptionConstSharedPtr upstreamHost() override; + +private: + class UpstreamRequest : public Tcp::ConnectionPool::Callbacks { + public: + UpstreamRequest(RouterImpl& router); + + void onPoolFailure(Tcp::ConnectionPool::PoolFailureReason reason, + Upstream::HostDescriptionConstSharedPtr host) override; + + void onPoolReady(Tcp::ConnectionPool::ConnectionDataPtr&& conn, + Upstream::HostDescriptionConstSharedPtr host) override; + + private: + RouterImpl& router_; + }; + using UpstreamRequestPtr = std::unique_ptr; + + Upstream::ClusterManager& cluster_manager_; + Tcp::ConnectionPool::ConnectionDataPtr connection_data_; + + /** + * On requesting connection from upstream connection pool, this handle may be assigned when no + * connection is readily available at the moment. We may cancel the request through this handle. + * + * If there are connections which can be returned immediately, this handle is assigned as nullptr. + */ + Tcp::ConnectionPool::Cancellable* handle_; + Upstream::HostDescriptionConstSharedPtr upstream_host_; + ActiveMessage* active_message_; + Upstream::ClusterInfoConstSharedPtr cluster_info_; + UpstreamRequestPtr upstream_request_; + const RouteEntry* route_entry_{}; +}; +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/stats.h b/source/extensions/filters/network/rocketmq_proxy/stats.h new file mode 100644 index 000000000000..13f3122b6eff --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/stats.h @@ -0,0 +1,62 @@ +#pragma once + +#include + +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +/** + * All rocketmq filter stats. @see stats_macros.h + */ +#define ALL_ROCKETMQ_FILTER_STATS(COUNTER, GAUGE, HISTOGRAM) \ + COUNTER(request) \ + COUNTER(request_decoding_error) \ + COUNTER(request_decoding_success) \ + COUNTER(response) \ + COUNTER(response_decoding_error) \ + COUNTER(response_decoding_success) \ + COUNTER(response_error) \ + COUNTER(response_success) \ + COUNTER(heartbeat) \ + COUNTER(unregister) \ + COUNTER(get_topic_route) \ + COUNTER(send_message_v1) \ + COUNTER(send_message_v2) \ + COUNTER(pop_message) \ + COUNTER(ack_message) \ + COUNTER(get_consumer_list) \ + COUNTER(maintenance_failure) \ + GAUGE(request_active, Accumulate) \ + GAUGE(send_message_v1_active, Accumulate) \ + GAUGE(send_message_v2_active, Accumulate) \ + GAUGE(pop_message_active, Accumulate) \ + GAUGE(get_topic_route_active, Accumulate) \ + GAUGE(send_message_pending, Accumulate) \ + GAUGE(pop_message_pending, Accumulate) \ + GAUGE(get_topic_route_pending, Accumulate) \ + GAUGE(total_pending, Accumulate) \ + HISTOGRAM(request_time_ms, Milliseconds) + +/** + * Struct definition for all rocketmq proxy stats. @see stats_macros.h + */ +struct RocketmqFilterStats { + ALL_ROCKETMQ_FILTER_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, + GENERATE_HISTOGRAM_STRUCT) + + static RocketmqFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) { + return RocketmqFilterStats{ALL_ROCKETMQ_FILTER_STATS(POOL_COUNTER_PREFIX(scope, prefix), + POOL_GAUGE_PREFIX(scope, prefix), + POOL_HISTOGRAM_PREFIX(scope, prefix))}; + } +}; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/rocketmq_proxy/topic_route.cc b/source/extensions/filters/network/rocketmq_proxy/topic_route.cc new file mode 100644 index 000000000000..8c445ab1c6f0 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/topic_route.cc @@ -0,0 +1,76 @@ +#include "extensions/filters/network/rocketmq_proxy/topic_route.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +void QueueData::encode(ProtobufWkt::Struct& data_struct) { + auto* fields = data_struct.mutable_fields(); + + ProtobufWkt::Value broker_name_v; + broker_name_v.set_string_value(broker_name_); + (*fields)["brokerName"] = broker_name_v; + + ProtobufWkt::Value read_queue_num_v; + read_queue_num_v.set_number_value(read_queue_nums_); + (*fields)["readQueueNums"] = read_queue_num_v; + + ProtobufWkt::Value write_queue_num_v; + write_queue_num_v.set_number_value(write_queue_nums_); + (*fields)["writeQueueNums"] = write_queue_num_v; + + ProtobufWkt::Value perm_v; + perm_v.set_number_value(perm_); + (*fields)["perm"] = perm_v; +} + +void BrokerData::encode(ProtobufWkt::Struct& data_struct) { + auto& members = *(data_struct.mutable_fields()); + + ProtobufWkt::Value cluster_v; + cluster_v.set_string_value(cluster_); + members["cluster"] = cluster_v; + + ProtobufWkt::Value broker_name_v; + broker_name_v.set_string_value(broker_name_); + members["brokerName"] = broker_name_v; + + if (!broker_addrs_.empty()) { + ProtobufWkt::Value brokerAddrsNode; + auto& brokerAddrsMembers = *(brokerAddrsNode.mutable_struct_value()->mutable_fields()); + for (auto& entry : broker_addrs_) { + ProtobufWkt::Value address_v; + address_v.set_string_value(entry.second); + brokerAddrsMembers[std::to_string(entry.first)] = address_v; + } + members["brokerAddrs"] = brokerAddrsNode; + } +} + +void TopicRouteData::encode(ProtobufWkt::Struct& data_struct) { + auto* fields = data_struct.mutable_fields(); + + if (!queue_data_.empty()) { + ProtobufWkt::ListValue queue_data_list_v; + for (auto& queueData : queue_data_) { + queueData.encode(data_struct); + queue_data_list_v.add_values()->mutable_struct_value()->CopyFrom(data_struct); + } + (*fields)["queueDatas"].mutable_list_value()->CopyFrom(queue_data_list_v); + } + + if (!broker_data_.empty()) { + ProtobufWkt::ListValue broker_data_list_v; + for (auto& brokerData : broker_data_) { + brokerData.encode(data_struct); + broker_data_list_v.add_values()->mutable_struct_value()->CopyFrom(data_struct); + } + (*fields)["brokerDatas"].mutable_list_value()->CopyFrom(broker_data_list_v); + } +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/topic_route.h b/source/extensions/filters/network/rocketmq_proxy/topic_route.h new file mode 100644 index 000000000000..2b9afdb1d526 --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/topic_route.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { +class QueueData { +public: + QueueData(const std::string& broker_name, int32_t read_queue_num, int32_t write_queue_num, + int32_t perm) + : broker_name_(broker_name), read_queue_nums_(read_queue_num), + write_queue_nums_(write_queue_num), perm_(perm) {} + + void encode(ProtobufWkt::Struct& data_struct); + + const std::string& brokerName() const { return broker_name_; } + + int32_t readQueueNum() const { return read_queue_nums_; } + + int32_t writeQueueNum() const { return write_queue_nums_; } + + int32_t perm() const { return perm_; } + +private: + std::string broker_name_; + int32_t read_queue_nums_; + int32_t write_queue_nums_; + int32_t perm_; +}; + +class BrokerData { +public: + BrokerData(const std::string& cluster, const std::string& broker_name, + std::unordered_map&& broker_addrs) + : cluster_(cluster), broker_name_(broker_name), broker_addrs_(broker_addrs) {} + + void encode(ProtobufWkt::Struct& data_struct); + + const std::string& cluster() const { return cluster_; } + + const std::string& brokerName() const { return broker_name_; } + + std::unordered_map& brokerAddresses() { return broker_addrs_; } + +private: + std::string cluster_; + std::string broker_name_; + std::unordered_map broker_addrs_; +}; + +class TopicRouteData { +public: + void encode(ProtobufWkt::Struct& data_struct); + + TopicRouteData() = default; + + TopicRouteData(std::vector&& queue_data, std::vector&& broker_data) + : queue_data_(queue_data), broker_data_(broker_data) {} + + std::vector& queueData() { return queue_data_; } + + std::vector& brokerData() { return broker_data_; } + +private: + std::vector queue_data_; + std::vector broker_data_; +}; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/rocketmq_proxy/well_known_names.h b/source/extensions/filters/network/rocketmq_proxy/well_known_names.h new file mode 100644 index 000000000000..659b387db28b --- /dev/null +++ b/source/extensions/filters/network/rocketmq_proxy/well_known_names.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "common/singleton/const_singleton.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +struct RocketmqValues { + /** + * All the values below are the properties of single broker in filter_metadata. + */ + const std::string ReadQueueNum = "read_queue_num"; + const std::string WriteQueueNum = "write_queue_num"; + const std::string ClusterName = "cluster_name"; + const std::string BrokerName = "broker_name"; + const std::string BrokerId = "broker_id"; + const std::string Perm = "perm"; +}; + +using RocketmqConstants = ConstSingleton; + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index a7577b8ffd2c..bc626950ee4c 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -18,6 +18,8 @@ class NetworkFilterNameValues { const std::string Echo = "envoy.filters.network.echo"; // Direct response filter const std::string DirectResponse = "envoy.filters.network.direct_response"; + // RocketMQ proxy filter + const std::string RocketmqProxy = "envoy.filters.network.rocketmq_proxy"; // Dubbo proxy filter const std::string DubboProxy = "envoy.filters.network.dubbo_proxy"; // HTTP connection manager filter diff --git a/test/extensions/filters/network/rocketmq_proxy/BUILD b/test/extensions/filters/network/rocketmq_proxy/BUILD new file mode 100644 index 000000000000..868ced554fcc --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/BUILD @@ -0,0 +1,136 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_mock", + "envoy_cc_test_library", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_cc_mock( + name = "mocks_lib", + srcs = ["mocks.cc"], + hdrs = ["mocks.h"], + deps = [ + "//source/extensions/filters/network/rocketmq_proxy:config", + "//source/extensions/filters/network/rocketmq_proxy/router:router_lib", + "//test/mocks/server:server_mocks", + "//test/mocks/upstream:upstream_mocks", + ], +) + +envoy_cc_test_library( + name = "utility_lib", + srcs = ["utility.cc"], + hdrs = ["utility.h"], + deps = [ + "//source/extensions/filters/network/rocketmq_proxy:config", + "//test/mocks/server:server_mocks", + ], +) + +envoy_extension_cc_test( + name = "protocol_test", + srcs = ["protocol_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + "//source/extensions/filters/network/rocketmq_proxy:config", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "router_test", + srcs = ["router_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + ":mocks_lib", + ":utility_lib", + "//source/extensions/filters/network/rocketmq_proxy:config", + "//test/mocks/server:server_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "topic_route_test", + srcs = ["topic_route_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/network/rocketmq_proxy:config", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "conn_manager_test", + srcs = ["conn_manager_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + ":utility_lib", + "//test/common/stats:stat_test_utility_lib", + "//test/common/upstream:utility_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "active_message_test", + srcs = ["active_message_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + ":utility_lib", + "//source/extensions/filters/network/rocketmq_proxy:config", + "//test/mocks/network:network_mocks", + "//test/mocks/server:server_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + "//source/extensions/filters/network/rocketmq_proxy:config", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/server:server_mocks", + "//test/test_common:registry_lib", + "@envoy_api//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "codec_test", + srcs = ["codec_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + ":utility_lib", + "//source/common/network:address_lib", + "//source/common/protobuf:utility_lib", + "//test/mocks/server:server_mocks", + "//test/test_common:registry_lib", + ], +) + +envoy_extension_cc_test( + name = "route_matcher_test", + srcs = ["route_matcher_test.cc"], + extension_name = "envoy.filters.network.rocketmq_proxy", + deps = [ + "//source/extensions/filters/network/rocketmq_proxy/router:route_matcher", + "//test/mocks/server:server_mocks", + "@envoy_api//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/network/rocketmq_proxy/active_message_test.cc b/test/extensions/filters/network/rocketmq_proxy/active_message_test.cc new file mode 100644 index 000000000000..8b87e034692b --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/active_message_test.cc @@ -0,0 +1,209 @@ +#include "extensions/filters/network/rocketmq_proxy/active_message.h" +#include "extensions/filters/network/rocketmq_proxy/config.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/protocol.h" +#include "extensions/filters/network/rocketmq_proxy/well_known_names.h" + +#include "test/extensions/filters/network/rocketmq_proxy/utility.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class ActiveMessageTest : public testing::Test { +public: + ActiveMessageTest() + : stats_(RocketmqFilterStats::generateStats("test.", store_)), + config_(rocketmq_proxy_config_, factory_context_), + connection_manager_(config_, factory_context_.dispatcher().timeSource()) { + connection_manager_.initializeReadFilterCallbacks(filter_callbacks_); + } + + ~ActiveMessageTest() override { + filter_callbacks_.connection_.dispatcher_.clearDeferredDeleteList(); + } + +protected: + ConfigImpl::RocketmqProxyConfig rocketmq_proxy_config_; + NiceMock filter_callbacks_; + NiceMock factory_context_; + Stats::IsolatedStoreImpl store_; + RocketmqFilterStats stats_; + ConfigImpl config_; + ConnectionManager connection_manager_; +}; + +TEST_F(ActiveMessageTest, ClusterName) { + std::string json = R"EOF( + { + "opaque": 1, + "code": 35, + "version": 1, + "language": "JAVA", + "serializeTypeCurrentRPC": "JSON", + "flag": 0, + "extFields": { + "clientID": "SampleClient_01", + "producerGroup": "PG_Example_01", + "consumerGroup": "CG_001" + } + } + )EOF"; + + Buffer::OwnedImpl buffer; + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + auto cmd = Decoder::decode(buffer, underflow, has_error); + EXPECT_FALSE(underflow); + EXPECT_FALSE(has_error); + + ActiveMessage activeMessage(connection_manager_, std::move(cmd)); + EXPECT_FALSE(activeMessage.metadata()->hasTopicName()); +} + +TEST_F(ActiveMessageTest, FillBrokerData) { + + std::unordered_map address; + address.emplace(0, "1.2.3.4:10911"); + BrokerData broker_data("DefaultCluster", "broker-a", std::move(address)); + + std::vector list; + list.push_back(broker_data); + + ActiveMessage::fillBrokerData(list, "DefaultCluster", "broker-a", 1, "localhost:10911"); + ActiveMessage::fillBrokerData(list, "DefaultCluster", "broker-a", 0, "localhost:10911"); + EXPECT_EQ(1, list.size()); + for (auto& it : list) { + auto& address = it.brokerAddresses(); + EXPECT_EQ(2, address.size()); + EXPECT_STREQ("1.2.3.4:10911", address[0].c_str()); + } +} + +TEST_F(ActiveMessageTest, FillAckMessageDirectiveSuccess) { + RemotingCommandPtr cmd = std::make_unique(); + ActiveMessage active_message(connection_manager_, std::move(cmd)); + + Buffer::OwnedImpl buffer; + // frame length + buffer.writeBEInt(98); + + // magic code + buffer.writeBEInt(enumToSignedInt(MessageVersion::V1)); + + // body CRC + buffer.writeBEInt(1); + + // queue Id + buffer.writeBEInt(2); + + // flag + buffer.writeBEInt(3); + + // queue offset + buffer.writeBEInt(4); + + // physical offset + buffer.writeBEInt(5); + + // system flag + buffer.writeBEInt(6); + + // born timestamp + buffer.writeBEInt(7); + + // born host + buffer.writeBEInt(8); + + // born host port + buffer.writeBEInt(9); + + // store timestamp + buffer.writeBEInt(10); + + // store host address ip:port --> long + Network::Address::Ipv4Instance host_address("127.0.0.1", 10911); + const sockaddr_in* sock_addr = reinterpret_cast(host_address.sockAddr()); + buffer.writeBEInt(sock_addr->sin_addr.s_addr); + buffer.writeBEInt(sock_addr->sin_port); + + // re-consume times + buffer.writeBEInt(11); + + // transaction offset + buffer.writeBEInt(12); + + // body size + buffer.writeBEInt(0); + + const std::string topic = "TopicTest"; + + // topic length + buffer.writeBEInt(topic.length()); + + // topic data + buffer.add(topic); + + AckMessageDirective directive("broker-a", 0, connection_manager_.timeSource().monotonicTime()); + const std::string group = "Group"; + active_message.fillAckMessageDirective(buffer, group, topic, directive); + + const std::string fake_topic = "FakeTopic"; + active_message.fillAckMessageDirective(buffer, group, fake_topic, directive); + + EXPECT_EQ(connection_manager_.getAckDirectiveTableForTest().size(), 1); +} + +TEST_F(ActiveMessageTest, RecordPopRouteInfo) { + auto host_description = new NiceMock(); + + auto metadata = std::make_shared(); + ProtobufWkt::Struct topic_route_data; + auto* fields = topic_route_data.mutable_fields(); + + std::string broker_name = "broker-a"; + int32_t broker_id = 0; + + (*fields)[RocketmqConstants::get().ReadQueueNum] = ValueUtil::numberValue(4); + (*fields)[RocketmqConstants::get().WriteQueueNum] = ValueUtil::numberValue(4); + (*fields)[RocketmqConstants::get().ClusterName] = ValueUtil::stringValue("DefaultCluster"); + (*fields)[RocketmqConstants::get().BrokerName] = ValueUtil::stringValue(broker_name); + (*fields)[RocketmqConstants::get().BrokerId] = ValueUtil::numberValue(broker_id); + (*fields)[RocketmqConstants::get().Perm] = ValueUtil::numberValue(6); + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + NetworkFilterNames::get().RocketmqProxy, topic_route_data)); + + EXPECT_CALL(*host_description, metadata()).WillRepeatedly(Return(metadata)); + + Upstream::HostDescriptionConstSharedPtr host_description_ptr(host_description); + + Buffer::OwnedImpl buffer; + BufferUtility::fillRequestBuffer(buffer, RequestCode::PopMessage); + + bool underflow = false; + bool has_error = false; + + RemotingCommandPtr cmd = Decoder::decode(buffer, underflow, has_error); + ActiveMessage active_message(connection_manager_, std::move(cmd)); + active_message.recordPopRouteInfo(host_description_ptr); + auto custom_header = active_message.downstreamRequest()->typedCustomHeader(); + EXPECT_EQ(custom_header->targetBrokerName(), broker_name); + EXPECT_EQ(custom_header->targetBrokerId(), broker_id); +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/rocketmq_proxy/codec_test.cc b/test/extensions/filters/network/rocketmq_proxy/codec_test.cc new file mode 100644 index 000000000000..08d8dd5021a1 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/codec_test.cc @@ -0,0 +1,797 @@ +#include "common/network/address_impl.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/rocketmq_proxy/codec.h" + +#include "test/extensions/filters/network/rocketmq_proxy/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class RocketmqCodecTest : public testing::Test { +public: + RocketmqCodecTest() = default; + ~RocketmqCodecTest() override = default; +}; + +TEST_F(RocketmqCodecTest, DecodeWithMinFrameSize) { + Buffer::OwnedImpl buffer; + + buffer.add(std::string({'\x00', '\x00', '\x01', '\x8b'})); + buffer.add(std::string({'\x00', '\x00', '\x01', '\x76'})); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_TRUE(underflow); + EXPECT_FALSE(has_error); + EXPECT_TRUE(nullptr == cmd); +} + +TEST_F(RocketmqCodecTest, DecodeWithOverMaxFrameSizeData) { + Buffer::OwnedImpl buffer; + + buffer.add(std::string({'\x00', '\x40', '\x00', '\x01'})); + buffer.add(std::string({'\x00', '\x20', '\x00', '\x00', '\x00'})); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(nullptr == cmd); +} + +TEST_F(RocketmqCodecTest, DecodeUnsupportHeaderSerialization) { + Buffer::OwnedImpl buffer; + std::string header = "random text suffices"; + + buffer.writeBEInt(4 + 4 + header.size()); + uint32_t mark = header.size(); + mark |= (1u << 24u); + buffer.writeBEInt(mark); + buffer.add(header); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(nullptr == cmd); +} + +TEST_F(RocketmqCodecTest, DecodeInvalidJson) { + Buffer::OwnedImpl buffer; + // Invalid json string. + std::string invalid_json = R"EOF({a: 3)EOF"; + + buffer.writeBEInt(4 + 4 + invalid_json.size()); + buffer.writeBEInt(invalid_json.size()); + buffer.add(invalid_json); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(cmd == nullptr); +} + +TEST_F(RocketmqCodecTest, DecodeCodeMissing) { + Buffer::OwnedImpl buffer; + // Invalid json string. + std::string invalid_json = R"EOF({"a": 3})EOF"; + + buffer.writeBEInt(4 + 4 + invalid_json.size()); + buffer.writeBEInt(invalid_json.size()); + buffer.add(invalid_json); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(cmd == nullptr); +} + +TEST_F(RocketmqCodecTest, DecodeVersionMissing) { + Buffer::OwnedImpl buffer; + // Invalid json string. + std::string invalid_json = R"EOF({"code": 3})EOF"; + + buffer.writeBEInt(4 + 4 + invalid_json.size()); + buffer.writeBEInt(invalid_json.size()); + buffer.add(invalid_json); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(cmd == nullptr); +} + +TEST_F(RocketmqCodecTest, DecodeOpaqueMissing) { + Buffer::OwnedImpl buffer; + // Invalid json string. + std::string invalid_json = R"EOF( + { + "code": 3, + "version": 1 + } + )EOF"; + + buffer.writeBEInt(4 + 4 + invalid_json.size()); + buffer.writeBEInt(invalid_json.size()); + buffer.add(invalid_json); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(cmd == nullptr); +} + +TEST_F(RocketmqCodecTest, DecodeFlagMissing) { + Buffer::OwnedImpl buffer; + // Invalid json string. + std::string invalid_json = R"EOF( + { + "code": 3, + "version": 1, + "opaque": 1 + } + )EOF"; + + buffer.writeBEInt(4 + 4 + invalid_json.size()); + buffer.writeBEInt(invalid_json.size()); + buffer.add(invalid_json); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_TRUE(has_error); + EXPECT_TRUE(cmd == nullptr); +} + +TEST_F(RocketmqCodecTest, DecodeRequestSendMessage) { + Buffer::OwnedImpl buffer; + BufferUtility::fillRequestBuffer(buffer, RequestCode::SendMessage); + + bool underflow = false; + bool has_error = false; + + RemotingCommandPtr request = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow || has_error); + EXPECT_EQ(request->opaque(), BufferUtility::opaque_); + Buffer::Instance& body = request->body(); + EXPECT_EQ(body.toString(), BufferUtility::msg_body_); + + auto header = request->typedCustomHeader(); + + EXPECT_EQ(header->topic(), BufferUtility::topic_name_); + EXPECT_EQ(header->version(), SendMessageRequestVersion::V1); + EXPECT_EQ(header->queueId(), -1); +} + +TEST_F(RocketmqCodecTest, DecodeRequestSendMessageV2) { + Buffer::OwnedImpl buffer; + + BufferUtility::fillRequestBuffer(buffer, RequestCode::SendMessageV2); + + bool underflow = false; + bool has_error = false; + + RemotingCommandPtr request = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow || has_error); + EXPECT_EQ(request->opaque(), BufferUtility::opaque_); + + Buffer::Instance& body = request->body(); + + EXPECT_EQ(body.toString(), BufferUtility::msg_body_); + + auto header = request->typedCustomHeader(); + + EXPECT_EQ(header->topic(), BufferUtility::topic_name_); + EXPECT_EQ(header->version(), SendMessageRequestVersion::V2); + EXPECT_EQ(header->queueId(), -1); +} + +TEST_F(RocketmqCodecTest, DecodeRequestSendMessageV1) { + std::string json = R"EOF( + { + "code": 10, + "version": 1, + "opaque": 1, + "flag": 0, + "extFields": { + "batch": false, + "bornTimestamp": 1575872212297, + "defaultTopic": "TBW102", + "defaultTopicQueueNums": 3, + "flag": 124, + "producerGroup": "FooBarGroup", + "queueId": 1, + "reconsumeTimes": 0, + "sysFlag": 0, + "topic": "FooBar", + "unitMode": false, + "properties": "mock_properties", + "maxReconsumeTimes": 32 + } + } + )EOF"; + Buffer::OwnedImpl buffer; + + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + + auto cmd = Decoder::decode(buffer, underflow, has_error); + + EXPECT_FALSE(underflow); + EXPECT_FALSE(has_error); + EXPECT_TRUE(nullptr != cmd); + EXPECT_EQ(10, cmd->code()); + EXPECT_EQ(1, cmd->version()); + EXPECT_EQ(1, cmd->opaque()); +} + +TEST_F(RocketmqCodecTest, DecodeSendMessageResponseWithSystemError) { + std::string json = R"EOF( + { + "code": 1, + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "remark": "System error", + "serializeTypeCurrentRPC": "JSON" + } + )EOF"; + Buffer::OwnedImpl buffer; + + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + + auto cmd = + Decoder::decode(buffer, underflow, has_error, static_cast(RequestCode::SendMessage)); + + EXPECT_FALSE(has_error); + EXPECT_FALSE(underflow); + EXPECT_TRUE(nullptr != cmd); + EXPECT_STREQ("JAVA", cmd->language().c_str()); + EXPECT_STREQ("JSON", cmd->serializeTypeCurrentRPC().c_str()); + EXPECT_STREQ("System error", cmd->remark().c_str()); + EXPECT_TRUE(nullptr == cmd->customHeader()); +} + +TEST_F(RocketmqCodecTest, DecodeSendMessageResponseWithSystemBusy) { + std::string json = R"EOF( + { + "code": 2, + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "remark": "System busy", + "serializeTypeCurrentRPC": "JSON" + } + )EOF"; + Buffer::OwnedImpl buffer; + + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + + auto cmd = + Decoder::decode(buffer, underflow, has_error, static_cast(RequestCode::SendMessage)); + + EXPECT_FALSE(has_error); + EXPECT_FALSE(underflow); + EXPECT_TRUE(nullptr != cmd); + EXPECT_STREQ("JAVA", cmd->language().c_str()); + EXPECT_STREQ("JSON", cmd->serializeTypeCurrentRPC().c_str()); + EXPECT_STREQ("System busy", cmd->remark().c_str()); + EXPECT_TRUE(nullptr == cmd->customHeader()); +} + +TEST_F(RocketmqCodecTest, DecodeSendMessageResponseWithCodeNotSupported) { + std::string json = R"EOF( + { + "code": 3, + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "remark": "Code not supported", + "serializeTypeCurrentRPC": "JSON" + } + )EOF"; + Buffer::OwnedImpl buffer; + + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + + auto cmd = + Decoder::decode(buffer, underflow, has_error, static_cast(RequestCode::SendMessage)); + + EXPECT_FALSE(has_error); + EXPECT_FALSE(underflow); + EXPECT_TRUE(nullptr != cmd); + EXPECT_STREQ("JAVA", cmd->language().c_str()); + EXPECT_STREQ("JSON", cmd->serializeTypeCurrentRPC().c_str()); + EXPECT_STREQ("Code not supported", cmd->remark().c_str()); + EXPECT_TRUE(nullptr == cmd->customHeader()); +} + +TEST_F(RocketmqCodecTest, DecodeSendMessageResponseNormal) { + std::string json = R"EOF( + { + "code": 0, + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "remark": "OK", + "serializeTypeCurrentRPC": "JSON", + "extFields": { + "msgId": "A001", + "queueId": "10", + "queueOffset": "2", + "transactionId": "" + } + } + )EOF"; + Buffer::OwnedImpl buffer; + + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + + auto cmd = + Decoder::decode(buffer, underflow, has_error, static_cast(RequestCode::SendMessage)); + + EXPECT_FALSE(has_error); + EXPECT_FALSE(underflow); + EXPECT_TRUE(nullptr != cmd); + EXPECT_STREQ("JAVA", cmd->language().c_str()); + EXPECT_STREQ("JSON", cmd->serializeTypeCurrentRPC().c_str()); + EXPECT_STREQ("OK", cmd->remark().c_str()); + EXPECT_TRUE(nullptr != cmd->customHeader()); + + auto extHeader = cmd->typedCustomHeader(); + + EXPECT_STREQ("A001", extHeader->msgId().c_str()); + EXPECT_EQ(10, extHeader->queueId()); + EXPECT_EQ(2, extHeader->queueOffset()); +} + +TEST_F(RocketmqCodecTest, DecodePopMessageResponseNormal) { + std::string json = R"EOF( + { + "code": 0, + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "remark": "OK", + "serializeTypeCurrentRPC": "JSON", + "extFields": { + "popTime": "1234", + "invisibleTime": "10", + "reviveQid": "2", + "restNum": "10", + "startOffsetInfo": "3", + "msgOffsetInfo": "mock_msg_offset_info", + "orderCountInfo": "mock_order_count_info" + } + } + )EOF"; + Buffer::OwnedImpl buffer; + + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + bool underflow = false; + bool has_error = false; + + auto cmd = + Decoder::decode(buffer, underflow, has_error, static_cast(RequestCode::PopMessage)); + + EXPECT_FALSE(has_error); + EXPECT_FALSE(underflow); + EXPECT_TRUE(nullptr != cmd); + EXPECT_STREQ("JAVA", cmd->language().c_str()); + EXPECT_STREQ("JSON", cmd->serializeTypeCurrentRPC().c_str()); + EXPECT_STREQ("OK", cmd->remark().c_str()); + EXPECT_TRUE(nullptr != cmd->customHeader()); + + auto extHeader = cmd->typedCustomHeader(); + + EXPECT_EQ(1234, extHeader->popTimeForTest()); + EXPECT_EQ(10, extHeader->invisibleTime()); + EXPECT_EQ(2, extHeader->reviveQid()); + EXPECT_EQ(10, extHeader->restNum()); + EXPECT_STREQ("3", extHeader->startOffsetInfo().c_str()); + EXPECT_STREQ("mock_msg_offset_info", extHeader->msgOffsetInfo().c_str()); + EXPECT_STREQ("mock_order_count_info", extHeader->orderCountInfo().c_str()); +} + +TEST_F(RocketmqCodecTest, DecodeRequestSendMessageV2underflow) { + Buffer::OwnedImpl buffer; + + buffer.add(std::string({'\x00', '\x00', '\x01', '\x8b'})); + buffer.add(std::string({'\x00', '\x00', '\x01', '\x76'})); + + std::string header_json = R"EOF( + { + "code": 310, + "extFields": { + "a": "GID_LINGCHU_TEST_0" + } + )EOF"; + + buffer.add(header_json); + buffer.add(std::string{"_Apache_RocketMQ_"}); + + bool underflow = false; + bool has_error = false; + + RemotingCommandPtr request = Decoder::decode(buffer, underflow, has_error); + + EXPECT_EQ(underflow, true); + EXPECT_EQ(has_error, false); +} + +TEST_F(RocketmqCodecTest, EncodeResponseSendMessageSuccess) { + const int version = 285; + const int opaque = 4; + const std::string msg_id = "1E05789ABD1F18B4AAC2895B8BE60003"; + + RemotingCommandPtr response = + std::make_unique(static_cast(ResponseCode::Success), version, opaque); + + response->markAsResponse(); + + const int queue_id = 0; + const int queue_offset = 0; + + std::unique_ptr sendMessageResponseHeader = + std::make_unique(msg_id, queue_id, queue_offset, EMPTY_STRING); + CommandCustomHeaderPtr extHeader(sendMessageResponseHeader.release()); + response->customHeader(extHeader); + + Buffer::OwnedImpl response_buffer; + Encoder::encode(response, response_buffer); + + uint32_t frame_length = response_buffer.peekBEInt(); + uint32_t header_length = + response_buffer.peekBEInt(Decoder::FRAME_HEADER_LENGTH_FIELD_SIZE); + + EXPECT_EQ(header_length + Decoder::FRAME_HEADER_LENGTH_FIELD_SIZE, frame_length); + + std::unique_ptr header_data = std::make_unique(header_length); + const uint32_t frame_header_content_offset = + Decoder::FRAME_LENGTH_FIELD_SIZE + Decoder::FRAME_HEADER_LENGTH_FIELD_SIZE; + response_buffer.copyOut(frame_header_content_offset, header_length, header_data.get()); + std::string header_json(header_data.get(), header_length); + ProtobufWkt::Struct doc; + MessageUtil::loadFromJson(header_json, doc); + const auto& members = doc.fields(); + + EXPECT_EQ(members.at("code").number_value(), 0); + EXPECT_EQ(members.at("version").number_value(), version); + EXPECT_EQ(members.at("opaque").number_value(), opaque); + + const auto& extFields = members.at("extFields").struct_value().fields(); + + EXPECT_EQ(extFields.at("msgId").string_value(), msg_id); + EXPECT_EQ(extFields.at("queueId").number_value(), queue_id); + EXPECT_EQ(extFields.at("queueOffset").number_value(), queue_offset); +} + +TEST_F(RocketmqCodecTest, DecodeQueueIdWithIncompleteBuffer) { + Buffer::OwnedImpl buffer; + // incomplete buffer + buffer.add(std::string({'\x00'})); + + EXPECT_EQ(Decoder::decodeQueueId(buffer, 0), -1); +} + +TEST_F(RocketmqCodecTest, DecodeQueueIdSuccess) { + Buffer::OwnedImpl buffer; + // frame length + buffer.writeBEInt(16); + + for (int i = 0; i < 3; i++) { + buffer.writeBEInt(i); + } + EXPECT_EQ(Decoder::decodeQueueId(buffer, 0), 2); +} + +TEST_F(RocketmqCodecTest, DecodeQueueIdFailure) { + Buffer::OwnedImpl buffer; + buffer.writeBEInt(128); + + // Some random data, but incomplete frame + buffer.writeBEInt(12); + + EXPECT_EQ(Decoder::decodeQueueId(buffer, 0), -1); +} + +TEST_F(RocketmqCodecTest, DecodeQueueOffsetSuccess) { + Buffer::OwnedImpl buffer; + // frame length + buffer.writeBEInt(28); + + // frame data + for (int i = 0; i < 4; i++) { + buffer.writeBEInt(i); + } + // write queue offset which takes up 8 bytes + buffer.writeBEInt(4); + + EXPECT_EQ(Decoder::decodeQueueOffset(buffer, 0), 4); +} + +TEST_F(RocketmqCodecTest, DecodeQueueOffsetFailure) { + Buffer::OwnedImpl buffer; + + // Define length of the frame as 128 bytes + buffer.writeBEInt(128); + + // some random data, just make sure the frame is incomplete + for (int i = 0; i < 6; i++) { + buffer.writeBEInt(i); + } + + EXPECT_EQ(Decoder::decodeQueueOffset(buffer, 0), -1); +} + +TEST_F(RocketmqCodecTest, DecodeMsgIdSuccess) { + Buffer::OwnedImpl buffer; + + // frame length + buffer.writeBEInt(64); + + // magic code + buffer.writeBEInt(0); + + // body CRC + buffer.writeBEInt(1); + + // queue Id + buffer.writeBEInt(2); + + // flag + buffer.writeBEInt(3); + + // queue offset + buffer.writeBEInt(4); + + // physical offset + buffer.writeBEInt(5); + + // system flag + buffer.writeBEInt(6); + + // born timestamp + buffer.writeBEInt(7); + + // born host + buffer.writeBEInt(8); + + // born host port + buffer.writeBEInt(9); + + // store timestamp + buffer.writeBEInt(10); + + // store host address ip:port --> long + Network::Address::Ipv4Instance host_address("127.0.0.1", 10911); + const sockaddr_in* sock_addr = reinterpret_cast(host_address.sockAddr()); + buffer.writeBEInt(sock_addr->sin_addr.s_addr); + buffer.writeBEInt(sock_addr->sin_port); + EXPECT_EQ(Decoder::decodeMsgId(buffer, 0).empty(), false); +} + +TEST_F(RocketmqCodecTest, DecodeMsgIdFailure) { + Buffer::OwnedImpl buffer; + + // frame length + buffer.writeBEInt(101); + + // magic code + buffer.writeBEInt(0); + EXPECT_EQ(Decoder::decodeMsgId(buffer, 0).empty(), true); +} + +TEST_F(RocketmqCodecTest, DecodeTopicSuccessV1) { + Buffer::OwnedImpl buffer; + + // frame length + buffer.writeBEInt(98); + + // magic code + buffer.writeBEInt(enumToSignedInt(MessageVersion::V1)); + + // body CRC + buffer.writeBEInt(1); + + // queue Id + buffer.writeBEInt(2); + + // flag + buffer.writeBEInt(3); + + // queue offset + buffer.writeBEInt(4); + + // physical offset + buffer.writeBEInt(5); + + // system flag + buffer.writeBEInt(6); + + // born timestamp + buffer.writeBEInt(7); + + // born host + buffer.writeBEInt(8); + + // born host port + buffer.writeBEInt(9); + + // store timestamp + buffer.writeBEInt(10); + + // store host address ip:port --> long + Network::Address::Ipv4Instance host_address("127.0.0.1", 10911); + const sockaddr_in* sock_addr = reinterpret_cast(host_address.sockAddr()); + buffer.writeBEInt(sock_addr->sin_addr.s_addr); + buffer.writeBEInt(sock_addr->sin_port); + + // re-consume times + buffer.writeBEInt(11); + + // transaction offset + buffer.writeBEInt(12); + + // body size + buffer.writeBEInt(0); + + const std::string topic = "TopicTest"; + + // topic length + buffer.writeBEInt(topic.length()); + + // topic data + buffer.add(topic); + + EXPECT_STREQ(Decoder::decodeTopic(buffer, 0).c_str(), topic.c_str()); +} + +TEST_F(RocketmqCodecTest, DecodeTopicSuccessV2) { + Buffer::OwnedImpl buffer; + + // frame length + buffer.writeBEInt(99); + + // magic code + buffer.writeBEInt(enumToSignedInt(MessageVersion::V2)); + + // body CRC + buffer.writeBEInt(1); + + // queue Id + buffer.writeBEInt(2); + + // flag + buffer.writeBEInt(3); + + // queue offset + buffer.writeBEInt(4); + + // physical offset + buffer.writeBEInt(5); + + // system flag + buffer.writeBEInt(6); + + // born timestamp + buffer.writeBEInt(7); + + // born host + buffer.writeBEInt(8); + + // born host port + buffer.writeBEInt(9); + + // store timestamp + buffer.writeBEInt(10); + + // store host address ip:port --> long + Network::Address::Ipv4Instance host_address("127.0.0.1", 10911); + const sockaddr_in* sock_addr = reinterpret_cast(host_address.sockAddr()); + buffer.writeBEInt(sock_addr->sin_addr.s_addr); + buffer.writeBEInt(sock_addr->sin_port); + + // re-consume times + buffer.writeBEInt(11); + + // transaction offset + buffer.writeBEInt(12); + + // body size + buffer.writeBEInt(0); + + const std::string topic = "TopicTest"; + + // topic length + buffer.writeBEInt(topic.length()); + + // topic data + buffer.add(topic); + + EXPECT_STREQ(Decoder::decodeTopic(buffer, 0).c_str(), topic.c_str()); +} + +TEST_F(RocketmqCodecTest, DecodeTopicFailure) { + Buffer::OwnedImpl buffer; + + // frame length + buffer.writeBEInt(64); + + // magic code + buffer.writeBEInt(0); + EXPECT_EQ(Decoder::decodeTopic(buffer, 0).empty(), true); +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/config_test.cc b/test/extensions/filters/network/rocketmq_proxy/config_test.cc new file mode 100644 index 000000000000..af4d5ef745e4 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/config_test.cc @@ -0,0 +1,170 @@ +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.validate.h" + +#include "extensions/filters/network/rocketmq_proxy/config.h" + +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/server/mocks.h" +#include "test/test_common/registry.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +using RocketmqProxyProto = envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy; + +RocketmqProxyProto parseRocketmqProxyFromV2Yaml(const std::string& yaml) { + RocketmqProxyProto rocketmq_proxy; + TestUtility::loadFromYaml(yaml, rocketmq_proxy); + return rocketmq_proxy; +} + +class RocketmqFilterConfigTestBase { +public: + void testConfig(RocketmqProxyProto& config) { + Network::FilterFactoryCb cb; + EXPECT_NO_THROW({ cb = factory_.createFilterFactoryFromProto(config, context_); }); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); + } + + NiceMock context_; + RocketmqProxyFilterConfigFactory factory_; +}; + +class RocketmqFilterConfigTest : public RocketmqFilterConfigTestBase, public testing::Test { +public: + ~RocketmqFilterConfigTest() override = default; +}; + +TEST_F(RocketmqFilterConfigTest, ValidateFail) { + NiceMock context; + EXPECT_THROW( + RocketmqProxyFilterConfigFactory().createFilterFactoryFromProto( + envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy(), context), + ProtoValidationException); +} + +TEST_F(RocketmqFilterConfigTest, ValidProtoConfiguration) { + envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy config{}; + config.set_stat_prefix("my_stat_prefix"); + NiceMock context; + RocketmqProxyFilterConfigFactory factory; + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(config, context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + +TEST_F(RocketmqFilterConfigTest, RocketmqProxyWithEmptyProto) { + NiceMock context; + RocketmqProxyFilterConfigFactory factory; + envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy config = + *dynamic_cast( + factory.createEmptyConfigProto().get()); + config.set_stat_prefix("my_stat_prefix"); + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(config, context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + +TEST_F(RocketmqFilterConfigTest, RocketmqProxyWithFullConfig) { + const std::string yaml = R"EOF( + stat_prefix: rocketmq_incomming_stats + develop_mode: true + transient_object_life_span: + seconds: 30 + )EOF"; + RocketmqProxyProto config = parseRocketmqProxyFromV2Yaml(yaml); + testConfig(config); +} + +TEST_F(RocketmqFilterConfigTest, ProxyAddress) { + NiceMock context; + Server::Configuration::MockServerFactoryContext factory_context; + EXPECT_CALL(context, getServerFactoryContext()).WillRepeatedly(ReturnRef(factory_context)); + + LocalInfo::MockLocalInfo local_info; + EXPECT_CALL(factory_context, localInfo()).WillRepeatedly(ReturnRef(local_info)); + std::shared_ptr instance = + std::make_shared("logical", "physical"); + EXPECT_CALL(local_info, address()).WillRepeatedly(Return(instance)); + EXPECT_CALL(*instance, type()).WillRepeatedly(Return(Network::Address::Type::Ip)); + + Network::MockIp* ip = new Network::MockIp(); + EXPECT_CALL(*instance, ip()).WillRepeatedly(testing::Return(ip)); + + std::string address("1.2.3.4"); + EXPECT_CALL(*ip, addressAsString()).WillRepeatedly(ReturnRef(address)); + EXPECT_CALL(*ip, port()).WillRepeatedly(Return(1234)); + ConfigImpl::RocketmqProxyConfig proxyConfig; + ConfigImpl configImpl(proxyConfig, context); + + EXPECT_STREQ("1.2.3.4:1234", configImpl.proxyAddress().c_str()); + delete ip; +} + +TEST_F(RocketmqFilterConfigTest, ProxyAddressWithDefaultPort) { + NiceMock context; + Server::Configuration::MockServerFactoryContext factory_context; + EXPECT_CALL(context, getServerFactoryContext()).WillRepeatedly(ReturnRef(factory_context)); + + LocalInfo::MockLocalInfo local_info; + EXPECT_CALL(factory_context, localInfo()).WillRepeatedly(ReturnRef(local_info)); + std::shared_ptr instance = + std::make_shared("logical", "physical"); + EXPECT_CALL(local_info, address()).WillRepeatedly(Return(instance)); + EXPECT_CALL(*instance, type()).WillRepeatedly(Return(Network::Address::Type::Ip)); + + Network::MockIp* ip = new Network::MockIp(); + EXPECT_CALL(*instance, ip()).WillRepeatedly(testing::Return(ip)); + + std::string address("1.2.3.4"); + EXPECT_CALL(*ip, addressAsString()).WillRepeatedly(ReturnRef(address)); + EXPECT_CALL(*ip, port()).WillRepeatedly(Return(0)); + ConfigImpl::RocketmqProxyConfig proxyConfig; + ConfigImpl configImpl(proxyConfig, context); + + EXPECT_STREQ("1.2.3.4:10000", configImpl.proxyAddress().c_str()); + delete ip; +} + +TEST_F(RocketmqFilterConfigTest, ProxyAddressWithNonIpType) { + NiceMock context; + Server::Configuration::MockServerFactoryContext factory_context; + EXPECT_CALL(context, getServerFactoryContext()).WillRepeatedly(ReturnRef(factory_context)); + + LocalInfo::MockLocalInfo local_info; + EXPECT_CALL(factory_context, localInfo()).WillRepeatedly(ReturnRef(local_info)); + std::shared_ptr instance = + std::make_shared("logical", "physical"); + EXPECT_CALL(local_info, address()).WillRepeatedly(Return(instance)); + EXPECT_CALL(*instance, type()).WillRepeatedly(Return(Network::Address::Type::Pipe)); + + Network::MockIp* ip = new Network::MockIp(); + EXPECT_CALL(*instance, ip()).WillRepeatedly(testing::Return(ip)); + + std::string address("1.2.3.4"); + EXPECT_CALL(*ip, addressAsString()).WillRepeatedly(ReturnRef(address)); + EXPECT_CALL(*ip, port()).WillRepeatedly(Return(0)); + ConfigImpl::RocketmqProxyConfig proxyConfig; + ConfigImpl configImpl(proxyConfig, context); + + EXPECT_STREQ("physical", configImpl.proxyAddress().c_str()); + delete ip; +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/conn_manager_test.cc b/test/extensions/filters/network/rocketmq_proxy/conn_manager_test.cc new file mode 100644 index 000000000000..46f3af3adef3 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/conn_manager_test.cc @@ -0,0 +1,690 @@ +#include "envoy/network/connection.h" + +#include "extensions/filters/network/rocketmq_proxy/config.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/well_known_names.h" + +#include "test/common/stats/stat_test_utility.h" +#include "test/common/upstream/utility.h" +#include "test/extensions/filters/network/rocketmq_proxy/utility.h" +#include "test/mocks/network/connection.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +using ConfigRocketmqProxy = envoy::extensions::filters::network::rocketmq_proxy::v3::RocketmqProxy; + +class TestConfigImpl : public ConfigImpl { +public: + TestConfigImpl(RocketmqProxyConfig config, Server::Configuration::MockFactoryContext& context, + RocketmqFilterStats& stats) + : ConfigImpl(config, context), stats_(stats) {} + + RocketmqFilterStats& stats() override { return stats_; } + +private: + RocketmqFilterStats stats_; +}; + +class RocketmqConnectionManagerTest : public testing::Test { +public: + RocketmqConnectionManagerTest() : stats_(RocketmqFilterStats::generateStats("test.", store_)) {} + + ~RocketmqConnectionManagerTest() override { + filter_callbacks_.connection_.dispatcher_.clearDeferredDeleteList(); + } + + void initializeFilter() { initializeFilter(""); } + + void initializeFilter(const std::string& yaml) { + if (!yaml.empty()) { + TestUtility::loadFromYaml(yaml, proto_config_); + TestUtility::validate(proto_config_); + } + config_ = std::make_unique(proto_config_, factory_context_, stats_); + conn_manager_ = + std::make_unique(*config_, factory_context_.dispatcher().timeSource()); + conn_manager_->initializeReadFilterCallbacks(filter_callbacks_); + conn_manager_->onNewConnection(); + current_ = factory_context_.dispatcher().timeSource().monotonicTime(); + } + + void initializeCluster() { + Upstream::HostVector hosts; + hosts.emplace_back(host_); + priority_set_.updateHosts( + 1, + Upstream::HostSetImpl::partitionHosts(std::make_shared(hosts), + Upstream::HostsPerLocalityImpl::empty()), + nullptr, hosts, {}, 100); + ON_CALL(thread_local_cluster_, prioritySet()).WillByDefault(ReturnRef(priority_set_)); + EXPECT_CALL(factory_context_.cluster_manager_, get(_)) + .WillRepeatedly(Return(&thread_local_cluster_)); + } + + NiceMock factory_context_; + Stats::TestUtil::TestStore store_; + RocketmqFilterStats stats_; + ConfigRocketmqProxy proto_config_; + + std::unique_ptr config_; + + Buffer::OwnedImpl buffer_; + NiceMock filter_callbacks_; + std::unique_ptr conn_manager_; + + Encoder encoder_; + Decoder decoder_; + + MonotonicTime current_; + + std::shared_ptr cluster_info_{ + new NiceMock()}; + Upstream::HostSharedPtr host_{Upstream::makeTestHost(cluster_info_, "tcp://127.0.0.1:80")}; + Upstream::PrioritySetImpl priority_set_; + NiceMock thread_local_cluster_; +}; + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeat) { + initializeFilter(); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::HeartBeat); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.heartbeat").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithDecodeError) { + initializeFilter(); + + std::string json = R"EOF( + { + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "serializeTypeCurrentRPC": "JSON" + } + )EOF"; + + buffer_.writeBEInt(4 + 4 + json.size()); + buffer_.writeBEInt(json.size()); + buffer_.add(json); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.request_decoding_error").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithInvalidBodyJson) { + initializeFilter(); + + RemotingCommandPtr cmd = std::make_unique(); + cmd->code(static_cast(RequestCode::HeartBeat)); + std::string heartbeat_data = R"EOF({"clientID": "127})EOF"; + cmd->body().add(heartbeat_data); + encoder_.encode(cmd, buffer_); + + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(0U, store_.counter("test.request_decoding_error").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithBodyJsonLackofClientId) { + initializeFilter(); + + RemotingCommandPtr cmd = std::make_unique(); + cmd->code(static_cast(RequestCode::HeartBeat)); + std::string heartbeat_data = R"EOF( + { + "consumerDataSet": [{}] + } + )EOF"; + cmd->body().add(heartbeat_data); + encoder_.encode(cmd, buffer_); + + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(0U, store_.counter("test.request_decoding_error").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithGroupMembersMapExists) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("127.0.0.1@90330", *conn_manager_); + group_member.setLastForTest(current_); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::HeartBeat); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.heartbeat").value()); + EXPECT_FALSE(group_member.expired()); + EXPECT_FALSE(group_members_map.at("test_cg").empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithGroupMembersMapExistsButExpired) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("127.0.0.2@90330", *conn_manager_); + group_member.setLastForTest(current_ - std::chrono::seconds(31)); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::HeartBeat); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.heartbeat").value()); + EXPECT_TRUE(group_member.expired()); + EXPECT_TRUE(group_members_map.empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithGroupMembersMapExistsButLackOfClientID) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("127.0.0.2@90330", *conn_manager_); + group_member.setLastForTest(current_); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::HeartBeat); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.heartbeat").value()); + EXPECT_FALSE(group_member.expired()); + EXPECT_FALSE(group_members_map.at("test_cg").empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithDownstreamConnecitonClosed) { + initializeFilter(); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::HeartBeat); + NiceMock connection; + EXPECT_CALL(connection, state()).Times(1).WillOnce(Invoke([&]() -> Network::Connection::State { + return Network::Connection::State::Closed; + })); + EXPECT_CALL(filter_callbacks_, connection()).WillRepeatedly(Invoke([&]() -> Network::Connection& { + return connection; + })); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.heartbeat").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnHeartbeatWithPurgeDirectiveTable) { + initializeFilter(); + + std::string broker_name = "broker_name"; + int32_t broker_id = 0; + std::chrono::milliseconds delay_0(31 * 1000); + AckMessageDirective directive_0(broker_name, broker_id, + conn_manager_->timeSource().monotonicTime() - delay_0); + std::string directive_key_0 = "key_0"; + conn_manager_->insertAckDirective(directive_key_0, directive_0); + + std::chrono::milliseconds delay_1(29 * 1000); + AckMessageDirective directive_1(broker_name, broker_id, + conn_manager_->timeSource().monotonicTime() - delay_1); + std::string directive_key_1 = "key_1"; + conn_manager_->insertAckDirective(directive_key_1, directive_1); + + EXPECT_EQ(2, conn_manager_->getAckDirectiveTableForTest().size()); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::HeartBeat); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.heartbeat").value()); + + EXPECT_EQ(1, conn_manager_->getAckDirectiveTableForTest().size()); + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnUnregisterClient) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + BufferUtility::fillRequestBuffer(buffer_, RequestCode::UnregisterClient); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.unregister").value()); + EXPECT_TRUE(group_members_map.empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnUnregisterClientWithGroupMembersMapExists) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("test_client_id", *conn_manager_); + group_member.setLastForTest(current_); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::UnregisterClient); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.unregister").value()); + EXPECT_FALSE(group_member.expired()); + EXPECT_TRUE(group_members_map.empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnUnregisterClientWithGroupMembersMapExistsButExpired) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("127.0.0.2@90330", *conn_manager_); + group_member.setLastForTest(current_ - std::chrono::seconds(31)); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::UnregisterClient); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.unregister").value()); + EXPECT_TRUE(group_member.expired()); + EXPECT_TRUE(group_members_map.empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, + OnUnregisterClientWithGroupMembersMapExistsButLackOfClientID) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("127.0.0.2@90330", *conn_manager_); + group_member.setLastForTest(current_); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::UnregisterClient); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.unregister").value()); + EXPECT_FALSE(group_member.expired()); + EXPECT_FALSE(group_members_map.empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnGetTopicRoute) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + auto metadata = std::make_shared(); + ProtobufWkt::Struct topic_route_data; + auto* fields = topic_route_data.mutable_fields(); + (*fields)[RocketmqConstants::get().ReadQueueNum] = ValueUtil::numberValue(4); + (*fields)[RocketmqConstants::get().WriteQueueNum] = ValueUtil::numberValue(4); + (*fields)[RocketmqConstants::get().ClusterName] = ValueUtil::stringValue("DefaultCluster"); + (*fields)[RocketmqConstants::get().BrokerName] = ValueUtil::stringValue("broker-a"); + (*fields)[RocketmqConstants::get().BrokerId] = ValueUtil::numberValue(0); + (*fields)[RocketmqConstants::get().Perm] = ValueUtil::numberValue(6); + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + NetworkFilterNames::get().RocketmqProxy, topic_route_data)); + host_->metadata(metadata); + initializeCluster(); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::GetRouteInfoByTopic); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.get_topic_route").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnGetTopicRouteWithoutRoutes) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_another_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::GetRouteInfoByTopic); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.get_topic_route").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnGetTopicRouteWithoutCluster) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + EXPECT_CALL(factory_context_.cluster_manager_, get(_)).WillRepeatedly(Return(nullptr)); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::GetRouteInfoByTopic); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.get_topic_route").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnGetTopicRouteInDevelopMode) { + const std::string yaml = R"EOF( +stat_prefix: test +develop_mode: true +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + NiceMock server_factory_context; + NiceMock local_info; + NiceMock ip; + std::shared_ptr instance = + std::make_shared("logical", "physical"); + EXPECT_CALL(factory_context_, getServerFactoryContext()) + .WillRepeatedly(ReturnRef(server_factory_context)); + EXPECT_CALL(server_factory_context, localInfo()).WillRepeatedly(ReturnRef(local_info)); + EXPECT_CALL(local_info, address()).WillRepeatedly(Return(instance)); + EXPECT_CALL(*instance, type()).WillRepeatedly(Return(Network::Address::Type::Ip)); + EXPECT_CALL(*instance, ip()).WillRepeatedly(testing::Return(&ip)); + const std::string address{"1.2.3.4"}; + EXPECT_CALL(ip, addressAsString()).WillRepeatedly(ReturnRef(address)); + EXPECT_CALL(ip, port()).WillRepeatedly(Return(1234)); + initializeFilter(yaml); + + auto metadata = std::make_shared(); + ProtobufWkt::Struct topic_route_data; + auto* fields = topic_route_data.mutable_fields(); + (*fields)[RocketmqConstants::get().ReadQueueNum] = ValueUtil::numberValue(4); + (*fields)[RocketmqConstants::get().WriteQueueNum] = ValueUtil::numberValue(4); + (*fields)[RocketmqConstants::get().ClusterName] = ValueUtil::stringValue("DefaultCluster"); + (*fields)[RocketmqConstants::get().BrokerName] = ValueUtil::stringValue("broker-a"); + (*fields)[RocketmqConstants::get().BrokerId] = ValueUtil::numberValue(0); + (*fields)[RocketmqConstants::get().Perm] = ValueUtil::numberValue(6); + metadata->mutable_filter_metadata()->insert(Protobuf::MapPair( + NetworkFilterNames::get().RocketmqProxy, topic_route_data)); + host_->metadata(metadata); + initializeCluster(); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::GetRouteInfoByTopic); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.get_topic_route").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnGetConsumerListByGroup) { + initializeFilter(); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::GetConsumerListByGroup); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.get_consumer_list").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnGetConsumerListByGroupWithGroupMemberMapExists) { + initializeFilter(); + + auto& group_members_map = conn_manager_->groupMembersForTest(); + std::vector group_members; + ConsumerGroupMember group_member("127.0.0.2@90330", *conn_manager_); + group_member.setLastForTest(current_ - std::chrono::seconds(31)); + group_members.emplace_back(group_member); + group_members_map["test_cg"] = group_members; + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::GetConsumerListByGroup); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.get_consumer_list").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnPopMessage) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::PopMessage); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.pop_message").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnAckMessage) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::AckMessage); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.ack_message").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnData) { + initializeFilter(); + + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(0, buffer_.length()); + EXPECT_EQ(0U, store_.counter("test.request").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnDataWithEndStream) { + initializeFilter(); + + Buffer::OwnedImpl buffer; + BufferUtility::fillRequestBuffer(buffer, RequestCode::SendMessageV2); + bool underflow, has_error; + RemotingCommandPtr request = Decoder::decode(buffer, underflow, has_error); + conn_manager_->createActiveMessage(request); + EXPECT_EQ(1, conn_manager_->activeMessageList().size()); + conn_manager_->onData(buffer_, true); + EXPECT_TRUE(conn_manager_->activeMessageList().empty()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnDataWithMinFrameSize) { + initializeFilter(); + + buffer_.add(std::string({'\x00', '\x00', '\x01', '\x8b'})); + buffer_.add(std::string({'\x00', '\x00', '\x01', '\x76'})); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(0U, store_.counter("test.request").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnDataSendMessage) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::SendMessage); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.send_message_v1").value()); + EXPECT_EQ( + 1U, + store_.gauge("test.send_message_v1_active", Stats::Gauge::ImportMode::Accumulate).value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnDataSendMessageV2) { + const std::string yaml = R"EOF( +stat_prefix: test +route_config: + name: default_route + routes: + - match: + topic: + exact: test_topic + route: + cluster: fake_cluster +)EOF"; + initializeFilter(yaml); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::SendMessageV2); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + EXPECT_EQ(1U, store_.counter("test.send_message_v2").value()); + EXPECT_EQ( + 1U, + store_.gauge("test.send_message_v2_active", Stats::Gauge::ImportMode::Accumulate).value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, OnDataWithUnsupportedCode) { + initializeFilter(); + + BufferUtility::fillRequestBuffer(buffer_, RequestCode::Unsupported); + EXPECT_EQ(conn_manager_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + + buffer_.drain(buffer_.length()); +} + +TEST_F(RocketmqConnectionManagerTest, ConsumerGroupMemberEqual) { + initializeFilter(); + + ConsumerGroupMember m1("abc", *conn_manager_); + ConsumerGroupMember m2("abc", *conn_manager_); + EXPECT_TRUE(m1 == m2); +} + +TEST_F(RocketmqConnectionManagerTest, ConsumerGroupMemberLessThan) { + initializeFilter(); + + ConsumerGroupMember m1("abc", *conn_manager_); + ConsumerGroupMember m2("def", *conn_manager_); + EXPECT_TRUE(m1 < m2); +} + +TEST_F(RocketmqConnectionManagerTest, ConsumerGroupMemberExpired) { + initializeFilter(); + + ConsumerGroupMember member("Mock", *conn_manager_); + EXPECT_FALSE(member.expired()); + EXPECT_STREQ("Mock", member.clientId().data()); +} + +TEST_F(RocketmqConnectionManagerTest, ConsumerGroupMemberRefresh) { + initializeFilter(); + + ConsumerGroupMember member("Mock", *conn_manager_); + EXPECT_FALSE(member.expired()); + member.setLastForTest(current_ - std::chrono::seconds(31)); + EXPECT_TRUE(member.expired()); + member.refresh(); + EXPECT_FALSE(member.expired()); +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/rocketmq_proxy/mocks.cc b/test/extensions/filters/network/rocketmq_proxy/mocks.cc new file mode 100644 index 000000000000..d346364491d7 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/mocks.cc @@ -0,0 +1,57 @@ +#include "test/extensions/filters/network/rocketmq_proxy/mocks.h" + +#include "extensions/filters/network/rocketmq_proxy/router/router_impl.h" + +#include "gtest/gtest.h" + +using testing::_; +using testing::ByMove; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +MockActiveMessage::MockActiveMessage(ConnectionManager& conn_manager, RemotingCommandPtr&& request) + : ActiveMessage(conn_manager, std::move(request)) { + route_ = std::make_shared>(); + + ON_CALL(*this, onError(_)).WillByDefault(Invoke([&](absl::string_view error_message) { + ActiveMessage::onError(error_message); + })); + ON_CALL(*this, onReset()).WillByDefault(Return()); + ON_CALL(*this, sendResponseToDownstream()).WillByDefault(Invoke([&]() { + ActiveMessage::sendResponseToDownstream(); + })); + ON_CALL(*this, metadata()).WillByDefault(Invoke([&]() { return ActiveMessage::metadata(); })); + ON_CALL(*this, route()).WillByDefault(Return(route_)); +} +MockActiveMessage::~MockActiveMessage() = default; + +MockConfig::MockConfig() : stats_(RocketmqFilterStats::generateStats("test.", store_)) { + ON_CALL(*this, stats()).WillByDefault(ReturnRef(stats_)); + ON_CALL(*this, clusterManager()).WillByDefault(ReturnRef(cluster_manager_)); + ON_CALL(*this, createRouter()) + .WillByDefault(Return(ByMove(std::make_unique(cluster_manager_)))); + ON_CALL(*this, developMode()).WillByDefault(Return(false)); + ON_CALL(*this, proxyAddress()).WillByDefault(Return(std::string{"1.2.3.4:1234"})); +} + +namespace Router { + +MockRouteEntry::MockRouteEntry() { + ON_CALL(*this, clusterName()).WillByDefault(ReturnRef(cluster_name_)); +} + +MockRouteEntry::~MockRouteEntry() = default; + +MockRoute::MockRoute() { ON_CALL(*this, routeEntry()).WillByDefault(Return(&route_entry_)); } +MockRoute::~MockRoute() = default; + +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/mocks.h b/test/extensions/filters/network/rocketmq_proxy/mocks.h new file mode 100644 index 000000000000..a6cc6a05dd4c --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/mocks.h @@ -0,0 +1,89 @@ +#pragma once + +#include "extensions/filters/network/rocketmq_proxy/active_message.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" + +#include "test/mocks/server/mocks.h" +#include "test/mocks/upstream/mocks.h" + +#include "gmock/gmock.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +namespace Router { +class MockRoute; +} // namespace Router + +class MockActiveMessage : public ActiveMessage { +public: + MockActiveMessage(ConnectionManager& conn_manager, RemotingCommandPtr&& request); + ~MockActiveMessage() override; + + MOCK_METHOD(void, createFilterChain, ()); + MOCK_METHOD(void, sendRequestToUpstream, ()); + MOCK_METHOD(RemotingCommandPtr&, downstreamRequest, ()); + MOCK_METHOD(void, sendResponseToDownstream, ()); + MOCK_METHOD(void, onQueryTopicRoute, ()); + MOCK_METHOD(void, onError, (absl::string_view)); + MOCK_METHOD(ConnectionManager&, connectionManager, ()); + MOCK_METHOD(void, onReset, ()); + MOCK_METHOD(bool, onUpstreamData, + (Buffer::Instance&, bool, Tcp::ConnectionPool::ConnectionDataPtr&)); + MOCK_METHOD(MessageMetadataSharedPtr, metadata, (), (const)); + MOCK_METHOD(Router::RouteConstSharedPtr, route, ()); + + std::shared_ptr route_; +}; + +class MockConfig : public Config { +public: + MockConfig(); + ~MockConfig() override = default; + + MOCK_METHOD(RocketmqFilterStats&, stats, ()); + MOCK_METHOD(Upstream::ClusterManager&, clusterManager, ()); + MOCK_METHOD(Router::RouterPtr, createRouter, ()); + MOCK_METHOD(bool, developMode, (), (const)); + MOCK_METHOD(std::string, proxyAddress, ()); + MOCK_METHOD(Router::Config&, routerConfig, ()); + +private: + Stats::IsolatedStoreImpl store_; + RocketmqFilterStats stats_; + NiceMock cluster_manager_; + Router::RouterPtr router_; +}; + +namespace Router { + +class MockRouteEntry : public RouteEntry { +public: + MockRouteEntry(); + ~MockRouteEntry() override; + + // RocketmqProxy::Router::RouteEntry + MOCK_METHOD(const std::string&, clusterName, (), (const)); + MOCK_METHOD(Envoy::Router::MetadataMatchCriteria*, metadataMatchCriteria, (), (const)); + + std::string cluster_name_{"fake_cluster"}; +}; + +class MockRoute : public Route { +public: + MockRoute(); + ~MockRoute() override; + + // RocketmqProxy::Router::Route + MOCK_METHOD(const RouteEntry*, routeEntry, (), (const)); + + NiceMock route_entry_; +}; +} // namespace Router + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/protocol_test.cc b/test/extensions/filters/network/rocketmq_proxy/protocol_test.cc new file mode 100644 index 000000000000..ac2aa63a0d81 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/protocol_test.cc @@ -0,0 +1,927 @@ +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/rocketmq_proxy/protocol.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class UnregisterClientRequestHeaderTest : public testing::Test { +public: + std::string client_id_{"SampleClient_01"}; + std::string producer_group_{"PG_Example_01"}; + std::string consumer_group_{"CG_001"}; +}; + +TEST_F(UnregisterClientRequestHeaderTest, Encode) { + UnregisterClientRequestHeader request_header; + request_header.clientId(client_id_); + request_header.producerGroup(producer_group_); + request_header.consumerGroup(consumer_group_); + + ProtobufWkt::Value doc; + request_header.encode(doc); + + const auto& members = doc.struct_value().fields(); + EXPECT_STREQ(client_id_.c_str(), members.at("clientID").string_value().c_str()); + EXPECT_STREQ(producer_group_.c_str(), members.at("producerGroup").string_value().c_str()); + EXPECT_STREQ(consumer_group_.c_str(), members.at("consumerGroup").string_value().c_str()); +} + +TEST_F(UnregisterClientRequestHeaderTest, Decode) { + + std::string json = R"EOF( + { + "clientID": "SampleClient_01", + "producerGroup": "PG_Example_01", + "consumerGroup": "CG_001" + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + UnregisterClientRequestHeader unregister_client_request_header; + unregister_client_request_header.decode(doc); + EXPECT_STREQ(client_id_.c_str(), unregister_client_request_header.clientId().c_str()); + EXPECT_STREQ(producer_group_.c_str(), unregister_client_request_header.producerGroup().c_str()); + EXPECT_STREQ(consumer_group_.c_str(), unregister_client_request_header.consumerGroup().c_str()); +} + +TEST(GetConsumerListByGroupResponseBodyTest, Encode) { + GetConsumerListByGroupResponseBody response_body; + response_body.add("localhost@1"); + response_body.add("localhost@2"); + + ProtobufWkt::Struct doc; + response_body.encode(doc); + + const auto& members = doc.fields(); + EXPECT_TRUE(members.contains("consumerIdList")); + EXPECT_EQ(2, members.at("consumerIdList").list_value().values_size()); +} + +class AckMessageRequestHeaderTest : public testing::Test { +public: + std::string consumer_group{"CG_Unit_Test"}; + std::string topic{"T_UnitTest"}; + int32_t queue_id{1}; + std::string extra_info{"extra_info_UT"}; + int64_t offset{100}; +}; + +TEST_F(AckMessageRequestHeaderTest, Encode) { + AckMessageRequestHeader ack_header; + ack_header.consumerGroup(consumer_group); + ack_header.topic(topic); + ack_header.queueId(queue_id); + ack_header.extraInfo(extra_info); + ack_header.offset(offset); + + ProtobufWkt::Value doc; + ack_header.encode(doc); + + const auto& members = doc.struct_value().fields(); + + EXPECT_TRUE(members.contains("consumerGroup")); + EXPECT_STREQ(consumer_group.c_str(), members.at("consumerGroup").string_value().c_str()); + + EXPECT_TRUE(members.contains("topic")); + EXPECT_STREQ(topic.c_str(), members.at("topic").string_value().c_str()); + + EXPECT_TRUE(members.contains("queueId")); + EXPECT_EQ(queue_id, members.at("queueId").number_value()); + + EXPECT_TRUE(members.contains("extraInfo")); + EXPECT_STREQ(extra_info.c_str(), members.at("extraInfo").string_value().c_str()); + + EXPECT_TRUE(members.contains("offset")); + EXPECT_EQ(offset, members.at("offset").number_value()); +} + +TEST_F(AckMessageRequestHeaderTest, Decode) { + std::string json = R"EOF( + { + "consumerGroup": "CG_Unit_Test", + "topic": "T_UnitTest", + "queueId": 1, + "extraInfo": "extra_info_UT", + "offset": 100 + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + + AckMessageRequestHeader ack_header; + ack_header.decode(doc); + ASSERT_STREQ(consumer_group.c_str(), ack_header.consumerGroup().data()); + ASSERT_STREQ(topic.c_str(), ack_header.topic().c_str()); + ASSERT_EQ(queue_id, ack_header.queueId()); + ASSERT_STREQ(extra_info.c_str(), ack_header.extraInfo().data()); + ASSERT_EQ(offset, ack_header.offset()); +} + +TEST_F(AckMessageRequestHeaderTest, DecodeNumSerializedAsString) { + std::string json = R"EOF( + { + "consumerGroup": "CG_Unit_Test", + "topic": "T_UnitTest", + "queueId": "1", + "extraInfo": "extra_info_UT", + "offset": "100" + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + + AckMessageRequestHeader ack_header; + ack_header.decode(doc); + ASSERT_STREQ(consumer_group.c_str(), ack_header.consumerGroup().data()); + ASSERT_STREQ(topic.c_str(), ack_header.topic().c_str()); + ASSERT_EQ(queue_id, ack_header.queueId()); + ASSERT_STREQ(extra_info.c_str(), ack_header.extraInfo().data()); + ASSERT_EQ(offset, ack_header.offset()); +} + +class PopMessageRequestHeaderTest : public testing::Test { +public: + std::string consumer_group{"CG_UT"}; + std::string topic{"T_UT"}; + int32_t queue_id{1}; + int32_t max_msg_nums{2}; + int64_t invisible_time{3}; + int64_t poll_time{4}; + int64_t born_time{5}; + int32_t init_mode{6}; + + std::string exp_type{"exp_type_UT"}; + std::string exp{"exp_UT"}; +}; + +TEST_F(PopMessageRequestHeaderTest, Encode) { + PopMessageRequestHeader pop_request_header; + pop_request_header.consumerGroup(consumer_group); + pop_request_header.topic(topic); + pop_request_header.queueId(queue_id); + pop_request_header.maxMsgNum(max_msg_nums); + pop_request_header.invisibleTime(invisible_time); + pop_request_header.pollTime(poll_time); + pop_request_header.bornTime(born_time); + pop_request_header.initMode(init_mode); + pop_request_header.expType(exp_type); + pop_request_header.exp(exp); + + ProtobufWkt::Value doc; + pop_request_header.encode(doc); + + const auto& members = doc.struct_value().fields(); + + EXPECT_TRUE(members.contains("consumerGroup")); + EXPECT_STREQ(consumer_group.c_str(), members.at("consumerGroup").string_value().c_str()); + + EXPECT_TRUE(members.contains("topic")); + EXPECT_STREQ(topic.c_str(), members.at("topic").string_value().c_str()); + + EXPECT_TRUE(members.contains("queueId")); + EXPECT_EQ(queue_id, members.at("queueId").number_value()); + + EXPECT_TRUE(members.contains("maxMsgNums")); + EXPECT_EQ(max_msg_nums, members.at("maxMsgNums").number_value()); + + EXPECT_TRUE(members.contains("invisibleTime")); + EXPECT_EQ(invisible_time, members.at("invisibleTime").number_value()); + + EXPECT_TRUE(members.contains("pollTime")); + EXPECT_EQ(poll_time, members.at("pollTime").number_value()); + + EXPECT_TRUE(members.contains("bornTime")); + EXPECT_EQ(born_time, members.at("bornTime").number_value()); + + EXPECT_TRUE(members.contains("initMode")); + EXPECT_EQ(init_mode, members.at("initMode").number_value()); + + EXPECT_TRUE(members.contains("expType")); + EXPECT_STREQ(exp_type.c_str(), members.at("expType").string_value().c_str()); + + EXPECT_TRUE(members.contains("exp")); + EXPECT_STREQ(exp.c_str(), members.at("exp").string_value().c_str()); +} + +TEST_F(PopMessageRequestHeaderTest, Decode) { + std::string json = R"EOF( + { + "consumerGroup": "CG_UT", + "topic": "T_UT", + "queueId": 1, + "maxMsgNums": 2, + "invisibleTime": 3, + "pollTime": 4, + "bornTime": 5, + "initMode": 6, + "expType": "exp_type_UT", + "exp": "exp_UT" + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + PopMessageRequestHeader pop_request_header; + pop_request_header.decode(doc); + + ASSERT_STREQ(consumer_group.c_str(), pop_request_header.consumerGroup().data()); + ASSERT_STREQ(topic.c_str(), pop_request_header.topic().c_str()); + ASSERT_EQ(queue_id, pop_request_header.queueId()); + ASSERT_EQ(max_msg_nums, pop_request_header.maxMsgNum()); + ASSERT_EQ(invisible_time, pop_request_header.invisibleTime()); + ASSERT_EQ(poll_time, pop_request_header.pollTime()); + ASSERT_EQ(born_time, pop_request_header.bornTime()); + ASSERT_EQ(init_mode, pop_request_header.initMode()); + ASSERT_STREQ(exp_type.c_str(), pop_request_header.expType().c_str()); + ASSERT_STREQ(exp.c_str(), pop_request_header.exp().c_str()); +} + +TEST_F(PopMessageRequestHeaderTest, DecodeNumSerializedAsString) { + std::string json = R"EOF( + { + "consumerGroup": "CG_UT", + "topic": "T_UT", + "queueId": "1", + "maxMsgNums": "2", + "invisibleTime": "3", + "pollTime": "4", + "bornTime": "5", + "initMode": "6", + "expType": "exp_type_UT", + "exp": "exp_UT" + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + PopMessageRequestHeader pop_request_header; + pop_request_header.decode(doc); + + ASSERT_STREQ(consumer_group.c_str(), pop_request_header.consumerGroup().data()); + ASSERT_STREQ(topic.c_str(), pop_request_header.topic().c_str()); + ASSERT_EQ(queue_id, pop_request_header.queueId()); + ASSERT_EQ(max_msg_nums, pop_request_header.maxMsgNum()); + ASSERT_EQ(invisible_time, pop_request_header.invisibleTime()); + ASSERT_EQ(poll_time, pop_request_header.pollTime()); + ASSERT_EQ(born_time, pop_request_header.bornTime()); + ASSERT_EQ(init_mode, pop_request_header.initMode()); + ASSERT_STREQ(exp_type.c_str(), pop_request_header.expType().c_str()); + ASSERT_STREQ(exp.c_str(), pop_request_header.exp().c_str()); +} + +class PopMessageResponseHeaderTest : public testing::Test { +public: + int64_t pop_time{1}; + int64_t invisible_time{2}; + int32_t revive_qid{3}; + int64_t rest_num{4}; + + std::string start_offset_info{"start"}; + std::string msg_offset_info{"msg"}; + std::string order_count_info{"order"}; +}; + +TEST_F(PopMessageResponseHeaderTest, Encode) { + PopMessageResponseHeader pop_response_header; + pop_response_header.popTime(pop_time); + pop_response_header.invisibleTime(invisible_time); + pop_response_header.reviveQid(revive_qid); + pop_response_header.restNum(rest_num); + pop_response_header.startOffsetInfo(start_offset_info); + pop_response_header.msgOffsetInfo(msg_offset_info); + pop_response_header.orderCountInfo(order_count_info); + + ProtobufWkt::Value doc; + pop_response_header.encode(doc); + + const auto& members = doc.struct_value().fields(); + + EXPECT_TRUE(members.contains("popTime")); + EXPECT_TRUE(members.contains("invisibleTime")); + EXPECT_TRUE(members.contains("reviveQid")); + EXPECT_TRUE(members.contains("restNum")); + EXPECT_TRUE(members.contains("startOffsetInfo")); + EXPECT_TRUE(members.contains("msgOffsetInfo")); + EXPECT_TRUE(members.contains("orderCountInfo")); + + EXPECT_EQ(pop_time, members.at("popTime").number_value()); + EXPECT_EQ(invisible_time, members.at("invisibleTime").number_value()); + EXPECT_EQ(revive_qid, members.at("reviveQid").number_value()); + EXPECT_EQ(rest_num, members.at("restNum").number_value()); + EXPECT_STREQ(start_offset_info.c_str(), members.at("startOffsetInfo").string_value().c_str()); + EXPECT_STREQ(msg_offset_info.c_str(), members.at("msgOffsetInfo").string_value().c_str()); + EXPECT_STREQ(order_count_info.c_str(), members.at("orderCountInfo").string_value().c_str()); +} + +TEST_F(PopMessageResponseHeaderTest, Decode) { + std::string json = R"EOF( + { + "popTime": 1, + "invisibleTime": 2, + "reviveQid": 3, + "restNum": 4, + "startOffsetInfo": "start", + "msgOffsetInfo": "msg", + "orderCountInfo": "order" + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + + PopMessageResponseHeader header; + header.decode(doc); + + EXPECT_EQ(pop_time, header.popTimeForTest()); + EXPECT_EQ(invisible_time, header.invisibleTime()); + EXPECT_EQ(revive_qid, header.reviveQid()); + EXPECT_EQ(rest_num, header.restNum()); + + EXPECT_STREQ(start_offset_info.c_str(), header.startOffsetInfo().data()); + EXPECT_STREQ(msg_offset_info.c_str(), header.msgOffsetInfo().data()); + EXPECT_STREQ(order_count_info.c_str(), header.orderCountInfo().data()); +} + +TEST_F(PopMessageResponseHeaderTest, DecodeNumSerializedAsString) { + std::string json = R"EOF( + { + "popTime": "1", + "invisibleTime": "2", + "reviveQid": "3", + "restNum": "4", + "startOffsetInfo": "start", + "msgOffsetInfo": "msg", + "orderCountInfo": "order" + } + )EOF"; + + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + + PopMessageResponseHeader header; + header.decode(doc); + + EXPECT_EQ(pop_time, header.popTimeForTest()); + EXPECT_EQ(invisible_time, header.invisibleTime()); + EXPECT_EQ(revive_qid, header.reviveQid()); + EXPECT_EQ(rest_num, header.restNum()); + + EXPECT_STREQ(start_offset_info.c_str(), header.startOffsetInfo().data()); + EXPECT_STREQ(msg_offset_info.c_str(), header.msgOffsetInfo().data()); + EXPECT_STREQ(order_count_info.c_str(), header.orderCountInfo().data()); +} + +class SendMessageResponseHeaderTest : public testing::Test { +public: + SendMessageResponseHeader response_header_; +}; + +TEST_F(SendMessageResponseHeaderTest, Encode) { + response_header_.msgIdForTest("MSG_ID_01"); + response_header_.queueId(1); + response_header_.queueOffset(100); + response_header_.transactionId("TX_01"); + ProtobufWkt::Value doc; + response_header_.encode(doc); + + const auto& members = doc.struct_value().fields(); + EXPECT_TRUE(members.contains("msgId")); + EXPECT_TRUE(members.contains("queueId")); + EXPECT_TRUE(members.contains("queueOffset")); + EXPECT_TRUE(members.contains("transactionId")); + + EXPECT_STREQ("MSG_ID_01", members.at("msgId").string_value().c_str()); + EXPECT_STREQ("TX_01", members.at("transactionId").string_value().c_str()); + EXPECT_EQ(1, members.at("queueId").number_value()); + EXPECT_EQ(100, members.at("queueOffset").number_value()); +} + +TEST_F(SendMessageResponseHeaderTest, Decode) { + std::string json = R"EOF( + { + "msgId": "abc", + "queueId": 1, + "queueOffset": 10, + "transactionId": "TX_1" + } + )EOF"; + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + response_header_.decode(doc); + EXPECT_STREQ("abc", response_header_.msgId().c_str()); + EXPECT_EQ(1, response_header_.queueId()); + EXPECT_EQ(10, response_header_.queueOffset()); + EXPECT_STREQ("TX_1", response_header_.transactionId().c_str()); +} + +TEST_F(SendMessageResponseHeaderTest, DecodeNumSerializedAsString) { + std::string json = R"EOF( + { + "msgId": "abc", + "queueId": "1", + "queueOffset": "10", + "transactionId": "TX_1" + } + )EOF"; + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + response_header_.decode(doc); + EXPECT_STREQ("abc", response_header_.msgId().c_str()); + EXPECT_EQ(1, response_header_.queueId()); + EXPECT_EQ(10, response_header_.queueOffset()); + EXPECT_STREQ("TX_1", response_header_.transactionId().c_str()); +} + +class SendMessageRequestHeaderTest : public testing::Test {}; + +TEST_F(SendMessageRequestHeaderTest, EncodeDefault) { + SendMessageRequestHeader header; + ProtobufWkt::Value doc; + header.encode(doc); + const auto& members = doc.struct_value().fields(); + EXPECT_TRUE(members.contains("producerGroup")); + EXPECT_TRUE(members.contains("topic")); + EXPECT_TRUE(members.contains("defaultTopic")); + EXPECT_TRUE(members.contains("defaultTopicQueueNums")); + EXPECT_TRUE(members.contains("queueId")); + EXPECT_TRUE(members.contains("sysFlag")); + EXPECT_TRUE(members.contains("bornTimestamp")); + EXPECT_TRUE(members.contains("flag")); + EXPECT_FALSE(members.contains("properties")); + EXPECT_FALSE(members.contains("reconsumeTimes")); + EXPECT_FALSE(members.contains("unitMode")); + EXPECT_FALSE(members.contains("batch")); + EXPECT_FALSE(members.contains("maxReconsumeTimes")); +} + +TEST_F(SendMessageRequestHeaderTest, EncodeOptional) { + SendMessageRequestHeader header; + header.properties("mock"); + header.reconsumeTimes(1); + header.unitMode(true); + header.batch(true); + header.maxReconsumeTimes(32); + ProtobufWkt::Value doc; + header.encode(doc); + const auto& members = doc.struct_value().fields(); + EXPECT_TRUE(members.contains("producerGroup")); + EXPECT_TRUE(members.contains("topic")); + EXPECT_TRUE(members.contains("defaultTopic")); + EXPECT_TRUE(members.contains("defaultTopicQueueNums")); + EXPECT_TRUE(members.contains("queueId")); + EXPECT_TRUE(members.contains("sysFlag")); + EXPECT_TRUE(members.contains("bornTimestamp")); + EXPECT_TRUE(members.contains("flag")); + EXPECT_TRUE(members.contains("properties")); + EXPECT_TRUE(members.contains("reconsumeTimes")); + EXPECT_TRUE(members.contains("unitMode")); + EXPECT_TRUE(members.contains("batch")); + EXPECT_TRUE(members.contains("maxReconsumeTimes")); + + EXPECT_STREQ("mock", members.at("properties").string_value().c_str()); + EXPECT_EQ(1, members.at("reconsumeTimes").number_value()); + EXPECT_TRUE(members.at("unitMode").bool_value()); + EXPECT_TRUE(members.at("batch").bool_value()); + EXPECT_EQ(32, members.at("maxReconsumeTimes").number_value()); +} + +TEST_F(SendMessageRequestHeaderTest, EncodeDefaultV2) { + SendMessageRequestHeader header; + header.version(SendMessageRequestVersion::V2); + ProtobufWkt::Value doc; + header.encode(doc); + const auto& members = doc.struct_value().fields(); + EXPECT_TRUE(members.contains("a")); + EXPECT_TRUE(members.contains("b")); + EXPECT_TRUE(members.contains("c")); + EXPECT_TRUE(members.contains("d")); + EXPECT_TRUE(members.contains("e")); + EXPECT_TRUE(members.contains("f")); + EXPECT_TRUE(members.contains("g")); + EXPECT_TRUE(members.contains("h")); + EXPECT_FALSE(members.contains("i")); + EXPECT_FALSE(members.contains("j")); + EXPECT_FALSE(members.contains("k")); + EXPECT_FALSE(members.contains("l")); + EXPECT_FALSE(members.contains("m")); +} + +TEST_F(SendMessageRequestHeaderTest, EncodeOptionalV2) { + SendMessageRequestHeader header; + header.properties("mock"); + header.reconsumeTimes(1); + header.unitMode(true); + header.batch(true); + header.maxReconsumeTimes(32); + header.version(SendMessageRequestVersion::V2); + ProtobufWkt::Value doc; + header.encode(doc); + + const auto& members = doc.struct_value().fields(); + EXPECT_TRUE(members.contains("a")); + EXPECT_TRUE(members.contains("b")); + EXPECT_TRUE(members.contains("c")); + EXPECT_TRUE(members.contains("d")); + EXPECT_TRUE(members.contains("e")); + EXPECT_TRUE(members.contains("f")); + EXPECT_TRUE(members.contains("g")); + EXPECT_TRUE(members.contains("h")); + EXPECT_TRUE(members.contains("i")); + EXPECT_TRUE(members.contains("j")); + EXPECT_TRUE(members.contains("k")); + EXPECT_TRUE(members.contains("l")); + EXPECT_TRUE(members.contains("m")); + + EXPECT_STREQ("mock", members.at("i").string_value().c_str()); + EXPECT_EQ(1, members.at("j").number_value()); + EXPECT_TRUE(members.at("k").bool_value()); + EXPECT_TRUE(members.at("m").bool_value()); + EXPECT_EQ(32, members.at("l").number_value()); +} + +TEST_F(SendMessageRequestHeaderTest, EncodeV3) { + SendMessageRequestHeader header; + header.version(SendMessageRequestVersion::V3); + ProtobufWkt::Value doc; + header.encode(doc); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV1) { + std::string json = R"EOF( + { + "batch": false, + "bornTimestamp": 1575872212297, + "defaultTopic": "TBW102", + "defaultTopicQueueNums": 3, + "flag": 124, + "producerGroup": "FooBarGroup", + "queueId": 1, + "reconsumeTimes": 0, + "sysFlag": 0, + "topic": "FooBar", + "unitMode": false + } + )EOF"; + + SendMessageRequestHeader header; + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.decode(doc); + EXPECT_STREQ("FooBar", header.topic().c_str()); + EXPECT_EQ(1, header.queueId()); + EXPECT_STREQ("FooBarGroup", header.producerGroup().c_str()); + EXPECT_STREQ("TBW102", header.defaultTopic().c_str()); + EXPECT_EQ(3, header.defaultTopicQueueNumber()); + EXPECT_EQ(0, header.sysFlag()); + EXPECT_EQ(1575872212297, header.bornTimestamp()); + EXPECT_EQ(124, header.flag()); + EXPECT_STREQ("", header.properties().c_str()); + EXPECT_EQ(0, header.reconsumeTimes()); + EXPECT_FALSE(header.unitMode()); + EXPECT_FALSE(header.batch()); + EXPECT_EQ(0, header.maxReconsumeTimes()); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV1Optional) { + std::string json = R"EOF( + { + "batch": false, + "bornTimestamp": 1575872212297, + "defaultTopic": "TBW102", + "defaultTopicQueueNums": 3, + "flag": 124, + "producerGroup": "FooBarGroup", + "queueId": 1, + "reconsumeTimes": 0, + "sysFlag": 0, + "topic": "FooBar", + "unitMode": false, + "properties": "mock_properties", + "maxReconsumeTimes": 32 + } + )EOF"; + + SendMessageRequestHeader header; + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.decode(doc); + EXPECT_STREQ("FooBar", header.topic().c_str()); + EXPECT_EQ(1, header.queueId()); + EXPECT_STREQ("FooBarGroup", header.producerGroup().c_str()); + EXPECT_STREQ("TBW102", header.defaultTopic().c_str()); + EXPECT_EQ(3, header.defaultTopicQueueNumber()); + EXPECT_EQ(0, header.sysFlag()); + EXPECT_EQ(1575872212297, header.bornTimestamp()); + EXPECT_EQ(124, header.flag()); + EXPECT_STREQ("mock_properties", header.properties().c_str()); + EXPECT_EQ(0, header.reconsumeTimes()); + EXPECT_FALSE(header.unitMode()); + EXPECT_FALSE(header.batch()); + EXPECT_EQ(32, header.maxReconsumeTimes()); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV1OptionalNumSerializedAsString) { + std::string json = R"EOF( + { + "batch": "false", + "bornTimestamp": "1575872212297", + "defaultTopic": "TBW102", + "defaultTopicQueueNums": "3", + "flag": "124", + "producerGroup": "FooBarGroup", + "queueId": "1", + "reconsumeTimes": "0", + "sysFlag": "0", + "topic": "FooBar", + "unitMode": "false", + "properties": "mock_properties", + "maxReconsumeTimes": "32" + } + )EOF"; + + SendMessageRequestHeader header; + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.decode(doc); + EXPECT_STREQ("FooBar", header.topic().c_str()); + EXPECT_EQ(1, header.queueId()); + EXPECT_STREQ("FooBarGroup", header.producerGroup().c_str()); + EXPECT_STREQ("TBW102", header.defaultTopic().c_str()); + EXPECT_EQ(3, header.defaultTopicQueueNumber()); + EXPECT_EQ(0, header.sysFlag()); + EXPECT_EQ(1575872212297, header.bornTimestamp()); + EXPECT_EQ(124, header.flag()); + EXPECT_STREQ("mock_properties", header.properties().c_str()); + EXPECT_EQ(0, header.reconsumeTimes()); + EXPECT_FALSE(header.unitMode()); + EXPECT_FALSE(header.batch()); + EXPECT_EQ(32, header.maxReconsumeTimes()); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV2) { + std::string json = R"EOF( + { + "a": "FooBarGroup", + "b": "FooBar", + "c": "TBW102", + "d": 3, + "e": 1, + "f": 0, + "g": 1575872563203, + "h": 124, + "j": 0, + "k": false, + "m": false + } + )EOF"; + + SendMessageRequestHeader header; + header.version(SendMessageRequestVersion::V2); + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.decode(doc); + EXPECT_STREQ("FooBar", header.topic().c_str()); + EXPECT_EQ(1, header.queueId()); + EXPECT_STREQ("FooBarGroup", header.producerGroup().c_str()); + EXPECT_STREQ("TBW102", header.defaultTopic().c_str()); + EXPECT_EQ(3, header.defaultTopicQueueNumber()); + EXPECT_EQ(0, header.sysFlag()); + EXPECT_EQ(1575872563203, header.bornTimestamp()); + EXPECT_EQ(124, header.flag()); + EXPECT_STREQ("", header.properties().c_str()); + EXPECT_EQ(0, header.reconsumeTimes()); + EXPECT_FALSE(header.unitMode()); + EXPECT_FALSE(header.batch()); + EXPECT_EQ(0, header.maxReconsumeTimes()); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV2Optional) { + std::string json = R"EOF( + { + "a": "FooBarGroup", + "b": "FooBar", + "c": "TBW102", + "d": 3, + "e": 1, + "f": 0, + "g": 1575872563203, + "h": 124, + "i": "mock_properties", + "j": 0, + "k": false, + "l": 1, + "m": false + } + )EOF"; + + SendMessageRequestHeader header; + header.version(SendMessageRequestVersion::V2); + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.decode(doc); + EXPECT_STREQ("FooBar", header.topic().c_str()); + EXPECT_EQ(1, header.queueId()); + EXPECT_STREQ("FooBarGroup", header.producerGroup().c_str()); + EXPECT_STREQ("TBW102", header.defaultTopic().c_str()); + EXPECT_EQ(3, header.defaultTopicQueueNumber()); + EXPECT_EQ(0, header.sysFlag()); + EXPECT_EQ(1575872563203, header.bornTimestamp()); + EXPECT_EQ(124, header.flag()); + EXPECT_STREQ("mock_properties", header.properties().c_str()); + EXPECT_EQ(0, header.reconsumeTimes()); + EXPECT_FALSE(header.unitMode()); + EXPECT_FALSE(header.batch()); + EXPECT_EQ(1, header.maxReconsumeTimes()); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV2OptionalNumSerializedAsString) { + std::string json = R"EOF( + { + "a": "FooBarGroup", + "b": "FooBar", + "c": "TBW102", + "d": "3", + "e": "1", + "f": "0", + "g": "1575872563203", + "h": "124", + "i": "mock_properties", + "j": "0", + "k": "false", + "l": "1", + "m": "false" + } + )EOF"; + + SendMessageRequestHeader header; + header.version(SendMessageRequestVersion::V2); + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.decode(doc); + EXPECT_STREQ("FooBar", header.topic().c_str()); + EXPECT_EQ(1, header.queueId()); + EXPECT_STREQ("FooBarGroup", header.producerGroup().c_str()); + EXPECT_STREQ("TBW102", header.defaultTopic().c_str()); + EXPECT_EQ(3, header.defaultTopicQueueNumber()); + EXPECT_EQ(0, header.sysFlag()); + EXPECT_EQ(1575872563203, header.bornTimestamp()); + EXPECT_EQ(124, header.flag()); + EXPECT_STREQ("mock_properties", header.properties().c_str()); + EXPECT_EQ(0, header.reconsumeTimes()); + EXPECT_FALSE(header.unitMode()); + EXPECT_FALSE(header.batch()); + EXPECT_EQ(1, header.maxReconsumeTimes()); +} + +TEST_F(SendMessageRequestHeaderTest, DecodeV3) { + std::string json = R"EOF( + { + "batch": false, + "bornTimestamp": 1575872212297, + "defaultTopic": "TBW102", + "defaultTopicQueueNums": 3, + "flag": 124, + "producerGroup": "FooBarGroup", + "queueId": 1, + "reconsumeTimes": 0, + "sysFlag": 0, + "topic": "FooBar", + "unitMode": false + } + )EOF"; + + SendMessageRequestHeader header; + ProtobufWkt::Value doc; + MessageUtil::loadFromJson(json, *(doc.mutable_struct_value())); + header.version(SendMessageRequestVersion::V3); + header.decode(doc); +} + +class HeartbeatDataTest : public testing::Test { +public: + HeartbeatData data_; +}; + +TEST_F(HeartbeatDataTest, Decoding) { + std::string json = R"EOF( + { + "clientID": "127.0.0.1@23606", + "consumerDataSet": [ + { + "consumeFromWhere": "CONSUME_FROM_LAST_OFFSET", + "consumeType": "CONSUME_ACTIVELY", + "groupName": "please_rename_unique_group_name_4", + "messageModel": "CLUSTERING", + "subscriptionDataSet": [ + { + "classFilterMode": false, + "codeSet": [], + "expressionType": "TAG", + "subString": "*", + "subVersion": 0, + "tagsSet": [], + "topic": "test_topic" + } + ], + "unitMode": false + } + ], + "producerDataSet": [ + { + "groupName": "CLIENT_INNER_PRODUCER" + } + ] + } + )EOF"; + + const char* clientId = "127.0.0.1@23606"; + const char* consumerGroup = "please_rename_unique_group_name_4"; + + HeartbeatData heart_beat_data; + ProtobufWkt::Struct doc; + MessageUtil::loadFromJson(json, doc); + + heart_beat_data.decode(doc); + EXPECT_STREQ(clientId, heart_beat_data.clientId().c_str()); + EXPECT_EQ(1, heart_beat_data.consumerGroups().size()); + EXPECT_STREQ(consumerGroup, heart_beat_data.consumerGroups()[0].c_str()); +} + +TEST_F(HeartbeatDataTest, DecodeClientIdMissing) { + std::string json = R"EOF( + { + "consumerDataSet": [ + { + "consumeFromWhere": "CONSUME_FROM_LAST_OFFSET", + "consumeType": "CONSUME_ACTIVELY", + "groupName": "please_rename_unique_group_name_4", + "messageModel": "CLUSTERING", + "subscriptionDataSet": [ + { + "classFilterMode": false, + "codeSet": [], + "expressionType": "TAG", + "subString": "*", + "subVersion": 0, + "tagsSet": [], + "topic": "test_topic" + } + ], + "unitMode": false + } + ], + "producerDataSet": [ + { + "groupName": "CLIENT_INNER_PRODUCER" + } + ] + } + )EOF"; + + ProtobufWkt::Struct doc; + MessageUtil::loadFromJson(json, doc); + EXPECT_FALSE(data_.decode(doc)); +} + +TEST_F(HeartbeatDataTest, Encode) { + data_.clientId("CID_01"); + ProtobufWkt::Struct doc; + data_.encode(doc); + const auto& members = doc.fields(); + EXPECT_TRUE(members.contains("clientID")); + EXPECT_STREQ("CID_01", members.at("clientID").string_value().c_str()); +} + +class RemotingCommandTest : public testing::Test { +public: + RemotingCommand cmd_; +}; + +TEST_F(RemotingCommandTest, FlagResponse) { + cmd_.markAsResponse(); + EXPECT_EQ(1, cmd_.flag()); +} + +TEST_F(RemotingCommandTest, FlagOneway) { + cmd_.markAsOneway(); + EXPECT_EQ(2, cmd_.flag()); +} + +TEST_F(RemotingCommandTest, Remark) { + const char* remark = "OK"; + cmd_.remark(remark); + EXPECT_STREQ(remark, cmd_.remark().c_str()); +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/route_matcher_test.cc b/test/extensions/filters/network/rocketmq_proxy/route_matcher_test.cc new file mode 100644 index 000000000000..c908602fc25e --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/route_matcher_test.cc @@ -0,0 +1,74 @@ +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/rocketmq_proxy.pb.validate.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/route.pb.h" +#include "envoy/extensions/filters/network/rocketmq_proxy/v3/route.pb.validate.h" + +#include "extensions/filters/network/rocketmq_proxy/metadata.h" +#include "extensions/filters/network/rocketmq_proxy/router/route_matcher.h" + +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { +namespace Router { + +using RouteConfigurationProto = + envoy::extensions::filters::network::rocketmq_proxy::v3::RouteConfiguration; + +RouteConfigurationProto parseRouteConfigurationFromV2Yaml(const std::string& yaml) { + RouteConfigurationProto route_config; + TestUtility::loadFromYaml(yaml, route_config); + TestUtility::validate(route_config); + return route_config; +} + +TEST(RocketmqRouteMatcherTest, RouteWithHeaders) { + const std::string yaml = R"EOF( +name: default_route +routes: + - match: + topic: + exact: test_topic + headers: + - name: code + exact_match: '310' + route: + cluster: fake_cluster + metadata_match: + filter_metadata: + envoy.lb: + k1: v1 +)EOF"; + + RouteConfigurationProto config = parseRouteConfigurationFromV2Yaml(yaml); + + MessageMetadata metadata; + std::string topic_name = "test_topic"; + metadata.setTopicName(topic_name); + uint64_t code = 310; + metadata.headers().addCopy(Http::LowerCaseString("code"), code); + RouteMatcher matcher(config); + const Envoy::Router::MetadataMatchCriteria* criteria = + matcher.route(metadata)->routeEntry()->metadataMatchCriteria(); + const std::vector& mmc = + criteria->metadataMatchCriteria(); + + ProtobufWkt::Value v1; + v1.set_string_value("v1"); + HashedValue hv1(v1); + + EXPECT_EQ(1, mmc.size()); + EXPECT_EQ("k1", mmc[0]->name()); + EXPECT_EQ(hv1, mmc[0]->value()); +} + +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/router_test.cc b/test/extensions/filters/network/rocketmq_proxy/router_test.cc new file mode 100644 index 000000000000..a80d837d1b10 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/router_test.cc @@ -0,0 +1,470 @@ +#include "extensions/filters/network/rocketmq_proxy/config.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" +#include "extensions/filters/network/rocketmq_proxy/router/router.h" +#include "extensions/filters/network/rocketmq_proxy/well_known_names.h" + +#include "test/extensions/filters/network/rocketmq_proxy/mocks.h" +#include "test/extensions/filters/network/rocketmq_proxy/utility.h" +#include "test/mocks/server/mocks.h" + +#include "gtest/gtest.h" + +using testing::_; +using testing::ContainsRegex; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { +namespace Router { + +class RocketmqRouterTestBase { +public: + RocketmqRouterTestBase() + : config_(rocketmq_proxy_config_, context_), + cluster_info_(std::make_shared()) { + conn_manager_ = + std::make_unique(config_, context_.dispatcher().timeSource()); + conn_manager_->initializeReadFilterCallbacks(filter_callbacks_); + } + + ~RocketmqRouterTestBase() { filter_callbacks_.connection_.dispatcher_.clearDeferredDeleteList(); } + + void initializeRouter() { + router_ = std::make_unique(context_.clusterManager()); + EXPECT_EQ(nullptr, router_->downstreamConnection()); + } + + void initSendMessageRequest(std::string topic_name = "test_topic", bool is_oneway = false) { + RemotingCommandPtr request = std::make_unique(); + request->code(static_cast(RequestCode::SendMessageV2)); + if (is_oneway) { + request->flag(2); + } + SendMessageRequestHeader* header = new SendMessageRequestHeader(); + absl::string_view t = topic_name; + header->topic(t); + CommandCustomHeaderPtr custom_header(header); + request->customHeader(custom_header); + active_message_ = + std::make_unique>(*conn_manager_, std::move(request)); + + // Not yet implemented: + EXPECT_EQ(nullptr, router_->metadataMatchCriteria()); + } + + void initPopMessageRequest() { + Buffer::OwnedImpl buffer; + BufferUtility::fillRequestBuffer(buffer, RequestCode::PopMessage); + + bool underflow = false; + bool has_error = false; + + RemotingCommandPtr request = Decoder::decode(buffer, underflow, has_error); + + active_message_ = + std::make_unique>(*conn_manager_, std::move(request)); + } + + void initAckMessageRequest() { + Buffer::OwnedImpl buffer; + BufferUtility::fillRequestBuffer(buffer, RequestCode::AckMessage); + + bool underflow = false; + bool has_error = false; + + RemotingCommandPtr request = Decoder::decode(buffer, underflow, has_error); + + active_message_ = + std::make_unique>(*conn_manager_, std::move(request)); + } + + void initOneWayAckMessageRequest() { + RemotingCommandPtr request = std::make_unique(); + request->code(static_cast(RequestCode::AckMessage)); + request->flag(2); + std::unique_ptr header = std::make_unique(); + header->consumerGroup("test_cg"); + header->topic("test_topic"); + header->queueId(0); + header->extraInfo("test_extra"); + header->offset(1); + CommandCustomHeaderPtr ptr(header.release()); + request->customHeader(ptr); + active_message_ = + std::make_unique>(*conn_manager_, std::move(request)); + } + + void startRequest() { router_->sendRequestToUpstream(*active_message_); } + + void connectUpstream() { + context_.cluster_manager_.tcp_conn_pool_.poolReady(upstream_connection_); + } + + void startRequestWithExistingConnection() { + EXPECT_CALL(context_.cluster_manager_.tcp_conn_pool_, newConnection(_)) + .WillOnce( + Invoke([&](Tcp::ConnectionPool::Callbacks& cb) -> Tcp::ConnectionPool::Cancellable* { + context_.cluster_manager_.tcp_conn_pool_.newConnectionImpl(cb); + context_.cluster_manager_.tcp_conn_pool_.poolReady(upstream_connection_); + return nullptr; + })); + router_->sendRequestToUpstream(*active_message_); + } + + void receiveEmptyResponse() { + Buffer::OwnedImpl buffer; + router_->onAboveWriteBufferHighWatermark(); + router_->onBelowWriteBufferLowWatermark(); + router_->onUpstreamData(buffer, false); + } + + void receiveSendMessageResponse(bool end_stream) { + Buffer::OwnedImpl buffer; + BufferUtility::fillResponseBuffer(buffer, RequestCode::SendMessageV2, ResponseCode::Success); + router_->onUpstreamData(buffer, end_stream); + } + + void receivePopMessageResponse() { + Buffer::OwnedImpl buffer; + BufferUtility::fillResponseBuffer(buffer, RequestCode::PopMessage, ResponseCode::Success); + router_->onUpstreamData(buffer, false); + } + + void receiveAckMessageResponse() { + Buffer::OwnedImpl buffer; + BufferUtility::fillResponseBuffer(buffer, RequestCode::AckMessage, ResponseCode::Success); + router_->onUpstreamData(buffer, false); + } + + NiceMock filter_callbacks_; + NiceMock context_; + ConfigImpl::RocketmqProxyConfig rocketmq_proxy_config_; + ConfigImpl config_; + std::unique_ptr conn_manager_; + + std::unique_ptr router_; + + std::unique_ptr> active_message_; + NiceMock upstream_connection_; + + std::shared_ptr cluster_info_; + NiceMock thread_local_cluster_; +}; + +class RocketmqRouterTest : public RocketmqRouterTestBase, public testing::Test {}; + +TEST_F(RocketmqRouterTest, PoolRemoteConnectionFailure) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*remote connection failure*.")); + })); + + startRequest(); + context_.cluster_manager_.tcp_conn_pool_.poolFailure( + Tcp::ConnectionPool::PoolFailureReason::RemoteConnectionFailure); +} + +TEST_F(RocketmqRouterTest, PoolTimeout) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*timeout*.")); + })); + EXPECT_CALL(*active_message_, onReset()); + + startRequest(); + context_.cluster_manager_.tcp_conn_pool_.poolFailure( + Tcp::ConnectionPool::PoolFailureReason::Timeout); +} + +TEST_F(RocketmqRouterTest, PoolLocalConnectionFailure) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*local connection failure*.")); + })); + EXPECT_CALL(*active_message_, onReset()); + + startRequest(); + context_.cluster_manager_.tcp_conn_pool_.poolFailure( + Tcp::ConnectionPool::PoolFailureReason::LocalConnectionFailure); +} + +TEST_F(RocketmqRouterTest, PoolOverflowFailure) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*overflow*.")); + })); + EXPECT_CALL(*active_message_, onReset()); + + startRequest(); + context_.cluster_manager_.tcp_conn_pool_.poolFailure( + Tcp::ConnectionPool::PoolFailureReason::Overflow); +} + +TEST_F(RocketmqRouterTest, ClusterMaintenanceMode) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*Cluster under maintenance*.")); + })); + EXPECT_CALL(*context_.cluster_manager_.thread_local_cluster_.cluster_.info_, maintenanceMode()) + .WillOnce(Return(true)); + EXPECT_CALL(*active_message_, onReset()); + + startRequest(); +} + +TEST_F(RocketmqRouterTest, NoHealthyHosts) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*No host available*.")); + })); + EXPECT_CALL(context_.cluster_manager_, tcpConnPoolForCluster("fake_cluster", _, _)) + .WillOnce(Return(nullptr)); + EXPECT_CALL(*active_message_, onReset()); + + startRequest(); +} + +TEST_F(RocketmqRouterTest, NoRouteForRequest) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*No route for current request*.")); + })); + EXPECT_CALL(*active_message_, route()).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(*active_message_, onReset()); + + startRequest(); +} + +TEST_F(RocketmqRouterTest, NoCluster) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onReset()); + EXPECT_CALL(context_.cluster_manager_, get(_)).WillRepeatedly(Return(nullptr)); + + startRequest(); +} + +TEST_F(RocketmqRouterTest, CallWithEmptyResponse) { + initializeRouter(); + initSendMessageRequest(); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()).Times(0); + EXPECT_CALL(*active_message_, onReset()).Times(0); + + receiveEmptyResponse(); +} + +TEST_F(RocketmqRouterTest, OneWayRequest) { + initializeRouter(); + initSendMessageRequest("test_topic", true); + startRequest(); + + EXPECT_CALL(*active_message_, onReset()); + + connectUpstream(); + + EXPECT_TRUE(active_message_->metadata()->isOneWay()); +} + +TEST_F(RocketmqRouterTest, ReceiveSendMessageResponse) { + initializeRouter(); + initSendMessageRequest(); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()); + EXPECT_CALL(*active_message_, onReset()); + + receiveSendMessageResponse(false); +} + +TEST_F(RocketmqRouterTest, ReceivePopMessageResponse) { + initializeRouter(); + initPopMessageRequest(); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()); + EXPECT_CALL(*active_message_, onReset()); + + receivePopMessageResponse(); +} + +TEST_F(RocketmqRouterTest, ReceiveAckMessageResponse) { + initializeRouter(); + initAckMessageRequest(); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()); + EXPECT_CALL(*active_message_, onReset()); + + receiveAckMessageResponse(); +} + +TEST_F(RocketmqRouterTest, OneWayAckMessage) { + initializeRouter(); + initOneWayAckMessageRequest(); + + startRequest(); + + EXPECT_CALL(*active_message_, onReset()); + + connectUpstream(); +} + +TEST_F(RocketmqRouterTest, ReceivedSendMessageResponseWithDecodeError) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*Failed to decode response*.")); + })); + + EXPECT_CALL(upstream_connection_, close(Network::ConnectionCloseType::NoFlush)); + + startRequest(); + connectUpstream(); + std::string json = R"EOF( + { + "language": "JAVA", + "version": 2, + "opaque": 1, + "flag": 1, + "serializeTypeCurrentRPC": "JSON" + } + )EOF"; + Buffer::OwnedImpl buffer; + buffer.writeBEInt(4 + 4 + json.size()); + buffer.writeBEInt(json.size()); + buffer.add(json); + + EXPECT_CALL(*active_message_, onReset()).WillRepeatedly(Invoke([&]() -> void { + conn_manager_->deferredDelete(**conn_manager_->activeMessageList().begin()); + })); + EXPECT_CALL(*active_message_, onReset()); + + active_message_->moveIntoList(std::move(active_message_), conn_manager_->activeMessageList()); + router_->onUpstreamData(buffer, false); +} + +TEST_F(RocketmqRouterTest, ReceivedSendMessageResponseWithStreamEnd) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(upstream_connection_, close(Network::ConnectionCloseType::NoFlush)); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()); + EXPECT_CALL(*active_message_, onReset()); + + receiveSendMessageResponse(true); +} + +TEST_F(RocketmqRouterTest, UpstreamRemoteCloseMidResponse) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*Connection to upstream is closed*.")); + })); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()).Times(0); + EXPECT_CALL(*active_message_, onReset()); + + router_->onEvent(Network::ConnectionEvent::RemoteClose); +} + +TEST_F(RocketmqRouterTest, UpstreamLocalCloseMidResponse) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)) + .Times(1) + .WillOnce(Invoke([&](absl::string_view error_message) -> void { + EXPECT_THAT(error_message, ContainsRegex(".*Connection to upstream has been closed*.")); + })); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()).Times(0); + EXPECT_CALL(*active_message_, onReset()); + + router_->onEvent(Network::ConnectionEvent::LocalClose); +} + +TEST_F(RocketmqRouterTest, UpstreamConnected) { + initializeRouter(); + initSendMessageRequest(); + + startRequest(); + connectUpstream(); + + EXPECT_CALL(*active_message_, sendResponseToDownstream()).Times(0); + EXPECT_CALL(*active_message_, onReset()).Times(0); + + router_->onEvent(Network::ConnectionEvent::Connected); +} + +TEST_F(RocketmqRouterTest, StartRequestWithExistingConnection) { + initializeRouter(); + initSendMessageRequest(); + + EXPECT_CALL(*active_message_, onError(_)).Times(0); + EXPECT_CALL(*active_message_, onReset()).Times(0); + + startRequestWithExistingConnection(); +} + +} // namespace Router +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/topic_route_test.cc b/test/extensions/filters/network/rocketmq_proxy/topic_route_test.cc new file mode 100644 index 000000000000..a2392b0c0603 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/topic_route_test.cc @@ -0,0 +1,74 @@ +#include + +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/rocketmq_proxy/topic_route.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +TEST(TopicRouteTest, Serialization) { + QueueData queue_data("broker-a", 8, 8, 6); + ProtobufWkt::Struct doc; + queue_data.encode(doc); + + const auto& members = doc.fields(); + + ASSERT_STREQ("broker-a", members.at("brokerName").string_value().c_str()); + ASSERT_EQ(queue_data.brokerName(), members.at("brokerName").string_value()); + ASSERT_EQ(queue_data.readQueueNum(), members.at("readQueueNums").number_value()); + ASSERT_EQ(queue_data.writeQueueNum(), members.at("writeQueueNums").number_value()); + ASSERT_EQ(queue_data.perm(), members.at("perm").number_value()); +} + +TEST(BrokerDataTest, Serialization) { + std::unordered_map broker_addrs; + std::string dummy_address("127.0.0.1:10911"); + for (int64_t i = 0; i < 3; i++) { + broker_addrs[i] = dummy_address; + } + std::string cluster("DefaultCluster"); + std::string broker_name("broker-a"); + BrokerData broker_data(cluster, broker_name, std::move(broker_addrs)); + + ProtobufWkt::Struct doc; + broker_data.encode(doc); + + const auto& members = doc.fields(); + + ASSERT_STREQ(cluster.c_str(), members.at("cluster").string_value().c_str()); + ASSERT_STREQ(broker_name.c_str(), members.at("brokerName").string_value().c_str()); +} + +TEST(TopicRouteDataTest, Serialization) { + TopicRouteData topic_route_data; + + for (int i = 0; i < 16; i++) { + topic_route_data.queueData().push_back(QueueData("broker-a", 8, 8, 6)); + } + + std::string cluster("DefaultCluster"); + std::string broker_name("broker-a"); + std::string dummy_address("127.0.0.1:10911"); + + for (int i = 0; i < 16; i++) { + std::unordered_map broker_addrs; + for (int64_t i = 0; i < 3; i++) { + broker_addrs[i] = dummy_address; + } + topic_route_data.brokerData().emplace_back( + BrokerData(cluster, broker_name, std::move(broker_addrs))); + } + ProtobufWkt::Struct doc; + EXPECT_NO_THROW(topic_route_data.encode(doc)); + MessageUtil::getJsonStringFromMessage(doc); +} + +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/utility.cc b/test/extensions/filters/network/rocketmq_proxy/utility.cc new file mode 100644 index 000000000000..a44f0cd0acb3 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/utility.cc @@ -0,0 +1,240 @@ +#include "test/extensions/filters/network/rocketmq_proxy/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +const std::string BufferUtility::topic_name_ = "test_topic"; +const std::string BufferUtility::client_id_ = "test_client_id"; +const std::string BufferUtility::producer_group_ = "test_pg"; +const std::string BufferUtility::consumer_group_ = "test_cg"; +const std::string BufferUtility::extra_info_ = "test_extra"; +const std::string BufferUtility::msg_body_ = "_Apache_RocketMQ_"; +const int BufferUtility::queue_id_ = 1; +int BufferUtility::opaque_ = 0; + +void BufferUtility::fillRequestBuffer(Buffer::OwnedImpl& buffer, RequestCode code) { + + RemotingCommandPtr cmd = std::make_unique(); + cmd->code(static_cast(code)); + cmd->opaque(++opaque_); + + switch (code) { + case RequestCode::SendMessage: { + std::unique_ptr header = std::make_unique(); + header->topic(topic_name_); + header->version(SendMessageRequestVersion::V1); + std::string msg_body = msg_body_; + cmd->body().add(msg_body); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + } break; + + case RequestCode::HeartBeat: { + std::string heartbeat_data = R"EOF( + { + "clientID": "127.0.0.1@90330", + "consumerDataSet": [ + { + "consumeFromWhere": "CONSUME_FROM_FIRST_OFFSET", + "consumeType": "CONSUME_PASSIVELY", + "groupName": "test_cg", + "messageModel": "CLUSTERING", + "subscriptionDataSet": [ + { + "classFilterMode": false, + "codeSet": [], + "expressionType": "TAG", + "subString": "*", + "subVersion": 1575630587925, + "tagsSet": [], + "topic": "test_topic" + }, + { + "classFilterMode": false, + "codeSet": [], + "expressionType": "TAG", + "subString": "*", + "subVersion": 1575630587945, + "tagsSet": [], + "topic": "%RETRY%please_rename_unique_group_name_4" + } + ], + "unitMode": false + } + ], + "producerDataSet": [ + { + "groupName": "CLIENT_INNER_PRODUCER" + } + ] + } + )EOF"; + cmd->body().add(heartbeat_data); + } break; + + case RequestCode::UnregisterClient: { + std::unique_ptr header = + std::make_unique(); + header->clientId(client_id_); + header->consumerGroup(consumer_group_); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + break; + } + + case RequestCode::GetRouteInfoByTopic: { + std::unique_ptr header = + std::make_unique(); + header->topic(topic_name_); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + break; + } + + case RequestCode::GetConsumerListByGroup: { + std::unique_ptr header = + std::make_unique(); + header->consumerGroup(consumer_group_); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + break; + } + + case RequestCode::SendMessageV2: { + std::unique_ptr header = std::make_unique(); + header->topic(topic_name_); + header->version(SendMessageRequestVersion::V2); + header->producerGroup(producer_group_); + std::string msg_body = msg_body_; + cmd->body().add(msg_body); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + break; + } + + case RequestCode::PopMessage: { + std::unique_ptr header = std::make_unique(); + header->consumerGroup(consumer_group_); + header->topic(topic_name_); + header->queueId(queue_id_); + header->maxMsgNum(32); + header->invisibleTime(6000); + header->pollTime(3000); + header->bornTime(1000); + header->initMode(4); + + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + break; + } + + case RequestCode::AckMessage: { + std::unique_ptr header = std::make_unique(); + header->consumerGroup(consumer_group_); + header->topic(topic_name_); + header->queueId(queue_id_); + header->extraInfo(extra_info_); + header->offset(1); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + break; + } + + default: + break; + } + Encoder encoder_; + buffer.drain(buffer.length()); + encoder_.encode(cmd, buffer); +} + +void BufferUtility::fillResponseBuffer(Buffer::OwnedImpl& buffer, RequestCode req_code, + ResponseCode resp_code) { + RemotingCommandPtr cmd = std::make_unique(); + cmd->code(static_cast(resp_code)); + cmd->opaque(opaque_); + + switch (req_code) { + case RequestCode::SendMessageV2: { + std::unique_ptr header = + std::make_unique(); + header->msgIdForTest("MSG_ID_01"); + header->queueId(1); + header->queueOffset(100); + header->transactionId("TX_01"); + break; + } + case RequestCode::PopMessage: { + std::unique_ptr header = std::make_unique(); + header->popTime(1587386521445); + header->invisibleTime(50000); + header->reviveQid(5); + std::string msg_offset_info = "0 6 147"; + header->msgOffsetInfo(msg_offset_info); + std::string start_offset_info = "0 6 147"; + header->startOffsetInfo(start_offset_info); + CommandCustomHeaderPtr ptr(header.release()); + cmd->customHeader(ptr); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\xD5'})); + cmd->body().add(std::string({'\xDA', '\xA3', '\x20', '\xA7'})); + cmd->body().add(std::string({'\x01', '\xE5', '\x9A', '\x3E'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x06'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x93'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x4A', '\xE0', '\x46'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x00', '\x01', '\x71'})); + cmd->body().add(std::string({'\x97', '\x98', '\x71', '\xB6'})); + cmd->body().add(std::string({'\x0A', '\x65', '\xC4', '\x91'})); + cmd->body().add(std::string({'\x00', '\x00', '\x1A', '\xF4'})); + cmd->body().add(std::string({'\x00', '\x00', '\x01', '\x71'})); + cmd->body().add(std::string({'\x97', '\x98', '\x71', '\xAF'})); + cmd->body().add(std::string({'\x0A', '\x65', '\xC1', '\x2D'})); + cmd->body().add(std::string({'\x00', '\x00', '\x1F', '\x53'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x00'})); + cmd->body().add(std::string({'\x00', '\x00', '\x00', '\x11'})); + cmd->body().add(std::string("Hello RocketMQ 52")); + cmd->body().add(std::string({'\x04'})); + cmd->body().add(std::string("mesh")); + cmd->body().add(std::string({'\x00', '\x65'})); + cmd->body().add(std::string("TRACE_ON")); + cmd->body().add(std::string({'\x01'})); + cmd->body().add(std::string("true")); + cmd->body().add(std::string({'\x02'})); + cmd->body().add(std::string("MSG_REGION")); + cmd->body().add(std::string({'\x01'})); + cmd->body().add(std::string("DefaultRegion")); + cmd->body().add(std::string({'\x02'})); + cmd->body().add(std::string("UNIQ_KEY")); + cmd->body().add(std::string({'\x01'})); + cmd->body().add(std::string("1EE10882893E18B4AAC2664649B60034")); + cmd->body().add(std::string({'\x02'})); + cmd->body().add(std::string("WAIT")); + cmd->body().add(std::string({'\x01'})); + cmd->body().add(std::string("true")); + cmd->body().add(std::string({'\x02'})); + cmd->body().add(std::string("TAGS")); + cmd->body().add(std::string({'\x01'})); + cmd->body().add(std::string("TagA")); + cmd->body().add(std::string({'\x02'})); + break; + } + default: + break; + } + Encoder encoder_; + buffer.drain(buffer.length()); + encoder_.encode(cmd, buffer); +} +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/rocketmq_proxy/utility.h b/test/extensions/filters/network/rocketmq_proxy/utility.h new file mode 100644 index 000000000000..1dc57d5f2a76 --- /dev/null +++ b/test/extensions/filters/network/rocketmq_proxy/utility.h @@ -0,0 +1,33 @@ +#pragma once + +#include "extensions/filters/network/rocketmq_proxy/config.h" +#include "extensions/filters/network/rocketmq_proxy/conn_manager.h" + +#include "test/mocks/server/mocks.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RocketmqProxy { + +class BufferUtility { +public: + static void fillRequestBuffer(Buffer::OwnedImpl& buffer, RequestCode code); + static void fillResponseBuffer(Buffer::OwnedImpl& buffer, RequestCode req_code, + ResponseCode resp_code); + + const static std::string topic_name_; + const static std::string client_id_; + const static std::string producer_group_; + const static std::string consumer_group_; + const static std::string msg_body_; + const static std::string extra_info_; + const static int queue_id_; + static int opaque_; +}; +} // namespace RocketmqProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 2bc47abeb63f..fdeccc570b1b 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -174,6 +174,7 @@ MB MD MERCHANTABILITY MGET +MQ MSET MSVC MTLS @@ -757,6 +758,7 @@ mutexes mux muxed mysql +nameserver namespace namespaces namespacing @@ -913,6 +915,7 @@ reimplements rele releasor reloadable +remoting reparse repeatability reperform @@ -934,6 +937,7 @@ resync retriable retriggers rmdir +rocketmq rollout roundtrip rpcs