-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Conversation
Signed-off-by: Kevin Baichoo <[email protected]>
/retest |
Retrying Azure Pipelines: |
There was a problem hiding this 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_)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will dump the length.
// Dump the first slice of the dispatching buffer and buffered_body if | ||
// applicable. | ||
auto dumpBuffer = [](auto* instance) { | ||
auto slice = instance->frontSlice(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
// applicable. | ||
auto dumpBuffer = [](auto* instance) { | ||
auto slice = instance->frontSlice(); | ||
return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]>
There was a problem hiding this 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.
}; | ||
|
||
os << DUMP_NULLABLE_MEMBER(current_dispatching_buffer_, dumpBuffer(current_dispatching_buffer_)); | ||
os << DUMP_MEMBER(buffered_body_, dumpBuffer(&buffered_body_)); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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_)); |
There was a problem hiding this comment.
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.
Signed-off-by: Kevin Baichoo <[email protected]>
There was a problem hiding this 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.
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"; |
There was a problem hiding this comment.
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";
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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.
…dumped. Signed-off-by: Kevin Baichoo <[email protected]>
There was a problem hiding this 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"; |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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.
grpc errors during build? /retest |
Retrying Azure Pipelines: |
Not sure if that restarted it; did run clang-tidy in docker locally no issue... /retest |
Retrying Azure Pipelines: |
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? |
/retest |
Retrying Azure Pipelines: |
PTAL @envoyproxy/senior-maintainers; seems like it was LGTM from @antoniovicente, but he's OOO. |
needs main merge. |
Signed-off-by: Kevin Baichoo <[email protected]>
Signed-off-by: Kevin Baichoo <[email protected]>
/retest |
Retrying Azure Pipelines: |
Signed-off-by: Kevin Baichoo <[email protected]>
Signed-off-by: Kevin Baichoo <[email protected]>
There was a problem hiding this 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.
@@ -457,6 +457,34 @@ std::string StringUtil::escape(const std::string& source) { | |||
return ret; | |||
} | |||
|
|||
void StringUtil::escapeToOstream(std::ostream& os, absl::string_view view) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); | ||
} |
There was a problem hiding this comment.
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_
There was a problem hiding this comment.
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_); |
There was a problem hiding this comment.
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());
There was a problem hiding this comment.
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(...) \ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Signed-off-by: Kevin Baichoo <[email protected]>
There was a problem hiding this 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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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_); |
There was a problem hiding this comment.
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(...) \ |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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()); | ||
} |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
Signed-off-by: Kevin Baichoo <[email protected]>
active_request_.has_value() && | ||
!active_request_.value().request_url_.getStringView().empty() | ||
? active_request_.value().request_url_.getStringView() | ||
: "null\n"); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]>
PTAL @ggreenway since you're the maintainer on-call this week. Thanks! |
/retest Flaky integration test. |
Retrying Azure Pipelines: |
There was a problem hiding this 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!
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