├── .gitignore ├── payload.proto ├── test ├── mocks │ ├── nats │ │ ├── mocks.cc │ │ ├── streaming │ │ │ ├── BUILD │ │ │ ├── mocks.h │ │ │ └── mocks.cc │ │ ├── BUILD │ │ └── mocks.h │ └── tcp │ │ ├── BUILD │ │ ├── mocks.cc │ │ └── mocks_nats.h ├── common │ ├── nats │ │ ├── nuid │ │ │ ├── BUILD │ │ │ └── nuid_test.cc │ │ ├── token_generator_impl_test.cc │ │ ├── streaming │ │ │ ├── client_impl_test.cc │ │ │ ├── heartbeat_handler_test.cc │ │ │ ├── connect_response_handler_test.cc │ │ │ ├── BUILD │ │ │ ├── message_utility_test.cc │ │ │ └── pub_request_handler_test.cc │ │ ├── message_builder_test.cc │ │ ├── BUILD │ │ ├── subject_utility_test.cc │ │ └── codec_impl_test.cc │ └── tcp │ │ ├── BUILD │ │ └── conn_pool_impl_test.cc └── extensions │ └── filters │ └── http │ └── nats │ └── streaming │ ├── mocks.cc │ ├── mocks.h │ ├── BUILD │ ├── metadata_subject_retriever_test.cc │ └── nats_streaming_filter_test.cc ├── include └── envoy │ ├── nats │ ├── streaming │ │ ├── inbox_handler.h │ │ ├── BUILD │ │ └── client.h │ ├── token_generator.h │ ├── BUILD │ └── codec.h │ └── tcp │ ├── BUILD │ ├── codec.h │ └── conn_pool_nats.h ├── source ├── common │ ├── nats │ │ ├── token_generator_impl.cc │ │ ├── nuid │ │ │ ├── BUILD │ │ │ ├── nuid.cc │ │ │ └── nuid.h │ │ ├── token_generator_impl.h │ │ ├── message_builder.h │ │ ├── streaming │ │ │ ├── connect_response_handler.h │ │ │ ├── heartbeat_handler.cc │ │ │ ├── heartbeat_handler.h │ │ │ ├── connect_response_handler.cc │ │ │ ├── message_utility.h │ │ │ ├── client_pool.h │ │ │ ├── pub_request_handler.h │ │ │ ├── client_pool.cc │ │ │ ├── message_utility.cc │ │ │ ├── BUILD │ │ │ ├── pub_request_handler.cc │ │ │ ├── client_impl.h │ │ │ └── client_impl.cc │ │ ├── subject_utility.h │ │ ├── codec_impl.h │ │ ├── BUILD │ │ ├── message_builder.cc │ │ └── codec_impl.cc │ ├── config │ │ ├── BUILD │ │ └── nats_streaming_well_known_names.h │ └── tcp │ │ ├── codec_impl.h │ │ ├── BUILD │ │ └── conn_pool_impl.h └── extensions │ └── filters │ └── http │ └── nats │ └── streaming │ ├── subject_retriever.h │ ├── metadata_subject_retriever.h │ ├── nats_streaming_filter_config_factory.h │ ├── nats_streaming_filter_config.h │ ├── metadata_subject_retriever.cc │ ├── BUILD │ ├── nats_streaming_filter_config_factory.cc │ ├── nats_streaming_filter.h │ └── nats_streaming_filter.cc ├── e2e ├── BUILD ├── create_config.sh └── e2e_test.py ├── nats_streaming_filter.proto ├── BUILD ├── WORKSPACE ├── README.md ├── protocol.proto └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | -------------------------------------------------------------------------------- /payload.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pb; 3 | 4 | message Payload { 5 | map headers = 1; 6 | bytes body = 2; 7 | } 8 | -------------------------------------------------------------------------------- /test/mocks/nats/mocks.cc: -------------------------------------------------------------------------------- 1 | #include "mocks.h" 2 | 3 | namespace Envoy { 4 | namespace Nats { 5 | 6 | bool operator==(const Message &lhs, const Message &rhs) { 7 | return lhs.asString() == rhs.asString(); 8 | } 9 | 10 | namespace ConnPoolNats { 11 | 12 | MockInstance::MockInstance() {} 13 | MockInstance::~MockInstance() {} 14 | 15 | } // namespace ConnPoolNats 16 | 17 | } // namespace Nats 18 | } // namespace Envoy 19 | -------------------------------------------------------------------------------- /include/envoy/nats/streaming/inbox_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | namespace Streaming { 10 | 11 | class InboxCallbacks { 12 | public: 13 | virtual ~InboxCallbacks() {} 14 | virtual void onFailure(const std::string &error) PURE; 15 | }; 16 | 17 | } // namespace Streaming 18 | } // namespace Nats 19 | } // namespace Envoy 20 | -------------------------------------------------------------------------------- /source/common/nats/token_generator_impl.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/token_generator_impl.h" 2 | 3 | #include 4 | 5 | namespace Envoy { 6 | namespace Nats { 7 | 8 | TokenGeneratorImpl::TokenGeneratorImpl( 9 | Runtime::RandomGenerator &random_generator) 10 | : nuid_(random_generator) {} 11 | 12 | std::string TokenGeneratorImpl::random() { return nuid_.next(); } 13 | 14 | } // namespace Nats 15 | } // namespace Envoy 16 | -------------------------------------------------------------------------------- /test/mocks/nats/streaming/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_mock", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_mock( 12 | name = "nats_streaming_mocks", 13 | srcs = ["mocks.cc"], 14 | hdrs = ["mocks.h"], 15 | repository = "@envoy", 16 | deps = [ 17 | "//source/common/nats/streaming:client_lib", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /include/envoy/nats/token_generator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | 10 | /** 11 | * Random token generator. 12 | */ 13 | class TokenGenerator { 14 | public: 15 | virtual ~TokenGenerator() {} 16 | 17 | /** 18 | * @return std::string a new random token. 19 | */ 20 | virtual std::string random() PURE; 21 | }; 22 | 23 | } // namespace Nats 24 | } // namespace Envoy 25 | -------------------------------------------------------------------------------- /test/mocks/nats/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_mock", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_mock( 12 | name = "nats_mocks", 13 | srcs = ["mocks.cc"], 14 | hdrs = ["mocks.h"], 15 | repository = "@envoy", 16 | deps = [ 17 | "//include/envoy/tcp:conn_pool_interface", 18 | "//source/common/nats:codec_lib", 19 | ], 20 | ) 21 | -------------------------------------------------------------------------------- /include/envoy/nats/streaming/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "client_interface", 13 | hdrs = ["client.h"], 14 | repository = "@envoy", 15 | ) 16 | 17 | envoy_cc_library( 18 | name = "inbox_handler_interface", 19 | hdrs = ["inbox_handler.h"], 20 | repository = "@envoy", 21 | ) 22 | -------------------------------------------------------------------------------- /e2e/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_binary", 6 | "envoy_cc_library", 7 | "envoy_cc_test", 8 | "envoy_package", 9 | ) 10 | 11 | envoy_package() 12 | 13 | load("@envoy_api//bazel:api_build_system.bzl", "api_proto_library") 14 | 15 | py_test( 16 | name = "e2e_test", 17 | srcs = ["e2e_test.py"], 18 | data = [ 19 | "//:envoy", 20 | "//e2e:create_config.sh", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /source/common/config/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_binary", 6 | "envoy_cc_library", 7 | "envoy_cc_test", 8 | "envoy_package", 9 | ) 10 | 11 | envoy_package() 12 | 13 | load("@envoy_api//bazel:api_build_system.bzl", "api_proto_library") 14 | 15 | envoy_cc_library( 16 | name = "nats_streaming_well_known_names_lib", 17 | hdrs = ["nats_streaming_well_known_names.h"], 18 | repository = "@envoy", 19 | ) 20 | -------------------------------------------------------------------------------- /source/common/tcp/codec_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "envoy/tcp/codec.h" 4 | 5 | namespace Envoy { 6 | namespace Tcp { 7 | 8 | /** 9 | * A factory implementation that returns a real decoder. 10 | */ 11 | template 12 | class DecoderFactoryImpl : public DecoderFactory { 13 | public: 14 | // Tcp::DecoderFactory 15 | DecoderPtr create(DecoderCallbacks &callbacks) override { 16 | return DecoderPtr{new D(callbacks)}; 17 | } 18 | }; 19 | 20 | } // namespace Tcp 21 | } // namespace Envoy 22 | -------------------------------------------------------------------------------- /nats_streaming_filter.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package envoy.api.v2.filter.http; 4 | 5 | import "google/protobuf/duration.proto"; 6 | import "validate/validate.proto"; 7 | 8 | // [#protodoc-title: NatsStreaming] 9 | // NATS Streaming :ref:`configuration overview 10 | // `. 11 | 12 | // [#proto-status: experimental] 13 | message NatsStreaming { 14 | string cluster = 1 [ (validate.rules).string.min_bytes = 1 ]; 15 | uint32 max_connections = 2; 16 | google.protobuf.Duration op_timeout = 3; 17 | } 18 | -------------------------------------------------------------------------------- /include/envoy/nats/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "codec_interface", 13 | hdrs = ["codec.h"], 14 | repository = "@envoy", 15 | deps = ["@envoy//include/envoy/buffer:buffer_interface"], 16 | ) 17 | 18 | envoy_cc_library( 19 | name = "token_generator_interface", 20 | hdrs = ["token_generator.h"], 21 | repository = "@envoy", 22 | ) 23 | -------------------------------------------------------------------------------- /source/common/nats/nuid/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "nuid_lib", 13 | srcs = ["nuid.cc"], 14 | hdrs = ["nuid.h"], 15 | external_deps = ["abseil_optional"], 16 | repository = "@envoy", 17 | deps = [ 18 | "@envoy//include/envoy/runtime:runtime_interface", 19 | "@envoy//source/common/common:assert_lib", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /test/common/nats/nuid/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_test", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_cc_test( 10 | name = "nuid_test", 11 | srcs = ["nuid_test.cc"], 12 | repository = "@envoy", 13 | deps = [ 14 | "//source/common/nats/nuid:nuid_lib", 15 | "@envoy//source/common/common:assert_lib", 16 | "@envoy//source/common/runtime:runtime_lib", 17 | "@envoy//test/mocks/runtime:runtime_mocks", 18 | ], 19 | ) 20 | -------------------------------------------------------------------------------- /source/common/nats/token_generator_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/nats/token_generator.h" 6 | #include "envoy/runtime/runtime.h" 7 | 8 | #include "common/nats/nuid/nuid.h" 9 | 10 | namespace Envoy { 11 | namespace Nats { 12 | 13 | class TokenGeneratorImpl : public TokenGenerator { 14 | public: 15 | explicit TokenGeneratorImpl(Runtime::RandomGenerator &random_generator); 16 | 17 | // Nats::TokenGenerator 18 | std::string random() override; 19 | 20 | private: 21 | Nuid::Nuid nuid_; 22 | }; 23 | 24 | } // namespace Nats 25 | } // namespace Envoy 26 | -------------------------------------------------------------------------------- /source/common/nats/message_builder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/nats/codec.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | 10 | class MessageBuilder { 11 | public: 12 | static Message createConnectMessage(); 13 | static Message createPubMessage(const std::string &subject); 14 | static Message createPubMessage(const std::string &subject, 15 | const std::string &reply_to, 16 | const std::string &payload); 17 | static Message createSubMessage(const std::string &subject, uint64_t sid); 18 | static Message createPongMessage(); 19 | }; 20 | 21 | } // namespace Nats 22 | } // namespace Envoy 23 | -------------------------------------------------------------------------------- /test/mocks/tcp/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_mock", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_mock( 12 | name = "tcp_mocks", 13 | srcs = ["mocks.cc"], 14 | 15 | # TODO(yuval-k): this has the nats suffix as it clashes with envoy's conn_pool. In the future 16 | # we want to remove this implementation as use the envoy's 17 | hdrs = ["mocks_nats.h"], 18 | repository = "@envoy", 19 | deps = [ 20 | "//include/envoy/tcp:conn_pool_interface", 21 | "//source/common/tcp:codec_lib", 22 | "@envoy//source/common/common:assert_lib", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /test/extensions/filters/http/nats/streaming/mocks.cc: -------------------------------------------------------------------------------- 1 | #include "mocks.h" 2 | 3 | using testing::_; 4 | using testing::Invoke; 5 | 6 | namespace Envoy { 7 | namespace Extensions { 8 | namespace HttpFilters { 9 | namespace Nats { 10 | namespace Streaming { 11 | 12 | MockSubjectRetriever::MockSubjectRetriever() { 13 | ON_CALL(*this, getSubject(_)) 14 | .WillByDefault(Invoke( 15 | [this](const Http::MetadataAccessor &) -> absl::optional { 16 | return subject_; 17 | })); 18 | } 19 | 20 | MockSubjectRetriever::~MockSubjectRetriever() {} 21 | 22 | } // namespace Streaming 23 | } // namespace Nats 24 | } // namespace HttpFilters 25 | } // namespace Extensions 26 | } // namespace Envoy 27 | -------------------------------------------------------------------------------- /source/common/nats/streaming/connect_response_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | #include "envoy/nats/streaming/inbox_handler.h" 7 | 8 | #include "absl/types/optional.h" 9 | 10 | namespace Envoy { 11 | namespace Nats { 12 | namespace Streaming { 13 | 14 | class ConnectResponseHandler { 15 | public: 16 | class Callbacks : public virtual InboxCallbacks { 17 | public: 18 | virtual ~Callbacks() {} 19 | virtual void onConnected(const std::string &pub_prefix) PURE; 20 | }; 21 | 22 | static void onMessage(absl::optional &reply_to, 23 | const std::string &payload, Callbacks &callbacks); 24 | }; 25 | 26 | } // namespace Streaming 27 | } // namespace Nats 28 | } // namespace Envoy 29 | -------------------------------------------------------------------------------- /test/mocks/nats/mocks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "envoy/tcp/conn_pool_nats.h" 4 | 5 | #include "common/nats/codec_impl.h" 6 | 7 | #include "gmock/gmock.h" 8 | 9 | namespace Envoy { 10 | namespace Nats { 11 | 12 | bool operator==(const Message &lhs, const Message &rhs); 13 | 14 | namespace ConnPoolNats { 15 | 16 | class MockInstance : public Tcp::ConnPoolNats::Instance { 17 | public: 18 | MockInstance(); 19 | ~MockInstance(); 20 | 21 | MOCK_METHOD1(setPoolCallbacks, 22 | void(Tcp::ConnPoolNats::PoolCallbacks &callbacks)); 23 | 24 | MOCK_METHOD2(makeRequest, 25 | void(const std::string &hash_key, const Message &request)); 26 | }; 27 | 28 | } // namespace ConnPoolNats 29 | 30 | } // namespace Nats 31 | } // namespace Envoy 32 | -------------------------------------------------------------------------------- /test/extensions/filters/http/nats/streaming/mocks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "extensions/filters/http/nats/streaming/subject_retriever.h" 6 | 7 | #include "gmock/gmock.h" 8 | 9 | namespace Envoy { 10 | namespace Extensions { 11 | namespace HttpFilters { 12 | namespace Nats { 13 | namespace Streaming { 14 | 15 | class MockSubjectRetriever : public SubjectRetriever { 16 | public: 17 | MockSubjectRetriever(); 18 | ~MockSubjectRetriever(); 19 | 20 | MOCK_METHOD1(getSubject, absl::optional( 21 | const Http::MetadataAccessor &metadataccessor)); 22 | 23 | absl::optional subject_; 24 | }; 25 | 26 | } // namespace Streaming 27 | } // namespace Nats 28 | } // namespace HttpFilters 29 | } // namespace Extensions 30 | } // namespace Envoy 31 | -------------------------------------------------------------------------------- /include/envoy/tcp/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "codec_interface", 13 | hdrs = ["codec.h"], 14 | repository = "@envoy", 15 | deps = ["@envoy//include/envoy/buffer:buffer_interface"], 16 | ) 17 | 18 | envoy_cc_library( 19 | name = "conn_pool_interface", 20 | # TODO(yuval-k): this has the nats suffix as it clashes with envoy's conn_pool. In the future 21 | # we want to remove this implementation as use the envoy's 22 | hdrs = ["conn_pool_nats.h"], 23 | repository = "@envoy", 24 | deps = [ 25 | ":codec_interface", 26 | "@envoy//include/envoy/upstream:cluster_manager_interface", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /test/common/tcp/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_test", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_test( 12 | name = "conn_pool_impl_test", 13 | srcs = ["conn_pool_impl_test.cc"], 14 | repository = "@envoy", 15 | deps = [ 16 | "//source/common/tcp:conn_pool_lib", 17 | "//test/mocks/tcp:tcp_mocks", 18 | "@envoy//source/common/event:dispatcher_lib", 19 | "@envoy//source/common/network:utility_lib", 20 | "@envoy//source/common/upstream:upstream_includes", 21 | "@envoy//source/common/upstream:upstream_lib", 22 | "@envoy//test/mocks/network:network_mocks", 23 | "@envoy//test/mocks/thread_local:thread_local_mocks", 24 | "@envoy//test/mocks/upstream:upstream_mocks", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /source/common/nats/streaming/heartbeat_handler.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/heartbeat_handler.h" 2 | 3 | #include 4 | 5 | #include "common/nats/message_builder.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | namespace Streaming { 10 | 11 | void HeartbeatHandler::onMessage(absl::optional &reply_to, 12 | const std::string &payload, 13 | Callbacks &callbacks) { 14 | if (!reply_to.has_value()) { 15 | callbacks.onFailure("incoming heartbeat without reply subject"); 16 | return; 17 | } 18 | 19 | if (!payload.empty()) { 20 | callbacks.onFailure("incoming heartbeat with non-empty payload"); 21 | return; 22 | } 23 | 24 | callbacks.send(MessageBuilder::createPubMessage(reply_to.value())); 25 | } 26 | 27 | } // namespace Streaming 28 | } // namespace Nats 29 | } // namespace Envoy 30 | -------------------------------------------------------------------------------- /source/common/nats/streaming/heartbeat_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | #include "envoy/nats/codec.h" 7 | #include "envoy/nats/streaming/inbox_handler.h" 8 | 9 | #include "absl/types/optional.h" 10 | 11 | namespace Envoy { 12 | namespace Nats { 13 | namespace Streaming { 14 | 15 | class HeartbeatHandler { 16 | public: 17 | class Callbacks : public virtual InboxCallbacks { 18 | public: 19 | virtual ~Callbacks() {} 20 | virtual void send(const Message &message) PURE; 21 | }; 22 | 23 | // TODO(talnordan): For this handler, the payload is always empty. In the 24 | // genral case, use a NATS streaming message type instead of a raw payload. 25 | static void onMessage(absl::optional &reply_to, 26 | const std::string &payload, Callbacks &callbacks); 27 | }; 28 | 29 | } // namespace Streaming 30 | } // namespace Nats 31 | } // namespace Envoy 32 | -------------------------------------------------------------------------------- /source/common/nats/streaming/connect_response_handler.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/connect_response_handler.h" 2 | 3 | #include 4 | 5 | #include "common/nats/streaming/message_utility.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | namespace Streaming { 10 | 11 | void ConnectResponseHandler::onMessage(absl::optional &reply_to, 12 | const std::string &payload, 13 | Callbacks &callbacks) { 14 | if (reply_to.has_value()) { 15 | callbacks.onFailure( 16 | "incoming ConnectResponse with non-empty reply subject"); 17 | return; 18 | } 19 | 20 | if (payload.empty()) { 21 | callbacks.onFailure("incoming ConnectResponse without payload"); 22 | return; 23 | } 24 | 25 | callbacks.onConnected(MessageUtility::getPubPrefix(payload)); 26 | } 27 | 28 | } // namespace Streaming 29 | } // namespace Nats 30 | } // namespace Envoy 31 | -------------------------------------------------------------------------------- /source/common/nats/subject_utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/nats/token_generator.h" 6 | 7 | #include "fmt/format.h" 8 | 9 | namespace Envoy { 10 | namespace Nats { 11 | 12 | class SubjectUtility { 13 | public: 14 | static inline std::string join(const std::string &prefix, 15 | const std::string &subject) { 16 | // Using `operator+=()` seems to be the most performant approach. 17 | std::string result{prefix}; 18 | result += '.'; 19 | result += subject; 20 | return result; 21 | } 22 | 23 | static inline std::string randomChild(const std::string &parent, 24 | TokenGenerator &token_generator) { 25 | return join(parent, token_generator.random()); 26 | } 27 | 28 | static inline std::string childWildcard(const std::string &parent) { 29 | return join(parent, "*"); 30 | } 31 | }; 32 | 33 | } // namespace Nats 34 | } // namespace Envoy 35 | -------------------------------------------------------------------------------- /include/envoy/nats/codec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/buffer/buffer.h" 6 | #include "envoy/common/exception.h" 7 | 8 | namespace Envoy { 9 | namespace Nats { 10 | 11 | class Message { 12 | public: 13 | Message() {} 14 | 15 | explicit Message(const std::string &string) : string_(string) {} 16 | 17 | ~Message() {} 18 | 19 | /** 20 | * Convert a message to a string for debugging purposes. 21 | */ 22 | std::string toString() const; 23 | 24 | /** 25 | * The following are a getter and a setter for the internal value. 26 | */ 27 | std::string &asString(); 28 | const std::string &asString() const; 29 | 30 | private: 31 | std::string string_; 32 | }; 33 | 34 | typedef std::unique_ptr MessagePtr; 35 | 36 | /** 37 | * A NATS protocol error. 38 | */ 39 | class ProtocolError : public EnvoyException { 40 | public: 41 | ProtocolError(const std::string &error) : EnvoyException(error) {} 42 | }; 43 | 44 | } // namespace Nats 45 | } // namespace Envoy 46 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/subject_retriever.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | #include "envoy/http/metadata_accessor.h" 7 | 8 | #include "absl/types/optional.h" 9 | 10 | namespace Envoy { 11 | namespace Extensions { 12 | namespace HttpFilters { 13 | namespace Nats { 14 | namespace Streaming { 15 | 16 | // TODO(yuval-k): rename this to something more descriptive? 17 | struct Subject { 18 | const std::string *subject; 19 | const std::string *cluster_id; 20 | const std::string *discover_prefix; 21 | }; 22 | 23 | // TODO(talnordan): Make generic and move to `envoy-common`. 24 | class SubjectRetriever { 25 | public: 26 | virtual ~SubjectRetriever() {} 27 | virtual absl::optional 28 | getSubject(const Http::MetadataAccessor &metadataccessor) PURE; 29 | }; 30 | 31 | typedef std::shared_ptr SubjectRetrieverSharedPtr; 32 | 33 | } // namespace Streaming 34 | } // namespace Nats 35 | } // namespace HttpFilters 36 | } // namespace Extensions 37 | } // namespace Envoy 38 | -------------------------------------------------------------------------------- /test/common/nats/token_generator_impl_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "common/common/assert.h" 5 | #include "common/nats/token_generator_impl.h" 6 | 7 | #include "test/mocks/runtime/mocks.h" 8 | 9 | #include "gmock/gmock.h" 10 | #include "gtest/gtest.h" 11 | 12 | using testing::NiceMock; 13 | using testing::Return; 14 | 15 | namespace Envoy { 16 | namespace Nats { 17 | 18 | class NatsTokenGeneratorImplTest : public testing::Test { 19 | public: 20 | NatsTokenGeneratorImplTest() {} 21 | }; 22 | 23 | TEST_F(NatsTokenGeneratorImplTest, Random) { 24 | NiceMock random_generator; 25 | 26 | for (auto i = 0; i < 1000; ++i) { 27 | TokenGeneratorImpl token_generator{random_generator}; 28 | for (auto j = 0; j < 10; ++j) { 29 | auto token = token_generator.random(); 30 | EXPECT_FALSE(token.empty()); 31 | const auto isalnum = [](const char c) { return std::isalnum(c); }; 32 | EXPECT_TRUE(std::all_of(token.begin(), token.end(), isalnum)); 33 | } 34 | } 35 | } 36 | 37 | } // namespace Nats 38 | } // namespace Envoy 39 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_binary", 6 | "envoy_cc_library", 7 | "envoy_cc_test", 8 | "envoy_package", 9 | ) 10 | 11 | envoy_package() 12 | 13 | load("@envoy_api//bazel:api_build_system.bzl", "api_proto_library") 14 | 15 | api_proto_library( 16 | name = "nats_streaming_filter_proto", 17 | srcs = ["nats_streaming_filter.proto"], 18 | ) 19 | 20 | api_proto_library( 21 | name = "payload_proto", 22 | srcs = ["payload.proto"], 23 | ) 24 | 25 | api_proto_library( 26 | name = "protocol_proto", 27 | srcs = ["protocol.proto"], 28 | ) 29 | 30 | envoy_cc_library( 31 | name = "filter_lib", 32 | repository = "@envoy", 33 | visibility = ["//visibility:public"], 34 | deps = [ 35 | "//source/extensions/filters/http/nats/streaming:nats_streaming_filter_config_lib", 36 | ], 37 | ) 38 | 39 | envoy_cc_binary( 40 | name = "envoy", 41 | repository = "@envoy", 42 | deps = [ 43 | ":filter_lib", 44 | "@envoy//source/exe:envoy_main_entry_lib", 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /test/common/nats/streaming/client_impl_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/client_impl.h" 2 | 3 | #include "test/mocks/event/mocks.h" 4 | #include "test/mocks/nats/mocks.h" 5 | #include "test/mocks/nats/streaming/mocks.h" 6 | #include "test/mocks/runtime/mocks.h" 7 | 8 | #include "gmock/gmock.h" 9 | #include "gtest/gtest.h" 10 | 11 | using testing::NiceMock; 12 | using testing::Return; 13 | 14 | namespace Envoy { 15 | namespace Nats { 16 | namespace Streaming { 17 | 18 | class NatsStreamingClientImplTest : public testing::Test { 19 | public: 20 | Nats::ConnPoolNats::MockInstance *conn_pool_{ 21 | new Nats::ConnPoolNats::MockInstance()}; 22 | NiceMock random_; 23 | Event::MockDispatcher dispatcher_; 24 | std::chrono::milliseconds op_timeout_{5000}; 25 | MockPublishCallbacks callbacks_; 26 | PublishRequestPtr handle_; 27 | }; 28 | 29 | TEST_F(NatsStreamingClientImplTest, Empty) { 30 | // TODO(talnordan): This is a dummy test. 31 | ClientImpl client{Tcp::ConnPoolNats::InstancePtr{conn_pool_}, 32 | random_, dispatcher_, op_timeout_}; 33 | } 34 | 35 | } // namespace Streaming 36 | } // namespace Nats 37 | } // namespace Envoy 38 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/metadata_subject_retriever.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/router/router.h" 6 | #include "envoy/upstream/upstream.h" 7 | 8 | #include "common/protobuf/protobuf.h" 9 | 10 | #include "subject_retriever.h" 11 | 12 | namespace Envoy { 13 | namespace Extensions { 14 | namespace HttpFilters { 15 | namespace Nats { 16 | namespace Streaming { 17 | 18 | /** 19 | * TODO (talnordan): 20 | * template 21 | * class MetadataRetriever { 22 | * absl::optional::__type...>> get(const RouteEntry &routeEntry, 24 | * const ClusterInfo &info); 25 | * }; 26 | */ 27 | class MetadataSubjectRetriever : public SubjectRetriever { 28 | public: 29 | MetadataSubjectRetriever(); 30 | 31 | absl::optional 32 | getSubject(const Http::MetadataAccessor &metadataccessor); 33 | 34 | private: 35 | static absl::optional 36 | nonEmptyStringValue(const ProtobufWkt::Struct &spec, const std::string &key); 37 | }; 38 | 39 | } // namespace Streaming 40 | } // namespace Nats 41 | } // namespace HttpFilters 42 | } // namespace Extensions 43 | } // namespace Envoy 44 | -------------------------------------------------------------------------------- /source/common/tcp/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "codec_lib", 13 | hdrs = ["codec_impl.h"], 14 | repository = "@envoy", 15 | deps = [ 16 | "//include/envoy/tcp:codec_interface", 17 | "@envoy//source/common/common:assert_lib", 18 | "@envoy//source/common/common:logger_lib", 19 | "@envoy//source/common/common:utility_lib", 20 | ], 21 | ) 22 | 23 | envoy_cc_library( 24 | name = "conn_pool_lib", 25 | hdrs = ["conn_pool_impl.h"], 26 | repository = "@envoy", 27 | deps = [ 28 | ":codec_lib", 29 | "//include/envoy/tcp:conn_pool_interface", 30 | "@envoy//include/envoy/router:router_interface", 31 | "@envoy//include/envoy/thread_local:thread_local_interface", 32 | "@envoy//include/envoy/upstream:cluster_manager_interface", 33 | "@envoy//source/common/buffer:buffer_lib", 34 | "@envoy//source/common/common:assert_lib", 35 | "@envoy//source/common/network:filter_lib", 36 | "@envoy//source/common/protobuf:utility_lib", 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "common/config/nats_streaming_well_known_names.h" 6 | 7 | #include "extensions/filters/http/common/factory_base.h" 8 | 9 | #include "nats_streaming_filter.pb.validate.h" 10 | 11 | namespace Envoy { 12 | namespace Extensions { 13 | namespace HttpFilters { 14 | namespace Nats { 15 | namespace Streaming { 16 | 17 | using Extensions::HttpFilters::Common::FactoryBase; 18 | 19 | /** 20 | * Config registration for the NATS Streaming filter. 21 | */ 22 | class NatsStreamingFilterConfigFactory 23 | : public FactoryBase { 24 | public: 25 | NatsStreamingFilterConfigFactory() 26 | : FactoryBase( 27 | Config::NatsStreamingHttpFilterNames::get().NATS_STREAMING) {} 28 | 29 | private: 30 | Http::FilterFactoryCb createFilterFactoryFromProtoTyped( 31 | const envoy::api::v2::filter::http::NatsStreaming &proto_config, 32 | const std::string &stats_prefix, 33 | Server::Configuration::FactoryContext &context) override; 34 | }; 35 | 36 | } // namespace Streaming 37 | } // namespace Nats 38 | } // namespace HttpFilters 39 | } // namespace Extensions 40 | } // namespace Envoy 41 | -------------------------------------------------------------------------------- /test/common/nats/message_builder_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/nats/message_builder.h" 3 | 4 | #include "test/mocks/nats/mocks.h" 5 | #include "test/test_common/printers.h" 6 | #include "test/test_common/utility.h" 7 | 8 | namespace Envoy { 9 | namespace Nats { 10 | 11 | class NatsMessageBuilderTest : public testing::Test { 12 | public: 13 | NatsMessageBuilderTest() {} 14 | }; 15 | 16 | TEST_F(NatsMessageBuilderTest, ConnectMessage) { 17 | Message expected_message{ 18 | R"(CONNECT {"verbose":false,"pedantic":false,"tls_required":false,"name":"","lang":"cpp","version":"1.2.2","protocol":1})"}; 19 | auto actual_message = MessageBuilder::createConnectMessage(); 20 | ASSERT_EQ(expected_message, actual_message); 21 | } 22 | 23 | TEST_F(NatsMessageBuilderTest, PubMessage) { 24 | Message expected_message{"PUB subject1 0\r\n"}; 25 | auto actual_message = MessageBuilder::createPubMessage("subject1"); 26 | ASSERT_EQ(expected_message, actual_message); 27 | } 28 | 29 | TEST_F(NatsMessageBuilderTest, SubMessage) { 30 | Message expected_message{"SUB subject1 6"}; 31 | auto actual_message = MessageBuilder::createSubMessage("subject1", 6); 32 | ASSERT_EQ(expected_message, actual_message); 33 | } 34 | 35 | } // namespace Nats 36 | } // namespace Envoy 37 | -------------------------------------------------------------------------------- /test/mocks/nats/streaming/mocks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common/nats/streaming/client_impl.h" 4 | 5 | #include "gmock/gmock.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | namespace Streaming { 10 | 11 | class MockPublishCallbacks : public PublishCallbacks { 12 | public: 13 | MockPublishCallbacks(); 14 | ~MockPublishCallbacks(); 15 | 16 | MOCK_METHOD0(onResponse, void()); 17 | MOCK_METHOD0(onFailure, void()); 18 | MOCK_METHOD0(onTimeout, void()); 19 | }; 20 | 21 | class MockClient : public Client { 22 | public: 23 | MockClient(); 24 | ~MockClient(); 25 | 26 | PublishRequestPtr makeRequest(const std::string &subject, 27 | const std::string &cluster_id, 28 | const std::string &discover_prefix, 29 | std::string &&payload, 30 | PublishCallbacks &callbacks) override; 31 | 32 | MOCK_METHOD5(makeRequest_, 33 | PublishRequestPtr(const std::string &, const std::string &, 34 | const std::string &, const std::string &, 35 | PublishCallbacks &callbacks)); 36 | 37 | std::string last_payload_; 38 | }; 39 | 40 | } // namespace Streaming 41 | } // namespace Nats 42 | } // namespace Envoy 43 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | workspace(name="nats_streaming_filter") 2 | 3 | # Use skylark for native Git. 4 | load('@bazel_tools//tools/build_defs/repo:git.bzl', 'git_repository') 5 | 6 | ENVOY_SHA = "45b90082918b4aed4e3c53a2a5cf79ba1b206505" # 2018-08-10 7 | 8 | http_archive( 9 | name = "envoy", 10 | strip_prefix = "envoy-" + ENVOY_SHA, 11 | url = "https://github.com/envoyproxy/envoy/archive/" + ENVOY_SHA + ".zip", 12 | ) 13 | 14 | ENVOY_COMMON_SHA = "79194c3b1ce1d5c7e5bb84ada2cc757efef36180" # July 15, 2018 (Upgrade Envoy) 15 | 16 | git_repository( 17 | name = "solo_envoy_common", 18 | remote = "https://github.com/solo-io/envoy-common", 19 | commit = ENVOY_COMMON_SHA, 20 | ) 21 | 22 | load("@envoy//bazel:repositories.bzl", "envoy_dependencies") 23 | load("@envoy//bazel:cc_configure.bzl", "cc_configure") 24 | 25 | envoy_dependencies() 26 | 27 | cc_configure() 28 | 29 | load("@envoy_api//bazel:repositories.bzl", "api_dependencies") 30 | api_dependencies() 31 | 32 | load("@io_bazel_rules_go//go:def.bzl", "go_rules_dependencies", "go_register_toolchains") 33 | load("@com_lyft_protoc_gen_validate//bazel:go_proto_library.bzl", "go_proto_repositories") 34 | go_proto_repositories(shared=0) 35 | go_rules_dependencies() 36 | go_register_toolchains() 37 | load("@io_bazel_rules_go//proto:def.bzl", "proto_register_toolchains") 38 | proto_register_toolchains() 39 | -------------------------------------------------------------------------------- /test/extensions/filters/http/nats/streaming/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_mock", 6 | "envoy_cc_test", 7 | "envoy_package", 8 | ) 9 | 10 | envoy_package() 11 | 12 | envoy_cc_test( 13 | name = "metadata_subject_retriever_test", 14 | srcs = ["metadata_subject_retriever_test.cc"], 15 | repository = "@envoy", 16 | deps = [ 17 | "//source/extensions/filters/http/nats/streaming:nats_streaming_filter_lib", 18 | "@envoy//test/test_common:utility_lib", 19 | ], 20 | ) 21 | 22 | envoy_cc_test( 23 | name = "nats_streaming_filter_test", 24 | srcs = ["nats_streaming_filter_test.cc"], 25 | repository = "@envoy", 26 | deps = [ 27 | ":mocks", 28 | "//source/extensions/filters/http/nats/streaming:nats_streaming_filter_config_lib", 29 | "//test/mocks/nats/streaming:nats_streaming_mocks", 30 | "@envoy//test/mocks/http:http_mocks", 31 | "@envoy//test/mocks/server:server_mocks", 32 | "@envoy//test/mocks/upstream:upstream_mocks", 33 | ], 34 | ) 35 | 36 | envoy_cc_mock( 37 | name = "mocks", 38 | srcs = ["mocks.cc"], 39 | hdrs = ["mocks.h"], 40 | repository = "@envoy", 41 | deps = [ 42 | "//source/extensions/filters/http/nats/streaming:nats_streaming_filter_lib", 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /source/common/nats/codec_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "envoy/nats/codec.h" 4 | #include "envoy/tcp/codec.h" 5 | 6 | #include "common/common/logger.h" 7 | 8 | namespace Envoy { 9 | namespace Nats { 10 | 11 | using Tcp::Decoder; 12 | using Tcp::DecoderCallbacks; 13 | using Tcp::Encoder; 14 | 15 | /** 16 | * Decoder implementation of 17 | * https://nats.io/documentation/internals/nats-protocol/ 18 | * 19 | * This implementation buffers when needed and will always consume all bytes 20 | * passed for decoding. 21 | */ 22 | class DecoderImpl : public Decoder, Logger::Loggable { 23 | public: 24 | DecoderImpl(DecoderCallbacks &callbacks) : callbacks_(callbacks) {} 25 | 26 | // Tcp::Decoder 27 | void decode(Buffer::Instance &data) override; 28 | 29 | private: 30 | enum class State { ValueRootStart, SimpleString, LF, ValueComplete }; 31 | 32 | void parseSlice(const Buffer::RawSlice &slice); 33 | 34 | DecoderCallbacks &callbacks_; 35 | State state_{State::ValueRootStart}; 36 | MessagePtr pending_value_root_; 37 | }; 38 | 39 | /** 40 | * Encoder implementation of 41 | * https://nats.io/documentation/internals/nats-protocol/ 42 | */ 43 | class EncoderImpl : public Encoder { 44 | public: 45 | // Tcp::Encoder 46 | void encode(const Message &value, Buffer::Instance &out) override; 47 | }; 48 | 49 | } // namespace Nats 50 | } // namespace Envoy 51 | -------------------------------------------------------------------------------- /source/common/nats/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "codec_lib", 13 | srcs = ["codec_impl.cc"], 14 | hdrs = ["codec_impl.h"], 15 | repository = "@envoy", 16 | deps = [ 17 | "//include/envoy/nats:codec_interface", 18 | "//include/envoy/tcp:codec_interface", 19 | "@envoy//source/common/common:assert_lib", 20 | "@envoy//source/common/common:logger_lib", 21 | "@envoy//source/common/common:utility_lib", 22 | ], 23 | ) 24 | 25 | envoy_cc_library( 26 | name = "message_builder_lib", 27 | srcs = ["message_builder.cc"], 28 | hdrs = ["message_builder.h"], 29 | repository = "@envoy", 30 | deps = [ 31 | "//include/envoy/nats:codec_interface", 32 | ], 33 | ) 34 | 35 | envoy_cc_library( 36 | name = "subject_utility_lib", 37 | hdrs = ["subject_utility.h"], 38 | repository = "@envoy", 39 | deps = [ 40 | "//include/envoy/nats:token_generator_interface", 41 | ], 42 | ) 43 | 44 | envoy_cc_library( 45 | name = "token_generator_lib", 46 | srcs = ["token_generator_impl.cc"], 47 | hdrs = ["token_generator_impl.h"], 48 | repository = "@envoy", 49 | deps = [ 50 | "//include/envoy/nats:token_generator_interface", 51 | "//source/common/nats/nuid:nuid_lib", 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /source/common/nats/message_builder.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/message_builder.h" 2 | 3 | #include 4 | 5 | namespace Envoy { 6 | namespace Nats { 7 | 8 | Message MessageBuilder::createConnectMessage() { 9 | return Message( 10 | R"(CONNECT {"verbose":false,"pedantic":false,"tls_required":false,"name":"","lang":"cpp","version":"1.2.2","protocol":1})"); 11 | } 12 | 13 | Message MessageBuilder::createPubMessage(const std::string &subject) { 14 | std::stringstream ss; 15 | ss << "PUB " << subject << " 0\r\n"; 16 | return Message(ss.str()); 17 | } 18 | 19 | Message MessageBuilder::createPubMessage(const std::string &subject, 20 | const std::string &reply_to, 21 | const std::string &payload) { 22 | // TODO(talnordan): Consider introducing a more explicit way to construct and 23 | // encode messages consisting of two lines. 24 | std::stringstream ss; 25 | ss << "PUB " << subject << " " << reply_to << " " << payload.length() 26 | << "\r\n" 27 | << payload; 28 | return Message(ss.str()); 29 | } 30 | 31 | Message MessageBuilder::createSubMessage(const std::string &subject, 32 | uint64_t sid) { 33 | std::stringstream ss; 34 | ss << "SUB " << subject << " " << sid; 35 | return Message(ss.str()); 36 | } 37 | 38 | Message MessageBuilder::createPongMessage() { return Message("PONG"); } 39 | 40 | } // namespace Nats 41 | } // namespace Envoy 42 | -------------------------------------------------------------------------------- /source/common/config/nats_streaming_well_known_names.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "common/singleton/const_singleton.h" 6 | 7 | namespace Envoy { 8 | namespace Config { 9 | 10 | // TODO(talnordan): TODO: Merge with 11 | // envoy/source/common/config/well_known_names.h. 12 | 13 | /** 14 | * Well-known http filter names. 15 | */ 16 | class NatsStreamingHttpFilterNameValues { 17 | public: 18 | // NATS Streaming filter 19 | const std::string NATS_STREAMING = "io.solo.nats_streaming"; 20 | }; 21 | 22 | typedef ConstSingleton 23 | NatsStreamingHttpFilterNames; 24 | 25 | /** 26 | * Well-known metadata filter namespaces. 27 | */ 28 | class NatsStreamingMetadataFilterValues { 29 | public: 30 | // Filter namespace for NATS Streaming Filter. 31 | const std::string NATS_STREAMING = "io.solo.nats_streaming"; 32 | }; 33 | 34 | typedef ConstSingleton 35 | NatsStreamingMetadataFilters; 36 | 37 | /** 38 | * Keys for NatsStreamingMetadataFilterValues::NATS_STREAMING metadata. 39 | */ 40 | class MetadataNatsStreamingKeyValues { 41 | public: 42 | // Key in the NATS Streaming Filter namespace for discover prefix value. 43 | const std::string DISCOVER_PREFIX = "discover_prefix"; 44 | 45 | // Key in the NATS Streaming Filter namespace for Cluster ID value. 46 | const std::string CLUSTER_ID = "cluster_id"; 47 | }; 48 | 49 | typedef ConstSingleton 50 | MetadataNatsStreamingKeys; 51 | 52 | } // namespace Config 53 | } // namespace Envoy 54 | -------------------------------------------------------------------------------- /test/common/nats/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_test", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_test( 12 | name = "codec_impl_test", 13 | srcs = ["codec_impl_test.cc"], 14 | repository = "@envoy", 15 | deps = [ 16 | "//source/common/nats:codec_lib", 17 | "//test/mocks/nats:nats_mocks", 18 | "@envoy//source/common/buffer:buffer_lib", 19 | "@envoy//source/common/common:assert_lib", 20 | ], 21 | ) 22 | 23 | envoy_cc_test( 24 | name = "message_builder_test", 25 | srcs = ["message_builder_test.cc"], 26 | repository = "@envoy", 27 | deps = [ 28 | "//source/common/nats:message_builder_lib", 29 | "//test/mocks/nats:nats_mocks", 30 | "@envoy//source/common/common:assert_lib", 31 | "@envoy//test/test_common:utility_lib", 32 | ], 33 | ) 34 | 35 | envoy_cc_test( 36 | name = "subject_utility_test", 37 | srcs = ["subject_utility_test.cc"], 38 | repository = "@envoy", 39 | deps = [ 40 | "//source/common/nats:subject_utility_lib", 41 | "@envoy//source/common/common:assert_lib", 42 | ], 43 | ) 44 | 45 | envoy_cc_test( 46 | name = "token_generator_impl_test", 47 | srcs = ["token_generator_impl_test.cc"], 48 | repository = "@envoy", 49 | deps = [ 50 | "//source/common/nats:token_generator_lib", 51 | "@envoy//source/common/common:assert_lib", 52 | "@envoy//test/mocks/runtime:runtime_mocks", 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Envoy NATS Streaming filter 2 | 3 | This project links a NATS Streaming HTTP filter with the Envoy binary. 4 | A new filter `io.solo.nats_streaming` which redirects requests to NATS Streaming is introduced. 5 | 6 | ## Building 7 | 8 | To build the Envoy static binary: 9 | 10 | ``` 11 | $ bazel build //:envoy 12 | ``` 13 | 14 | ## Testing 15 | 16 | To run the all tests: 17 | 18 | ``` 19 | $ bazel test //test/... 20 | ``` 21 | 22 | To run the all tests in debug mode: 23 | 24 | ``` 25 | $ bazel test //test/... -c dbg 26 | ``` 27 | 28 | To run integration tests using a clang build: 29 | 30 | ``` 31 | $ CXX=clang++-5.0 CC=clang-5.0 bazel test -c dbg --config=clang-tsan //test/integration:nats_streaming_filter_integration_test 32 | ``` 33 | 34 | ## E2E 35 | 36 | The e2e tests depend on `nats-streaming-server` and `stan-sub`, which need to be in your path. 37 | They also require the [GRequests](https://github.com/kennethreitz/grequests) Python package. 38 | 39 | To install GRequests: 40 | 41 | ``` 42 | $ pip install grequests 43 | ``` 44 | 45 | To run the e2e test: 46 | 47 | ``` 48 | $ bazel test //e2e/... 49 | ``` 50 | 51 | ## Profiling 52 | To run a profiler, first install the `perf` tool. In ubuntu run these command (a reboot may be needed): 53 | ``` 54 | $ sudo apt install linux-tools-generic linux-tools-common 55 | ```` 56 | 57 | Then: 58 | ``` 59 | $ cd e2e 60 | $ ulimit -n 2048 61 | $ DEBUG=0 TEST_ENVOY_BIN=../bazel-bin/envoy TEST_PROF_REPORT=report.data python e2e_test.py 2> output.txt 62 | ``` 63 | To read the report, run: 64 | ``` 65 | $ perf report 66 | ``` 67 | -------------------------------------------------------------------------------- /test/mocks/nats/streaming/mocks.cc: -------------------------------------------------------------------------------- 1 | #include "mocks.h" 2 | 3 | #include "common/common/macros.h" 4 | 5 | using testing::_; 6 | using testing::Invoke; 7 | 8 | namespace Envoy { 9 | namespace Nats { 10 | namespace Streaming { 11 | 12 | MockPublishCallbacks::MockPublishCallbacks() {} 13 | MockPublishCallbacks::~MockPublishCallbacks() {} 14 | 15 | MockClient::MockClient() { 16 | ON_CALL(*this, makeRequest_(_, _, _, _, _)) 17 | .WillByDefault(Invoke( 18 | [this](const std::string &subject, const std::string &cluster_id, 19 | const std::string &discover_prefix, const std::string &payload, 20 | PublishCallbacks &callbacks) -> PublishRequestPtr { 21 | UNREFERENCED_PARAMETER(subject); 22 | UNREFERENCED_PARAMETER(cluster_id); 23 | UNREFERENCED_PARAMETER(discover_prefix); 24 | 25 | last_payload_ = payload; 26 | callbacks.onResponse(); 27 | return nullptr; 28 | })); 29 | } 30 | 31 | MockClient::~MockClient() {} 32 | 33 | PublishRequestPtr MockClient::makeRequest(const std::string &subject, 34 | const std::string &cluster_id, 35 | const std::string &discover_prefix, 36 | std::string &&payload, 37 | PublishCallbacks &callbacks) { 38 | return makeRequest_(subject, cluster_id, discover_prefix, payload, callbacks); 39 | } 40 | 41 | } // namespace Streaming 42 | } // namespace Nats 43 | } // namespace Envoy 44 | -------------------------------------------------------------------------------- /source/common/nats/streaming/message_utility.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "absl/types/optional.h" 7 | #include "protocol.pb.h" 8 | 9 | namespace Envoy { 10 | namespace Nats { 11 | namespace Streaming { 12 | 13 | class MessageUtility { 14 | public: 15 | static std::string 16 | createConnectRequestMessage(const std::string &client_id, 17 | const std::string &heartbeat_inbox); 18 | 19 | static std::string createConnectResponseMessage( 20 | const std::string &pub_prefix, const std::string &sub_requests, 21 | const std::string &unsub_requests, const std::string &close_requests); 22 | 23 | static std::string createPubMsgMessage(const std::string &client_id, 24 | const std::string &guid, 25 | const std::string &subject, 26 | const std::string &data); 27 | 28 | static std::string createPubAckMessage(const std::string &guid, 29 | const std::string &error); 30 | 31 | static absl::optional 32 | parsePubAckMessage(const std::string &pub_ack_message); 33 | 34 | static std::string getPubPrefix(const std::string &connect_response_message); 35 | 36 | private: 37 | template static std::string serializeToString(T &&message) { 38 | std::string message_str; 39 | message.SerializeToString(&message_str); 40 | 41 | return message_str; 42 | } 43 | }; 44 | 45 | } // namespace Streaming 46 | } // namespace Nats 47 | } // namespace Envoy 48 | -------------------------------------------------------------------------------- /test/common/nats/nuid/nuid_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "common/nats/nuid/nuid.h" 4 | #include "common/runtime/runtime_impl.h" 5 | 6 | #include "gmock/gmock.h" 7 | #include "gtest/gtest.h" 8 | 9 | using testing::NiceMock; 10 | 11 | namespace Envoy { 12 | namespace Nats { 13 | namespace Nuid { 14 | 15 | /** 16 | * See https://github.com/nats-io/nuid/blob/master/nuid_test.go 17 | */ 18 | class NuidTest : public testing::Test { 19 | protected: 20 | Runtime::RandomGeneratorImpl random_generator_; 21 | }; 22 | 23 | TEST_F(NuidTest, Digits) { EXPECT_EQ(Nuid::BASE, strlen(Nuid::DIGITS)); } 24 | 25 | TEST_F(NuidTest, Rollover) { 26 | Nuid nuid(random_generator_, Nuid::MAX_SEQ - 1); 27 | std::string old_pre = nuid.pre(); 28 | nuid.next(); 29 | EXPECT_NE(old_pre, nuid.pre()); 30 | } 31 | 32 | TEST_F(NuidTest, Length) { 33 | Nuid nuid(random_generator_); 34 | EXPECT_EQ(Nuid::TOTAL_LEN, nuid.next().length()); 35 | } 36 | 37 | TEST_F(NuidTest, ProperPrefix) { 38 | auto min = CHAR_MAX; 39 | auto max = CHAR_MIN; 40 | for (auto i = 0; i < Nuid::BASE; ++i) { 41 | if (Nuid::DIGITS[i] < min) { 42 | min = Nuid::DIGITS[i]; 43 | } 44 | if (Nuid::DIGITS[i] > max) { 45 | max = Nuid::DIGITS[i]; 46 | } 47 | } 48 | EXPECT_EQ('0', min); 49 | EXPECT_EQ('z', max); 50 | auto total = 100000; 51 | for (auto i = 0; i < total; ++i) { 52 | Nuid n(random_generator_); 53 | auto pre = n.pre(); 54 | for (auto j = 0; j < Nuid::PRE_LEN; ++j) { 55 | EXPECT_GE(pre[j], min); 56 | EXPECT_LE(pre[j], max); 57 | } 58 | } 59 | } 60 | 61 | } // namespace Nuid 62 | } // namespace Nats 63 | } // namespace Envoy 64 | -------------------------------------------------------------------------------- /e2e/create_config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | set -e 5 | 6 | # prepare envoy config file. 7 | 8 | cat > envoy.yaml << EOF 9 | admin: 10 | access_log_path: /dev/stdout 11 | address: 12 | socket_address: 13 | address: 127.0.0.1 14 | port_value: 19000 15 | static_resources: 16 | listeners: 17 | - name: listener_0 18 | address: 19 | socket_address: { address: 127.0.0.1, port_value: 10000 } 20 | filter_chains: 21 | - filters: 22 | - name: envoy.http_connection_manager 23 | config: 24 | stat_prefix: http 25 | codec_type: AUTO 26 | route_config: 27 | name: local_route 28 | virtual_hosts: 29 | - name: local_service 30 | domains: ["*"] 31 | routes: 32 | - match: 33 | prefix: /post 34 | route: 35 | cluster: cluster_0 36 | metadata: 37 | filter_metadata: 38 | io.solo.function_router: 39 | cluster_0: 40 | function: subject1 41 | http_filters: 42 | - name: io.solo.nats_streaming 43 | config: 44 | op_timeout: 1s 45 | cluster: cluster_0 46 | max_connections: 1 47 | - name: envoy.router 48 | clusters: 49 | - connect_timeout: 5.000s 50 | hosts: 51 | - socket_address: 52 | address: localhost 53 | port_value: 4222 54 | name: cluster_0 55 | type: STRICT_DNS 56 | metadata: 57 | filter_metadata: 58 | io.solo.nats_streaming: 59 | discover_prefix: _STAN.discover 60 | cluster_id: test-cluster 61 | EOF 62 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/nats_streaming_filter_config.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "envoy/upstream/cluster_manager.h" 7 | 8 | #include "common/protobuf/utility.h" 9 | 10 | #include "nats_streaming_filter.pb.h" 11 | 12 | namespace Envoy { 13 | namespace Extensions { 14 | namespace HttpFilters { 15 | namespace Nats { 16 | namespace Streaming { 17 | 18 | class NatsStreamingFilterConfig { 19 | 20 | using ProtoConfig = envoy::api::v2::filter::http::NatsStreaming; 21 | 22 | public: 23 | NatsStreamingFilterConfig(const ProtoConfig &proto_config, 24 | Upstream::ClusterManager &clusterManager) 25 | : op_timeout_(PROTOBUF_GET_MS_OR_DEFAULT(proto_config, op_timeout, 5000)), 26 | cluster_(proto_config.cluster()), 27 | max_connections_(proto_config.max_connections()) { 28 | if (max_connections_ != 1) { 29 | throw EnvoyException("nats-streaming filter: only one concurrent " 30 | "connection is currently supported"); 31 | } 32 | if (!clusterManager.get(cluster_)) { 33 | throw EnvoyException(fmt::format( 34 | "nats-streaming filter: unknown cluster '{}' in config", cluster_)); 35 | } 36 | } 37 | 38 | const std::chrono::milliseconds &opTimeout() const { return op_timeout_; } 39 | const std::string &cluster() const { return cluster_; } 40 | uint32_t maxConnections() const { return max_connections_; } 41 | 42 | private: 43 | std::chrono::milliseconds op_timeout_; 44 | std::string cluster_; 45 | uint32_t max_connections_; 46 | }; 47 | 48 | typedef std::shared_ptr 49 | NatsStreamingFilterConfigSharedPtr; 50 | 51 | } // namespace Streaming 52 | } // namespace Nats 53 | } // namespace HttpFilters 54 | } // namespace Extensions 55 | } // namespace Envoy 56 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/metadata_subject_retriever.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/filters/http/nats/streaming/metadata_subject_retriever.h" 2 | 3 | #include "common/common/macros.h" 4 | #include "common/config/metadata.h" 5 | #include "common/config/nats_streaming_well_known_names.h" 6 | #include "common/config/solo_metadata.h" 7 | 8 | namespace Envoy { 9 | namespace Extensions { 10 | namespace HttpFilters { 11 | namespace Nats { 12 | namespace Streaming { 13 | 14 | using Config::SoloMetadata; 15 | 16 | MetadataSubjectRetriever::MetadataSubjectRetriever() {} 17 | 18 | absl::optional MetadataSubjectRetriever::getSubject( 19 | const Http::MetadataAccessor &metadataccessor) { 20 | auto maybe_subject = metadataccessor.getFunctionName(); 21 | if (!maybe_subject.has_value()) { 22 | return {}; 23 | } 24 | 25 | absl::optional maybe_cluster_meta = 26 | metadataccessor.getClusterMetadata(); 27 | if (!maybe_cluster_meta.has_value()) { 28 | return {}; 29 | } 30 | const ProtobufWkt::Struct *cluster_meta = maybe_cluster_meta.value(); 31 | 32 | auto maybe_discover_prefix = SoloMetadata::nonEmptyStringValue( 33 | *cluster_meta, Config::MetadataNatsStreamingKeys::get().DISCOVER_PREFIX); 34 | if (!maybe_discover_prefix.has_value()) { 35 | return {}; 36 | } 37 | 38 | auto maybe_cluster_id = SoloMetadata::nonEmptyStringValue( 39 | *cluster_meta, Config::MetadataNatsStreamingKeys::get().CLUSTER_ID); 40 | if (!maybe_cluster_id.has_value()) { 41 | return {}; 42 | } 43 | 44 | Subject subject{maybe_subject.value(), maybe_cluster_id.value(), 45 | maybe_discover_prefix.value()}; 46 | 47 | return subject; 48 | } 49 | 50 | } // namespace Streaming 51 | } // namespace Nats 52 | } // namespace HttpFilters 53 | } // namespace Extensions 54 | } // namespace Envoy 55 | -------------------------------------------------------------------------------- /source/common/nats/streaming/client_pool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "envoy/nats/codec.h" 4 | #include "envoy/nats/streaming/client.h" 5 | #include "envoy/tcp/conn_pool_nats.h" 6 | #include "envoy/thread_local/thread_local.h" 7 | #include "envoy/upstream/cluster_manager.h" 8 | 9 | #include "common/nats/streaming/client_impl.h" 10 | 11 | namespace Envoy { 12 | namespace Nats { 13 | namespace Streaming { 14 | 15 | class ClientPool : public Client { 16 | public: 17 | ClientPool(const std::string &cluster_name, Upstream::ClusterManager &cm, 18 | Tcp::ConnPoolNats::ClientFactory &client_factory, 19 | ThreadLocal::SlotAllocator &tls, Runtime::RandomGenerator &random, 20 | const std::chrono::milliseconds &op_timeout); 21 | 22 | // Nats::Streaming::Client 23 | PublishRequestPtr makeRequest(const std::string &subject, 24 | const std::string &cluster_id, 25 | const std::string &discover_prefix, 26 | std::string &&payload, 27 | PublishCallbacks &callbacks) override; 28 | 29 | private: 30 | struct ThreadLocalPool : public ThreadLocal::ThreadLocalObject { 31 | ThreadLocalPool(Tcp::ConnPoolNats::InstancePtr &&conn_pool, 32 | Runtime::RandomGenerator &random, 33 | Event::Dispatcher &dispatcher, 34 | const std::chrono::milliseconds &op_timeout); 35 | Client &getClient(); 36 | 37 | private: 38 | ClientImpl client_; 39 | }; 40 | 41 | Upstream::ClusterManager &cm_; 42 | Tcp::ConnPoolNats::ClientFactory &client_factory_; 43 | ThreadLocal::SlotPtr slot_; 44 | Runtime::RandomGenerator &random_; 45 | const std::chrono::milliseconds op_timeout_; 46 | }; 47 | 48 | } // namespace Streaming 49 | } // namespace Nats 50 | } // namespace Envoy 51 | -------------------------------------------------------------------------------- /test/common/nats/streaming/heartbeat_handler_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/nats/message_builder.h" 3 | #include "common/nats/streaming/heartbeat_handler.h" 4 | 5 | #include "test/mocks/nats/mocks.h" 6 | 7 | #include "gmock/gmock.h" 8 | 9 | namespace Envoy { 10 | namespace Nats { 11 | namespace Streaming { 12 | 13 | class NatsStreamingHeartbeatHandlerTest : public testing::Test { 14 | public: 15 | NatsStreamingHeartbeatHandlerTest() {} 16 | 17 | protected: 18 | class MockCallbacks : public HeartbeatHandler::Callbacks { 19 | public: 20 | MOCK_METHOD1(send, void(const Message &message)); 21 | MOCK_METHOD1(onFailure, void(const std::string &error)); 22 | }; 23 | 24 | MockCallbacks callbacks_; 25 | }; 26 | 27 | TEST_F(NatsStreamingHeartbeatHandlerTest, NoReplyTo) { 28 | absl::optional reply_to{}; 29 | const std::string payload{}; 30 | 31 | EXPECT_CALL(callbacks_, onFailure("incoming heartbeat without reply subject")) 32 | .Times(1); 33 | HeartbeatHandler::onMessage(reply_to, payload, callbacks_); 34 | } 35 | 36 | TEST_F(NatsStreamingHeartbeatHandlerTest, NonEmptyPayload) { 37 | absl::optional reply_to{"reply-to"}; 38 | const std::string payload{"payload"}; 39 | 40 | EXPECT_CALL(callbacks_, 41 | onFailure("incoming heartbeat with non-empty payload")) 42 | .Times(1); 43 | HeartbeatHandler::onMessage(reply_to, payload, callbacks_); 44 | } 45 | 46 | TEST_F(NatsStreamingHeartbeatHandlerTest, Reply) { 47 | absl::optional reply_to{"reply-to"}; 48 | const std::string payload{}; 49 | Message expected_message = MessageBuilder::createPubMessage(reply_to.value()); 50 | EXPECT_CALL(callbacks_, send(expected_message)).Times(1); 51 | HeartbeatHandler::onMessage(reply_to, payload, callbacks_); 52 | } 53 | 54 | } // namespace Streaming 55 | } // namespace Nats 56 | } // namespace Envoy 57 | -------------------------------------------------------------------------------- /test/mocks/tcp/mocks.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/common/macros.h" 3 | 4 | #include "test/mocks/tcp/mocks_nats.h" 5 | #include "test/test_common/printers.h" 6 | 7 | #include "gmock/gmock.h" 8 | #include "gtest/gtest.h" 9 | 10 | using testing::_; 11 | using testing::Invoke; 12 | 13 | namespace Envoy { 14 | namespace Tcp { 15 | 16 | MockEncoder::MockEncoder() { 17 | ON_CALL(*this, encode(_, _)) 18 | .WillByDefault(Invoke( 19 | [this](const std::string &value, Buffer::Instance &out) -> void { 20 | encodeSimpleString(value, out); 21 | })); 22 | } 23 | 24 | MockEncoder::~MockEncoder() {} 25 | 26 | void MockEncoder::encodeSimpleString(const std::string &string, 27 | Buffer::Instance &out) { 28 | out.add("+", 1); 29 | out.add(string); 30 | out.add("\r\n", 2); 31 | } 32 | 33 | MockDecoder::MockDecoder() {} 34 | 35 | MockDecoder::MockDecoder(DecoderCallbacks &callbacks) { 36 | UNREFERENCED_PARAMETER(callbacks); 37 | } 38 | 39 | MockDecoder::~MockDecoder() {} 40 | 41 | namespace ConnPoolNats { 42 | 43 | MockClient::MockClient() { 44 | ON_CALL(*this, addConnectionCallbacks(_)) 45 | .WillByDefault( 46 | Invoke([this](Network::ConnectionCallbacks &callbacks) -> void { 47 | callbacks_.push_back(&callbacks); 48 | })); 49 | ON_CALL(*this, close()).WillByDefault(Invoke([this]() -> void { 50 | raiseEvent(Network::ConnectionEvent::LocalClose); 51 | })); 52 | } 53 | 54 | MockClient::~MockClient() {} 55 | 56 | MockClientFactory::MockClientFactory() {} 57 | 58 | MockClientFactory::~MockClientFactory() {} 59 | 60 | MockPoolCallbacks::MockPoolCallbacks() {} 61 | MockPoolCallbacks::~MockPoolCallbacks() {} 62 | 63 | MockInstance::MockInstance() {} 64 | MockInstance::~MockInstance() {} 65 | 66 | } // namespace ConnPoolNats 67 | 68 | } // namespace Tcp 69 | } // namespace Envoy 70 | -------------------------------------------------------------------------------- /test/common/nats/subject_utility_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/nats/subject_utility.h" 3 | 4 | #include "gtest/gtest.h" 5 | 6 | namespace Envoy { 7 | namespace Nats { 8 | 9 | class NatsSubjectUtilityTest : public testing::Test { 10 | public: 11 | NatsSubjectUtilityTest() {} 12 | 13 | protected: 14 | struct MockTokenGenerator : public TokenGenerator { 15 | explicit MockTokenGenerator(const std::string &token) : token_(token) {} 16 | 17 | std::string random() override { return token_; } 18 | std::string token_; 19 | }; 20 | }; 21 | 22 | TEST_F(NatsSubjectUtilityTest, Join) { 23 | std::string expected_subject{"_STAN.acks.DL0Gdhfh3RvpPyDOLQuLNI"}; 24 | auto actual_subject = 25 | SubjectUtility::join("_STAN.acks", "DL0Gdhfh3RvpPyDOLQuLNI"); 26 | EXPECT_EQ(expected_subject, actual_subject); 27 | } 28 | 29 | TEST_F(NatsSubjectUtilityTest, RandomChild) { 30 | MockTokenGenerator token_generator{"H39pAjTnENSgSH3HIHnEON"}; 31 | std::string expected_subject{"_INBOX.H39pAjTnENSgSH3HIHnEON"}; 32 | auto actual_subject = SubjectUtility::randomChild("_INBOX", token_generator); 33 | EXPECT_EQ(expected_subject, actual_subject); 34 | } 35 | 36 | TEST_F(NatsSubjectUtilityTest, RandomGrandchild) { 37 | MockTokenGenerator token_generator{"gSH3HIH1YJ70TA744uhFid"}; 38 | std::string expected_subject{ 39 | "_INBOX.M2kl72gBUTGH12kgXu5c9i.gSH3HIH1YJ70TA744uhFid"}; 40 | auto actual_subject = SubjectUtility::randomChild( 41 | "_INBOX.M2kl72gBUTGH12kgXu5c9i", token_generator); 42 | EXPECT_EQ(expected_subject, actual_subject); 43 | } 44 | 45 | TEST_F(NatsSubjectUtilityTest, ChildWildcard) { 46 | std::string expected_subject{"_INBOX.M2kl72gBUTGH12kgXu5c9i.*"}; 47 | auto actual_subject = 48 | SubjectUtility::childWildcard("_INBOX.M2kl72gBUTGH12kgXu5c9i"); 49 | EXPECT_EQ(expected_subject, actual_subject); 50 | } 51 | 52 | } // namespace Nats 53 | } // namespace Envoy 54 | -------------------------------------------------------------------------------- /test/common/nats/streaming/connect_response_handler_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/nats/streaming/connect_response_handler.h" 3 | #include "common/nats/streaming/message_utility.h" 4 | 5 | #include "gmock/gmock.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | namespace Streaming { 10 | 11 | class NatsStreamingConnectResponseHandlerTest : public testing::Test { 12 | public: 13 | NatsStreamingConnectResponseHandlerTest() {} 14 | 15 | protected: 16 | struct MockCallbacks : ConnectResponseHandler::Callbacks { 17 | MOCK_METHOD1(onConnected, void(const std::string &pub_prefix)); 18 | MOCK_METHOD1(onFailure, void(const std::string &error)); 19 | }; 20 | 21 | MockCallbacks callbacks_; 22 | }; 23 | 24 | TEST_F(NatsStreamingConnectResponseHandlerTest, NonEmptyReplyTo) { 25 | absl::optional reply_to{"reply-to"}; 26 | const std::string payload{}; 27 | 28 | EXPECT_CALL( 29 | callbacks_, 30 | onFailure("incoming ConnectResponse with non-empty reply subject")) 31 | .Times(1); 32 | ConnectResponseHandler::onMessage(reply_to, payload, callbacks_); 33 | } 34 | 35 | TEST_F(NatsStreamingConnectResponseHandlerTest, NoPayload) { 36 | absl::optional reply_to{}; 37 | const std::string payload{}; 38 | 39 | EXPECT_CALL(callbacks_, onFailure("incoming ConnectResponse without payload")) 40 | .Times(1); 41 | ConnectResponseHandler::onMessage(reply_to, payload, callbacks_); 42 | } 43 | 44 | TEST_F(NatsStreamingConnectResponseHandlerTest, OnConnected) { 45 | absl::optional reply_to{}; 46 | const std::string payload{ 47 | MessageUtility::createConnectResponseMessage("pub_prefix_1", "", "", "")}; 48 | 49 | EXPECT_CALL(callbacks_, onConnected("pub_prefix_1")).Times(1); 50 | ConnectResponseHandler::onMessage(reply_to, payload, callbacks_); 51 | } 52 | 53 | } // namespace Streaming 54 | } // namespace Nats 55 | } // namespace Envoy 56 | -------------------------------------------------------------------------------- /source/common/nats/streaming/pub_request_handler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "envoy/event/timer.h" 7 | #include "envoy/nats/streaming/client.h" 8 | #include "envoy/nats/streaming/inbox_handler.h" 9 | 10 | #include "absl/types/optional.h" 11 | 12 | namespace Envoy { 13 | namespace Nats { 14 | namespace Streaming { 15 | 16 | // TODO(talnordan): Consider moving to `include/envoy`. 17 | class PubRequest { 18 | public: 19 | PubRequest(PublishCallbacks *callbacks, Event::TimerPtr timeout_timer) 20 | : callbacks_(callbacks), timeout_timer_(std::move(timeout_timer)) {} 21 | 22 | PublishCallbacks &callbacks() { return *callbacks_; } 23 | 24 | void onDestroy() { 25 | timeout_timer_->disableTimer(); 26 | timeout_timer_ = nullptr; 27 | } 28 | 29 | private: 30 | PublishCallbacks *callbacks_; 31 | Event::TimerPtr timeout_timer_; 32 | }; 33 | 34 | class PubRequestHandler { 35 | public: 36 | static void onMessage(const absl::optional &reply_to, 37 | const std::string &payload, 38 | InboxCallbacks &inbox_callbacks, 39 | PublishCallbacks &publish_callbacks); 40 | 41 | static void onMessage(const std::string &inbox, 42 | const absl::optional &reply_to, 43 | const std::string &payload, 44 | InboxCallbacks &inbox_callbacks, 45 | std::map &request_per_inbox); 46 | 47 | static void onTimeout(const std::string &inbox, 48 | std::map &request_per_inbox); 49 | 50 | static void onCancel(const std::string &inbox, 51 | std::map &request_per_inbox); 52 | 53 | private: 54 | static inline void 55 | eraseRequest(std::map &request_per_inbox, 56 | std::map::iterator position); 57 | }; 58 | 59 | } // namespace Streaming 60 | } // namespace Nats 61 | } // namespace Envoy 62 | -------------------------------------------------------------------------------- /include/envoy/tcp/codec.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "envoy/buffer/buffer.h" 7 | #include "envoy/common/exception.h" 8 | #include "envoy/common/pure.h" 9 | 10 | namespace Envoy { 11 | namespace Tcp { 12 | 13 | template using MessagePtr = std::unique_ptr; 14 | 15 | /** 16 | * Callbacks that the decoder fires. 17 | */ 18 | template class DecoderCallbacks { 19 | public: 20 | virtual ~DecoderCallbacks() {} 21 | 22 | /** 23 | * Called when a new top level value has been decoded. 24 | * @param value supplies the decoded value that is now owned by the callee. 25 | */ 26 | virtual void onValue(MessagePtr &&value) PURE; 27 | }; 28 | 29 | /** 30 | * A byte decoder. 31 | */ 32 | class Decoder { 33 | public: 34 | virtual ~Decoder() {} 35 | 36 | /** 37 | * Decode protocol bytes. 38 | * @param data supplies the data to decode. All bytes will be consumed by the 39 | * decoder or a ProtocolError will be thrown. 40 | */ 41 | virtual void decode(Buffer::Instance &data) PURE; 42 | }; 43 | 44 | typedef std::unique_ptr DecoderPtr; 45 | 46 | /** 47 | * A factory for a decoder. 48 | */ 49 | template class DecoderFactory { 50 | public: 51 | virtual ~DecoderFactory() {} 52 | 53 | /** 54 | * Create a decoder given a set of decoder callbacks. 55 | */ 56 | virtual DecoderPtr create(DecoderCallbacks &callbacks) PURE; 57 | }; 58 | 59 | /** 60 | * A byte encoder. 61 | */ 62 | template class Encoder { 63 | public: 64 | virtual ~Encoder() {} 65 | 66 | /** 67 | * Encode a value to a buffer. 68 | * @param value supplies the value to encode. 69 | * @param out supplies the buffer to encode to. 70 | */ 71 | virtual void encode(const T &value, Buffer::Instance &out) PURE; 72 | }; 73 | 74 | template using EncoderPtr = std::unique_ptr>; 75 | 76 | /** 77 | * A protocol error. 78 | */ 79 | class ProtocolError : public EnvoyException { 80 | public: 81 | ProtocolError(const std::string &error) : EnvoyException(error) {} 82 | }; 83 | 84 | } // namespace Tcp 85 | } // namespace Envoy 86 | -------------------------------------------------------------------------------- /source/common/nats/streaming/client_pool.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/client_pool.h" 2 | 3 | #include "common/nats/codec_impl.h" 4 | #include "common/nats/streaming/client_impl.h" 5 | #include "common/tcp/conn_pool_impl.h" 6 | 7 | namespace Envoy { 8 | namespace Nats { 9 | namespace Streaming { 10 | 11 | ClientPool::ClientPool( 12 | const std::string &cluster_name, Upstream::ClusterManager &cm, 13 | Tcp::ConnPoolNats::ClientFactory &client_factory, 14 | ThreadLocal::SlotAllocator &tls, Runtime::RandomGenerator &random, 15 | const std::chrono::milliseconds &op_timeout) 16 | : cm_(cm), client_factory_(client_factory), slot_(tls.allocateSlot()), 17 | random_(random), op_timeout_(op_timeout) { 18 | slot_->set([this, cluster_name](Event::Dispatcher &dispatcher) 19 | -> ThreadLocal::ThreadLocalObjectSharedPtr { 20 | Tcp::ConnPoolNats::InstancePtr conn_pool( 21 | new Tcp::ConnPoolNats::InstanceImpl( 22 | cluster_name, cm_, client_factory_, dispatcher)); 23 | return std::make_shared(std::move(conn_pool), random_, 24 | dispatcher, op_timeout_); 25 | }); 26 | } 27 | 28 | PublishRequestPtr ClientPool::makeRequest(const std::string &subject, 29 | const std::string &cluster_id, 30 | const std::string &discover_prefix, 31 | std::string &&payload, 32 | PublishCallbacks &callbacks) { 33 | return slot_->getTyped().getClient().makeRequest( 34 | subject, cluster_id, discover_prefix, std::move(payload), callbacks); 35 | } 36 | 37 | ClientPool::ThreadLocalPool::ThreadLocalPool( 38 | Tcp::ConnPoolNats::InstancePtr &&conn_pool, 39 | Runtime::RandomGenerator &random, Event::Dispatcher &dispatcher, 40 | const std::chrono::milliseconds &op_timeout) 41 | : client_(std::move(conn_pool), random, dispatcher, op_timeout) {} 42 | 43 | Client &ClientPool::ThreadLocalPool::getClient() { return client_; } 44 | 45 | } // namespace Streaming 46 | } // namespace Nats 47 | } // namespace Envoy 48 | -------------------------------------------------------------------------------- /source/common/nats/nuid/nuid.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/nuid/nuid.h" 2 | 3 | namespace Envoy { 4 | namespace Nats { 5 | namespace Nuid { 6 | 7 | constexpr const char Nuid::DIGITS[]; 8 | constexpr uint8_t Nuid::BASE; 9 | constexpr uint8_t Nuid::PRE_LEN; 10 | constexpr uint8_t Nuid::SEQ_LEN; 11 | constexpr uint64_t Nuid::MAX_SEQ; 12 | constexpr uint64_t Nuid::MIN_INC; 13 | constexpr uint64_t Nuid::MAX_INC; 14 | constexpr uint8_t Nuid::TOTAL_LEN; 15 | 16 | Nuid::Nuid(Runtime::RandomGenerator &random_generator) 17 | : random_generator_(random_generator), seq_(int63_n(MAX_SEQ)), 18 | inc_(MIN_INC + int63_n(MAX_INC - MIN_INC)) { 19 | 20 | randomizePrefix(); 21 | } 22 | 23 | Nuid::Nuid(Runtime::RandomGenerator &random_generator, uint64_t seq) 24 | : random_generator_(random_generator), seq_(seq), 25 | inc_(MIN_INC + int63_n(MAX_INC - MIN_INC)) { 26 | ASSERT(seq < Nuid::MAX_SEQ); 27 | 28 | randomizePrefix(); 29 | } 30 | 31 | std::string Nuid::next() { 32 | // Increment and capture. 33 | seq_ += inc_; 34 | if (seq_ >= MAX_SEQ) { 35 | randomizePrefix(); 36 | resetSequential(); 37 | } 38 | 39 | // Convert the sequential into base62. 40 | char seq_str[SEQ_LEN]; 41 | convert(seq_, seq_str); 42 | 43 | // Instantiate a string. 44 | std::string result; 45 | result.reserve(TOTAL_LEN); 46 | 47 | // Copy prefix. 48 | result.append(pre_, PRE_LEN); 49 | 50 | // Copy sequential in base62. 51 | result.append(seq_str, SEQ_LEN); 52 | 53 | return result; 54 | } 55 | 56 | std::string Nuid::pre() { return std::string(pre_, PRE_LEN); } 57 | 58 | uint64_t Nuid::int63_n(int64_t n) { return random_generator_.random() % n; } 59 | 60 | void Nuid::resetSequential() { 61 | seq_ = int63_n(MAX_SEQ); 62 | inc_ = MIN_INC + int63_n(MAX_INC - MIN_INC); 63 | } 64 | 65 | void Nuid::randomizePrefix() { 66 | convert(int63_n(MAX_SEQ), pre_); 67 | 68 | // BASE ^ (PRE_LEN - SEQ_LEN) == 62 ^ (12 - 10) == 62 ^ 2 69 | constexpr uint64_t max = BASE * BASE; 70 | 71 | convert(int63_n(max), pre_ + SEQ_LEN); 72 | } 73 | 74 | template void Nuid::convert(uint64_t n, char *output) { 75 | static_assert(len <= 10, "Output length should not exceed 10 characters"); 76 | for (uint64_t i = 1; i <= len; ++i, n /= BASE) { 77 | output[len - i] = DIGITS[n % BASE]; 78 | } 79 | } 80 | 81 | } // namespace Nuid 82 | } // namespace Nats 83 | } // namespace Envoy 84 | -------------------------------------------------------------------------------- /test/common/nats/streaming/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_test", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_test( 12 | name = "client_impl_test", 13 | srcs = ["client_impl_test.cc"], 14 | repository = "@envoy", 15 | deps = [ 16 | "//source/common/nats/streaming:client_lib", 17 | "//test/mocks/nats:nats_mocks", 18 | "//test/mocks/nats/streaming:nats_streaming_mocks", 19 | "//test/mocks/tcp:tcp_mocks", 20 | "@envoy//source/common/common:assert_lib", 21 | "@envoy//test/mocks/event:event_mocks", 22 | "@envoy//test/mocks/runtime:runtime_mocks", 23 | "@envoy//test/test_common:utility_lib", 24 | ], 25 | ) 26 | 27 | envoy_cc_test( 28 | name = "connect_response_handler_test", 29 | srcs = ["connect_response_handler_test.cc"], 30 | repository = "@envoy", 31 | deps = [ 32 | "//source/common/nats/streaming:connect_response_handler_lib", 33 | "@envoy//source/common/common:assert_lib", 34 | "@envoy//test/test_common:utility_lib", 35 | ], 36 | ) 37 | 38 | envoy_cc_test( 39 | name = "heartbeat_handler_test", 40 | srcs = ["heartbeat_handler_test.cc"], 41 | repository = "@envoy", 42 | deps = [ 43 | "//source/common/nats/streaming:heartbeat_handler_lib", 44 | "//test/mocks/nats:nats_mocks", 45 | "@envoy//source/common/common:assert_lib", 46 | "@envoy//test/test_common:utility_lib", 47 | ], 48 | ) 49 | 50 | envoy_cc_test( 51 | name = "message_utility_test", 52 | srcs = ["message_utility_test.cc"], 53 | repository = "@envoy", 54 | deps = [ 55 | "//source/common/nats/streaming:message_utility_lib", 56 | "@envoy//source/common/common:assert_lib", 57 | "@envoy//test/test_common:utility_lib", 58 | ], 59 | ) 60 | 61 | envoy_cc_test( 62 | name = "pub_request_handler_test", 63 | srcs = ["pub_request_handler_test.cc"], 64 | repository = "@envoy", 65 | deps = [ 66 | "//source/common/nats:codec_lib", 67 | "//source/common/nats/streaming:pub_request_handler_lib", 68 | "//test/mocks/nats/streaming:nats_streaming_mocks", 69 | "@envoy//source/common/common:assert_lib", 70 | "@envoy//test/mocks/event:event_mocks", 71 | "@envoy//test/test_common:utility_lib", 72 | ], 73 | ) 74 | -------------------------------------------------------------------------------- /source/common/nats/codec_impl.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/codec_impl.h" 2 | 3 | #include "envoy/nats/codec.h" 4 | 5 | namespace Envoy { 6 | namespace Nats { 7 | 8 | std::string Message::toString() const { 9 | return fmt::format("\"{}\"", asString()); 10 | } 11 | 12 | std::string &Message::asString() { return string_; } 13 | 14 | const std::string &Message::asString() const { return string_; } 15 | 16 | void DecoderImpl::decode(Buffer::Instance &data) { 17 | uint64_t num_slices = data.getRawSlices(nullptr, 0); 18 | Buffer::RawSlice slices[num_slices]; 19 | data.getRawSlices(slices, num_slices); 20 | for (const Buffer::RawSlice &slice : slices) { 21 | parseSlice(slice); 22 | } 23 | 24 | data.drain(data.length()); 25 | } 26 | 27 | void DecoderImpl::parseSlice(const Buffer::RawSlice &slice) { 28 | const char *buffer = reinterpret_cast(slice.mem_); 29 | uint64_t remaining = slice.len_; 30 | 31 | while (remaining || state_ == State::ValueComplete) { 32 | ENVOY_LOG(trace, "parse slice: {} remaining", remaining); 33 | switch (state_) { 34 | case State::ValueRootStart: { 35 | ENVOY_LOG(trace, "parse slice: ValueRootStart"); 36 | pending_value_root_.reset(new Message()); 37 | state_ = State::SimpleString; 38 | break; 39 | } 40 | 41 | case State::SimpleString: { 42 | ENVOY_LOG(trace, "parse slice: SimpleString: {}", buffer[0]); 43 | if (buffer[0] == '\r') { 44 | state_ = State::LF; 45 | } else { 46 | pending_value_root_->asString().push_back(buffer[0]); 47 | } 48 | 49 | remaining--; 50 | buffer++; 51 | break; 52 | } 53 | 54 | case State::LF: { 55 | ENVOY_LOG(trace, "parse slice: LF"); 56 | if (buffer[0] != '\n') { 57 | // TODO(talnordan): Consider gracefully ignoring this error. 58 | throw ProtocolError("expected new line"); 59 | } 60 | 61 | remaining--; 62 | buffer++; 63 | state_ = State::ValueComplete; 64 | break; 65 | } 66 | 67 | case State::ValueComplete: { 68 | ENVOY_LOG(trace, "parse slice: ValueComplete"); 69 | callbacks_.onValue(std::move(pending_value_root_)); 70 | state_ = State::ValueRootStart; 71 | 72 | break; 73 | } 74 | } 75 | } 76 | } 77 | 78 | void EncoderImpl::encode(const Message &value, Buffer::Instance &out) { 79 | out.add(value.asString()); 80 | out.add("\r\n", 2); 81 | } 82 | 83 | } // namespace Nats 84 | } // namespace Envoy 85 | -------------------------------------------------------------------------------- /source/common/nats/streaming/message_utility.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/message_utility.h" 2 | 3 | namespace Envoy { 4 | namespace Nats { 5 | namespace Streaming { 6 | 7 | std::string MessageUtility::createConnectRequestMessage( 8 | const std::string &client_id, const std::string &heartbeat_inbox) { 9 | pb::ConnectRequest connect_request; 10 | connect_request.set_clientid(client_id); 11 | connect_request.set_heartbeatinbox(heartbeat_inbox); 12 | 13 | return serializeToString(connect_request); 14 | } 15 | 16 | std::string MessageUtility::createConnectResponseMessage( 17 | const std::string &pub_prefix, const std::string &sub_requests, 18 | const std::string &unsub_requests, const std::string &close_requests) { 19 | pb::ConnectResponse connect_response; 20 | connect_response.set_pubprefix(pub_prefix); 21 | connect_response.set_subrequests(sub_requests); 22 | connect_response.set_unsubrequests(unsub_requests); 23 | connect_response.set_closerequests(close_requests); 24 | 25 | return serializeToString(connect_response); 26 | } 27 | 28 | std::string MessageUtility::createPubMsgMessage(const std::string &client_id, 29 | const std::string &guid, 30 | const std::string &subject, 31 | const std::string &data) { 32 | pb::PubMsg pub_msg; 33 | pub_msg.set_clientid(client_id); 34 | pub_msg.set_guid(guid); 35 | pub_msg.set_subject(subject); 36 | pub_msg.set_data(data); 37 | 38 | return serializeToString(pub_msg); 39 | } 40 | 41 | std::string MessageUtility::createPubAckMessage(const std::string &guid, 42 | const std::string &error) { 43 | pb::PubAck pub_ack; 44 | pub_ack.set_guid(guid); 45 | pub_ack.set_error(error); 46 | 47 | return serializeToString(pub_ack); 48 | } 49 | 50 | absl::optional 51 | MessageUtility::parsePubAckMessage(const std::string &pub_ack_message) { 52 | pb::PubAck pub_ack; 53 | if (pub_ack.ParseFromString(pub_ack_message)) { 54 | return pub_ack; 55 | } 56 | 57 | return {}; 58 | } 59 | 60 | std::string 61 | MessageUtility::getPubPrefix(const std::string &connect_response_message) { 62 | pb::ConnectResponse connect_response; 63 | connect_response.ParseFromString(connect_response_message); 64 | return connect_response.pubprefix(); 65 | } 66 | 67 | } // namespace Streaming 68 | } // namespace Nats 69 | } // namespace Envoy 70 | -------------------------------------------------------------------------------- /include/envoy/nats/streaming/client.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | 7 | #include "common/buffer/buffer_impl.h" 8 | 9 | namespace Envoy { 10 | namespace Nats { 11 | namespace Streaming { 12 | 13 | /** 14 | * A handle to a publish request. 15 | */ 16 | class PublishRequest { 17 | public: 18 | virtual ~PublishRequest() {} 19 | 20 | /** 21 | * Cancel the request. No further request callbacks will be called. 22 | */ 23 | virtual void cancel() PURE; 24 | }; 25 | 26 | typedef std::unique_ptr PublishRequestPtr; 27 | 28 | /** 29 | * Publish request callbacks. 30 | */ 31 | class PublishCallbacks { 32 | public: 33 | virtual ~PublishCallbacks() {} 34 | 35 | /** 36 | * Called when the response is ready. 37 | */ 38 | virtual void onResponse() PURE; 39 | 40 | /** 41 | * Called when a network/protocol error occurs and there is no response. 42 | */ 43 | virtual void onFailure() PURE; 44 | 45 | /** 46 | * Called when a timeout occurs and there is no response. 47 | */ 48 | virtual void onTimeout() PURE; 49 | }; 50 | 51 | /** 52 | * A NATS streaming client that takes requests routed to NATS streaming and 53 | * publishes them using a backend connection pool. 54 | */ 55 | class Client { 56 | public: 57 | virtual ~Client() {} 58 | 59 | // TODO(talnordan): Add `ack_prefix`. 60 | /** 61 | * Makes a request. 62 | * @param subject supplies the subject. 63 | * @param cluster_id supplies the cluster-id with which the NATS Streaming 64 | * Server was started. 65 | * @param discover_prefix supplies the prefix subject used to connect to the 66 | * NATS Streaming server. 67 | * @param payload supplies the fully buffered payload as buffered by this 68 | * filter or previous ones in the filter chain. 69 | * @param callbacks supplies the request completion callbacks. 70 | * @return PublishRequestPtr a handle to the active request or nullptr if the 71 | * request could not be made for some reason. 72 | */ 73 | virtual PublishRequestPtr makeRequest(const std::string &subject, 74 | const std::string &cluster_id, 75 | const std::string &discover_prefix, 76 | std::string &&payload, 77 | PublishCallbacks &callbacks) PURE; 78 | }; 79 | 80 | typedef std::shared_ptr ClientPtr; 81 | 82 | } // namespace Streaming 83 | } // namespace Nats 84 | } // namespace Envoy 85 | -------------------------------------------------------------------------------- /test/mocks/tcp/mocks_nats.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/tcp/codec.h" 6 | #include "envoy/tcp/conn_pool_nats.h" 7 | 8 | #include "common/tcp/codec_impl.h" 9 | 10 | #include "gmock/gmock.h" 11 | 12 | namespace Envoy { 13 | namespace Tcp { 14 | 15 | using T = std::string; 16 | using TPtr = MessagePtr; 17 | 18 | class MockEncoder : public Encoder { 19 | public: 20 | MockEncoder(); 21 | ~MockEncoder(); 22 | 23 | MOCK_METHOD2(encode, void(const T &value, Buffer::Instance &out)); 24 | 25 | private: 26 | void encodeSimpleString(const T &string, Buffer::Instance &out); 27 | }; 28 | 29 | class MockDecoder : public Decoder { 30 | public: 31 | MockDecoder(); 32 | MockDecoder(DecoderCallbacks &callbacks); 33 | ~MockDecoder(); 34 | 35 | MOCK_METHOD1(decode, void(Buffer::Instance &data)); 36 | }; 37 | 38 | namespace ConnPoolNats { 39 | 40 | class MockClient : public Client { 41 | public: 42 | MockClient(); 43 | ~MockClient(); 44 | 45 | void raiseEvent(Network::ConnectionEvent event) { 46 | for (Network::ConnectionCallbacks *callbacks : callbacks_) { 47 | callbacks->onEvent(event); 48 | } 49 | } 50 | 51 | MOCK_METHOD1(addConnectionCallbacks, 52 | void(Network::ConnectionCallbacks &callbacks)); 53 | MOCK_METHOD0(close, void()); 54 | MOCK_METHOD1(makeRequest, void(const T &request)); 55 | MOCK_METHOD0(cancel, void()); 56 | 57 | std::list callbacks_; 58 | }; 59 | 60 | class MockClientFactory : public ClientFactory { 61 | public: 62 | MockClientFactory(); 63 | ~MockClientFactory(); 64 | 65 | // Tcp::ConnPoolNats::ClientFactory 66 | ClientPtr create(Upstream::HostConstSharedPtr host, Event::Dispatcher &, 67 | PoolCallbacks &, const Config &) override { 68 | return ClientPtr{create_(host)}; 69 | } 70 | 71 | MOCK_METHOD1(create_, Client *(Upstream::HostConstSharedPtr host)); 72 | }; 73 | 74 | class MockPoolCallbacks : public PoolCallbacks { 75 | public: 76 | MockPoolCallbacks(); 77 | ~MockPoolCallbacks(); 78 | 79 | void onResponse(TPtr &&value) override { onResponse_(value); } 80 | 81 | MOCK_METHOD1(onResponse_, void(TPtr &value)); 82 | MOCK_METHOD0(onClose, void()); 83 | }; 84 | 85 | class MockInstance : public Instance { 86 | public: 87 | MockInstance(); 88 | ~MockInstance(); 89 | 90 | MOCK_METHOD2(makeRequest, 91 | void(const std::string &hash_key, const T &request)); 92 | }; 93 | 94 | } // namespace ConnPoolNats 95 | 96 | } // namespace Tcp 97 | } // namespace Envoy 98 | -------------------------------------------------------------------------------- /source/common/nats/nuid/nuid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "envoy/runtime/runtime.h" 9 | 10 | #include "common/common/assert.h" 11 | 12 | namespace Envoy { 13 | namespace Nats { 14 | 15 | /** 16 | * A unique identifier generator that is high performance, very fast, and tries 17 | * to be entropy pool friendly. 18 | * 19 | * See https://github.com/nats-io/nuid/blob/master/nuid.go 20 | */ 21 | namespace Nuid { 22 | 23 | /** 24 | * NUID needs to be very fast to generate and truly unique, all while being 25 | * entropy pool friendly. We will use 12 bytes of crypto generated data (entropy 26 | * draining), and 10 bytes of sequential data that is started at a pseudo random 27 | * number and increments with a pseudo-random increment. Total is 22 bytes of 28 | * base 62 ascii text :) 29 | * */ 30 | class Nuid { 31 | public: 32 | static constexpr const char DIGITS[] = 33 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 34 | static constexpr uint8_t BASE = 62; 35 | static constexpr uint8_t PRE_LEN = 12; 36 | static constexpr uint8_t SEQ_LEN = 10; 37 | static constexpr uint64_t MAX_SEQ = 38 | 839299365868340224L; // BASE ^ SEQ_LEN == 62^10 39 | static constexpr uint64_t MIN_INC = 33; 40 | static constexpr uint64_t MAX_INC = 333; 41 | static constexpr uint8_t TOTAL_LEN = PRE_LEN + SEQ_LEN; 42 | 43 | /** 44 | * Generate a new NUID and properly initialize the prefix, sequential start, 45 | * and sequential increment. 46 | */ 47 | Nuid(Runtime::RandomGenerator &random_generator); 48 | Nuid(Runtime::RandomGenerator &random_generator, uint64_t seq); 49 | 50 | /** 51 | * Generate the next NUID string. 52 | */ 53 | std::string next(); 54 | 55 | std::string pre(); 56 | 57 | private: 58 | /** 59 | * Returns, as an `uint64_t`, a non-negative pseudo-random number in [0,n). 60 | * n is assumed to be > 0. 61 | * 62 | * See: https://golang.org/pkg/math/rand/#Int63n 63 | */ 64 | inline uint64_t int63_n(int64_t n); 65 | 66 | /** 67 | * Resets the sequential portion of the NUID. 68 | */ 69 | inline void resetSequential(); 70 | /** 71 | * Generate a new pseudo-random prefix. 72 | */ 73 | inline void randomizePrefix(); 74 | 75 | template inline void convert(uint64_t n, char *output); 76 | 77 | Runtime::RandomGenerator &random_generator_; 78 | char pre_[PRE_LEN]; 79 | uint64_t seq_; 80 | uint64_t inc_; 81 | }; 82 | 83 | } // namespace Nuid 84 | } // namespace Nats 85 | } // namespace Envoy 86 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_binary", 6 | "envoy_cc_library", 7 | "envoy_cc_test", 8 | "envoy_package", 9 | ) 10 | 11 | envoy_package() 12 | 13 | load("@envoy_api//bazel:api_build_system.bzl", "api_proto_library") 14 | 15 | envoy_cc_library( 16 | name = "nats_streaming_filter_config", 17 | hdrs = [ 18 | "nats_streaming_filter_config.h", 19 | ], 20 | repository = "@envoy", 21 | deps = [ 22 | "//:nats_streaming_filter_proto_cc", 23 | "//source/common/config:nats_streaming_well_known_names_lib", 24 | ], 25 | ) 26 | 27 | envoy_cc_library( 28 | name = "subject_retriever_lib", 29 | srcs = [ 30 | "metadata_subject_retriever.cc", 31 | ], 32 | hdrs = [ 33 | "metadata_subject_retriever.h", 34 | "subject_retriever.h", 35 | ], 36 | repository = "@envoy", 37 | deps = [ 38 | "//source/common/config:nats_streaming_well_known_names_lib", 39 | "@envoy//source/common/config:metadata_lib", 40 | "@solo_envoy_common//source/common/config:solo_metadata_lib", 41 | "@solo_envoy_common//source/common/http:functional_stream_decoder_base_lib", 42 | "@solo_envoy_common//source/common/http:solo_filter_utility_lib", 43 | ], 44 | ) 45 | 46 | envoy_cc_library( 47 | name = "nats_streaming_filter_lib", 48 | srcs = [ 49 | "nats_streaming_filter.cc", 50 | ], 51 | hdrs = [ 52 | "nats_streaming_filter.h", 53 | ], 54 | repository = "@envoy", 55 | deps = [ 56 | ":nats_streaming_filter_config", 57 | ":subject_retriever_lib", 58 | "//:payload_proto_cc", 59 | "//include/envoy/nats/streaming:client_interface", 60 | "@envoy//source/common/grpc:common_lib", 61 | "@solo_envoy_common//source/common/http:functional_stream_decoder_base_lib", 62 | "@solo_envoy_common//source/common/http:solo_filter_utility_lib", 63 | ], 64 | ) 65 | 66 | envoy_cc_library( 67 | name = "nats_streaming_filter_config_lib", 68 | srcs = ["nats_streaming_filter_config_factory.cc"], 69 | hdrs = ["nats_streaming_filter_config_factory.h"], 70 | repository = "@envoy", 71 | deps = [ 72 | ":nats_streaming_filter_lib", 73 | "//source/common/config:nats_streaming_well_known_names_lib", 74 | "//source/common/nats:codec_lib", 75 | "//source/common/nats/streaming:client_pool_lib", 76 | "//source/common/tcp:conn_pool_lib", 77 | "@envoy//source/extensions/filters/http/common:factory_base_lib", 78 | ], 79 | ) 80 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.h" 2 | 3 | #include "envoy/registry/registry.h" 4 | 5 | #include "common/nats/codec_impl.h" 6 | #include "common/nats/streaming/client_pool.h" 7 | #include "common/tcp/conn_pool_impl.h" 8 | 9 | #include "extensions/filters/http/nats/streaming/metadata_subject_retriever.h" 10 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter.h" 11 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter_config.h" 12 | 13 | namespace Envoy { 14 | namespace Extensions { 15 | namespace HttpFilters { 16 | namespace Nats { 17 | namespace Streaming { 18 | 19 | typedef Http::FunctionalFilterMixin 20 | MixedNatsStreamingFilter; 21 | 22 | Http::FilterFactoryCb 23 | NatsStreamingFilterConfigFactory::createFilterFactoryFromProtoTyped( 24 | const envoy::api::v2::filter::http::NatsStreaming &proto_config, 25 | const std::string &, Server::Configuration::FactoryContext &context) { 26 | 27 | NatsStreamingFilterConfigSharedPtr config = 28 | std::make_shared( 29 | NatsStreamingFilterConfig(proto_config, context.clusterManager())); 30 | 31 | SubjectRetrieverSharedPtr subjectRetriever = 32 | std::make_shared(); 33 | 34 | Tcp::ConnPoolNats::ClientFactory &client_factory = 35 | Tcp::ConnPoolNats::ClientFactoryImpl::instance_; 38 | 39 | Envoy::Nats::Streaming::ClientPtr nats_streaming_client = 40 | std::make_shared( 41 | config->cluster(), context.clusterManager(), client_factory, 42 | context.threadLocal(), context.random(), config->opTimeout()); 43 | 44 | return [&context, config, subjectRetriever, nats_streaming_client]( 45 | Envoy::Http::FilterChainFactoryCallbacks &callbacks) -> void { 46 | auto filter = new MixedNatsStreamingFilter( 47 | context, Config::NatsStreamingMetadataFilters::get().NATS_STREAMING, 48 | config, subjectRetriever, nats_streaming_client); 49 | callbacks.addStreamDecoderFilter( 50 | Http::StreamDecoderFilterSharedPtr{filter}); 51 | }; 52 | } 53 | 54 | /** 55 | * Static registration for the Nats Streaming filter. @see RegisterFactory. 56 | */ 57 | static Envoy::Registry::RegisterFactory< 58 | NatsStreamingFilterConfigFactory, 59 | Envoy::Server::Configuration::NamedHttpFilterConfigFactory> 60 | register_; 61 | 62 | } // namespace Streaming 63 | } // namespace Nats 64 | } // namespace HttpFilters 65 | } // namespace Extensions 66 | } // namespace Envoy 67 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/nats_streaming_filter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "envoy/nats/streaming/client.h" 4 | 5 | #include "common/http/functional_stream_decoder_base.h" 6 | 7 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter_config.h" 8 | #include "extensions/filters/http/nats/streaming/subject_retriever.h" 9 | 10 | #include "nats_streaming_filter.pb.h" 11 | #include "payload.pb.h" 12 | 13 | namespace Envoy { 14 | namespace Extensions { 15 | namespace HttpFilters { 16 | namespace Nats { 17 | namespace Streaming { 18 | 19 | using Upstream::ClusterManager; 20 | 21 | class NatsStreamingFilter : public Http::StreamDecoderFilter, 22 | public Http::FunctionalFilter, 23 | public Envoy::Nats::Streaming::PublishCallbacks { 24 | public: 25 | NatsStreamingFilter(NatsStreamingFilterConfigSharedPtr config, 26 | SubjectRetrieverSharedPtr retreiver, 27 | Envoy::Nats::Streaming::ClientPtr nats_streaming_client); 28 | ~NatsStreamingFilter(); 29 | 30 | // Http::StreamFilterBase 31 | void onDestroy() override; 32 | 33 | // Http::StreamDecoderFilter 34 | Http::FilterHeadersStatus decodeHeaders(Http::HeaderMap &, bool) override; 35 | Http::FilterDataStatus decodeData(Buffer::Instance &, bool) override; 36 | Http::FilterTrailersStatus decodeTrailers(Http::HeaderMap &) override; 37 | 38 | void setDecoderFilterCallbacks( 39 | Http::StreamDecoderFilterCallbacks &decoder_callbacks) override { 40 | decoder_callbacks_ = &decoder_callbacks; 41 | auto decoder_limit = decoder_callbacks.decoderBufferLimit(); 42 | if (decoder_limit > 0) { 43 | decoder_buffer_limit_ = decoder_limit; 44 | } 45 | } 46 | 47 | bool retrieveFunction(const Http::MetadataAccessor &meta_accessor) override; 48 | 49 | // Nats::Streaming::PublishCallbacks 50 | virtual void onResponse() override; 51 | virtual void onFailure() override; 52 | virtual void onTimeout() override; 53 | 54 | private: 55 | void retrieveSubject(const Http::MetadataAccessor &meta_accessor); 56 | 57 | inline bool isActive() { return optional_subject_.has_value(); } 58 | 59 | void relayToNatsStreaming(); 60 | 61 | inline void onCompletion(Http::Code response_code, 62 | const std::string &body_text); 63 | 64 | inline void onCompletion(Http::Code response_code, 65 | const std::string &body_text, 66 | RequestInfo::ResponseFlag response_flag); 67 | 68 | const NatsStreamingFilterConfigSharedPtr config_; 69 | SubjectRetrieverSharedPtr subject_retriever_; 70 | Envoy::Nats::Streaming::ClientPtr nats_streaming_client_; 71 | absl::optional optional_subject_; 72 | Http::StreamDecoderFilterCallbacks *decoder_callbacks_{}; 73 | absl::optional decoder_buffer_limit_{}; 74 | pb::Payload payload_; 75 | Buffer::OwnedImpl body_{}; 76 | Envoy::Nats::Streaming::PublishRequestPtr in_flight_request_{}; 77 | }; 78 | 79 | } // namespace Streaming 80 | } // namespace Nats 81 | } // namespace HttpFilters 82 | } // namespace Extensions 83 | } // namespace Envoy 84 | -------------------------------------------------------------------------------- /source/common/nats/streaming/BUILD: -------------------------------------------------------------------------------- 1 | licenses(["notice"]) # Apache 2 2 | 3 | load( 4 | "@envoy//bazel:envoy_build_system.bzl", 5 | "envoy_cc_library", 6 | "envoy_package", 7 | ) 8 | 9 | envoy_package() 10 | 11 | envoy_cc_library( 12 | name = "client_lib", 13 | srcs = ["client_impl.cc"], 14 | hdrs = ["client_impl.h"], 15 | repository = "@envoy", 16 | deps = [ 17 | "//include/envoy/nats:codec_interface", 18 | "//include/envoy/nats/streaming:client_interface", 19 | "//include/envoy/tcp:conn_pool_interface", 20 | "//source/common/nats:message_builder_lib", 21 | "//source/common/nats:subject_utility_lib", 22 | "//source/common/nats:token_generator_lib", 23 | "//source/common/nats/streaming:connect_response_handler_lib", 24 | "//source/common/nats/streaming:heartbeat_handler_lib", 25 | "//source/common/nats/streaming:message_utility_lib", 26 | "//source/common/nats/streaming:pub_request_handler_lib", 27 | "@solo_envoy_common//source/common/buffer:buffer_utility_lib", 28 | ], 29 | ) 30 | 31 | envoy_cc_library( 32 | name = "client_pool_lib", 33 | srcs = ["client_pool.cc"], 34 | hdrs = ["client_pool.h"], 35 | repository = "@envoy", 36 | deps = [ 37 | "//include/envoy/nats:codec_interface", 38 | "//include/envoy/nats/streaming:client_interface", 39 | "//include/envoy/tcp:conn_pool_interface", 40 | "//source/common/nats:codec_lib", 41 | "//source/common/nats/streaming:client_lib", 42 | "//source/common/tcp:conn_pool_lib", 43 | ], 44 | ) 45 | 46 | envoy_cc_library( 47 | name = "connect_response_handler_lib", 48 | srcs = ["connect_response_handler.cc"], 49 | hdrs = ["connect_response_handler.h"], 50 | repository = "@envoy", 51 | deps = [ 52 | "//include/envoy/nats/streaming:inbox_handler_interface", 53 | "//source/common/nats/streaming:message_utility_lib", 54 | ], 55 | ) 56 | 57 | envoy_cc_library( 58 | name = "heartbeat_handler_lib", 59 | srcs = ["heartbeat_handler.cc"], 60 | hdrs = ["heartbeat_handler.h"], 61 | external_deps = ["abseil_optional"], 62 | repository = "@envoy", 63 | deps = [ 64 | "//include/envoy/nats/streaming:inbox_handler_interface", 65 | "//source/common/nats:message_builder_lib", 66 | ], 67 | ) 68 | 69 | envoy_cc_library( 70 | name = "message_utility_lib", 71 | srcs = ["message_utility.cc"], 72 | hdrs = ["message_utility.h"], 73 | external_deps = ["abseil_optional"], 74 | repository = "@envoy", 75 | deps = [ 76 | "//:protocol_proto_cc", 77 | ], 78 | ) 79 | 80 | envoy_cc_library( 81 | name = "pub_request_handler_lib", 82 | srcs = ["pub_request_handler.cc"], 83 | hdrs = ["pub_request_handler.h"], 84 | repository = "@envoy", 85 | deps = [ 86 | "//include/envoy/nats/streaming:client_interface", 87 | "//include/envoy/nats/streaming:inbox_handler_interface", 88 | "//source/common/nats/streaming:message_utility_lib", 89 | "@envoy//include/envoy/event:timer_interface", 90 | "@envoy//source/common/buffer:buffer_lib", 91 | ], 92 | ) 93 | -------------------------------------------------------------------------------- /source/common/nats/streaming/pub_request_handler.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/pub_request_handler.h" 2 | 3 | #include "common/common/assert.h" 4 | #include "common/nats/streaming/message_utility.h" 5 | 6 | namespace Envoy { 7 | namespace Nats { 8 | namespace Streaming { 9 | 10 | void PubRequestHandler::onMessage(const absl::optional &reply_to, 11 | const std::string &payload, 12 | InboxCallbacks &inbox_callbacks, 13 | PublishCallbacks &publish_callbacks) { 14 | if (reply_to.has_value()) { 15 | inbox_callbacks.onFailure("incoming PubAck with non-empty reply subject"); 16 | return; 17 | } 18 | 19 | if (payload.empty()) { 20 | inbox_callbacks.onFailure("incoming PubAck without payload"); 21 | return; 22 | } 23 | 24 | auto &&maybe_pub_ack = MessageUtility::parsePubAckMessage(payload); 25 | if (maybe_pub_ack.has_value() && maybe_pub_ack.value().error().empty()) { 26 | publish_callbacks.onResponse(); 27 | } else { 28 | publish_callbacks.onFailure(); 29 | } 30 | } 31 | 32 | void PubRequestHandler::onMessage( 33 | const std::string &inbox, const absl::optional &reply_to, 34 | const std::string &payload, InboxCallbacks &inbox_callbacks, 35 | std::map &request_per_inbox) { 36 | // Find the inbox in the map. 37 | auto it = request_per_inbox.find(inbox); 38 | 39 | // Gracefully ignore a missing inbox. 40 | if (it == request_per_inbox.end()) { 41 | // TODO(talnordan): consider logging the message and/or updating stats. 42 | return; 43 | } 44 | 45 | // Handle the message using the publish callbacks associated with the inbox. 46 | PubRequest &request = it->second; 47 | PublishCallbacks &publish_callbacks = request.callbacks(); 48 | onMessage(reply_to, payload, inbox_callbacks, publish_callbacks); 49 | 50 | // Remove the inbox from the map. 51 | eraseRequest(request_per_inbox, it); 52 | } 53 | 54 | void PubRequestHandler::onTimeout( 55 | const std::string &inbox, 56 | std::map &request_per_inbox) { 57 | // Find the inbox in the map. 58 | auto it = request_per_inbox.find(inbox); 59 | 60 | // Gracefully ignore a missing inbox. 61 | if (it == request_per_inbox.end()) { 62 | return; 63 | } 64 | 65 | // Notify of a timeout using the publish callbacks associated with the inbox. 66 | PubRequest &request = it->second; 67 | request.callbacks().onTimeout(); 68 | 69 | // Remove the inbox from the map. 70 | eraseRequest(request_per_inbox, it); 71 | } 72 | 73 | void PubRequestHandler::onCancel( 74 | const std::string &inbox, 75 | std::map &request_per_inbox) { 76 | // Find the inbox in the map. 77 | auto it = request_per_inbox.find(inbox); 78 | 79 | // Gracefully ignore a missing inbox. 80 | if (it == request_per_inbox.end()) { 81 | return; 82 | } 83 | 84 | // Remove the inbox from the map. 85 | eraseRequest(request_per_inbox, it); 86 | } 87 | 88 | void PubRequestHandler::eraseRequest( 89 | std::map &request_per_inbox, 90 | std::map::iterator position) { 91 | PubRequest &request = position->second; 92 | request.onDestroy(); 93 | request_per_inbox.erase(position); 94 | } 95 | 96 | } // namespace Streaming 97 | } // namespace Nats 98 | } // namespace Envoy 99 | -------------------------------------------------------------------------------- /include/envoy/tcp/conn_pool_nats.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/common/pure.h" 6 | #include "envoy/event/deferred_deletable.h" 7 | #include "envoy/network/connection.h" 8 | #include "envoy/tcp/codec.h" 9 | #include "envoy/upstream/upstream.h" 10 | 11 | namespace Envoy { 12 | namespace Tcp { 13 | namespace ConnPoolNats { 14 | 15 | /** 16 | * Outbound request callbacks. 17 | */ 18 | template class PoolCallbacks { 19 | public: 20 | virtual ~PoolCallbacks() {} 21 | 22 | /** 23 | * Called when a pipelined response is received. 24 | * @param value supplies the response which is now owned by the callee. 25 | */ 26 | virtual void onResponse(MessagePtr &&value) PURE; 27 | 28 | /** 29 | * Called when close event occurs on a connection. 30 | */ 31 | virtual void onClose() PURE; 32 | }; 33 | 34 | /** 35 | * A single client connection. 36 | */ 37 | template class Client : public Event::DeferredDeletable { 38 | public: 39 | virtual ~Client() {} 40 | 41 | /** 42 | * Adds network connection callbacks to the underlying network connection. 43 | */ 44 | virtual void 45 | addConnectionCallbacks(Network::ConnectionCallbacks &callbacks) PURE; 46 | 47 | /** 48 | * Closes the underlying network connection. 49 | */ 50 | virtual void close() PURE; 51 | 52 | /** 53 | * Make a pipelined request to the remote server. 54 | * @param request supplies the request to make. 55 | */ 56 | virtual void makeRequest(const T &request) PURE; 57 | 58 | /** 59 | * Cancel all requests. No further request callbacks will be called. 60 | */ 61 | virtual void cancel() PURE; 62 | }; 63 | 64 | template using ClientPtr = std::unique_ptr>; 65 | 66 | /** 67 | * Configuration for a connection pool. 68 | */ 69 | class Config { 70 | public: 71 | virtual ~Config() {} 72 | 73 | /** 74 | * @return bool disable outlier events even if the cluster has it enabled. 75 | * This is used by the healthchecker's connection pool to avoid double 76 | * counting active healthcheck operations as passive healthcheck operations. 77 | */ 78 | virtual bool disableOutlierEvents() const PURE; 79 | }; 80 | 81 | /** 82 | * A factory for individual client connections. 83 | */ 84 | template class ClientFactory { 85 | public: 86 | virtual ~ClientFactory() {} 87 | 88 | /** 89 | * Create a client given an upstream host. 90 | * @param host supplies the upstream host. 91 | * @param dispatcher supplies the owning thread's dispatcher. 92 | * @param callbacks supplies the pool callbacks. 93 | * @param config supplies the connection pool configuration. 94 | * @return ClientPtr a new connection pool client. 95 | */ 96 | virtual ClientPtr create(Upstream::HostConstSharedPtr host, 97 | Event::Dispatcher &dispatcher, 98 | PoolCallbacks &callbacks, 99 | const Config &config) PURE; 100 | }; 101 | 102 | /** 103 | * A connection pool. Wraps M connections to N upstream hosts, consistent 104 | * hashing, pipelining, failure handling, etc. 105 | */ 106 | template class Instance { 107 | public: 108 | virtual ~Instance() {} 109 | 110 | /** 111 | * Sets the pool callbacks. 112 | */ 113 | virtual void setPoolCallbacks(PoolCallbacks &callbacks) PURE; 114 | 115 | /** 116 | * Makes a request. 117 | * @param hash_key supplies the key to use for consistent hashing. 118 | * @param request supplies the request to make. 119 | */ 120 | virtual void makeRequest(const std::string &hash_key, const T &request) PURE; 121 | }; 122 | 123 | template using InstancePtr = std::unique_ptr>; 124 | 125 | } // namespace ConnPoolNats 126 | } // namespace Tcp 127 | } // namespace Envoy 128 | -------------------------------------------------------------------------------- /test/common/nats/codec_impl_test.cc: -------------------------------------------------------------------------------- 1 | #include "envoy/nats/codec.h" 2 | 3 | #include "common/buffer/buffer_impl.h" 4 | #include "common/common/assert.h" 5 | #include "common/nats/codec_impl.h" 6 | 7 | #include "test/mocks/nats/mocks.h" 8 | #include "test/test_common/printers.h" 9 | 10 | #include "gtest/gtest.h" 11 | 12 | namespace Envoy { 13 | namespace Nats { 14 | 15 | class NatsEncoderDecoderImplTest : public testing::Test, 16 | public Tcp::DecoderCallbacks { 17 | public: 18 | NatsEncoderDecoderImplTest() : decoder_(*this) {} 19 | 20 | // Tcp::DecoderCallbacks 21 | void onValue(MessagePtr &&value) override { 22 | decoded_values_.emplace_back(std::move(value)); 23 | } 24 | 25 | EncoderImpl encoder_; 26 | DecoderImpl decoder_; 27 | Buffer::OwnedImpl buffer_; 28 | std::vector decoded_values_; 29 | }; 30 | 31 | TEST_F(NatsEncoderDecoderImplTest, Empty) { 32 | Message value; 33 | EXPECT_EQ("\"\"", value.toString()); 34 | encoder_.encode(value, buffer_); 35 | EXPECT_EQ("\r\n", buffer_.toString()); 36 | decoder_.decode(buffer_); 37 | EXPECT_EQ(value, *decoded_values_[0]); 38 | EXPECT_EQ(0UL, buffer_.length()); 39 | } 40 | 41 | TEST_F(NatsEncoderDecoderImplTest, SimpleString) { 42 | Message value; 43 | value.asString() = "simple string"; 44 | EXPECT_EQ("\"simple string\"", value.toString()); 45 | encoder_.encode(value, buffer_); 46 | EXPECT_EQ("simple string\r\n", buffer_.toString()); 47 | decoder_.decode(buffer_); 48 | EXPECT_EQ(value, *decoded_values_[0]); 49 | EXPECT_EQ(0UL, buffer_.length()); 50 | } 51 | 52 | TEST_F(NatsEncoderDecoderImplTest, MultipleSimpleStrings) { 53 | Message value1; 54 | value1.asString() = "simple string 1"; 55 | encoder_.encode(value1, buffer_); 56 | 57 | Message value2; 58 | value2.asString() = "simple string 2"; 59 | encoder_.encode(value2, buffer_); 60 | 61 | EXPECT_EQ("simple string 1\r\nsimple string 2\r\n", buffer_.toString()); 62 | 63 | decoder_.decode(buffer_); 64 | EXPECT_EQ(value1, *decoded_values_[0]); 65 | EXPECT_EQ(value2, *decoded_values_[1]); 66 | EXPECT_EQ(0UL, buffer_.length()); 67 | } 68 | 69 | TEST_F(NatsEncoderDecoderImplTest, MultipleSimpleStringsMultipleDecode) { 70 | Message value1; 71 | value1.asString() = "simple string 1"; 72 | encoder_.encode(value1, buffer_); 73 | EXPECT_EQ("simple string 1\r\n", buffer_.toString()); 74 | decoder_.decode(buffer_); 75 | EXPECT_EQ(value1, *decoded_values_[0]); 76 | EXPECT_EQ(0UL, buffer_.length()); 77 | 78 | Message value2; 79 | value2.asString() = "simple string 2"; 80 | encoder_.encode(value2, buffer_); 81 | EXPECT_EQ("simple string 2\r\n", buffer_.toString()); 82 | decoder_.decode(buffer_); 83 | EXPECT_EQ(value2, *decoded_values_[1]); 84 | EXPECT_EQ(0UL, buffer_.length()); 85 | } 86 | 87 | TEST_F(NatsEncoderDecoderImplTest, MultipleSimpleStringsFragmentedDecode) { 88 | Message value1; 89 | value1.asString() = "simple string 1"; 90 | 91 | Message value2; 92 | value2.asString() = "simple string 2"; 93 | 94 | Message value3; 95 | value3.asString() = "simple string 3"; 96 | 97 | buffer_.add("simple string 1\r\nsimple s"); 98 | decoder_.decode(buffer_); 99 | EXPECT_EQ(1, decoded_values_.size()); 100 | EXPECT_EQ(value1, *decoded_values_[0]); 101 | EXPECT_EQ(0UL, buffer_.length()); 102 | 103 | buffer_.add("tring 2\r\nsimple string 3\r\n"); 104 | decoder_.decode(buffer_); 105 | EXPECT_EQ(3, decoded_values_.size()); 106 | EXPECT_EQ(value2, *decoded_values_[1]); 107 | EXPECT_EQ(value3, *decoded_values_[2]); 108 | EXPECT_EQ(0UL, buffer_.length()); 109 | } 110 | 111 | TEST_F(NatsEncoderDecoderImplTest, InvalidSimpleStringExpectLF) { 112 | buffer_.add(":-123\ra"); 113 | EXPECT_THROW(decoder_.decode(buffer_), ProtocolError); 114 | } 115 | 116 | } // namespace Nats 117 | } // namespace Envoy 118 | -------------------------------------------------------------------------------- /test/common/nats/streaming/message_utility_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/nats/streaming/message_utility.h" 3 | 4 | #include "test/test_common/utility.h" 5 | 6 | #include "protocol.pb.h" 7 | 8 | namespace Envoy { 9 | namespace Nats { 10 | namespace Streaming { 11 | 12 | class NatsStreamingMessageUtilityTest : public testing::Test { 13 | public: 14 | NatsStreamingMessageUtilityTest() {} 15 | }; 16 | 17 | TEST_F(NatsStreamingMessageUtilityTest, ConnectRequestMessage) { 18 | const auto message = MessageUtility::createConnectRequestMessage( 19 | "client_id", "heartbeat_inbox"); 20 | 21 | pb::ConnectRequest connect_request; 22 | connect_request.ParseFromString(message); 23 | 24 | EXPECT_EQ("client_id", connect_request.clientid()); 25 | EXPECT_EQ("heartbeat_inbox", connect_request.heartbeatinbox()); 26 | } 27 | 28 | TEST_F(NatsStreamingMessageUtilityTest, ConnectResponseMessage) { 29 | const auto message = MessageUtility::createConnectResponseMessage( 30 | "pub_prefix", "sub_requests", "unsub_requests", "close_requests"); 31 | 32 | pb::ConnectResponse connect_response; 33 | connect_response.ParseFromString(message); 34 | 35 | EXPECT_EQ("pub_prefix", connect_response.pubprefix()); 36 | EXPECT_EQ("sub_requests", connect_response.subrequests()); 37 | EXPECT_EQ("unsub_requests", connect_response.unsubrequests()); 38 | EXPECT_EQ("close_requests", connect_response.closerequests()); 39 | } 40 | 41 | TEST_F(NatsStreamingMessageUtilityTest, PubMsgMessage) { 42 | const std::string client_id{"client1"}; 43 | const std::string uuid{"13581321-dead-beef-b77c-24f6818b6043"}; 44 | const std::string subject{"subject1"}; 45 | const std::string data{"\"d\ra\0t\t \na\v"}; 46 | const auto message = 47 | MessageUtility::createPubMsgMessage(client_id, uuid, subject, data); 48 | 49 | pb::PubMsg pub_msg; 50 | pub_msg.ParseFromString(message); 51 | 52 | EXPECT_EQ(client_id, pub_msg.clientid()); 53 | EXPECT_EQ(uuid, pub_msg.guid()); 54 | EXPECT_EQ(subject, pub_msg.subject()); 55 | EXPECT_EQ(data, pub_msg.data()); 56 | } 57 | 58 | TEST_F(NatsStreamingMessageUtilityTest, PubAckMessage) { 59 | const std::string uuid{"13581321-dead-beef-b77c-24f6818b6043"}; 60 | const std::string error{"E\"R\rR\0O\t \nR\v"}; 61 | const auto message = MessageUtility::createPubAckMessage(uuid, error); 62 | 63 | pb::PubAck pub_ack; 64 | pub_ack.ParseFromString(message); 65 | 66 | EXPECT_EQ(uuid, pub_ack.guid()); 67 | EXPECT_EQ(error, pub_ack.error()); 68 | } 69 | 70 | TEST_F(NatsStreamingMessageUtilityTest, GetPubPrefix) { 71 | const auto message = MessageUtility::createConnectResponseMessage( 72 | "pub_prefix", "sub_requests", "unsub_requests", "close_requests"); 73 | 74 | const auto pub_prefix = MessageUtility::getPubPrefix(message); 75 | 76 | EXPECT_EQ("pub_prefix", pub_prefix); 77 | } 78 | 79 | TEST_F(NatsStreamingMessageUtilityTest, ParsePubAckMessage) { 80 | const std::string uuid{"13581321-dead-beef-b77c-24f6818b6043"}; 81 | 82 | // No error. 83 | const std::string error{""}; 84 | const auto message = MessageUtility::createPubAckMessage(uuid, error); 85 | 86 | auto &&maybe_result = MessageUtility::parsePubAckMessage(message); 87 | 88 | EXPECT_TRUE(maybe_result.has_value()); 89 | EXPECT_EQ(uuid, maybe_result.value().guid()); 90 | EXPECT_TRUE(maybe_result.value().error().empty()); 91 | } 92 | 93 | TEST_F(NatsStreamingMessageUtilityTest, ParsePubAckMessageWithError) { 94 | const std::string uuid{"13581321-dead-beef-b77c-24f6818b6043"}; 95 | const std::string error{"Error!"}; 96 | const auto message = MessageUtility::createPubAckMessage(uuid, error); 97 | 98 | auto &&maybe_result = MessageUtility::parsePubAckMessage(message); 99 | 100 | EXPECT_TRUE(maybe_result.has_value()); 101 | EXPECT_EQ(uuid, maybe_result.value().guid()); 102 | EXPECT_EQ(error, maybe_result.value().error()); 103 | } 104 | 105 | TEST_F(NatsStreamingMessageUtilityTest, ParseInvalidPubAckMessage) { 106 | const std::string invalid_message{"This is not a PubAck message."}; 107 | auto &&maybe_result{MessageUtility::parsePubAckMessage(invalid_message)}; 108 | EXPECT_FALSE(maybe_result.has_value()); 109 | } 110 | 111 | } // namespace Streaming 112 | } // namespace Nats 113 | } // namespace Envoy 114 | -------------------------------------------------------------------------------- /protocol.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package pb; 3 | 4 | // import "github.com/gogo/protobuf/gogoproto/gogo.proto"; 5 | 6 | // option (gogoproto.marshaler_all) = true; 7 | // option (gogoproto.sizer_all) = true; 8 | // option (gogoproto.unmarshaler_all) = true; 9 | // option (gogoproto.goproto_getters_all) = false; 10 | 11 | // How messages are delivered to the STAN cluster 12 | message PubMsg { 13 | string clientID = 1; // ClientID 14 | string guid = 2; // guid 15 | string subject = 3; // subject 16 | string reply = 4; // optional reply 17 | bytes data = 5; // payload 18 | 19 | bytes sha256 = 10; // optional sha256 of data 20 | } 21 | 22 | // Used to ACK to publishers 23 | message PubAck { 24 | string guid = 1; // guid 25 | string error = 2; // err string, empty/omitted if no error 26 | } 27 | 28 | // Msg struct. Sequence is assigned for global ordering by 29 | // the cluster after the publisher has been acknowledged. 30 | message MsgProto { 31 | uint64 sequence = 32 | 1; // globally ordered sequence number for the subject's channel 33 | string subject = 2; // subject 34 | string reply = 3; // optional reply 35 | bytes data = 4; // payload 36 | int64 timestamp = 5; // received timestamp 37 | bool redelivered = 6; // Flag specifying if the message is being redelivered 38 | 39 | uint32 CRC32 = 10; // optional IEEE CRC32 40 | } 41 | 42 | // Ack will deliver an ack for a delivered msg. 43 | message Ack { 44 | string subject = 1; // Subject 45 | uint64 sequence = 2; // Sequence to acknowledge 46 | } 47 | 48 | // Connection Request 49 | message ConnectRequest { 50 | string clientID = 1; // Client name/identifier. 51 | string heartbeatInbox = 2; // Inbox for server initiated heartbeats. 52 | } 53 | 54 | // Response to a client connect 55 | message ConnectResponse { 56 | string pubPrefix = 1; // Prefix to use when publishing to this STAN cluster 57 | string subRequests = 2; // Subject to use for subscription requests 58 | string unsubRequests = 3; // Subject to use for unsubscribe requests 59 | string closeRequests = 4; // Subject for closing the stan connection 60 | string error = 5; // err string, empty/omitted if no error 61 | string subCloseRequests = 6; // Subject to use for subscription close requests 62 | 63 | string publicKey = 100; // Possibly used to sign acks, etc. 64 | } 65 | 66 | // Enum for start position type. 67 | enum StartPosition { 68 | NewOnly = 0; 69 | LastReceived = 1; 70 | TimeDeltaStart = 2; 71 | SequenceStart = 3; 72 | First = 4; 73 | } 74 | 75 | // Protocol for a client to subscribe 76 | message SubscriptionRequest { 77 | string clientID = 1; // ClientID 78 | string subject = 2; // Formal subject to subscribe to, e.g. foo.bar 79 | string qGroup = 3; // Optional queue group 80 | string inbox = 4; // Inbox subject to deliver messages on 81 | int32 maxInFlight = 5; // Maximum inflight messages without an ack allowed 82 | int32 ackWaitInSecs = 6; // Timeout for receiving an ack from the client 83 | string durableName = 84 | 7; // Optional durable name which survives client restarts 85 | StartPosition startPosition = 10; // Start position 86 | uint64 startSequence = 11; // Optional start sequence number 87 | int64 startTimeDelta = 12; // Optional start time 88 | } 89 | 90 | // Response for SubscriptionRequest and UnsubscribeRequests 91 | message SubscriptionResponse { 92 | string ackInbox = 2; // ackInbox for sending acks 93 | string error = 3; // err string, empty/omitted if no error 94 | } 95 | 96 | // Protocol for a clients to unsubscribe. Will return a SubscriptionResponse 97 | message UnsubscribeRequest { 98 | string clientID = 1; // ClientID 99 | string subject = 2; // subject for the subscription 100 | string inbox = 3; // Inbox subject to identify subscription 101 | string durableName = 102 | 4; // Optional durable name which survives client restarts 103 | } 104 | 105 | // Protocol for a client to close a connection 106 | message CloseRequest { 107 | string clientID = 1; // Client name provided to Connect() requests 108 | } 109 | 110 | // Response for CloseRequest 111 | message CloseResponse { 112 | string error = 1; // err string, empty/omitted if no error 113 | } 114 | -------------------------------------------------------------------------------- /test/extensions/filters/http/nats/streaming/metadata_subject_retriever_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "envoy/http/metadata_accessor.h" 4 | 5 | #include "common/config/nats_streaming_well_known_names.h" 6 | #include "common/protobuf/utility.h" 7 | 8 | #include "extensions/filters/http/nats/streaming/metadata_subject_retriever.h" 9 | 10 | #include "test/test_common/utility.h" 11 | 12 | #include "fmt/format.h" 13 | 14 | namespace Envoy { 15 | namespace Extensions { 16 | namespace HttpFilters { 17 | namespace Nats { 18 | namespace Streaming { 19 | 20 | namespace { 21 | 22 | const std::string empty_json = R"EOF( 23 | { 24 | } 25 | )EOF"; 26 | 27 | // TODO(talnordan): Move this to `mocks/http/filter`. 28 | class TesterMetadataAccessor : public Http::MetadataAccessor { 29 | public: 30 | virtual absl::optional getFunctionName() const { 31 | if (!function_name_.empty()) { 32 | return &function_name_; 33 | } 34 | return {}; 35 | } 36 | 37 | virtual absl::optional getFunctionSpec() const { 38 | if (function_spec_.has_value()) { 39 | return &function_spec_.value(); 40 | } 41 | return {}; 42 | } 43 | 44 | virtual absl::optional 45 | getClusterMetadata() const { 46 | if (cluster_metadata_.has_value()) { 47 | return &cluster_metadata_.value(); 48 | } 49 | return {}; 50 | } 51 | 52 | virtual absl::optional getRouteMetadata() const { 53 | if (route_metadata_.has_value()) { 54 | return &route_metadata_.value(); 55 | } 56 | return {}; 57 | } 58 | 59 | std::string function_name_; 60 | absl::optional function_spec_; 61 | absl::optional cluster_metadata_; 62 | absl::optional route_metadata_; 63 | }; 64 | 65 | ProtobufWkt::Struct getMetadata(const std::string &json) { 66 | ProtobufWkt::Struct metadata; 67 | MessageUtil::loadFromJson(json, metadata); 68 | 69 | return metadata; 70 | } 71 | 72 | TesterMetadataAccessor 73 | getMetadataAccessor(const std::string &function_name, 74 | const ProtobufWkt::Struct &func_metadata, 75 | const ProtobufWkt::Struct &cluster_metadata, 76 | const ProtobufWkt::Struct &route_metadata) { 77 | TesterMetadataAccessor testaccessor; 78 | testaccessor.function_name_ = function_name; 79 | testaccessor.function_spec_ = func_metadata; 80 | testaccessor.cluster_metadata_ = cluster_metadata; 81 | testaccessor.route_metadata_ = route_metadata; 82 | 83 | return testaccessor; 84 | } 85 | 86 | TesterMetadataAccessor getMetadataAccessorFromJson( 87 | const std::string &function_name, const std::string &func_json, 88 | const std::string &cluster_json, const std::string &route_json) { 89 | auto func_metadata = getMetadata(func_json); 90 | auto cluster_metadata = getMetadata(cluster_json); 91 | auto route_metadata = getMetadata(route_json); 92 | 93 | return getMetadataAccessor(function_name, func_metadata, cluster_metadata, 94 | route_metadata); 95 | } 96 | 97 | } // namespace 98 | 99 | TEST(MetadataSubjectRetrieverTest, EmptyJsonAndNoFunctions) { 100 | const std::string function_name = ""; 101 | const std::string &func_json = empty_json; 102 | const std::string &cluster_json = empty_json; 103 | const std::string &route_json = empty_json; 104 | 105 | auto metadata_accessor = 106 | getMetadataAccessorFromJson("", func_json, cluster_json, route_json); 107 | 108 | MetadataSubjectRetriever subjectRetriever; 109 | auto subject = subjectRetriever.getSubject(metadata_accessor); 110 | 111 | EXPECT_FALSE(subject.has_value()); 112 | } 113 | 114 | TEST(MetadataSubjectRetrieverTest, ConfiguredSubject) { 115 | const std::string configuredSubject = "Subject1"; 116 | 117 | const std::string func_json = empty_json; 118 | const std::string cluster_json_template = R"EOF( 119 | {{ 120 | "{}": "ci", 121 | "{}": "dp", 122 | }} 123 | )EOF"; 124 | 125 | const std::string cluster_json = 126 | fmt::format(cluster_json_template, 127 | Config::MetadataNatsStreamingKeys::get().CLUSTER_ID, 128 | Config::MetadataNatsStreamingKeys::get().DISCOVER_PREFIX); 129 | const std::string route_json = empty_json; 130 | 131 | auto metadata_accessor = getMetadataAccessorFromJson( 132 | configuredSubject, func_json, cluster_json, route_json); 133 | 134 | MetadataSubjectRetriever subjectRetriever; 135 | auto maybe_actual_subject = subjectRetriever.getSubject(metadata_accessor); 136 | 137 | ASSERT_TRUE(maybe_actual_subject.has_value()); 138 | auto actual_subject = maybe_actual_subject.value(); 139 | EXPECT_EQ(*actual_subject.subject, configuredSubject); 140 | EXPECT_EQ(*actual_subject.cluster_id, "ci"); 141 | EXPECT_EQ(*actual_subject.discover_prefix, "dp"); 142 | } 143 | 144 | } // namespace Streaming 145 | } // namespace Nats 146 | } // namespace HttpFilters 147 | } // namespace Extensions 148 | } // namespace Envoy 149 | -------------------------------------------------------------------------------- /source/extensions/filters/http/nats/streaming/nats_streaming_filter.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "envoy/http/header_map.h" 9 | #include "envoy/nats/streaming/client.h" 10 | 11 | #include "common/common/empty_string.h" 12 | #include "common/common/macros.h" 13 | #include "common/common/utility.h" 14 | #include "common/grpc/common.h" 15 | #include "common/http/filter_utility.h" 16 | #include "common/http/solo_filter_utility.h" 17 | #include "common/http/utility.h" 18 | 19 | namespace Envoy { 20 | namespace Extensions { 21 | namespace HttpFilters { 22 | namespace Nats { 23 | namespace Streaming { 24 | 25 | NatsStreamingFilter::NatsStreamingFilter( 26 | NatsStreamingFilterConfigSharedPtr config, 27 | SubjectRetrieverSharedPtr retreiver, 28 | Envoy::Nats::Streaming::ClientPtr nats_streaming_client) 29 | : config_(config), subject_retriever_(retreiver), 30 | nats_streaming_client_(nats_streaming_client) {} 31 | 32 | NatsStreamingFilter::~NatsStreamingFilter() {} 33 | 34 | void NatsStreamingFilter::onDestroy() { 35 | 36 | if (in_flight_request_ != nullptr) { 37 | in_flight_request_->cancel(); 38 | in_flight_request_ = nullptr; 39 | } 40 | } 41 | 42 | Http::FilterHeadersStatus 43 | NatsStreamingFilter::decodeHeaders(Envoy::Http::HeaderMap &headers, 44 | bool end_stream) { 45 | RELEASE_ASSERT(isActive(), ""); 46 | 47 | // Fill in the headers. 48 | // TODO(talnordan): Consider extracting a common utility function which 49 | // converts a `HeaderMap` to a Protobuf `Map`, to reduce code duplication 50 | // with `Filters::Common::ExtAuthz::CheckRequestUtils::setHttpRequest()`. 51 | auto mutable_headers = payload_.mutable_headers(); 52 | headers.iterate( 53 | [](const Envoy::Http::HeaderEntry &e, void *ctx) { 54 | Envoy::Protobuf::Map *mutable_headers = 56 | static_cast *>( 58 | ctx); 59 | (*mutable_headers)[std::string(e.key().getStringView())] = 60 | std::string(e.value().getStringView()); 61 | return Envoy::Http::HeaderMap::Iterate::Continue; 62 | }, 63 | mutable_headers); 64 | 65 | if (end_stream) { 66 | relayToNatsStreaming(); 67 | } 68 | 69 | return Http::FilterHeadersStatus::StopIteration; 70 | } 71 | 72 | Http::FilterDataStatus 73 | NatsStreamingFilter::decodeData(Envoy::Buffer::Instance &data, 74 | bool end_stream) { 75 | RELEASE_ASSERT(isActive(), ""); 76 | body_.move(data); 77 | 78 | if ((decoder_buffer_limit_.has_value()) && 79 | ((body_.length() + data.length()) > decoder_buffer_limit_.value())) { 80 | 81 | decoder_callbacks_->sendLocalReply(Http::Code::PayloadTooLarge, 82 | "nats streaming paylaod too large", 83 | nullptr); 84 | return Http::FilterDataStatus::StopIterationNoBuffer; 85 | } 86 | 87 | body_.move(data); 88 | 89 | if (end_stream) { 90 | relayToNatsStreaming(); 91 | 92 | // TODO(talnordan): We need to make sure that life time of the buffer makes 93 | // sense. 94 | return Http::FilterDataStatus::StopIterationNoBuffer; 95 | } 96 | 97 | return Http::FilterDataStatus::StopIterationNoBuffer; 98 | } 99 | 100 | Http::FilterTrailersStatus 101 | NatsStreamingFilter::decodeTrailers(Envoy::Http::HeaderMap &) { 102 | RELEASE_ASSERT(isActive(), ""); 103 | 104 | relayToNatsStreaming(); 105 | return Http::FilterTrailersStatus::StopIteration; 106 | } 107 | 108 | bool NatsStreamingFilter::retrieveFunction( 109 | const Http::MetadataAccessor &meta_accessor) { 110 | retrieveSubject(meta_accessor); 111 | return isActive(); 112 | } 113 | 114 | void NatsStreamingFilter::onResponse() { onCompletion(Http::Code::OK, ""); } 115 | 116 | void NatsStreamingFilter::onFailure() { 117 | onCompletion(Http::Code::InternalServerError, "nats streaming filter abort", 118 | RequestInfo::ResponseFlag::NoHealthyUpstream); 119 | } 120 | 121 | void NatsStreamingFilter::onTimeout() { 122 | onCompletion(Http::Code::RequestTimeout, "nats streaming filter timeout", 123 | RequestInfo::ResponseFlag::UpstreamRequestTimeout); 124 | } 125 | 126 | void NatsStreamingFilter::retrieveSubject( 127 | const Http::MetadataAccessor &meta_accessor) { 128 | optional_subject_ = subject_retriever_->getSubject(meta_accessor); 129 | } 130 | 131 | void NatsStreamingFilter::relayToNatsStreaming() { 132 | RELEASE_ASSERT(optional_subject_.has_value(), ""); 133 | RELEASE_ASSERT(!optional_subject_.value().subject->empty(), ""); 134 | 135 | const std::string *cluster_name = 136 | Http::SoloFilterUtility::resolveClusterName(decoder_callbacks_); 137 | if (!cluster_name) { 138 | // TODO(talnordan): Consider changing the return type to `bool` and 139 | // returning `false`. 140 | return; 141 | } 142 | 143 | auto &&subject_entry = optional_subject_.value(); 144 | const std::string &subject = *subject_entry.subject; 145 | const std::string &cluster_id = *subject_entry.cluster_id; 146 | const std::string &discover_prefix = *subject_entry.discover_prefix; 147 | 148 | // TODO(talnordan): Consider minimizing content copying. 149 | payload_.set_body(body_.toString()); 150 | std::string payload_string = payload_.SerializeAsString(); 151 | in_flight_request_ = nats_streaming_client_->makeRequest( 152 | subject, cluster_id, discover_prefix, std::move(payload_string), *this); 153 | } 154 | 155 | void NatsStreamingFilter::onCompletion(Http::Code response_code, 156 | const std::string &body_text) { 157 | in_flight_request_ = nullptr; 158 | 159 | decoder_callbacks_->sendLocalReply(response_code, body_text, nullptr); 160 | } 161 | 162 | void NatsStreamingFilter::onCompletion( 163 | Http::Code response_code, const std::string &body_text, 164 | RequestInfo::ResponseFlag response_flag) { 165 | decoder_callbacks_->requestInfo().setResponseFlag(response_flag); 166 | onCompletion(response_code, body_text); 167 | } 168 | 169 | } // namespace Streaming 170 | } // namespace Nats 171 | } // namespace HttpFilters 172 | } // namespace Extensions 173 | } // namespace Envoy 174 | -------------------------------------------------------------------------------- /e2e/e2e_test.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import ctypes.util 3 | import grequests 4 | import httplib 5 | import logging 6 | import os 7 | import requests 8 | import signal 9 | import subprocess 10 | import tempfile 11 | import time 12 | import unittest 13 | 14 | def envoy_preexec_fn(): 15 | PR_SET_PDEATHSIG = 1 # See prtcl(2). 16 | os.setpgrp() 17 | libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True) 18 | libc.prctl(PR_SET_PDEATHSIG, signal.SIGTERM) 19 | 20 | DEBUG=True 21 | 22 | class ManyRequestsTestCase(unittest.TestCase): 23 | def setUp(self): 24 | # A temporary file is used to avoid pipe buffering issues. 25 | self.cleanup() 26 | self.stderr = tempfile.NamedTemporaryFile("rw+", delete=True) 27 | 28 | def tearDown(self): 29 | for p in (self.sub_process, self.nats_server, self.nats_streaming_server): 30 | if p is not None: 31 | p.terminate() 32 | if self.envoy is not None: 33 | self.envoy.send_signal(signal.SIGINT) 34 | self.envoy.wait() 35 | 36 | # The file is deleted as soon as it is closed. 37 | if self.stderr is not None: 38 | self.stderr.close() 39 | self.cleanup() 40 | 41 | def cleanup(self): 42 | self.sub_process = None 43 | self.nats_server = None 44 | self.nats_streaming_server = None 45 | self.envoy = None 46 | self.stderr = None 47 | 48 | def __create_config(self): 49 | for create_config_path in ("./create_config.sh", "./e2e/create_config.sh"): 50 | if os.path.isfile(create_config_path): 51 | subprocess.check_call(create_config_path) 52 | break 53 | else: 54 | self.fail('"create_config.sh" was not found') 55 | 56 | def __start_nats_server(self): 57 | if DEBUG: 58 | self.nats_server = subprocess.Popen(["gnatsd", "-DV"]) 59 | else: 60 | self.nats_server = subprocess.Popen("gnatsd") 61 | 62 | def __start_nats_streaming_server(self): 63 | if DEBUG: 64 | self.nats_streaming_server = subprocess.Popen( 65 | ["nats-streaming-server", "-ns", "nats://localhost:4222", "-SDV"]) 66 | else: 67 | self.nats_streaming_server = subprocess.Popen(["nats-streaming-server", "-ns", "nats://localhost:4222"]) 68 | 69 | def __start_envoy(self, prefix = None, suffix = None): 70 | if prefix is None: 71 | prefix = [] 72 | if suffix is None: 73 | suffix = suffix = ["--log-level", "debug"] if DEBUG else [] 74 | 75 | envoy = os.environ.get("TEST_ENVOY_BIN","envoy") 76 | 77 | self.envoy = subprocess.Popen(prefix + [envoy, "-c", "./envoy.yaml"]+suffix, preexec_fn=envoy_preexec_fn) 78 | time.sleep(5) 79 | 80 | def __sub(self): 81 | self.sub_process = subprocess.Popen( 82 | ["stan-sub", "-id", "17", "subject1"], 83 | stderr=self.stderr) 84 | time.sleep(.1) 85 | 86 | def __make_request(self, payload, expected_status): 87 | response = requests.post('http://localhost:10000/post', payload) 88 | self.assertEqual(expected_status, response.status_code) 89 | 90 | def __make_many_requests(self, payloads, expected_status): 91 | requests = (grequests.post('http://localhost:10000/post', data=p) for p in payloads) 92 | responses = grequests.map(requests) 93 | if expected_status: 94 | for response in responses: 95 | self.assertEqual(expected_status, response.status_code) 96 | 97 | def __wait_for_response(self, data): 98 | time.sleep(0.1) 99 | self.sub_process.terminate() 100 | self.sub_process = None 101 | self.stderr.seek(0, 0) 102 | stderr = self.stderr.read() 103 | 104 | # TODO(talnordan): Validate the entire Protobuf message, including headers. 105 | self.assertIn('subject:"subject1"', stderr) 106 | self.assertIn(data, stderr) 107 | 108 | def __make_request_batches(self, 109 | format_string, 110 | batches, 111 | requests_in_batch, 112 | sleep_interval, 113 | expected_status): 114 | for i in xrange(batches): 115 | payloads = [(format_string % (i, j)) for j in xrange(requests_in_batch)] 116 | self.__make_many_requests(payloads, expected_status) 117 | time.sleep(sleep_interval) 118 | 119 | def test_make_many_requests(self): 120 | # Set up environment. 121 | self.__create_config() 122 | self.__start_nats_server() 123 | self.__start_nats_streaming_server() 124 | self.__start_envoy() 125 | self.__sub() 126 | 127 | # Make many requests and assert that they succeed. 128 | self.__make_request_batches("solopayload %d %d", 3, 1024, 0.1, httplib.OK) 129 | self.__wait_for_response("solopayload 2 1023") 130 | 131 | # Terminate NATS Streaming to make future requests timeout. 132 | self.nats_streaming_server.terminate() 133 | self.nats_streaming_server = None 134 | 135 | # Make many requests and assert that they timeout. 136 | self.__make_request_batches("solopayload %d %d", 2, 1024, 0.1, httplib.REQUEST_TIMEOUT) 137 | 138 | def test_profile(self): 139 | report_loc = os.environ.get("TEST_PROF_REPORT","") 140 | if not report_loc: 141 | self.skipTest("to enable, set TEST_PROF_REPORT to where you want the report to be saved. " + \ 142 | "i.e. TEST_PROF_REPORT=report.data") 143 | print("Starting perf tests; if you have issues you might need to enable perf for normal users:") 144 | print("'echo -1 | sudo tee /proc/sys/kernel/perf_event_paranoid'") 145 | print("'echo 0 | sudo tee /proc/sys/kernel/kptr_restrict'") 146 | # Set up environment. 147 | # See https://github.com/envoyproxy/envoy/blob/e51c8ad0e0526f78c47a7f90807c184a039207d5/tools/envoy_collect/envoy_collect.py#L192 148 | self.__create_config() 149 | self.__start_nats_server() 150 | self.__start_nats_streaming_server() 151 | self.__start_envoy(["perf", "record", "-g","--"], ["-l","error"]) 152 | self.__sub() 153 | 154 | # Make many requests and assert that they succeed. 155 | self.__make_request_batches("solopayload %d %d", 20, 1024, 0.1, None) 156 | # The performance tests are slower so we have lower expectations of whats received 157 | self.__wait_for_response("solopayload 0 500") 158 | 159 | # tear down everything so we can copy the report 160 | self.tearDown() 161 | 162 | # print the report 163 | subprocess.check_call(["cp", "perf.data", report_loc]) 164 | 165 | if __name__ == "__main__": 166 | global DEBUG 167 | DEBUG = True if os.environ.get("DEBUG","") != "0" else False 168 | if DEBUG: 169 | logging.basicConfig(level=logging.DEBUG) 170 | unittest.main() 171 | -------------------------------------------------------------------------------- /source/common/nats/streaming/client_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/event/timer.h" 6 | #include "envoy/nats/codec.h" 7 | #include "envoy/nats/streaming/client.h" 8 | #include "envoy/runtime/runtime.h" 9 | #include "envoy/tcp/conn_pool_nats.h" 10 | 11 | #include "common/common/logger.h" 12 | #include "common/nats/streaming/connect_response_handler.h" 13 | #include "common/nats/streaming/heartbeat_handler.h" 14 | #include "common/nats/streaming/message_utility.h" 15 | #include "common/nats/streaming/pub_request_handler.h" 16 | #include "common/nats/subject_utility.h" 17 | #include "common/nats/token_generator_impl.h" 18 | 19 | #include "absl/types/optional.h" 20 | 21 | namespace Envoy { 22 | namespace Nats { 23 | namespace Streaming { 24 | 25 | // TODO(talnordan): Maintaining the state of multiple requests and multiple 26 | // inboxes in a single object is becoming cumbersome, error-prone and hard to 27 | // unit-test. Consider refactoring this code into an object hierarchy parallel 28 | // to the inbox hierarchy. After the refactoring, each object is going to be 29 | // responsible for changing its internal state upon incoming messages from a 30 | // particular inbox. Such design would be similar to an actor system. 31 | class ClientImpl : public Client, 32 | public Tcp::ConnPoolNats::PoolCallbacks, 33 | public ConnectResponseHandler::Callbacks, 34 | public HeartbeatHandler::Callbacks, 35 | public Envoy::Logger::Loggable { 36 | public: 37 | ClientImpl(Tcp::ConnPoolNats::InstancePtr &&conn_pool, 38 | Runtime::RandomGenerator &random, Event::Dispatcher &dispatcher, 39 | const std::chrono::milliseconds &op_timeout); 40 | 41 | // Nats::Streaming::Client 42 | PublishRequestPtr makeRequest(const std::string &subject, 43 | const std::string &cluster_id, 44 | const std::string &discover_prefix, 45 | std::string &&payload, 46 | PublishCallbacks &callbacks) override; 47 | 48 | // Tcp::ConnPoolNats::PoolCallbacks 49 | void onResponse(Nats::MessagePtr &&value) override; 50 | void onClose() override; 51 | 52 | // Nats::Streaming::InboxCallbacks 53 | void onFailure(const std::string &error) override; 54 | 55 | // Nats::Streaming::ConnectResponseHandler::Callbacks 56 | void onConnected(const std::string &pub_prefix) override; 57 | 58 | // Nats::Streaming::HeartbeatHandler::Callbacks 59 | void send(const Message &message) override; 60 | 61 | void cancel(const std::string &pub_ack_inbox); 62 | 63 | private: 64 | enum class State { NotConnected, Connecting, Connected }; 65 | 66 | struct PendingRequest { 67 | std::string subject; 68 | std::string payload; 69 | PublishCallbacks *callbacks; 70 | }; 71 | 72 | class PublishRequestCanceler : public PublishRequest { 73 | public: 74 | PublishRequestCanceler(ClientImpl &parent, std::string &&pub_ack_inbox); 75 | 76 | // Nats::Streaming::PublishRequest 77 | void cancel(); 78 | 79 | private: 80 | ClientImpl &parent_; 81 | const std::string pub_ack_inbox_; 82 | }; 83 | 84 | inline void onOperation(Nats::MessagePtr &&value); 85 | 86 | inline void onPayload(Nats::MessagePtr &&value); 87 | 88 | inline void onInfo(Nats::MessagePtr &&value); 89 | 90 | inline void onMsg(std::vector &&tokens); 91 | 92 | inline void onPing(); 93 | 94 | inline void onTimeout(const std::string &pub_ack_inbox); 95 | 96 | inline void subInbox(const std::string &subject); 97 | 98 | inline void subChildWildcardInbox(const std::string &parent_subject); 99 | 100 | inline void subHeartbeatInbox(); 101 | 102 | inline void subReplyInbox(); 103 | 104 | inline void subPubAckInbox(); 105 | 106 | inline void pubConnectRequest(); 107 | 108 | inline void enqueuePendingRequest(const std::string &subject, 109 | const std::string &payload, 110 | PublishCallbacks &callbacks, 111 | const std::string &pub_ack_inbox); 112 | 113 | inline void pubPubMsg(const std::string &subject, const std::string &payload, 114 | PublishCallbacks &callbacks, 115 | const std::string &pub_ack_inbox); 116 | 117 | inline void pong(); 118 | 119 | inline void sendNatsMessage(const Message &message); 120 | 121 | // TODO(talnordan): Consider introducing `Nats::streaming::Message` instead of 122 | // using `std::string`. 123 | inline void pubNatsStreamingMessage(const std::string &subject, 124 | const std::string &reply_to, 125 | const std::string &message); 126 | 127 | inline void waitForPayload(std::string subject, 128 | absl::optional reply_to) { 129 | subect_and_reply_to_waiting_for_payload_.emplace( 130 | make_pair(subject, reply_to)); 131 | } 132 | 133 | inline bool isWaitingForPayload() const { 134 | return subect_and_reply_to_waiting_for_payload_.has_value(); 135 | } 136 | 137 | inline std::string &getSubjectWaitingForPayload() { 138 | return subect_and_reply_to_waiting_for_payload_.value().first; 139 | } 140 | 141 | inline absl::optional &getReplyToWaitingForPayload() { 142 | return subect_and_reply_to_waiting_for_payload_.value().second; 143 | } 144 | 145 | inline void doneWaitingForPayload() { 146 | subect_and_reply_to_waiting_for_payload_ = 147 | absl::optional>>{}; 148 | } 149 | 150 | Tcp::ConnPoolNats::InstancePtr conn_pool_; 151 | TokenGeneratorImpl token_generator_; 152 | Event::Dispatcher &dispatcher_; 153 | const std::chrono::milliseconds op_timeout_; 154 | State state_{}; 155 | const std::string heartbeat_inbox_; 156 | const std::string root_inbox_; 157 | const std::string root_pub_ack_inbox_; 158 | const std::string connect_response_inbox_; 159 | const std::string client_id_; 160 | std::map pending_request_per_inbox_; 161 | std::map pub_request_per_inbox_; 162 | uint64_t sid_; 163 | absl::optional cluster_id_{}; 164 | absl::optional discover_prefix_{}; 165 | absl::optional>> 166 | subect_and_reply_to_waiting_for_payload_{}; 167 | absl::optional pub_prefix_{}; 168 | 169 | static const std::string INBOX_PREFIX; 170 | static const std::string PUB_ACK_PREFIX; 171 | }; 172 | 173 | } // namespace Streaming 174 | } // namespace Nats 175 | } // namespace Envoy 176 | -------------------------------------------------------------------------------- /test/common/nats/streaming/pub_request_handler_test.cc: -------------------------------------------------------------------------------- 1 | #include "common/common/assert.h" 2 | #include "common/nats/streaming/message_utility.h" 3 | #include "common/nats/streaming/pub_request_handler.h" 4 | 5 | #include "test/mocks/event/mocks.h" 6 | #include "test/mocks/nats/streaming/mocks.h" 7 | 8 | #include "gmock/gmock.h" 9 | 10 | using testing::NiceMock; 11 | 12 | namespace Envoy { 13 | namespace Nats { 14 | namespace Streaming { 15 | 16 | class NatsStreamingPubRequestHandlerTest : public testing::Test { 17 | public: 18 | NatsStreamingPubRequestHandlerTest() {} 19 | 20 | protected: 21 | class MockInboxCallbacks : public InboxCallbacks { 22 | public: 23 | MOCK_METHOD1(onFailure, void(const std::string &error)); 24 | }; 25 | 26 | MockInboxCallbacks inbox_callbacks_; 27 | MockPublishCallbacks publish_callbacks_; 28 | }; 29 | 30 | TEST_F(NatsStreamingPubRequestHandlerTest, NonEmptyReplyTo) { 31 | const absl::optional reply_to{"reply-to"}; 32 | const std::string payload{}; 33 | 34 | EXPECT_CALL(inbox_callbacks_, 35 | onFailure("incoming PubAck with non-empty reply subject")) 36 | .Times(1); 37 | PubRequestHandler::onMessage(reply_to, payload, inbox_callbacks_, 38 | publish_callbacks_); 39 | } 40 | 41 | TEST_F(NatsStreamingPubRequestHandlerTest, NoPayload) { 42 | const absl::optional reply_to{}; 43 | const std::string payload{}; 44 | 45 | EXPECT_CALL(inbox_callbacks_, onFailure("incoming PubAck without payload")) 46 | .Times(1); 47 | PubRequestHandler::onMessage(reply_to, payload, inbox_callbacks_, 48 | publish_callbacks_); 49 | } 50 | 51 | TEST_F(NatsStreamingPubRequestHandlerTest, Error) { 52 | const absl::optional reply_to{}; 53 | const std::string guid{"guid1"}; 54 | const std::string error{"error1"}; 55 | const std::string payload{MessageUtility::createPubAckMessage(guid, error)}; 56 | 57 | EXPECT_CALL(publish_callbacks_, onFailure()).Times(1); 58 | PubRequestHandler::onMessage(reply_to, payload, inbox_callbacks_, 59 | publish_callbacks_); 60 | } 61 | 62 | TEST_F(NatsStreamingPubRequestHandlerTest, NoError) { 63 | const absl::optional reply_to{}; 64 | const std::string guid{"guid1"}; 65 | const std::string error{}; 66 | const std::string payload{MessageUtility::createPubAckMessage(guid, error)}; 67 | 68 | EXPECT_CALL(publish_callbacks_, onResponse()).Times(1); 69 | PubRequestHandler::onMessage(reply_to, payload, inbox_callbacks_, 70 | publish_callbacks_); 71 | } 72 | 73 | TEST_F(NatsStreamingPubRequestHandlerTest, InvalidPayload) { 74 | const absl::optional reply_to{}; 75 | const std::string guid{"guid1"}; 76 | const std::string error{}; 77 | const std::string payload{"This is not a PubAck message."}; 78 | 79 | EXPECT_CALL(publish_callbacks_, onFailure()).Times(1); 80 | PubRequestHandler::onMessage(reply_to, payload, inbox_callbacks_, 81 | publish_callbacks_); 82 | } 83 | 84 | TEST_F(NatsStreamingPubRequestHandlerTest, MapNoPayload) { 85 | const std::string inbox{"inbox1"}; 86 | const absl::optional reply_to{}; 87 | const std::string payload{}; 88 | auto timeout_timer = Event::TimerPtr(new NiceMock); 89 | std::map request_per_inbox; 90 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 91 | request_per_inbox.emplace(inbox, std::move(pub_request)); 92 | 93 | EXPECT_CALL(inbox_callbacks_, onFailure("incoming PubAck without payload")) 94 | .Times(1); 95 | PubRequestHandler::onMessage(inbox, reply_to, payload, inbox_callbacks_, 96 | request_per_inbox); 97 | 98 | EXPECT_EQ(request_per_inbox.end(), request_per_inbox.find(inbox)); 99 | } 100 | 101 | TEST_F(NatsStreamingPubRequestHandlerTest, MapError) { 102 | const std::string inbox{"inbox1"}; 103 | const absl::optional reply_to{}; 104 | const std::string guid{"guid1"}; 105 | const std::string error{"error1"}; 106 | const std::string payload{MessageUtility::createPubAckMessage(guid, error)}; 107 | auto timeout_timer = Event::TimerPtr(new NiceMock); 108 | std::map request_per_inbox; 109 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 110 | request_per_inbox.emplace(inbox, std::move(pub_request)); 111 | 112 | EXPECT_CALL(publish_callbacks_, onFailure()).Times(1); 113 | PubRequestHandler::onMessage(inbox, reply_to, payload, inbox_callbacks_, 114 | request_per_inbox); 115 | 116 | EXPECT_EQ(request_per_inbox.end(), request_per_inbox.find(inbox)); 117 | } 118 | 119 | TEST_F(NatsStreamingPubRequestHandlerTest, MapInvalidPayload) { 120 | const std::string inbox{"inbox1"}; 121 | const absl::optional reply_to{}; 122 | const std::string guid{"guid1"}; 123 | const std::string error{}; 124 | const std::string payload{"This is not a PubAck message."}; 125 | auto timeout_timer = Event::TimerPtr(new NiceMock); 126 | std::map request_per_inbox; 127 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 128 | request_per_inbox.emplace(inbox, std::move(pub_request)); 129 | 130 | EXPECT_CALL(publish_callbacks_, onFailure()).Times(1); 131 | PubRequestHandler::onMessage(inbox, reply_to, payload, inbox_callbacks_, 132 | request_per_inbox); 133 | 134 | EXPECT_EQ(request_per_inbox.end(), request_per_inbox.find(inbox)); 135 | } 136 | 137 | TEST_F(NatsStreamingPubRequestHandlerTest, MapNoError) { 138 | const std::string inbox{"inbox1"}; 139 | const absl::optional reply_to{}; 140 | const std::string guid{"guid1"}; 141 | const std::string error{}; 142 | const std::string payload{MessageUtility::createPubAckMessage(guid, error)}; 143 | auto timeout_timer = Event::TimerPtr(new NiceMock); 144 | std::map request_per_inbox; 145 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 146 | request_per_inbox.emplace(inbox, std::move(pub_request)); 147 | 148 | EXPECT_CALL(publish_callbacks_, onResponse()).Times(1); 149 | PubRequestHandler::onMessage(inbox, reply_to, payload, inbox_callbacks_, 150 | request_per_inbox); 151 | 152 | EXPECT_EQ(request_per_inbox.end(), request_per_inbox.find(inbox)); 153 | } 154 | 155 | TEST_F(NatsStreamingPubRequestHandlerTest, MapMissingInbox) { 156 | const std::string inbox{"inbox1"}; 157 | const absl::optional reply_to{}; 158 | const std::string guid{"guid1"}; 159 | const std::string error{}; 160 | const std::string payload{MessageUtility::createPubAckMessage(guid, error)}; 161 | auto timeout_timer = Event::TimerPtr(new NiceMock); 162 | std::map request_per_inbox; 163 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 164 | request_per_inbox.emplace(inbox, std::move(pub_request)); 165 | 166 | PubRequestHandler::onMessage("inbox2", reply_to, payload, inbox_callbacks_, 167 | request_per_inbox); 168 | 169 | EXPECT_NE(request_per_inbox.end(), request_per_inbox.find(inbox)); 170 | } 171 | 172 | TEST_F(NatsStreamingPubRequestHandlerTest, OnTimeout) { 173 | const std::string inbox{"inbox1"}; 174 | auto timeout_timer = Event::TimerPtr(new NiceMock); 175 | std::map request_per_inbox; 176 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 177 | request_per_inbox.emplace(inbox, std::move(pub_request)); 178 | 179 | EXPECT_CALL(publish_callbacks_, onTimeout()).Times(1); 180 | PubRequestHandler::onTimeout(inbox, request_per_inbox); 181 | 182 | EXPECT_EQ(request_per_inbox.end(), request_per_inbox.find(inbox)); 183 | } 184 | 185 | TEST_F(NatsStreamingPubRequestHandlerTest, OnTimeoutMissingInbox) { 186 | const std::string inbox{"inbox1"}; 187 | auto timeout_timer = Event::TimerPtr(new NiceMock); 188 | std::map request_per_inbox; 189 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 190 | request_per_inbox.emplace(inbox, std::move(pub_request)); 191 | 192 | EXPECT_CALL(publish_callbacks_, onTimeout()).Times(0); 193 | PubRequestHandler::onTimeout("inbox2", request_per_inbox); 194 | 195 | EXPECT_NE(request_per_inbox.end(), request_per_inbox.find(inbox)); 196 | } 197 | 198 | TEST_F(NatsStreamingPubRequestHandlerTest, OnCancel) { 199 | const std::string inbox{"inbox1"}; 200 | auto timeout_timer = Event::TimerPtr(new NiceMock); 201 | std::map request_per_inbox; 202 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 203 | request_per_inbox.emplace(inbox, std::move(pub_request)); 204 | 205 | EXPECT_CALL(publish_callbacks_, onResponse()).Times(0); 206 | EXPECT_CALL(publish_callbacks_, onFailure()).Times(0); 207 | EXPECT_CALL(publish_callbacks_, onTimeout()).Times(0); 208 | PubRequestHandler::onCancel(inbox, request_per_inbox); 209 | 210 | EXPECT_EQ(request_per_inbox.end(), request_per_inbox.find(inbox)); 211 | } 212 | 213 | TEST_F(NatsStreamingPubRequestHandlerTest, OnCancelMissingInbox) { 214 | const std::string inbox{"inbox1"}; 215 | auto timeout_timer = Event::TimerPtr(new NiceMock); 216 | std::map request_per_inbox; 217 | PubRequest pub_request{&publish_callbacks_, std::move(timeout_timer)}; 218 | request_per_inbox.emplace(inbox, std::move(pub_request)); 219 | 220 | EXPECT_CALL(publish_callbacks_, onResponse()).Times(0); 221 | EXPECT_CALL(publish_callbacks_, onFailure()).Times(0); 222 | EXPECT_CALL(publish_callbacks_, onTimeout()).Times(0); 223 | PubRequestHandler::onCancel("inbox2", request_per_inbox); 224 | 225 | EXPECT_NE(request_per_inbox.end(), request_per_inbox.find(inbox)); 226 | } 227 | 228 | } // namespace Streaming 229 | } // namespace Nats 230 | } // namespace Envoy 231 | -------------------------------------------------------------------------------- /test/extensions/filters/http/nats/streaming/nats_streaming_filter_test.cc: -------------------------------------------------------------------------------- 1 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter.h" 2 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter_config.h" 3 | #include "extensions/filters/http/nats/streaming/nats_streaming_filter_config_factory.h" 4 | #include "extensions/filters/http/nats/streaming/subject_retriever.h" 5 | 6 | #include "test/extensions/filters/http/nats/streaming/mocks.h" 7 | #include "test/mocks/http/mocks.h" 8 | #include "test/mocks/nats/streaming/mocks.h" 9 | #include "test/mocks/server/mocks.h" 10 | #include "test/mocks/upstream/mocks.h" 11 | 12 | #include "./nats_streaming_filter.pb.h" 13 | #include "gmock/gmock.h" 14 | #include "gtest/gtest.h" 15 | 16 | using testing::_; 17 | using testing::NiceMock; 18 | using testing::Ref; 19 | 20 | namespace Envoy { 21 | namespace Extensions { 22 | namespace HttpFilters { 23 | namespace Nats { 24 | namespace Streaming { 25 | 26 | // TODO: move to common 27 | class NothingMetadataAccessor : public Http::MetadataAccessor { 28 | public: 29 | virtual absl::optional getFunctionName() const { 30 | return {}; 31 | } 32 | virtual absl::optional getFunctionSpec() const { 33 | return {}; 34 | } 35 | virtual absl::optional 36 | getClusterMetadata() const { 37 | return {}; 38 | } 39 | virtual absl::optional getRouteMetadata() const { 40 | return {}; 41 | } 42 | 43 | virtual ~NothingMetadataAccessor() {} 44 | }; 45 | 46 | class NatsStreamingFilterTest : public testing::Test { 47 | public: 48 | NatsStreamingFilterTest() {} 49 | 50 | protected: 51 | void SetUp() override { 52 | 53 | envoy::api::v2::filter::http::NatsStreaming proto_config; 54 | proto_config.mutable_op_timeout()->set_nanos(17 * 1000000); 55 | proto_config.set_max_connections(1); 56 | proto_config.set_cluster("cluster"); 57 | 58 | config_.reset(new NatsStreamingFilterConfig( 59 | proto_config, factory_context_.clusterManager())); 60 | subject_retriever_.reset(new NiceMock); 61 | nats_streaming_client_.reset( 62 | new NiceMock); 63 | filter_.reset(new NatsStreamingFilter(config_, subject_retriever_, 64 | nats_streaming_client_)); 65 | filter_->setDecoderFilterCallbacks(callbacks_); 66 | } 67 | 68 | bool retreivefunction() { 69 | return filter_->retrieveFunction(NothingMetadataAccessor()); 70 | } 71 | 72 | NiceMock factory_context_; 73 | NatsStreamingFilterConfigSharedPtr config_; 74 | std::shared_ptr> subject_retriever_; 75 | std::shared_ptr> 76 | nats_streaming_client_; 77 | std::unique_ptr filter_; 78 | NiceMock callbacks_; 79 | }; 80 | 81 | TEST_F(NatsStreamingFilterTest, NoSubjectHeaderOnlyRequest) { 82 | // `subject_retriever_->getSubject()` should be called. 83 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 84 | 85 | // `nats_streaming_client_->makeRequest()` should not be called. 86 | EXPECT_CALL(*nats_streaming_client_, makeRequest_(_, _, _, _, _)).Times(0); 87 | 88 | ASSERT_EQ(false, retreivefunction()); 89 | } 90 | 91 | TEST_F(NatsStreamingFilterTest, NoSubjectRequestWithData) { 92 | // `subject_retriever_->getSubject()` should be called. 93 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 94 | 95 | // `nats_streaming_client_->makeRequest()` should not be called. 96 | EXPECT_CALL(*nats_streaming_client_, makeRequest_(_, _, _, _, _)).Times(0); 97 | 98 | ASSERT_EQ(false, retreivefunction()); 99 | } 100 | 101 | TEST_F(NatsStreamingFilterTest, NoSubjectRequestWithTrailers) { 102 | // `subject_retriever_->getSubject()` should be called. 103 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 104 | 105 | // `nats_streaming_client_->makeRequest()` should not be called. 106 | EXPECT_CALL(*nats_streaming_client_, makeRequest_(_, _, _, _, _)).Times(0); 107 | 108 | ASSERT_EQ(false, retreivefunction()); 109 | } 110 | 111 | TEST_F(NatsStreamingFilterTest, HeaderOnlyRequest) { 112 | // `subject_retriever_->getSubject()` should be called. 113 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 114 | 115 | // `nats_streaming_client_->makeRequest()` should be called exactly once. 116 | EXPECT_CALL(*nats_streaming_client_, 117 | makeRequest_("Subject1", "cluster_id", "discover_prefix1", _, 118 | Ref(*filter_))) 119 | .Times(1); 120 | 121 | const std::string subject = "Subject1"; 122 | const std::string cluster_id = "cluster_id"; 123 | const std::string discover_prefix = "discover_prefix1"; 124 | subject_retriever_->subject_ = 125 | absl::optional(Subject{&subject, &cluster_id, &discover_prefix}); 126 | 127 | ASSERT_EQ(true, retreivefunction()); 128 | 129 | Http::TestHeaderMapImpl headers; 130 | EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, 131 | filter_->decodeHeaders(headers, true)); 132 | 133 | EXPECT_TRUE(nats_streaming_client_->last_payload_.empty()); 134 | } 135 | 136 | TEST_F(NatsStreamingFilterTest, RequestWithData) { 137 | // `subject_retriever_->getSubject()` should be called. 138 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 139 | 140 | // `nats_streaming_client_->makeRequest()` should be called exactly once. 141 | EXPECT_CALL(*nats_streaming_client_, 142 | makeRequest_("Subject1", "cluster_id", "discover_prefix1", _, 143 | Ref(*filter_))) 144 | .Times(1); 145 | 146 | const std::string subject = "Subject1"; 147 | const std::string cluster_id = "cluster_id"; 148 | const std::string discover_prefix = "discover_prefix1"; 149 | subject_retriever_->subject_ = 150 | absl::optional(Subject{&subject, &cluster_id, &discover_prefix}); 151 | 152 | callbacks_.buffer_.reset(new Buffer::OwnedImpl); 153 | 154 | ASSERT_EQ(true, retreivefunction()); 155 | 156 | Http::TestHeaderMapImpl headers; 157 | EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, 158 | filter_->decodeHeaders(headers, false)); 159 | 160 | Buffer::OwnedImpl data1("hello"); 161 | callbacks_.buffer_->add(data1); 162 | EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, 163 | filter_->decodeData(data1, false)); 164 | 165 | Buffer::OwnedImpl data2(" world"); 166 | callbacks_.buffer_->add(data2); 167 | EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, 168 | filter_->decodeData(data2, true)); 169 | 170 | pb::Payload actual_payload; 171 | EXPECT_TRUE( 172 | actual_payload.ParseFromString(nats_streaming_client_->last_payload_)); 173 | EXPECT_TRUE(actual_payload.headers().empty()); 174 | EXPECT_EQ("hello world", actual_payload.body()); 175 | } 176 | 177 | TEST_F(NatsStreamingFilterTest, RequestWithHeadersAndOneChunkOfData) { 178 | // `subject_retriever_->getSubject()` should be called. 179 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 180 | 181 | // `nats_streaming_client_->makeRequest()` should be called exactly once. 182 | EXPECT_CALL(*nats_streaming_client_, 183 | makeRequest_("Subject1", "cluster_id", "discover_prefix1", _, 184 | Ref(*filter_))) 185 | .Times(1); 186 | 187 | const std::string subject = "Subject1"; 188 | const std::string cluster_id = "cluster_id"; 189 | const std::string discover_prefix = "discover_prefix1"; 190 | subject_retriever_->subject_ = 191 | absl::optional(Subject{&subject, &cluster_id, &discover_prefix}); 192 | 193 | callbacks_.buffer_.reset(new Buffer::OwnedImpl); 194 | 195 | ASSERT_EQ(true, retreivefunction()); 196 | 197 | Http::TestHeaderMapImpl headers{{"some-header", "a"}, {"other-header", "b"}}; 198 | EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, 199 | filter_->decodeHeaders(headers, false)); 200 | 201 | Buffer::OwnedImpl data("hello world"); 202 | callbacks_.buffer_->add(data); 203 | EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, 204 | filter_->decodeData(data, true)); 205 | 206 | pb::Payload actual_payload; 207 | EXPECT_TRUE( 208 | actual_payload.ParseFromString(nats_streaming_client_->last_payload_)); 209 | EXPECT_EQ("a", actual_payload.headers().at("some-header")); 210 | EXPECT_EQ("b", actual_payload.headers().at("other-header")); 211 | EXPECT_EQ("hello world", actual_payload.body()); 212 | } 213 | 214 | TEST_F(NatsStreamingFilterTest, RequestWithTrailers) { 215 | // `subject_retriever_->getSubject()` should be called. 216 | EXPECT_CALL(*subject_retriever_, getSubject(_)).Times(1); 217 | 218 | // `nats_streaming_client_->makeRequest()` should be called exactly once. 219 | EXPECT_CALL(*nats_streaming_client_, 220 | makeRequest_("Subject1", "cluster_id", "discover_prefix1", _, 221 | Ref(*filter_))) 222 | .Times(1); 223 | 224 | const std::string subject = "Subject1"; 225 | const std::string cluster_id = "cluster_id"; 226 | const std::string discover_prefix = "discover_prefix1"; 227 | subject_retriever_->subject_ = 228 | absl::optional(Subject{&subject, &cluster_id, &discover_prefix}); 229 | 230 | callbacks_.buffer_.reset(new Buffer::OwnedImpl); 231 | 232 | ASSERT_EQ(true, retreivefunction()); 233 | 234 | Http::TestHeaderMapImpl headers; 235 | EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, 236 | filter_->decodeHeaders(headers, false)); 237 | 238 | Buffer::OwnedImpl data1("hello"); 239 | callbacks_.buffer_->add(data1); 240 | EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, 241 | filter_->decodeData(data1, false)); 242 | 243 | Buffer::OwnedImpl data2(" world"); 244 | callbacks_.buffer_->add(data2); 245 | EXPECT_EQ(Http::FilterDataStatus::StopIterationNoBuffer, 246 | filter_->decodeData(data2, false)); 247 | 248 | Http::TestHeaderMapImpl trailers; 249 | EXPECT_EQ(Envoy::Http::FilterTrailersStatus::StopIteration, 250 | filter_->decodeTrailers(trailers)); 251 | 252 | pb::Payload actual_payload; 253 | EXPECT_TRUE( 254 | actual_payload.ParseFromString(nats_streaming_client_->last_payload_)); 255 | EXPECT_TRUE(actual_payload.headers().empty()); 256 | EXPECT_EQ("hello world", actual_payload.body()); 257 | } 258 | 259 | } // namespace Streaming 260 | } // namespace Nats 261 | } // namespace HttpFilters 262 | } // namespace Extensions 263 | } // namespace Envoy 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Solo.io, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /source/common/nats/streaming/client_impl.cc: -------------------------------------------------------------------------------- 1 | #include "common/nats/streaming/client_impl.h" 2 | 3 | #include "envoy/event/dispatcher.h" 4 | 5 | #include "common/buffer/buffer_utility.h" 6 | #include "common/common/assert.h" 7 | #include "common/common/macros.h" 8 | #include "common/common/utility.h" 9 | #include "common/nats/message_builder.h" 10 | 11 | namespace Envoy { 12 | namespace Nats { 13 | namespace Streaming { 14 | 15 | using Buffer::BufferUtility; 16 | 17 | const std::string ClientImpl::INBOX_PREFIX{"_INBOX"}; 18 | const std::string ClientImpl::PUB_ACK_PREFIX{"_STAN.acks"}; 19 | 20 | ClientImpl::ClientImpl(Tcp::ConnPoolNats::InstancePtr &&conn_pool_, 21 | Runtime::RandomGenerator &random, 22 | Event::Dispatcher &dispatcher, 23 | const std::chrono::milliseconds &op_timeout) 24 | : conn_pool_(std::move(conn_pool_)), token_generator_(random), 25 | dispatcher_(dispatcher), op_timeout_(op_timeout), 26 | heartbeat_inbox_( 27 | SubjectUtility::randomChild(INBOX_PREFIX, token_generator_)), 28 | root_inbox_(SubjectUtility::randomChild(INBOX_PREFIX, token_generator_)), 29 | root_pub_ack_inbox_( 30 | SubjectUtility::randomChild(PUB_ACK_PREFIX, token_generator_)), 31 | connect_response_inbox_( 32 | SubjectUtility::randomChild(root_inbox_, token_generator_)), 33 | client_id_(token_generator_.random()), sid_(1) {} 34 | 35 | PublishRequestPtr ClientImpl::makeRequest(const std::string &subject, 36 | const std::string &cluster_id, 37 | const std::string &discover_prefix, 38 | std::string &&payload, 39 | PublishCallbacks &callbacks) { 40 | // TODO(talnordan): For a possible performance improvement, consider replacing 41 | // the random child token with a counter. 42 | std::string pub_ack_inbox{ 43 | SubjectUtility::randomChild(root_pub_ack_inbox_, token_generator_)}; 44 | 45 | switch (state_) { 46 | case State::NotConnected: 47 | enqueuePendingRequest(subject, payload, callbacks, pub_ack_inbox); 48 | cluster_id_.emplace(cluster_id); 49 | discover_prefix_.emplace(discover_prefix); 50 | conn_pool_->setPoolCallbacks(*this); 51 | sendNatsMessage(MessageBuilder::createConnectMessage()); 52 | state_ = State::Connecting; 53 | break; 54 | case State::Connecting: 55 | enqueuePendingRequest(subject, payload, callbacks, pub_ack_inbox); 56 | break; 57 | case State::Connected: 58 | pubPubMsg(subject, payload, callbacks, pub_ack_inbox); 59 | break; 60 | } 61 | 62 | PublishRequestPtr request_ptr( 63 | new PublishRequestCanceler(*this, std::move(pub_ack_inbox))); 64 | return request_ptr; 65 | } 66 | 67 | void ClientImpl::onResponse(Nats::MessagePtr &&value) { 68 | ENVOY_LOG(trace, "on response: value is\n[{}]", value->asString()); 69 | 70 | // Check whether a payload is expected prior to NATS operation extraction. 71 | // TODO(talnordan): Eventually, we might want `onResponse()` to be passed a 72 | // single decoded message consisting of both the `MSG` arguments and the 73 | // payload. 74 | if (isWaitingForPayload()) { 75 | onPayload(std::move(value)); 76 | } else { 77 | onOperation(std::move(value)); 78 | } 79 | } 80 | 81 | void ClientImpl::onClose() { 82 | // TODO(talnordan) 83 | } 84 | 85 | void ClientImpl::onFailure(const std::string &error) { 86 | // TODO(talnordan): Error handling: 87 | // 1. Fail all things pending: `pending_request_per_inbox_`, 88 | // `pub_request_per_inbox_`. 89 | // 2. Do a best effort to gracefully unsubscribe and disconnect from NATS 90 | // streaming and NATS. 91 | // 3. Mark the `State` as `State::NotConnected`. 92 | ENVOY_LOG(error, "on failure: error is\n[{}]", error); 93 | } 94 | 95 | void ClientImpl::onConnected(const std::string &pub_prefix) { 96 | state_ = State::Connected; 97 | 98 | pub_prefix_.emplace(pub_prefix); 99 | 100 | for (auto it = pending_request_per_inbox_.begin(); 101 | it != pending_request_per_inbox_.end(); ++it) { 102 | auto &&pub_ack_inbox = it->first; 103 | auto &&pending_request = it->second; 104 | pubPubMsg(pending_request.subject, pending_request.payload, 105 | *pending_request.callbacks, pub_ack_inbox); 106 | } 107 | pending_request_per_inbox_.clear(); 108 | } 109 | 110 | void ClientImpl::send(const Message &message) { sendNatsMessage(message); } 111 | 112 | void ClientImpl::cancel(const std::string &pub_ack_inbox) { 113 | if (state_ == State::Connected) { 114 | PubRequestHandler::onCancel(pub_ack_inbox, pub_request_per_inbox_); 115 | } else { 116 | // Remove the pending request with the specified inbox, if such exists. 117 | pending_request_per_inbox_.erase(pub_ack_inbox); 118 | } 119 | } 120 | 121 | ClientImpl::PublishRequestCanceler::PublishRequestCanceler( 122 | ClientImpl &parent, std::string &&pub_ack_inbox) 123 | : parent_(parent), pub_ack_inbox_(pub_ack_inbox) {} 124 | 125 | void ClientImpl::PublishRequestCanceler::cancel() { 126 | parent_.cancel(pub_ack_inbox_); 127 | } 128 | 129 | void ClientImpl::onOperation(Nats::MessagePtr &&value) { 130 | // TODO(talnordan): For better performance, a future decoder implementation 131 | // might use zero allocation byte parsing. In such case, this function would 132 | // need to switch over an `enum class` representing the message type. See: 133 | // https://github.com/nats-io/go-nats/blob/master/parser.go 134 | // https://youtu.be/ylRKac5kSOk?t=10m46s 135 | 136 | auto delimiters = " \t"; 137 | auto keep_empty_string = false; 138 | auto tokens = 139 | StringUtil::splitToken(value->asString(), delimiters, keep_empty_string); 140 | 141 | auto &&op = tokens[0]; 142 | if (StringUtil::caseCompare(op, "INFO")) { 143 | onInfo(std::move(value)); 144 | } else if (StringUtil::caseCompare(op, "MSG")) { 145 | onMsg(std::move(tokens)); 146 | } else if (StringUtil::caseCompare(op, "PING")) { 147 | onPing(); 148 | } else if (StringUtil::caseCompare(op, "+OK")) { 149 | ENVOY_LOG(error, "on operation: op is [{}], not throwing", op); 150 | } else { 151 | // TODO(talnordan): Error handling. 152 | // TODO(talnordan): Increment error stats. 153 | ENVOY_LOG(error, "on operation: op is [{}], throwing", op); 154 | throw ProtocolError("invalid message"); 155 | } 156 | } 157 | 158 | void ClientImpl::onPayload(Nats::MessagePtr &&value) { 159 | std::string &subject = getSubjectWaitingForPayload(); 160 | absl::optional &reply_to = getReplyToWaitingForPayload(); 161 | std::string &payload = value->asString(); 162 | if (subject == heartbeat_inbox_) { 163 | HeartbeatHandler::onMessage(reply_to, payload, *this); 164 | } else if (subject == connect_response_inbox_) { 165 | ConnectResponseHandler::onMessage(reply_to, payload, *this); 166 | } else { 167 | PubRequestHandler::onMessage(subject, reply_to, payload, *this, 168 | pub_request_per_inbox_); 169 | } 170 | 171 | // Mark that the payload has been received. 172 | doneWaitingForPayload(); 173 | } 174 | 175 | void ClientImpl::onInfo(Nats::MessagePtr &&value) { 176 | // TODO(talnordan): Process `INFO` options. 177 | UNREFERENCED_PARAMETER(value); 178 | 179 | // TODO(talnordan): The following behavior is part of the PoC implementation. 180 | // TODO(talnordan): `UNSUB` before connection shutdown. 181 | subHeartbeatInbox(); 182 | subReplyInbox(); 183 | subPubAckInbox(); 184 | pubConnectRequest(); 185 | } 186 | 187 | void ClientImpl::onMsg(std::vector &&tokens) { 188 | auto num_tokens = tokens.size(); 189 | switch (num_tokens) { 190 | case 4: 191 | waitForPayload(std::string(tokens[1]), absl::optional{}); 192 | break; 193 | case 5: 194 | waitForPayload(std::string(tokens[1]), 195 | absl::optional(std::string(tokens[3]))); 196 | break; 197 | default: 198 | // TODO(talnordan): Error handling. 199 | ENVOY_LOG(error, "on MSG: num_tokens is {}", num_tokens); 200 | throw ProtocolError("invalid MSG"); 201 | } 202 | } 203 | 204 | void ClientImpl::onPing() { pong(); } 205 | 206 | void ClientImpl::onTimeout(const std::string &pub_ack_inbox) { 207 | PubRequestHandler::onTimeout(pub_ack_inbox, pub_request_per_inbox_); 208 | } 209 | 210 | void ClientImpl::subInbox(const std::string &subject) { 211 | sendNatsMessage(MessageBuilder::createSubMessage(subject, sid_)); 212 | ++sid_; 213 | } 214 | 215 | void ClientImpl::subChildWildcardInbox(const std::string &parent_subject) { 216 | std::string child_wildcard{SubjectUtility::childWildcard(parent_subject)}; 217 | subInbox(child_wildcard); 218 | } 219 | 220 | void ClientImpl::subHeartbeatInbox() { subInbox(heartbeat_inbox_); } 221 | 222 | void ClientImpl::subReplyInbox() { subChildWildcardInbox(root_inbox_); } 223 | 224 | void ClientImpl::subPubAckInbox() { 225 | subChildWildcardInbox(root_pub_ack_inbox_); 226 | } 227 | 228 | void ClientImpl::pubConnectRequest() { 229 | const std::string subject{ 230 | SubjectUtility::join(discover_prefix_.value(), cluster_id_.value())}; 231 | 232 | const std::string connect_request_message = 233 | MessageUtility::createConnectRequestMessage(client_id_, heartbeat_inbox_); 234 | 235 | pubNatsStreamingMessage(subject, connect_response_inbox_, 236 | connect_request_message); 237 | } 238 | 239 | void ClientImpl::enqueuePendingRequest(const std::string &subject, 240 | const std::string &payload, 241 | PublishCallbacks &callbacks, 242 | const std::string &pub_ack_inbox) { 243 | PendingRequest pending_request{subject, payload, &callbacks}; 244 | pending_request_per_inbox_.emplace(pub_ack_inbox, std::move(pending_request)); 245 | } 246 | 247 | void ClientImpl::pubPubMsg(const std::string &subject, 248 | const std::string &payload, 249 | PublishCallbacks &callbacks, 250 | const std::string &pub_ack_inbox) { 251 | // TODO(talnordan): Consider moving the following logic to 252 | // `PubRequestHandler`. 253 | 254 | // TODO(talnordan): Consider making `PubRequest` use the RAII pattern, to make 255 | // sure that the timer is disabled whenever a request is removed from the map, 256 | // or if an exception is thrown. We might want the `PubRequest` isntance to 257 | // keep being created on the stack even after such modififcation. 258 | // TODO(talnordan): Can we create a timer on the stack rather than on the 259 | // heap? 260 | Event::TimerPtr timeout_timer = dispatcher_.createTimer( 261 | [this, pub_ack_inbox]() -> void { onTimeout(pub_ack_inbox); }); 262 | timeout_timer->enableTimer(op_timeout_); 263 | PubRequest pub_request(&callbacks, std::move(timeout_timer)); 264 | pub_request_per_inbox_.emplace(pub_ack_inbox, std::move(pub_request)); 265 | 266 | const std::string pub_subject{ 267 | SubjectUtility::join(pub_prefix_.value(), subject)}; 268 | 269 | const std::string guid = token_generator_.random(); 270 | const std::string pub_msg_message = 271 | MessageUtility::createPubMsgMessage(client_id_, guid, subject, payload); 272 | 273 | pubNatsStreamingMessage(pub_subject, pub_ack_inbox, pub_msg_message); 274 | } 275 | 276 | void ClientImpl::pong() { 277 | sendNatsMessage(MessageBuilder::createPongMessage()); 278 | } 279 | 280 | inline void ClientImpl::sendNatsMessage(const Message &message) { 281 | // TODO(talnordan): Manage hash key computation. 282 | const std::string hash_key; 283 | 284 | conn_pool_->makeRequest(hash_key, message); 285 | } 286 | 287 | inline void ClientImpl::pubNatsStreamingMessage(const std::string &subject, 288 | const std::string &reply_to, 289 | const std::string &message) { 290 | const Message pubMessage = 291 | MessageBuilder::createPubMessage(subject, reply_to, message); 292 | sendNatsMessage(pubMessage); 293 | } 294 | 295 | } // namespace Streaming 296 | } // namespace Nats 297 | } // namespace Envoy 298 | -------------------------------------------------------------------------------- /source/common/tcp/conn_pool_impl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "envoy/event/dispatcher.h" 6 | #include "envoy/network/connection.h" 7 | #include "envoy/tcp/conn_pool_nats.h" 8 | #include "envoy/thread_local/thread_local.h" 9 | #include "envoy/upstream/cluster_manager.h" 10 | #include "envoy/upstream/thread_local_cluster.h" 11 | #include "envoy/upstream/upstream.h" 12 | 13 | #include "common/buffer/buffer_impl.h" 14 | #include "common/common/assert.h" 15 | #include "common/network/filter_impl.h" 16 | #include "common/tcp/codec_impl.h" 17 | 18 | namespace Envoy { 19 | namespace Tcp { 20 | namespace ConnPoolNats { 21 | 22 | class ConfigImpl : public Config { 23 | public: 24 | bool disableOutlierEvents() const override { return false; } 25 | }; 26 | 27 | template 28 | class ClientImpl : public Client, 29 | public DecoderCallbacks, 30 | public Network::ConnectionCallbacks { 31 | public: 32 | static ClientPtr 33 | create(Upstream::HostConstSharedPtr host, Event::Dispatcher &dispatcher, 34 | EncoderPtr &&encoder, DecoderFactory &decoder_factory, 35 | PoolCallbacks &callbacks, const Config &config) { 36 | std::unique_ptr client(new ClientImpl( 37 | host, std::move(encoder), decoder_factory, callbacks, config)); 38 | client->connection_ = 39 | host->createConnection(dispatcher, nullptr).connection_; 40 | client->connection_->addConnectionCallbacks(*client); 41 | client->connection_->addReadFilter( 42 | Network::ReadFilterSharedPtr{new UpstreamReadFilter(*client)}); 43 | client->connection_->connect(); 44 | client->connection_->noDelay(true); 45 | return std::move(client); 46 | } 47 | 48 | ~ClientImpl() { 49 | ASSERT(connection_->state() == Network::Connection::State::Closed); 50 | host_->cluster().stats().upstream_cx_active_.dec(); 51 | host_->stats().cx_active_.dec(); 52 | } 53 | 54 | // Tcp::ConnPool::Client 55 | void 56 | addConnectionCallbacks(Network::ConnectionCallbacks &callbacks) override { 57 | connection_->addConnectionCallbacks(callbacks); 58 | } 59 | void close() override { 60 | connection_->close(Network::ConnectionCloseType::NoFlush); 61 | } 62 | void makeRequest(const T &request) override { 63 | ASSERT(connection_->state() == Network::Connection::State::Open); 64 | 65 | incRequestStats(); 66 | encoder_->encode(request, encoder_buffer_); 67 | connection_->write(encoder_buffer_, false); 68 | } 69 | void cancel() override { 70 | // If we get a cancellation, we just mark all pending request as canceled, 71 | // and then we drop all responses as they come through. There is no reason 72 | // to blow away the connection when the remote is already responding as fast 73 | // as possible. 74 | canceled_ = true; 75 | } 76 | 77 | private: 78 | struct UpstreamReadFilter : public Network::ReadFilterBaseImpl { 79 | UpstreamReadFilter(ClientImpl &parent) : parent_(parent) {} 80 | 81 | // Network::ReadFilter 82 | Network::FilterStatus onData(Buffer::Instance &data, bool) override { 83 | parent_.onData(data); 84 | return Network::FilterStatus::Continue; 85 | } 86 | 87 | ClientImpl &parent_; 88 | }; 89 | 90 | // TODO(talnordan): 91 | // The current implementation considers the number of TCP messages sent to be 92 | // the number of requests. Perhaps it would be more accurate to count the 93 | // number of HTTP requests? An example of a case in which it can make a 94 | // difference is whether `PING` messages and `PONG` messages at the NATS layer 95 | // should be counted as requests. 96 | void incRequestStats() { 97 | host_->cluster().stats().upstream_rq_total_.inc(); 98 | host_->stats().rq_total_.inc(); 99 | } 100 | 101 | ClientImpl(Upstream::HostConstSharedPtr host, EncoderPtr &&encoder, 102 | DecoderFactory &decoder_factory, PoolCallbacks &callbacks, 103 | const Config &config) 104 | : host_(host), encoder_(std::move(encoder)), 105 | decoder_(decoder_factory.create(*this)), callbacks_(callbacks), 106 | config_(config) { 107 | host->cluster().stats().upstream_cx_total_.inc(); 108 | host->cluster().stats().upstream_cx_active_.inc(); 109 | host->stats().cx_total_.inc(); 110 | host->stats().cx_active_.inc(); 111 | } 112 | void onData(Buffer::Instance &data) { 113 | try { 114 | decoder_->decode(data); 115 | } catch (ProtocolError &) { 116 | putOutlierEvent(Upstream::Outlier::Result::REQUEST_FAILED); 117 | host_->cluster().stats().upstream_cx_protocol_error_.inc(); 118 | connection_->close(Network::ConnectionCloseType::NoFlush); 119 | } 120 | } 121 | void putOutlierEvent(Upstream::Outlier::Result result) { 122 | if (!config_.disableOutlierEvents()) { 123 | host_->outlierDetector().putResult(result); 124 | } 125 | } 126 | 127 | // Tcp::DecoderCallbacks 128 | void onValue(MessagePtr &&value) override { 129 | if (!canceled_) { 130 | callbacks_.onResponse(std::move(value)); 131 | } 132 | 133 | // TODO(talnordan): How should we count these? 134 | // else { 135 | // host_->cluster().stats().upstream_rq_cancelled_.inc(); 136 | // } 137 | 138 | putOutlierEvent(Upstream::Outlier::Result::SUCCESS); 139 | } 140 | 141 | // Network::ConnectionCallbacks 142 | void onEvent(Network::ConnectionEvent event) override { 143 | if (event == Network::ConnectionEvent::RemoteClose || 144 | event == Network::ConnectionEvent::LocalClose) { 145 | // TODO(talnordan): How should we count these? 146 | // if (!pending_requests_.empty()) { 147 | // host_->cluster().stats().upstream_cx_destroy_with_active_rq_.inc(); 148 | if (event == Network::ConnectionEvent::RemoteClose) { 149 | putOutlierEvent(Upstream::Outlier::Result::SERVER_FAILURE); 150 | // host_->cluster() 151 | // .stats() 152 | // .upstream_cx_destroy_remote_with_active_rq_.inc(); 153 | } 154 | // if (event == Network::ConnectionEvent::LocalClose) { 155 | // host_->cluster() 156 | // .stats() 157 | // .upstream_cx_destroy_local_with_active_rq_.inc(); 158 | // } 159 | // } 160 | 161 | // TODO(talnordan): How should we count these? 162 | // if (canceled_) { 163 | // while (!pending_requests_.empty()) { 164 | // host_->cluster().stats().upstream_rq_cancelled_.inc(); 165 | // } 166 | // } 167 | 168 | if (!canceled_) { 169 | callbacks_.onClose(); 170 | } 171 | 172 | } else if (event == Network::ConnectionEvent::Connected) { 173 | connected_ = true; 174 | } 175 | 176 | if (event == Network::ConnectionEvent::RemoteClose && !connected_) { 177 | host_->cluster().stats().upstream_cx_connect_fail_.inc(); 178 | host_->stats().cx_connect_fail_.inc(); 179 | } 180 | } 181 | void onAboveWriteBufferHighWatermark() override {} 182 | void onBelowWriteBufferLowWatermark() override {} 183 | 184 | Upstream::HostConstSharedPtr host_; 185 | Network::ClientConnectionPtr connection_; 186 | EncoderPtr encoder_; 187 | Buffer::OwnedImpl encoder_buffer_; 188 | DecoderPtr decoder_; 189 | PoolCallbacks &callbacks_; 190 | const Config &config_; 191 | bool connected_{}; 192 | bool canceled_{}; 193 | }; 194 | 195 | template 196 | class ClientFactoryImpl : public ClientFactory { 197 | static_assert(std::is_base_of, E>::value, 198 | "Encoder should be a base of E"); 199 | static_assert(std::is_base_of::value, 200 | "Decoder should be a base of D"); 201 | 202 | public: 203 | // Tcp::ConnPool::ClientFactoryImpl 204 | ClientPtr create(Upstream::HostConstSharedPtr host, 205 | Event::Dispatcher &dispatcher, 206 | PoolCallbacks &callbacks, 207 | const Config &config) override { 208 | return ClientImpl::create(host, dispatcher, EncoderPtr{new E()}, 209 | decoder_factory_, callbacks, config); 210 | } 211 | 212 | static ClientFactoryImpl instance_; 213 | 214 | private: 215 | DecoderFactoryImpl decoder_factory_; 216 | }; 217 | 218 | template 219 | ClientFactoryImpl ClientFactoryImpl::instance_; 220 | 221 | template class InstanceImpl : public Instance { 222 | public: 223 | InstanceImpl(const std::string &cluster_name, Upstream::ClusterManager &cm, 224 | ClientFactory &client_factory, Event::Dispatcher &dispatcher) 225 | : cm_(cm), client_factory_(client_factory) { 226 | thread_local_pool_ = 227 | std::make_shared(*this, dispatcher, cluster_name); 228 | } 229 | 230 | // Tcp::ConnPool::Instance 231 | void setPoolCallbacks(PoolCallbacks &callbacks) override { 232 | RELEASE_ASSERT(callbacks_ == nullptr, ""); 233 | callbacks_ = &callbacks; 234 | } 235 | void makeRequest(const std::string &hash_key, const T &request) override { 236 | thread_local_pool_->makeRequest(hash_key, request); 237 | } 238 | 239 | private: 240 | struct ThreadLocalPool; 241 | 242 | struct ThreadLocalActiveClient : public Network::ConnectionCallbacks { 243 | ThreadLocalActiveClient(ThreadLocalPool &parent) : parent_(parent) {} 244 | 245 | // Network::ConnectionCallbacks 246 | void onEvent(Network::ConnectionEvent event) override { 247 | if (event == Network::ConnectionEvent::RemoteClose || 248 | event == Network::ConnectionEvent::LocalClose) { 249 | ASSERT(parent_.maybe_client_); 250 | ThreadLocalActiveClientPtr &client_to_delete = parent_.maybe_client_; 251 | parent_.dispatcher_.deferredDelete( 252 | std::move(client_to_delete->client_)); 253 | parent_.maybe_client_ = nullptr; 254 | } 255 | } 256 | void onAboveWriteBufferHighWatermark() override {} 257 | void onBelowWriteBufferLowWatermark() override {} 258 | 259 | ThreadLocalPool &parent_; 260 | ClientPtr client_; 261 | }; 262 | 263 | typedef std::unique_ptr ThreadLocalActiveClientPtr; 264 | 265 | struct ThreadLocalPool : public ThreadLocal::ThreadLocalObject { 266 | ThreadLocalPool(InstanceImpl &parent, Event::Dispatcher &dispatcher, 267 | const std::string &cluster_name) 268 | : parent_(parent), dispatcher_(dispatcher), 269 | cluster_name_(cluster_name) {} 270 | ~ThreadLocalPool() { 271 | if (maybe_client_) { 272 | maybe_client_->client_->close(); 273 | } 274 | } 275 | void makeRequest(const std::string &hash_key, const T &request) { 276 | if (!maybe_client_) { 277 | auto *cluster = parent_.cm_.get(cluster_name_); 278 | if (!cluster) { 279 | // TODO(talnordan): 280 | // parent_.callbacks_->onFailure("no cluster"); 281 | return; 282 | } 283 | LbContextImpl lb_context(hash_key); 284 | Upstream::HostConstSharedPtr host = 285 | cluster->loadBalancer().chooseHost(&lb_context); 286 | if (!host) { 287 | // TODO(talnordan): 288 | // parent_.callbacks_->onFailure("no host"); 289 | return; 290 | } 291 | 292 | ThreadLocalActiveClientPtr client{new ThreadLocalActiveClient(*this)}; 293 | RELEASE_ASSERT(parent_.callbacks_ != nullptr, ""); 294 | client->client_ = parent_.client_factory_.create( 295 | host, dispatcher_, *parent_.callbacks_, parent_.config_); 296 | client->client_->addConnectionCallbacks(*client); 297 | 298 | maybe_client_ = std::move(client); 299 | } 300 | 301 | ThreadLocalActiveClientPtr &client = maybe_client_; 302 | 303 | client->client_->makeRequest(request); 304 | } 305 | 306 | InstanceImpl &parent_; 307 | Event::Dispatcher &dispatcher_; 308 | std::string cluster_name_; 309 | ThreadLocalActiveClientPtr maybe_client_; 310 | Common::CallbackHandle *local_host_set_member_update_cb_handle_; 311 | }; 312 | 313 | struct LbContextImpl : public Upstream::LoadBalancerContext { 314 | LbContextImpl(const std::string &hash_key) 315 | : hash_key_(std::hash()(hash_key)) {} 316 | // TODO(danielhochman): convert to HashUtil::xxHash64 when we have a 317 | // migration strategy. Upstream::LoadBalancerContext 318 | absl::optional computeHashKey() override { return hash_key_; } 319 | const Router::MetadataMatchCriteria *metadataMatchCriteria() override { 320 | return nullptr; 321 | } 322 | const Network::Connection *downstreamConnection() const override { 323 | return nullptr; 324 | } 325 | 326 | const Http::HeaderMap *downstreamHeaders() const override { 327 | return nullptr; 328 | } 329 | 330 | const absl::optional hash_key_; 331 | }; 332 | 333 | Upstream::ClusterManager &cm_; 334 | ClientFactory &client_factory_; 335 | 336 | // TODO(talnordan): This member can be owned directly rather than using a 337 | // `shared_ptr<>`. 338 | std::shared_ptr thread_local_pool_; 339 | 340 | ConfigImpl config_; 341 | PoolCallbacks *callbacks_{}; 342 | }; 343 | 344 | } // namespace ConnPoolNats 345 | } // namespace Tcp 346 | } // namespace Envoy 347 | -------------------------------------------------------------------------------- /test/common/tcp/conn_pool_impl_test.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "envoy/tcp/codec.h" 5 | 6 | #include "common/network/utility.h" 7 | #include "common/tcp/conn_pool_impl.h" 8 | #include "common/upstream/upstream_impl.h" 9 | 10 | #include "test/mocks/event/mocks.h" 11 | #include "test/mocks/network/mocks.h" 12 | #include "test/mocks/tcp/mocks_nats.h" 13 | #include "test/mocks/thread_local/mocks.h" 14 | #include "test/mocks/upstream/mocks.h" 15 | #include "test/test_common/printers.h" 16 | 17 | #include "gmock/gmock.h" 18 | #include "gtest/gtest.h" 19 | 20 | using testing::_; 21 | using testing::Eq; 22 | using testing::InSequence; 23 | using testing::Invoke; 24 | using testing::Ref; 25 | using testing::Return; 26 | using testing::ReturnRef; 27 | using testing::SaveArg; 28 | 29 | namespace Envoy { 30 | namespace Tcp { 31 | namespace ConnPoolNats { 32 | 33 | using T = std::string; 34 | using TPtr = MessagePtr; 35 | 36 | class TcpClientImplTest : public testing::Test, public DecoderFactory { 37 | public: 38 | // Tcp::DecoderFactory 39 | DecoderPtr create(DecoderCallbacks &callbacks) override { 40 | callbacks_ = &callbacks; 41 | return DecoderPtr{decoder_}; 42 | } 43 | 44 | ~TcpClientImplTest() { 45 | client_.reset(); 46 | 47 | // Make sure all gauges are 0. 48 | for (const Stats::GaugeSharedPtr &gauge : 49 | host_->cluster_.stats_store_.gauges()) { 50 | EXPECT_EQ(0U, gauge->value()); 51 | } 52 | for (const Stats::GaugeSharedPtr &gauge : host_->stats_store_.gauges()) { 53 | EXPECT_EQ(0U, gauge->value()); 54 | } 55 | } 56 | 57 | void setup() { 58 | config_.reset(new ConfigImpl()); 59 | finishSetup(); 60 | } 61 | 62 | void setup(std::unique_ptr &&config) { 63 | config_ = std::move(config); 64 | finishSetup(); 65 | } 66 | 67 | void finishSetup() { 68 | upstream_connection_ = new NiceMock(); 69 | Upstream::MockHost::MockCreateConnectionData conn_info; 70 | conn_info.connection_ = upstream_connection_; 71 | EXPECT_CALL(*host_, createConnection_(_, _)).WillOnce(Return(conn_info)); 72 | EXPECT_CALL(*upstream_connection_, addReadFilter(_)) 73 | .WillOnce(SaveArg<0>(&upstream_read_filter_)); 74 | EXPECT_CALL(*upstream_connection_, connect()); 75 | EXPECT_CALL(*upstream_connection_, noDelay(true)); 76 | 77 | client_ = ClientImpl::create(host_, dispatcher_, EncoderPtr{encoder_}, 78 | *this, pool_callbacks_, *config_); 79 | EXPECT_EQ(1UL, host_->cluster_.stats_.upstream_cx_total_.value()); 80 | EXPECT_EQ(1UL, host_->stats_.cx_total_.value()); 81 | } 82 | 83 | void onConnected() { 84 | upstream_connection_->raiseEvent(Network::ConnectionEvent::Connected); 85 | } 86 | 87 | const std::string cluster_name_{"foo"}; 88 | std::shared_ptr host_{new NiceMock()}; 89 | Event::MockDispatcher dispatcher_; 90 | MockEncoder *encoder_{new MockEncoder()}; 91 | MockDecoder *decoder_{new MockDecoder()}; 92 | DecoderCallbacks *callbacks_{}; 93 | MockPoolCallbacks pool_callbacks_; 94 | NiceMock *upstream_connection_{}; 95 | Network::ReadFilterSharedPtr upstream_read_filter_; 96 | std::unique_ptr config_; 97 | ClientPtr client_; 98 | }; 99 | 100 | TEST_F(TcpClientImplTest, Basic) { 101 | InSequence s; 102 | 103 | setup(); 104 | 105 | T request1; 106 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 107 | client_->makeRequest(request1); 108 | 109 | onConnected(); 110 | 111 | T request2; 112 | EXPECT_CALL(*encoder_, encode(Ref(request2), _)); 113 | client_->makeRequest(request2); 114 | 115 | EXPECT_EQ(2UL, host_->cluster_.stats_.upstream_rq_total_.value()); 116 | EXPECT_EQ(2UL, host_->stats_.rq_total_.value()); 117 | 118 | // TODO(talnordan): What should be counted as an active request? 119 | EXPECT_EQ(0UL, host_->cluster_.stats_.upstream_rq_active_.value()); 120 | EXPECT_EQ(0UL, host_->stats_.rq_active_.value()); 121 | 122 | Buffer::OwnedImpl fake_data; 123 | EXPECT_CALL(*decoder_, decode(Ref(fake_data))) 124 | .WillOnce(Invoke([&](Buffer::Instance &) -> void { 125 | InSequence s; 126 | TPtr response1(new T()); 127 | EXPECT_CALL(pool_callbacks_, onResponse_(Ref(response1))); 128 | EXPECT_CALL(host_->outlier_detector_, 129 | putResult(Upstream::Outlier::Result::SUCCESS)); 130 | callbacks_->onValue(std::move(response1)); 131 | 132 | TPtr response2(new T()); 133 | EXPECT_CALL(pool_callbacks_, onResponse_(Ref(response2))); 134 | EXPECT_CALL(host_->outlier_detector_, 135 | putResult(Upstream::Outlier::Result::SUCCESS)); 136 | callbacks_->onValue(std::move(response2)); 137 | })); 138 | upstream_read_filter_->onData(fake_data, false); 139 | 140 | EXPECT_CALL(*upstream_connection_, 141 | close(Network::ConnectionCloseType::NoFlush)); 142 | EXPECT_CALL(pool_callbacks_, onClose()); 143 | client_->close(); 144 | } 145 | 146 | TEST_F(TcpClientImplTest, Cancel) { 147 | InSequence s; 148 | 149 | setup(); 150 | 151 | T request1; 152 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 153 | client_->makeRequest(request1); 154 | 155 | onConnected(); 156 | 157 | T request2; 158 | EXPECT_CALL(*encoder_, encode(Ref(request2), _)); 159 | client_->makeRequest(request2); 160 | 161 | client_->cancel(); 162 | 163 | Buffer::OwnedImpl fake_data; 164 | EXPECT_CALL(*decoder_, decode(Ref(fake_data))) 165 | .WillOnce(Invoke([&](Buffer::Instance &) -> void { 166 | InSequence s; 167 | 168 | TPtr response1(new T()); 169 | EXPECT_CALL(pool_callbacks_, onResponse_(_)).Times(0); 170 | EXPECT_CALL(host_->outlier_detector_, 171 | putResult(Upstream::Outlier::Result::SUCCESS)); 172 | callbacks_->onValue(std::move(response1)); 173 | 174 | TPtr response2(new T()); 175 | EXPECT_CALL(pool_callbacks_, onResponse_(_)).Times(0); 176 | EXPECT_CALL(host_->outlier_detector_, 177 | putResult(Upstream::Outlier::Result::SUCCESS)); 178 | callbacks_->onValue(std::move(response2)); 179 | })); 180 | upstream_read_filter_->onData(fake_data, false); 181 | 182 | EXPECT_CALL(*upstream_connection_, 183 | close(Network::ConnectionCloseType::NoFlush)); 184 | client_->close(); 185 | 186 | // TODO(talnordan): What should be counted as a canceled request? 187 | EXPECT_EQ(0UL, host_->cluster_.stats_.upstream_rq_cancelled_.value()); 188 | } 189 | 190 | TEST_F(TcpClientImplTest, FailAll) { 191 | InSequence s; 192 | 193 | setup(); 194 | 195 | NiceMock connection_callbacks; 196 | client_->addConnectionCallbacks(connection_callbacks); 197 | 198 | T request1; 199 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 200 | client_->makeRequest(request1); 201 | 202 | onConnected(); 203 | 204 | EXPECT_CALL(host_->outlier_detector_, 205 | putResult(Upstream::Outlier::Result::SERVER_FAILURE)); 206 | EXPECT_CALL(pool_callbacks_, onClose()); 207 | EXPECT_CALL(connection_callbacks, 208 | onEvent(Network::ConnectionEvent::RemoteClose)); 209 | upstream_connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); 210 | 211 | // TODO(talnordan): What should be counted as an active request? 212 | EXPECT_EQ(0UL, 213 | host_->cluster_.stats_.upstream_cx_destroy_with_active_rq_.value()); 214 | EXPECT_EQ(0UL, host_->cluster_.stats_ 215 | .upstream_cx_destroy_remote_with_active_rq_.value()); 216 | } 217 | 218 | TEST_F(TcpClientImplTest, FailAllWithCancel) { 219 | InSequence s; 220 | 221 | setup(); 222 | 223 | NiceMock connection_callbacks; 224 | client_->addConnectionCallbacks(connection_callbacks); 225 | 226 | T request1; 227 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 228 | client_->makeRequest(request1); 229 | 230 | onConnected(); 231 | client_->cancel(); 232 | 233 | EXPECT_CALL(pool_callbacks_, onClose()).Times(0); 234 | EXPECT_CALL(connection_callbacks, 235 | onEvent(Network::ConnectionEvent::LocalClose)); 236 | upstream_connection_->raiseEvent(Network::ConnectionEvent::LocalClose); 237 | 238 | // TODO(talnordan): What should be counted as an active request or a canceled 239 | // one? 240 | EXPECT_EQ(0UL, 241 | host_->cluster_.stats_.upstream_cx_destroy_with_active_rq_.value()); 242 | EXPECT_EQ( 243 | 0UL, 244 | host_->cluster_.stats_.upstream_cx_destroy_local_with_active_rq_.value()); 245 | EXPECT_EQ(0UL, host_->cluster_.stats_.upstream_rq_cancelled_.value()); 246 | } 247 | 248 | TEST_F(TcpClientImplTest, ProtocolError) { 249 | InSequence s; 250 | 251 | setup(); 252 | 253 | T request1; 254 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 255 | client_->makeRequest(request1); 256 | 257 | onConnected(); 258 | 259 | Buffer::OwnedImpl fake_data; 260 | EXPECT_CALL(*decoder_, decode(Ref(fake_data))) 261 | .WillOnce(Invoke( 262 | [&](Buffer::Instance &) -> void { throw ProtocolError("error"); })); 263 | EXPECT_CALL(host_->outlier_detector_, 264 | putResult(Upstream::Outlier::Result::REQUEST_FAILED)); 265 | EXPECT_CALL(*upstream_connection_, 266 | close(Network::ConnectionCloseType::NoFlush)); 267 | 268 | EXPECT_CALL(pool_callbacks_, onClose()); 269 | 270 | upstream_read_filter_->onData(fake_data, false); 271 | 272 | EXPECT_EQ(1UL, host_->cluster_.stats_.upstream_cx_protocol_error_.value()); 273 | } 274 | 275 | TEST_F(TcpClientImplTest, ConnectFail) { 276 | InSequence s; 277 | 278 | setup(); 279 | 280 | T request1; 281 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 282 | client_->makeRequest(request1); 283 | 284 | EXPECT_CALL(host_->outlier_detector_, 285 | putResult(Upstream::Outlier::Result::SERVER_FAILURE)); 286 | 287 | EXPECT_CALL(pool_callbacks_, onClose()); 288 | 289 | upstream_connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); 290 | 291 | EXPECT_EQ(1UL, host_->cluster_.stats_.upstream_cx_connect_fail_.value()); 292 | EXPECT_EQ(1UL, host_->stats_.cx_connect_fail_.value()); 293 | } 294 | 295 | class ConfigOutlierDisabled : public Config { 296 | bool disableOutlierEvents() const override { return true; } 297 | }; 298 | 299 | TEST_F(TcpClientImplTest, OutlierDisabled) { 300 | InSequence s; 301 | 302 | setup(std::make_unique()); 303 | 304 | T request1; 305 | EXPECT_CALL(*encoder_, encode(Ref(request1), _)); 306 | client_->makeRequest(request1); 307 | 308 | EXPECT_CALL(host_->outlier_detector_, putResult(_)).Times(0); 309 | 310 | EXPECT_CALL(pool_callbacks_, onClose()); 311 | 312 | upstream_connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); 313 | 314 | EXPECT_EQ(1UL, host_->cluster_.stats_.upstream_cx_connect_fail_.value()); 315 | EXPECT_EQ(1UL, host_->stats_.cx_connect_fail_.value()); 316 | } 317 | 318 | TEST(TcpClientFactoryImplTest, Basic) { 319 | ClientFactoryImpl factory; 320 | Upstream::MockHost::MockCreateConnectionData conn_info; 321 | conn_info.connection_ = new NiceMock(); 322 | std::shared_ptr host(new NiceMock()); 323 | EXPECT_CALL(*host, createConnection_(_, _)).WillOnce(Return(conn_info)); 324 | NiceMock dispatcher; 325 | MockPoolCallbacks callbacks; 326 | ConfigImpl config; 327 | ClientPtr client = factory.create(host, dispatcher, callbacks, config); 328 | EXPECT_CALL(callbacks, onClose()); 329 | client->close(); 330 | } 331 | 332 | class TcpConnPoolImplTest : public testing::Test, public ClientFactory { 333 | public: 334 | TcpConnPoolImplTest() { 335 | conn_pool_.reset(new InstanceImpl(cluster_name_, cm_, *this, 336 | dispatcher_)); 337 | conn_pool_->setPoolCallbacks(callbacks_); 338 | } 339 | 340 | // Tcp::ConnPoolNats::ClientFactory 341 | // TODO(talnordan): Use `MockClientFactory` instead of having this class 342 | // implemnting `ClientFactory. 343 | ClientPtr create(Upstream::HostConstSharedPtr host, Event::Dispatcher &, 344 | PoolCallbacks &, const Config &) override { 345 | return ClientPtr{create_(host)}; 346 | } 347 | 348 | MOCK_METHOD1(create_, Client *(Upstream::HostConstSharedPtr host)); 349 | 350 | const std::string cluster_name_{"foo"}; 351 | NiceMock cm_; 352 | MockPoolCallbacks callbacks_; 353 | NiceMock dispatcher_; 354 | InstancePtr conn_pool_; 355 | }; 356 | 357 | TEST_F(TcpConnPoolImplTest, Basic) { 358 | InSequence s; 359 | 360 | T value; 361 | MockClient *client = new NiceMock(); 362 | 363 | EXPECT_CALL(cm_.thread_local_cluster_.lb_, chooseHost(_)) 364 | .WillOnce(Invoke([&](Upstream::LoadBalancerContext *context) 365 | -> Upstream::HostConstSharedPtr { 366 | EXPECT_EQ(context->computeHashKey().value(), 367 | std::hash()("foo")); 368 | return cm_.thread_local_cluster_.lb_.host_; 369 | })); 370 | EXPECT_CALL(*this, create_(_)).WillOnce(Return(client)); 371 | EXPECT_CALL(*client, makeRequest(Ref(value))).Times(1); 372 | conn_pool_->makeRequest("foo", value); 373 | 374 | // TODO(talnordan): Should `onClose()` be invoked? 375 | // EXPECT_CALL(callbacks_, onClose()); 376 | EXPECT_CALL(*client, close()); 377 | conn_pool_ = {}; 378 | }; 379 | 380 | TEST_F(TcpConnPoolImplTest, DeleteFollowedByClusterUpdateCallback) { 381 | conn_pool_.reset(); 382 | 383 | std::shared_ptr host(new Upstream::MockHost()); 384 | cm_.thread_local_cluster_.cluster_.prioritySet() 385 | .getMockHostSet(0) 386 | ->runCallbacks({}, {host}); 387 | } 388 | 389 | TEST_F(TcpConnPoolImplTest, NoHost) { 390 | InSequence s; 391 | 392 | T value; 393 | EXPECT_CALL(cm_.thread_local_cluster_.lb_, chooseHost(_)) 394 | .WillOnce(Return(nullptr)); 395 | conn_pool_->makeRequest("foo", value); 396 | 397 | conn_pool_ = {}; 398 | } 399 | 400 | TEST_F(TcpConnPoolImplTest, RemoteClose) { 401 | InSequence s; 402 | 403 | T value; 404 | MockClient *client = new NiceMock(); 405 | 406 | EXPECT_CALL(cm_.thread_local_cluster_.lb_, chooseHost(_)); 407 | EXPECT_CALL(*this, create_(_)).WillOnce(Return(client)); 408 | EXPECT_CALL(*client, makeRequest(Ref(value))).Times(1); 409 | conn_pool_->makeRequest("foo", value); 410 | 411 | EXPECT_CALL(dispatcher_, deferredDelete_(_)); 412 | client->raiseEvent(Network::ConnectionEvent::RemoteClose); 413 | 414 | conn_pool_ = {}; 415 | } 416 | 417 | } // namespace ConnPoolNats 418 | } // namespace Tcp 419 | } // namespace Envoy 420 | --------------------------------------------------------------------------------