Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP1: Add DumpState support for HTTP1. #14812

Merged
merged 11 commits into from
Feb 2, 2021
Merged

Conversation

KBaichoo
Copy link
Contributor

Signed-off-by: Kevin Baichoo [email protected]

For an explanation of how to fill out the fields, please see the relevant section
in PULL_REQUESTS.md

Commit Message: HTTP1: Add DumpState support for HTTP1.
Additional Description: While dispatching, the HTTP1 Connection object registers itself as a scopetrackedobject. If we crash during parsing pipeline, we'll dump information such as the partial headers parsed, current buffer, headermap, etc.
Risk Level: medium
Testing: unit tests
Docs Changes: N/A
Release Notes: Included
Platform Specific Features: N/A
Related Issue #14249

/assign @antoniovicente

@KBaichoo
Copy link
Contributor Author

/retest

@repokitteh-read-only
Copy link

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #14812 (comment) was created by @KBaichoo.

see: more, trace.

Copy link
Contributor

@antoniovicente antoniovicente left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for further improving debug dumps when crashing in H1 parsing.

};

os << DUMP_NULLABLE_MEMBER(current_dispatching_buffer_, dumpBuffer(current_dispatching_buffer_));
os << DUMP_MEMBER(buffered_body_, dumpBuffer(&buffered_body_));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shoud consider dumping just buffered_body_.length() or omit this field, the body contents is not that relevant. The current_dispatching_buffer_ seems more relevant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will dump the length.

source/common/http/http1/codec_impl.cc Outdated Show resolved Hide resolved
// Dump the first slice of the dispatching buffer and buffered_body if
// applicable.
auto dumpBuffer = [](auto* instance) {
auto slice = instance->frontSlice();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should you quote and escape the buffer contents? Should you explicitly dump the size of the slice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explicitly dump the size of the slice now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer contents are escaped in \t, \r, \n, ', " and \

return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
};

os << DUMP_NULLABLE_MEMBER(current_dispatching_buffer_, dumpBuffer(current_dispatching_buffer_));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parsing is a destructive operation on current_dispatching_buffer_ in some cases involving processing bodies. I guess this may dump the wrong slice in those cases. That said, crashing while processing body is unlikely; crashes during header parse should produce the right output.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will not dump the first slice then if the dispatching buffer is already drained.

source/common/http/http1/codec_impl.cc Show resolved Hide resolved
source/common/http/http1/codec_impl.cc Show resolved Hide resolved
// applicable.
auto dumpBuffer = [](auto* instance) {
auto slice = instance->frontSlice();
return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, I see tests that cover dumping of current_dispatching_buffer_ in
Http1ClientConnectionImplTest.ShouldDumpBuffersWithoutAllocatingMemory.

os << DUMP_MEMBER(current_header_value_, current_header_value_.getStringView());
}

// Dump Child
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the dump for current_dispatching_buffer_ should be done via DUMP_DETAILS. Consider putting it after the dump of header maps in subclasses. Reasoning: buffer contents can be long and may contain newlines

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense; It's now after the dump of header maps. By "via DUMP_DETAILS" I'm interpreting that as dumping the buffer in a similar style as the macro would do.

We could explicitly add a dumpState method to the buffer if that's what you meant, but given we're just dumping the first slice.

Signed-off-by: Kevin Baichoo <[email protected]>
Copy link
Contributor Author

@KBaichoo KBaichoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review Antonio.

source/common/http/http1/codec_impl.cc Show resolved Hide resolved
source/common/http/http1/codec_impl.cc Show resolved Hide resolved
};

os << DUMP_NULLABLE_MEMBER(current_dispatching_buffer_, dumpBuffer(current_dispatching_buffer_));
os << DUMP_MEMBER(buffered_body_, dumpBuffer(&buffered_body_));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will dump the length.

os << DUMP_MEMBER(current_header_value_, current_header_value_.getStringView());
}

// Dump Child
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense; It's now after the dump of header maps. By "via DUMP_DETAILS" I'm interpreting that as dumping the buffer in a similar style as the macro would do.

We could explicitly add a dumpState method to the buffer if that's what you meant, but given we're just dumping the first slice.

// Dump the first slice of the dispatching buffer and buffered_body if
// applicable.
auto dumpBuffer = [](auto* instance) {
auto slice = instance->frontSlice();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explicitly dump the size of the slice now.

return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
};

os << DUMP_NULLABLE_MEMBER(current_dispatching_buffer_, dumpBuffer(current_dispatching_buffer_));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will not dump the first slice then if the dispatching buffer is already drained.

Copy link
Contributor

@antoniovicente antoniovicente left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few nits, thanks again for improving crash dump coverage.

source/common/http/http1/codec_impl.cc Outdated Show resolved Hide resolved
return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
}(current_dispatching_buffer_);

