Skip to content

HTTP1: Add DumpState support for HTTP1.#14812

Merged
snowp merged 11 commits intoenvoyproxy:mainfrom
KBaichoo:bufferDump
Feb 2, 2021
Merged

HTTP1: Add DumpState support for HTTP1.#14812
snowp merged 11 commits intoenvoyproxy:mainfrom
KBaichoo:bufferDump

Conversation

@KBaichoo
Copy link
Copy Markdown
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
Copy Markdown
Contributor Author

/retest

@repokitteh-read-only
Copy link
Copy Markdown

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

🐱

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

see: more, trace.

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

// Dump the first slice of the dispatching buffer and buffered_body if
// applicable.
auto dumpBuffer = [](auto* instance) {
auto slice = instance->frontSlice();
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

// applicable.
auto dumpBuffer = [](auto* instance) {
auto slice = instance->frontSlice();
return absl::string_view(static_cast<const char*>(slice.mem_), slice.len_);
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

};

os << DUMP_NULLABLE_MEMBER(current_dispatching_buffer_, dumpBuffer(current_dispatching_buffer_));
os << DUMP_MEMBER(buffered_body_, dumpBuffer(&buffered_body_));
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor

grpc errors during build?

/retest

@repokitteh-read-only
Copy link
Copy Markdown

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
Copy Markdown
Contributor Author

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

/retest

@repokitteh-read-only
Copy link
Copy Markdown

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

🐱

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

see: more, trace.

@KBaichoo
Copy link
Copy Markdown
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
Copy Markdown
Contributor Author

/retest

@repokitteh-read-only
Copy link
Copy Markdown

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

🐱

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

see: more, trace.

@KBaichoo
Copy link
Copy Markdown
Contributor Author

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

@jmarantz
Copy link
Copy Markdown
Contributor

jmarantz commented Feb 1, 2021

needs main merge.

@KBaichoo
Copy link
Copy Markdown
Contributor Author

KBaichoo commented Feb 1, 2021

/retest

@repokitteh-read-only
Copy link
Copy Markdown

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

🐱

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

see: more, trace.

Copy link
Copy Markdown
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.

return ret;
}

void StringUtil::escapeToOstream(std::ostream& os, absl::string_view view) {
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Member

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
Copy Markdown
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.


void ConnectionImpl::dumpState(std::ostream& os, int indent_level) const {
const char* spaces = spacesForLevel(indent_level);
// Dump all bool to provide context
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

.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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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


void ConnectionImpl::dumpState(std::ostream& os, int indent_level) const {
const char* spaces = spacesForLevel(indent_level);
// Dump all bool to provide context
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

return ret;
}

void StringUtil::escapeToOstream(std::ostream& os, absl::string_view view) {
Copy link
Copy Markdown
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
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

.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
Copy Markdown
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
active_request_.has_value() &&
!active_request_.value().request_url_.getStringView().empty()
? active_request_.value().request_url_.getStringView()
: "null\n");
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Contributor Author

KBaichoo commented Feb 2, 2021

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

@KBaichoo
Copy link
Copy Markdown
Contributor Author

KBaichoo commented Feb 2, 2021

/retest

Flaky integration test.

@repokitteh-read-only
Copy link
Copy Markdown

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
Copy Markdown
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