From c226497f6a8f9a82c92d4cf154f19386b55b4a96 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 15 Jun 2020 17:31:51 +0200 Subject: [PATCH 01/48] Save state on origin timeing Signed-off-by: Otto van der Schaaf --- source/client/benchmark_client_impl.cc | 15 ++++++++++---- source/client/benchmark_client_impl.h | 7 +++++-- source/client/factories_impl.cc | 5 +++-- source/client/stream_decoder.cc | 18 +++++++++++++++++ source/client/stream_decoder.h | 7 ++++++- source/server/http_test_server_filter.cc | 8 ++++++++ source/server/http_test_server_filter.h | 3 +++ test/stream_decoder_test.cc | 25 +++++++++++++++--------- 8 files changed, 70 insertions(+), 18 deletions(-) diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 37902e58f..c95061295 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -50,14 +50,17 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( Envoy::Api::Api& api, Envoy::Event::Dispatcher& dispatcher, Envoy::Stats::Scope& scope, StatisticPtr&& connect_statistic, StatisticPtr&& response_statistic, StatisticPtr&& response_header_size_statistic, StatisticPtr&& response_body_size_statistic, - bool use_h2, Envoy::Upstream::ClusterManagerPtr& cluster_manager, + StatisticPtr&& origin_latency_statistic, StatisticPtr&& origin_receipt_statistic, bool use_h2, + Envoy::Upstream::ClusterManagerPtr& cluster_manager, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, const bool provide_resource_backpressure) : api_(api), dispatcher_(dispatcher), scope_(scope.createScope("benchmark.")), connect_statistic_(std::move(connect_statistic)), response_statistic_(std::move(response_statistic)), response_header_size_statistic_(std::move(response_header_size_statistic)), - response_body_size_statistic_(std::move(response_body_size_statistic)), use_h2_(use_h2), + response_body_size_statistic_(std::move(response_body_size_statistic)), + origin_latency_statistic_(std::move(origin_latency_statistic)), + origin_receipt_statistic_(std::move(origin_receipt_statistic)), use_h2_(use_h2), benchmark_client_stats_({ALL_BENCHMARK_CLIENT_STATS(POOL_COUNTER(*scope_))}), cluster_manager_(cluster_manager), http_tracer_(http_tracer), cluster_name_(std::string(cluster_name)), request_generator_(std::move(request_generator)), @@ -66,6 +69,8 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( response_statistic_->setId("benchmark_http_client.request_to_response"); response_header_size_statistic_->setId("benchmark_http_client.response_header_size"); response_body_size_statistic_->setId("benchmark_http_client.response_body_size"); + origin_latency_statistic_->setId("benchmark_http_client.origin_latency"); + origin_receipt_statistic_->setId("benchmark_http_client.origin_delta"); } void BenchmarkClientHttpImpl::terminate() { @@ -82,6 +87,8 @@ StatisticPtrMap BenchmarkClientHttpImpl::statistics() const { statistics[response_statistic_->id()] = response_statistic_.get(); statistics[response_header_size_statistic_->id()] = response_header_size_statistic_.get(); statistics[response_body_size_statistic_->id()] = response_body_size_statistic_.get(); + statistics[origin_latency_statistic_->id()] = origin_latency_statistic_.get(); + statistics[origin_receipt_statistic_->id()] = origin_receipt_statistic_.get(); return statistics; }; @@ -120,8 +127,8 @@ bool BenchmarkClientHttpImpl::tryStartRequest(CompletionCallback caller_completi auto stream_decoder = new StreamDecoder( dispatcher_, api_.timeSource(), *this, std::move(caller_completion_callback), *connect_statistic_, *response_statistic_, *response_header_size_statistic_, - *response_body_size_statistic_, request->header(), shouldMeasureLatencies(), content_length, - generator_, http_tracer_); + *response_body_size_statistic_, *origin_latency_statistic_, *origin_receipt_statistic_, + request->header(), shouldMeasureLatencies(), content_length, generator_, http_tracer_); requests_initiated_++; pool_ptr->newStream(*stream_decoder, *stream_decoder); return true; diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index a40e90a19..4ad30d03a 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -75,8 +75,9 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, Envoy::Stats::Scope& scope, StatisticPtr&& connect_statistic, StatisticPtr&& response_statistic, StatisticPtr&& response_header_size_statistic, - StatisticPtr&& response_body_size_statistic, bool use_h2, - Envoy::Upstream::ClusterManagerPtr& cluster_manager, + StatisticPtr&& response_body_size_statistic, + StatisticPtr&& origin_latency_statistic, StatisticPtr&& origin_receipt_statistic, + bool use_h2, Envoy::Upstream::ClusterManagerPtr& cluster_manager, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, const bool provide_resource_backpressure); @@ -122,6 +123,8 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, StatisticPtr response_statistic_; StatisticPtr response_header_size_statistic_; StatisticPtr response_body_size_statistic_; + StatisticPtr origin_latency_statistic_; + StatisticPtr origin_receipt_statistic_; const bool use_h2_; std::chrono::seconds timeout_{5s}; uint32_t connection_limit_{1}; diff --git a/source/client/factories_impl.cc b/source/client/factories_impl.cc index e65111f01..c19cdbc79 100644 --- a/source/client/factories_impl.cc +++ b/source/client/factories_impl.cc @@ -39,8 +39,9 @@ BenchmarkClientPtr BenchmarkClientFactoryImpl::create( // statistics. auto benchmark_client = std::make_unique( api, dispatcher, scope, statistic_factory.create(), statistic_factory.create(), - std::make_unique(), std::make_unique(), options_.h2(), - cluster_manager, http_tracer, cluster_name, request_generator.get(), !options_.openLoop()); + std::make_unique(), std::make_unique(), + statistic_factory.create(), statistic_factory.create(), options_.h2(), cluster_manager, + http_tracer, cluster_name, request_generator.get(), !options_.openLoop()); auto request_options = options_.toCommandLineOptions()->request_options(); benchmark_client->setConnectionLimit(options_.connections()); benchmark_client->setMaxPendingRequests(options_.maxPendingRequests()); diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index afdfd367d..d9124d07a 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -19,6 +19,24 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b response_header_sizes_statistic_.addValue(response_headers_->byteSize()); const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); + const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-origin-timings"); + const auto* timing_header = response_headers_->get(timing_header_name); + if (timing_header != nullptr) { + auto timing_value = timing_header->value().getStringView(); + std::vector split_result = absl::StrSplit(timing_value, ","); + if (split_result.size() == 2) { + int64_t origin_start, origin_delta; + bool ok = absl::SimpleAtoi(split_result[0], &origin_start) && + absl::SimpleAtoi(split_result[1], &origin_delta); + if (ok) { + origin_receipt_statistic_.addValue(origin_start - request_start_.time_since_epoch().count()); + origin_latency_statistic_.addValue(origin_delta); + } else { + // TODO(oschaaf): dispatch warning. watch out for high frequency logging. + } + } + } + if (complete_) { onComplete(true); } diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index 869a8d57e..17ee97852 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -40,7 +40,8 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, StreamDecoderCompletionCallback& decoder_completion_callback, OperationCallback caller_completion_callback, Statistic& connect_statistic, Statistic& latency_statistic, Statistic& response_header_sizes_statistic, - Statistic& response_body_sizes_statistic, HeaderMapPtr request_headers, + Statistic& response_body_sizes_statistic, Statistic& origin_latency_statistic, + Statistic& origin_receipt_statistic, HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size, Envoy::Runtime::RandomGenerator& random_generator, Envoy::Tracing::HttpTracerSharedPtr& http_tracer) @@ -50,6 +51,8 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, connect_statistic_(connect_statistic), latency_statistic_(latency_statistic), response_header_sizes_statistic_(response_header_sizes_statistic), response_body_sizes_statistic_(response_body_sizes_statistic), + origin_latency_statistic_(origin_latency_statistic), + origin_receipt_statistic_(origin_receipt_statistic), request_headers_(std::move(request_headers)), connect_start_(time_source_.monotonicTime()), complete_(false), measure_latencies_(measure_latencies), request_body_size_(request_body_size), stream_info_(time_source_), @@ -100,6 +103,8 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, Statistic& latency_statistic_; Statistic& response_header_sizes_statistic_; Statistic& response_body_sizes_statistic_; + Statistic& origin_latency_statistic_; + Statistic& origin_receipt_statistic_; HeaderMapPtr request_headers_; Envoy::Http::ResponseHeaderMapPtr response_headers_; Envoy::Http::ResponseTrailerMapPtr trailer_headers_; diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index b43c413ed..b03d54f79 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -10,6 +10,7 @@ #include "api/server/response_options.pb.validate.h" #include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" namespace Nighthawk { namespace Server { @@ -63,6 +64,11 @@ void HttpTestServerDecoderFilter::sendReply() { static_cast(200), response_body, [this](Envoy::Http::ResponseHeaderMap& direct_response_headers) { applyConfigToResponseHeaders(direct_response_headers, base_config_); + const auto timing_header = Envoy::Http::LowerCaseString("x-nh-origin-timings"); + auto delta = time_source_->monotonicTime() - request_time_; + std::string timing_value = + absl::StrCat(request_time_.time_since_epoch().count(), ",", delta.count()); + direct_response_headers.appendCopy(timing_header, timing_value); }, absl::nullopt, ""); } else { @@ -78,6 +84,7 @@ HttpTestServerDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& header bool end_stream) { // TODO(oschaaf): Add functionality to clear fields base_config_ = config_->server_config(); + request_time_ = time_source_->monotonicTime(); const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); if (request_config_header) { mergeJsonConfig(request_config_header->value().getStringView(), base_config_, error_message_); @@ -109,6 +116,7 @@ HttpTestServerDecoderFilter::decodeTrailers(Envoy::Http::RequestTrailerMap&) { void HttpTestServerDecoderFilter::setDecoderFilterCallbacks( Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { decoder_callbacks_ = &callbacks; + time_source_ = &callbacks.dispatcher().timeSource(); } } // namespace Server diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index f7ba094a2..cc9d3fdc2 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -2,6 +2,7 @@ #include +#include "envoy/common/time.h" #include "envoy/server/filter_config.h" #include "api/server/response_options.pb.h" @@ -74,6 +75,8 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { nighthawk::server::ResponseOptions base_config_; absl::optional error_message_; absl::optional request_headers_dump_; + Envoy::TimeSource* time_source_{nullptr}; + Envoy::MonotonicTime request_time_; }; } // namespace Server diff --git a/test/stream_decoder_test.cc b/test/stream_decoder_test.cc index 3cba8b2c7..fa35467dd 100644 --- a/test/stream_decoder_test.cc +++ b/test/stream_decoder_test.cc @@ -46,6 +46,8 @@ class StreamDecoderTest : public Test, public StreamDecoderCompletionCallback { StreamingStatistic latency_statistic_; StreamingStatistic response_header_size_statistic_; StreamingStatistic response_body_size_statistic_; + StreamingStatistic origin_latency_statistic_; + StreamingStatistic origin_receipt_statistic_; HeaderMapPtr request_headers_; uint64_t stream_decoder_completion_callbacks_{0}; uint64_t pool_failures_{0}; @@ -60,7 +62,8 @@ TEST_F(StreamDecoderTest, HeaderOnlyTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + request_headers_, false, 0, random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), true); EXPECT_TRUE(is_complete); EXPECT_EQ(1, stream_decoder_completion_callbacks_); @@ -71,7 +74,8 @@ TEST_F(StreamDecoderTest, HeaderWithBodyTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + request_headers_, false, 0, random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), false); EXPECT_FALSE(is_complete); Envoy::Buffer::OwnedImpl buf(std::string(1, 'a')); @@ -87,7 +91,8 @@ TEST_F(StreamDecoderTest, TrailerTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Http::ResponseHeaderMapPtr headers{ new Envoy::Http::TestResponseHeaderMapImpl{{":status", "200"}}}; decoder->decodeHeaders(std::move(headers), false); @@ -100,8 +105,8 @@ TEST_F(StreamDecoderTest, TrailerTest) { TEST_F(StreamDecoderTest, LatencyIsNotMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, - response_header_size_statistic_, response_body_size_statistic_, request_headers_, false, 0, - random_generator_, http_tracer_); + response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, + origin_receipt_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; @@ -135,8 +140,8 @@ TEST_F(StreamDecoderTest, LatencyIsMeasured) { {{":method", "GET"}, {":path", "/"}})); auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, - response_header_size_statistic_, response_body_size_statistic_, request_header, true, 0, - random_generator_, http_tracer_); + response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, + origin_receipt_statistic_, request_header, true, 0, random_generator_, http_tracer_); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); @@ -156,7 +161,8 @@ TEST_F(StreamDecoderTest, StreamResetTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + request_headers_, false, 0, random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), false); decoder->onResetStream(Envoy::Http::StreamResetReason::LocalReset, "fooreason"); EXPECT_TRUE(is_complete); // these do get reported. @@ -168,7 +174,8 @@ TEST_F(StreamDecoderTest, PoolFailureTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; decoder->onPoolFailure(Envoy::Http::ConnectionPool::PoolFailureReason::Overflow, "fooreason", ptr); From 28e0d34b0bcd86e2854fb0269983a8c8dcebd494 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 16 Jun 2020 21:50:28 +0200 Subject: [PATCH 02/48] Fix clang-tidy, format, and test build Will still fail the test because of mismatched expectations, but this will help merging master periodically in here to verify (almost) all is well. Signed-off-by: Otto van der Schaaf --- source/client/benchmark_client_impl.h | 18 ++++++++---------- source/client/stream_decoder.cc | 5 +++-- test/benchmark_http_client_test.cc | 2 ++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index 4ad30d03a..5444e02c4 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -71,16 +71,14 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, public StreamDecoderCompletionCallback, public Envoy::Logger::Loggable { public: - BenchmarkClientHttpImpl(Envoy::Api::Api& api, Envoy::Event::Dispatcher& dispatcher, - Envoy::Stats::Scope& scope, StatisticPtr&& connect_statistic, - StatisticPtr&& response_statistic, - StatisticPtr&& response_header_size_statistic, - StatisticPtr&& response_body_size_statistic, - StatisticPtr&& origin_latency_statistic, StatisticPtr&& origin_receipt_statistic, - bool use_h2, Envoy::Upstream::ClusterManagerPtr& cluster_manager, - Envoy::Tracing::HttpTracerSharedPtr& http_tracer, - absl::string_view cluster_name, RequestGenerator request_generator, - const bool provide_resource_backpressure); + BenchmarkClientHttpImpl( + Envoy::Api::Api& api, Envoy::Event::Dispatcher& dispatcher, Envoy::Stats::Scope& scope, + StatisticPtr&& connect_statistic, StatisticPtr&& response_statistic, + StatisticPtr&& response_header_size_statistic, StatisticPtr&& response_body_size_statistic, + StatisticPtr&& origin_latency_statistic, StatisticPtr&& origin_receipt_statistic, bool use_h2, + Envoy::Upstream::ClusterManagerPtr& cluster_manager, + Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, + RequestGenerator request_generator, const bool provide_resource_backpressure); void setConnectionLimit(uint32_t connection_limit) { connection_limit_ = connection_limit; } void setMaxPendingRequests(uint32_t max_pending_requests) { max_pending_requests_ = max_pending_requests; diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index d9124d07a..b6a2da964 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -23,13 +23,14 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b const auto* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { auto timing_value = timing_header->value().getStringView(); - std::vector split_result = absl::StrSplit(timing_value, ","); + std::vector split_result = absl::StrSplit(timing_value, ','); if (split_result.size() == 2) { int64_t origin_start, origin_delta; bool ok = absl::SimpleAtoi(split_result[0], &origin_start) && absl::SimpleAtoi(split_result[1], &origin_delta); if (ok) { - origin_receipt_statistic_.addValue(origin_start - request_start_.time_since_epoch().count()); + origin_receipt_statistic_.addValue(origin_start - + request_start_.time_since_epoch().count()); origin_latency_statistic_.addValue(origin_delta); } else { // TODO(oschaaf): dispatch warning. watch out for high frequency logging. diff --git a/test/benchmark_http_client_test.cc b/test/benchmark_http_client_test.cc index 983995df5..9f726887b 100644 --- a/test/benchmark_http_client_test.cc +++ b/test/benchmark_http_client_test.cc @@ -124,6 +124,7 @@ class BenchmarkClientHttpTest : public Test { client_ = std::make_unique( *api_, *dispatcher_, store_, std::make_unique(), std::make_unique(), std::make_unique(), + std::make_unique(), std::make_unique(), std::make_unique(), false, cluster_manager_, http_tracer_, "benchmark", request_generator_, true); } @@ -189,6 +190,7 @@ TEST_F(BenchmarkClientHttpTest, StatusTrackingInOnComplete) { client_ = std::make_unique( *api_, *dispatcher_, *store, std::make_unique(), std::make_unique(), std::make_unique(), + std::make_unique(), std::make_unique(), std::make_unique(), false, cluster_manager_, http_tracer_, "foo", request_generator_, true); Envoy::Http::ResponseHeaderMapImpl header; From 933c589b4b4eddf3af6304750faf19b907afc35d Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 19 Aug 2020 16:16:11 +0200 Subject: [PATCH 03/48] stats naming Signed-off-by: Otto van der Schaaf --- source/client/benchmark_client_impl.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 84c39bc1c..4d142d7cf 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -102,8 +102,8 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( statistic_.latency_4xx_statistic->setId("benchmark_http_client.latency_4xx"); statistic_.latency_5xx_statistic->setId("benchmark_http_client.latency_5xx"); statistic_.latency_xxx_statistic->setId("benchmark_http_client.latency_xxx"); - statistic_.origin_latency_statistic->setId("benchmark_http_client.origin_latency"); - statistic_.origin_receipt_statistic->setId("benchmark_http_client.origin_delta"); + statistic_.origin_latency_statistic->setId("benchmark_http_client.origin_processing_time"); + statistic_.origin_receipt_statistic->setId("benchmark_http_client.origin_receive_delta"); } void BenchmarkClientHttpImpl::terminate() { From 740d0cad616e92e73f8066f4ce9b7d6f2ba793e2 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 21 Aug 2020 17:04:28 +0200 Subject: [PATCH 04/48] Track request receipt deltas. Configure response header name. Signed-off-by: Otto van der Schaaf --- api/server/response_options.proto | 2 ++ source/client/benchmark_client_impl.cc | 14 +++++--------- source/client/benchmark_client_impl.h | 4 +--- source/client/factories_impl.cc | 1 - source/client/stream_decoder.cc | 17 +++++------------ source/client/stream_decoder.h | 4 +--- source/server/http_test_server_filter.cc | 16 ++++++++++------ source/server/http_test_server_filter.h | 13 +++++++++++-- .../http_test_server_filter_integration_test.cc | 13 ++++++++++--- test/stream_decoder_test.cc | 15 +++++++-------- 10 files changed, 52 insertions(+), 47 deletions(-) diff --git a/api/server/response_options.proto b/api/server/response_options.proto index 0644d3d0e..c74b2b3bc 100644 --- a/api/server/response_options.proto +++ b/api/server/response_options.proto @@ -32,4 +32,6 @@ message ResponseOptions { // Concurrency based linear delay configuration. ConcurrencyBasedLinearDelay concurrency_based_linear_delay = 5; } + // If set, makes the extension echo timing data in the supplied response header name. + string emit_previous_request_delta_in_response_header = 6; } diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 4d142d7cf..c1294f6ad 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -31,8 +31,7 @@ BenchmarkClientStatistic::BenchmarkClientStatistic(BenchmarkClientStatistic&& st latency_4xx_statistic(std::move(statistic.latency_4xx_statistic)), latency_5xx_statistic(std::move(statistic.latency_5xx_statistic)), latency_xxx_statistic(std::move(statistic.latency_xxx_statistic)), - origin_latency_statistic(std::move(statistic.origin_latency_statistic)), - origin_receipt_statistic(std::move(statistic.origin_receipt_statistic)) {} + origin_latency_statistic(std::move(statistic.origin_latency_statistic)) {} BenchmarkClientStatistic::BenchmarkClientStatistic( StatisticPtr&& connect_stat, StatisticPtr&& response_stat, @@ -40,7 +39,7 @@ BenchmarkClientStatistic::BenchmarkClientStatistic( StatisticPtr&& latency_1xx_stat, StatisticPtr&& latency_2xx_stat, StatisticPtr&& latency_3xx_stat, StatisticPtr&& latency_4xx_stat, StatisticPtr&& latency_5xx_stat, StatisticPtr&& latency_xxx_stat, - StatisticPtr&& origin_latency_stat, StatisticPtr&& origin_receipt_stat) + StatisticPtr&& origin_latency_stat) : connect_statistic(std::move(connect_stat)), response_statistic(std::move(response_stat)), response_header_size_statistic(std::move(response_header_size_stat)), response_body_size_statistic(std::move(response_body_size_stat)), @@ -50,8 +49,7 @@ BenchmarkClientStatistic::BenchmarkClientStatistic( latency_4xx_statistic(std::move(latency_4xx_stat)), latency_5xx_statistic(std::move(latency_5xx_stat)), latency_xxx_statistic(std::move(latency_xxx_stat)), - origin_latency_statistic(std::move(origin_latency_stat)), - origin_receipt_statistic(std::move(origin_receipt_stat)) {} + origin_latency_statistic(std::move(origin_latency_stat)) {} Envoy::Http::ConnectionPool::Cancellable* Http1PoolImpl::newStream(Envoy::Http::ResponseDecoder& response_decoder, @@ -103,7 +101,6 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( statistic_.latency_5xx_statistic->setId("benchmark_http_client.latency_5xx"); statistic_.latency_xxx_statistic->setId("benchmark_http_client.latency_xxx"); statistic_.origin_latency_statistic->setId("benchmark_http_client.origin_processing_time"); - statistic_.origin_receipt_statistic->setId("benchmark_http_client.origin_receive_delta"); } void BenchmarkClientHttpImpl::terminate() { @@ -129,7 +126,6 @@ StatisticPtrMap BenchmarkClientHttpImpl::statistics() const { statistics[statistic_.latency_5xx_statistic->id()] = statistic_.latency_5xx_statistic.get(); statistics[statistic_.latency_xxx_statistic->id()] = statistic_.latency_xxx_statistic.get(); statistics[statistic_.origin_latency_statistic->id()] = statistic_.origin_latency_statistic.get(); - statistics[statistic_.origin_receipt_statistic->id()] = statistic_.origin_receipt_statistic.get(); return statistics; }; @@ -169,8 +165,8 @@ bool BenchmarkClientHttpImpl::tryStartRequest(CompletionCallback caller_completi dispatcher_, api_.timeSource(), *this, std::move(caller_completion_callback), *statistic_.connect_statistic, *statistic_.response_statistic, *statistic_.response_header_size_statistic, *statistic_.response_body_size_statistic, - *statistic_.origin_latency_statistic, *statistic_.origin_receipt_statistic, request->header(), - shouldMeasureLatencies(), content_length, generator_, http_tracer_); + *statistic_.origin_latency_statistic, request->header(), shouldMeasureLatencies(), + content_length, generator_, http_tracer_); requests_initiated_++; pool_ptr->newStream(*stream_decoder, *stream_decoder); return true; diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index 851dffd32..75ab2e3c5 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -59,8 +59,7 @@ struct BenchmarkClientStatistic { StatisticPtr&& response_body_size_stat, StatisticPtr&& latency_1xx_stat, StatisticPtr&& latency_2xx_stat, StatisticPtr&& latency_3xx_stat, StatisticPtr&& latency_4xx_stat, StatisticPtr&& latency_5xx_stat, - StatisticPtr&& latency_xxx_stat, StatisticPtr&& origin_latency_statistic, - StatisticPtr&& origin_receipt_statistic); + StatisticPtr&& latency_xxx_stat, StatisticPtr&& origin_latency_statistic); // These are declared order dependent. Changing ordering may trigger on assert upon // destruction when tls has been involved during usage. @@ -75,7 +74,6 @@ struct BenchmarkClientStatistic { StatisticPtr latency_5xx_statistic; StatisticPtr latency_xxx_statistic; StatisticPtr origin_latency_statistic; - StatisticPtr origin_receipt_statistic; }; class Http1PoolImpl : public Envoy::Http::Http1::ProdConnPoolImpl { diff --git a/source/client/factories_impl.cc b/source/client/factories_impl.cc index c9c52ffcc..33b3b3f2a 100644 --- a/source/client/factories_impl.cc +++ b/source/client/factories_impl.cc @@ -48,7 +48,6 @@ BenchmarkClientPtr BenchmarkClientFactoryImpl::create( std::make_unique(scope, worker_id), std::make_unique(scope, worker_id), std::make_unique(scope, worker_id), - std::make_unique(scope, worker_id), std::make_unique(scope, worker_id)); auto benchmark_client = std::make_unique( api, dispatcher, scope, statistic, options_.h2(), cluster_manager, http_tracer, cluster_name, diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index dd63cef60..91ad74e80 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -23,18 +23,11 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b const auto* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { auto timing_value = timing_header->value().getStringView(); - std::vector split_result = absl::StrSplit(timing_value, ','); - if (split_result.size() == 2) { - int64_t origin_start, origin_delta; - bool ok = absl::SimpleAtoi(split_result[0], &origin_start) && - absl::SimpleAtoi(split_result[1], &origin_delta); - if (ok) { - origin_receipt_statistic_.addValue(origin_start - - request_start_.time_since_epoch().count()); - origin_latency_statistic_.addValue(origin_delta); - } else { - // TODO(oschaaf): dispatch warning. watch out for high frequency logging. - } + int64_t origin_delta; + if (absl::SimpleAtoi(timing_value, &origin_delta)) { + origin_latency_statistic_.addValue(origin_delta); + } else { + // TODO(oschaaf): dispatch warning. watch out for high frequency logging. } } diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index 90d1b6e0e..a440837be 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -44,7 +44,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, OperationCallback caller_completion_callback, Statistic& connect_statistic, Statistic& latency_statistic, Statistic& response_header_sizes_statistic, Statistic& response_body_sizes_statistic, Statistic& origin_latency_statistic, - Statistic& origin_receipt_statistic, HeaderMapPtr request_headers, + HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size, Envoy::Random::RandomGenerator& random_generator, Envoy::Tracing::HttpTracerSharedPtr& http_tracer) @@ -55,7 +55,6 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, response_header_sizes_statistic_(response_header_sizes_statistic), response_body_sizes_statistic_(response_body_sizes_statistic), origin_latency_statistic_(origin_latency_statistic), - origin_receipt_statistic_(origin_receipt_statistic), request_headers_(std::move(request_headers)), connect_start_(time_source_.monotonicTime()), complete_(false), measure_latencies_(measure_latencies), request_body_size_(request_body_size), stream_info_(time_source_), @@ -107,7 +106,6 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, Statistic& response_header_sizes_statistic_; Statistic& response_body_sizes_statistic_; Statistic& origin_latency_statistic_; - Statistic& origin_receipt_statistic_; HeaderMapPtr request_headers_; Envoy::Http::ResponseHeaderMapPtr response_headers_; Envoy::Http::ResponseTrailerMapPtr trailer_headers_; diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index d68fddb74..408e60c7f 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -33,11 +33,13 @@ void HttpTestServerDecoderFilter::sendReply() { static_cast(200), response_body, [this](Envoy::Http::ResponseHeaderMap& direct_response_headers) { Configuration::applyConfigToResponseHeaders(direct_response_headers, base_config_); - const auto timing_header = Envoy::Http::LowerCaseString("x-nh-origin-timings"); - auto delta = time_source_->monotonicTime() - request_time_; - std::string timing_value = - absl::StrCat(request_time_.time_since_epoch().count(), ",", delta.count()); - direct_response_headers.appendCopy(timing_header, timing_value); + const std::string previous_request_delta_in_response_header = + base_config_.emit_previous_request_delta_in_response_header(); + if (!previous_request_delta_in_response_header.empty()) { + direct_response_headers.appendCopy( + Envoy::Http::LowerCaseString(previous_request_delta_in_response_header), + absl::StrCat(last_request_delta_ns_)); + } }, absl::nullopt, ""); } else { @@ -53,7 +55,6 @@ HttpTestServerDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& header bool end_stream) { // TODO(oschaaf): Add functionality to clear fields base_config_ = config_->server_config(); - request_time_ = time_source_->monotonicTime(); const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); if (request_config_header) { json_merge_error_ = !Configuration::mergeJsonConfig( @@ -87,6 +88,9 @@ void HttpTestServerDecoderFilter::setDecoderFilterCallbacks( Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { decoder_callbacks_ = &callbacks; time_source_ = &callbacks.dispatcher().timeSource(); + const Envoy::MonotonicTime current_time = time_source_->monotonicTime(); + const Envoy::MonotonicTime last_request_time = config_->swapLastRequestTime(current_time); + last_request_delta_ns_ = (current_time - last_request_time).count(); } } // namespace Server diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index c7c076eda..3ed5d86c5 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -15,8 +15,17 @@ class HttpTestServerDecoderFilterConfig { public: HttpTestServerDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config); const nighthawk::server::ResponseOptions& server_config() { return server_config_; } + static Envoy::MonotonicTime swapLastRequestTime(const Envoy::MonotonicTime new_time) { + std::atomic& mutable_last_request_time = mutableLastRequestTime(); + return mutable_last_request_time.exchange(new_time); + } private: + static std::atomic& mutableLastRequestTime() { + // We lazy-init the atomic to avoid static initialization / a fiasco. + MUTABLE_CONSTRUCT_ON_FIRST_USE(std::atomic, + Envoy::MonotonicTime::min()); // NOLINT + } const nighthawk::server::ResponseOptions server_config_; }; @@ -45,8 +54,8 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { std::string error_message_; absl::optional request_headers_dump_; Envoy::TimeSource* time_source_{nullptr}; - Envoy::MonotonicTime request_time_; + uint64_t last_request_delta_ns_; }; -} // namespace Server +} } // namespace Nighthawk diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 702b77c3c..c9bd1ff5c 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -270,11 +270,14 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestTooLarge) { } TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { + const std::string kPreviousRequestDeltaHeader = "x-prd"; Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [](Envoy::Http::RequestHeaderMapImpl& request_headers) { - const std::string header_config = - R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"; + [kPreviousRequestDeltaHeader](Envoy::Http::RequestHeaderMapImpl& request_headers) { + const std::string header_config = fmt::format( + R"({{response_headers: [ {{ header: {{ key: "foo", value: "bar2"}}, append: true }} ], emit_previous_request_delta_in_response_header: "{}"}})", + kPreviousRequestDeltaHeader); + std::cerr << header_config << std::endl; request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, header_config); }); @@ -283,6 +286,10 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { EXPECT_EQ("bar2", response->headers().get(Envoy::Http::LowerCaseString("foo"))->value().getStringView()); EXPECT_EQ("", response->body()); + EXPECT_EQ("1", response->headers() + .get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)) + ->value() + .getStringView()); } TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) { diff --git a/test/stream_decoder_test.cc b/test/stream_decoder_test.cc index 2720e9c2c..dfd4ee3e8 100644 --- a/test/stream_decoder_test.cc +++ b/test/stream_decoder_test.cc @@ -50,7 +50,6 @@ class StreamDecoderTest : public Test, public StreamDecoderCompletionCallback { StreamingStatistic response_header_size_statistic_; StreamingStatistic response_body_size_statistic_; StreamingStatistic origin_latency_statistic_; - StreamingStatistic origin_receipt_statistic_; HeaderMapPtr request_headers_; uint64_t stream_decoder_completion_callbacks_{0}; uint64_t pool_failures_{0}; @@ -66,7 +65,7 @@ TEST_F(StreamDecoderTest, HeaderOnlyTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), true); EXPECT_TRUE(is_complete); @@ -79,7 +78,7 @@ TEST_F(StreamDecoderTest, HeaderWithBodyTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), false); EXPECT_FALSE(is_complete); @@ -96,7 +95,7 @@ TEST_F(StreamDecoderTest, TrailerTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Http::ResponseHeaderMapPtr headers{ new Envoy::Http::TestResponseHeaderMapImpl{{":status", "200"}}}; @@ -111,7 +110,7 @@ TEST_F(StreamDecoderTest, LatencyIsNotMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - origin_receipt_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); + request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; @@ -147,7 +146,7 @@ TEST_F(StreamDecoderTest, LatencyIsMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - origin_receipt_statistic_, request_header, true, 0, random_generator_, http_tracer_); + request_header, true, 0, random_generator_, http_tracer_); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); @@ -169,7 +168,7 @@ TEST_F(StreamDecoderTest, StreamResetTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), false); decoder->onResetStream(Envoy::Http::StreamResetReason::LocalReset, "fooreason"); @@ -183,7 +182,7 @@ TEST_F(StreamDecoderTest, PoolFailureTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, origin_receipt_statistic_, + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; decoder->onPoolFailure(Envoy::Http::ConnectionPool::PoolFailureReason::Overflow, "fooreason", From 1afe2f1dbdb51b21dd96682170849c3779523b58 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 21 Aug 2020 20:20:02 +0200 Subject: [PATCH 05/48] Save state before context switching Signed-off-by: Otto van der Schaaf --- api/server/response_options.proto | 4 ++-- source/client/benchmark_client_impl.cc | 2 +- source/client/benchmark_client_impl.h | 3 ++- source/client/stream_decoder.cc | 3 ++- source/server/http_test_server_filter.h | 2 +- test/benchmark_http_client_test.cc | 2 +- .../server/http_test_server_filter_integration_test.cc | 10 ++++++---- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/api/server/response_options.proto b/api/server/response_options.proto index c74b2b3bc..bb84b28ff 100644 --- a/api/server/response_options.proto +++ b/api/server/response_options.proto @@ -32,6 +32,6 @@ message ResponseOptions { // Concurrency based linear delay configuration. ConcurrencyBasedLinearDelay concurrency_based_linear_delay = 5; } - // If set, makes the extension echo timing data in the supplied response header name. - string emit_previous_request_delta_in_response_header = 6; + // If set, makes the extension echo timing data in the supplied response header name. + string emit_previous_request_delta_in_response_header = 6; } diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index c1294f6ad..e29e1706f 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -100,7 +100,7 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( statistic_.latency_4xx_statistic->setId("benchmark_http_client.latency_4xx"); statistic_.latency_5xx_statistic->setId("benchmark_http_client.latency_5xx"); statistic_.latency_xxx_statistic->setId("benchmark_http_client.latency_xxx"); - statistic_.origin_latency_statistic->setId("benchmark_http_client.origin_processing_time"); + statistic_.origin_latency_statistic->setId("benchmark_http_client.origin_latency_statistic"); } void BenchmarkClientHttpImpl::terminate() { diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index 75ab2e3c5..ca4dedabd 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -59,7 +59,8 @@ struct BenchmarkClientStatistic { StatisticPtr&& response_body_size_stat, StatisticPtr&& latency_1xx_stat, StatisticPtr&& latency_2xx_stat, StatisticPtr&& latency_3xx_stat, StatisticPtr&& latency_4xx_stat, StatisticPtr&& latency_5xx_stat, - StatisticPtr&& latency_xxx_stat, StatisticPtr&& origin_latency_statistic); + StatisticPtr&& latency_xxx_stat, + StatisticPtr&& origin_latency_statistic); // These are declared order dependent. Changing ordering may trigger on assert upon // destruction when tls has been involved during usage. diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index 91ad74e80..3d8fee8e6 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -27,7 +27,8 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b if (absl::SimpleAtoi(timing_value, &origin_delta)) { origin_latency_statistic_.addValue(origin_delta); } else { - // TODO(oschaaf): dispatch warning. watch out for high frequency logging. + // TODO(XXX): Can we make sure we avoid high frequency logging for this somehow? + ENVOY_LOG(warn, "Origin delta {} could not be interpreted as an integer.", timing_value); } } diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 3ed5d86c5..833e25619 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -57,5 +57,5 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { uint64_t last_request_delta_ns_; }; -} +} // namespace Server } // namespace Nighthawk diff --git a/test/benchmark_http_client_test.cc b/test/benchmark_http_client_test.cc index d76467dec..739012e5b 100644 --- a/test/benchmark_http_client_test.cc +++ b/test/benchmark_http_client_test.cc @@ -62,7 +62,7 @@ class BenchmarkClientHttpTest : public Test { std::make_unique(), std::make_unique(), std::make_unique(), std::make_unique(), std::make_unique(), std::make_unique(), - std::make_unique(), std::make_unique()) { + std::make_unique()) { auto header_map_param = std::initializer_list>{ {":scheme", "http"}, {":method", "GET"}, {":path", "/"}, {":host", "localhost"}}; default_header_map_ = diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index c9bd1ff5c..e22953bcf 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -286,10 +286,12 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { EXPECT_EQ("bar2", response->headers().get(Envoy::Http::LowerCaseString("foo"))->value().getStringView()); EXPECT_EQ("", response->body()); - EXPECT_EQ("1", response->headers() - .get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)) - ->value() - .getStringView()); + uint64_t dummy; + ASSERT_TRUE(absl::SimpleAtoi(response->headers() + .get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)) + ->value() + .getStringView(), + &dummy)); } TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) { From 1d1f03a86c99df65e62f820e3ad6b9a5009ca3be Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 24 Aug 2020 12:31:45 +0200 Subject: [PATCH 06/48] Fix format Signed-off-by: Otto van der Schaaf --- source/client/stream_decoder.h | 3 +-- test/stream_decoder_test.cc | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index a440837be..890ffcc28 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -44,8 +44,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, OperationCallback caller_completion_callback, Statistic& connect_statistic, Statistic& latency_statistic, Statistic& response_header_sizes_statistic, Statistic& response_body_sizes_statistic, Statistic& origin_latency_statistic, - HeaderMapPtr request_headers, - bool measure_latencies, uint32_t request_body_size, + HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size, Envoy::Random::RandomGenerator& random_generator, Envoy::Tracing::HttpTracerSharedPtr& http_tracer) : dispatcher_(dispatcher), time_source_(time_source), diff --git a/test/stream_decoder_test.cc b/test/stream_decoder_test.cc index dfd4ee3e8..df2b2d262 100644 --- a/test/stream_decoder_test.cc +++ b/test/stream_decoder_test.cc @@ -65,8 +65,8 @@ TEST_F(StreamDecoderTest, HeaderOnlyTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, + random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), true); EXPECT_TRUE(is_complete); EXPECT_EQ(1, stream_decoder_completion_callbacks_); @@ -78,8 +78,8 @@ TEST_F(StreamDecoderTest, HeaderWithBodyTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, + random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), false); EXPECT_FALSE(is_complete); Envoy::Buffer::OwnedImpl buf(std::string(1, 'a')); @@ -95,8 +95,8 @@ TEST_F(StreamDecoderTest, TrailerTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, + random_generator_, http_tracer_); Envoy::Http::ResponseHeaderMapPtr headers{ new Envoy::Http::TestResponseHeaderMapImpl{{":status", "200"}}}; decoder->decodeHeaders(std::move(headers), false); @@ -110,7 +110,7 @@ TEST_F(StreamDecoderTest, LatencyIsNotMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + request_headers_, false, 0, random_generator_, http_tracer_); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; @@ -146,7 +146,7 @@ TEST_F(StreamDecoderTest, LatencyIsMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - request_header, true, 0, random_generator_, http_tracer_); + request_header, true, 0, random_generator_, http_tracer_); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); @@ -168,8 +168,8 @@ TEST_F(StreamDecoderTest, StreamResetTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, + random_generator_, http_tracer_); decoder->decodeHeaders(std::move(test_header_), false); decoder->onResetStream(Envoy::Http::StreamResetReason::LocalReset, "fooreason"); EXPECT_TRUE(is_complete); // these do get reported. @@ -182,8 +182,8 @@ TEST_F(StreamDecoderTest, PoolFailureTest) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, - response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, + random_generator_, http_tracer_); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; decoder->onPoolFailure(Envoy::Http::ConnectionPool::PoolFailureReason::Overflow, "fooreason", ptr); From a9d99ebb54b05585f41236f0823bb537b65cfbc1 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 24 Aug 2020 13:45:32 +0200 Subject: [PATCH 07/48] Suppress clang-tidy on MUTABLE_CONSTRUCT_ON_FIRST_USE Signed-off-by: Otto van der Schaaf --- source/server/http_test_server_filter.h | 4 ++-- test/server/http_test_server_filter_integration_test.cc | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 833e25619..69206b0b1 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -23,8 +23,8 @@ class HttpTestServerDecoderFilterConfig { private: static std::atomic& mutableLastRequestTime() { // We lazy-init the atomic to avoid static initialization / a fiasco. - MUTABLE_CONSTRUCT_ON_FIRST_USE(std::atomic, - Envoy::MonotonicTime::min()); // NOLINT + MUTABLE_CONSTRUCT_ON_FIRST_USE(std::atomic, // NOLINT + Envoy::MonotonicTime::min()); } const nighthawk::server::ResponseOptions server_config_; }; diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index e22953bcf..2acd54ea1 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -277,7 +277,6 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { const std::string header_config = fmt::format( R"({{response_headers: [ {{ header: {{ key: "foo", value: "bar2"}}, append: true }} ], emit_previous_request_delta_in_response_header: "{}"}})", kPreviousRequestDeltaHeader); - std::cerr << header_config << std::endl; request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, header_config); }); From 0aa86308bb8ff344743ef158a872816cf8c696af Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 25 Aug 2020 00:01:21 +0200 Subject: [PATCH 08/48] Proper locking, add TODOs and doc comments. Signed-off-by: Otto van der Schaaf --- source/server/BUILD | 2 + source/server/http_test_server_filter.cc | 28 ++++++++---- source/server/http_test_server_filter.h | 57 +++++++++++++++++++----- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/source/server/BUILD b/source/server/BUILD index a2d2c0857..5c4f1fe51 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -40,6 +40,8 @@ envoy_cc_library( ":configuration_lib", ":well_known_headers_lib", "//api/server:response_options_proto_cc_proto", + "@envoy//source/common/common:lock_guard_lib_with_external_headers", + "@envoy//source/common/common:thread_lib_with_external_headers", "@envoy//source/exe:envoy_common_lib_with_external_headers", ], ) diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index d18575ddf..4cbcd4aa2 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -17,6 +17,22 @@ HttpTestServerDecoderFilterConfig::HttpTestServerDecoderFilterConfig( nighthawk::server::ResponseOptions proto_config) : server_config_(std::move(proto_config)) {} +uint64_t HttpTestServerDecoderFilterConfig::ThreadSafeMontonicTimeStopwatch::getElapsedNsAndReset( + Envoy::TimeSource& time_source) { + Envoy::Thread::LockGuard guard(lock_); + // Note that we obtain monotonic time under lock, to ensure that start_ will be updated + // monotonically. + const Envoy::MonotonicTime new_time = time_source.monotonicTime(); + const uint64_t elapsed = start_ == Envoy::MonotonicTime::min() ? 0 : (new_time - start_).count(); + start_ = new_time; + return elapsed; +} + +uint64_t +HttpTestServerDecoderFilterConfig::getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source) { + return lastRequestStopwatch().getElapsedNsAndReset(time_source); +} + HttpTestServerDecoderFilter::HttpTestServerDecoderFilter( HttpTestServerDecoderFilterConfigSharedPtr config) : config_(std::move(config)) {} @@ -87,14 +103,10 @@ HttpTestServerDecoderFilter::decodeTrailers(Envoy::Http::RequestTrailerMap&) { void HttpTestServerDecoderFilter::setDecoderFilterCallbacks( Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { decoder_callbacks_ = &callbacks; - time_source_ = &callbacks.dispatcher().timeSource(); - const Envoy::MonotonicTime current_time = time_source_->monotonicTime(); - const Envoy::MonotonicTime last_request_time = config_->swapLastRequestTime(current_time); - if (last_request_time == Envoy::MonotonicTime::min()) { - last_request_delta_ns_ = 0; - } else { - last_request_delta_ns_ = (current_time - last_request_time).count(); - } + // TODO(oschaaf): this adds locking in the hot path. Consider moving this into a separate + // extension, which will also allow tracking multiple points via configuration. + last_request_delta_ns_ = + config_->getElapsedNanosSinceLastRequest(callbacks.dispatcher().timeSource()); } } // namespace Server diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 69206b0b1..75d58d24a 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -5,26 +5,64 @@ #include "envoy/common/time.h" #include "envoy/server/filter_config.h" +#include "external/envoy/source/common/common/lock_guard.h" +#include "external/envoy/source/common/common/thread.h" + #include "api/server/response_options.pb.h" namespace Nighthawk { namespace Server { -// Basically this is left in as a placeholder for further configuration. +/** + * Filter configuration container class for the test server extension. + * Instances of this class will be shared accross instances of HttpTestServerDecoderFilter. + */ class HttpTestServerDecoderFilterConfig { public: + /** + * Constructs a new HttpTestServerDecoderFilterConfig instance. + * + * @param proto_config The proto configuration of the filter. + */ HttpTestServerDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config); + + /** + * @return const nighthawk::server::ResponseOptions& read-only reference to the proto config + * object. + */ const nighthawk::server::ResponseOptions& server_config() { return server_config_; } - static Envoy::MonotonicTime swapLastRequestTime(const Envoy::MonotonicTime new_time) { - std::atomic& mutable_last_request_time = mutableLastRequestTime(); - return mutable_last_request_time.exchange(new_time); - } + + /** + * Gets the number of elapsed nanoseconds since the last call (server wide). + * Safe to use concurrently. + * + * @param time_source Time source that will be used to obain an updated monotonic time sample. + * @return uint64_t 0 on the first call, else the number of elapsed nanoseconds since the last + * call. + */ + static uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source); private: - static std::atomic& mutableLastRequestTime() { - // We lazy-init the atomic to avoid static initialization / a fiasco. - MUTABLE_CONSTRUCT_ON_FIRST_USE(std::atomic, // NOLINT - Envoy::MonotonicTime::min()); + /** + * Utility class for thread safe tracking of elapsed monotonic time. + */ + class ThreadSafeMontonicTimeStopwatch { + public: + ThreadSafeMontonicTimeStopwatch() : start_(Envoy::MonotonicTime::min()) {} + /** + * @param time_source used to obtain a sample of the current monotonic time. + * @return uint64_t 0 on the first invocation, and the number of elapsed nanoseconds since the + * last invocation otherwise. + */ + uint64_t getElapsedNsAndReset(Envoy::TimeSource& time_source); + + private: + Envoy::Thread::MutexBasicLockable lock_; + Envoy::MonotonicTime start_ GUARDED_BY(lock_); + }; + + static ThreadSafeMontonicTimeStopwatch& lastRequestStopwatch() { + MUTABLE_CONSTRUCT_ON_FIRST_USE(ThreadSafeMontonicTimeStopwatch); // NOLINT } const nighthawk::server::ResponseOptions server_config_; }; @@ -53,7 +91,6 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { bool json_merge_error_{false}; std::string error_message_; absl::optional request_headers_dump_; - Envoy::TimeSource* time_source_{nullptr}; uint64_t last_request_delta_ns_; }; From 5e48513a185ff797bb6976f1d555ef32918707d4 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 25 Aug 2020 00:12:37 +0200 Subject: [PATCH 09/48] Small fixes Signed-off-by: Otto van der Schaaf --- source/client/stream_decoder.cc | 2 +- test/server/http_test_server_filter_integration_test.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index 3d8fee8e6..4904125db 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -19,7 +19,7 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b response_header_sizes_statistic_.addValue(response_headers_->byteSize()); const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); - const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-origin-timings"); + const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-do-not-use-origin-timings"); const auto* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { auto timing_value = timing_header->value().getStringView(); diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 2acd54ea1..3288720ca 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -286,7 +286,7 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { response->headers().get(Envoy::Http::LowerCaseString("foo"))->value().getStringView()); EXPECT_EQ("", response->body()); uint64_t dummy; - ASSERT_TRUE(absl::SimpleAtoi(response->headers() + EXPECT_TRUE(absl::SimpleAtoi(response->headers() .get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)) ->value() .getStringView(), From d9569cbdd14c6f5f17c0e21cd5ec6b6901f18138 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 25 Aug 2020 14:24:31 +0200 Subject: [PATCH 10/48] Add unit test for StreamDecoder Signed-off-by: Otto van der Schaaf --- source/client/stream_decoder.cc | 4 ++-- test/stream_decoder_test.cc | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index 4904125db..69f2f41aa 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -24,11 +24,11 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b if (timing_header != nullptr) { auto timing_value = timing_header->value().getStringView(); int64_t origin_delta; - if (absl::SimpleAtoi(timing_value, &origin_delta)) { + if (absl::SimpleAtoi(timing_value, &origin_delta) && origin_delta >= 0) { origin_latency_statistic_.addValue(origin_delta); } else { // TODO(XXX): Can we make sure we avoid high frequency logging for this somehow? - ENVOY_LOG(warn, "Origin delta {} could not be interpreted as an integer.", timing_value); + ENVOY_LOG(warn, "Bad origin delta: '{}'.", timing_value); } } diff --git a/test/stream_decoder_test.cc b/test/stream_decoder_test.cc index df2b2d262..e778ed905 100644 --- a/test/stream_decoder_test.cc +++ b/test/stream_decoder_test.cc @@ -214,5 +214,36 @@ TEST_F(StreamDecoderTest, StreamResetReasonToResponseFlag) { Envoy::StreamInfo::ResponseFlag::UpstreamRemoteReset); } +// This test parameterization structure carries the response header name that ought to be treated +// as a latency input that should be tracked, as well as a boolean indicating if we ought to expect +// the latency delivered via that header to be added to the histogram. +using LatencyTrackingViaResponseHeaderTestParam = std::tuple; + +class LatencyTrackingViaResponseHeaderTest + : public StreamDecoderTest, + public WithParamInterface {}; + +INSTANTIATE_TEST_SUITE_P(ResponseHeaderLatencies, LatencyTrackingViaResponseHeaderTest, + ValuesIn({LatencyTrackingViaResponseHeaderTestParam{"0", true}, + LatencyTrackingViaResponseHeaderTestParam{"1", true}, + LatencyTrackingViaResponseHeaderTestParam{"-1", false}, + LatencyTrackingViaResponseHeaderTestParam{"1000", true}, + LatencyTrackingViaResponseHeaderTestParam{"invalid", false}, + LatencyTrackingViaResponseHeaderTestParam{"", false}})); + +// Tests that the StreamDecoder handles delivery of latencies by response header. +TEST_P(LatencyTrackingViaResponseHeaderTest, LatencyTrackingViaResponseHeader) { + auto decoder = new StreamDecoder( + *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, + response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, + request_headers_, false, 0, random_generator_, http_tracer_); + const LatencyTrackingViaResponseHeaderTestParam param = GetParam(); + Envoy::Http::ResponseHeaderMapPtr headers{new Envoy::Http::TestResponseHeaderMapImpl{ + {":status", "200"}, {"x-nh-do-not-use-origin-timings", std::get<0>(param)}}}; + decoder->decodeHeaders(std::move(headers), true); + const uint64_t expected_count = std::get<1>(param) ? 1 : 0; + EXPECT_EQ(origin_latency_statistic_.count(), expected_count); +} + } // namespace Client } // namespace Nighthawk From cf1170c274185567ce7c67fc9e8083d91e2f9cbf Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 26 Aug 2020 09:33:45 +0200 Subject: [PATCH 11/48] Review feedback Signed-off-by: Otto van der Schaaf --- api/server/response_options.proto | 6 +++++- source/client/stream_decoder.cc | 2 +- source/server/http_test_server_filter.cc | 7 ++++--- test/server/http_test_server_filter_integration_test.cc | 9 ++++----- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/api/server/response_options.proto b/api/server/response_options.proto index bb84b28ff..ca4c9585a 100644 --- a/api/server/response_options.proto +++ b/api/server/response_options.proto @@ -32,6 +32,10 @@ message ResponseOptions { // Concurrency based linear delay configuration. ConcurrencyBasedLinearDelay concurrency_based_linear_delay = 5; } - // If set, makes the extension echo timing data in the supplied response header name. + // If set, makes the extension include timing data in the supplied response header name. + // For example, when set to "x-abc", and 3 requests are performed, the test server will respond + // with: Response 1: No x-abc header because there's no previous response Response 2: Header + // x-abc: Response 3: Header x-abc: string emit_previous_request_delta_in_response_header = 6; } diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index 69f2f41aa..4511bdda2 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -27,7 +27,7 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b if (absl::SimpleAtoi(timing_value, &origin_delta) && origin_delta >= 0) { origin_latency_statistic_.addValue(origin_delta); } else { - // TODO(XXX): Can we make sure we avoid high frequency logging for this somehow? + // TODO(#484): avoid high frequency logging. ENVOY_LOG(warn, "Bad origin delta: '{}'.", timing_value); } } diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index 4cbcd4aa2..57ebb6ae6 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -23,9 +23,10 @@ uint64_t HttpTestServerDecoderFilterConfig::ThreadSafeMontonicTimeStopwatch::get // Note that we obtain monotonic time under lock, to ensure that start_ will be updated // monotonically. const Envoy::MonotonicTime new_time = time_source.monotonicTime(); - const uint64_t elapsed = start_ == Envoy::MonotonicTime::min() ? 0 : (new_time - start_).count(); + const uint64_t elapsed_ns = + start_ == Envoy::MonotonicTime::min() ? 0 : (new_time - start_).count(); start_ = new_time; - return elapsed; + return elapsed_ns; } uint64_t @@ -103,7 +104,7 @@ HttpTestServerDecoderFilter::decodeTrailers(Envoy::Http::RequestTrailerMap&) { void HttpTestServerDecoderFilter::setDecoderFilterCallbacks( Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { decoder_callbacks_ = &callbacks; - // TODO(oschaaf): this adds locking in the hot path. Consider moving this into a separate + // TODO(#486): this adds locking in the hot path. Consider moving this into a separate // extension, which will also allow tracking multiple points via configuration. last_request_delta_ns_ = config_->getElapsedNanosSinceLastRequest(callbacks.dispatcher().timeSource()); diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 3288720ca..fe6c4a271 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -285,12 +285,11 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { EXPECT_EQ("bar2", response->headers().get(Envoy::Http::LowerCaseString("foo"))->value().getStringView()); EXPECT_EQ("", response->body()); + const auto delta_header = + response->headers().get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)); + ASSERT_FALSE(delta_header == nullptr); uint64_t dummy; - EXPECT_TRUE(absl::SimpleAtoi(response->headers() - .get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)) - ->value() - .getStringView(), - &dummy)); + EXPECT_TRUE(absl::SimpleAtoi(delta_header->value().getStringView(), &dummy)); } TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) { From 421feb85799773a278e3543248ef569548817643 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 26 Aug 2020 09:36:11 +0200 Subject: [PATCH 12/48] check_format introduced proto comment issues. Add punctuation. Signed-off-by: Otto van der Schaaf --- api/server/response_options.proto | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/server/response_options.proto b/api/server/response_options.proto index ca4c9585a..79c9e2dc9 100644 --- a/api/server/response_options.proto +++ b/api/server/response_options.proto @@ -34,8 +34,8 @@ message ResponseOptions { } // If set, makes the extension include timing data in the supplied response header name. // For example, when set to "x-abc", and 3 requests are performed, the test server will respond - // with: Response 1: No x-abc header because there's no previous response Response 2: Header - // x-abc: Response 3: Header x-abc: + // with: Response 1: No x-abc header because there's no previous response. Response 2: Header + // x-abc: . Response 3: Header x-abc: . string emit_previous_request_delta_in_response_header = 6; } From e813e7525c20d7df3692a8b7b5c2534b2711152a Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Thu, 27 Aug 2020 14:45:54 +0200 Subject: [PATCH 13/48] Review feedback Signed-off-by: Otto van der Schaaf --- nighthawk.code-workspace | 11 ++++++++++- source/client/stream_decoder.cc | 4 ++-- source/server/http_test_server_filter.cc | 2 +- source/server/http_test_server_filter.h | 9 ++++++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/nighthawk.code-workspace b/nighthawk.code-workspace index 876a1499c..d9359b9fc 100644 --- a/nighthawk.code-workspace +++ b/nighthawk.code-workspace @@ -4,5 +4,14 @@ "path": "." } ], - "settings": {} + "settings": { + "files.associations": { + "array": "cpp", + "*.tcc": "cpp", + "istream": "cpp", + "tuple": "cpp", + "utility": "cpp", + "variant": "cpp" + } + } } \ No newline at end of file diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index 4511bdda2..ea7fea80f 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -20,9 +20,9 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-do-not-use-origin-timings"); - const auto* timing_header = response_headers_->get(timing_header_name); + const Envoy::Http::HeaderEntry* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { - auto timing_value = timing_header->value().getStringView(); + absl::string_view timing_value = timing_header->value().getStringView(); int64_t origin_delta; if (absl::SimpleAtoi(timing_value, &origin_delta) && origin_delta >= 0) { origin_latency_statistic_.addValue(origin_delta); diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index 57ebb6ae6..8a52b01d7 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -31,7 +31,7 @@ uint64_t HttpTestServerDecoderFilterConfig::ThreadSafeMontonicTimeStopwatch::get uint64_t HttpTestServerDecoderFilterConfig::getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source) { - return lastRequestStopwatch().getElapsedNsAndReset(time_source); + return getRequestStopwatch().getElapsedNsAndReset(time_source); } HttpTestServerDecoderFilter::HttpTestServerDecoderFilter( diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 75d58d24a..89719e788 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -48,6 +48,9 @@ class HttpTestServerDecoderFilterConfig { */ class ThreadSafeMontonicTimeStopwatch { public: + /** + * Construct a new ThreadSafe & MontonicTime-based Stopwatch object. + */ ThreadSafeMontonicTimeStopwatch() : start_(Envoy::MonotonicTime::min()) {} /** * @param time_source used to obtain a sample of the current monotonic time. @@ -61,7 +64,11 @@ class HttpTestServerDecoderFilterConfig { Envoy::MonotonicTime start_ GUARDED_BY(lock_); }; - static ThreadSafeMontonicTimeStopwatch& lastRequestStopwatch() { + /** + * @return ThreadSafeMontonicTimeStopwatch& Get the Stopwatch singleton instance used to track + * the deltas between inbound requests. + */ + static ThreadSafeMontonicTimeStopwatch& getRequestStopwatch() { MUTABLE_CONSTRUCT_ON_FIRST_USE(ThreadSafeMontonicTimeStopwatch); // NOLINT } const nighthawk::server::ResponseOptions server_config_; From 505495c52929b6ad1dda3f909840c23fc86f0509 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 28 Aug 2020 00:41:20 +0200 Subject: [PATCH 14/48] Save state on splitting out a separate extension and stopwatch Signed-off-by: Otto van der Schaaf --- include/nighthawk/common/BUILD | 11 ++ include/nighthawk/common/stopwatch.h | 19 +++ source/common/BUILD | 20 +++ .../thread_safe_monotonic_time_stopwatch.cc | 16 ++ .../thread_safe_monotonic_time_stopwatch.h | 32 ++++ source/server/BUILD | 27 +++- source/server/http_test_server_filter.cc | 29 ---- source/server/http_test_server_filter.h | 58 +------ source/server/http_time_tracking_filter.cc | 68 ++++++++ source/server/http_time_tracking_filter.h | 80 ++++++++++ .../http_time_tracking_filter_config.cc | 55 +++++++ test/BUILD | 11 ++ test/server/BUILD | 13 ++ ...ttp_test_server_filter_integration_test.cc | 13 +- ...p_time_tracking_filter_integration_test.cc | 147 ++++++++++++++++++ test/stopwatch_test.cc | 31 ++++ 16 files changed, 532 insertions(+), 98 deletions(-) create mode 100644 include/nighthawk/common/stopwatch.h create mode 100644 source/common/thread_safe_monotonic_time_stopwatch.cc create mode 100644 source/common/thread_safe_monotonic_time_stopwatch.h create mode 100644 source/server/http_time_tracking_filter.cc create mode 100644 source/server/http_time_tracking_filter.h create mode 100644 source/server/http_time_tracking_filter_config.cc create mode 100644 test/server/http_time_tracking_filter_integration_test.cc create mode 100644 test/stopwatch_test.cc diff --git a/include/nighthawk/common/BUILD b/include/nighthawk/common/BUILD index c6eb60b40..bdc7e221a 100644 --- a/include/nighthawk/common/BUILD +++ b/include/nighthawk/common/BUILD @@ -59,3 +59,14 @@ envoy_basic_cc_library( "@envoy//source/common/http:headers_lib", ], ) + +envoy_basic_cc_library( + name = "stopwatch_lib", + hdrs = [ + "stopwatch.h", + ], + include_prefix = "nighthawk/common", + deps = [ + "@envoy//include/envoy/common:time_interface", + ], +) diff --git a/include/nighthawk/common/stopwatch.h b/include/nighthawk/common/stopwatch.h new file mode 100644 index 000000000..e7b5714e0 --- /dev/null +++ b/include/nighthawk/common/stopwatch.h @@ -0,0 +1,19 @@ +#pragma once + +#include "envoy/common/pure.h" +#include "envoy/common/time.h" + +namespace Nighthawk { + +class Stopwatch { +public: + virtual ~Stopwatch() = default; + /** + * @param time_source used to obtain a sample of the current time. + * @return uint64_t 0 on the first invocation, and the number of elapsed nanoseconds since the + * last invocation otherwise. + */ + virtual uint64_t getElapsedNsAndReset(Envoy::TimeSource& time_source) PURE; +}; + +} // namespace Nighthawk \ No newline at end of file diff --git a/source/common/BUILD b/source/common/BUILD index a18abb833..9f8b08504 100644 --- a/source/common/BUILD +++ b/source/common/BUILD @@ -70,6 +70,24 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "thread_safe_monotonic_time_stopwatch_lib", + srcs = [ + "thread_safe_monotonic_time_stopwatch.cc", + ], + hdrs = [ + "thread_safe_monotonic_time_stopwatch.h", + ], + repository = "@envoy", + visibility = ["//visibility:public"], + deps = [ + "//include/nighthawk/common:stopwatch_lib", + "@envoy//include/envoy/common:time_interface", + "@envoy//source/common/common:lock_guard_lib_with_external_headers", + "@envoy//source/common/common:thread_lib_with_external_headers", + ], +) + envoy_cc_library( name = "nighthawk_common_lib", srcs = [ @@ -79,6 +97,7 @@ envoy_cc_library( "signal_handler.cc", "statistic_impl.cc", "termination_predicate_impl.cc", + "thread_safe_monotonic_time_stopwatch.cc", "uri_impl.cc", "utility.cc", "version_info.cc", @@ -94,6 +113,7 @@ envoy_cc_library( "signal_handler.h", "statistic_impl.h", "termination_predicate_impl.h", + "thread_safe_monotonic_time_stopwatch.h", "uri_impl.h", "utility.h", "version_info.h", diff --git a/source/common/thread_safe_monotonic_time_stopwatch.cc b/source/common/thread_safe_monotonic_time_stopwatch.cc new file mode 100644 index 000000000..e1048f1c5 --- /dev/null +++ b/source/common/thread_safe_monotonic_time_stopwatch.cc @@ -0,0 +1,16 @@ +#include "common/thread_safe_monotonic_time_stopwatch.h" + +namespace Nighthawk { + +uint64_t ThreadSafeMontonicTimeStopwatch::getElapsedNsAndReset(Envoy::TimeSource& time_source) { + Envoy::Thread::LockGuard guard(lock_); + // Note that we obtain monotonic time under lock, to ensure that start_ will be updated + // monotonically. + const Envoy::MonotonicTime new_time = time_source.monotonicTime(); + const uint64_t elapsed_ns = + start_ == Envoy::MonotonicTime::min() ? 0 : (new_time - start_).count(); + start_ = new_time; + return elapsed_ns; +} + +} // namespace Nighthawk \ No newline at end of file diff --git a/source/common/thread_safe_monotonic_time_stopwatch.h b/source/common/thread_safe_monotonic_time_stopwatch.h new file mode 100644 index 000000000..074b9ed0d --- /dev/null +++ b/source/common/thread_safe_monotonic_time_stopwatch.h @@ -0,0 +1,32 @@ +#pragma once + +#include "nighthawk/common/stopwatch.h" + +#include "external/envoy/source/common/common/lock_guard.h" +#include "external/envoy/source/common/common/thread.h" + +namespace Nighthawk { + +/** + * Utility class for thread safe tracking of elapsed monotonic time. + */ +class ThreadSafeMontonicTimeStopwatch : public Stopwatch { +public: + /** + * Construct a new ThreadSafe & MontonicTime-based Stopwatch object. + */ + ThreadSafeMontonicTimeStopwatch() : start_(Envoy::MonotonicTime::min()) {} + + /** + * @param time_source used to obtain a sample of the current monotonic time. + * @return uint64_t 0 on the first invocation, and the number of elapsed nanoseconds since the + * last invocation otherwise. + */ + uint64_t getElapsedNsAndReset(Envoy::TimeSource& time_source) override; + +private: + Envoy::Thread::MutexBasicLockable lock_; + Envoy::MonotonicTime start_ GUARDED_BY(lock_); +}; + +} // namespace Nighthawk \ No newline at end of file diff --git a/source/server/BUILD b/source/server/BUILD index 5c4f1fe51..dde36af5a 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -40,12 +40,25 @@ envoy_cc_library( ":configuration_lib", ":well_known_headers_lib", "//api/server:response_options_proto_cc_proto", - "@envoy//source/common/common:lock_guard_lib_with_external_headers", - "@envoy//source/common/common:thread_lib_with_external_headers", "@envoy//source/exe:envoy_common_lib_with_external_headers", ], ) +envoy_cc_library( + name = "http_time_tracking_filter_lib", + srcs = ["http_time_tracking_filter.cc"], + hdrs = ["http_time_tracking_filter.h"], + repository = "@envoy", + deps = [ + ":configuration_lib", + ":well_known_headers_lib", + "//api/server:response_options_proto_cc_proto", + "//source/common:thread_safe_monotonic_time_stopwatch_lib", + "@envoy//source/exe:envoy_common_lib_with_external_headers", + "@envoy//source/extensions/filters/http/common:pass_through_filter_lib_with_external_headers", + ], +) + envoy_cc_library( name = "http_dynamic_delay_filter_lib", srcs = ["http_dynamic_delay_filter.cc"], @@ -79,3 +92,13 @@ envoy_cc_library( "@envoy//include/envoy/server:filter_config_interface_with_external_headers", ], ) + +envoy_cc_library( + name = "http_time_tracking_filter_config", + srcs = ["http_time_tracking_filter_config.cc"], + repository = "@envoy", + deps = [ + ":http_time_tracking_filter_lib", + "@envoy//include/envoy/server:filter_config_interface_with_external_headers", + ], +) diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index 8a52b01d7..cf01105cf 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -8,7 +8,6 @@ #include "server/well_known_headers.h" #include "absl/strings/numbers.h" -#include "absl/strings/str_cat.h" namespace Nighthawk { namespace Server { @@ -17,23 +16,6 @@ HttpTestServerDecoderFilterConfig::HttpTestServerDecoderFilterConfig( nighthawk::server::ResponseOptions proto_config) : server_config_(std::move(proto_config)) {} -uint64_t HttpTestServerDecoderFilterConfig::ThreadSafeMontonicTimeStopwatch::getElapsedNsAndReset( - Envoy::TimeSource& time_source) { - Envoy::Thread::LockGuard guard(lock_); - // Note that we obtain monotonic time under lock, to ensure that start_ will be updated - // monotonically. - const Envoy::MonotonicTime new_time = time_source.monotonicTime(); - const uint64_t elapsed_ns = - start_ == Envoy::MonotonicTime::min() ? 0 : (new_time - start_).count(); - start_ = new_time; - return elapsed_ns; -} - -uint64_t -HttpTestServerDecoderFilterConfig::getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source) { - return getRequestStopwatch().getElapsedNsAndReset(time_source); -} - HttpTestServerDecoderFilter::HttpTestServerDecoderFilter( HttpTestServerDecoderFilterConfigSharedPtr config) : config_(std::move(config)) {} @@ -50,13 +32,6 @@ void HttpTestServerDecoderFilter::sendReply() { static_cast(200), response_body, [this](Envoy::Http::ResponseHeaderMap& direct_response_headers) { Configuration::applyConfigToResponseHeaders(direct_response_headers, base_config_); - const std::string previous_request_delta_in_response_header = - base_config_.emit_previous_request_delta_in_response_header(); - if (!previous_request_delta_in_response_header.empty() && last_request_delta_ns_ > 0) { - direct_response_headers.appendCopy( - Envoy::Http::LowerCaseString(previous_request_delta_in_response_header), - absl::StrCat(last_request_delta_ns_)); - } }, absl::nullopt, ""); } else { @@ -104,10 +79,6 @@ HttpTestServerDecoderFilter::decodeTrailers(Envoy::Http::RequestTrailerMap&) { void HttpTestServerDecoderFilter::setDecoderFilterCallbacks( Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { decoder_callbacks_ = &callbacks; - // TODO(#486): this adds locking in the hot path. Consider moving this into a separate - // extension, which will also allow tracking multiple points via configuration. - last_request_delta_ns_ = - config_->getElapsedNanosSinceLastRequest(callbacks.dispatcher().timeSource()); } } // namespace Server diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 89719e788..6f8d2ace1 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -2,75 +2,20 @@ #include -#include "envoy/common/time.h" #include "envoy/server/filter_config.h" -#include "external/envoy/source/common/common/lock_guard.h" -#include "external/envoy/source/common/common/thread.h" - #include "api/server/response_options.pb.h" namespace Nighthawk { namespace Server { -/** - * Filter configuration container class for the test server extension. - * Instances of this class will be shared accross instances of HttpTestServerDecoderFilter. - */ +// Basically this is left in as a placeholder for further configuration. class HttpTestServerDecoderFilterConfig { public: - /** - * Constructs a new HttpTestServerDecoderFilterConfig instance. - * - * @param proto_config The proto configuration of the filter. - */ HttpTestServerDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config); - - /** - * @return const nighthawk::server::ResponseOptions& read-only reference to the proto config - * object. - */ const nighthawk::server::ResponseOptions& server_config() { return server_config_; } - /** - * Gets the number of elapsed nanoseconds since the last call (server wide). - * Safe to use concurrently. - * - * @param time_source Time source that will be used to obain an updated monotonic time sample. - * @return uint64_t 0 on the first call, else the number of elapsed nanoseconds since the last - * call. - */ - static uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source); - private: - /** - * Utility class for thread safe tracking of elapsed monotonic time. - */ - class ThreadSafeMontonicTimeStopwatch { - public: - /** - * Construct a new ThreadSafe & MontonicTime-based Stopwatch object. - */ - ThreadSafeMontonicTimeStopwatch() : start_(Envoy::MonotonicTime::min()) {} - /** - * @param time_source used to obtain a sample of the current monotonic time. - * @return uint64_t 0 on the first invocation, and the number of elapsed nanoseconds since the - * last invocation otherwise. - */ - uint64_t getElapsedNsAndReset(Envoy::TimeSource& time_source); - - private: - Envoy::Thread::MutexBasicLockable lock_; - Envoy::MonotonicTime start_ GUARDED_BY(lock_); - }; - - /** - * @return ThreadSafeMontonicTimeStopwatch& Get the Stopwatch singleton instance used to track - * the deltas between inbound requests. - */ - static ThreadSafeMontonicTimeStopwatch& getRequestStopwatch() { - MUTABLE_CONSTRUCT_ON_FIRST_USE(ThreadSafeMontonicTimeStopwatch); // NOLINT - } const nighthawk::server::ResponseOptions server_config_; }; @@ -98,7 +43,6 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { bool json_merge_error_{false}; std::string error_message_; absl::optional request_headers_dump_; - uint64_t last_request_delta_ns_; }; } // namespace Server diff --git a/source/server/http_time_tracking_filter.cc b/source/server/http_time_tracking_filter.cc new file mode 100644 index 000000000..848c602d9 --- /dev/null +++ b/source/server/http_time_tracking_filter.cc @@ -0,0 +1,68 @@ +#include "server/http_time_tracking_filter.h" + +#include + +#include "envoy/server/filter_config.h" + +#include "server/configuration.h" +#include "server/well_known_headers.h" + +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" + +namespace Nighthawk { +namespace Server { + +HttpTimeTrackingFilterConfig::HttpTimeTrackingFilterConfig( + nighthawk::server::ResponseOptions proto_config) + : server_config_(std::move(proto_config)) {} + +uint64_t +HttpTimeTrackingFilterConfig::getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source) { + return getRequestStopwatch().getElapsedNsAndReset(time_source); +} + +HttpTimeTrackingFilter::HttpTimeTrackingFilter(HttpTimeTrackingFilterConfigSharedPtr config) + : config_(std::move(config)) {} + +Envoy::Http::FilterHeadersStatus +HttpTimeTrackingFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& headers, bool /*end_stream*/) { + base_config_ = config_->server_config(); + const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); + if (request_config_header) { + json_merge_error_ = !Configuration::mergeJsonConfig( + request_config_header->value().getStringView(), base_config_, error_message_); + if (json_merge_error_) { + decoder_callbacks_->sendLocalReply( + static_cast(500), + fmt::format("time-tracking didn't understand the request: {}", error_message_), nullptr, + absl::nullopt, ""); + return Envoy::Http::FilterHeadersStatus::StopIteration; + } + } + return Envoy::Http::FilterHeadersStatus::Continue; +} + +Envoy::Http::FilterHeadersStatus +HttpTimeTrackingFilter::encodeHeaders(Envoy::Http::ResponseHeaderMap& response_headers, bool) { + if (!json_merge_error_) { + const std::string previous_request_delta_in_response_header = + base_config_.emit_previous_request_delta_in_response_header(); + if (!previous_request_delta_in_response_header.empty() && last_request_delta_ns_ > 0) { + response_headers.appendCopy( + Envoy::Http::LowerCaseString(previous_request_delta_in_response_header), + absl::StrCat(last_request_delta_ns_)); + } + } + return Envoy::Http::FilterHeadersStatus::Continue; +} + +void HttpTimeTrackingFilter::setDecoderFilterCallbacks( + Envoy::Http::StreamDecoderFilterCallbacks& callbacks) { + Envoy::Http::PassThroughFilter::setDecoderFilterCallbacks(callbacks); + last_request_delta_ns_ = + config_->getElapsedNanosSinceLastRequest(callbacks.dispatcher().timeSource()); +} + +} // namespace Server +} // namespace Nighthawk diff --git a/source/server/http_time_tracking_filter.h b/source/server/http_time_tracking_filter.h new file mode 100644 index 000000000..9f1a536e5 --- /dev/null +++ b/source/server/http_time_tracking_filter.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include "envoy/common/time.h" +#include "envoy/server/filter_config.h" + +#include "external/envoy/source/extensions/filters/http/common/pass_through_filter.h" + +#include "api/server/response_options.pb.h" + +#include "common/thread_safe_monotonic_time_stopwatch.h" + +namespace Nighthawk { +namespace Server { + +/** + * Filter configuration container class for the test server extension. + * Instances of this class will be shared accross instances of HttpTimeTrackingFilter. + */ +class HttpTimeTrackingFilterConfig { +public: + /** + * Constructs a new HttpTimeTrackingFilterConfig instance. + * + * @param proto_config The proto configuration of the filter. + */ + HttpTimeTrackingFilterConfig(nighthawk::server::ResponseOptions proto_config); + + /** + * @return const nighthawk::server::ResponseOptions& read-only reference to the proto config + * object. + */ + const nighthawk::server::ResponseOptions& server_config() { return server_config_; } + + /** + * Gets the number of elapsed nanoseconds since the last call (server wide). + * Safe to use concurrently. + * + * @param time_source Time source that will be used to obain an updated monotonic time sample. + * @return uint64_t 0 on the first call, else the number of elapsed nanoseconds since the last + * call. + */ + static uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source); + +private: + /** + * @return ThreadSafeMontonicTimeStopwatch& Get the Stopwatch singleton instance used to track + * the deltas between inbound requests. + */ + static ThreadSafeMontonicTimeStopwatch& getRequestStopwatch() { + MUTABLE_CONSTRUCT_ON_FIRST_USE(ThreadSafeMontonicTimeStopwatch); // NOLINT + } + const nighthawk::server::ResponseOptions server_config_; +}; + +using HttpTimeTrackingFilterConfigSharedPtr = std::shared_ptr; + +class HttpTimeTrackingFilter : public Envoy::Http::PassThroughFilter { +public: + HttpTimeTrackingFilter(HttpTimeTrackingFilterConfigSharedPtr); + + // Http::StreamDecoderFilter + Envoy::Http::FilterHeadersStatus decodeHeaders(Envoy::Http::RequestHeaderMap& headers, + bool /*end_stream*/) override; + void setDecoderFilterCallbacks(Envoy::Http::StreamDecoderFilterCallbacks&) override; + + // Http::StreamEncoderFilter + Envoy::Http::FilterHeadersStatus encodeHeaders(Envoy::Http::ResponseHeaderMap&, bool) override; + +private: + const HttpTimeTrackingFilterConfigSharedPtr config_; + nighthawk::server::ResponseOptions base_config_; + bool json_merge_error_{false}; + std::string error_message_; + uint64_t last_request_delta_ns_; +}; + +} // namespace Server +} // namespace Nighthawk diff --git a/source/server/http_time_tracking_filter_config.cc b/source/server/http_time_tracking_filter_config.cc new file mode 100644 index 000000000..1b351538f --- /dev/null +++ b/source/server/http_time_tracking_filter_config.cc @@ -0,0 +1,55 @@ +#include + +#include "envoy/registry/registry.h" + +#include "external/envoy/source/common/protobuf/message_validator_impl.h" + +#include "api/server/response_options.pb.h" +#include "api/server/response_options.pb.validate.h" + +#include "server/http_time_tracking_filter.h" + +namespace Nighthawk { +namespace Server { +namespace Configuration { + +class HttpTimeTrackingFilterConfig + : public Envoy::Server::Configuration::NamedHttpFilterConfigFactory { +public: + Envoy::Http::FilterFactoryCb + createFilterFactoryFromProto(const Envoy::Protobuf::Message& proto_config, const std::string&, + Envoy::Server::Configuration::FactoryContext& context) override { + + auto& validation_visitor = Envoy::ProtobufMessage::getStrictValidationVisitor(); + return createFilter( + Envoy::MessageUtil::downcastAndValidate( + proto_config, validation_visitor), + context); + } + + Envoy::ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return Envoy::ProtobufTypes::MessagePtr{new nighthawk::server::ResponseOptions()}; + } + + std::string name() const override { return "time-tracking"; } + +private: + Envoy::Http::FilterFactoryCb createFilter(const nighthawk::server::ResponseOptions& proto_config, + Envoy::Server::Configuration::FactoryContext&) { + Nighthawk::Server::HttpTimeTrackingFilterConfigSharedPtr config = + std::make_shared( + Nighthawk::Server::HttpTimeTrackingFilterConfig(proto_config)); + + return [config](Envoy::Http::FilterChainFactoryCallbacks& callbacks) -> void { + auto* filter = new Nighthawk::Server::HttpTimeTrackingFilter(config); + callbacks.addStreamFilter(Envoy::Http::StreamFilterSharedPtr{filter}); + }; + } +}; + +static Envoy::Registry::RegisterFactory + register_; +} // namespace Configuration +} // namespace Server +} // namespace Nighthawk diff --git a/test/BUILD b/test/BUILD index 64a0c4fb4..6d134a965 100644 --- a/test/BUILD +++ b/test/BUILD @@ -337,3 +337,14 @@ envoy_cc_test( "@envoy//test/test_common:utility_lib", ], ) + +envoy_cc_test( + name = "stopwatch_test", + srcs = ["stopwatch_test.cc"], + repository = "@envoy", + deps = [ + "//source/common:thread_safe_monotonic_time_stopwatch_lib", + "@envoy//test/test_common:simulated_time_system_lib", + "@envoy//test/test_common:utility_lib", + ], +) diff --git a/test/server/BUILD b/test/server/BUILD index 23723c3ac..0ea078851 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -30,3 +30,16 @@ envoy_cc_test( "@envoy//test/integration:http_integration_lib", ], ) + +envoy_cc_test( + name = "http_time_tracking_filter_integration_test", + srcs = ["http_time_tracking_filter_integration_test.cc"], + repository = "@envoy", + deps = [ + "//source/server:http_time_tracking_filter_config", + "@envoy//include/envoy/upstream:cluster_manager_interface_with_external_headers", + "@envoy//source/common/api:api_lib_with_external_headers", + "@envoy//test/integration:http_integration_lib", + "@envoy//test/test_common:simulated_time_system_lib", + ], +) diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index fe6c4a271..702b77c3c 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -270,13 +270,11 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestTooLarge) { } TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { - const std::string kPreviousRequestDeltaHeader = "x-prd"; Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [kPreviousRequestDeltaHeader](Envoy::Http::RequestHeaderMapImpl& request_headers) { - const std::string header_config = fmt::format( - R"({{response_headers: [ {{ header: {{ key: "foo", value: "bar2"}}, append: true }} ], emit_previous_request_delta_in_response_header: "{}"}})", - kPreviousRequestDeltaHeader); + [](Envoy::Http::RequestHeaderMapImpl& request_headers) { + const std::string header_config = + R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"; request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, header_config); }); @@ -285,11 +283,6 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { EXPECT_EQ("bar2", response->headers().get(Envoy::Http::LowerCaseString("foo"))->value().getStringView()); EXPECT_EQ("", response->body()); - const auto delta_header = - response->headers().get(Envoy::Http::LowerCaseString(kPreviousRequestDeltaHeader)); - ASSERT_FALSE(delta_header == nullptr); - uint64_t dummy; - EXPECT_TRUE(absl::SimpleAtoi(delta_header->value().getStringView(), &dummy)); } TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) { diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc new file mode 100644 index 000000000..4760cec13 --- /dev/null +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -0,0 +1,147 @@ +#include + +#include "envoy/upstream/cluster_manager.h" +#include "envoy/upstream/upstream.h" + +#include "external/envoy/test/common/upstream/utility.h" +#include "external/envoy/test/integration/http_integration.h" +#include "external/envoy/test/test_common/simulated_time_system.h" + +#include "api/server/response_options.pb.h" +#include "api/server/response_options.pb.validate.h" + +#include "server/configuration.h" +#include "server/http_time_tracking_filter.h" +#include "server/well_known_headers.h" + +#include "gtest/gtest.h" + +namespace Nighthawk { + +using namespace std::chrono_literals; + +const std::string kLatencyResponseHeaderName = "x-prd"; +const std::string kDefaultProtoFragment = fmt::format( + "emit_previous_request_delta_in_response_header: \"{}\"", kLatencyResponseHeaderName); +const std::string kProtoConfigTemplate = R"EOF( +name: time-tracking +typed_config: + "@type": type.googleapis.com/nighthawk.server.ResponseOptions + {} +)EOF"; + +class HttpTimeTrackingIntegrationTest + : public Envoy::HttpIntegrationTest, + public testing::TestWithParam { +protected: + HttpTimeTrackingIntegrationTest() + : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, GetParam()), + request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} + + // We don't override SetUp(): tests in this file will call setup() instead to avoid having to + // create a fixture per filter configuration. + void setup(const std::string& config) { + config_helper_.addFilter(config); + HttpIntegrationTest::initialize(); + } + + // Fetches a response with request-level configuration set in the request header. + Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, + bool setup_for_upstream_request = true) { + Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; + request_headers.setCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, + request_level_config); + return getResponse(request_headers, setup_for_upstream_request); + } + + // Fetches a response with the default request headers, expecting the fake upstream to supply + // the response. + Envoy::IntegrationStreamDecoderPtr getResponse() { return getResponse(request_headers_); } + + // Fetches a response using the provided request headers. When setup_for_upstream_request + // is true, the expectation will be that an upstream request will be needed to provide a + // response. If it is set to false, the extension is expected to supply the response, and + // no upstream request ought to occur. + Envoy::IntegrationStreamDecoderPtr + getResponse(const Envoy::Http::TestRequestHeaderMapImpl& request_headers, + bool setup_for_upstream_request = true) { + cleanupUpstreamAndDownstream(); + codec_client_ = makeHttpConnection(lookupPort("http")); + Envoy::IntegrationStreamDecoderPtr response; + if (setup_for_upstream_request) { + response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + } else { + response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + } + return response; + } + + const Envoy::Http::TestRequestHeaderMapImpl request_headers_; +}; + +class HttpTimeTrackingFilterConfigTest : public testing::Test, + public Envoy::Event::TestUsingSimulatedTime {}; + +// WARNING: Don't move this test. Keep it on top. +// The static stopwatch member of the extension config persists accross tests, and +// we test the first call to that singleton instance here. +TEST_F(HttpTimeTrackingFilterConfigTest, GetElapsedNanosSinceLastRequest) { + Envoy::Event::SimulatedTimeSystem& time_system = simTime(); + EXPECT_EQ(Server::HttpTimeTrackingFilterConfig::getElapsedNanosSinceLastRequest(time_system), 0); + time_system.setMonotonicTime(1ns); + EXPECT_EQ(Server::HttpTimeTrackingFilterConfig::getElapsedNanosSinceLastRequest(time_system), 1); +} + +INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, + testing::ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest())); + +// Verify expectations with static/file-based time-tracking configuration. +TEST_P(HttpTimeTrackingIntegrationTest, StaticConfiguration) { + setup(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); + Envoy::IntegrationStreamDecoderPtr response = getResponse(); + int64_t latency; + const Envoy::Http::HeaderEntry* latency_header = + response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); + ASSERT_NE(latency_header, nullptr); + EXPECT_TRUE(absl::SimpleAtoi(latency_header->value().getStringView(), &latency)); + EXPECT_GT(latency, 0); +} + +// Verify expectations with an empty time-tracking configuration. +TEST_P(HttpTimeTrackingIntegrationTest, NoStaticConfiguration) { + setup(fmt::format(kProtoConfigTemplate, "")); + // Don't send any config request header + getResponse(); + // Send a config request header with an empty / default config. Should be a no-op. + getResponse("{}"); + // Send a config request header, this should become effective. + Envoy::IntegrationStreamDecoderPtr response = + getResponse(fmt::format("{{{}}}", kDefaultProtoFragment)); + const Envoy::Http::HeaderEntry* latency_header = + response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); + ASSERT_NE(latency_header, nullptr); + int64_t latency; + EXPECT_TRUE(absl::SimpleAtoi(latency_header->value().getStringView(), &latency)); + // TODO(oschaaf): figure out if we can use simtime here, and verify actual timing matches + // what we'd expect using that. + EXPECT_GT(latency, 0); + + // Send a malformed config request header. This ought to shortcut and directly reply, + // hence we don't expect an upstream request. + response = getResponse("bad_json", false); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_EQ( + response->body(), + "time-tracking didn't understand the request: Error merging json config: Unable to parse " + "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); + // Send an empty config header, which ought to trigger failure mode as well. + response = getResponse("", false); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_EQ( + response->body(), + "time-tracking didn't understand the request: Error merging json config: Unable to " + "parse JSON as proto (INVALID_ARGUMENT:Unexpected end of string. Expected a value.\n\n^): "); +} + +} // namespace Nighthawk diff --git a/test/stopwatch_test.cc b/test/stopwatch_test.cc new file mode 100644 index 000000000..94255e59e --- /dev/null +++ b/test/stopwatch_test.cc @@ -0,0 +1,31 @@ +#include "external/envoy/test/test_common/simulated_time_system.h" +#include "external/envoy/test/test_common/utility.h" + +#include "common/thread_safe_monotonic_time_stopwatch.h" + +#include "gtest/gtest.h" + +namespace Nighthawk { + +using namespace std::chrono_literals; + +class StopwatchTest : public testing::Test, public Envoy::Event::TestUsingSimulatedTime {}; + +TEST_F(StopwatchTest, TestElapsedAndReset) { + ThreadSafeMontonicTimeStopwatch stopwatch; + Envoy::Event::SimulatedTimeSystem& time_system = simTime(); + time_system.setMonotonicTime(1ns); + // The first call should always return 0. + EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 0); + time_system.setMonotonicTime(2ns); + // Verify that moving the clock yields correct results. + EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 1); + time_system.setMonotonicTime(3ns); + EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 1); + time_system.setMonotonicTime(5ns); + EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 2); +} + +// TODO(oschaaf): test spamming a single stopwatch from multiple threads. + +} // namespace Nighthawk From d858b5557fc15e4d68f1eb408d7fbcbad0905167 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 28 Aug 2020 09:36:04 +0200 Subject: [PATCH 15/48] Small corrections Signed-off-by: Otto van der Schaaf --- nighthawk.code-workspace | 9 +-------- source/common/BUILD | 2 -- test/stopwatch_test.cc | 2 ++ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/nighthawk.code-workspace b/nighthawk.code-workspace index d9359b9fc..f951ebff4 100644 --- a/nighthawk.code-workspace +++ b/nighthawk.code-workspace @@ -5,13 +5,6 @@ } ], "settings": { - "files.associations": { - "array": "cpp", - "*.tcc": "cpp", - "istream": "cpp", - "tuple": "cpp", - "utility": "cpp", - "variant": "cpp" - } + "settings": {} } } \ No newline at end of file diff --git a/source/common/BUILD b/source/common/BUILD index 9f8b08504..28fa33a75 100644 --- a/source/common/BUILD +++ b/source/common/BUILD @@ -97,7 +97,6 @@ envoy_cc_library( "signal_handler.cc", "statistic_impl.cc", "termination_predicate_impl.cc", - "thread_safe_monotonic_time_stopwatch.cc", "uri_impl.cc", "utility.cc", "version_info.cc", @@ -113,7 +112,6 @@ envoy_cc_library( "signal_handler.h", "statistic_impl.h", "termination_predicate_impl.h", - "thread_safe_monotonic_time_stopwatch.h", "uri_impl.h", "utility.h", "version_info.h", diff --git a/test/stopwatch_test.cc b/test/stopwatch_test.cc index 94255e59e..3be0e017b 100644 --- a/test/stopwatch_test.cc +++ b/test/stopwatch_test.cc @@ -1,3 +1,5 @@ +#include + #include "external/envoy/test/test_common/simulated_time_system.h" #include "external/envoy/test/test_common/utility.h" From 3cf32b84b70c2f1fe8758af5fb5f4cbba7958d22 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 28 Aug 2020 10:41:43 +0200 Subject: [PATCH 16/48] Add threaded spam test for stopwatch. Add doc comments. Signed-off-by: Otto van der Schaaf --- include/nighthawk/common/BUILD | 1 + include/nighthawk/common/stopwatch.h | 3 ++ nighthawk.code-workspace | 4 +-- test/BUILD | 1 + test/stopwatch_test.cc | 41 ++++++++++++++++++++++++++-- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/include/nighthawk/common/BUILD b/include/nighthawk/common/BUILD index bdc7e221a..de672823a 100644 --- a/include/nighthawk/common/BUILD +++ b/include/nighthawk/common/BUILD @@ -67,6 +67,7 @@ envoy_basic_cc_library( ], include_prefix = "nighthawk/common", deps = [ + "@envoy//include/envoy/common:base_includes", "@envoy//include/envoy/common:time_interface", ], ) diff --git a/include/nighthawk/common/stopwatch.h b/include/nighthawk/common/stopwatch.h index e7b5714e0..87177962a 100644 --- a/include/nighthawk/common/stopwatch.h +++ b/include/nighthawk/common/stopwatch.h @@ -5,6 +5,9 @@ namespace Nighthawk { +/** + * Interface for measuring elapsed time between events. + */ class Stopwatch { public: virtual ~Stopwatch() = default; diff --git a/nighthawk.code-workspace b/nighthawk.code-workspace index f951ebff4..876a1499c 100644 --- a/nighthawk.code-workspace +++ b/nighthawk.code-workspace @@ -4,7 +4,5 @@ "path": "." } ], - "settings": { - "settings": {} - } + "settings": {} } \ No newline at end of file diff --git a/test/BUILD b/test/BUILD index 6d134a965..f88f59e3a 100644 --- a/test/BUILD +++ b/test/BUILD @@ -344,6 +344,7 @@ envoy_cc_test( repository = "@envoy", deps = [ "//source/common:thread_safe_monotonic_time_stopwatch_lib", + "//test/common:fake_time_source", "@envoy//test/test_common:simulated_time_system_lib", "@envoy//test/test_common:utility_lib", ], diff --git a/test/stopwatch_test.cc b/test/stopwatch_test.cc index 3be0e017b..444704891 100644 --- a/test/stopwatch_test.cc +++ b/test/stopwatch_test.cc @@ -1,19 +1,24 @@ #include +#include +#include +#include #include "external/envoy/test/test_common/simulated_time_system.h" #include "external/envoy/test/test_common/utility.h" #include "common/thread_safe_monotonic_time_stopwatch.h" +#include "test/common/fake_time_source.h" + #include "gtest/gtest.h" namespace Nighthawk { using namespace std::chrono_literals; -class StopwatchTest : public testing::Test, public Envoy::Event::TestUsingSimulatedTime {}; +class SimTimeStopwatchTest : public testing::Test, public Envoy::Event::TestUsingSimulatedTime {}; -TEST_F(StopwatchTest, TestElapsedAndReset) { +TEST_F(SimTimeStopwatchTest, TestElapsedAndReset) { ThreadSafeMontonicTimeStopwatch stopwatch; Envoy::Event::SimulatedTimeSystem& time_system = simTime(); time_system.setMonotonicTime(1ns); @@ -28,6 +33,36 @@ TEST_F(StopwatchTest, TestElapsedAndReset) { EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 2); } -// TODO(oschaaf): test spamming a single stopwatch from multiple threads. +class FakeTimeStopwatchTest : public testing::Test {}; + +TEST_F(FakeTimeStopwatchTest, ThreadedStopwatchSpamming) { + constexpr uint64_t kFakeTimeSourceDefaultTick = 1000000000; + constexpr uint32_t kNumThreads = 100; + ThreadSafeMontonicTimeStopwatch stopwatch; + FakeIncrementingMonotonicTimeSource time_system; + std::vector threads(kNumThreads); + std::promise signal_all_threads_running; + std::shared_future future(signal_all_threads_running.get_future()); + + // The first call should always return 0. + EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 0); + for (uint32_t i = 0; i < threads.size(); i++) { + threads[i] = std::thread([&stopwatch, &time_system, kFakeTimeSourceDefaultTick, future] { + // We wait for all threads to be up and running here to maximize concurrency + // of the call below. + future.wait(); + // Subsequent calls should always return 1s. + EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), kFakeTimeSourceDefaultTick); + }); + } + signal_all_threads_running.set_value(); + for (std::thread& thread : threads) { + thread.join(); + } + // Verify monotonic time has advanced right up to the point we expect + // it to, based on the number of threads that have excecuted. + EXPECT_EQ(time_system.monotonicTime().time_since_epoch().count(), + (kNumThreads * kFakeTimeSourceDefaultTick) + kFakeTimeSourceDefaultTick); +} } // namespace Nighthawk From 35aee4738d9a4141e788c5d0f04997c7ea6bdded Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 28 Aug 2020 11:05:49 +0200 Subject: [PATCH 17/48] clang-tidy: fix for loop Signed-off-by: Otto van der Schaaf --- test/stopwatch_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/stopwatch_test.cc b/test/stopwatch_test.cc index 444704891..043d1e700 100644 --- a/test/stopwatch_test.cc +++ b/test/stopwatch_test.cc @@ -46,8 +46,8 @@ TEST_F(FakeTimeStopwatchTest, ThreadedStopwatchSpamming) { // The first call should always return 0. EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 0); - for (uint32_t i = 0; i < threads.size(); i++) { - threads[i] = std::thread([&stopwatch, &time_system, kFakeTimeSourceDefaultTick, future] { + for (auto& thread : threads) { + thread = std::thread([&stopwatch, &time_system, kFakeTimeSourceDefaultTick, future] { // We wait for all threads to be up and running here to maximize concurrency // of the call below. future.wait(); From 70502e1f2cea37b86edd180c54a66fea75843c17 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Sat, 29 Aug 2020 18:53:59 +0200 Subject: [PATCH 18/48] Review feedback: doc comments Signed-off-by: Otto van der Schaaf --- source/common/thread_safe_monotonic_time_stopwatch.h | 10 +++++++++- source/server/http_time_tracking_filter.h | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/source/common/thread_safe_monotonic_time_stopwatch.h b/source/common/thread_safe_monotonic_time_stopwatch.h index 074b9ed0d..2e7b57d44 100644 --- a/source/common/thread_safe_monotonic_time_stopwatch.h +++ b/source/common/thread_safe_monotonic_time_stopwatch.h @@ -8,7 +8,15 @@ namespace Nighthawk { /** - * Utility class for thread safe tracking of elapsed monotonic time. + * Utility class for thread safe tracking of elapsed monotonic time. + * Example usage: + * + * ThreadSafeMontonicTimeStopwatch stopwatch; + * int i = 0; + * do { + * std::cerr << stopwatch.getElapsedNsAndReset() << + * "ns elapsed since last iteration." << std::endl; + * } while (++i < 100); */ class ThreadSafeMontonicTimeStopwatch : public Stopwatch { public: diff --git a/source/server/http_time_tracking_filter.h b/source/server/http_time_tracking_filter.h index 9f1a536e5..1990ea0ae 100644 --- a/source/server/http_time_tracking_filter.h +++ b/source/server/http_time_tracking_filter.h @@ -15,7 +15,7 @@ namespace Nighthawk { namespace Server { /** - * Filter configuration container class for the test server extension. + * Filter configuration container class for the time tracking extension. * Instances of this class will be shared accross instances of HttpTimeTrackingFilter. */ class HttpTimeTrackingFilterConfig { @@ -56,9 +56,17 @@ class HttpTimeTrackingFilterConfig { using HttpTimeTrackingFilterConfigSharedPtr = std::shared_ptr; +/** + * Extension that tracks elapsed time between inbound requests. + */ class HttpTimeTrackingFilter : public Envoy::Http::PassThroughFilter { public: - HttpTimeTrackingFilter(HttpTimeTrackingFilterConfigSharedPtr); + /** + * Construct a new Http Time Tracking Filter object. + * + * @param config Configuration of the extension. + */ + HttpTimeTrackingFilter(HttpTimeTrackingFilterConfigSharedPtr config); // Http::StreamDecoderFilter Envoy::Http::FilterHeadersStatus decodeHeaders(Envoy::Http::RequestHeaderMap& headers, From a55bbe709f060090c1db9b42c78f5b447ad93a31 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 31 Aug 2020 13:30:54 +0200 Subject: [PATCH 19/48] Wire up proto option Signed-off-by: Otto van der Schaaf --- api/client/options.proto | 5 +++++ include/nighthawk/client/options.h | 2 +- source/client/benchmark_client_impl.cc | 8 +++++--- source/client/benchmark_client_impl.h | 4 +++- source/client/factories_impl.cc | 2 +- source/client/options_impl.h | 4 ++++ source/client/stream_decoder.cc | 23 +++++++++++++---------- source/client/stream_decoder.h | 7 +++++-- test/benchmark_http_client_test.cc | 2 +- test/factories_test.cc | 1 + test/mocks/client/mock_options.h | 1 + test/stream_decoder_test.cc | 19 ++++++++++--------- 12 files changed, 50 insertions(+), 28 deletions(-) diff --git a/api/client/options.proto b/api/client/options.proto index 0cfd8bf30..db5cb3da5 100644 --- a/api/client/options.proto +++ b/api/client/options.proto @@ -202,4 +202,9 @@ message CommandLineOptions { // specified the default is 5 seconds. Time interval must be at least 1s and at most 300s. google.protobuf.UInt32Value stats_flush_interval = 35 [(validate.rules).uint32 = {gte: 1, lte: 300}]; + // Set an optional response header name, whose values will be tracked in a latency histogram if + // set. Can be used in tandem with the test server's + // "emit_previous_request_delta_in_response_header" option to get a sense of elapsed time between + // request arrivals. + google.protobuf.StringValue response_header_with_latency_input = 36; } diff --git a/include/nighthawk/client/options.h b/include/nighthawk/client/options.h index f3803f777..fc82ed281 100644 --- a/include/nighthawk/client/options.h +++ b/include/nighthawk/client/options.h @@ -71,7 +71,7 @@ class Options { virtual bool noDuration() const PURE; virtual std::vector statsSinks() const PURE; virtual uint32_t statsFlushInterval() const PURE; - + virtual std::string responseHeaderWithLatencyInput() const PURE; /** * Converts an Options instance to an equivalent CommandLineOptions instance in terms of option * values. diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 7c7dd1fab..163d21f0d 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -83,13 +83,15 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( BenchmarkClientStatistic& statistic, bool use_h2, Envoy::Upstream::ClusterManagerPtr& cluster_manager, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, - RequestGenerator request_generator, const bool provide_resource_backpressure) + RequestGenerator request_generator, const bool provide_resource_backpressure, + absl::string_view response_header_with_latency_input) : api_(api), dispatcher_(dispatcher), scope_(scope.createScope("benchmark.")), statistic_(std::move(statistic)), use_h2_(use_h2), benchmark_client_counters_({ALL_BENCHMARK_CLIENT_COUNTERS(POOL_COUNTER(*scope_))}), cluster_manager_(cluster_manager), http_tracer_(http_tracer), cluster_name_(std::string(cluster_name)), request_generator_(std::move(request_generator)), - provide_resource_backpressure_(provide_resource_backpressure) { + provide_resource_backpressure_(provide_resource_backpressure), + response_header_with_latency_input_(response_header_with_latency_input) { statistic_.connect_statistic->setId("benchmark_http_client.queue_to_connect"); statistic_.response_statistic->setId("benchmark_http_client.request_to_response"); statistic_.response_header_size_statistic->setId("benchmark_http_client.response_header_size"); @@ -166,7 +168,7 @@ bool BenchmarkClientHttpImpl::tryStartRequest(CompletionCallback caller_completi *statistic_.connect_statistic, *statistic_.response_statistic, *statistic_.response_header_size_statistic, *statistic_.response_body_size_statistic, *statistic_.origin_latency_statistic, request->header(), shouldMeasureLatencies(), - content_length, generator_, http_tracer_); + content_length, generator_, http_tracer_, response_header_with_latency_input_); requests_initiated_++; pool_ptr->newStream(*stream_decoder, *stream_decoder); return true; diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index 1fa052135..f3bac8228 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -106,7 +106,8 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, bool use_h2, Envoy::Upstream::ClusterManagerPtr& cluster_manager, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, - const bool provide_resource_backpressure); + const bool provide_resource_backpressure, + absl::string_view response_header_with_latency_input); void setConnectionLimit(uint32_t connection_limit) { connection_limit_ = connection_limit; } void setMaxPendingRequests(uint32_t max_pending_requests) { max_pending_requests_ = max_pending_requests; @@ -162,6 +163,7 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, std::string cluster_name_; const RequestGenerator request_generator_; const bool provide_resource_backpressure_; + const std::string response_header_with_latency_input_; }; } // namespace Client diff --git a/source/client/factories_impl.cc b/source/client/factories_impl.cc index 33b3b3f2a..ba1cb9e56 100644 --- a/source/client/factories_impl.cc +++ b/source/client/factories_impl.cc @@ -51,7 +51,7 @@ BenchmarkClientPtr BenchmarkClientFactoryImpl::create( std::make_unique(scope, worker_id)); auto benchmark_client = std::make_unique( api, dispatcher, scope, statistic, options_.h2(), cluster_manager, http_tracer, cluster_name, - request_generator.get(), !options_.openLoop()); + request_generator.get(), !options_.openLoop(), options_.responseHeaderWithLatencyInput()); auto request_options = options_.toCommandLineOptions()->request_options(); benchmark_client->setConnectionLimit(options_.connections()); benchmark_client->setMaxPendingRequests(options_.maxPendingRequests()); diff --git a/source/client/options_impl.h b/source/client/options_impl.h index 4983c15a6..df7f634ad 100644 --- a/source/client/options_impl.h +++ b/source/client/options_impl.h @@ -85,6 +85,9 @@ class OptionsImpl : public Options, public Envoy::Logger::Loggable& arg, @@ -138,6 +141,7 @@ class OptionsImpl : public Options, public Envoy::Logger::Loggable stats_sinks_; uint32_t stats_flush_interval_{5}; + std::string response_header_with_latency_input_; }; } // namespace Client diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index ea7fea80f..6138d192d 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -19,16 +19,19 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b response_header_sizes_statistic_.addValue(response_headers_->byteSize()); const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); - const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-do-not-use-origin-timings"); - const Envoy::Http::HeaderEntry* timing_header = response_headers_->get(timing_header_name); - if (timing_header != nullptr) { - absl::string_view timing_value = timing_header->value().getStringView(); - int64_t origin_delta; - if (absl::SimpleAtoi(timing_value, &origin_delta) && origin_delta >= 0) { - origin_latency_statistic_.addValue(origin_delta); - } else { - // TODO(#484): avoid high frequency logging. - ENVOY_LOG(warn, "Bad origin delta: '{}'.", timing_value); + if (!response_header_with_latency_input_.empty()) { + const auto timing_header_name = + Envoy::Http::LowerCaseString(response_header_with_latency_input_); + const Envoy::Http::HeaderEntry* timing_header = response_headers_->get(timing_header_name); + if (timing_header != nullptr) { + absl::string_view timing_value = timing_header->value().getStringView(); + int64_t origin_delta; + if (absl::SimpleAtoi(timing_value, &origin_delta) && origin_delta >= 0) { + origin_latency_statistic_.addValue(origin_delta); + } else { + // TODO(#484): avoid high frequency logging. + ENVOY_LOG(warn, "Bad origin delta: '{}'.", timing_value); + } } } diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index 890ffcc28..d137308bd 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -46,7 +46,8 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, Statistic& response_body_sizes_statistic, Statistic& origin_latency_statistic, HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size, Envoy::Random::RandomGenerator& random_generator, - Envoy::Tracing::HttpTracerSharedPtr& http_tracer) + Envoy::Tracing::HttpTracerSharedPtr& http_tracer, + std::string response_header_with_latency_input) : dispatcher_(dispatcher), time_source_(time_source), decoder_completion_callback_(decoder_completion_callback), caller_completion_callback_(std::move(caller_completion_callback)), @@ -57,7 +58,8 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, request_headers_(std::move(request_headers)), connect_start_(time_source_.monotonicTime()), complete_(false), measure_latencies_(measure_latencies), request_body_size_(request_body_size), stream_info_(time_source_), - random_generator_(random_generator), http_tracer_(http_tracer) { + random_generator_(random_generator), http_tracer_(http_tracer), + response_header_with_latency_input_(response_header_with_latency_input) { if (measure_latencies_ && http_tracer_ != nullptr) { setupForTracing(); } @@ -119,6 +121,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, Envoy::Tracing::HttpTracerSharedPtr& http_tracer_; Envoy::Tracing::SpanPtr active_span_; Envoy::StreamInfo::UpstreamTiming upstream_timing_; + const std::string response_header_with_latency_input_; }; } // namespace Client diff --git a/test/benchmark_http_client_test.cc b/test/benchmark_http_client_test.cc index 739012e5b..ab3ec15a0 100644 --- a/test/benchmark_http_client_test.cc +++ b/test/benchmark_http_client_test.cc @@ -179,7 +179,7 @@ class BenchmarkClientHttpTest : public Test { void setupBenchmarkClient(const RequestGenerator& request_generator) { client_ = std::make_unique( *api_, *dispatcher_, store_, statistic_, false, cluster_manager_, http_tracer_, "benchmark", - request_generator, true); + request_generator, true, ""); } uint64_t getCounter(absl::string_view name) { diff --git a/test/factories_test.cc b/test/factories_test.cc index 2a74d8c5b..fa4d6dbc7 100644 --- a/test/factories_test.cc +++ b/test/factories_test.cc @@ -41,6 +41,7 @@ TEST_F(FactoriesTest, CreateBenchmarkClient) { EXPECT_CALL(options_, maxActiveRequests()).Times(1); EXPECT_CALL(options_, maxRequestsPerConnection()).Times(1); EXPECT_CALL(options_, openLoop()).Times(1); + EXPECT_CALL(options_, responseHeaderWithLatencyInput()).Times(1); auto cmd = std::make_unique(); EXPECT_CALL(options_, toCommandLineOptions()).Times(1).WillOnce(Return(ByMove(std::move(cmd)))); StaticRequestSourceImpl request_generator( diff --git a/test/mocks/client/mock_options.h b/test/mocks/client/mock_options.h index 94da6eaef..258904cd5 100644 --- a/test/mocks/client/mock_options.h +++ b/test/mocks/client/mock_options.h @@ -54,6 +54,7 @@ class MockOptions : public Options { MOCK_CONST_METHOD0(noDuration, bool()); MOCK_CONST_METHOD0(statsSinks, std::vector()); MOCK_CONST_METHOD0(statsFlushInterval, uint32_t()); + MOCK_CONST_METHOD0(responseHeaderWithLatencyInput, std::string()); }; } // namespace Client diff --git a/test/stream_decoder_test.cc b/test/stream_decoder_test.cc index e778ed905..0efc1cc81 100644 --- a/test/stream_decoder_test.cc +++ b/test/stream_decoder_test.cc @@ -66,7 +66,7 @@ TEST_F(StreamDecoderTest, HeaderOnlyTest) { *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, - random_generator_, http_tracer_); + random_generator_, http_tracer_, ""); decoder->decodeHeaders(std::move(test_header_), true); EXPECT_TRUE(is_complete); EXPECT_EQ(1, stream_decoder_completion_callbacks_); @@ -79,7 +79,7 @@ TEST_F(StreamDecoderTest, HeaderWithBodyTest) { *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, - random_generator_, http_tracer_); + random_generator_, http_tracer_, ""); decoder->decodeHeaders(std::move(test_header_), false); EXPECT_FALSE(is_complete); Envoy::Buffer::OwnedImpl buf(std::string(1, 'a')); @@ -96,7 +96,7 @@ TEST_F(StreamDecoderTest, TrailerTest) { *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, - random_generator_, http_tracer_); + random_generator_, http_tracer_, ""); Envoy::Http::ResponseHeaderMapPtr headers{ new Envoy::Http::TestResponseHeaderMapImpl{{":status", "200"}}}; decoder->decodeHeaders(std::move(headers), false); @@ -110,7 +110,7 @@ TEST_F(StreamDecoderTest, LatencyIsNotMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + request_headers_, false, 0, random_generator_, http_tracer_, ""); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; @@ -146,7 +146,7 @@ TEST_F(StreamDecoderTest, LatencyIsMeasured) { auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - request_header, true, 0, random_generator_, http_tracer_); + request_header, true, 0, random_generator_, http_tracer_, ""); Envoy::Http::MockRequestEncoder stream_encoder; EXPECT_CALL(stream_encoder, getStream()); @@ -169,7 +169,7 @@ TEST_F(StreamDecoderTest, StreamResetTest) { *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, - random_generator_, http_tracer_); + random_generator_, http_tracer_, ""); decoder->decodeHeaders(std::move(test_header_), false); decoder->onResetStream(Envoy::Http::StreamResetReason::LocalReset, "fooreason"); EXPECT_TRUE(is_complete); // these do get reported. @@ -183,7 +183,7 @@ TEST_F(StreamDecoderTest, PoolFailureTest) { *dispatcher_, time_system_, *this, [&is_complete](bool, bool) { is_complete = true; }, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, request_headers_, false, 0, - random_generator_, http_tracer_); + random_generator_, http_tracer_, ""); Envoy::Upstream::HostDescriptionConstSharedPtr ptr; decoder->onPoolFailure(Envoy::Http::ConnectionPool::PoolFailureReason::Overflow, "fooreason", ptr); @@ -233,13 +233,14 @@ INSTANTIATE_TEST_SUITE_P(ResponseHeaderLatencies, LatencyTrackingViaResponseHead // Tests that the StreamDecoder handles delivery of latencies by response header. TEST_P(LatencyTrackingViaResponseHeaderTest, LatencyTrackingViaResponseHeader) { + const std::string kLatencyTrackingResponseHeader = "latency-in-response-header"; auto decoder = new StreamDecoder( *dispatcher_, time_system_, *this, [](bool, bool) {}, connect_statistic_, latency_statistic_, response_header_size_statistic_, response_body_size_statistic_, origin_latency_statistic_, - request_headers_, false, 0, random_generator_, http_tracer_); + request_headers_, false, 0, random_generator_, http_tracer_, kLatencyTrackingResponseHeader); const LatencyTrackingViaResponseHeaderTestParam param = GetParam(); Envoy::Http::ResponseHeaderMapPtr headers{new Envoy::Http::TestResponseHeaderMapImpl{ - {":status", "200"}, {"x-nh-do-not-use-origin-timings", std::get<0>(param)}}}; + {":status", "200"}, {kLatencyTrackingResponseHeader, std::get<0>(param)}}}; decoder->decodeHeaders(std::move(headers), true); const uint64_t expected_count = std::get<1>(param) ? 1 : 0; EXPECT_EQ(origin_latency_statistic_.count(), expected_count); From d1993132c2fd26150f26dc76f5e8ec78dee3e635 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 31 Aug 2020 23:12:08 +0200 Subject: [PATCH 20/48] Wire up TCLAP, add OptionImpl tests, regen CLI docs. Signed-off-by: Otto van der Schaaf --- README.md | 10 ++++++++-- source/client/options_impl.cc | 12 ++++++++++++ test/options_test.cc | 6 +++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index efc97b845..47857f481 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,9 @@ bazel build -c opt //:nighthawk ``` USAGE: -bazel-bin/nighthawk_client [--stats-flush-interval ] -[--stats-sinks ] ... +bazel-bin/nighthawk_client [--response-header-with-latency-input +] [--stats-flush-interval +] [--stats-sinks ] ... [--no-duration] [--simple-warmup] [--request-source ] [--label ] ... [--multi-target-use-https] @@ -80,6 +81,11 @@ format> Where: +--response-header-with-latency-input +Response header whose values should be tracked in a latency histogram. +The response header value is assumed to be in nanoseconds. Default: +"" + --stats-flush-interval Time interval (in seconds) between flushes to configured stats sinks. Default: 5. diff --git a/source/client/options_impl.cc b/source/client/options_impl.cc index 93fac0efd..85c64cd63 100644 --- a/source/client/options_impl.cc +++ b/source/client/options_impl.cc @@ -294,6 +294,12 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { stats_flush_interval_), false, 5, "uint32_t", cmd); + TCLAP::ValueArg response_header_with_latency_input( + "", "response-header-with-latency-input", + "Response header whose values should be tracked in a latency histogram. The response header " + "value is assumed to be in nanoseconds. Default: \"\"", + false, "", "string", cmd); + Utility::parseCommand(cmd, argc, argv); // --duration and --no-duration are mutually exclusive @@ -425,6 +431,7 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { } } TCLAP_SET_IF_SPECIFIED(stats_flush_interval, stats_flush_interval_); + TCLAP_SET_IF_SPECIFIED(response_header_with_latency_input, response_header_with_latency_input_); // CLI-specific tests. // TODO(oschaaf): as per mergconflicts's remark, it would be nice to aggregate @@ -610,6 +617,9 @@ OptionsImpl::OptionsImpl(const nighthawk::client::CommandLineOptions& options) { no_duration_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(options, no_duration, no_duration_); } std::copy(options.labels().begin(), options.labels().end(), std::back_inserter(labels_)); + response_header_with_latency_input_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( + options, response_header_with_latency_input, response_header_with_latency_input_); + validate(); } @@ -781,6 +791,8 @@ CommandLineOptionsPtr OptionsImpl::toCommandLineOptionsInternal() const { *command_line_options->add_stats_sinks() = stats_sink; } command_line_options->mutable_stats_flush_interval()->set_value(stats_flush_interval_); + command_line_options->mutable_response_header_with_latency_input()->set_value( + response_header_with_latency_input_); return command_line_options; } diff --git a/test/options_test.cc b/test/options_test.cc index 5f26e7114..8315b7054 100644 --- a/test/options_test.cc +++ b/test/options_test.cc @@ -117,7 +117,8 @@ TEST_F(OptionsImplTest, AlmostAll) { "--failure-predicate f2:2 --jitter-uniform .00001s " "--experimental-h2-use-multiple-connections " "--experimental-h1-connection-reuse-strategy lru --label label1 --label label2 {} " - "--simple-warmup --stats-sinks {} --stats-sinks {} --stats-flush-interval 10", + "--simple-warmup --stats-sinks {} --stats-sinks {} --stats-flush-interval 10 " + "--response-header-with-latency-input zz", client_name_, "{name:\"envoy.transport_sockets.tls\"," "typed_config:{\"@type\":\"type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext\"," @@ -189,6 +190,7 @@ TEST_F(OptionsImplTest, AlmostAll) { "}\n" "183412668: \"envoy.config.metrics.v2.StatsSink\"\n", options->statsSinks()[1].DebugString()); + EXPECT_EQ("zz", options->responseHeaderWithLatencyInput()); // Check that our conversion to CommandLineOptionsPtr makes sense. CommandLineOptionsPtr cmd = options->toCommandLineOptions(); @@ -246,6 +248,8 @@ TEST_F(OptionsImplTest, AlmostAll) { ASSERT_EQ(cmd->stats_sinks_size(), options->statsSinks().size()); EXPECT_TRUE(util(cmd->stats_sinks(0), options->statsSinks()[0])); EXPECT_TRUE(util(cmd->stats_sinks(1), options->statsSinks()[1])); + EXPECT_EQ(cmd->response_header_with_latency_input().value(), + options->responseHeaderWithLatencyInput()); OptionsImpl options_from_proto(*cmd); std::string s1 = Envoy::MessageUtil::getYamlStringFromMessage( From a16d8dd95bd5d1d42d26d63a48c7939094500b13 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 31 Aug 2020 23:19:20 +0200 Subject: [PATCH 21/48] Address clang-tidy nit, better CLI/proto option description. Signed-off-by: Otto van der Schaaf --- README.md | 7 ++++--- source/client/benchmark_client_impl.cc | 2 +- source/client/options_impl.cc | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 47857f481..3d39529af 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,10 @@ format> Where: --response-header-with-latency-input -Response header whose values should be tracked in a latency histogram. -The response header value is assumed to be in nanoseconds. Default: -"" +Set an optional response header name, whose values will be tracked in +a latency histogram if set. Can be used in tandem with the test +server's "emit_previous_request_delta_in_response_header" option to +get a sense of elapsed time between request arrivals. Default: "" --stats-flush-interval Time interval (in seconds) between flushes to configured stats sinks. diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 163d21f0d..1bd832b87 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -91,7 +91,7 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( cluster_manager_(cluster_manager), http_tracer_(http_tracer), cluster_name_(std::string(cluster_name)), request_generator_(std::move(request_generator)), provide_resource_backpressure_(provide_resource_backpressure), - response_header_with_latency_input_(response_header_with_latency_input) { + response_header_with_latency_input_(std::move(response_header_with_latency_input)) { statistic_.connect_statistic->setId("benchmark_http_client.queue_to_connect"); statistic_.response_statistic->setId("benchmark_http_client.request_to_response"); statistic_.response_header_size_statistic->setId("benchmark_http_client.response_header_size"); diff --git a/source/client/options_impl.cc b/source/client/options_impl.cc index 85c64cd63..a820cfa72 100644 --- a/source/client/options_impl.cc +++ b/source/client/options_impl.cc @@ -296,8 +296,10 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { TCLAP::ValueArg response_header_with_latency_input( "", "response-header-with-latency-input", - "Response header whose values should be tracked in a latency histogram. The response header " - "value is assumed to be in nanoseconds. Default: \"\"", + "Set an optional response header name, whose values will be tracked in a latency histogram " + "if set. Can be used in tandem with the test server's " + "\"emit_previous_request_delta_in_response_header\" option to get a sense of elapsed time " + "between request arrivals. Default: \"\"", false, "", "string", cmd); Utility::parseCommand(cmd, argc, argv); From f3008d92e410a66b55c50c74986d37bbaf4b6e70 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 1 Sep 2020 00:00:13 +0200 Subject: [PATCH 22/48] Build time-tracking extension into test sever. Add end-to-end test. Signed-off-by: Otto van der Schaaf --- BUILD | 1 + source/client/stream_decoder.h | 2 +- test/integration/BUILD | 1 + .../nighthawk_track_timings.yaml | 34 +++++++++++++++++++ test/integration/test_integration_basics.py | 18 ++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/integration/configurations/nighthawk_track_timings.yaml diff --git a/BUILD b/BUILD index 7c0820a4b..662004eec 100644 --- a/BUILD +++ b/BUILD @@ -32,6 +32,7 @@ envoy_cc_binary( deps = [ "//source/server:http_dynamic_delay_filter_config", "//source/server:http_test_server_filter_config", + "//source/server:http_time_tracking_filter_config", "@envoy//source/exe:envoy_main_entry_lib", ], ) diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index d137308bd..a92d11d36 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -59,7 +59,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, complete_(false), measure_latencies_(measure_latencies), request_body_size_(request_body_size), stream_info_(time_source_), random_generator_(random_generator), http_tracer_(http_tracer), - response_header_with_latency_input_(response_header_with_latency_input) { + response_header_with_latency_input_(std::move(response_header_with_latency_input)) { if (measure_latencies_ && http_tracer_ != nullptr) { setupForTracing(); } diff --git a/test/integration/BUILD b/test/integration/BUILD index 365881061..00765a29b 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -14,6 +14,7 @@ py_library( data = [ "configurations/nighthawk_http_origin.yaml", "configurations/nighthawk_https_origin.yaml", + "configurations/nighthawk_track_timings.yaml", "configurations/sni_origin.yaml", "//:nighthawk_client", "//:nighthawk_output_transform", diff --git a/test/integration/configurations/nighthawk_track_timings.yaml b/test/integration/configurations/nighthawk_track_timings.yaml new file mode 100644 index 000000000..aae01a226 --- /dev/null +++ b/test/integration/configurations/nighthawk_track_timings.yaml @@ -0,0 +1,34 @@ +admin: + access_log_path: $tmpdir/nighthawk-test-server-admin-access.log + profile_path: $tmpdir/nighthawk-test-server.prof + address: + socket_address: { address: $server_ip, port_value: 0 } +static_resources: + listeners: + - address: + socket_address: + address: $server_ip + port_value: 0 + filter_chains: + - filters: + - name: envoy.http_connection_manager + config: + generate_request_id: false + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: service + domains: + - "*" + http_filters: + - name: time-tracking + config: + emit_previous_request_delta_in_response_header: x-origin-request-receipt-delta + - name: test-server + config: + response_body_size: 10 + - name: envoy.router + config: + dynamic_stats: false diff --git a/test/integration/test_integration_basics.py b/test/integration/test_integration_basics.py index e342c27c4..572ac5609 100644 --- a/test/integration/test_integration_basics.py +++ b/test/integration/test_integration_basics.py @@ -700,6 +700,24 @@ def test_cancellation_with_infinite_duration(http_test_server_fixture): asserts.assertCounterGreaterEqual(counters, "benchmark.http_2xx", 1) +@pytest.mark.parametrize('server_config', + ["nighthawk/test/integration/configurations/nighthawk_track_timings.yaml"]) +def test_http_h1_response_header_latency_tracking(http_test_server_fixture): + """Test emission and tracking of response header latencies. + + Run the CLI configured to track latencies delivered by response header from the test-server + which is set up emit those. Ensure the expected histogram is observed. + """ + parsed_json, _ = http_test_server_fixture.runNighthawkClient([ + http_test_server_fixture.getTestServerRootUri(), "--connections", "1", "--rps", "100", + "--duration", "100", "--termination-predicate", "benchmark.http_2xx:99", + "--response-header-with-latency-input", "x-origin-request-receipt-delta" + ]) + global_histograms = http_test_server_fixture.getNighthawkGlobalHistogramsbyIdFromJson(parsed_json) + asserts.assertEqual( + int(global_histograms["benchmark_http_client.origin_latency_statistic"]["count"]), 99) + + def _run_client_with_args(args): return utility.run_binary_with_args("nighthawk_client", args) From 7774e2d3aa69508125634912518850e4771d21a3 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 1 Sep 2020 13:29:24 +0200 Subject: [PATCH 23/48] review feedback Signed-off-by: Otto van der Schaaf --- source/client/stream_decoder.cc | 3 +- source/server/http_time_tracking_filter.cc | 7 ++- source/server/http_time_tracking_filter.h | 14 ++---- .../http_time_tracking_filter_config.cc | 4 +- ...p_time_tracking_filter_integration_test.cc | 49 ++++++++++++------- test/stopwatch_test.cc | 8 +-- test/stream_decoder_test.cc | 2 +- 7 files changed, 48 insertions(+), 39 deletions(-) diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index ea7fea80f..f70109608 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -19,7 +19,8 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b response_header_sizes_statistic_.addValue(response_headers_->byteSize()); const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); - const auto timing_header_name = Envoy::Http::LowerCaseString("x-nh-do-not-use-origin-timings"); + const auto timing_header_name = + Envoy::Http::LowerCaseString("x-nighthawk-do-not-use-origin-timings"); const Envoy::Http::HeaderEntry* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { absl::string_view timing_value = timing_header->value().getStringView(); diff --git a/source/server/http_time_tracking_filter.cc b/source/server/http_time_tracking_filter.cc index 848c602d9..a17b9cb98 100644 --- a/source/server/http_time_tracking_filter.cc +++ b/source/server/http_time_tracking_filter.cc @@ -4,6 +4,8 @@ #include "envoy/server/filter_config.h" +#include "common/thread_safe_monotonic_time_stopwatch.h" + #include "server/configuration.h" #include "server/well_known_headers.h" @@ -15,11 +17,12 @@ namespace Server { HttpTimeTrackingFilterConfig::HttpTimeTrackingFilterConfig( nighthawk::server::ResponseOptions proto_config) - : server_config_(std::move(proto_config)) {} + : server_config_(std::move(proto_config)), + stopwatch_(std::make_unique()) {} uint64_t HttpTimeTrackingFilterConfig::getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source) { - return getRequestStopwatch().getElapsedNsAndReset(time_source); + return stopwatch_->getElapsedNsAndReset(time_source); } HttpTimeTrackingFilter::HttpTimeTrackingFilter(HttpTimeTrackingFilterConfigSharedPtr config) diff --git a/source/server/http_time_tracking_filter.h b/source/server/http_time_tracking_filter.h index 1990ea0ae..a466c5afe 100644 --- a/source/server/http_time_tracking_filter.h +++ b/source/server/http_time_tracking_filter.h @@ -5,12 +5,12 @@ #include "envoy/common/time.h" #include "envoy/server/filter_config.h" +#include "nighthawk/common/stopwatch.h" + #include "external/envoy/source/extensions/filters/http/common/pass_through_filter.h" #include "api/server/response_options.pb.h" -#include "common/thread_safe_monotonic_time_stopwatch.h" - namespace Nighthawk { namespace Server { @@ -41,17 +41,11 @@ class HttpTimeTrackingFilterConfig { * @return uint64_t 0 on the first call, else the number of elapsed nanoseconds since the last * call. */ - static uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source); + uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source); private: - /** - * @return ThreadSafeMontonicTimeStopwatch& Get the Stopwatch singleton instance used to track - * the deltas between inbound requests. - */ - static ThreadSafeMontonicTimeStopwatch& getRequestStopwatch() { - MUTABLE_CONSTRUCT_ON_FIRST_USE(ThreadSafeMontonicTimeStopwatch); // NOLINT - } const nighthawk::server::ResponseOptions server_config_; + std::shared_ptr stopwatch_; }; using HttpTimeTrackingFilterConfigSharedPtr = std::shared_ptr; diff --git a/source/server/http_time_tracking_filter_config.cc b/source/server/http_time_tracking_filter_config.cc index 1b351538f..76adb0bb1 100644 --- a/source/server/http_time_tracking_filter_config.cc +++ b/source/server/http_time_tracking_filter_config.cc @@ -19,8 +19,8 @@ class HttpTimeTrackingFilterConfig Envoy::Http::FilterFactoryCb createFilterFactoryFromProto(const Envoy::Protobuf::Message& proto_config, const std::string&, Envoy::Server::Configuration::FactoryContext& context) override { - - auto& validation_visitor = Envoy::ProtobufMessage::getStrictValidationVisitor(); + Envoy::ProtobufMessage::ValidationVisitor& validation_visitor = + Envoy::ProtobufMessage::getStrictValidationVisitor(); return createFilter( Envoy::MessageUtil::downcastAndValidate( proto_config, validation_visitor), diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 4760cec13..55d08e9c9 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -17,6 +17,7 @@ #include "gtest/gtest.h" namespace Nighthawk { +namespace { using namespace std::chrono_literals; @@ -80,36 +81,27 @@ class HttpTimeTrackingIntegrationTest const Envoy::Http::TestRequestHeaderMapImpl request_headers_; }; -class HttpTimeTrackingFilterConfigTest : public testing::Test, - public Envoy::Event::TestUsingSimulatedTime {}; - -// WARNING: Don't move this test. Keep it on top. -// The static stopwatch member of the extension config persists accross tests, and -// we test the first call to that singleton instance here. -TEST_F(HttpTimeTrackingFilterConfigTest, GetElapsedNanosSinceLastRequest) { - Envoy::Event::SimulatedTimeSystem& time_system = simTime(); - EXPECT_EQ(Server::HttpTimeTrackingFilterConfig::getElapsedNanosSinceLastRequest(time_system), 0); - time_system.setMonotonicTime(1ns); - EXPECT_EQ(Server::HttpTimeTrackingFilterConfig::getElapsedNanosSinceLastRequest(time_system), 1); -} - INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, testing::ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest())); // Verify expectations with static/file-based time-tracking configuration. -TEST_P(HttpTimeTrackingIntegrationTest, StaticConfiguration) { +TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfiguration) { setup(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); Envoy::IntegrationStreamDecoderPtr response = getResponse(); int64_t latency; - const Envoy::Http::HeaderEntry* latency_header = + const Envoy::Http::HeaderEntry* latency_header_1 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); - ASSERT_NE(latency_header, nullptr); - EXPECT_TRUE(absl::SimpleAtoi(latency_header->value().getStringView(), &latency)); + EXPECT_EQ(latency_header_1, nullptr); + response = getResponse(); + const Envoy::Http::HeaderEntry* latency_header_2 = + response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); + ASSERT_NE(latency_header_2, nullptr); + EXPECT_TRUE(absl::SimpleAtoi(latency_header_2->value().getStringView(), &latency)); EXPECT_GT(latency, 0); } // Verify expectations with an empty time-tracking configuration. -TEST_P(HttpTimeTrackingIntegrationTest, NoStaticConfiguration) { +TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfiguration) { setup(fmt::format(kProtoConfigTemplate, "")); // Don't send any config request header getResponse(); @@ -126,10 +118,13 @@ TEST_P(HttpTimeTrackingIntegrationTest, NoStaticConfiguration) { // TODO(oschaaf): figure out if we can use simtime here, and verify actual timing matches // what we'd expect using that. EXPECT_GT(latency, 0); +} +TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguration) { + setup(fmt::format(kProtoConfigTemplate, "")); // Send a malformed config request header. This ought to shortcut and directly reply, // hence we don't expect an upstream request. - response = getResponse("bad_json", false); + Envoy::IntegrationStreamDecoderPtr response = getResponse("bad_json", false); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -144,4 +139,20 @@ TEST_P(HttpTimeTrackingIntegrationTest, NoStaticConfiguration) { "parse JSON as proto (INVALID_ARGUMENT:Unexpected end of string. Expected a value.\n\n^): "); } +class HttpTimeTrackingFilterConfigTest : public testing::Test, + public Envoy::Event::TestUsingSimulatedTime {}; + +TEST_F(HttpTimeTrackingFilterConfigTest, GetElapsedNanosSinceLastRequest) { + Envoy::Event::SimulatedTimeSystem& time_system = simTime(); + Server::HttpTimeTrackingFilterConfig config({}); + EXPECT_EQ(config.getElapsedNanosSinceLastRequest(time_system), 0); + time_system.setMonotonicTime(1ns); + EXPECT_EQ(config.getElapsedNanosSinceLastRequest(time_system), 1); + time_system.setMonotonicTime(1s + 1ns); + EXPECT_EQ(config.getElapsedNanosSinceLastRequest(time_system), 1e9); + time_system.setMonotonicTime(60s + 1s + 1ns); + EXPECT_EQ(config.getElapsedNanosSinceLastRequest(time_system), 60 * 1e9); +} + +} // namespace } // namespace Nighthawk diff --git a/test/stopwatch_test.cc b/test/stopwatch_test.cc index 043d1e700..31883e1be 100644 --- a/test/stopwatch_test.cc +++ b/test/stopwatch_test.cc @@ -13,6 +13,7 @@ #include "gtest/gtest.h" namespace Nighthawk { +namespace { using namespace std::chrono_literals; @@ -33,9 +34,7 @@ TEST_F(SimTimeStopwatchTest, TestElapsedAndReset) { EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 2); } -class FakeTimeStopwatchTest : public testing::Test {}; - -TEST_F(FakeTimeStopwatchTest, ThreadedStopwatchSpamming) { +TEST(ThreadSafeStopwatchTest, ThreadedStopwatchSpamming) { constexpr uint64_t kFakeTimeSourceDefaultTick = 1000000000; constexpr uint32_t kNumThreads = 100; ThreadSafeMontonicTimeStopwatch stopwatch; @@ -46,7 +45,7 @@ TEST_F(FakeTimeStopwatchTest, ThreadedStopwatchSpamming) { // The first call should always return 0. EXPECT_EQ(stopwatch.getElapsedNsAndReset(time_system), 0); - for (auto& thread : threads) { + for (std::thread& thread : threads) { thread = std::thread([&stopwatch, &time_system, kFakeTimeSourceDefaultTick, future] { // We wait for all threads to be up and running here to maximize concurrency // of the call below. @@ -65,4 +64,5 @@ TEST_F(FakeTimeStopwatchTest, ThreadedStopwatchSpamming) { (kNumThreads * kFakeTimeSourceDefaultTick) + kFakeTimeSourceDefaultTick); } +} // namespace } // namespace Nighthawk diff --git a/test/stream_decoder_test.cc b/test/stream_decoder_test.cc index e778ed905..bdd9a0a65 100644 --- a/test/stream_decoder_test.cc +++ b/test/stream_decoder_test.cc @@ -239,7 +239,7 @@ TEST_P(LatencyTrackingViaResponseHeaderTest, LatencyTrackingViaResponseHeader) { request_headers_, false, 0, random_generator_, http_tracer_); const LatencyTrackingViaResponseHeaderTestParam param = GetParam(); Envoy::Http::ResponseHeaderMapPtr headers{new Envoy::Http::TestResponseHeaderMapImpl{ - {":status", "200"}, {"x-nh-do-not-use-origin-timings", std::get<0>(param)}}}; + {":status", "200"}, {"x-nighthawk-do-not-use-origin-timings", std::get<0>(param)}}}; decoder->decodeHeaders(std::move(headers), true); const uint64_t expected_count = std::get<1>(param) ? 1 : 0; EXPECT_EQ(origin_latency_statistic_.count(), expected_count); From 8152829aab1fab0e6467923cee9ffd7f37439928 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 1 Sep 2020 13:32:32 +0200 Subject: [PATCH 24/48] Fix clang-tidy nit. Signed-off-by: Otto van der Schaaf --- source/client/benchmark_client_impl.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 1bd832b87..163d21f0d 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -91,7 +91,7 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( cluster_manager_(cluster_manager), http_tracer_(http_tracer), cluster_name_(std::string(cluster_name)), request_generator_(std::move(request_generator)), provide_resource_backpressure_(provide_resource_backpressure), - response_header_with_latency_input_(std::move(response_header_with_latency_input)) { + response_header_with_latency_input_(response_header_with_latency_input) { statistic_.connect_statistic->setId("benchmark_http_client.queue_to_connect"); statistic_.response_statistic->setId("benchmark_http_client.request_to_response"); statistic_.response_header_size_statistic->setId("benchmark_http_client.response_header_size"); From af60b8d38c4d48bf6a5ce8d9192bd4bd66be6a75 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 1 Sep 2020 19:52:17 +0200 Subject: [PATCH 25/48] Replace stopwatch shared_ptr to unique_ptr Signed-off-by: Otto van der Schaaf --- source/server/http_time_tracking_filter.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/server/http_time_tracking_filter.h b/source/server/http_time_tracking_filter.h index a466c5afe..b3148fed3 100644 --- a/source/server/http_time_tracking_filter.h +++ b/source/server/http_time_tracking_filter.h @@ -45,7 +45,7 @@ class HttpTimeTrackingFilterConfig { private: const nighthawk::server::ResponseOptions server_config_; - std::shared_ptr stopwatch_; + std::unique_ptr stopwatch_; }; using HttpTimeTrackingFilterConfigSharedPtr = std::shared_ptr; From 3996c65daa405078aa564413057497489e3ffe3c Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 1 Sep 2020 21:27:54 +0200 Subject: [PATCH 26/48] Fix a few small nits & improve option description. Signed-off-by: Otto van der Schaaf --- README.md | 6 +++--- include/nighthawk/client/options.h | 1 + source/client/options_impl.cc | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d39529af..023ef1a47 100644 --- a/README.md +++ b/README.md @@ -83,9 +83,9 @@ Where: --response-header-with-latency-input Set an optional response header name, whose values will be tracked in -a latency histogram if set. Can be used in tandem with the test -server's "emit_previous_request_delta_in_response_header" option to -get a sense of elapsed time between request arrivals. Default: "" +a latency histogram if set. Can be used in tandem with the test server +"emit_previous_request_delta_in_response_header" option to get a sense +of elapsed time between request arrivals. Default: "" --stats-flush-interval Time interval (in seconds) between flushes to configured stats sinks. diff --git a/include/nighthawk/client/options.h b/include/nighthawk/client/options.h index fc82ed281..6e73637df 100644 --- a/include/nighthawk/client/options.h +++ b/include/nighthawk/client/options.h @@ -72,6 +72,7 @@ class Options { virtual std::vector statsSinks() const PURE; virtual uint32_t statsFlushInterval() const PURE; virtual std::string responseHeaderWithLatencyInput() const PURE; + /** * Converts an Options instance to an equivalent CommandLineOptions instance in terms of option * values. diff --git a/source/client/options_impl.cc b/source/client/options_impl.cc index a820cfa72..1bcbadf52 100644 --- a/source/client/options_impl.cc +++ b/source/client/options_impl.cc @@ -297,7 +297,7 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { TCLAP::ValueArg response_header_with_latency_input( "", "response-header-with-latency-input", "Set an optional response header name, whose values will be tracked in a latency histogram " - "if set. Can be used in tandem with the test server's " + "if set. Can be used in tandem with the test server " "\"emit_previous_request_delta_in_response_header\" option to get a sense of elapsed time " "between request arrivals. Default: \"\"", false, "", "string", cmd); From 00b6861e78f9ca0f07a8eddbe9e733fe1bcb18df Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Sat, 5 Sep 2020 13:05:08 +0200 Subject: [PATCH 27/48] Fix todo Signed-off-by: Otto van der Schaaf --- source/client/stream_decoder.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index 6138d192d..d5911243f 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -29,8 +29,7 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b if (absl::SimpleAtoi(timing_value, &origin_delta) && origin_delta >= 0) { origin_latency_statistic_.addValue(origin_delta); } else { - // TODO(#484): avoid high frequency logging. - ENVOY_LOG(warn, "Bad origin delta: '{}'.", timing_value); + ENVOY_LOG_EVERY_POW_2(warn, "Bad origin delta: '{}'.", timing_value); } } } From 7cf2edd03b5b59281e731643f5b4eabfd34d398f Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Sat, 5 Sep 2020 17:08:19 +0200 Subject: [PATCH 28/48] Server extension: start factoring out common code / functionality. A first step to clean this up. Functional no-op. Part of #498 Signed-off-by: Otto van der Schaaf --- ci/docker/default-config.yaml | 9 +- source/server/BUILD | 16 ++- source/server/configuration.cc | 2 +- source/server/configuration.h | 2 +- source/server/http_dynamic_delay_filter.cc | 43 ++++--- source/server/http_dynamic_delay_filter.h | 27 +---- source/server/http_filter_config_base.cc | 42 +++++++ source/server/http_filter_config_base.h | 80 +++++++++++++ source/server/http_test_server_filter.cc | 56 ++++----- source/server/http_test_server_filter.h | 13 +-- source/server/http_time_tracking_filter.cc | 32 +++--- source/server/http_time_tracking_filter.h | 15 +-- .../configurations/nighthawk_http_origin.yaml | 8 +- test/integration/test_integration_basics.py | 13 ++- test/server/BUILD | 15 ++- ...p_dynamic_delay_filter_integration_test.cc | 107 ++++++++---------- .../http_filter_integration_test_base.cc | 60 ++++++++++ .../http_filter_integration_test_base.h | 36 ++++++ ...ttp_test_server_filter_integration_test.cc | 6 +- ...p_time_tracking_filter_integration_test.cc | 81 ++++++------- 20 files changed, 413 insertions(+), 250 deletions(-) create mode 100644 source/server/http_filter_config_base.cc create mode 100644 source/server/http_filter_config_base.h create mode 100644 test/server/http_filter_integration_test_base.cc create mode 100644 test/server/http_filter_integration_test_base.h diff --git a/ci/docker/default-config.yaml b/ci/docker/default-config.yaml index b417a4a88..8c931f188 100644 --- a/ci/docker/default-config.yaml +++ b/ci/docker/default-config.yaml @@ -23,13 +23,8 @@ static_resources: domains: - "*" http_filters: - - name: envoy.fault - config: - max_active_faults: 100 - delay: - header_delay: {} - percentage: - numerator: 100 + - name: time-tracking + - name: dynamic-delay - name: test-server config: response_body_size: 10 diff --git a/source/server/BUILD b/source/server/BUILD index dde36af5a..33e9a6dfc 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -20,11 +20,20 @@ envoy_cc_library( envoy_cc_library( name = "configuration_lib", - srcs = ["configuration.cc"], - hdrs = ["configuration.h"], + srcs = [ + "configuration.cc", + "http_filter_config_base.cc", + ], + hdrs = [ + "configuration.h", + "http_filter_config_base.h", + ], repository = "@envoy", deps = [ + ":well_known_headers_lib", "//api/server:response_options_proto_cc_proto", + "@envoy//include/envoy/server:filter_config_interface_with_external_headers", + "@envoy//source/common/common:statusor_lib_with_external_headers", "@envoy//source/common/protobuf:message_validator_lib_with_external_headers", "@envoy//source/common/protobuf:utility_lib_with_external_headers", "@envoy//source/common/singleton:const_singleton_with_external_headers", @@ -38,7 +47,6 @@ envoy_cc_library( repository = "@envoy", deps = [ ":configuration_lib", - ":well_known_headers_lib", "//api/server:response_options_proto_cc_proto", "@envoy//source/exe:envoy_common_lib_with_external_headers", ], @@ -51,7 +59,6 @@ envoy_cc_library( repository = "@envoy", deps = [ ":configuration_lib", - ":well_known_headers_lib", "//api/server:response_options_proto_cc_proto", "//source/common:thread_safe_monotonic_time_stopwatch_lib", "@envoy//source/exe:envoy_common_lib_with_external_headers", @@ -66,7 +73,6 @@ envoy_cc_library( repository = "@envoy", deps = [ ":configuration_lib", - ":well_known_headers_lib", "//api/server:response_options_proto_cc_proto", "@envoy//source/exe:envoy_common_lib_with_external_headers", "@envoy//source/extensions/filters/http/fault:fault_filter_lib_with_external_headers", diff --git a/source/server/configuration.cc b/source/server/configuration.cc index 8ddc92fb6..a6037bf14 100644 --- a/source/server/configuration.cc +++ b/source/server/configuration.cc @@ -29,7 +29,7 @@ bool mergeJsonConfig(absl::string_view json, nighthawk::server::ResponseOptions& } void applyConfigToResponseHeaders(Envoy::Http::ResponseHeaderMap& response_headers, - nighthawk::server::ResponseOptions& response_options) { + const nighthawk::server::ResponseOptions& response_options) { for (const auto& header_value_option : response_options.response_headers()) { const auto& header = header_value_option.header(); auto lower_case_key = Envoy::Http::LowerCaseString(header.key()); diff --git a/source/server/configuration.h b/source/server/configuration.h index ec1f77165..e44cca4a6 100644 --- a/source/server/configuration.h +++ b/source/server/configuration.h @@ -29,7 +29,7 @@ bool mergeJsonConfig(absl::string_view json, nighthawk::server::ResponseOptions& * @param response_options Configuration specifying how to transform the header map. */ void applyConfigToResponseHeaders(Envoy::Http::ResponseHeaderMap& response_headers, - nighthawk::server::ResponseOptions& response_options); + const nighthawk::server::ResponseOptions& response_options); } // namespace Configuration } // namespace Server diff --git a/source/server/http_dynamic_delay_filter.cc b/source/server/http_dynamic_delay_filter.cc index 542cedefa..f8d82959a 100644 --- a/source/server/http_dynamic_delay_filter.cc +++ b/source/server/http_dynamic_delay_filter.cc @@ -15,8 +15,8 @@ namespace Server { HttpDynamicDelayDecoderFilterConfig::HttpDynamicDelayDecoderFilterConfig( nighthawk::server::ResponseOptions proto_config, Envoy::Runtime::Loader& runtime, const std::string& stats_prefix, Envoy::Stats::Scope& scope, Envoy::TimeSource& time_source) - : server_config_(std::move(proto_config)), runtime_(runtime), - stats_prefix_(absl::StrCat(stats_prefix, "dynamic-delay.")), scope_(scope), + : FilterConfigurationBase(std::move(proto_config), "dynamic-delay"), runtime_(runtime), + stats_prefix_(absl::StrCat(stats_prefix, fmt::format("{}.", filter_name()))), scope_(scope), time_source_(time_source) {} HttpDynamicDelayDecoderFilter::HttpDynamicDelayDecoderFilter( @@ -40,32 +40,31 @@ void HttpDynamicDelayDecoderFilter::onDestroy() { Envoy::Http::FilterHeadersStatus HttpDynamicDelayDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& headers, bool end_stream) { - response_options_ = config_->server_config(); - std::string error_message; - if (!computeResponseOptions(headers, error_message)) { - decoder_callbacks_->sendLocalReply( - static_cast(500), - fmt::format("dynamic-delay didn't understand the request: {}", error_message), nullptr, - absl::nullopt, ""); - return Envoy::Http::FilterHeadersStatus::StopIteration; + config_->computeEffectiveConfiguration(headers); + if (config_->getEffectiveConfiguration().ok()) { + const absl::optional delay_ms = computeDelayMs( + *config_->getEffectiveConfiguration().value(), config_->approximateFilterInstances()); + maybeRequestFaultFilterDelay(delay_ms, headers); + } else { + if (end_stream) { + config_->maybeSendErrorReply(*decoder_callbacks_); + return Envoy::Http::FilterHeadersStatus::StopIteration; + } + return Envoy::Http::FilterHeadersStatus::Continue; } - const absl::optional delay_ms = - computeDelayMs(response_options_, config_->approximateFilterInstances()); - maybeRequestFaultFilterDelay(delay_ms, headers); return Envoy::Extensions::HttpFilters::Fault::FaultFilter::decodeHeaders(headers, end_stream); } -bool HttpDynamicDelayDecoderFilter::computeResponseOptions( - const Envoy::Http::RequestHeaderMap& headers, std::string& error_message) { - response_options_ = config_->server_config(); - const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); - if (request_config_header) { - if (!Configuration::mergeJsonConfig(request_config_header->value().getStringView(), - response_options_, error_message)) { - return false; +Envoy::Http::FilterDataStatus +HttpDynamicDelayDecoderFilter::decodeData(Envoy::Buffer::Instance& buffer, bool end_stream) { + if (!config_->getEffectiveConfiguration().ok()) { + if (end_stream) { + config_->maybeSendErrorReply(*decoder_callbacks_); + return Envoy::Http::FilterDataStatus::StopIterationNoBuffer; } + return Envoy::Http::FilterDataStatus::Continue; } - return true; + return Envoy::Extensions::HttpFilters::Fault::FaultFilter::decodeData(buffer, end_stream); } absl::optional HttpDynamicDelayDecoderFilter::computeDelayMs( diff --git a/source/server/http_dynamic_delay_filter.h b/source/server/http_dynamic_delay_filter.h index 4c60a1822..12bff0d61 100644 --- a/source/server/http_dynamic_delay_filter.h +++ b/source/server/http_dynamic_delay_filter.h @@ -9,6 +9,8 @@ #include "api/server/response_options.pb.h" +#include "server/http_filter_config_base.h" + namespace Nighthawk { namespace Server { @@ -17,7 +19,7 @@ namespace Server { * Instances of this class will be shared accross instances of HttpDynamicDelayDecoderFilter. * The methods for getting and manipulating (global) active filter instance counts are thread safe. */ -class HttpDynamicDelayDecoderFilterConfig { +class HttpDynamicDelayDecoderFilterConfig : public FilterConfigurationBase { public: /** @@ -35,13 +37,6 @@ class HttpDynamicDelayDecoderFilterConfig { Envoy::Runtime::Loader& runtime, const std::string& stats_prefix, Envoy::Stats::Scope& scope, Envoy::TimeSource& time_source); - - /** - * @return const nighthawk::server::ResponseOptions& read-only reference to the proto config - * object. - */ - const nighthawk::server::ResponseOptions& server_config() const { return server_config_; } - /** * Increments the number of globally active filter instances. */ @@ -79,7 +74,6 @@ class HttpDynamicDelayDecoderFilterConfig { std::string stats_prefix() { return stats_prefix_; } private: - const nighthawk::server::ResponseOptions server_config_; static std::atomic& instances() { // We lazy-init the atomic to avoid static initialization / a fiasco. MUTABLE_CONSTRUCT_ON_FIRST_USE(std::atomic, 0); // NOLINT @@ -112,21 +106,9 @@ class HttpDynamicDelayDecoderFilter : public Envoy::Extensions::HttpFilters::Fau // Http::StreamDecoderFilter Envoy::Http::FilterHeadersStatus decodeHeaders(Envoy::Http::RequestHeaderMap&, bool) override; + Envoy::Http::FilterDataStatus decodeData(Envoy::Buffer::Instance&, bool) override; void setDecoderFilterCallbacks(Envoy::Http::StreamDecoderFilterCallbacks&) override; - /** - * Compute the response options based on the static configuration and optional configuration - * provided via the request headers. After a successfull call the response_options_ field will - * have been modified to reflect request-level configuration. - * - * @param request_headers The request headers set to inspect for configuration. - * @param error_message Set to an error message if the request-level configuration couldn't be - * interpreted. - * @return true iff the configuration was successfully computed. - */ - bool computeResponseOptions(const Envoy::Http::RequestHeaderMap& request_headers, - std::string& error_message); - /** * Compute the concurrency based linear delay in milliseconds. * @@ -179,7 +161,6 @@ class HttpDynamicDelayDecoderFilter : public Envoy::Extensions::HttpFilters::Fau private: const HttpDynamicDelayDecoderFilterConfigSharedPtr config_; Envoy::Http::StreamDecoderFilterCallbacks* decoder_callbacks_; - nighthawk::server::ResponseOptions response_options_; bool destroyed_{false}; }; diff --git a/source/server/http_filter_config_base.cc b/source/server/http_filter_config_base.cc new file mode 100644 index 000000000..36c5e5e08 --- /dev/null +++ b/source/server/http_filter_config_base.cc @@ -0,0 +1,42 @@ +#include "server/http_filter_config_base.h" + +namespace Nighthawk { +namespace Server { + +FilterConfigurationBase::FilterConfigurationBase(nighthawk::server::ResponseOptions proto_config, + absl::string_view filter_name) + : filter_name_(filter_name), + server_config_(std::make_shared(std::move(proto_config))), + effective_config_(server_config_) {} + +void FilterConfigurationBase::computeEffectiveConfiguration( + const Envoy::Http::RequestHeaderMap& headers) { + const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); + if (request_config_header) { + nighthawk::server::ResponseOptions response_options = *server_config_; + std::string error_message; + if (Configuration::mergeJsonConfig(request_config_header->value().getStringView(), + response_options, error_message)) { + effective_config_ = + std::make_shared(std::move(response_options)); + } else { + effective_config_ = absl::InvalidArgumentError(error_message); + } + } +} + +bool FilterConfigurationBase::maybeSendErrorReply( + Envoy::Http::StreamDecoderFilterCallbacks& decoder_callbacks) const { + if (!effective_config_.ok()) { + decoder_callbacks.sendLocalReply(static_cast(500), + fmt::format("{} didn't understand the request: {}", + filter_name_, + effective_config_.status().message()), + nullptr, absl::nullopt, ""); + return true; + } + return false; +} + +} // namespace Server +} // namespace Nighthawk diff --git a/source/server/http_filter_config_base.h b/source/server/http_filter_config_base.h new file mode 100644 index 000000000..5e8448010 --- /dev/null +++ b/source/server/http_filter_config_base.h @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include "envoy/server/filter_config.h" + +#include "external/envoy/source/common/common/statusor.h" + +#include "api/server/response_options.pb.h" + +#include "server/configuration.h" +#include "server/well_known_headers.h" + +#include "absl/status/status.h" + +namespace Nighthawk { +namespace Server { + +/** + * Shorthand and canonical representation of the effective filter configuration. Either a status + * or a shared pointer to the effective configuration. We use a shared pointer to avoid copying + * in the static configuration flow. + */ +using EffectiveFilterConfiguration = + absl::StatusOr>; + +/** + * Provides functionality for parsing and merging request-header based configuration, as well as + * generating a common error response accross all extensions. + */ +class FilterConfigurationBase { +public: + /** + * @brief Construct a new Filter Configuration Base object + * + * @param proto_config the static disk-based response options configuration + * @param filter_name name of the extension that is consuming this. Used during error response + * generation. + */ + FilterConfigurationBase(nighthawk::server::ResponseOptions static_proto_config, + absl::string_view filter_name); + + /** + * Copmute the effective configuration, based on considering the static configuration as well as + * any configuration provided via request headers. + * + * @param request_headers Full set of request headers to be inspected for configuration. + */ + void computeEffectiveConfiguration(const Envoy::Http::RequestHeaderMap& request_headers); + + /** + * Send an error reply based on status of the effective configuration. For example, when dynamic + * configuration delivered via request headers could not be parsed or was out of spec. + * + * @param decoder_callbacks Decoder used to generate the reply. + * @return true iff an error reply was generated. + */ + bool maybeSendErrorReply(Envoy::Http::StreamDecoderFilterCallbacks& decoder_callbacks) const; + + /** + * @brief Get the effective configuration. Depending on state ,this could be one of static + * configuration, dynamic configuration, or an error status. + * + * @return const EffectiveFilterConfiguration The effective configuration, or an error status. + */ + const EffectiveFilterConfiguration getEffectiveConfiguration() const { return effective_config_; } + + /** + * @return absl::string_view Name of the filter that constructed this instance. + */ + absl::string_view filter_name() const { return filter_name_; } + +private: + const std::string filter_name_; + const std::shared_ptr server_config_; + EffectiveFilterConfiguration effective_config_; +}; + +} // namespace Server +} // namespace Nighthawk diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index cf01105cf..1e0eb0994 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -14,7 +14,7 @@ namespace Server { HttpTestServerDecoderFilterConfig::HttpTestServerDecoderFilterConfig( nighthawk::server::ResponseOptions proto_config) - : server_config_(std::move(proto_config)) {} + : FilterConfigurationBase(std::move(proto_config), "test-server") {} HttpTestServerDecoderFilter::HttpTestServerDecoderFilter( HttpTestServerDecoderFilterConfigSharedPtr config) @@ -22,43 +22,33 @@ HttpTestServerDecoderFilter::HttpTestServerDecoderFilter( void HttpTestServerDecoderFilter::onDestroy() {} -void HttpTestServerDecoderFilter::sendReply() { - if (!json_merge_error_) { - std::string response_body(base_config_.response_body_size(), 'a'); - if (request_headers_dump_.has_value()) { - response_body += *request_headers_dump_; - } - decoder_callbacks_->sendLocalReply( - static_cast(200), response_body, - [this](Envoy::Http::ResponseHeaderMap& direct_response_headers) { - Configuration::applyConfigToResponseHeaders(direct_response_headers, base_config_); - }, - absl::nullopt, ""); - } else { - decoder_callbacks_->sendLocalReply( - static_cast(500), - fmt::format("test-server didn't understand the request: {}", error_message_), nullptr, - absl::nullopt, ""); +void HttpTestServerDecoderFilter::sendReply(const nighthawk::server::ResponseOptions& options) { + std::string response_body(options.response_body_size(), 'a'); + if (request_headers_dump_.has_value()) { + response_body += *request_headers_dump_; } + decoder_callbacks_->sendLocalReply( + static_cast(200), response_body, + [options](Envoy::Http::ResponseHeaderMap& direct_response_headers) { + Configuration::applyConfigToResponseHeaders(direct_response_headers, options); + }, + absl::nullopt, ""); } Envoy::Http::FilterHeadersStatus HttpTestServerDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& headers, bool end_stream) { - // TODO(oschaaf): Add functionality to clear fields - base_config_ = config_->server_config(); - const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); - if (request_config_header) { - json_merge_error_ = !Configuration::mergeJsonConfig( - request_config_header->value().getStringView(), base_config_, error_message_); - } - if (base_config_.echo_request_headers()) { - std::stringstream headers_dump; - headers_dump << "\nRequest Headers:\n" << headers; - request_headers_dump_ = headers_dump.str(); - } + config_->computeEffectiveConfiguration(headers); if (end_stream) { - sendReply(); + if (!config_->maybeSendErrorReply(*decoder_callbacks_)) { + const auto effective_config = config_->getEffectiveConfiguration(); + if (effective_config.value()->echo_request_headers()) { + std::stringstream headers_dump; + headers_dump << "\nRequest Headers:\n" << headers; + request_headers_dump_ = headers_dump.str(); + } + sendReply(*effective_config.value()); + } } return Envoy::Http::FilterHeadersStatus::StopIteration; } @@ -66,7 +56,9 @@ HttpTestServerDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& header Envoy::Http::FilterDataStatus HttpTestServerDecoderFilter::decodeData(Envoy::Buffer::Instance&, bool end_stream) { if (end_stream) { - sendReply(); + if (!config_->maybeSendErrorReply(*decoder_callbacks_)) { + sendReply(*config_->getEffectiveConfiguration().value()); + } } return Envoy::Http::FilterDataStatus::StopIterationNoBuffer; } diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 6f8d2ace1..927f7ee2b 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -6,17 +6,15 @@ #include "api/server/response_options.pb.h" +#include "server/http_filter_config_base.h" + namespace Nighthawk { namespace Server { // Basically this is left in as a placeholder for further configuration. -class HttpTestServerDecoderFilterConfig { +class HttpTestServerDecoderFilterConfig : public FilterConfigurationBase { public: HttpTestServerDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config); - const nighthawk::server::ResponseOptions& server_config() { return server_config_; } - -private: - const nighthawk::server::ResponseOptions server_config_; }; using HttpTestServerDecoderFilterConfigSharedPtr = @@ -36,12 +34,9 @@ class HttpTestServerDecoderFilter : public Envoy::Http::StreamDecoderFilter { void setDecoderFilterCallbacks(Envoy::Http::StreamDecoderFilterCallbacks&) override; private: - void sendReply(); + void sendReply(const nighthawk::server::ResponseOptions& options); const HttpTestServerDecoderFilterConfigSharedPtr config_; Envoy::Http::StreamDecoderFilterCallbacks* decoder_callbacks_; - nighthawk::server::ResponseOptions base_config_; - bool json_merge_error_{false}; - std::string error_message_; absl::optional request_headers_dump_; }; diff --git a/source/server/http_time_tracking_filter.cc b/source/server/http_time_tracking_filter.cc index a17b9cb98..36e6e041c 100644 --- a/source/server/http_time_tracking_filter.cc +++ b/source/server/http_time_tracking_filter.cc @@ -17,7 +17,7 @@ namespace Server { HttpTimeTrackingFilterConfig::HttpTimeTrackingFilterConfig( nighthawk::server::ResponseOptions proto_config) - : server_config_(std::move(proto_config)), + : FilterConfigurationBase(std::move(proto_config), "time-tracking"), stopwatch_(std::make_unique()) {} uint64_t @@ -29,28 +29,28 @@ HttpTimeTrackingFilter::HttpTimeTrackingFilter(HttpTimeTrackingFilterConfigShare : config_(std::move(config)) {} Envoy::Http::FilterHeadersStatus -HttpTimeTrackingFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& headers, bool /*end_stream*/) { - base_config_ = config_->server_config(); - const auto* request_config_header = headers.get(TestServer::HeaderNames::get().TestServerConfig); - if (request_config_header) { - json_merge_error_ = !Configuration::mergeJsonConfig( - request_config_header->value().getStringView(), base_config_, error_message_); - if (json_merge_error_) { - decoder_callbacks_->sendLocalReply( - static_cast(500), - fmt::format("time-tracking didn't understand the request: {}", error_message_), nullptr, - absl::nullopt, ""); - return Envoy::Http::FilterHeadersStatus::StopIteration; - } +HttpTimeTrackingFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& headers, bool end_stream) { + config_->computeEffectiveConfiguration(headers); + if (end_stream && config_->maybeSendErrorReply(*decoder_callbacks_)) { + return Envoy::Http::FilterHeadersStatus::StopIteration; } return Envoy::Http::FilterHeadersStatus::Continue; } +Envoy::Http::FilterDataStatus HttpTimeTrackingFilter::decodeData(Envoy::Buffer::Instance&, + bool end_stream) { + if (end_stream && config_->maybeSendErrorReply(*decoder_callbacks_)) { + return Envoy::Http::FilterDataStatus::StopIterationNoBuffer; + } + return Envoy::Http::FilterDataStatus::Continue; +} + Envoy::Http::FilterHeadersStatus HttpTimeTrackingFilter::encodeHeaders(Envoy::Http::ResponseHeaderMap& response_headers, bool) { - if (!json_merge_error_) { + const auto effective_config = config_->getEffectiveConfiguration(); + if (effective_config.ok()) { const std::string previous_request_delta_in_response_header = - base_config_.emit_previous_request_delta_in_response_header(); + effective_config.value()->emit_previous_request_delta_in_response_header(); if (!previous_request_delta_in_response_header.empty() && last_request_delta_ns_ > 0) { response_headers.appendCopy( Envoy::Http::LowerCaseString(previous_request_delta_in_response_header), diff --git a/source/server/http_time_tracking_filter.h b/source/server/http_time_tracking_filter.h index b3148fed3..5a2288a9b 100644 --- a/source/server/http_time_tracking_filter.h +++ b/source/server/http_time_tracking_filter.h @@ -11,6 +11,8 @@ #include "api/server/response_options.pb.h" +#include "server/http_filter_config_base.h" + namespace Nighthawk { namespace Server { @@ -18,7 +20,7 @@ namespace Server { * Filter configuration container class for the time tracking extension. * Instances of this class will be shared accross instances of HttpTimeTrackingFilter. */ -class HttpTimeTrackingFilterConfig { +class HttpTimeTrackingFilterConfig : public FilterConfigurationBase { public: /** * Constructs a new HttpTimeTrackingFilterConfig instance. @@ -27,12 +29,6 @@ class HttpTimeTrackingFilterConfig { */ HttpTimeTrackingFilterConfig(nighthawk::server::ResponseOptions proto_config); - /** - * @return const nighthawk::server::ResponseOptions& read-only reference to the proto config - * object. - */ - const nighthawk::server::ResponseOptions& server_config() { return server_config_; } - /** * Gets the number of elapsed nanoseconds since the last call (server wide). * Safe to use concurrently. @@ -44,7 +40,6 @@ class HttpTimeTrackingFilterConfig { uint64_t getElapsedNanosSinceLastRequest(Envoy::TimeSource& time_source); private: - const nighthawk::server::ResponseOptions server_config_; std::unique_ptr stopwatch_; }; @@ -65,6 +60,7 @@ class HttpTimeTrackingFilter : public Envoy::Http::PassThroughFilter { // Http::StreamDecoderFilter Envoy::Http::FilterHeadersStatus decodeHeaders(Envoy::Http::RequestHeaderMap& headers, bool /*end_stream*/) override; + Envoy::Http::FilterDataStatus decodeData(Envoy::Buffer::Instance&, bool) override; void setDecoderFilterCallbacks(Envoy::Http::StreamDecoderFilterCallbacks&) override; // Http::StreamEncoderFilter @@ -72,9 +68,6 @@ class HttpTimeTrackingFilter : public Envoy::Http::PassThroughFilter { private: const HttpTimeTrackingFilterConfigSharedPtr config_; - nighthawk::server::ResponseOptions base_config_; - bool json_merge_error_{false}; - std::string error_message_; uint64_t last_request_delta_ns_; }; diff --git a/test/integration/configurations/nighthawk_http_origin.yaml b/test/integration/configurations/nighthawk_http_origin.yaml index 08247b781..4854523de 100644 --- a/test/integration/configurations/nighthawk_http_origin.yaml +++ b/test/integration/configurations/nighthawk_http_origin.yaml @@ -23,14 +23,8 @@ static_resources: domains: - "*" http_filters: + - name: time-tracking - name: dynamic-delay - - name: envoy.fault - config: - max_active_faults: 100 - delay: - header_delay: {} - percentage: - numerator: 100 - name: test-server config: response_body_size: 10 diff --git a/test/integration/test_integration_basics.py b/test/integration/test_integration_basics.py index 0a9a4afd9..7cb450a12 100644 --- a/test/integration/test_integration_basics.py +++ b/test/integration/test_integration_basics.py @@ -414,8 +414,11 @@ def test_cli_output_format(http_test_server_fixture): asserts.assertIn("Percentile", output) -def test_request_body_gets_transmitted(http_test_server_fixture): - """Test request body transmission. +@pytest.mark.parametrize( + 'filter_configs', + ["{}", "{static_delay: \"0.01s\"}", "{emit_previous_request_delta_in_response_header: \"aa\"}"]) +def test_request_body_gets_transmitted(http_test_server_fixture, filter_configs): + """Test request body transmission with. Ensure that the number of bytes we request for the request body gets reflected in the upstream connection transmitted bytes counter for h1 and h2. @@ -434,13 +437,17 @@ def check_upload_expectations(fixture, parsed_json, expected_transmitted_bytes, expected_received_bytes) upload_bytes = 1024 * 1024 * 3 + # TODO(XXX): The dynamic-delay extension hangs unless we lower the request entity body size. + if "static_delay" in filter_configs: + upload_bytes = int(upload_bytes / 3) requests = 10 args = [ http_test_server_fixture.getTestServerRootUri(), "--duration", "100", "--rps", "100", "--request-body-size", str(upload_bytes), "--termination-predicate", "benchmark.http_2xx:%s" % str(requests), "--connections", "1", "--request-method", "POST", - "--max-active-requests", "1" + "--max-active-requests", "1", "--request-header", + "x-nighthawk-test-server-config:%s" % filter_configs ] # Test we transmit the expected amount of bytes with H1 parsed_json, _ = http_test_server_fixture.runNighthawkClient(args) diff --git a/test/server/BUILD b/test/server/BUILD index 0ea078851..68717484b 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -1,6 +1,7 @@ load( "@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", + "envoy_cc_test_library", "envoy_package", ) @@ -8,6 +9,16 @@ licenses(["notice"]) # Apache 2 envoy_package() +envoy_cc_test_library( + name = "http_filter_integration_test_base_lib", + srcs = ["http_filter_integration_test_base.cc"], + hdrs = ["http_filter_integration_test_base.h"], + repository = "@envoy", + deps = [ + "@envoy//test/integration:http_integration_lib", + ], +) + envoy_cc_test( name = "http_test_server_filter_integration_test", srcs = ["http_test_server_filter_integration_test.cc"], @@ -25,9 +36,9 @@ envoy_cc_test( srcs = ["http_dynamic_delay_filter_integration_test.cc"], repository = "@envoy", deps = [ + ":http_filter_integration_test_base_lib", "//source/server:http_dynamic_delay_filter_config", "@envoy//source/common/api:api_lib_with_external_headers", - "@envoy//test/integration:http_integration_lib", ], ) @@ -36,10 +47,10 @@ envoy_cc_test( srcs = ["http_time_tracking_filter_integration_test.cc"], repository = "@envoy", deps = [ + ":http_filter_integration_test_base_lib", "//source/server:http_time_tracking_filter_config", "@envoy//include/envoy/upstream:cluster_manager_interface_with_external_headers", "@envoy//source/common/api:api_lib_with_external_headers", - "@envoy//test/integration:http_integration_lib", "@envoy//test/test_common:simulated_time_system_lib", ], ) diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index c4a38e429..250a6ae3b 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -1,16 +1,18 @@ #include -#include "external/envoy/test/integration/http_integration.h" - #include "api/server/response_options.pb.h" #include "server/configuration.h" #include "server/http_dynamic_delay_filter.h" +#include "test/server/http_filter_integration_test_base.h" + #include "gtest/gtest.h" namespace Nighthawk { +const Envoy::Http::LowerCaseString kDelayHeaderString("x-envoy-fault-delay-request"); + /** * Support class for testing the dynamic delay filter. We rely on the fault filter for * inducing the actual delay, so this aims to prove that: @@ -21,56 +23,12 @@ namespace Nighthawk { * - TODO(#393): An end to end test which proves that the interaction between this filter * and the fault filter work as expected. */ + class HttpDynamicDelayIntegrationTest - : public Envoy::HttpIntegrationTest, + : public HttpFilterIntegrationTestBase, public testing::TestWithParam { -protected: - HttpDynamicDelayIntegrationTest() - : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, GetParam()), - request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}), - delay_header_string_(Envoy::Http::LowerCaseString("x-envoy-fault-delay-request")) {} - - // We don't override SetUp(): tests in this file will call setup() instead to avoid having to - // create a fixture per filter configuration. - void setup(const std::string& config) { - config_helper_.addFilter(config); - HttpIntegrationTest::initialize(); - } - - // Fetches a response with request-level configuration set in the request header. - Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, - bool setup_for_upstream_request = true) { - const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); - Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; - request_headers.setCopy(key, request_level_config); - return getResponse(request_headers, setup_for_upstream_request); - } - - // Fetches a response with the default request headers, expecting the fake upstream to supply - // the response. - Envoy::IntegrationStreamDecoderPtr getResponse() { return getResponse(request_headers_); } - - // Fetches a response using the provided request headers. When setup_for_upstream_request - // is true, the expectation will be that an upstream request will be needed to provide a - // response. If it is set to false, the extension is expected to supply the response, and - // no upstream request ought to occur. - Envoy::IntegrationStreamDecoderPtr - getResponse(const Envoy::Http::TestRequestHeaderMapImpl& request_headers, - bool setup_for_upstream_request = true) { - cleanupUpstreamAndDownstream(); - codec_client_ = makeHttpConnection(lookupPort("http")); - Envoy::IntegrationStreamDecoderPtr response; - if (setup_for_upstream_request) { - response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - } else { - response = codec_client_->makeHeaderOnlyRequest(request_headers); - response->waitForEndStream(); - } - return response; - } - - const Envoy::Http::TestRequestHeaderMapImpl request_headers_; - const Envoy::Http::LowerCaseString delay_header_string_; +public: + HttpDynamicDelayIntegrationTest() : HttpFilterIntegrationTestBase(GetParam()){}; }; INSTANTIATE_TEST_SUITE_P(IpVersions, HttpDynamicDelayIntegrationTest, @@ -85,14 +43,13 @@ name: dynamic-delay )"); // Don't send any config request header getResponse(); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_), nullptr); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header with an empty / default config. Should be a no-op. getResponse("{}"); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_), nullptr); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header, this should become effective. getResponse("{static_delay: \"1.6s\"}"); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_)->value().getStringView(), - "1600"); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1600"); // Send a malformed config request header. This ought to shortcut and directly reply, // hence we don't expect an upstream request. @@ -111,6 +68,35 @@ name: dynamic-delay "parse JSON as proto (INVALID_ARGUMENT:Unexpected end of string. Expected a value.\n\n^): "); } +// Verify the filter is well-behaved when it comes to requests with an entity body. +// This takes a slightly different code path, so it is important to test this explicitly. +TEST_P(HttpDynamicDelayIntegrationTest, BehaviorWithRequestBody) { + setup(R"EOF( +name: dynamic-delay +typed_config: + "@type": type.googleapis.com/nighthawk.server.ResponseOptions + static_delay: 0.1s +)EOF"); + Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; + request_headers.setMethod("POST"); + + // Post without any request-level configuration. Should succeed. + getResponse(request_headers, true); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "100"); + + // Post with bad request-level configuration. The extension should response directly with an + // error. + const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); + request_headers.setCopy(key, "bad_json"); + fake_upstreams_[0]->set_allow_unexpected_disconnects(true); + auto response = getResponse(request_headers, false); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_EQ( + response->body(), + "dynamic-delay didn't understand the request: Error merging json config: Unable to parse " + "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); +} + // Verify expectations with static/file-based static_delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, StaticConfigurationStaticDelay) { setup(R"EOF( @@ -120,22 +106,19 @@ name: dynamic-delay static_delay: 1.33s )EOF"); getResponse(); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_)->value().getStringView(), - "1330"); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); getResponse("{}"); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_)->value().getStringView(), - "1330"); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); getResponse("{static_delay: \"0.2s\"}"); // TODO(#392): This fails, because the duration is a two-field message: it would make here to see // both the number of seconds and nanoseconds to be overridden. // However, the seconds part is set to '0', which equates to the default of the underlying int // type, and the fact that we are using proto3, which doesn't merge default values. // Hence the following expectation will fail, as it yields 1200 instead of the expected 200. - // EXPECT_EQ(upstream_request_->headers().get(delay_header_string_)->value().getStringView(), + // EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), // "200"); getResponse("{static_delay: \"2.2s\"}"); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_)->value().getStringView(), - "2200"); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "2200"); } // Verify expectations with static/file-based concurrency_based_linear_delay configuration. @@ -149,7 +132,7 @@ name: dynamic-delay concurrency_delay_factor: 0.01s )EOF"); getResponse(); - EXPECT_EQ(upstream_request_->headers().get(delay_header_string_)->value().getStringView(), "60"); + EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "60"); } class ComputeTest : public testing::Test { diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc new file mode 100644 index 000000000..e03a779a4 --- /dev/null +++ b/test/server/http_filter_integration_test_base.cc @@ -0,0 +1,60 @@ +#include "test/server/http_filter_integration_test_base.h" + +#include "gtest/gtest.h" + +namespace Nighthawk { + +HttpFilterIntegrationTestBase::HttpFilterIntegrationTestBase( + Envoy::Network::Address::IpVersion ip_version) + : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, ip_version), + request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} + +// We don't override SetUp(): tests in this file will call setup() instead to avoid having to +// create a fixture per filter configuration. +void HttpFilterIntegrationTestBase::setup(const std::string& config) { + config_helper_.addFilter(config); + HttpIntegrationTest::initialize(); +} + +// Fetches a response with request-level configuration set in the request header. +Envoy::IntegrationStreamDecoderPtr +HttpFilterIntegrationTestBase::getResponse(absl::string_view request_level_config, + bool setup_for_upstream_request) { + const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); + Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; + request_headers.setCopy(key, request_level_config); + return getResponse(request_headers, setup_for_upstream_request); +} + +// Fetches a response with the default request headers, expecting the fake upstream to supply +// the response. +Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponse() { + return getResponse(request_headers_); +} + +// Fetches a response using the provided request headers. When setup_for_upstream_request +// is true, the expectation will be that an upstream request will be needed to provide a +// response. If it is set to false, the extension is expected to supply the response, and +// no upstream request ought to occur. +Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponse( + const Envoy::Http::TestRequestHeaderMapImpl& request_headers, bool setup_for_upstream_request) { + cleanupUpstreamAndDownstream(); + codec_client_ = makeHttpConnection(lookupPort("http")); + Envoy::IntegrationStreamDecoderPtr response; + const bool is_post = request_headers.Method()->value().getStringView() == "POST"; + const uint64_t request_body_size = is_post ? 1024 : 0; + if (setup_for_upstream_request) { + response = sendRequestAndWaitForResponse(request_headers, request_body_size, + default_response_headers_, 0); + } else { + if (is_post) { + response = codec_client_->makeRequestWithBody(request_headers, request_body_size); + } else { + response = codec_client_->makeHeaderOnlyRequest(request_headers); + } + response->waitForEndStream(); + } + return response; +} + +} // namespace Nighthawk diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h new file mode 100644 index 000000000..568cb0274 --- /dev/null +++ b/test/server/http_filter_integration_test_base.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "external/envoy/test/integration/http_integration.h" + +namespace Nighthawk { + +class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { +protected: + HttpFilterIntegrationTestBase(Envoy::Network::Address::IpVersion ip_version); + + // We don't override SetUp(): tests in this file will call setup() instead to avoid having to + // create a fixture per filter configuration. + void setup(const std::string& config); + + // Fetches a response with request-level configuration set in the request header. + Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, + bool setup_for_upstream_request = true); + + // Fetches a response with the default request headers, expecting the fake upstream to supply + // the response. + Envoy::IntegrationStreamDecoderPtr getResponse(); + + // Fetches a response using the provided request headers. When setup_for_upstream_request + // is true, the expectation will be that an upstream request will be needed to provide a + // response. If it is set to false, the extension is expected to supply the response, and + // no upstream request ought to occur. + Envoy::IntegrationStreamDecoderPtr + getResponse(const Envoy::Http::TestRequestHeaderMapImpl& request_headers, + bool setup_for_upstream_request = true); + + const Envoy::Http::TestRequestHeaderMapImpl request_headers_; +}; + +} // namespace Nighthawk diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 702b77c3c..ca5bf273a 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -312,9 +312,10 @@ TEST_F(HttpTestServerDecoderFilterTest, HeaderMerge) { Server::HttpTestServerDecoderFilterConfigSharedPtr config = std::make_shared(initial_options); Server::HttpTestServerDecoderFilter f(config); - std::string error_message; - nighthawk::server::ResponseOptions options = config->server_config(); + Server::EffectiveFilterConfiguration options_or = config->getEffectiveConfiguration(); + ASSERT_TRUE(options_or.ok()); + nighthawk::server::ResponseOptions options = *options_or.value(); EXPECT_EQ(1, options.response_headers_size()); EXPECT_EQ("foo", options.response_headers(0).header().key()); @@ -326,6 +327,7 @@ TEST_F(HttpTestServerDecoderFilterTest, HeaderMerge) { EXPECT_TRUE(Envoy::TestUtility::headerMapEqualIgnoreOrder( header_map, Envoy::Http::TestResponseHeaderMapImpl{{":status", "200"}, {"foo", "bar1"}})); + std::string error_message; EXPECT_TRUE(Server::Configuration::mergeJsonConfig( R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: false } ]})", options, error_message)); diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 55d08e9c9..ad6014f4b 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -4,7 +4,6 @@ #include "envoy/upstream/upstream.h" #include "external/envoy/test/common/upstream/utility.h" -#include "external/envoy/test/integration/http_integration.h" #include "external/envoy/test/test_common/simulated_time_system.h" #include "api/server/response_options.pb.h" @@ -14,6 +13,8 @@ #include "server/http_time_tracking_filter.h" #include "server/well_known_headers.h" +#include "test/server/http_filter_integration_test_base.h" + #include "gtest/gtest.h" namespace Nighthawk { @@ -32,53 +33,10 @@ name: time-tracking )EOF"; class HttpTimeTrackingIntegrationTest - : public Envoy::HttpIntegrationTest, + : public HttpFilterIntegrationTestBase, public testing::TestWithParam { -protected: - HttpTimeTrackingIntegrationTest() - : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, GetParam()), - request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} - - // We don't override SetUp(): tests in this file will call setup() instead to avoid having to - // create a fixture per filter configuration. - void setup(const std::string& config) { - config_helper_.addFilter(config); - HttpIntegrationTest::initialize(); - } - - // Fetches a response with request-level configuration set in the request header. - Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, - bool setup_for_upstream_request = true) { - Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; - request_headers.setCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, - request_level_config); - return getResponse(request_headers, setup_for_upstream_request); - } - - // Fetches a response with the default request headers, expecting the fake upstream to supply - // the response. - Envoy::IntegrationStreamDecoderPtr getResponse() { return getResponse(request_headers_); } - - // Fetches a response using the provided request headers. When setup_for_upstream_request - // is true, the expectation will be that an upstream request will be needed to provide a - // response. If it is set to false, the extension is expected to supply the response, and - // no upstream request ought to occur. - Envoy::IntegrationStreamDecoderPtr - getResponse(const Envoy::Http::TestRequestHeaderMapImpl& request_headers, - bool setup_for_upstream_request = true) { - cleanupUpstreamAndDownstream(); - codec_client_ = makeHttpConnection(lookupPort("http")); - Envoy::IntegrationStreamDecoderPtr response; - if (setup_for_upstream_request) { - response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); - } else { - response = codec_client_->makeHeaderOnlyRequest(request_headers); - response->waitForEndStream(); - } - return response; - } - - const Envoy::Http::TestRequestHeaderMapImpl request_headers_; +public: + HttpTimeTrackingIntegrationTest() : HttpFilterIntegrationTestBase(GetParam()){}; }; INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, @@ -139,6 +97,35 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguratio "parse JSON as proto (INVALID_ARGUMENT:Unexpected end of string. Expected a value.\n\n^): "); } +// Verify the filter is well-behaved when it comes to requests with an entity body. +// This takes a slightly different code path, so it is important to test this explicitly. +TEST_P(HttpTimeTrackingIntegrationTest, BehaviorWithRequestBody) { + setup(fmt::format(kProtoConfigTemplate, "")); + Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; + request_headers.setMethod("POST"); + + // Post without any request-level configuration. Should succeed. + getResponse(request_headers, true); + + Envoy::IntegrationStreamDecoderPtr response = + getResponse(fmt::format("{{{}}}", kDefaultProtoFragment)); + const Envoy::Http::HeaderEntry* latency_header = + response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); + ASSERT_NE(latency_header, nullptr); + + // Post with bad request-level configuration. The extension should response directly with an + // error. + const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); + request_headers.setCopy(key, "bad_json"); + fake_upstreams_[0]->set_allow_unexpected_disconnects(true); + response = getResponse(request_headers, false); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_EQ( + response->body(), + "time-tracking didn't understand the request: Error merging json config: Unable to parse " + "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); +} + class HttpTimeTrackingFilterConfigTest : public testing::Test, public Envoy::Event::TestUsingSimulatedTime {}; From 68ed58affe29eaddd11b5d2e5486a1d9fad746c7 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 9 Sep 2020 11:15:03 +0200 Subject: [PATCH 29/48] Review feedback Signed-off-by: Otto van der Schaaf --- README.md | 17 ++++++------- api/client/options.proto | 10 ++++---- source/client/benchmark_client_impl.cc | 6 ++--- source/client/benchmark_client_impl.h | 4 ++-- source/client/options_impl.cc | 24 ++++++++++--------- source/client/options_impl.h | 4 ++-- source/client/stream_decoder.cc | 5 ++-- source/client/stream_decoder.h | 6 ++--- test/benchmark_http_client_test.cc | 5 ++-- .../nighthawk_track_timings.yaml | 4 ++++ test/integration/test_integration_basics.py | 20 ++++++++++------ test/options_test.cc | 5 ++-- 12 files changed, 61 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 023ef1a47..a64914e0f 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ bazel build -c opt //:nighthawk ``` USAGE: -bazel-bin/nighthawk_client [--response-header-with-latency-input -] [--stats-flush-interval -] [--stats-sinks ] ... +bazel-bin/nighthawk_client [--response-latency-header-name ] +[--stats-flush-interval ] +[--stats-sinks ] ... [--no-duration] [--simple-warmup] [--request-source ] [--label ] ... [--multi-target-use-https] @@ -81,11 +81,12 @@ format> Where: ---response-header-with-latency-input -Set an optional response header name, whose values will be tracked in -a latency histogram if set. Can be used in tandem with the test server -"emit_previous_request_delta_in_response_header" option to get a sense -of elapsed time between request arrivals. Default: "" +--response-latency-header-name +Set an optional header name that will be returned in responses, whose +values will be tracked in a latency histogram if set. Can be used in +tandem with the test server's response option +"emit_previous_request_delta_in_response_header" to record elapsed +time between request arrivals. Default: "" --stats-flush-interval Time interval (in seconds) between flushes to configured stats sinks. diff --git a/api/client/options.proto b/api/client/options.proto index db5cb3da5..a0d60fe61 100644 --- a/api/client/options.proto +++ b/api/client/options.proto @@ -202,9 +202,9 @@ message CommandLineOptions { // specified the default is 5 seconds. Time interval must be at least 1s and at most 300s. google.protobuf.UInt32Value stats_flush_interval = 35 [(validate.rules).uint32 = {gte: 1, lte: 300}]; - // Set an optional response header name, whose values will be tracked in a latency histogram if - // set. Can be used in tandem with the test server's - // "emit_previous_request_delta_in_response_header" option to get a sense of elapsed time between - // request arrivals. - google.protobuf.StringValue response_header_with_latency_input = 36; + // Set an optional header name that will be returned in responses, whose values will be tracked in + // a latency histogram if set. Can be used in tandem with the test server's response option + // "emit_previous_request_delta_in_response_header" to record elapsed time between request + // arrivals. + google.protobuf.StringValue response_latency_header_name = 36; } diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 163d21f0d..792178c7f 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -84,14 +84,14 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( Envoy::Upstream::ClusterManagerPtr& cluster_manager, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, const bool provide_resource_backpressure, - absl::string_view response_header_with_latency_input) + absl::string_view response_latency_header_name) : api_(api), dispatcher_(dispatcher), scope_(scope.createScope("benchmark.")), statistic_(std::move(statistic)), use_h2_(use_h2), benchmark_client_counters_({ALL_BENCHMARK_CLIENT_COUNTERS(POOL_COUNTER(*scope_))}), cluster_manager_(cluster_manager), http_tracer_(http_tracer), cluster_name_(std::string(cluster_name)), request_generator_(std::move(request_generator)), provide_resource_backpressure_(provide_resource_backpressure), - response_header_with_latency_input_(response_header_with_latency_input) { + response_latency_header_name_(response_latency_header_name) { statistic_.connect_statistic->setId("benchmark_http_client.queue_to_connect"); statistic_.response_statistic->setId("benchmark_http_client.request_to_response"); statistic_.response_header_size_statistic->setId("benchmark_http_client.response_header_size"); @@ -168,7 +168,7 @@ bool BenchmarkClientHttpImpl::tryStartRequest(CompletionCallback caller_completi *statistic_.connect_statistic, *statistic_.response_statistic, *statistic_.response_header_size_statistic, *statistic_.response_body_size_statistic, *statistic_.origin_latency_statistic, request->header(), shouldMeasureLatencies(), - content_length, generator_, http_tracer_, response_header_with_latency_input_); + content_length, generator_, http_tracer_, response_latency_header_name_); requests_initiated_++; pool_ptr->newStream(*stream_decoder, *stream_decoder); return true; diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index f3bac8228..5b7285f6d 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -107,7 +107,7 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, const bool provide_resource_backpressure, - absl::string_view response_header_with_latency_input); + absl::string_view response_latency_header_name); void setConnectionLimit(uint32_t connection_limit) { connection_limit_ = connection_limit; } void setMaxPendingRequests(uint32_t max_pending_requests) { max_pending_requests_ = max_pending_requests; @@ -163,7 +163,7 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, std::string cluster_name_; const RequestGenerator request_generator_; const bool provide_resource_backpressure_; - const std::string response_header_with_latency_input_; + const std::string response_latency_header_name_; }; } // namespace Client diff --git a/source/client/options_impl.cc b/source/client/options_impl.cc index 1bcbadf52..eb6b15947 100644 --- a/source/client/options_impl.cc +++ b/source/client/options_impl.cc @@ -294,12 +294,14 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { stats_flush_interval_), false, 5, "uint32_t", cmd); - TCLAP::ValueArg response_header_with_latency_input( - "", "response-header-with-latency-input", - "Set an optional response header name, whose values will be tracked in a latency histogram " - "if set. Can be used in tandem with the test server " - "\"emit_previous_request_delta_in_response_header\" option to get a sense of elapsed time " - "between request arrivals. Default: \"\"", + TCLAP::ValueArg response_latency_header_name( + "", "response-latency-header-name", + "Set an optional header name that will be returned in responses, whose values will be " + "tracked in a latency histogram if set. " + "Can be used in tandem with the test server's response option " + "\"emit_previous_request_delta_in_response_header\" to record elapsed time between request " + "arrivals. " + "Default: \"\"", false, "", "string", cmd); Utility::parseCommand(cmd, argc, argv); @@ -433,7 +435,7 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { } } TCLAP_SET_IF_SPECIFIED(stats_flush_interval, stats_flush_interval_); - TCLAP_SET_IF_SPECIFIED(response_header_with_latency_input, response_header_with_latency_input_); + TCLAP_SET_IF_SPECIFIED(response_latency_header_name, response_latency_header_name_); // CLI-specific tests. // TODO(oschaaf): as per mergconflicts's remark, it would be nice to aggregate @@ -619,8 +621,8 @@ OptionsImpl::OptionsImpl(const nighthawk::client::CommandLineOptions& options) { no_duration_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(options, no_duration, no_duration_); } std::copy(options.labels().begin(), options.labels().end(), std::back_inserter(labels_)); - response_header_with_latency_input_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - options, response_header_with_latency_input, response_header_with_latency_input_); + response_latency_header_name_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( + options, response_latency_header_name, response_latency_header_name_); validate(); } @@ -793,8 +795,8 @@ CommandLineOptionsPtr OptionsImpl::toCommandLineOptionsInternal() const { *command_line_options->add_stats_sinks() = stats_sink; } command_line_options->mutable_stats_flush_interval()->set_value(stats_flush_interval_); - command_line_options->mutable_response_header_with_latency_input()->set_value( - response_header_with_latency_input_); + command_line_options->mutable_response_latency_header_name()->set_value( + response_latency_header_name_); return command_line_options; } diff --git a/source/client/options_impl.h b/source/client/options_impl.h index df7f634ad..2cac61aa2 100644 --- a/source/client/options_impl.h +++ b/source/client/options_impl.h @@ -86,7 +86,7 @@ class OptionsImpl : public Options, public Envoy::Logger::Loggable stats_sinks_; uint32_t stats_flush_interval_{5}; - std::string response_header_with_latency_input_; + std::string response_latency_header_name_; }; } // namespace Client diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index d5911243f..1b15d4f9e 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -19,9 +19,8 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b response_header_sizes_statistic_.addValue(response_headers_->byteSize()); const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); - if (!response_header_with_latency_input_.empty()) { - const auto timing_header_name = - Envoy::Http::LowerCaseString(response_header_with_latency_input_); + if (!response_latency_header_name_.empty()) { + const auto timing_header_name = Envoy::Http::LowerCaseString(response_latency_header_name_); const Envoy::Http::HeaderEntry* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { absl::string_view timing_value = timing_header->value().getStringView(); diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index a92d11d36..6f04b1530 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -47,7 +47,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size, Envoy::Random::RandomGenerator& random_generator, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, - std::string response_header_with_latency_input) + absl::string_view response_latency_header_name) : dispatcher_(dispatcher), time_source_(time_source), decoder_completion_callback_(decoder_completion_callback), caller_completion_callback_(std::move(caller_completion_callback)), @@ -59,7 +59,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, complete_(false), measure_latencies_(measure_latencies), request_body_size_(request_body_size), stream_info_(time_source_), random_generator_(random_generator), http_tracer_(http_tracer), - response_header_with_latency_input_(std::move(response_header_with_latency_input)) { + response_latency_header_name_(response_latency_header_name) { if (measure_latencies_ && http_tracer_ != nullptr) { setupForTracing(); } @@ -121,7 +121,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, Envoy::Tracing::HttpTracerSharedPtr& http_tracer_; Envoy::Tracing::SpanPtr active_span_; Envoy::StreamInfo::UpstreamTiming upstream_timing_; - const std::string response_header_with_latency_input_; + const std::string response_latency_header_name_; }; } // namespace Client diff --git a/test/benchmark_http_client_test.cc b/test/benchmark_http_client_test.cc index ab3ec15a0..f61f0f8a6 100644 --- a/test/benchmark_http_client_test.cc +++ b/test/benchmark_http_client_test.cc @@ -178,8 +178,9 @@ class BenchmarkClientHttpTest : public Test { // verifyBenchmarkClientProcessesExpectedInflightRequests. void setupBenchmarkClient(const RequestGenerator& request_generator) { client_ = std::make_unique( - *api_, *dispatcher_, store_, statistic_, false, cluster_manager_, http_tracer_, "benchmark", - request_generator, true, ""); + *api_, *dispatcher_, store_, statistic_, /*use_h2*/ false, cluster_manager_, http_tracer_, + "benchmark", request_generator, /*provide_resource_backpressure*/ true, + /*response_header_with_latency_input=*/""); } uint64_t getCounter(absl::string_view name) { diff --git a/test/integration/configurations/nighthawk_track_timings.yaml b/test/integration/configurations/nighthawk_track_timings.yaml index aae01a226..7f4d1b2f5 100644 --- a/test/integration/configurations/nighthawk_track_timings.yaml +++ b/test/integration/configurations/nighthawk_track_timings.yaml @@ -1,3 +1,6 @@ +# Envoy configuration template for testing the time-tracking http filter extension. +# Sets up the time-tracking extension plus the test-server extension for generating +# responses. admin: access_log_path: $tmpdir/nighthawk-test-server-admin-access.log profile_path: $tmpdir/nighthawk-test-server.prof @@ -23,6 +26,7 @@ static_resources: domains: - "*" http_filters: + # Here we set up the time-tracking extension to emit request-arrival delta timings in a response header. - name: time-tracking config: emit_previous_request_delta_in_response_header: x-origin-request-receipt-delta diff --git a/test/integration/test_integration_basics.py b/test/integration/test_integration_basics.py index 0a9a4afd9..314c02198 100644 --- a/test/integration/test_integration_basics.py +++ b/test/integration/test_integration_basics.py @@ -700,22 +700,28 @@ def test_cancellation_with_infinite_duration(http_test_server_fixture): asserts.assertCounterGreaterEqual(counters, "benchmark.http_2xx", 1) -@pytest.mark.parametrize('server_config', - ["nighthawk/test/integration/configurations/nighthawk_track_timings.yaml"]) -def test_http_h1_response_header_latency_tracking(http_test_server_fixture): +@pytest.mark.parametrize('server_config', [ + "nighthawk/test/integration/configurations/nighthawk_http_origin.yaml", + "nighthawk/test/integration/configurations/nighthawk_track_timings.yaml" +]) +def test_http_h1_response_header_latency_tracking(http_test_server_fixture, server_config): """Test emission and tracking of response header latencies. - Run the CLI configured to track latencies delivered by response header from the test-server - which is set up emit those. Ensure the expected histogram is observed. + Run the CLI configured to track latencies delivered by response header from the test-server. + Ensure that the origin_latency_statistic histogram receives the correct number of inputs. """ parsed_json, _ = http_test_server_fixture.runNighthawkClient([ http_test_server_fixture.getTestServerRootUri(), "--connections", "1", "--rps", "100", "--duration", "100", "--termination-predicate", "benchmark.http_2xx:99", - "--response-header-with-latency-input", "x-origin-request-receipt-delta" + "--response-latency-header-name", "x-origin-request-receipt-delta" ]) global_histograms = http_test_server_fixture.getNighthawkGlobalHistogramsbyIdFromJson(parsed_json) + asserts.assertEqual(int(global_histograms["benchmark_http_client.latency_2xx"]["count"]), 100) + # Verify behavior is correct both with and without the timing filter enabled. + expected_histogram_count = 99 if "nighthawk_track_timings.yaml" in server_config else 0 asserts.assertEqual( - int(global_histograms["benchmark_http_client.origin_latency_statistic"]["count"]), 99) + int(global_histograms["benchmark_http_client.origin_latency_statistic"]["count"]), + expected_histogram_count) def _run_client_with_args(args): diff --git a/test/options_test.cc b/test/options_test.cc index 8315b7054..e4eabdfb9 100644 --- a/test/options_test.cc +++ b/test/options_test.cc @@ -118,7 +118,7 @@ TEST_F(OptionsImplTest, AlmostAll) { "--experimental-h2-use-multiple-connections " "--experimental-h1-connection-reuse-strategy lru --label label1 --label label2 {} " "--simple-warmup --stats-sinks {} --stats-sinks {} --stats-flush-interval 10 " - "--response-header-with-latency-input zz", + "--response-latency-header-name zz", client_name_, "{name:\"envoy.transport_sockets.tls\"," "typed_config:{\"@type\":\"type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext\"," @@ -248,8 +248,7 @@ TEST_F(OptionsImplTest, AlmostAll) { ASSERT_EQ(cmd->stats_sinks_size(), options->statsSinks().size()); EXPECT_TRUE(util(cmd->stats_sinks(0), options->statsSinks()[0])); EXPECT_TRUE(util(cmd->stats_sinks(1), options->statsSinks()[1])); - EXPECT_EQ(cmd->response_header_with_latency_input().value(), - options->responseHeaderWithLatencyInput()); + EXPECT_EQ(cmd->response_latency_header_name().value(), options->responseHeaderWithLatencyInput()); OptionsImpl options_from_proto(*cmd); std::string s1 = Envoy::MessageUtil::getYamlStringFromMessage( From 54ca814a6efa456307b443d9e4484a8bb143dcb2 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 9 Sep 2020 18:00:26 +0200 Subject: [PATCH 30/48] Doc & refactor integration test base Signed-off-by: Otto van der Schaaf --- ...p_dynamic_delay_filter_integration_test.cc | 28 ++--- .../http_filter_integration_test_base.cc | 42 ++++--- .../http_filter_integration_test_base.h | 112 ++++++++++++++++-- ...p_time_tracking_filter_integration_test.cc | 24 ++-- 4 files changed, 152 insertions(+), 54 deletions(-) diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index 250a6ae3b..39fe3fe7e 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -42,25 +42,25 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions )"); // Don't send any config request header - getResponse(); + getResponseFromUpstream(); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header with an empty / default config. Should be a no-op. - getResponse("{}"); + getResponseFromUpstream("{}"); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header, this should become effective. - getResponse("{static_delay: \"1.6s\"}"); + getResponseFromUpstream("{static_delay: \"1.6s\"}"); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1600"); // Send a malformed config request header. This ought to shortcut and directly reply, // hence we don't expect an upstream request. - auto response = getResponse("bad_json", false); + auto response = getResponseFromExtension("bad_json"); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), "dynamic-delay didn't understand the request: Error merging json config: Unable to parse " "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); // Send an empty config header, which ought to trigger failure mode as well. - response = getResponse("", false); + response = getResponseFromExtension(""); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -77,11 +77,11 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions static_delay: 0.1s )EOF"); - Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; - request_headers.setMethod("POST"); + Envoy::Http::TestRequestHeaderMapImpl request_headers( + {{":method", "POST"}, {":path", "/"}, {":authority", "host"}}); // Post without any request-level configuration. Should succeed. - getResponse(request_headers, true); + getResponseFromUpstream(request_headers); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "100"); // Post with bad request-level configuration. The extension should response directly with an @@ -89,7 +89,7 @@ name: dynamic-delay const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); request_headers.setCopy(key, "bad_json"); fake_upstreams_[0]->set_allow_unexpected_disconnects(true); - auto response = getResponse(request_headers, false); + auto response = getResponseFromExtension(request_headers); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -105,11 +105,11 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions static_delay: 1.33s )EOF"); - getResponse(); + getResponseFromUpstream(); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); - getResponse("{}"); + getResponseFromUpstream("{}"); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); - getResponse("{static_delay: \"0.2s\"}"); + getResponseFromUpstream("{static_delay: \"0.2s\"}"); // TODO(#392): This fails, because the duration is a two-field message: it would make here to see // both the number of seconds and nanoseconds to be overridden. // However, the seconds part is set to '0', which equates to the default of the underlying int @@ -117,7 +117,7 @@ name: dynamic-delay // Hence the following expectation will fail, as it yields 1200 instead of the expected 200. // EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), // "200"); - getResponse("{static_delay: \"2.2s\"}"); + getResponseFromUpstream("{static_delay: \"2.2s\"}"); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "2200"); } @@ -131,7 +131,7 @@ name: dynamic-delay minimal_delay: 0.05s concurrency_delay_factor: 0.01s )EOF"); - getResponse(); + getResponseFromUpstream(); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "60"); } diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc index e03a779a4..44c849224 100644 --- a/test/server/http_filter_integration_test_base.cc +++ b/test/server/http_filter_integration_test_base.cc @@ -9,14 +9,35 @@ HttpFilterIntegrationTestBase::HttpFilterIntegrationTestBase( : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, ip_version), request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} -// We don't override SetUp(): tests in this file will call setup() instead to avoid having to -// create a fixture per filter configuration. void HttpFilterIntegrationTestBase::setup(const std::string& config) { config_helper_.addFilter(config); HttpIntegrationTest::initialize(); } -// Fetches a response with request-level configuration set in the request header. +Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponseFromUpstream() { + return getResponse(request_headers_, true); +} + +Envoy::IntegrationStreamDecoderPtr +HttpFilterIntegrationTestBase::getResponseFromUpstream(absl::string_view request_level_config) { + return getResponse(request_level_config, true); +} + +Envoy::IntegrationStreamDecoderPtr +HttpFilterIntegrationTestBase::getResponseFromExtension(absl::string_view request_level_config) { + return getResponse(request_level_config, false); +} + +Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponseFromUpstream( + const Envoy::Http::TestRequestHeaderMapImpl& request_headers) { + return getResponse(request_headers, true); +} + +Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponseFromExtension( + const Envoy::Http::TestRequestHeaderMapImpl& request_headers) { + return getResponse(request_headers, false); +} + Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponse(absl::string_view request_level_config, bool setup_for_upstream_request) { @@ -26,26 +47,17 @@ HttpFilterIntegrationTestBase::getResponse(absl::string_view request_level_confi return getResponse(request_headers, setup_for_upstream_request); } -// Fetches a response with the default request headers, expecting the fake upstream to supply -// the response. -Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponse() { - return getResponse(request_headers_); -} - -// Fetches a response using the provided request headers. When setup_for_upstream_request -// is true, the expectation will be that an upstream request will be needed to provide a -// response. If it is set to false, the extension is expected to supply the response, and -// no upstream request ought to occur. Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponse( const Envoy::Http::TestRequestHeaderMapImpl& request_headers, bool setup_for_upstream_request) { cleanupUpstreamAndDownstream(); codec_client_ = makeHttpConnection(lookupPort("http")); Envoy::IntegrationStreamDecoderPtr response; - const bool is_post = request_headers.Method()->value().getStringView() == "POST"; + const bool is_post = request_headers.Method()->value().getStringView() == + Envoy::Http::Headers::get().MethodValues.Post; const uint64_t request_body_size = is_post ? 1024 : 0; if (setup_for_upstream_request) { response = sendRequestAndWaitForResponse(request_headers, request_body_size, - default_response_headers_, 0); + default_response_headers_, /*response_body_size*/ 0); } else { if (is_post) { response = codec_client_->makeRequestWithBody(request_headers, request_body_size); diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index 568cb0274..0b9db9ec3 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -6,29 +6,115 @@ namespace Nighthawk { +/** + * Base class with shared functionality for testing Nighthawk test server http filter extensions. + */ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { protected: + /** + * @brief Construct a new HttpFilterIntegrationTestBase instance. + * + * @param ip_version Specify the ip version that the integration test server will use to listen + * for connections. + */ HttpFilterIntegrationTestBase(Envoy::Network::Address::IpVersion ip_version); - // We don't override SetUp(): tests in this file will call setup() instead to avoid having to - // create a fixture per filter configuration. + /** + * We don't override SetUp(): tests using this fixture must call setup() instead. + * This is to avoid imposing the need to create a fixture per filter configuration. + * + * @param config configuration to pass to Envoy::HttpIntegrationTest::config_helper_.addFilter. + */ void setup(const std::string& config); - // Fetches a response with request-level configuration set in the request header. - Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, - bool setup_for_upstream_request = true); + /** + * @brief Fetch a response with the default request headers, and set up a fake upstream to supply + * the response. + * + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr getResponseFromUpstream(); + + /** + * @brief Fetches a response with request-level configuration set in the request header, and set + * up a fake upstream to supply the response. + * + * @param request_level_config Configuration to be delivered by request header. For example + * "{{response_body_size:1024}". + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr + getResponseFromUpstream(absl::string_view request_level_config); + + /** + * @brief Fetches a response with request-level configuration set in the request header. The + * extension under test should supply the response. + * + * @param request_level_config Configuration to be delivered by request header. For example + * "{{response_body_size:1024}". + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr + getResponseFromExtension(absl::string_view request_level_config); - // Fetches a response with the default request headers, expecting the fake upstream to supply - // the response. - Envoy::IntegrationStreamDecoderPtr getResponse(); + /** + * @brief Fetch a response using the provided request headers, and set up a fake upstream to + * supply a response. + * + * @param request_headers Supply a full set of request headers. If the request method is set to + * POST, an entity body will be send after the request headers. + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr + getResponseFromUpstream(const Envoy::Http::TestRequestHeaderMapImpl& request_headers); + + /** + * @brief Fetch a response using the provided request headers. The extension under test must + * supply a response. + * + * @param request_headers Supply a full set of request headers. If the request method is set to + * POST, an entity body will be send after the request headers. + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr + getResponseFromExtension(const Envoy::Http::TestRequestHeaderMapImpl& request_headers); + +private: + /** + * @brief Fetches a response with request-level configuration set in the request header. + * + * @param request_level_config Configuration to be delivered by request header. For example + * "{{response_body_size:1024}". + * @param setup_for_upstream_request Set to true iff the filter extension under test is expected + * to short-circuit and supply a response directly. For example because it couldn't parse the + * supplied request-level configuration. Otherwise this should be set to true, and a stock + * response will be yielded by the integration test server through an upstream request. + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, + bool setup_for_upstream_request); - // Fetches a response using the provided request headers. When setup_for_upstream_request - // is true, the expectation will be that an upstream request will be needed to provide a - // response. If it is set to false, the extension is expected to supply the response, and - // no upstream request ought to occur. + /** + * @brief Fetch a response using the provided request headers. + * + * @param request_headers Supply a full set of request headers. If the request method is set to + * POST, an entity body will be send after the request headers. + * @param setup_for_upstream_request Set to true iff the filter extension under test is expected + * to short-circuit and supply a response directly. For example because it couldn't parse the + * supplied request-level configuration. Otherwise this should be set to true, and a stock + * response will be yielded by the integration test server through an upstream request. + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ Envoy::IntegrationStreamDecoderPtr getResponse(const Envoy::Http::TestRequestHeaderMapImpl& request_headers, - bool setup_for_upstream_request = true); + bool setup_for_upstream_request); const Envoy::Http::TestRequestHeaderMapImpl request_headers_; }; diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index ad6014f4b..43384ffb5 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -45,12 +45,12 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, // Verify expectations with static/file-based time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfiguration) { setup(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); - Envoy::IntegrationStreamDecoderPtr response = getResponse(); + Envoy::IntegrationStreamDecoderPtr response = getResponseFromUpstream(); int64_t latency; const Envoy::Http::HeaderEntry* latency_header_1 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); EXPECT_EQ(latency_header_1, nullptr); - response = getResponse(); + response = getResponseFromUpstream(); const Envoy::Http::HeaderEntry* latency_header_2 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header_2, nullptr); @@ -62,12 +62,12 @@ TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfigura TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfiguration) { setup(fmt::format(kProtoConfigTemplate, "")); // Don't send any config request header - getResponse(); + getResponseFromUpstream(); // Send a config request header with an empty / default config. Should be a no-op. - getResponse("{}"); + getResponseFromUpstream("{}"); // Send a config request header, this should become effective. Envoy::IntegrationStreamDecoderPtr response = - getResponse(fmt::format("{{{}}}", kDefaultProtoFragment)); + getResponseFromUpstream(fmt::format("{{{}}}", kDefaultProtoFragment)); const Envoy::Http::HeaderEntry* latency_header = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header, nullptr); @@ -82,14 +82,14 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguratio setup(fmt::format(kProtoConfigTemplate, "")); // Send a malformed config request header. This ought to shortcut and directly reply, // hence we don't expect an upstream request. - Envoy::IntegrationStreamDecoderPtr response = getResponse("bad_json", false); + Envoy::IntegrationStreamDecoderPtr response = getResponseFromExtension("bad_json"); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), "time-tracking didn't understand the request: Error merging json config: Unable to parse " "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); // Send an empty config header, which ought to trigger failure mode as well. - response = getResponse("", false); + response = getResponseFromExtension(""); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -101,14 +101,14 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguratio // This takes a slightly different code path, so it is important to test this explicitly. TEST_P(HttpTimeTrackingIntegrationTest, BehaviorWithRequestBody) { setup(fmt::format(kProtoConfigTemplate, "")); - Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; - request_headers.setMethod("POST"); + Envoy::Http::TestRequestHeaderMapImpl request_headers( + {{":method", "POST"}, {":path", "/"}, {":authority", "host"}}); // Post without any request-level configuration. Should succeed. - getResponse(request_headers, true); + getResponseFromUpstream(request_headers); Envoy::IntegrationStreamDecoderPtr response = - getResponse(fmt::format("{{{}}}", kDefaultProtoFragment)); + getResponseFromUpstream(fmt::format("{{{}}}", kDefaultProtoFragment)); const Envoy::Http::HeaderEntry* latency_header = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header, nullptr); @@ -118,7 +118,7 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehaviorWithRequestBody) { const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); request_headers.setCopy(key, "bad_json"); fake_upstreams_[0]->set_allow_unexpected_disconnects(true); - response = getResponse(request_headers, false); + response = getResponseFromExtension(request_headers); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), From 594bbbecf3a34d736ca454ef7df0e7ef44954a8f Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 9 Sep 2020 18:07:52 +0200 Subject: [PATCH 31/48] Review feedback: option rename Signed-off-by: Otto van der Schaaf --- README.md | 4 ++-- api/client/options.proto | 2 +- source/client/benchmark_client_impl.cc | 6 +++--- source/client/benchmark_client_impl.h | 4 ++-- source/client/options_impl.cc | 14 +++++++------- source/client/options_impl.h | 4 ++-- source/client/stream_decoder.cc | 4 ++-- source/client/stream_decoder.h | 6 +++--- test/integration/test_integration_basics.py | 2 +- test/options_test.cc | 4 ++-- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index a64914e0f..7b87c3a14 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ bazel build -c opt //:nighthawk ``` USAGE: -bazel-bin/nighthawk_client [--response-latency-header-name ] +bazel-bin/nighthawk_client [--latency-response-header-name ] [--stats-flush-interval ] [--stats-sinks ] ... [--no-duration] [--simple-warmup] @@ -81,7 +81,7 @@ format> Where: ---response-latency-header-name +--latency-response-header-name Set an optional header name that will be returned in responses, whose values will be tracked in a latency histogram if set. Can be used in tandem with the test server's response option diff --git a/api/client/options.proto b/api/client/options.proto index 1d3421cc7..3cf370141 100644 --- a/api/client/options.proto +++ b/api/client/options.proto @@ -206,5 +206,5 @@ message CommandLineOptions { // a latency histogram if set. Can be used in tandem with the test server's response option // "emit_previous_request_delta_in_response_header" to record elapsed time between request // arrivals. - google.protobuf.StringValue response_latency_header_name = 36; + google.protobuf.StringValue latency_response_header_name = 36; } diff --git a/source/client/benchmark_client_impl.cc b/source/client/benchmark_client_impl.cc index 177562988..daedecb48 100644 --- a/source/client/benchmark_client_impl.cc +++ b/source/client/benchmark_client_impl.cc @@ -84,14 +84,14 @@ BenchmarkClientHttpImpl::BenchmarkClientHttpImpl( Envoy::Upstream::ClusterManagerPtr& cluster_manager, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, const bool provide_resource_backpressure, - absl::string_view response_latency_header_name) + absl::string_view latency_response_header_name) : api_(api), dispatcher_(dispatcher), scope_(scope.createScope("benchmark.")), statistic_(std::move(statistic)), use_h2_(use_h2), benchmark_client_counters_({ALL_BENCHMARK_CLIENT_COUNTERS(POOL_COUNTER(*scope_))}), cluster_manager_(cluster_manager), http_tracer_(http_tracer), cluster_name_(std::string(cluster_name)), request_generator_(std::move(request_generator)), provide_resource_backpressure_(provide_resource_backpressure), - response_latency_header_name_(response_latency_header_name) { + latency_response_header_name_(latency_response_header_name) { statistic_.connect_statistic->setId("benchmark_http_client.queue_to_connect"); statistic_.response_statistic->setId("benchmark_http_client.request_to_response"); statistic_.response_header_size_statistic->setId("benchmark_http_client.response_header_size"); @@ -168,7 +168,7 @@ bool BenchmarkClientHttpImpl::tryStartRequest(CompletionCallback caller_completi *statistic_.connect_statistic, *statistic_.response_statistic, *statistic_.response_header_size_statistic, *statistic_.response_body_size_statistic, *statistic_.origin_latency_statistic, request->header(), shouldMeasureLatencies(), - content_length, generator_, http_tracer_, response_latency_header_name_); + content_length, generator_, http_tracer_, latency_response_header_name_); requests_initiated_++; pool_ptr->newStream(*stream_decoder, *stream_decoder); return true; diff --git a/source/client/benchmark_client_impl.h b/source/client/benchmark_client_impl.h index 5b7285f6d..0304d4460 100644 --- a/source/client/benchmark_client_impl.h +++ b/source/client/benchmark_client_impl.h @@ -107,7 +107,7 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, absl::string_view cluster_name, RequestGenerator request_generator, const bool provide_resource_backpressure, - absl::string_view response_latency_header_name); + absl::string_view latency_response_header_name); void setConnectionLimit(uint32_t connection_limit) { connection_limit_ = connection_limit; } void setMaxPendingRequests(uint32_t max_pending_requests) { max_pending_requests_ = max_pending_requests; @@ -163,7 +163,7 @@ class BenchmarkClientHttpImpl : public BenchmarkClient, std::string cluster_name_; const RequestGenerator request_generator_; const bool provide_resource_backpressure_; - const std::string response_latency_header_name_; + const std::string latency_response_header_name_; }; } // namespace Client diff --git a/source/client/options_impl.cc b/source/client/options_impl.cc index d5729173d..9dec9f379 100644 --- a/source/client/options_impl.cc +++ b/source/client/options_impl.cc @@ -294,8 +294,8 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { stats_flush_interval_), false, 5, "uint32_t", cmd); - TCLAP::ValueArg response_latency_header_name( - "", "response-latency-header-name", + TCLAP::ValueArg latency_response_header_name( + "", "latency-response-header-name", "Set an optional header name that will be returned in responses, whose values will be " "tracked in a latency histogram if set. " "Can be used in tandem with the test server's response option " @@ -435,7 +435,7 @@ OptionsImpl::OptionsImpl(int argc, const char* const* argv) { } } TCLAP_SET_IF_SPECIFIED(stats_flush_interval, stats_flush_interval_); - TCLAP_SET_IF_SPECIFIED(response_latency_header_name, response_latency_header_name_); + TCLAP_SET_IF_SPECIFIED(latency_response_header_name, latency_response_header_name_); // CLI-specific tests. // TODO(oschaaf): as per mergconflicts's remark, it would be nice to aggregate @@ -621,8 +621,8 @@ OptionsImpl::OptionsImpl(const nighthawk::client::CommandLineOptions& options) { no_duration_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(options, no_duration, no_duration_); } std::copy(options.labels().begin(), options.labels().end(), std::back_inserter(labels_)); - response_latency_header_name_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - options, response_latency_header_name, response_latency_header_name_); + latency_response_header_name_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( + options, latency_response_header_name, latency_response_header_name_); validate(); } @@ -795,8 +795,8 @@ CommandLineOptionsPtr OptionsImpl::toCommandLineOptionsInternal() const { *command_line_options->add_stats_sinks() = stats_sink; } command_line_options->mutable_stats_flush_interval()->set_value(stats_flush_interval_); - command_line_options->mutable_response_latency_header_name()->set_value( - response_latency_header_name_); + command_line_options->mutable_latency_response_header_name()->set_value( + latency_response_header_name_); return command_line_options; } diff --git a/source/client/options_impl.h b/source/client/options_impl.h index 2cac61aa2..af529f7b8 100644 --- a/source/client/options_impl.h +++ b/source/client/options_impl.h @@ -86,7 +86,7 @@ class OptionsImpl : public Options, public Envoy::Logger::Loggable stats_sinks_; uint32_t stats_flush_interval_{5}; - std::string response_latency_header_name_; + std::string latency_response_header_name_; }; } // namespace Client diff --git a/source/client/stream_decoder.cc b/source/client/stream_decoder.cc index d4d2a2aae..21bf877f9 100644 --- a/source/client/stream_decoder.cc +++ b/source/client/stream_decoder.cc @@ -19,8 +19,8 @@ void StreamDecoder::decodeHeaders(Envoy::Http::ResponseHeaderMapPtr&& headers, b response_header_sizes_statistic_.addValue(response_headers_->byteSize()); const uint64_t response_code = Envoy::Http::Utility::getResponseStatus(*response_headers_); stream_info_.response_code_ = static_cast(response_code); - if (!response_latency_header_name_.empty()) { - const auto timing_header_name = Envoy::Http::LowerCaseString(response_latency_header_name_); + if (!latency_response_header_name_.empty()) { + const auto timing_header_name = Envoy::Http::LowerCaseString(latency_response_header_name_); const Envoy::Http::HeaderEntry* timing_header = response_headers_->get(timing_header_name); if (timing_header != nullptr) { absl::string_view timing_value = timing_header->value().getStringView(); diff --git a/source/client/stream_decoder.h b/source/client/stream_decoder.h index 6f04b1530..f641d171f 100644 --- a/source/client/stream_decoder.h +++ b/source/client/stream_decoder.h @@ -47,7 +47,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, HeaderMapPtr request_headers, bool measure_latencies, uint32_t request_body_size, Envoy::Random::RandomGenerator& random_generator, Envoy::Tracing::HttpTracerSharedPtr& http_tracer, - absl::string_view response_latency_header_name) + absl::string_view latency_response_header_name) : dispatcher_(dispatcher), time_source_(time_source), decoder_completion_callback_(decoder_completion_callback), caller_completion_callback_(std::move(caller_completion_callback)), @@ -59,7 +59,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, complete_(false), measure_latencies_(measure_latencies), request_body_size_(request_body_size), stream_info_(time_source_), random_generator_(random_generator), http_tracer_(http_tracer), - response_latency_header_name_(response_latency_header_name) { + latency_response_header_name_(latency_response_header_name) { if (measure_latencies_ && http_tracer_ != nullptr) { setupForTracing(); } @@ -121,7 +121,7 @@ class StreamDecoder : public Envoy::Http::ResponseDecoder, Envoy::Tracing::HttpTracerSharedPtr& http_tracer_; Envoy::Tracing::SpanPtr active_span_; Envoy::StreamInfo::UpstreamTiming upstream_timing_; - const std::string response_latency_header_name_; + const std::string latency_response_header_name_; }; } // namespace Client diff --git a/test/integration/test_integration_basics.py b/test/integration/test_integration_basics.py index 314c02198..4ffa0e0ba 100644 --- a/test/integration/test_integration_basics.py +++ b/test/integration/test_integration_basics.py @@ -713,7 +713,7 @@ def test_http_h1_response_header_latency_tracking(http_test_server_fixture, serv parsed_json, _ = http_test_server_fixture.runNighthawkClient([ http_test_server_fixture.getTestServerRootUri(), "--connections", "1", "--rps", "100", "--duration", "100", "--termination-predicate", "benchmark.http_2xx:99", - "--response-latency-header-name", "x-origin-request-receipt-delta" + "--latency-response-header-name", "x-origin-request-receipt-delta" ]) global_histograms = http_test_server_fixture.getNighthawkGlobalHistogramsbyIdFromJson(parsed_json) asserts.assertEqual(int(global_histograms["benchmark_http_client.latency_2xx"]["count"]), 100) diff --git a/test/options_test.cc b/test/options_test.cc index 3f0e250ef..ddbb80595 100644 --- a/test/options_test.cc +++ b/test/options_test.cc @@ -118,7 +118,7 @@ TEST_F(OptionsImplTest, AlmostAll) { "--experimental-h2-use-multiple-connections " "--experimental-h1-connection-reuse-strategy lru --label label1 --label label2 {} " "--simple-warmup --stats-sinks {} --stats-sinks {} --stats-flush-interval 10 " - "--response-latency-header-name zz", + "--latency-response-header-name zz", client_name_, "{name:\"envoy.transport_sockets.tls\"," "typed_config:{\"@type\":\"type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext\"," @@ -248,7 +248,7 @@ TEST_F(OptionsImplTest, AlmostAll) { ASSERT_EQ(cmd->stats_sinks_size(), options->statsSinks().size()); EXPECT_TRUE(util(cmd->stats_sinks(0), options->statsSinks()[0])); EXPECT_TRUE(util(cmd->stats_sinks(1), options->statsSinks()[1])); - EXPECT_EQ(cmd->response_latency_header_name().value(), options->responseHeaderWithLatencyInput()); + EXPECT_EQ(cmd->latency_response_header_name().value(), options->responseHeaderWithLatencyInput()); OptionsImpl options_from_proto(*cmd); std::string s1 = Envoy::MessageUtil::getYamlStringFromMessage( From dddc71009316466173685af4823f5bf12582c429 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Thu, 10 Sep 2020 19:09:35 +0200 Subject: [PATCH 32/48] review feedback for the http filter config base Signed-off-by: Otto van der Schaaf --- source/server/http_filter_config_base.cc | 4 ++-- source/server/http_filter_config_base.h | 15 +++++++++------ .../http_dynamic_delay_filter_integration_test.cc | 1 - .../http_test_server_filter_integration_test.cc | 2 +- .../http_time_tracking_filter_integration_test.cc | 1 - 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/source/server/http_filter_config_base.cc b/source/server/http_filter_config_base.cc index 36c5e5e08..cd1d52e14 100644 --- a/source/server/http_filter_config_base.cc +++ b/source/server/http_filter_config_base.cc @@ -3,8 +3,8 @@ namespace Nighthawk { namespace Server { -FilterConfigurationBase::FilterConfigurationBase(nighthawk::server::ResponseOptions proto_config, - absl::string_view filter_name) +FilterConfigurationBase::FilterConfigurationBase( + const nighthawk::server::ResponseOptions& proto_config, absl::string_view filter_name) : filter_name_(filter_name), server_config_(std::make_shared(std::move(proto_config))), effective_config_(server_config_) {} diff --git a/source/server/http_filter_config_base.h b/source/server/http_filter_config_base.h index 5e8448010..9ce419690 100644 --- a/source/server/http_filter_config_base.h +++ b/source/server/http_filter_config_base.h @@ -16,13 +16,13 @@ namespace Nighthawk { namespace Server { +using EffectiveFilterConfigurationPtr = std::shared_ptr; /** * Shorthand and canonical representation of the effective filter configuration. Either a status * or a shared pointer to the effective configuration. We use a shared pointer to avoid copying * in the static configuration flow. */ -using EffectiveFilterConfiguration = - absl::StatusOr>; +using StatusOrEffectiveFilterConfigurationPtr = absl::StatusOr; /** * Provides functionality for parsing and merging request-header based configuration, as well as @@ -37,7 +37,7 @@ class FilterConfigurationBase { * @param filter_name name of the extension that is consuming this. Used during error response * generation. */ - FilterConfigurationBase(nighthawk::server::ResponseOptions static_proto_config, + FilterConfigurationBase(const nighthawk::server::ResponseOptions& proto_config, absl::string_view filter_name); /** @@ -61,9 +61,12 @@ class FilterConfigurationBase { * @brief Get the effective configuration. Depending on state ,this could be one of static * configuration, dynamic configuration, or an error status. * - * @return const EffectiveFilterConfiguration The effective configuration, or an error status. + * @return const StatusOrEffectiveFilterConfigurationPtr The effective configuration, or an error + * status. */ - const EffectiveFilterConfiguration getEffectiveConfiguration() const { return effective_config_; } + const StatusOrEffectiveFilterConfigurationPtr getEffectiveConfiguration() const { + return effective_config_; + } /** * @return absl::string_view Name of the filter that constructed this instance. @@ -73,7 +76,7 @@ class FilterConfigurationBase { private: const std::string filter_name_; const std::shared_ptr server_config_; - EffectiveFilterConfiguration effective_config_; + StatusOrEffectiveFilterConfigurationPtr effective_config_; }; } // namespace Server diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index 39fe3fe7e..5fbdccdcc 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -88,7 +88,6 @@ name: dynamic-delay // error. const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); request_headers.setCopy(key, "bad_json"); - fake_upstreams_[0]->set_allow_unexpected_disconnects(true); auto response = getResponseFromExtension(request_headers); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index ca5bf273a..69e418ea0 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -313,7 +313,7 @@ TEST_F(HttpTestServerDecoderFilterTest, HeaderMerge) { std::make_shared(initial_options); Server::HttpTestServerDecoderFilter f(config); - Server::EffectiveFilterConfiguration options_or = config->getEffectiveConfiguration(); + Server::StatusOrEffectiveFilterConfigurationPtr options_or = config->getEffectiveConfiguration(); ASSERT_TRUE(options_or.ok()); nighthawk::server::ResponseOptions options = *options_or.value(); EXPECT_EQ(1, options.response_headers_size()); diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 43384ffb5..6a9b6c100 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -117,7 +117,6 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehaviorWithRequestBody) { // error. const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); request_headers.setCopy(key, "bad_json"); - fake_upstreams_[0]->set_allow_unexpected_disconnects(true); response = getResponseFromExtension(request_headers); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( From b1993a132e14bc62605260dcb00157b788e58e42 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Thu, 10 Sep 2020 20:42:38 +0200 Subject: [PATCH 33/48] Review feedback Signed-off-by: Otto van der Schaaf --- .../server/http_dynamic_delay_filter_config.cc | 2 +- source/server/http_filter_config_base.h | 16 +++++++--------- .../http_test_server_filter_integration_test.cc | 3 ++- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/source/server/http_dynamic_delay_filter_config.cc b/source/server/http_dynamic_delay_filter_config.cc index 336a5da7b..1e32cea17 100644 --- a/source/server/http_dynamic_delay_filter_config.cc +++ b/source/server/http_dynamic_delay_filter_config.cc @@ -40,7 +40,7 @@ class HttpDynamicDelayDecoderFilterConfigFactory Nighthawk::Server::HttpDynamicDelayDecoderFilterConfigSharedPtr config = std::make_shared( Nighthawk::Server::HttpDynamicDelayDecoderFilterConfig( - proto_config, context.runtime(), "" /*stats_prefix*/, context.scope(), + std::move(proto_config), context.runtime(), "" /*stats_prefix*/, context.scope(), context.timeSource())); return [config](Envoy::Http::FilterChainFactoryCallbacks& callbacks) -> void { diff --git a/source/server/http_filter_config_base.h b/source/server/http_filter_config_base.h index 9ce419690..88d20405e 100644 --- a/source/server/http_filter_config_base.h +++ b/source/server/http_filter_config_base.h @@ -16,13 +16,11 @@ namespace Nighthawk { namespace Server { -using EffectiveFilterConfigurationPtr = std::shared_ptr; /** - * Shorthand and canonical representation of the effective filter configuration. Either a status - * or a shared pointer to the effective configuration. We use a shared pointer to avoid copying - * in the static configuration flow. + * Canonical representation of the effective filter configuration. + * We use a shared pointer to avoid copying in the static configuration flow. */ -using StatusOrEffectiveFilterConfigurationPtr = absl::StatusOr; +using EffectiveFilterConfigurationPtr = std::shared_ptr; /** * Provides functionality for parsing and merging request-header based configuration, as well as @@ -61,10 +59,10 @@ class FilterConfigurationBase { * @brief Get the effective configuration. Depending on state ,this could be one of static * configuration, dynamic configuration, or an error status. * - * @return const StatusOrEffectiveFilterConfigurationPtr The effective configuration, or an error - * status. + * @return const absl::StatusOr The effective configuration, or + * an error status. */ - const StatusOrEffectiveFilterConfigurationPtr getEffectiveConfiguration() const { + const absl::StatusOr getEffectiveConfiguration() const { return effective_config_; } @@ -76,7 +74,7 @@ class FilterConfigurationBase { private: const std::string filter_name_; const std::shared_ptr server_config_; - StatusOrEffectiveFilterConfigurationPtr effective_config_; + absl::StatusOr effective_config_; }; } // namespace Server diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 69e418ea0..f91b9b404 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -313,7 +313,8 @@ TEST_F(HttpTestServerDecoderFilterTest, HeaderMerge) { std::make_shared(initial_options); Server::HttpTestServerDecoderFilter f(config); - Server::StatusOrEffectiveFilterConfigurationPtr options_or = config->getEffectiveConfiguration(); + absl::StatusOr options_or = + config->getEffectiveConfiguration(); ASSERT_TRUE(options_or.ok()); nighthawk::server::ResponseOptions options = *options_or.value(); EXPECT_EQ(1, options.response_headers_size()); From 677872322978ba7adcca0eec50b6c37a7b25b6c5 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Thu, 10 Sep 2020 22:18:55 +0200 Subject: [PATCH 34/48] tidy up Signed-off-by: Otto van der Schaaf --- source/server/http_dynamic_delay_filter.cc | 4 ++-- source/server/http_dynamic_delay_filter.h | 2 +- source/server/http_dynamic_delay_filter_config.cc | 2 +- source/server/http_test_server_filter.cc | 4 ++-- source/server/http_test_server_filter.h | 2 +- source/server/http_time_tracking_filter.cc | 2 +- source/server/http_time_tracking_filter.h | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/source/server/http_dynamic_delay_filter.cc b/source/server/http_dynamic_delay_filter.cc index f8d82959a..ebc3254fc 100644 --- a/source/server/http_dynamic_delay_filter.cc +++ b/source/server/http_dynamic_delay_filter.cc @@ -13,9 +13,9 @@ namespace Nighthawk { namespace Server { HttpDynamicDelayDecoderFilterConfig::HttpDynamicDelayDecoderFilterConfig( - nighthawk::server::ResponseOptions proto_config, Envoy::Runtime::Loader& runtime, + const nighthawk::server::ResponseOptions& proto_config, Envoy::Runtime::Loader& runtime, const std::string& stats_prefix, Envoy::Stats::Scope& scope, Envoy::TimeSource& time_source) - : FilterConfigurationBase(std::move(proto_config), "dynamic-delay"), runtime_(runtime), + : FilterConfigurationBase(proto_config, "dynamic-delay"), runtime_(runtime), stats_prefix_(absl::StrCat(stats_prefix, fmt::format("{}.", filter_name()))), scope_(scope), time_source_(time_source) {} diff --git a/source/server/http_dynamic_delay_filter.h b/source/server/http_dynamic_delay_filter.h index 12bff0d61..654a3d752 100644 --- a/source/server/http_dynamic_delay_filter.h +++ b/source/server/http_dynamic_delay_filter.h @@ -33,7 +33,7 @@ class HttpDynamicDelayDecoderFilterConfig : public FilterConfigurationBase { * @param scope Statistics scope to be used by the filter. * @param time_source Time source to be used by the filter. */ - HttpDynamicDelayDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config, + HttpDynamicDelayDecoderFilterConfig(const nighthawk::server::ResponseOptions& proto_config, Envoy::Runtime::Loader& runtime, const std::string& stats_prefix, Envoy::Stats::Scope& scope, Envoy::TimeSource& time_source); diff --git a/source/server/http_dynamic_delay_filter_config.cc b/source/server/http_dynamic_delay_filter_config.cc index 1e32cea17..336a5da7b 100644 --- a/source/server/http_dynamic_delay_filter_config.cc +++ b/source/server/http_dynamic_delay_filter_config.cc @@ -40,7 +40,7 @@ class HttpDynamicDelayDecoderFilterConfigFactory Nighthawk::Server::HttpDynamicDelayDecoderFilterConfigSharedPtr config = std::make_shared( Nighthawk::Server::HttpDynamicDelayDecoderFilterConfig( - std::move(proto_config), context.runtime(), "" /*stats_prefix*/, context.scope(), + proto_config, context.runtime(), "" /*stats_prefix*/, context.scope(), context.timeSource())); return [config](Envoy::Http::FilterChainFactoryCallbacks& callbacks) -> void { diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index 1e0eb0994..744024741 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -13,8 +13,8 @@ namespace Nighthawk { namespace Server { HttpTestServerDecoderFilterConfig::HttpTestServerDecoderFilterConfig( - nighthawk::server::ResponseOptions proto_config) - : FilterConfigurationBase(std::move(proto_config), "test-server") {} + const nighthawk::server::ResponseOptions& proto_config) + : FilterConfigurationBase(proto_config, "test-server") {} HttpTestServerDecoderFilter::HttpTestServerDecoderFilter( HttpTestServerDecoderFilterConfigSharedPtr config) diff --git a/source/server/http_test_server_filter.h b/source/server/http_test_server_filter.h index 927f7ee2b..16f3e378e 100644 --- a/source/server/http_test_server_filter.h +++ b/source/server/http_test_server_filter.h @@ -14,7 +14,7 @@ namespace Server { // Basically this is left in as a placeholder for further configuration. class HttpTestServerDecoderFilterConfig : public FilterConfigurationBase { public: - HttpTestServerDecoderFilterConfig(nighthawk::server::ResponseOptions proto_config); + HttpTestServerDecoderFilterConfig(const nighthawk::server::ResponseOptions& proto_config); }; using HttpTestServerDecoderFilterConfigSharedPtr = diff --git a/source/server/http_time_tracking_filter.cc b/source/server/http_time_tracking_filter.cc index 36e6e041c..3eb6feb35 100644 --- a/source/server/http_time_tracking_filter.cc +++ b/source/server/http_time_tracking_filter.cc @@ -16,7 +16,7 @@ namespace Nighthawk { namespace Server { HttpTimeTrackingFilterConfig::HttpTimeTrackingFilterConfig( - nighthawk::server::ResponseOptions proto_config) + const nighthawk::server::ResponseOptions& proto_config) : FilterConfigurationBase(std::move(proto_config), "time-tracking"), stopwatch_(std::make_unique()) {} diff --git a/source/server/http_time_tracking_filter.h b/source/server/http_time_tracking_filter.h index 5a2288a9b..6f08b42f5 100644 --- a/source/server/http_time_tracking_filter.h +++ b/source/server/http_time_tracking_filter.h @@ -27,7 +27,7 @@ class HttpTimeTrackingFilterConfig : public FilterConfigurationBase { * * @param proto_config The proto configuration of the filter. */ - HttpTimeTrackingFilterConfig(nighthawk::server::ResponseOptions proto_config); + HttpTimeTrackingFilterConfig(const nighthawk::server::ResponseOptions& proto_config); /** * Gets the number of elapsed nanoseconds since the last call (server wide). From 809715e99fc5af0753d4f0fc361c15fdbfe80542 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Thu, 10 Sep 2020 22:28:02 +0200 Subject: [PATCH 35/48] Tidy up more Signed-off-by: Otto van der Schaaf --- source/server/http_filter_config_base.cc | 2 +- source/server/http_time_tracking_filter.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/server/http_filter_config_base.cc b/source/server/http_filter_config_base.cc index cd1d52e14..dfc7c4be6 100644 --- a/source/server/http_filter_config_base.cc +++ b/source/server/http_filter_config_base.cc @@ -6,7 +6,7 @@ namespace Server { FilterConfigurationBase::FilterConfigurationBase( const nighthawk::server::ResponseOptions& proto_config, absl::string_view filter_name) : filter_name_(filter_name), - server_config_(std::make_shared(std::move(proto_config))), + server_config_(std::make_shared(proto_config)), effective_config_(server_config_) {} void FilterConfigurationBase::computeEffectiveConfiguration( diff --git a/source/server/http_time_tracking_filter.cc b/source/server/http_time_tracking_filter.cc index 3eb6feb35..045192d0e 100644 --- a/source/server/http_time_tracking_filter.cc +++ b/source/server/http_time_tracking_filter.cc @@ -17,7 +17,7 @@ namespace Server { HttpTimeTrackingFilterConfig::HttpTimeTrackingFilterConfig( const nighthawk::server::ResponseOptions& proto_config) - : FilterConfigurationBase(std::move(proto_config), "time-tracking"), + : FilterConfigurationBase(proto_config, "time-tracking"), stopwatch_(std::make_unique()) {} uint64_t From 8b3db6c12e437934feca1eddf221a8ea28043470 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Sat, 12 Sep 2020 01:43:49 +0200 Subject: [PATCH 36/48] Review feedback: simplify the filter integration test base some more. Signed-off-by: Otto van der Schaaf --- source/server/http_filter_config_base.cc | 2 + source/server/http_filter_config_base.h | 1 - test/server/BUILD | 1 + ...p_dynamic_delay_filter_integration_test.cc | 41 ++++---- .../http_filter_integration_test_base.cc | 50 +++------- .../http_filter_integration_test_base.h | 98 ++++--------------- ...p_time_tracking_filter_integration_test.cc | 37 ++++--- 7 files changed, 80 insertions(+), 150 deletions(-) diff --git a/source/server/http_filter_config_base.cc b/source/server/http_filter_config_base.cc index dfc7c4be6..a8dc933e9 100644 --- a/source/server/http_filter_config_base.cc +++ b/source/server/http_filter_config_base.cc @@ -1,5 +1,7 @@ #include "server/http_filter_config_base.h" +#include "server/well_known_headers.h" + namespace Nighthawk { namespace Server { diff --git a/source/server/http_filter_config_base.h b/source/server/http_filter_config_base.h index 88d20405e..9f3f700fe 100644 --- a/source/server/http_filter_config_base.h +++ b/source/server/http_filter_config_base.h @@ -9,7 +9,6 @@ #include "api/server/response_options.pb.h" #include "server/configuration.h" -#include "server/well_known_headers.h" #include "absl/status/status.h" diff --git a/test/server/BUILD b/test/server/BUILD index 68717484b..71ae178ab 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -15,6 +15,7 @@ envoy_cc_test_library( hdrs = ["http_filter_integration_test_base.h"], repository = "@envoy", deps = [ + "//source/server:well_known_headers_lib", "@envoy//test/integration:http_integration_lib", ], ) diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index 5fbdccdcc..f76067685 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -42,25 +42,30 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions )"); // Don't send any config request header - getResponseFromUpstream(); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header with an empty / default config. Should be a no-op. - getResponseFromUpstream("{}"); + + updateRequestLevelConfiguration("{}"); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header, this should become effective. - getResponseFromUpstream("{static_delay: \"1.6s\"}"); + updateRequestLevelConfiguration("{static_delay: \"1.6s\"}"); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1600"); // Send a malformed config request header. This ought to shortcut and directly reply, // hence we don't expect an upstream request. - auto response = getResponseFromExtension("bad_json"); + updateRequestLevelConfiguration("bad_json"); + auto response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), "dynamic-delay didn't understand the request: Error merging json config: Unable to parse " "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); // Send an empty config header, which ought to trigger failure mode as well. - response = getResponseFromExtension(""); + updateRequestLevelConfiguration(""); + response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -77,18 +82,15 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions static_delay: 0.1s )EOF"); - Envoy::Http::TestRequestHeaderMapImpl request_headers( - {{":method", "POST"}, {":path", "/"}, {":authority", "host"}}); - + switchToPostWithEntityBody(); // Post without any request-level configuration. Should succeed. - getResponseFromUpstream(request_headers); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "100"); - // Post with bad request-level configuration. The extension should response directly with an + // Post with bad request-level configuration. The extension should respond directly with an // error. - const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); - request_headers.setCopy(key, "bad_json"); - auto response = getResponseFromExtension(request_headers); + updateRequestLevelConfiguration("bad_json"); + auto response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -104,11 +106,13 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions static_delay: 1.33s )EOF"); - getResponseFromUpstream(); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); - getResponseFromUpstream("{}"); + updateRequestLevelConfiguration("{}"); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); - getResponseFromUpstream("{static_delay: \"0.2s\"}"); + updateRequestLevelConfiguration("{static_delay: \"0.2s\"}"); + getResponse(ResponseOrigin::UPSTREAM); // TODO(#392): This fails, because the duration is a two-field message: it would make here to see // both the number of seconds and nanoseconds to be overridden. // However, the seconds part is set to '0', which equates to the default of the underlying int @@ -116,7 +120,8 @@ name: dynamic-delay // Hence the following expectation will fail, as it yields 1200 instead of the expected 200. // EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), // "200"); - getResponseFromUpstream("{static_delay: \"2.2s\"}"); + updateRequestLevelConfiguration("{static_delay: \"2.2s\"}"); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "2200"); } @@ -130,7 +135,7 @@ name: dynamic-delay minimal_delay: 0.05s concurrency_delay_factor: 0.01s )EOF"); - getResponseFromUpstream(); + getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "60"); } diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc index 44c849224..c6f525db6 100644 --- a/test/server/http_filter_integration_test_base.cc +++ b/test/server/http_filter_integration_test_base.cc @@ -1,5 +1,7 @@ #include "test/server/http_filter_integration_test_base.h" +#include "server/well_known_headers.h" + #include "gtest/gtest.h" namespace Nighthawk { @@ -14,55 +16,33 @@ void HttpFilterIntegrationTestBase::setup(const std::string& config) { HttpIntegrationTest::initialize(); } -Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponseFromUpstream() { - return getResponse(request_headers_, true); +void HttpFilterIntegrationTestBase::updateRequestLevelConfiguration( + absl::string_view request_level_config) { + request_headers_.setCopy(Server::TestServer::HeaderNames::get().TestServerConfig, + request_level_config); } -Envoy::IntegrationStreamDecoderPtr -HttpFilterIntegrationTestBase::getResponseFromUpstream(absl::string_view request_level_config) { - return getResponse(request_level_config, true); +void HttpFilterIntegrationTestBase::switchToPostWithEntityBody() { + request_headers_.setCopy(Envoy::Http::Headers::get().Method, + Envoy::Http::Headers::get().MethodValues.Post); } Envoy::IntegrationStreamDecoderPtr -HttpFilterIntegrationTestBase::getResponseFromExtension(absl::string_view request_level_config) { - return getResponse(request_level_config, false); -} - -Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponseFromUpstream( - const Envoy::Http::TestRequestHeaderMapImpl& request_headers) { - return getResponse(request_headers, true); -} - -Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponseFromExtension( - const Envoy::Http::TestRequestHeaderMapImpl& request_headers) { - return getResponse(request_headers, false); -} - -Envoy::IntegrationStreamDecoderPtr -HttpFilterIntegrationTestBase::getResponse(absl::string_view request_level_config, - bool setup_for_upstream_request) { - const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); - Envoy::Http::TestRequestHeaderMapImpl request_headers = request_headers_; - request_headers.setCopy(key, request_level_config); - return getResponse(request_headers, setup_for_upstream_request); -} - -Envoy::IntegrationStreamDecoderPtr HttpFilterIntegrationTestBase::getResponse( - const Envoy::Http::TestRequestHeaderMapImpl& request_headers, bool setup_for_upstream_request) { +HttpFilterIntegrationTestBase::getResponse(ResponseOrigin expected_origin) { cleanupUpstreamAndDownstream(); codec_client_ = makeHttpConnection(lookupPort("http")); Envoy::IntegrationStreamDecoderPtr response; - const bool is_post = request_headers.Method()->value().getStringView() == + const bool is_post = request_headers_.Method()->value().getStringView() == Envoy::Http::Headers::get().MethodValues.Post; const uint64_t request_body_size = is_post ? 1024 : 0; - if (setup_for_upstream_request) { - response = sendRequestAndWaitForResponse(request_headers, request_body_size, + if (expected_origin == ResponseOrigin::UPSTREAM) { + response = sendRequestAndWaitForResponse(request_headers_, request_body_size, default_response_headers_, /*response_body_size*/ 0); } else { if (is_post) { - response = codec_client_->makeRequestWithBody(request_headers, request_body_size); + response = codec_client_->makeRequestWithBody(request_headers_, request_body_size); } else { - response = codec_client_->makeHeaderOnlyRequest(request_headers); + response = codec_client_->makeHeaderOnlyRequest(request_headers_); } response->waitForEndStream(); } diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index 0b9db9ec3..c3d72ca18 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -12,7 +12,11 @@ namespace Nighthawk { class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { protected: /** - * @brief Construct a new HttpFilterIntegrationTestBase instance. + * Indicate the expected response origin. + */ + enum class ResponseOrigin { UPSTREAM, EXTENSION }; + /** + * Construct a new HttpFilterIntegrationTestBase instance. * * @param ip_version Specify the ip version that the integration test server will use to listen * for connections. @@ -28,95 +32,35 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { void setup(const std::string& config); /** - * @brief Fetch a response with the default request headers, and set up a fake upstream to supply - * the response. - * - * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can - * be used to inspect the response. - */ - Envoy::IntegrationStreamDecoderPtr getResponseFromUpstream(); - - /** - * @brief Fetches a response with request-level configuration set in the request header, and set - * up a fake upstream to supply the response. + * Make getResponse send request-level configuration. Test server extensions read that + * configuration and merge it with their static configuration to determine a final effective + * configuration. See TestServerConfig in well_known_headers.h for the up to date header name. * - * @param request_level_config Configuration to be delivered by request header. For example - * "{{response_body_size:1024}". - * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can - * be used to inspect the response. + * @param request_level_config Configuration to be delivered by request-header in future calls to + * getResponse(). For example: "{response_body_size:1024}". */ - Envoy::IntegrationStreamDecoderPtr - getResponseFromUpstream(absl::string_view request_level_config); + void updateRequestLevelConfiguration(absl::string_view request_level_config); /** - * @brief Fetches a response with request-level configuration set in the request header. The - * extension under test should supply the response. - * - * @param request_level_config Configuration to be delivered by request header. For example - * "{{response_body_size:1024}". - * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can - * be used to inspect the response. + * Switch getResponse() to use the POST request method with an entity body. + * Doing so will make tests hit a different code paths in extensions. */ - Envoy::IntegrationStreamDecoderPtr - getResponseFromExtension(absl::string_view request_level_config); + void switchToPostWithEntityBody(); /** - * @brief Fetch a response using the provided request headers, and set up a fake upstream to - * supply a response. + * Fetch a response. The request headers default to a minimal GET, but this may be changed + * via other methods in this class. * - * @param request_headers Supply a full set of request headers. If the request method is set to - * POST, an entity body will be send after the request headers. + * @param expected_origin Indicate which component will be expected to reply: the extension or + * a fake upstream. Will cause a test failure upon mismatch. Can be used to verify that an + * extension short circuits and directly responds when expected. * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can * be used to inspect the response. */ - Envoy::IntegrationStreamDecoderPtr - getResponseFromUpstream(const Envoy::Http::TestRequestHeaderMapImpl& request_headers); - - /** - * @brief Fetch a response using the provided request headers. The extension under test must - * supply a response. - * - * @param request_headers Supply a full set of request headers. If the request method is set to - * POST, an entity body will be send after the request headers. - * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can - * be used to inspect the response. - */ - Envoy::IntegrationStreamDecoderPtr - getResponseFromExtension(const Envoy::Http::TestRequestHeaderMapImpl& request_headers); + Envoy::IntegrationStreamDecoderPtr getResponse(ResponseOrigin expected_origin); private: - /** - * @brief Fetches a response with request-level configuration set in the request header. - * - * @param request_level_config Configuration to be delivered by request header. For example - * "{{response_body_size:1024}". - * @param setup_for_upstream_request Set to true iff the filter extension under test is expected - * to short-circuit and supply a response directly. For example because it couldn't parse the - * supplied request-level configuration. Otherwise this should be set to true, and a stock - * response will be yielded by the integration test server through an upstream request. - * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can - * be used to inspect the response. - */ - Envoy::IntegrationStreamDecoderPtr getResponse(absl::string_view request_level_config, - bool setup_for_upstream_request); - - /** - * @brief Fetch a response using the provided request headers. - * - * @param request_headers Supply a full set of request headers. If the request method is set to - * POST, an entity body will be send after the request headers. - * @param setup_for_upstream_request Set to true iff the filter extension under test is expected - * to short-circuit and supply a response directly. For example because it couldn't parse the - * supplied request-level configuration. Otherwise this should be set to true, and a stock - * response will be yielded by the integration test server through an upstream request. - * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can - * be used to inspect the response. - */ - Envoy::IntegrationStreamDecoderPtr - getResponse(const Envoy::Http::TestRequestHeaderMapImpl& request_headers, - bool setup_for_upstream_request); - - const Envoy::Http::TestRequestHeaderMapImpl request_headers_; + Envoy::Http::TestRequestHeaderMapImpl request_headers_; }; } // namespace Nighthawk diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 6a9b6c100..8bebc2e68 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -11,7 +11,6 @@ #include "server/configuration.h" #include "server/http_time_tracking_filter.h" -#include "server/well_known_headers.h" #include "test/server/http_filter_integration_test_base.h" @@ -45,12 +44,12 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, // Verify expectations with static/file-based time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfiguration) { setup(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); - Envoy::IntegrationStreamDecoderPtr response = getResponseFromUpstream(); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); int64_t latency; const Envoy::Http::HeaderEntry* latency_header_1 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); EXPECT_EQ(latency_header_1, nullptr); - response = getResponseFromUpstream(); + response = getResponse(ResponseOrigin::UPSTREAM); const Envoy::Http::HeaderEntry* latency_header_2 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header_2, nullptr); @@ -62,12 +61,13 @@ TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfigura TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfiguration) { setup(fmt::format(kProtoConfigTemplate, "")); // Don't send any config request header - getResponseFromUpstream(); + getResponse(ResponseOrigin::UPSTREAM); // Send a config request header with an empty / default config. Should be a no-op. - getResponseFromUpstream("{}"); + updateRequestLevelConfiguration("{}"); + getResponse(ResponseOrigin::UPSTREAM); // Send a config request header, this should become effective. - Envoy::IntegrationStreamDecoderPtr response = - getResponseFromUpstream(fmt::format("{{{}}}", kDefaultProtoFragment)); + updateRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); const Envoy::Http::HeaderEntry* latency_header = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header, nullptr); @@ -82,14 +82,16 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguratio setup(fmt::format(kProtoConfigTemplate, "")); // Send a malformed config request header. This ought to shortcut and directly reply, // hence we don't expect an upstream request. - Envoy::IntegrationStreamDecoderPtr response = getResponseFromExtension("bad_json"); + updateRequestLevelConfiguration("bad_json"); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), "time-tracking didn't understand the request: Error merging json config: Unable to parse " "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); // Send an empty config header, which ought to trigger failure mode as well. - response = getResponseFromExtension(""); + updateRequestLevelConfiguration(""); + response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), @@ -101,23 +103,20 @@ TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguratio // This takes a slightly different code path, so it is important to test this explicitly. TEST_P(HttpTimeTrackingIntegrationTest, BehaviorWithRequestBody) { setup(fmt::format(kProtoConfigTemplate, "")); - Envoy::Http::TestRequestHeaderMapImpl request_headers( - {{":method", "POST"}, {":path", "/"}, {":authority", "host"}}); + switchToPostWithEntityBody(); // Post without any request-level configuration. Should succeed. - getResponseFromUpstream(request_headers); - - Envoy::IntegrationStreamDecoderPtr response = - getResponseFromUpstream(fmt::format("{{{}}}", kDefaultProtoFragment)); + getResponse(ResponseOrigin::UPSTREAM); + updateRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); const Envoy::Http::HeaderEntry* latency_header = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header, nullptr); - // Post with bad request-level configuration. The extension should response directly with an + // Post with bad request-level configuration. The extension should respond directly with an // error. - const Envoy::Http::LowerCaseString key("x-nighthawk-test-server-config"); - request_headers.setCopy(key, "bad_json"); - response = getResponseFromExtension(request_headers); + updateRequestLevelConfiguration("bad_json"); + response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_EQ( response->body(), From 44496b9b905a6fc77e96ab359c2a8043071f899a Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Sat, 12 Sep 2020 23:19:07 +0200 Subject: [PATCH 37/48] Move over the test-server extension to the new extension test base. The most complex one. Also explicitly test the post path. Signed-off-by: Otto van der Schaaf --- test/server/BUILD | 3 +- .../http_filter_integration_test_base.cc | 12 +- .../http_filter_integration_test_base.h | 8 + ...ttp_test_server_filter_integration_test.cc | 269 ++++++------------ ...p_time_tracking_filter_integration_test.cc | 4 - 5 files changed, 109 insertions(+), 187 deletions(-) diff --git a/test/server/BUILD b/test/server/BUILD index 71ae178ab..9ceb82d5a 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -25,10 +25,9 @@ envoy_cc_test( srcs = ["http_test_server_filter_integration_test.cc"], repository = "@envoy", deps = [ + ":http_filter_integration_test_base_lib", "//source/server:http_test_server_filter_config", - "@envoy//include/envoy/upstream:cluster_manager_interface_with_external_headers", "@envoy//source/common/api:api_lib_with_external_headers", - "@envoy//test/integration:http_integration_lib", ], ) diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc index c6f525db6..e7585e73e 100644 --- a/test/server/http_filter_integration_test_base.cc +++ b/test/server/http_filter_integration_test_base.cc @@ -18,13 +18,17 @@ void HttpFilterIntegrationTestBase::setup(const std::string& config) { void HttpFilterIntegrationTestBase::updateRequestLevelConfiguration( absl::string_view request_level_config) { - request_headers_.setCopy(Server::TestServer::HeaderNames::get().TestServerConfig, - request_level_config); + setRequestHeader(Server::TestServer::HeaderNames::get().TestServerConfig, request_level_config); } void HttpFilterIntegrationTestBase::switchToPostWithEntityBody() { - request_headers_.setCopy(Envoy::Http::Headers::get().Method, - Envoy::Http::Headers::get().MethodValues.Post); + setRequestHeader(Envoy::Http::Headers::get().Method, + Envoy::Http::Headers::get().MethodValues.Post); +} + +void HttpFilterIntegrationTestBase::setRequestHeader(Envoy::Http::LowerCaseString header_name, + absl::string_view header_value) { + request_headers_.setCopy(header_name, header_value); } Envoy::IntegrationStreamDecoderPtr diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index c3d72ca18..6a8fecdc6 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -47,6 +47,14 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { */ void switchToPostWithEntityBody(); + /** + * Set a request header value. Overwrites any existing value. + * + * @param header_name Name of the request header to set. + * @param header_value Value to set for the request header. + */ + void setRequestHeader(Envoy::Http::LowerCaseString header_name, absl::string_view header_value); + /** * Fetch a response. The request headers default to a minimal GET, but this may be changed * via other methods in this class. diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index f91b9b404..4ec36d4d3 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -1,94 +1,39 @@ -#include "envoy/upstream/cluster_manager.h" -#include "envoy/upstream/upstream.h" - -#include "external/envoy/test/common/upstream/utility.h" -#include "external/envoy/test/integration/http_integration.h" - #include "api/server/response_options.pb.h" #include "api/server/response_options.pb.validate.h" #include "server/configuration.h" #include "server/http_test_server_filter.h" -#include "server/well_known_headers.h" + +#include "test/server/http_filter_integration_test_base.h" #include "gtest/gtest.h" namespace Nighthawk { using namespace testing; -constexpr absl::string_view kBadJson = "bad_json"; -class HttpTestServerIntegrationTestBase : public Envoy::HttpIntegrationTest, - public TestWithParam { -public: - HttpTestServerIntegrationTestBase() - : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, GetParam(), realTime()) {} - - // TODO(oschaaf): Modify Envoy's Envoy::IntegrationUtil::makeSingleRequest() to allow for a way to - // manipulate the request headers before they get send. Then we can eliminate these copies. - Envoy::BufferingStreamDecoderPtr makeSingleRequest( - uint32_t port, absl::string_view method, absl::string_view url, absl::string_view body, - Envoy::Http::CodecClient::Type type, Envoy::Network::Address::IpVersion ip_version, - absl::string_view host, absl::string_view content_type, - const std::function& request_header_delegate) { - auto addr = Envoy::Network::Utility::resolveUrl(fmt::format( - "tcp://{}:{}", Envoy::Network::Test::getLoopbackAddressUrlString(ip_version), port)); - return makeSingleRequest(addr, method, url, body, type, host, content_type, - request_header_delegate); - } +constexpr absl::string_view kBadJson = "bad_json"; +constexpr char kDefaultProto[] = R"EOF( +name: test-server +typed_config: + "@type": type.googleapis.com/nighthawk.server.ResponseOptions + response_body_size: 10 + response_headers: + - { header: { key: "x-supplied-by", value: "nighthawk-test-server"} } +)EOF"; - Envoy::BufferingStreamDecoderPtr makeSingleRequest( - const Envoy::Network::Address::InstanceConstSharedPtr& addr, absl::string_view method, - absl::string_view url, absl::string_view body, Envoy::Http::CodecClient::Type type, - absl::string_view host, absl::string_view content_type, - const std::function& request_header_delegate) { - Envoy::Api::ApiPtr api = Envoy::Api::createApiForTest(); - Envoy::Event::DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); - std::shared_ptr cluster{ - new NiceMock()}; - Envoy::Upstream::HostDescriptionConstSharedPtr host_description{ - Envoy::Upstream::makeTestHostDescription(cluster, "tcp://127.0.0.1:80")}; - Envoy::Http::CodecClientProd client( - type, - dispatcher->createClientConnection(addr, Envoy::Network::Address::InstanceConstSharedPtr(), - Envoy::Network::Test::createRawBufferSocket(), nullptr), - host_description, *dispatcher); - Envoy::BufferingStreamDecoderPtr response( - new Envoy::BufferingStreamDecoder([&client, &dispatcher]() -> void { - client.close(); - dispatcher->exit(); - })); - Envoy::Http::RequestEncoder& encoder = client.newStream(*response); - encoder.getStream().addCallbacks(*response); - - auto headers = Envoy::Http::RequestHeaderMapImpl::create(); - headers->setMethod(method); - headers->setPath(url); - headers->setHost(host); - headers->setScheme(Envoy::Http::Headers::get().SchemeValues.Http); - if (!content_type.empty()) { - headers->setContentType(content_type); - } - request_header_delegate(*headers); - encoder.encodeHeaders(*headers, body.empty()); - if (!body.empty()) { - Envoy::Buffer::OwnedImpl body_buffer(body); - encoder.encodeData(body_buffer, true); - } +constexpr char kNoConfigProto[] = R"EOF( +name: test-server +)EOF"; - dispatcher->run(Envoy::Event::Dispatcher::RunType::Block); - return response; - } +class HttpTestServerIntegrationTest : public HttpFilterIntegrationTestBase, + public TestWithParam { +public: + HttpTestServerIntegrationTest() : HttpFilterIntegrationTestBase(GetParam()) {} void testWithResponseSize(int response_body_size, bool expect_header = true) { - Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( - lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [response_body_size](Envoy::Http::RequestHeaderMapImpl& request_headers) { - const std::string header_config = - fmt::format("{{response_body_size:{}}}", response_body_size); - request_headers.addCopy( - Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, header_config); - }); + updateRequestLevelConfiguration(fmt::format("{{response_body_size:{}}}", response_body_size)); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); if (expect_header) { @@ -105,55 +50,26 @@ class HttpTestServerIntegrationTestBase : public Envoy::HttpIntegrationTest, } void testBadResponseSize(int response_body_size) { - Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( - lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [response_body_size](Envoy::Http::RequestHeaderMapImpl& request_headers) { - const std::string header_config = - fmt::format("{{response_body_size:{}}}", response_body_size); - request_headers.addCopy( - Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, header_config); - }); + updateRequestLevelConfiguration(fmt::format("{{response_body_size:{}}}", response_body_size)); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("500", response->headers().Status()->value().getStringView()); } }; -class HttpTestServerIntegrationTest : public HttpTestServerIntegrationTestBase { -public: - void SetUp() override { initialize(); } - - void initialize() override { - config_helper_.addFilter(R"EOF( -name: test-server -typed_config: - "@type": type.googleapis.com/nighthawk.server.ResponseOptions - response_body_size: 10 - response_headers: - - { header: { key: "x-supplied-by", value: "nighthawk-test-server"} } -)EOF"); - HttpTestServerIntegrationTestBase::initialize(); - } - - void TearDown() override { - cleanupUpstreamAndDownstream(); - test_server_.reset(); - fake_upstreams_.clear(); - } -}; - INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTestServerIntegrationTest, ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest())); TEST_P(HttpTestServerIntegrationTest, TestNoHeaderConfig) { - Envoy::BufferingStreamDecoderPtr response = - makeSingleRequest(lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, - "foo.com", "", [](Envoy::Http::RequestHeaderMapImpl&) {}); + setup(kDefaultProto); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_EQ(std::string(10, 'a'), response->body()); } TEST_P(HttpTestServerIntegrationTest, TestBasics) { + setup(kDefaultProto); testWithResponseSize(1); testWithResponseSize(10); testWithResponseSize(100); @@ -161,30 +77,34 @@ TEST_P(HttpTestServerIntegrationTest, TestBasics) { testWithResponseSize(10000); } -TEST_P(HttpTestServerIntegrationTest, TestNegative) { testBadResponseSize(-1); } +TEST_P(HttpTestServerIntegrationTest, TestNegative) { + setup(kDefaultProto); + testBadResponseSize(-1); +} // TODO(oschaaf): We can't currently override with a default value ('0') in this case. -TEST_P(HttpTestServerIntegrationTest, DISABLED_TestZeroLengthRequest) { testWithResponseSize(0); } +TEST_P(HttpTestServerIntegrationTest, DISABLED_TestZeroLengthRequest) { + setup(kDefaultProto); + testWithResponseSize(0); +} TEST_P(HttpTestServerIntegrationTest, TestMaxBoundaryLengthRequest) { + setup(kDefaultProto); const int max = 1024 * 1024 * 4; testWithResponseSize(max); } TEST_P(HttpTestServerIntegrationTest, TestTooLarge) { + setup(kDefaultProto); const int max = 1024 * 1024 * 4; testBadResponseSize(max + 1); } TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { - Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( - lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [](Envoy::Http::RequestHeaderMapImpl& request_headers) { - const std::string header_config = - R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"; - request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, - header_config); - }); + setup(kDefaultProto); + updateRequestLevelConfiguration( + R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_EQ("bar2", @@ -193,17 +113,15 @@ TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { + setup(kDefaultProto); + updateRequestLevelConfiguration("{echo_request_headers: true}"); + setRequestHeader(Envoy::Http::LowerCaseString("gray"), "pidgeon"); + setRequestHeader(Envoy::Http::LowerCaseString("red"), "fox"); + setRequestHeader(Envoy::Http::LowerCaseString(":authority"), "foo.com"); + setRequestHeader(Envoy::Http::LowerCaseString(":path"), "/somepath"); for (auto unique_header : {"one", "two", "three"}) { - Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( - lookupPort("http"), "GET", "/somepath", "", downstream_protocol_, version_, "foo.com", "", - [unique_header](Envoy::Http::RequestHeaderMapImpl& request_headers) { - request_headers.addCopy(Envoy::Http::LowerCaseString("gray"), "pidgeon"); - request_headers.addCopy(Envoy::Http::LowerCaseString("red"), "fox"); - request_headers.addCopy(Envoy::Http::LowerCaseString("unique_header"), unique_header); - request_headers.addCopy( - Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, - "{echo_request_headers: true}"); - }); + setRequestHeader(Envoy::Http::LowerCaseString("unique_header"), unique_header); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_THAT(response->body(), HasSubstr(R"(':authority', 'foo.com')")); @@ -215,37 +133,16 @@ TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { } } -class HttpTestServerIntegrationNoConfigTest : public HttpTestServerIntegrationTestBase { -public: - void SetUp() override { initialize(); } - - void TearDown() override { - cleanupUpstreamAndDownstream(); - test_server_.reset(); - fake_upstreams_.clear(); - } - - void initialize() override { - config_helper_.addFilter(R"EOF( -name: test-server -)EOF"); - HttpTestServerIntegrationTestBase::initialize(); - } -}; - -INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTestServerIntegrationNoConfigTest, - ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest())); - -TEST_P(HttpTestServerIntegrationNoConfigTest, TestNoHeaderConfig) { - Envoy::BufferingStreamDecoderPtr response = - makeSingleRequest(lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, - "foo.com", "", [](Envoy::Http::RequestHeaderMapImpl&) {}); +TEST_P(HttpTestServerIntegrationTest, NoNoStaticConfigHeaderConfig) { + setup(kNoConfigProto); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_EQ("", response->body()); } -TEST_P(HttpTestServerIntegrationNoConfigTest, TestBasics) { +TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigBasics) { + setup(kNoConfigProto); testWithResponseSize(1, false); testWithResponseSize(10, false); testWithResponseSize(100, false); @@ -253,31 +150,34 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestBasics) { testWithResponseSize(10000, false); } -TEST_P(HttpTestServerIntegrationNoConfigTest, TestNegative) { testBadResponseSize(-1); } +TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigNegative) { + setup(kNoConfigProto); + testBadResponseSize(-1); +} -TEST_P(HttpTestServerIntegrationNoConfigTest, TestZeroLengthRequest) { +TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigZeroLengthRequest) { + setup(kNoConfigProto); testWithResponseSize(0, false); } -TEST_P(HttpTestServerIntegrationNoConfigTest, TestMaxBoundaryLengthRequest) { +TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigMaxBoundaryLengthRequest) { + setup(kNoConfigProto); const int max = 1024 * 1024 * 4; testWithResponseSize(max, false); } -TEST_P(HttpTestServerIntegrationNoConfigTest, TestTooLarge) { +TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigTooLarge) { + setup(kNoConfigProto); const int max = 1024 * 1024 * 4; testBadResponseSize(max + 1); } -TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { - Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( - lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [](Envoy::Http::RequestHeaderMapImpl& request_headers) { - const std::string header_config = - R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"; - request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, - header_config); - }); +TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigHeaderConfig) { + setup(kNoConfigProto); + updateRequestLevelConfiguration( + R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); + ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_EQ("bar2", @@ -285,13 +185,10 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, TestHeaderConfig) { EXPECT_EQ("", response->body()); } -TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) { - Envoy::BufferingStreamDecoderPtr response = makeSingleRequest( - lookupPort("http"), "GET", "/", "", downstream_protocol_, version_, "foo.com", "", - [](Envoy::Http::RequestHeaderMapImpl& request_headers) { - request_headers.addCopy(Nighthawk::Server::TestServer::HeaderNames::get().TestServerConfig, - kBadJson); - }); +TEST_P(HttpTestServerIntegrationTest, BadNoStaticConfigTestHeaderConfig) { + setup(kNoConfigProto); + updateRequestLevelConfiguration(kBadJson); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("500", response->headers().Status()->value().getStringView()); EXPECT_EQ("test-server didn't understand the request: Error merging json config: Unable to parse " @@ -299,10 +196,28 @@ TEST_P(HttpTestServerIntegrationNoConfigTest, BadTestHeaderConfig) { response->body()); } -class HttpTestServerDecoderFilterTest : public Test {}; +// Verify the filter is well-behaved when it comes to requests with an entity body. +// This takes a slightly different code path, so it is important to test this explicitly. +TEST_P(HttpTestServerIntegrationTest, BehaviorWithRequestBody) { + setup(kDefaultProto); + switchToPostWithEntityBody(); + // Post without any request-level configuration. Should succeed. + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_EQ(std::string(10, 'a'), response->body()); + // Post with bad request-level configuration. The extension should respond directly with an + // error. + updateRequestLevelConfiguration("bad_json"); + response = getResponse(ResponseOrigin::EXTENSION); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_EQ(response->body(), + "test-server didn't understand the request: Error merging json config: Unable to parse " + "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); +} // Here we test config-level merging as well as its application at the response-header level. -TEST_F(HttpTestServerDecoderFilterTest, HeaderMerge) { +TEST(HttpTestServerDecoderFilterTest, HeaderMerge) { nighthawk::server::ResponseOptions initial_options; auto response_header = initial_options.add_response_headers(); response_header->mutable_header()->set_key("foo"); diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 8bebc2e68..4261929af 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -1,9 +1,5 @@ #include -#include "envoy/upstream/cluster_manager.h" -#include "envoy/upstream/upstream.h" - -#include "external/envoy/test/common/upstream/utility.h" #include "external/envoy/test/test_common/simulated_time_system.h" #include "api/server/response_options.pb.h" From 4022a79161179d3922833562ab4fe3af21c4bee3 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Sat, 12 Sep 2020 23:39:02 +0200 Subject: [PATCH 38/48] tidy up Signed-off-by: Otto van der Schaaf --- test/server/BUILD | 1 - test/server/http_dynamic_delay_filter_integration_test.cc | 1 - test/server/http_filter_integration_test_base.cc | 4 ++-- test/server/http_filter_integration_test_base.h | 3 ++- test/server/http_test_server_filter_integration_test.cc | 6 +++--- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/test/server/BUILD b/test/server/BUILD index 9ceb82d5a..c9457c55f 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -49,7 +49,6 @@ envoy_cc_test( deps = [ ":http_filter_integration_test_base_lib", "//source/server:http_time_tracking_filter_config", - "@envoy//include/envoy/upstream:cluster_manager_interface_with_external_headers", "@envoy//source/common/api:api_lib_with_external_headers", "@envoy//test/test_common:simulated_time_system_lib", ], diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index f76067685..f116c4ec1 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -23,7 +23,6 @@ const Envoy::Http::LowerCaseString kDelayHeaderString("x-envoy-fault-delay-reque * - TODO(#393): An end to end test which proves that the interaction between this filter * and the fault filter work as expected. */ - class HttpDynamicDelayIntegrationTest : public HttpFilterIntegrationTestBase, public testing::TestWithParam { diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc index e7585e73e..ee0239fc5 100644 --- a/test/server/http_filter_integration_test_base.cc +++ b/test/server/http_filter_integration_test_base.cc @@ -26,8 +26,8 @@ void HttpFilterIntegrationTestBase::switchToPostWithEntityBody() { Envoy::Http::Headers::get().MethodValues.Post); } -void HttpFilterIntegrationTestBase::setRequestHeader(Envoy::Http::LowerCaseString header_name, - absl::string_view header_value) { +void HttpFilterIntegrationTestBase::setRequestHeader( + const Envoy::Http::LowerCaseString& header_name, absl::string_view header_value) { request_headers_.setCopy(header_name, header_value); } diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index 6a8fecdc6..1d6809a1f 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -53,7 +53,8 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { * @param header_name Name of the request header to set. * @param header_value Value to set for the request header. */ - void setRequestHeader(Envoy::Http::LowerCaseString header_name, absl::string_view header_value); + void setRequestHeader(const Envoy::Http::LowerCaseString& header_name, + absl::string_view header_value); /** * Fetch a response. The request headers default to a minimal GET, but this may be changed diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 4ec36d4d3..df1216996 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -12,8 +12,8 @@ namespace Nighthawk { using namespace testing; -constexpr absl::string_view kBadJson = "bad_json"; -constexpr char kDefaultProto[] = R"EOF( +const std::string kBadJson = "bad_json"; +const std::string kDefaultProto = R"EOF( name: test-server typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions @@ -22,7 +22,7 @@ name: test-server - { header: { key: "x-supplied-by", value: "nighthawk-test-server"} } )EOF"; -constexpr char kNoConfigProto[] = R"EOF( +const std::string kNoConfigProto = R"EOF( name: test-server )EOF"; From 38614565aef46f31622f2bd734cb66f201fe50c8 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 14 Sep 2020 02:00:25 +0200 Subject: [PATCH 39/48] Add http_filter_base_test Signed-off-by: Otto van der Schaaf --- test/server/BUILD | 12 +++ ...p_dynamic_delay_filter_integration_test.cc | 43 --------- test/server/http_filter_base_test.cc | 89 +++++++++++++++++++ ...ttp_test_server_filter_integration_test.cc | 34 +------ ...p_time_tracking_filter_integration_test.cc | 46 ---------- 5 files changed, 102 insertions(+), 122 deletions(-) create mode 100644 test/server/http_filter_base_test.cc diff --git a/test/server/BUILD b/test/server/BUILD index c9457c55f..68d5bab08 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -20,6 +20,18 @@ envoy_cc_test_library( ], ) +envoy_cc_test( + name = "http_filter_base_test", + srcs = ["http_filter_base_test.cc"], + repository = "@envoy", + deps = [ + ":http_filter_integration_test_base_lib", + "//source/server:http_dynamic_delay_filter_config", + "//source/server:http_test_server_filter_config", + "//source/server:http_time_tracking_filter_config", + ], +) + envoy_cc_test( name = "http_test_server_filter_integration_test", srcs = ["http_test_server_filter_integration_test.cc"], diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index f116c4ec1..a5bbd1850 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -52,49 +52,6 @@ name: dynamic-delay updateRequestLevelConfiguration("{static_delay: \"1.6s\"}"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1600"); - - // Send a malformed config request header. This ought to shortcut and directly reply, - // hence we don't expect an upstream request. - updateRequestLevelConfiguration("bad_json"); - auto response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ( - response->body(), - "dynamic-delay didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); - // Send an empty config header, which ought to trigger failure mode as well. - updateRequestLevelConfiguration(""); - response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ( - response->body(), - "dynamic-delay didn't understand the request: Error merging json config: Unable to " - "parse JSON as proto (INVALID_ARGUMENT:Unexpected end of string. Expected a value.\n\n^): "); -} - -// Verify the filter is well-behaved when it comes to requests with an entity body. -// This takes a slightly different code path, so it is important to test this explicitly. -TEST_P(HttpDynamicDelayIntegrationTest, BehaviorWithRequestBody) { - setup(R"EOF( -name: dynamic-delay -typed_config: - "@type": type.googleapis.com/nighthawk.server.ResponseOptions - static_delay: 0.1s -)EOF"); - switchToPostWithEntityBody(); - // Post without any request-level configuration. Should succeed. - getResponse(ResponseOrigin::UPSTREAM); - EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "100"); - - // Post with bad request-level configuration. The extension should respond directly with an - // error. - updateRequestLevelConfiguration("bad_json"); - auto response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ( - response->body(), - "dynamic-delay didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); } // Verify expectations with static/file-based static_delay configuration. diff --git a/test/server/http_filter_base_test.cc b/test/server/http_filter_base_test.cc new file mode 100644 index 000000000..6408288fb --- /dev/null +++ b/test/server/http_filter_base_test.cc @@ -0,0 +1,89 @@ +#include "server/http_dynamic_delay_filter.h" +#include "server/http_test_server_filter.h" +#include "server/http_time_tracking_filter.h" + +#include "test/server/http_filter_integration_test_base.h" + +#include "gtest/gtest.h" + +namespace Nighthawk { + +using namespace testing; + +class HttpFilterBaseIntegrationTest + : public HttpFilterIntegrationTestBase, + public testing::TestWithParam< + std::tuple> { +public: + HttpFilterBaseIntegrationTest() : HttpFilterIntegrationTestBase(std::get<0>(GetParam())){}; +}; + +INSTANTIATE_TEST_SUITE_P( + IpVersions, HttpFilterBaseIntegrationTest, + ::testing::Combine(testing::ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest()), + testing::ValuesIn({absl::string_view(R"EOF( +name: time-tracking +typed_config: + "@type": type.googleapis.com/nighthawk.server.ResponseOptions + emit_previous_request_delta_in_response_header: "foo" +)EOF"), + absl::string_view(R"EOF( +name: dynamic-delay +typed_config: + "@type": type.googleapis.com/nighthawk.server.ResponseOptions + static_delay: 0.1s +)EOF"), + absl::string_view("name: test-server")}), + testing::ValuesIn({true, false}))); + +// Verify extensions are well-behaved when it comes to: +// - GET requests. +// - POST requests with an entity body (takes a slightly different code path). +// - Valid but empty configuration. +// - Bad request-level json configuration. +// We will be low on functional expectations for the extensions, but this will catch hangs and +// ensure that bad configuration handling is in-place. +TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { + absl::string_view config = std::get<1>(GetParam()); + setup(std::string(config)); + bool is_post = std::get<2>(GetParam()); + if (is_post) { + switchToPostWithEntityBody(); + } + + // Modulo the test-server, extensions are expected to need an upstream to synthesize a reply + // when effective configuration is valid. + ResponseOrigin happy_flow_response_origin = config.find_first_of("name: test-server") == 0 + ? ResponseOrigin::EXTENSION + : ResponseOrigin::UPSTREAM; + // Post without any request-level configuration. Should succeed. + Envoy::IntegrationStreamDecoderPtr response = getResponse(happy_flow_response_origin); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_TRUE(response->body().empty()); + + // Test with a valid but empty request-level configuration. + updateRequestLevelConfiguration("{}"); + response = getResponse(happy_flow_response_origin); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + EXPECT_TRUE(response->body().empty()); + + const char kBadConfig[] = + "didn't understand the request: Error merging json config: Unable to parse " + "JSON as proto (INVALID_ARGUMENT:Unexpected"; + + // When sending bad request-level configuration, the extension ought to reply directly. + updateRequestLevelConfiguration("bad_json"); + response = getResponse(ResponseOrigin::EXTENSION); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_THAT(response->body(), HasSubstr(kBadConfig)); + + // When sending empty request-level configuration, the extension ought to reply directly. + updateRequestLevelConfiguration(""); + response = getResponse(ResponseOrigin::EXTENSION); + EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); + EXPECT_THAT(response->body(), HasSubstr(kBadConfig)); +} + +} // namespace Nighthawk \ No newline at end of file diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index df1216996..25866b915 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -12,7 +12,6 @@ namespace Nighthawk { using namespace testing; -const std::string kBadJson = "bad_json"; const std::string kDefaultProto = R"EOF( name: test-server typed_config: @@ -185,37 +184,6 @@ TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigHeaderConfig) { EXPECT_EQ("", response->body()); } -TEST_P(HttpTestServerIntegrationTest, BadNoStaticConfigTestHeaderConfig) { - setup(kNoConfigProto); - updateRequestLevelConfiguration(kBadJson); - Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); - ASSERT_TRUE(response->complete()); - EXPECT_EQ("500", response->headers().Status()->value().getStringView()); - EXPECT_EQ("test-server didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json", - response->body()); -} - -// Verify the filter is well-behaved when it comes to requests with an entity body. -// This takes a slightly different code path, so it is important to test this explicitly. -TEST_P(HttpTestServerIntegrationTest, BehaviorWithRequestBody) { - setup(kDefaultProto); - switchToPostWithEntityBody(); - // Post without any request-level configuration. Should succeed. - Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); - ASSERT_TRUE(response->complete()); - EXPECT_EQ("200", response->headers().Status()->value().getStringView()); - EXPECT_EQ(std::string(10, 'a'), response->body()); - // Post with bad request-level configuration. The extension should respond directly with an - // error. - updateRequestLevelConfiguration("bad_json"); - response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ(response->body(), - "test-server didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); -} - // Here we test config-level merging as well as its application at the response-header level. TEST(HttpTestServerDecoderFilterTest, HeaderMerge) { nighthawk::server::ResponseOptions initial_options; @@ -273,7 +241,7 @@ TEST(HttpTestServerDecoderFilterTest, HeaderMerge) { header_map, Envoy::Http::TestResponseHeaderMapImpl{ {":status", "200"}, {"foo", "bar2"}, {"foo2", "bar3"}})); - EXPECT_FALSE(Server::Configuration::mergeJsonConfig(kBadJson, options, error_message)); + EXPECT_FALSE(Server::Configuration::mergeJsonConfig("bad_json", options, error_message)); EXPECT_EQ("Error merging json config: Unable to parse JSON as proto (INVALID_ARGUMENT:Unexpected " "token.\nbad_json\n^): bad_json", error_message); diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 4261929af..f94f3a406 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -74,52 +74,6 @@ TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfi EXPECT_GT(latency, 0); } -TEST_P(HttpTimeTrackingIntegrationTest, BehavesWellWithBadPerRequestConfiguration) { - setup(fmt::format(kProtoConfigTemplate, "")); - // Send a malformed config request header. This ought to shortcut and directly reply, - // hence we don't expect an upstream request. - updateRequestLevelConfiguration("bad_json"); - Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ( - response->body(), - "time-tracking didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); - // Send an empty config header, which ought to trigger failure mode as well. - updateRequestLevelConfiguration(""); - response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ( - response->body(), - "time-tracking didn't understand the request: Error merging json config: Unable to " - "parse JSON as proto (INVALID_ARGUMENT:Unexpected end of string. Expected a value.\n\n^): "); -} - -// Verify the filter is well-behaved when it comes to requests with an entity body. -// This takes a slightly different code path, so it is important to test this explicitly. -TEST_P(HttpTimeTrackingIntegrationTest, BehaviorWithRequestBody) { - setup(fmt::format(kProtoConfigTemplate, "")); - switchToPostWithEntityBody(); - - // Post without any request-level configuration. Should succeed. - getResponse(ResponseOrigin::UPSTREAM); - updateRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); - Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); - const Envoy::Http::HeaderEntry* latency_header = - response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); - ASSERT_NE(latency_header, nullptr); - - // Post with bad request-level configuration. The extension should respond directly with an - // error. - updateRequestLevelConfiguration("bad_json"); - response = getResponse(ResponseOrigin::EXTENSION); - EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_EQ( - response->body(), - "time-tracking didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected token.\nbad_json\n^): bad_json"); -} - class HttpTimeTrackingFilterConfigTest : public testing::Test, public Envoy::Event::TestUsingSimulatedTime {}; From 9d333acf9643f9d2fe0e9cdccdf40fc14d039251 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 14 Sep 2020 09:28:23 +0200 Subject: [PATCH 40/48] apply clang-tidy fix Signed-off-by: Otto van der Schaaf --- test/server/http_filter_base_test.cc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/server/http_filter_base_test.cc b/test/server/http_filter_base_test.cc index 6408288fb..8f4f789fc 100644 --- a/test/server/http_filter_base_test.cc +++ b/test/server/http_filter_base_test.cc @@ -69,7 +69,7 @@ TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_TRUE(response->body().empty()); - const char kBadConfig[] = + const std::string kBadConfigErrorSentinel = "didn't understand the request: Error merging json config: Unable to parse " "JSON as proto (INVALID_ARGUMENT:Unexpected"; @@ -77,13 +77,13 @@ TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { updateRequestLevelConfiguration("bad_json"); response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_THAT(response->body(), HasSubstr(kBadConfig)); + EXPECT_THAT(response->body(), HasSubstr(kBadConfigErrorSentinel)); // When sending empty request-level configuration, the extension ought to reply directly. updateRequestLevelConfiguration(""); response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); - EXPECT_THAT(response->body(), HasSubstr(kBadConfig)); + EXPECT_THAT(response->body(), HasSubstr(kBadConfigErrorSentinel)); } } // namespace Nighthawk \ No newline at end of file From 988d62a961a6804d377e57719ae4b35d21676899 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 14 Sep 2020 19:00:25 +0200 Subject: [PATCH 41/48] review feedback sync Signed-off-by: Otto van der Schaaf --- test/integration/test_integration_basics.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/integration/test_integration_basics.py b/test/integration/test_integration_basics.py index 4d922f8f4..5639fd99d 100644 --- a/test/integration/test_integration_basics.py +++ b/test/integration/test_integration_basics.py @@ -418,7 +418,7 @@ def test_cli_output_format(http_test_server_fixture): 'filter_configs', ["{}", "{static_delay: \"0.01s\"}", "{emit_previous_request_delta_in_response_header: \"aa\"}"]) def test_request_body_gets_transmitted(http_test_server_fixture, filter_configs): - """Test request body transmission with. + """Test request body transmission handling code for our extensions. Ensure that the number of bytes we request for the request body gets reflected in the upstream connection transmitted bytes counter for h1 and h2. @@ -436,10 +436,8 @@ def check_upload_expectations(fixture, parsed_json, expected_transmitted_bytes, "http.ingress_http.downstream_cx_rx_bytes_total"), expected_received_bytes) - upload_bytes = 1024 * 1024 * 3 - # TODO(XXX): The dynamic-delay extension hangs unless we lower the request entity body size. - if "static_delay" in filter_configs: - upload_bytes = int(upload_bytes / 3) + # TODO(#531): The dynamic-delay extension hangs unless we lower the request entity body size. + upload_bytes = 1024 * 1024 if "static_delay" in filter_configs else 1024 * 1024 * 3 requests = 10 args = [ http_test_server_fixture.getTestServerRootUri(), "--duration", "100", "--rps", "100", From 9f0229a01a18c9df35086fbfbb65e32a80439373 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Mon, 14 Sep 2020 23:35:52 +0200 Subject: [PATCH 42/48] Review feedback Signed-off-by: Otto van der Schaaf --- ...p_dynamic_delay_filter_integration_test.cc | 6 ++-- test/server/http_filter_base_test.cc | 2 +- .../http_filter_integration_test_base.cc | 11 +++++-- .../http_filter_integration_test_base.h | 2 +- ...ttp_test_server_filter_integration_test.cc | 30 +++++++++---------- ...p_time_tracking_filter_integration_test.cc | 4 +-- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index a5bbd1850..96f38f6ce 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -35,7 +35,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpDynamicDelayIntegrationTest, // Verify expectations with an empty dynamic-delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, NoStaticConfiguration) { - setup(R"( + initializeConfig(R"( name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions @@ -56,7 +56,7 @@ name: dynamic-delay // Verify expectations with static/file-based static_delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, StaticConfigurationStaticDelay) { - setup(R"EOF( + initializeConfig(R"EOF( name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions @@ -83,7 +83,7 @@ name: dynamic-delay // Verify expectations with static/file-based concurrency_based_linear_delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, StaticConfigurationConcurrentDelay) { - setup(R"EOF( + initializeConfig(R"EOF( name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions diff --git a/test/server/http_filter_base_test.cc b/test/server/http_filter_base_test.cc index 8f4f789fc..a6bc5084c 100644 --- a/test/server/http_filter_base_test.cc +++ b/test/server/http_filter_base_test.cc @@ -45,7 +45,7 @@ name: dynamic-delay // ensure that bad configuration handling is in-place. TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { absl::string_view config = std::get<1>(GetParam()); - setup(std::string(config)); + initializeConfig(std::string(config)); bool is_post = std::get<2>(GetParam()); if (is_post) { switchToPostWithEntityBody(); diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc index ee0239fc5..c85fddbaf 100644 --- a/test/server/http_filter_integration_test_base.cc +++ b/test/server/http_filter_integration_test_base.cc @@ -11,8 +11,8 @@ HttpFilterIntegrationTestBase::HttpFilterIntegrationTestBase( : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, ip_version), request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} -void HttpFilterIntegrationTestBase::setup(const std::string& config) { - config_helper_.addFilter(config); +void HttpFilterIntegrationTestBase::initializeConfig(absl::string_view config) { + config_helper_.addFilter(std::string(config)); HttpIntegrationTest::initialize(); } @@ -38,7 +38,14 @@ HttpFilterIntegrationTestBase::getResponse(ResponseOrigin expected_origin) { Envoy::IntegrationStreamDecoderPtr response; const bool is_post = request_headers_.Method()->value().getStringView() == Envoy::Http::Headers::get().MethodValues.Post; + // Upon observing a POST request method, we inject a content body, as promised in the header file. + // This is useful, because emitting an entity body will hit distinct code in extensions. Hence we + // facilitate that. const uint64_t request_body_size = is_post ? 1024 : 0; + // An extension can either act as an origin and synthesize a reply, or delegate that + // responsibility to an upstream. This behavior may change from request to request. For example, + // an extension is designed to transform input from an upstream, may start acting as an origin on + // misconfiguration. if (expected_origin == ResponseOrigin::UPSTREAM) { response = sendRequestAndWaitForResponse(request_headers_, request_body_size, default_response_headers_, /*response_body_size*/ 0); diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index 1d6809a1f..92c45ab86 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -29,7 +29,7 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { * * @param config configuration to pass to Envoy::HttpIntegrationTest::config_helper_.addFilter. */ - void setup(const std::string& config); + void initializeConfig(absl::string_view config); /** * Make getResponse send request-level configuration. Test server extensions read that diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index 25866b915..fdc1004fb 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -60,7 +60,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTestServerIntegrationTest, ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest())); TEST_P(HttpTestServerIntegrationTest, TestNoHeaderConfig) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); @@ -68,7 +68,7 @@ TEST_P(HttpTestServerIntegrationTest, TestNoHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestBasics) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); testWithResponseSize(1); testWithResponseSize(10); testWithResponseSize(100); @@ -77,30 +77,30 @@ TEST_P(HttpTestServerIntegrationTest, TestBasics) { } TEST_P(HttpTestServerIntegrationTest, TestNegative) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); testBadResponseSize(-1); } // TODO(oschaaf): We can't currently override with a default value ('0') in this case. TEST_P(HttpTestServerIntegrationTest, DISABLED_TestZeroLengthRequest) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); testWithResponseSize(0); } TEST_P(HttpTestServerIntegrationTest, TestMaxBoundaryLengthRequest) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); const int max = 1024 * 1024 * 4; testWithResponseSize(max); } TEST_P(HttpTestServerIntegrationTest, TestTooLarge) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); const int max = 1024 * 1024 * 4; testBadResponseSize(max + 1); } TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); updateRequestLevelConfiguration( R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); @@ -112,7 +112,7 @@ TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { - setup(kDefaultProto); + initializeConfig(kDefaultProto); updateRequestLevelConfiguration("{echo_request_headers: true}"); setRequestHeader(Envoy::Http::LowerCaseString("gray"), "pidgeon"); setRequestHeader(Envoy::Http::LowerCaseString("red"), "fox"); @@ -133,7 +133,7 @@ TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { } TEST_P(HttpTestServerIntegrationTest, NoNoStaticConfigHeaderConfig) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); @@ -141,7 +141,7 @@ TEST_P(HttpTestServerIntegrationTest, NoNoStaticConfigHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigBasics) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); testWithResponseSize(1, false); testWithResponseSize(10, false); testWithResponseSize(100, false); @@ -150,29 +150,29 @@ TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigBasics) { } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigNegative) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); testBadResponseSize(-1); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigZeroLengthRequest) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); testWithResponseSize(0, false); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigMaxBoundaryLengthRequest) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); const int max = 1024 * 1024 * 4; testWithResponseSize(max, false); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigTooLarge) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); const int max = 1024 * 1024 * 4; testBadResponseSize(max + 1); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigHeaderConfig) { - setup(kNoConfigProto); + initializeConfig(kNoConfigProto); updateRequestLevelConfiguration( R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index f94f3a406..bb2b9a20b 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -39,7 +39,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, // Verify expectations with static/file-based time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfiguration) { - setup(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); + initializeConfig(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); int64_t latency; const Envoy::Http::HeaderEntry* latency_header_1 = @@ -55,7 +55,7 @@ TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfigura // Verify expectations with an empty time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfiguration) { - setup(fmt::format(kProtoConfigTemplate, "")); + initializeConfig(fmt::format(kProtoConfigTemplate, "")); // Don't send any config request header getResponse(ResponseOrigin::UPSTREAM); // Send a config request header with an empty / default config. Should be a no-op. From c32c92b7839b31659d2c5326cd841ce006fd26f1 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 16 Sep 2020 22:26:24 +0200 Subject: [PATCH 43/48] Review feedback Signed-off-by: Otto van der Schaaf --- ...p_dynamic_delay_filter_integration_test.cc | 16 ++++---- test/server/http_filter_base_test.cc | 8 ++-- .../http_filter_integration_test_base.cc | 4 +- .../http_filter_integration_test_base.h | 30 +++++++++----- ...ttp_test_server_filter_integration_test.cc | 40 +++++++++---------- ...p_time_tracking_filter_integration_test.cc | 8 ++-- 6 files changed, 58 insertions(+), 48 deletions(-) diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index 96f38f6ce..7726e98e3 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -35,7 +35,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpDynamicDelayIntegrationTest, // Verify expectations with an empty dynamic-delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, NoStaticConfiguration) { - initializeConfig(R"( + initializeFilterConfiguration(R"( name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions @@ -45,18 +45,18 @@ name: dynamic-delay EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header with an empty / default config. Should be a no-op. - updateRequestLevelConfiguration("{}"); + setRequestLevelConfiguration("{}"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); // Send a config request header, this should become effective. - updateRequestLevelConfiguration("{static_delay: \"1.6s\"}"); + setRequestLevelConfiguration("{static_delay: \"1.6s\"}"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1600"); } // Verify expectations with static/file-based static_delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, StaticConfigurationStaticDelay) { - initializeConfig(R"EOF( + initializeFilterConfiguration(R"EOF( name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions @@ -64,10 +64,10 @@ name: dynamic-delay )EOF"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); - updateRequestLevelConfiguration("{}"); + setRequestLevelConfiguration("{}"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); - updateRequestLevelConfiguration("{static_delay: \"0.2s\"}"); + setRequestLevelConfiguration("{static_delay: \"0.2s\"}"); getResponse(ResponseOrigin::UPSTREAM); // TODO(#392): This fails, because the duration is a two-field message: it would make here to see // both the number of seconds and nanoseconds to be overridden. @@ -76,14 +76,14 @@ name: dynamic-delay // Hence the following expectation will fail, as it yields 1200 instead of the expected 200. // EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), // "200"); - updateRequestLevelConfiguration("{static_delay: \"2.2s\"}"); + setRequestLevelConfiguration("{static_delay: \"2.2s\"}"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "2200"); } // Verify expectations with static/file-based concurrency_based_linear_delay configuration. TEST_P(HttpDynamicDelayIntegrationTest, StaticConfigurationConcurrentDelay) { - initializeConfig(R"EOF( + initializeFilterConfiguration(R"EOF( name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions diff --git a/test/server/http_filter_base_test.cc b/test/server/http_filter_base_test.cc index a6bc5084c..222c35186 100644 --- a/test/server/http_filter_base_test.cc +++ b/test/server/http_filter_base_test.cc @@ -45,7 +45,7 @@ name: dynamic-delay // ensure that bad configuration handling is in-place. TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { absl::string_view config = std::get<1>(GetParam()); - initializeConfig(std::string(config)); + initializeFilterConfiguration(std::string(config)); bool is_post = std::get<2>(GetParam()); if (is_post) { switchToPostWithEntityBody(); @@ -63,7 +63,7 @@ TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { EXPECT_TRUE(response->body().empty()); // Test with a valid but empty request-level configuration. - updateRequestLevelConfiguration("{}"); + setRequestLevelConfiguration("{}"); response = getResponse(happy_flow_response_origin); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); @@ -74,13 +74,13 @@ TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { "JSON as proto (INVALID_ARGUMENT:Unexpected"; // When sending bad request-level configuration, the extension ought to reply directly. - updateRequestLevelConfiguration("bad_json"); + setRequestLevelConfiguration("bad_json"); response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_THAT(response->body(), HasSubstr(kBadConfigErrorSentinel)); // When sending empty request-level configuration, the extension ought to reply directly. - updateRequestLevelConfiguration(""); + setRequestLevelConfiguration(""); response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_THAT(response->body(), HasSubstr(kBadConfigErrorSentinel)); diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc index c85fddbaf..d204133ad 100644 --- a/test/server/http_filter_integration_test_base.cc +++ b/test/server/http_filter_integration_test_base.cc @@ -11,12 +11,12 @@ HttpFilterIntegrationTestBase::HttpFilterIntegrationTestBase( : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, ip_version), request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} -void HttpFilterIntegrationTestBase::initializeConfig(absl::string_view config) { +void HttpFilterIntegrationTestBase::initializeFilterConfiguration(absl::string_view config) { config_helper_.addFilter(std::string(config)); HttpIntegrationTest::initialize(); } -void HttpFilterIntegrationTestBase::updateRequestLevelConfiguration( +void HttpFilterIntegrationTestBase::setRequestLevelConfiguration( absl::string_view request_level_config) { setRequestHeader(Server::TestServer::HeaderNames::get().TestServerConfig, request_level_config); } diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index 92c45ab86..ccf68a24b 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -8,13 +8,23 @@ namespace Nighthawk { /** * Base class with shared functionality for testing Nighthawk test server http filter extensions. + * The class is stateful, and not safed to use from multiple threads. */ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { protected: /** - * Indicate the expected response origin. + * Indicate the expected response origin. A test failure will occur upon mismatch. */ - enum class ResponseOrigin { UPSTREAM, EXTENSION }; + enum class ResponseOrigin { + /** + * The upstream will supply the response, and not the extension under test. + */ + UPSTREAM, + /** + * The extension under test will suplly a response, and no upstream will be set up to do that. + */ + EXTENSION + }; /** * Construct a new HttpFilterIntegrationTestBase instance. * @@ -24,12 +34,13 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { HttpFilterIntegrationTestBase(Envoy::Network::Address::IpVersion ip_version); /** - * We don't override SetUp(): tests using this fixture must call setup() instead. - * This is to avoid imposing the need to create a fixture per filter configuration. + * We don't override SetUp(): tests using this fixture must call initializeFilterConfiguration() + * instead. This is to avoid imposing the need to create a fixture per filter configuration. * - * @param config configuration to pass to Envoy::HttpIntegrationTest::config_helper_.addFilter. + * @param filter_configuration Pass configuration for the filter under test. Will be handed off to + * Envoy::HttpIntegrationTest::config_helper_.addFilter. */ - void initializeConfig(absl::string_view config); + void initializeFilterConfiguration(absl::string_view filter_configuration); /** * Make getResponse send request-level configuration. Test server extensions read that @@ -39,7 +50,7 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { * @param request_level_config Configuration to be delivered by request-header in future calls to * getResponse(). For example: "{response_body_size:1024}". */ - void updateRequestLevelConfiguration(absl::string_view request_level_config); + void setRequestLevelConfiguration(absl::string_view request_level_config); /** * Switch getResponse() to use the POST request method with an entity body. @@ -57,9 +68,8 @@ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { absl::string_view header_value); /** - * Fetch a response. The request headers default to a minimal GET, but this may be changed - * via other methods in this class. - * + * Fetch a response, according to the options specified by the class methods. By default, + * simulates a GET request with minimal headers. * @param expected_origin Indicate which component will be expected to reply: the extension or * a fake upstream. Will cause a test failure upon mismatch. Can be used to verify that an * extension short circuits and directly responds when expected. diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index fdc1004fb..e35f5076d 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -31,7 +31,7 @@ class HttpTestServerIntegrationTest : public HttpFilterIntegrationTestBase, HttpTestServerIntegrationTest() : HttpFilterIntegrationTestBase(GetParam()) {} void testWithResponseSize(int response_body_size, bool expect_header = true) { - updateRequestLevelConfiguration(fmt::format("{{response_body_size:{}}}", response_body_size)); + setRequestLevelConfiguration(fmt::format("{{response_body_size:{}}}", response_body_size)); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); @@ -49,7 +49,7 @@ class HttpTestServerIntegrationTest : public HttpFilterIntegrationTestBase, } void testBadResponseSize(int response_body_size) { - updateRequestLevelConfiguration(fmt::format("{{response_body_size:{}}}", response_body_size)); + setRequestLevelConfiguration(fmt::format("{{response_body_size:{}}}", response_body_size)); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("500", response->headers().Status()->value().getStringView()); @@ -60,7 +60,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTestServerIntegrationTest, ValuesIn(Envoy::TestEnvironment::getIpVersionsForTest())); TEST_P(HttpTestServerIntegrationTest, TestNoHeaderConfig) { - initializeConfig(kDefaultProto); + initializeFilterConfiguration(kDefaultProto); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); @@ -68,7 +68,7 @@ TEST_P(HttpTestServerIntegrationTest, TestNoHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestBasics) { - initializeConfig(kDefaultProto); + initializeFilterConfiguration(kDefaultProto); testWithResponseSize(1); testWithResponseSize(10); testWithResponseSize(100); @@ -77,31 +77,31 @@ TEST_P(HttpTestServerIntegrationTest, TestBasics) { } TEST_P(HttpTestServerIntegrationTest, TestNegative) { - initializeConfig(kDefaultProto); + initializeFilterConfiguration(kDefaultProto); testBadResponseSize(-1); } // TODO(oschaaf): We can't currently override with a default value ('0') in this case. TEST_P(HttpTestServerIntegrationTest, DISABLED_TestZeroLengthRequest) { - initializeConfig(kDefaultProto); + initializeFilterConfiguration(kDefaultProto); testWithResponseSize(0); } TEST_P(HttpTestServerIntegrationTest, TestMaxBoundaryLengthRequest) { - initializeConfig(kDefaultProto); + initializeFilterConfiguration(kDefaultProto); const int max = 1024 * 1024 * 4; testWithResponseSize(max); } TEST_P(HttpTestServerIntegrationTest, TestTooLarge) { - initializeConfig(kDefaultProto); + initializeFilterConfiguration(kDefaultProto); const int max = 1024 * 1024 * 4; testBadResponseSize(max + 1); } TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { - initializeConfig(kDefaultProto); - updateRequestLevelConfiguration( + initializeFilterConfiguration(kDefaultProto); + setRequestLevelConfiguration( R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); @@ -112,8 +112,8 @@ TEST_P(HttpTestServerIntegrationTest, TestHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { - initializeConfig(kDefaultProto); - updateRequestLevelConfiguration("{echo_request_headers: true}"); + initializeFilterConfiguration(kDefaultProto); + setRequestLevelConfiguration("{echo_request_headers: true}"); setRequestHeader(Envoy::Http::LowerCaseString("gray"), "pidgeon"); setRequestHeader(Envoy::Http::LowerCaseString("red"), "fox"); setRequestHeader(Envoy::Http::LowerCaseString(":authority"), "foo.com"); @@ -133,7 +133,7 @@ TEST_P(HttpTestServerIntegrationTest, TestEchoHeaders) { } TEST_P(HttpTestServerIntegrationTest, NoNoStaticConfigHeaderConfig) { - initializeConfig(kNoConfigProto); + initializeFilterConfiguration(kNoConfigProto); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); @@ -141,7 +141,7 @@ TEST_P(HttpTestServerIntegrationTest, NoNoStaticConfigHeaderConfig) { } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigBasics) { - initializeConfig(kNoConfigProto); + initializeFilterConfiguration(kNoConfigProto); testWithResponseSize(1, false); testWithResponseSize(10, false); testWithResponseSize(100, false); @@ -150,30 +150,30 @@ TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigBasics) { } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigNegative) { - initializeConfig(kNoConfigProto); + initializeFilterConfiguration(kNoConfigProto); testBadResponseSize(-1); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigZeroLengthRequest) { - initializeConfig(kNoConfigProto); + initializeFilterConfiguration(kNoConfigProto); testWithResponseSize(0, false); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigMaxBoundaryLengthRequest) { - initializeConfig(kNoConfigProto); + initializeFilterConfiguration(kNoConfigProto); const int max = 1024 * 1024 * 4; testWithResponseSize(max, false); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigTooLarge) { - initializeConfig(kNoConfigProto); + initializeFilterConfiguration(kNoConfigProto); const int max = 1024 * 1024 * 4; testBadResponseSize(max + 1); } TEST_P(HttpTestServerIntegrationTest, TestNoStaticConfigHeaderConfig) { - initializeConfig(kNoConfigProto); - updateRequestLevelConfiguration( + initializeFilterConfiguration(kNoConfigProto); + setRequestLevelConfiguration( R"({response_headers: [ { header: { key: "foo", value: "bar2"}, append: true } ]})"); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index bb2b9a20b..9fd7e5847 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -39,7 +39,7 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, // Verify expectations with static/file-based time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfiguration) { - initializeConfig(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); + initializeFilterConfiguration(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); int64_t latency; const Envoy::Http::HeaderEntry* latency_header_1 = @@ -55,14 +55,14 @@ TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfigura // Verify expectations with an empty time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfiguration) { - initializeConfig(fmt::format(kProtoConfigTemplate, "")); + initializeFilterConfiguration(fmt::format(kProtoConfigTemplate, "")); // Don't send any config request header getResponse(ResponseOrigin::UPSTREAM); // Send a config request header with an empty / default config. Should be a no-op. - updateRequestLevelConfiguration("{}"); + setRequestLevelConfiguration("{}"); getResponse(ResponseOrigin::UPSTREAM); // Send a config request header, this should become effective. - updateRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); + setRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); const Envoy::Http::HeaderEntry* latency_header = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); From 5cdb2a07c21f9f589ba50e06de3fde86ad48a597 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Wed, 16 Sep 2020 22:39:49 +0200 Subject: [PATCH 44/48] Fix a typo Signed-off-by: Otto van der Schaaf --- test/server/http_filter_integration_test_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h index ccf68a24b..8027753ec 100644 --- a/test/server/http_filter_integration_test_base.h +++ b/test/server/http_filter_integration_test_base.h @@ -8,7 +8,7 @@ namespace Nighthawk { /** * Base class with shared functionality for testing Nighthawk test server http filter extensions. - * The class is stateful, and not safed to use from multiple threads. + * The class is stateful, and not safe to use from multiple threads. */ class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { protected: From 3292e2b5fca06cbdc8ea19ccea9492238b473f55 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Fri, 18 Sep 2020 01:45:15 +0200 Subject: [PATCH 45/48] Sync review feedback Signed-off-by: Otto van der Schaaf --- ...p_dynamic_delay_filter_integration_test.cc | 30 +++++++++++++++++-- ...p_time_tracking_filter_integration_test.cc | 18 +++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/test/server/http_dynamic_delay_filter_integration_test.cc b/test/server/http_dynamic_delay_filter_integration_test.cc index 7726e98e3..3840a11ec 100644 --- a/test/server/http_dynamic_delay_filter_integration_test.cc +++ b/test/server/http_dynamic_delay_filter_integration_test.cc @@ -22,6 +22,11 @@ const Envoy::Http::LowerCaseString kDelayHeaderString("x-envoy-fault-delay-reque * - Failure modes work. * - TODO(#393): An end to end test which proves that the interaction between this filter * and the fault filter work as expected. + * + * The Dynamic Delay filter communicates with the fault filter by adding kDelayHeaderString + * to the request headers. We use that in tests below to verify expectations. The fault filter + * accepts input values via request headers specified in milliseconds, so our expectations are + * also using milliseconds. */ class HttpDynamicDelayIntegrationTest : public HttpFilterIntegrationTestBase, @@ -40,17 +45,21 @@ name: dynamic-delay typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions )"); - // Don't send any config request header + // Don't send any config request header ... getResponse(ResponseOrigin::UPSTREAM); + // ... we shouldn't observe any delay being requested via the upstream request headers. EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); - // Send a config request header with an empty / default config. Should be a no-op. + // Send a config request header with an empty / default configuration .... setRequestLevelConfiguration("{}"); getResponse(ResponseOrigin::UPSTREAM); + // ... we shouldn't observe any delay being requested via the upstream request headers. EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString), nullptr); - // Send a config request header, this should become effective. + + // Send a config request header requesting a 1.6s delay... setRequestLevelConfiguration("{static_delay: \"1.6s\"}"); getResponse(ResponseOrigin::UPSTREAM); + // ...we should observe a delay of 1.6s in the upstream request. EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1600"); } @@ -62,11 +71,20 @@ name: dynamic-delay "@type": type.googleapis.com/nighthawk.server.ResponseOptions static_delay: 1.33s )EOF"); + + // Without any request-level configuration, we expect the statically configured static delay to + // apply. getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); + + // With an empty request-level configuration, we expect the statically configured static delay to + // apply. setRequestLevelConfiguration("{}"); getResponse(ResponseOrigin::UPSTREAM); EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "1330"); + + // Overriding the statically configured static delay via request-level configuration should be + // reflected in the output. setRequestLevelConfiguration("{static_delay: \"0.2s\"}"); getResponse(ResponseOrigin::UPSTREAM); // TODO(#392): This fails, because the duration is a two-field message: it would make here to see @@ -76,8 +94,12 @@ name: dynamic-delay // Hence the following expectation will fail, as it yields 1200 instead of the expected 200. // EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), // "200"); + + // Overriding the statically configured static delay via request-level configuration should be + // reflected in the output. setRequestLevelConfiguration("{static_delay: \"2.2s\"}"); getResponse(ResponseOrigin::UPSTREAM); + // 2.2 seconds -> 2200 ms. EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "2200"); } @@ -92,6 +114,8 @@ name: dynamic-delay concurrency_delay_factor: 0.01s )EOF"); getResponse(ResponseOrigin::UPSTREAM); + // Based on the algorithm of concurrency_based_linear_delay, for the first request we expect to + // observe the configured minimal_delay + concurrency_delay_factor = 0.06s -> 60ms. EXPECT_EQ(upstream_request_->headers().get(kDelayHeaderString)->value().getStringView(), "60"); } diff --git a/test/server/http_time_tracking_filter_integration_test.cc b/test/server/http_time_tracking_filter_integration_test.cc index 9fd7e5847..5f1348c56 100644 --- a/test/server/http_time_tracking_filter_integration_test.cc +++ b/test/server/http_time_tracking_filter_integration_test.cc @@ -40,11 +40,15 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, HttpTimeTrackingIntegrationTest, // Verify expectations with static/file-based time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfiguration) { initializeFilterConfiguration(fmt::format(kProtoConfigTemplate, kDefaultProtoFragment)); + + // As the first request doesn't have a prior one, we should not observe a delta. Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); int64_t latency; const Envoy::Http::HeaderEntry* latency_header_1 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); EXPECT_EQ(latency_header_1, nullptr); + + // On the second request we should observe a delta. response = getResponse(ResponseOrigin::UPSTREAM); const Envoy::Http::HeaderEntry* latency_header_2 = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); @@ -56,14 +60,16 @@ TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForStaticConfigura // Verify expectations with an empty time-tracking configuration. TEST_P(HttpTimeTrackingIntegrationTest, ReturnsPositiveLatencyForPerRequestConfiguration) { initializeFilterConfiguration(fmt::format(kProtoConfigTemplate, "")); - // Don't send any config request header - getResponse(ResponseOrigin::UPSTREAM); - // Send a config request header with an empty / default config. Should be a no-op. + // As the first request doesn't have a prior one, we should not observe a delta. setRequestLevelConfiguration("{}"); - getResponse(ResponseOrigin::UPSTREAM); - // Send a config request header, this should become effective. - setRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::UPSTREAM); + EXPECT_EQ(response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)), + nullptr); + + // With request level configuration indicating that the timing header should be emitted, + // we should be able to observe it. + setRequestLevelConfiguration(fmt::format("{{{}}}", kDefaultProtoFragment)); + response = getResponse(ResponseOrigin::UPSTREAM); const Envoy::Http::HeaderEntry* latency_header = response->headers().get(Envoy::Http::LowerCaseString(kLatencyResponseHeaderName)); ASSERT_NE(latency_header, nullptr); From 746ad1aad4f2029295dbb3866f78f3881d2d71db Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 22 Sep 2020 23:52:30 +0200 Subject: [PATCH 46/48] Sync changes to http_filter_base_test.cc Signed-off-by: Otto van der Schaaf --- test/server/http_filter_base_test.cc | 73 +++++++++++++++------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/test/server/http_filter_base_test.cc b/test/server/http_filter_base_test.cc index 222c35186..212bd59b5 100644 --- a/test/server/http_filter_base_test.cc +++ b/test/server/http_filter_base_test.cc @@ -7,15 +7,38 @@ #include "gtest/gtest.h" namespace Nighthawk { +namespace { -using namespace testing; +using ::testing::HasSubstr; + +enum TestRequestMethod { GET, POST }; + +const std::string kBadConfigErrorSentinel = + "didn't understand the request: Error merging json config: Unable to parse " + "JSON as proto (INVALID_ARGUMENT:Unexpected"; class HttpFilterBaseIntegrationTest : public HttpFilterIntegrationTestBase, public testing::TestWithParam< - std::tuple> { + std::tuple> { public: - HttpFilterBaseIntegrationTest() : HttpFilterIntegrationTestBase(std::get<0>(GetParam())){}; + HttpFilterBaseIntegrationTest() + : HttpFilterIntegrationTestBase(std::get<0>(GetParam())), config_(std::get<1>(GetParam())) { + initializeFilterConfiguration(config_); + if (std::get<2>(GetParam()) == TestRequestMethod::POST) { + switchToPostWithEntityBody(); + } + }; + + ResponseOrigin getHappyFlowResponseOrigin() { + // Modulo the test-server, extensions are expected to need an upstream to synthesize a reply + // when the effective configuration is valid. + return config_.find_first_of("name: test-server") == 0 ? ResponseOrigin::EXTENSION + : ResponseOrigin::UPSTREAM; + } + +protected: + const std::string config_; }; INSTANTIATE_TEST_SUITE_P( @@ -34,56 +57,38 @@ name: dynamic-delay static_delay: 0.1s )EOF"), absl::string_view("name: test-server")}), - testing::ValuesIn({true, false}))); + testing::ValuesIn({TestRequestMethod::GET, TestRequestMethod::POST}))); -// Verify extensions are well-behaved when it comes to: -// - GET requests. -// - POST requests with an entity body (takes a slightly different code path). -// - Valid but empty configuration. -// - Bad request-level json configuration. -// We will be low on functional expectations for the extensions, but this will catch hangs and -// ensure that bad configuration handling is in-place. -TEST_P(HttpFilterBaseIntegrationTest, BasicExtensionFlows) { - absl::string_view config = std::get<1>(GetParam()); - initializeFilterConfiguration(std::string(config)); - bool is_post = std::get<2>(GetParam()); - if (is_post) { - switchToPostWithEntityBody(); - } - - // Modulo the test-server, extensions are expected to need an upstream to synthesize a reply - // when effective configuration is valid. - ResponseOrigin happy_flow_response_origin = config.find_first_of("name: test-server") == 0 - ? ResponseOrigin::EXTENSION - : ResponseOrigin::UPSTREAM; - // Post without any request-level configuration. Should succeed. - Envoy::IntegrationStreamDecoderPtr response = getResponse(happy_flow_response_origin); +TEST_P(HttpFilterBaseIntegrationTest, NoRequestLevelConfigurationShouldSucceed) { + Envoy::IntegrationStreamDecoderPtr response = getResponse(getHappyFlowResponseOrigin()); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_TRUE(response->body().empty()); +} - // Test with a valid but empty request-level configuration. +TEST_P(HttpFilterBaseIntegrationTest, EmptyJsonRequestLevelConfigurationShouldSucceed) { setRequestLevelConfiguration("{}"); - response = getResponse(happy_flow_response_origin); + Envoy::IntegrationStreamDecoderPtr response = getResponse(getHappyFlowResponseOrigin()); ASSERT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().Status()->value().getStringView()); EXPECT_TRUE(response->body().empty()); +} - const std::string kBadConfigErrorSentinel = - "didn't understand the request: Error merging json config: Unable to parse " - "JSON as proto (INVALID_ARGUMENT:Unexpected"; - +TEST_P(HttpFilterBaseIntegrationTest, BadJsonAsRequestLevelConfigurationShouldFail) { // When sending bad request-level configuration, the extension ought to reply directly. setRequestLevelConfiguration("bad_json"); - response = getResponse(ResponseOrigin::EXTENSION); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_THAT(response->body(), HasSubstr(kBadConfigErrorSentinel)); +} +TEST_P(HttpFilterBaseIntegrationTest, EmptyRequestLevelConfigurationShouldFail) { // When sending empty request-level configuration, the extension ought to reply directly. setRequestLevelConfiguration(""); - response = getResponse(ResponseOrigin::EXTENSION); + Envoy::IntegrationStreamDecoderPtr response = getResponse(ResponseOrigin::EXTENSION); EXPECT_EQ(Envoy::Http::Utility::getResponseStatus(response->headers()), 500); EXPECT_THAT(response->body(), HasSubstr(kBadConfigErrorSentinel)); } +} // namespace } // namespace Nighthawk \ No newline at end of file From 2529ea6826f348215ab22f8cbf77064ef37fa329 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 6 Oct 2020 08:57:43 +0200 Subject: [PATCH 47/48] Review feedback Signed-off-by: Otto van der Schaaf --- source/server/http_test_server_filter.cc | 3 ++- source/server/http_time_tracking_filter.cc | 3 ++- test/server/http_test_server_filter_integration_test.cc | 6 ++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/source/server/http_test_server_filter.cc b/source/server/http_test_server_filter.cc index 744024741..5dd51851a 100644 --- a/source/server/http_test_server_filter.cc +++ b/source/server/http_test_server_filter.cc @@ -41,7 +41,8 @@ HttpTestServerDecoderFilter::decodeHeaders(Envoy::Http::RequestHeaderMap& header config_->computeEffectiveConfiguration(headers); if (end_stream) { if (!config_->maybeSendErrorReply(*decoder_callbacks_)) { - const auto effective_config = config_->getEffectiveConfiguration(); + const absl::StatusOr effective_config = + config_->getEffectiveConfiguration(); if (effective_config.value()->echo_request_headers()) { std::stringstream headers_dump; headers_dump << "\nRequest Headers:\n" << headers; diff --git a/source/server/http_time_tracking_filter.cc b/source/server/http_time_tracking_filter.cc index 045192d0e..aba57d46f 100644 --- a/source/server/http_time_tracking_filter.cc +++ b/source/server/http_time_tracking_filter.cc @@ -47,7 +47,8 @@ Envoy::Http::FilterDataStatus HttpTimeTrackingFilter::decodeData(Envoy::Buffer:: Envoy::Http::FilterHeadersStatus HttpTimeTrackingFilter::encodeHeaders(Envoy::Http::ResponseHeaderMap& response_headers, bool) { - const auto effective_config = config_->getEffectiveConfiguration(); + const absl::StatusOr effective_config = + config_->getEffectiveConfiguration(); if (effective_config.ok()) { const std::string previous_request_delta_in_response_header = effective_config.value()->emit_previous_request_delta_in_response_header(); diff --git a/test/server/http_test_server_filter_integration_test.cc b/test/server/http_test_server_filter_integration_test.cc index e35f5076d..8b355101f 100644 --- a/test/server/http_test_server_filter_integration_test.cc +++ b/test/server/http_test_server_filter_integration_test.cc @@ -9,10 +9,11 @@ #include "gtest/gtest.h" namespace Nighthawk { +namespace { using namespace testing; -const std::string kDefaultProto = R"EOF( +constexpr absl::string_view kDefaultProto = R"EOF( name: test-server typed_config: "@type": type.googleapis.com/nighthawk.server.ResponseOptions @@ -21,7 +22,7 @@ name: test-server - { header: { key: "x-supplied-by", value: "nighthawk-test-server"} } )EOF"; -const std::string kNoConfigProto = R"EOF( +constexpr absl::string_view kNoConfigProto = R"EOF( name: test-server )EOF"; @@ -248,4 +249,5 @@ TEST(HttpTestServerDecoderFilterTest, HeaderMerge) { EXPECT_EQ(3, options.response_headers_size()); } +} // namespace } // namespace Nighthawk From 2bcbb2b37fef5f9fc50de17face5835edb5c7087 Mon Sep 17 00:00:00 2001 From: Otto van der Schaaf Date: Tue, 6 Oct 2020 09:06:27 +0200 Subject: [PATCH 48/48] bump coverage thresholds Signed-off-by: Otto van der Schaaf --- ci/do_ci.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/do_ci.sh b/ci/do_ci.sh index 8d430b706..4abbcfe8e 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -39,7 +39,7 @@ function do_clang_tidy() { function do_unit_test_coverage() { export TEST_TARGETS="//test/... -//test:python_test" - export COVERAGE_THRESHOLD=93.2 + export COVERAGE_THRESHOLD=94.0 echo "bazel coverage build with tests ${TEST_TARGETS}" test/run_nighthawk_bazel_coverage.sh ${TEST_TARGETS} exit 0 @@ -47,7 +47,7 @@ function do_unit_test_coverage() { function do_integration_test_coverage() { export TEST_TARGETS="//test:python_test" - export COVERAGE_THRESHOLD=78.0 + export COVERAGE_THRESHOLD=78.6 echo "bazel coverage build with tests ${TEST_TARGETS}" test/run_nighthawk_bazel_coverage.sh ${TEST_TARGETS} exit 0