-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
http_filter: add addEncodedTrailers and addDecodedTrailers #3980
Conversation
Adds a function that allows inserting trailers into the stream response outside of the encodeTrailers callback. This allows inserting trailers when the upstream response has no trailers to begin with (e.g. h/1.1). Signed-off-by: Snow Pettersen <snowp@squareup.com>
include/envoy/http/filter.h
Outdated
* trailers need to be injected into a response and the upstream response did not contain any | ||
* trailers (in which case encodeTrailers is not called). | ||
*/ | ||
virtual HeaderMap& addEncodedTrailers() PURE; |
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.
unsure what the best signature was here - it seems like HeaderMap
has a relatively rich API to allow calls to specify reference/copy/etc, so I wasn't sure how best to allow that without providing access to the HeaderMap
itself. On the other hand, giving out an actual reference to the map might not be ideal as it opens up for more potential bugs (filter implementations keeping it around for too long). thoughts?
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.
How does this work if the target stream protocol is not able to support trailers?
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.
From what I understand the http/1.1 codec will just ignore the trailers: https://github.com/envoyproxy/envoy/blob/master/source/common/http/http1/codec_impl.cc#L148
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.
Perhaps we should just pass a HeaderMapPtr via move? This would just overwrite existing trailers? It would be uyp to the user to figure out if trailers already exist? Thoughts?
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.
Perhaps we should just pass a HeaderMapPtr via move? This would just overwrite existing trailers? It would be uyp to the user to figure out if trailers already exist? Thoughts?
In that case you'll leaking HeaderMapImpl into filter codes and let filter create the HeaderMapPtr. Can we somehow (e.g. ASSERT) make sure this can be only called after or in encodeData(data, end_stream=true)
? So the filter doesn't have to care about potentially overwriting existing (future) trailers.
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 will want to make sure for any protocol which doesn't support trailers we don't then lose the end_stream=true passing through the pipeline, but that's what tests are for :-)
@@ -1058,6 +1058,13 @@ void ConnectionManagerImpl::ActiveStream::encodeHeaders(ActiveStreamEncoderFilte | |||
} | |||
} | |||
|
|||
HeaderMap& ConnectionManagerImpl::ActiveStream::addEncodedTrailers() { | |||
if (!response_trailers_) { |
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 whole interaction ends up becoming somewhat complicated so i wanted to put up the PR before spending too much time on the edge cases:
the make_unique
allows calling this before encodeTrailers
is called, but if there are any upstream trailers then they'll be moved into this variable, overwriting what we wrote here. This means that right now this should only be called in encodeData
if there are no upstream trailers. If called in encodeTrailers
it should correctly modify the right map.
This similarly applies to encodeHeaders
: if there are upstream trailers they will overwrite anything that was added in encodeHeaders
so my question: should we try to make this behavior more sane (e.g. try to merge added trailers with upstream trailers?) or simply document addEncodedTrailers
to spell this 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.
I commented above. IMO this is a real filter edge case, and I think I would just go around all of this and pass the new trailers by move semantics, which then overwrite. Thoughts?
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 makes sense and works with our use case. Seems cleaner than to have to reason about how it may interact with existing trailers.
Known limitation: if the upstream response is headers only then trailers won't be written even if Can attempt to fix this if necessary |
Signed-off-by: Snow Pettersen <snowp@squareup.com>
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 initial comments so we can flesh out the API, then I can do into the implementation/tests. Thank you!
include/envoy/http/filter.h
Outdated
@@ -395,6 +395,13 @@ class StreamEncoderFilterCallbacks : public virtual StreamFilterCallbacks { | |||
*/ | |||
virtual void addEncodedData(Buffer::Instance& data, bool streaming_filter) PURE; | |||
|
|||
/** | |||
* Provides a trailer map that can be used to modify the trailers for a response. Used when |
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.
As long as we are in here and have all of this logic paged in, can we add a similar function on the decoding side? It should be symmetrical and pretty trivial to duplicate tests for.
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.
yeah, will do
include/envoy/http/filter.h
Outdated
* trailers need to be injected into a response and the upstream response did not contain any | ||
* trailers (in which case encodeTrailers is not called). | ||
*/ | ||
virtual HeaderMap& addEncodedTrailers() PURE; |
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.
Perhaps we should just pass a HeaderMapPtr via move? This would just overwrite existing trailers? It would be uyp to the user to figure out if trailers already exist? Thoughts?
@@ -1058,6 +1058,13 @@ void ConnectionManagerImpl::ActiveStream::encodeHeaders(ActiveStreamEncoderFilte | |||
} | |||
} | |||
|
|||
HeaderMap& ConnectionManagerImpl::ActiveStream::addEncodedTrailers() { | |||
if (!response_trailers_) { |
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 commented above. IMO this is a real filter edge case, and I think I would just go around all of this and pass the new trailers by move semantics, which then overwrite. Thoughts?
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Updated this to use move semantics and include a |
@@ -1099,18 +1109,25 @@ void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* | |||
end_stream); | |||
|
|||
request_info_.addBytesSent(data.length()); | |||
response_encoder_->encodeData(data, end_stream); | |||
maybeEndEncode(end_stream); | |||
if (end_stream && response_trailers_) { |
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'm a little confused about the semantics here. When is a filter expected to add trailers for the first time? Do we want to support setting trailers during the final data frame where end_stream was otherwise true? I don't think we want to support that and we probably do? Also where is the parallel to this code on the decoding side?
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.
What happens right now (since you can override trailers whenever)
- Trailers are set during header/data when upstream has no trailers: trailers get written out at the end of
[en|de]codeData(_, true)
- Trailers are set during onHeader/onData when upstream HAS trailers: the original trailers override the trailers set previously (because in that case the upstream trailers are moved into
response_trailers_
AFTER we've gone through the header/data callbacks). Trailers are written out at the end of[en|de]codeTrailers(_)
- Trailers are set during onTrailers: the trailers override those provided by upstream. Trailers are written out at the end of
[en|de]codeTrailers(_)
3 is definitely confusing, so I'd be happy to prevent that. The question to me that remains is whether we want trailers to be written out at the end of [en|de]codeData(_, true)
or if we want setting the trailers to cause [en|de]codeTrailers
to be executed. I think based on your question about the decoding side that you were thinking more along the lines of the latter, because iirc that's what would allow the router to include it in the upstream request?
What steered me away from that originally was that was that it doesn't seem like addEncodedData
will cause the onEncodedData
callbacks to run if called in onHeaders(_, true)
.
So after thinking about it some, how about this:
At the end of [en|de]codeData(_, true)
we trigger [en|de]codeTrailers
if the trailers ptr is non-null. This should cover both encoding and decoding and we'd end up writing out the trailers to the encoder like I'm already doing. If addTrailers
is called during the [en|de]codeTrailers(_)
cb we can fail with an ASSERT.
This does mean we get both a [en|de]codeData(_, true)
and [en|de]codeTrailers
which might be confusing, but it would allow a filter to inject trailers when it can see that there aren't already any there (because it sees [en|de]codeData(_, true)
).
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.
So after thinking about it some, how about this:
At the end of [en|de]codeData(, true) we trigger [en|de]codeTrailers if the trailers ptr is non-null. This should cover both encoding and decoding and we'd end up writing out the trailers to the encoder like I'm already doing. If addTrailers is called during the [en|de]codeTrailers() cb we can fail with an ASSERT.
I think this is more on the right track.
This does mean we get both a [en|de]codeData(, true) and [en|de]codeTrailers which might be confusing, but it would allow a filter to inject trailers when it can see that there aren't already any there (because it sees [en|de]codeData(, true)).
I don't think this is going to work as it will completely confuse filters that do things when end_stream is true. I think this is the behavior you want:
- Do what you said previously about detecting if there are trailers set during a decodeData call.
- If trailers get set by a decode data call, make sure that subsequent filter decodeData() calls have end_stream set to false (basically key that off of whether trailers is non-null).
- Then dispatch trailers.
I don't think this should be too hard to implement and should cleanly cover both encode/decode. Does that make sense?
For the error checking side of things, IMO I would probably just block trailers being added in any context other than decodeData(..., true). Does it make sense anywhere else? The otehr thing to consider is what if someone calls this when a filter has been paused and then calls continue? Will it work correctly?
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.
Yeah that makes sense for the most part. One question:
I would probably just block trailers being added in any context other than decodeData(..., true).
Do you mean decodeData(..., false)
? Allowing setting the trailers in decodeData(..., true)
seems to contradict point 2
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.
No I meant what I said, that I think the only valid context to add trailers should be within a *Data(..., true)
call or outside of a direct call in a different callback before calling continue*()
. What I mean by (2) is that an intermediate filter can add trailers in the context of data call with end_stream true, but subsequent filters should then see a data call with end_stream false, followed by a trailers call. I believe this captures the intent of what your filter needs to do and IMO is pretty clear. Does that make more sense?
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.
Ah, that makes perfect sense. Thanks for clarifying.
Yes I think we should force this, but see my other comment first. I still think we need to figure out semantics. |
include/envoy/http/filter.h
Outdated
/** | ||
* Adds decoded trailers. This will overwrite any existing trailers should any already exist. | ||
*/ | ||
virtual void addDecodedTrailers(HeaderMapPtr&& trailers) PURE; |
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.
still -1 to moving a pointer here, and prefer the first revision: HeaderMap& addDecodedTrailers()
for reasons:
- moving a pointer doesn't prevent filters from keeping reference to
HeaderMap
itself too long, so that still can happen. Returning a reference is already clear that Envoy core (ActiveStream) owns theHeaderMap
. - the filter will now have to know implementations of HeaderMap (i.e. HeaderMapImpl) and instantiate, I know there is already a couple places doing this, mostly to deal with
AsyncHttpClient
, perhaps we shouldn't add another place requiring that?
@mattklein123 thoughts?
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.
moving a pointer doesn't prevent filters from keeping reference to HeaderMap itself too long, so that still can happen. Returning a reference is already clear that Envoy core (ActiveStream) owns the HeaderMap.
IMO move semantics make it clear that the pointer is now owned by the callee. We do this in a variety of places.
the filter will now have to know implementations of HeaderMap (i.e. HeaderMapImpl) and instantiate, I know there is already a couple places doing this, mostly to deal with AsyncHttpClient, perhaps we shouldn't add another place requiring that?
I think this is a better argument. Assuming with the new semantics we have discussed it's just as easy to return a reference to either the existing trailers or a newly created trailers, it sounds reasonable to me to switch it back to what we had before.
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 one thing that's unclear to me is what would HeaderPtr& addEncodedTrailers()
return if called before encodeTrailers(..., true)
? would we just throw/assert?
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.
Yes, I would throw some type of bad argument exception that will cause Envoy to crash and can be checked in tests. (I would probably not derive from EnvoyException and pick a std exception for this).
also cause decodeTrailers to get triggered Signed-off-by: Snow Pettersen <snowp@squareup.com>
void ConnectionManagerImpl::ActiveStream::addEncodedTrailers(HeaderMapPtr&& trailers) { | ||
if (state_.filter_call_state_ & FilterCallState::LastDataFrame) { | ||
response_trailers_ = std::move(trailers); | ||
state_.local_complete_ = false; |
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 was necessary because state_.local_complete_
gets set to true at the start of encodeData
, so we have to undo that so that the ASSERT in commonEncodePrefix
doesn't fire at the start of encodeTrailers
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.
If it's worth a comment in the PR it's worth a comment in the code :-)
updated to use the semantics as discussed in comments:
when called outside of the required context it currently just drops the provided trailers on the floor. open to suggestions on to better handle this |
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.
Very cool change - I think it'll be a helpful feature to have!
bool end_stream_no_trailers = end_stream && !request_trailers_; | ||
if (end_stream_no_trailers) { | ||
state_.filter_call_state_ |= FilterCallState::LastDataFrame; | ||
} |
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'd argue changing HCM state probably bumps this up to a medium-risk change.
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.
+1
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.
What does HCM stand for in this 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.
Http Connection Manager (HCM)
@@ -775,15 +775,36 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* | |||
|
|||
for (; entry != decoder_filters_.end(); entry++) { | |||
ASSERT(!(state_.filter_call_state_ & FilterCallState::DecodeData)); | |||
|
|||
bool end_stream_no_trailers = end_stream && !request_trailers_; |
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.
Mind adding a comment that we check the state of end stream inside the loop in case a filter adds trailers?
ENVOY_STREAM_LOG(trace, "decode data called: filter={} status={}", *this, | ||
static_cast<const void*>((*entry).get()), static_cast<uint64_t>(status)); | ||
if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.decoder_filters_streaming_)) { | ||
return; | ||
} | ||
} | ||
|
||
if (end_stream && request_trailers_) { |
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.
again let's comment this is explicitly for the case that the remote side didn't send trailers but a filter added them.
request_trailers_ = std::move(trailers); | ||
} else { | ||
// do nothing | ||
// todo: should this throw/assert? |
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 it should :-)
I also think we should have integration tests for both the happy path and over-enthusiastic filters. I think we can start on those now, because even if the APIs change a bit the end to end tests should stay the same
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.
+1 same as my comment elsewhere. We should be doing this in a bunch of places (see some other TODOs from me) so we might as well start doing it now w/ tests.
void ConnectionManagerImpl::ActiveStream::addEncodedTrailers(HeaderMapPtr&& trailers) { | ||
if (state_.filter_call_state_ & FilterCallState::LastDataFrame) { | ||
response_trailers_ = std::move(trailers); | ||
state_.local_complete_ = false; |
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.
If it's worth a comment in the PR it's worth a comment in the code :-)
include/envoy/http/filter.h
Outdated
/** | ||
* Adds decoded trailers. This will overwrite any existing trailers should any already exist. | ||
*/ | ||
virtual void addDecodedTrailers(HeaderMapPtr&& trailers) PURE; |
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.
Is this out of date? With the code as it stands now, I think we can only call under decodeData with end_stream = true, and subsequent filters will never encounter that state (end stream will be false for future invocations) so should not be able to overwrite trailers.
I do think we should document when this is safe to call but if you prefer a TODO while the design is being hashed out that's fine :-)
|
||
void ConnectionManagerImpl::ActiveStream::addDecodedTrailers(HeaderMapPtr&& trailers) { | ||
if (state_.filter_call_state_ & FilterCallState::LastDataFrame) { | ||
request_trailers_ = std::move(trailers); |
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.
If I'm correct above that we can only set request_trailers_once now, can we also have an invariant ASSERT that they're empty when this is 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.
Few responses, not a full review. I think we are now converged on design, so let me know when the next set of changes are pushed and I will take a really detailed look. Thanks for working on this!
include/envoy/http/filter.h
Outdated
/** | ||
* Adds decoded trailers. This will overwrite any existing trailers should any already exist. | ||
*/ | ||
virtual void addDecodedTrailers(HeaderMapPtr&& trailers) PURE; |
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.
Yes, I would throw some type of bad argument exception that will cause Envoy to crash and can be checked in tests. (I would probably not derive from EnvoyException and pick a std exception for this).
bool end_stream_no_trailers = end_stream && !request_trailers_; | ||
if (end_stream_no_trailers) { | ||
state_.filter_call_state_ |= FilterCallState::LastDataFrame; | ||
} |
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.
+1
request_trailers_ = std::move(trailers); | ||
} else { | ||
// do nothing | ||
// todo: should this throw/assert? |
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.
+1 same as my comment elsewhere. We should be doing this in a bunch of places (see some other TODOs from me) so we might as well start doing it now w/ tests.
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
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.
Looking much cleaner - thanks for all the comments! I am still looking for integration tests when you have a chance - I think it'd be good to have tests both of the happy path and things like bad filters double-adding trailers causing disconnects rather than crashes :-)
include/envoy/http/filter.h
Outdated
virtual void addDecodedTrailers(HeaderMapPtr&& trailers) PURE; | ||
/** | ||
* Adds decoded trailers. May only be called in decodeData when end_stream is set to true or in | ||
* decodeTrailers. If called in any other context, std::logic_error will be thrown. |
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.
Can you call this under decodeTrailers? isn't decodeTrailers the wrong state (not LastDataFrame) and would have request_trailers_ non-null (exception either way)?
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.
Also I'm not sure if we're consistent about this but can you add a javadoc style @throws here and below?
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.
Yeah you're right - I was misunderstanding when request_trailers_
was set in the normal flow. The test failures were due to exactly this.
I'll add in the @throws
as well.
if (state_.filter_call_state_ & FilterCallState::LastDataFrame) { | ||
request_trailers_ = std::move(trailers); | ||
if (request_trailers_) { | ||
throw std::logic_error("decodedTrailers added more than once"); |
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.
So what will catch these exceptions? Perhaps they should be CodecProtocolException so that the existing codec code will catch them and terminate the connection?
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 picked a non-EnvoyException exception as per @mattklein123 suggestion in a previous comment (#3980 (comment)). If we want this to fail more gracefully then I'm happy to do so, just want to make sure we're all in agreement before switching over.
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.
IMO this is the right exception type so that we don't catch it and crash/core dump. I think a logic error like this should be caught during development time and should be very obvious?
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.
Sorry, to be clear, you think this exception should not be caught? I grant with our current example filters it should be fairly obvious, but once folks have custom filters which do non-trivial async work which may have timing invariants, and/or we make the pipeline more complicated with internal redirects, I think it gets less obvious. I'd strongly prefer developer errors cause disconnects and not crashes.
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.
Yes, I think it should not be caught. If you catch this exception you will need to add stats and logging to make it obvious what is happening otherwise it will be incredibly confusing. Basically, it's a bunch of error handling and operational work that IMO should not happen in practice. If there is disagreement on using an exception for this I would switch to RELEASE_ASSERT and make it crash.
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.
So the tl;dr here is that I should be using ASSERTs instead of exceptions, deferring better error handling to a future change?
@alyssawilk Given this, would you still want integration tests? It'd only test the happy path (unless we can test asserts somehow?), but it might still be worth having?
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.
tl;dr sounds right!
Yeah, I think given this we're just testing the happy path. One theoretically could do a debug death test for broken filters but even I think that'd be overkill :-)
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.
Cool I'll look into it. Could you point me to some integration tests I could extend/based mine off to cover these changes?
Out of curiosity: what would a "debug death test" look like?
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've got a bunch of tests which add filters (see config_helper_.addFilter()) so what we'll need to do is add a test (probably in http_integration.cc and then called from the various http[2][_upstream]integration_test.cc) which does makeRequestWithBody() and then verifies header/body/data get proxied downstream and then the flip side, upstream_request->encodeHeaders() and encodeData with trailers being received by H2 downstream.
The trickier bit is we don't have a sample filter which does this, so you'll need to add a test filter which always adds trailers. In the long run I'd like a test filter factory which creates MockStreamFilter which we can use that for all sorts of custom test behavior but I think that's out of scope for this PR :-) If you do the simple one I'll hopefully get some free time to convert it to the fancy version later in.
Actually it occurs to me thinking about makeRequestWithBody vs makeHeaderOnlyRequest.... we allow adding trailers on decodeData(end_stream = true) but should we also allow it in decodeHeaders (end_stream=true)? It seems more consistent for H2 but if we're really only wanting this for gRPC and we think we won't hit gRPC with no body where we want to add trailers, I'd be OK documenting it as a known limitation someone else can fix when/if they need it.
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 pointers, I'll try to get some tests set up.
I can't really think of a use case of adding trailers to request/response with no DATA frames (how is that even different from just adding additional headers?). I'd prefer to to just leave it as it is right now. Adding it in later should be fairly straightforward once all the unit tests/integration tests are in place.
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
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.
Hurrah for integration tests! They've prompted a couple of new thoughts along with a few nits I missed earlier.
include/envoy/http/filter.h
Outdated
* | ||
* When called in decodeData, the trailers map will be initialized to an empty map and returned by | ||
* reference. Calling this function more than once is invalid. | ||
*/ |
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.
Sorry, I should have called this out before, but please doc the return as @return here and below
@@ -792,7 +792,8 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* | |||
ENVOY_STREAM_LOG(trace, "decode data called: filter={} status={}", *this, | |||
static_cast<const void*>((*entry).get()), static_cast<uint64_t>(status)); | |||
if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.decoder_filters_streaming_)) { | |||
return; | |||
// break here to ensure that we call decodeTrailers below |
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.
if commonHandleAfterDataCallback fails, doesn't it mean some data filter has requested the pipeline pause iteration? Let's say we have filter A which adds trailers and filter B which waits for full body and then does an RPC. If B gets the full body and asks the pipeline to pause while it does a callback, I believe we have to pause (not process trailers) until we get commonContinue. Are the added trailers not being processed there?
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.
Yeah I was a bit unsure how to handle this. The problem I ran into was that the router filter will stop iteration after decodeData
and keep the connection alive since end_stream
is false. Since there's no trailers coming in from the decoding codec, nothing ever calls continue, so the the request hangs.
,
Maybe only break when end_stream
is true to cover this case? In this case we know that we won't get any more data from the codec, so by immediately calling decodeTrailers we're not skipping any data.
Open to other suggestions
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.
Actually that might not be feasible since we'd hide some data from filters behind the one that stopped iteration...
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.
Oh yeah, I see the problem now. Hmm.... The best workaround I can think of is fairly inelegant, which is when trailers are added to schedule a closure which basically fakes the "you have received trailers from a client" path but gets called safely from not under the stack of the connection manager pipeline. Maybe @mattklein123 has a better suggestion?
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 this is exactly the same situation that is handled here with body added in the headers callback? https://github.com/envoyproxy/envoy/blob/master/source/common/http/conn_manager_impl.cc#L721. Can we handle in the same way?
} else { | ||
throw std::logic_error("addDecodedTrailers called in invalid context"); | ||
} | ||
// traileres can only be added once |
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.
-> Trailers can only be added once.
here and below. Also trailers->Trailers above
Http::FilterDataStatus decodeData(Buffer::Instance&, bool end_stream) override; | ||
|
||
Http::FilterTrailersStatus decodeTrailers(Http::HeaderMap&) override { | ||
std::cout << "trailers cb" << std::endl; |
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.
clean up debug printing?
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.
whops yeah I'll remove
@@ -38,6 +38,8 @@ TEST_P(Http2IntegrationTest, MultipleContentLengths) { testMultipleContentLength | |||
|
|||
TEST_P(Http2IntegrationTest, ComputedHealthCheck) { testComputedHealthCheck(); } | |||
|
|||
TEST_P(Http2IntegrationTest, AddEncodedTrailers) { testAddEncodedTrailers(); } |
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.
It's a weird request but mind adding this to integration_test as well? I'd just like end to end coverage that if we add trailers over-enthusiastically on the upstream or downstream side that we still get a complete response.
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.
not weird at all! i was going to but it slipped my mind
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 actually caught a bug, so it was a great idea
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 iterating on this. These changes are difficult/scary but well worth it.
@@ -792,7 +792,8 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* | |||
ENVOY_STREAM_LOG(trace, "decode data called: filter={} status={}", *this, | |||
static_cast<const void*>((*entry).get()), static_cast<uint64_t>(status)); | |||
if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.decoder_filters_streaming_)) { | |||
return; | |||
// break here to ensure that we call decodeTrailers below |
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 this is exactly the same situation that is handled here with body added in the headers callback? https://github.com/envoyproxy/envoy/blob/master/source/common/http/conn_manager_impl.cc#L721. Can we handle in the same way?
// If trailers were adding during decodeData we need to trigger decodeTrailers in order | ||
// to allow filters to process the trailers. | ||
if (end_stream && request_trailers_) { | ||
decodeTrailers(filter, *request_trailers_); |
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.
Don't we need to actually only send trailers to the filter that set the trailer and filters after it? I think we need to actually track which filter added the trailers... (See how this is handled in the headers/data case).
// to allow filters to process the trailers. | ||
if (end_stream && response_trailers_) { | ||
response_encoder_->encodeData(data, false); | ||
encodeTrailers(filter, *response_trailers_); |
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.
Same comment here about keeping track of which filter added the trailers?
As an aside, I'm a little confused as to why the logic in encodeHeaders() does not mirror decodeHeaders() WRT to handling continue/last filter. (And yes I know I wrote this code!) I can look into this more tomorrow or Friday, but if you feel like taking a look that would be appreciated.
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 looked into it a little bit: adding the same last filter check to encodHeaders causes a test failure in HttpConnectionManagerImplTest.HitRequestBufferLimitsIntermediateFilter
because of a missing expectation for a call to response_encoder.encodeHeaders
, and an integration test fails due to
[2018-08-09 23:36:14.552][694871][critical][assert] bazel-out/darwin-dbg/bin/source/common/request_info/_virtual_includes/request_info_lib/common/request_info/request_info_impl.h:83] assert failure: !first_downstream_tx_byte_sent_.
which presumably comes from hitting this code more than once (towards the end of encodeHeaders
):
// Now actually encode via the codec.
request_info_.onFirstDownstreamTxByteSent(); // <- this bit
response_encoder_->encodeHeaders(headers,
end_stream && continue_data_entry == encoder_filters_.end());
The test that's failing is IpVersions/Http2IntegrationTest.HittingDecoderFilterLimit/IPv4
- I'm guessing the filter used there normally stops iteration, but by continuing on the last filter we end up running through the last part of encodeHeaders
again. Seems like the issue is that somehow encodeTrailers
is being called twice, and special casing the last filter causes the second call to hit onFirstDownstreamTxByteSent
.
Unclear whether this is why we don't special case the last filter in encodeHeaders
, or if it's just a issue that hasn't surfaced because we don't.
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.
OK, thanks for looking. I can take a look tomorrow or this weekend more. I wouldn't worry about it for now but it seemed strange to me that it wasn't symmetrical and I couldn't remember why.
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
Signed-off-by: Snow Pettersen <snowp@squareup.com>
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.
Looking very close. Very nice.
@@ -775,15 +777,50 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* | |||
|
|||
for (; entry != decoder_filters_.end(); entry++) { | |||
ASSERT(!(state_.filter_call_state_ & FilterCallState::DecodeData)); | |||
|
|||
// We check the request_trailers_ pointer here in case addDecodedTrailers | |||
// is called in decodeData - at which point we communicate to the filter |
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.
"is called in decodeData during a previous filter invocation, at which point we communicate to the current and future filters that the stream has not yet ended."
@@ -767,6 +767,8 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* | |||
} | |||
|
|||
std::list<ActiveStreamDecoderFilterPtr>::iterator entry; | |||
auto trailers_added_entry = decoder_filters_.end(); | |||
bool trailers_exists = request_trailers_ != nullptr; |
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.
s/trailers_exists/trailers_exists_at_start (or something like that)
|
||
if (!(*entry)->commonHandleAfterDataCallback(status, data, state_.decoder_filters_streaming_) && | ||
std::next(entry) != decoder_filters_.end()) { | ||
// break here to ensure that we call decodeTrailers below |
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 don't think this comment is correct. We are returning. Can you replicate a similar comment as we have in the headers case about why we are checking if this is the last filter?
// If trailers were adding during decodeData we need to trigger decodeTrailers in order | ||
// to allow filters to process the trailers. | ||
if (trailers_added_entry != decoder_filters_.end()) { | ||
(*trailers_added_entry)->stopped_ = true; |
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.
Why not just call decodeTrailers(...) here? What's the reason to stop and then continue?
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.
calling decodeTrailers
seems to work just as well so I'm changing it back to that
for (; entry != encoder_filters_.end(); entry++) { | ||
ASSERT(!(state_.filter_call_state_ & FilterCallState::EncodeData)); | ||
|
||
// We check the request_trailers_ pointer here in case addEncodedTrailers | ||
// is called in encodeData - at which point we communicate to the filter |
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.
Same comment about the comment and the local variable.
// to allow filters to process the trailers. | ||
if (trailers_added_entry != encoder_filters_.end()) { | ||
response_encoder_->encodeData(data, false); | ||
(*trailers_added_entry)->stopped_ = true; |
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.
Same comment about why stop/continue?
Signed-off-by: Snow Pettersen <snowp@squareup.com>
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 LGTM but I'm now officially out of my depth w.r.t http connection manger internals, so I'll defer to Matt from here on :-)
Thanks for tackling this complicated feature - it'll be a really nice enhancement for gRPC!
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, this is awesome. Really great work. Couple of nits then let's ship!
@@ -767,6 +767,8 @@ void ConnectionManagerImpl::ActiveStream::decodeData(ActiveStreamDecoderFilter* | |||
} | |||
|
|||
std::list<ActiveStreamDecoderFilterPtr>::iterator entry; | |||
auto trailers_added_entry = decoder_filters_.end(); | |||
bool trailers_exists_at_start = request_trailers_ != nullptr; |
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.
nit: const
@@ -1083,13 +1133,33 @@ void ConnectionManagerImpl::ActiveStream::encodeData(ActiveStreamEncoderFilter* | |||
Buffer::Instance& data, bool end_stream) { | |||
resetIdleTimer(); | |||
std::list<ActiveStreamEncoderFilterPtr>::iterator entry = commonEncodePrefix(filter, end_stream); | |||
auto trailers_added_entry = encoder_filters_.end(); | |||
|
|||
bool trailers_exists_at_start = response_trailers_ != nullptr; |
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.
nit: const
@@ -339,6 +343,9 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>, | |||
// to verify we do not encode100Continue headers more than once per | |||
// filter. | |||
static constexpr uint32_t Encode100ContinueHeaders = 0x40; | |||
// Used to indicate that we're processing the final [en|de]Code frame, |
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.
nit: [En|De]codeData
Signed-off-by: Snow Pettersen <snowp@squareup.com>
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.
Nice!
* origin/master: http_filter: add addEncodedTrailers and addDecodedTrailers (envoyproxy#3980) rbac/fuzz: fix build (envoyproxy#4150) Signed-off-by: Snow Pettersen <snowp@squareup.com>
Adds a function that allows inserting trailers into the stream response
outside of the encodeTrailers callback. This allows inserting trailers
when the upstream response has no trailers to begin with (e.g. h/1.1).
Signed-off-by: Snow Pettersen snowp@squareup.com
Risk Level: Low, new optional feature
Testing: Unit tests
Docs Changes: Doc string on public interface
Release Notes: n/a
Fixes: #3966