os << spaces << DUMP_MEMBER(front_slice.length(), front_slice.length()) << ", front_slice: \n";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider doing the following here:

os << spaces << "current_dispatching_buffer_ front_slice length: " << front_slice.length()
   << " contents: \"" <<  StringUtil::escapeToOstream(os, front_slice) << "\"\n";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Buffer::OwnedImpl request("POST / HTTP/1.1\r\n"
"Content-Length: 11\r\n"
"\r\n"
"Hello Envoy");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to split the request over 2 slices and verify that only the first slice is logged?

Something like:

Buffer::OwnedImpl request;
request.appendSliceForTest("POST / HTTP/1.1\r\n"
                            "Content-Length: 11\r\n"
                            "\r\n"
                            "Hello");
request.appendSliceForTest(" Envoy");

  EXPECT_THAT(ostream.contents(),
             HasSubstr("front_slice.length(): 44 ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did something similar to this; We were using onMessageCompleteBase's callback from the parser to dump the data of the buffer.

I added extraneous bytes in the other slice to check that only the front slice is dumped. The content length is used to tell the parser the message should only have the front slice.

source/common/http/http1/codec_impl.cc Outdated Show resolved Hide resolved
Copy link
Contributor Author

@KBaichoo KBaichoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review Antonio.

return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
}(current_dispatching_buffer_);

os << spaces << DUMP_MEMBER(front_slice.length(), front_slice.length()) << ", front_slice: \n";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Buffer::OwnedImpl request("POST / HTTP/1.1\r\n"
"Content-Length: 11\r\n"
"\r\n"
"Hello Envoy");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did something similar to this; We were using onMessageCompleteBase's callback from the parser to dump the data of the buffer.

I added extraneous bytes in the other slice to check that only the front slice is dumped. The content length is used to tell the parser the message should only have the front slice.

@antoniovicente
Copy link
Contributor

grpc errors during build?

/retest

@repokitteh-read-only
Copy link

Retrying Azure Pipelines:
Check envoy-presubmit isn't fully completed, but will still attempt retrying.
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #14812 (comment) was created by @antoniovicente.

see: more, trace.

@KBaichoo
Copy link
Contributor Author

Not sure if that restarted it; did run clang-tidy in docker locally no issue...

/retest

@repokitteh-read-only
Copy link

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #14812 (comment) was created by @KBaichoo.

see: more, trace.

@KBaichoo
Copy link
Contributor Author

grpc issue seems to be due to some loadshedding that some service is performing -- maybe too much going on for CI.

Will re-kick this in a bit, perhaps we should get some other reviewing eyes on it in the mean time?

@KBaichoo
Copy link
Contributor Author

/retest

@repokitteh-read-only
Copy link

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #14812 (comment) was created by @KBaichoo.

see: more, trace.

@KBaichoo
Copy link
Contributor Author

PTAL @envoyproxy/senior-maintainers; seems like it was LGTM from @antoniovicente, but he's OOO.

@jmarantz
Copy link
Contributor

jmarantz commented Feb 1, 2021

needs main merge.

@KBaichoo
Copy link
Contributor Author

KBaichoo commented Feb 1, 2021

/retest

@repokitteh-read-only
Copy link

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #14812 (comment) was created by @KBaichoo.

see: more, trace.

Copy link
Contributor

@antoniovicente antoniovicente left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some style nits, looks good otherwise.

docs/root/version_history/current.rst Outdated Show resolved Hide resolved
@@ -457,6 +457,34 @@ std::string StringUtil::escape(const std::string& source) {
return ret;
}

void StringUtil::escapeToOstream(std::ostream& os, absl::string_view view) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these are the most important to escape, but I can see us also escaping c < ' ' and c >= 127. We can take that on in a followup if we decide it is useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in a followup if needed. Added a todo here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also escaping code in source/common/common/logger.cc in DelegatingLogSink::escapeLogLine() which calls absl::CEscape. Can you use that instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, absl::CEscape allocs memory via the string it'll create and return. This avoids memory allocs.

@@ -863,6 +866,76 @@ void ConnectionImpl::onResetStreamBase(StreamResetReason reason) {
onResetStream(reason);
}

void ConnectionImpl::dumpState(std::ostream& os, int indent_level) const {
const char* spaces = spacesForLevel(indent_level);
// Dump all bool to provide context
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not 100% accurate; not all the dumped members are of bool type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

os << '\n';
dumpAdditionalState(os, indent_level);

// Dump the first slice of the dispatching buffer if not drained.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain why we are doing this last.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} else if (header_parsing_state_ == HeaderParsingState::Value) {
os << DUMP_MEMBER(current_header_field_, current_header_field_.getStringView());
os << DUMP_MEMBER(current_header_value_, current_header_value_.getStringView());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could unconditionally dump both current_header_field_ and current_header_value_

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

auto front_slice = [](Buffer::Instance* instance) {
auto slice = instance->frontSlice();
return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
}(current_dispatching_buffer_);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous 4 lines are hard to follow due to the inline lambda use and excessive use of 'auto'.

Consider instead:

absl::string_view front_slice = [](RawSlice slice) {
  return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
}(current_dispatching_buffer_->frontSlice());

or

auto to_str_view = [](RawSlice slice) {
  return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
};
absl::string_view front_slice = to_str_view(current_dispatching_buffer_->frontSlice());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

went with the former.


// If DUMP_MEMBER is called with one argument, then _DUMP_MEMBER is called.
// If DUMP_MEMBER is called with two arguments, then _DUMP_MEMBER_VIA_VALUE is called.
#define DUMP_MEMBER(...) \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code would be easier to follow if you use a different macro name in cases we want to override how the member is printed. Something like:

#define DUMP_MEMBER_VIA_VALUE(member, value) ", " #member ": " << (value)

Other possible macro names include DUMP_MEMBER_VIA and DUMP_MEMBER_AS

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg. will go with DUMP_MEMBER_AS.

@@ -2635,7 +2713,8 @@ TEST_F(Http1ClientConnectionImplTest, LowWatermarkDuringClose) {

EXPECT_CALL(response_decoder, decodeHeaders_(_, true))
.WillOnce(Invoke([&](ResponseHeaderMapPtr&, bool) {
// Fake a call for going below the low watermark. Make sure no stream callbacks get called.
// Fake a call for going below the low watermark. Make sure no stream callbacks get
// called.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear why the previous 3 whitespace changes were done. Was it the fix format script or a consequence of some indent change that was later backed out?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter, I've reverted it.

Copy link
Contributor Author

@KBaichoo KBaichoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @antoniovicente

@@ -863,6 +866,76 @@ void ConnectionImpl::onResetStreamBase(StreamResetReason reason) {
onResetStream(reason);
}

void ConnectionImpl::dumpState(std::ostream& os, int indent_level) const {
const char* spaces = spacesForLevel(indent_level);
// Dump all bool to provide context
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

os << '\n';
dumpAdditionalState(os, indent_level);

// Dump the first slice of the dispatching buffer if not drained.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

auto front_slice = [](Buffer::Instance* instance) {
auto slice = instance->frontSlice();
return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
}(current_dispatching_buffer_);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

went with the former.


// If DUMP_MEMBER is called with one argument, then _DUMP_MEMBER is called.
// If DUMP_MEMBER is called with two arguments, then _DUMP_MEMBER_VIA_VALUE is called.
#define DUMP_MEMBER(...) \
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sg. will go with DUMP_MEMBER_AS.

@@ -457,6 +457,34 @@ std::string StringUtil::escape(const std::string& source) {
return ret;
}

void StringUtil::escapeToOstream(std::ostream& os, absl::string_view view) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do in a followup if needed. Added a todo here.

} else if (header_parsing_state_ == HeaderParsingState::Value) {
os << DUMP_MEMBER(current_header_field_, current_header_field_.getStringView());
os << DUMP_MEMBER(current_header_value_, current_header_value_.getStringView());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -2635,7 +2713,8 @@ TEST_F(Http1ClientConnectionImplTest, LowWatermarkDuringClose) {

EXPECT_CALL(response_decoder, decodeHeaders_(_, true))
.WillOnce(Invoke([&](ResponseHeaderMapPtr&, bool) {
// Fake a call for going below the low watermark. Make sure no stream callbacks get called.
// Fake a call for going below the low watermark. Make sure no stream callbacks get
// called.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter, I've reverted it.

antoniovicente
antoniovicente previously approved these changes Feb 1, 2021
source/common/common/utility.cc Outdated Show resolved Hide resolved
active_request_.has_value() &&
!active_request_.value().request_url_.getStringView().empty()
? active_request_.value().request_url_.getStringView()
: "null\n");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's a trailing newline after "null", but no similar newline is added when the URL is not empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, they both have the newline now.

Signed-off-by: Kevin Baichoo <[email protected]>
@KBaichoo
Copy link
Contributor Author

KBaichoo commented Feb 2, 2021

PTAL @ggreenway since you're the maintainer on-call this week. Thanks!

@KBaichoo
Copy link
Contributor Author

KBaichoo commented Feb 2, 2021

/retest

Flaky integration test.

@repokitteh-read-only
Copy link

Retrying Azure Pipelines:
Retried failed jobs in: envoy-presubmit

🐱

Caused by: a #14812 (comment) was created by @KBaichoo.

see: more, trace.

@ggreenway ggreenway assigned alyssawilk and snowp and unassigned alyssawilk Feb 2, 2021
Copy link
Contributor

@snowp snowp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, this looks super useful!

@snowp snowp merged commit 1ce7fab into envoyproxy:main Feb 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants