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
listener: remove the peek from the listener filters #17395
Conversation
cc @mattklein123 @ggreenway @antoniovicente @lambdai I created this PoC for showing the interface design, so I didn't work out the unittest/functional test since I thought there will be some discussion on the interface design. But if you guys think the test should be work out, I will work on it. |
/wait |
I like the direction this is going. I'd like to take it even further, and do a read, not a peek. Then the listener filter can drain data from the buffer if it wants to consume the data (proxy proto listener filter needs to do this), or just look at the contents of the buffer if it only wants to inspect. At the end of the listener filters, pass the buffer (probably non-empty) along with already-read data. If we went that route, then callback name just becomes |
thanks for your review! l will deep into the way of read instead of peek |
+1 The current duplicate peek includes independent peek among listener filters which you are addressing. |
@ggreenway @lambdai I update the PoC, using read instead of peek. But it is the same as previous the buffer kept in the ActiveTcpSocket. In the end, the buffer will transfer to the ConnectionImpl. |
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 like this approach a lot! This will make the listener filter interface so much nicer.
The biggest thing left to figure out is getting the data that is read for listener filters to go as input to the Transport Socket, not directly into the ConnectionImpl read buffer.
/wait
Thanks for the feedback! I will dig into the transport socket. |
I update the PoC. I found we have a custom BIO for the SSL socket. And I think all the transport sockets are using envoy/source/extensions/transport_sockets/tls/ssl_socket.cc Lines 75 to 76 in 95038fe
So I inject the data into the ioHandle . When read the data from ioHandle , it will return the injected data first, then ready the data from the real socket.
Then the data read by the listener filter will be as an input to all the transport socket. |
@mattklein123 @ggreenway I find out a way to move the inspect data into transport, looking for your feedback when you have time, thanks in advance! |
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 for the delay on this; I've been thinking through these changes.
I think putting the data into the IoHandle
makes sense, because then none of the transport sockets need to change. But I'm not thrilled with any IoHandle being able to have data injected to it at any time. Maybe just documenting the usage and adding ASSERTs is good enough. Another idea (not sure if this would work or not) is to have an IoHandle that wraps the real IoHandle, stores the listener-filter data, returns that data from read operations, and passes all other interface calls to the real/wrapped IoHandle (including reads when the buffer is empty). I'm not sure if that will actually be an improvement or not, but it would at least mean that all of these changes aren't present for outgoing/upstream connections.
@mattklein123 what are your thoughts on these interfaces?
There are some other comments I'll have regarding the implementation, but I want to settle the interfaces somewhat before spending time on that.
/wait-any
Thanks for working on this. I think this will be a great improvement. Here are my high level thoughts, riffing a bit on some of @ggreenway ideas. I think the way I would think about this at a high level would look something like:
It's possible that we can greatly simplify this if we really only have 2 cases which are:
Do we have any cases of straight up modification? If not I think we can punt this and at that point we don't need to inject at all. We either peek only read and remove only. If we do have to modify I think we could then have the logic to send the modified data + potential peeked data to further filters, and then finally inject any extra data directly into the connection when it is created. ^ is not fully formed but this is how I would think about it personally. @ggreenway wdyt? |
I'm not sure we can make removing data work without passing some read data to the next filters, because we need to read the data to know exactly how many bytes we need to consume. But I suppose a hybrid of these approaches is to always pass data to the listener filter via peek, but give the listener filter a callback to drain some number of bytes from the connection. The implementation of that would be a I've never really liked using PEEK because if the data arrives in multiple segments, we have to re-read data multiple times. For something like a large TLS clienthello, sent pathologically (1 byte at a time), this can add up to a substantial amount of wasted effort. But it does avoid having to muck with IoHandle at all, which is nice. I think the most important thing at this point is to get the interface to the ListenerFilter correct; the interface shouldn't depend on our underlying implementation of how we read from sockets. |
Yes, totally agree with you. It is bad we have injection but we don't use it for some place. It is a great idea to have wrapped IoHandle for listener. If we are going to the road to use |
+1, totally agree that we shouldn't expose the underlayer to the listner filter
If the listener filter ask for data modifying, then we still need the data injection dance, right?
@lambdai mentioned the data modification in the issue, maybe he has any usecase. If no data modification usecase, I guess you mean it back to the initial version of this PoC, peek the data, and an interface for listener to tell how much data they want to remove from the socket. Then using the read to drain the data from the socket. I kind of like the current interface
emm...so we still need the inject data for modifying. I'm not sure about the benefit of hybrid the |
got it, thanks! |
@ggreenway @yanavlasov gentle ping :) @soulxu DCO seems to have some issue, can you PTAL? |
Seems stuck at somewhere /retest |
Retrying Azure Pipelines: |
I don't think you can re-trigger DCO-bot. Can you push an empty commit, or merge main and push, to re-trigger DCO-bot? |
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 looks great! Thanks for the all the work you've put into this!
I can just force merge as all the commits have sign off. Thank you! |
@ggreenway @mattklein123 thanks for the guidance again! |
I thought it might be due to my upgrade to Bazel 5.1.0 but I'm now getting a MacOS M1 aarch compilation error on a file changed by this PR
|
Yea, it should be the |
I suspect this is causing issues with Istio import. The following minimal bootstrap is now not performing TCP proxying:
Every connection is terminated. Seems like some bug got introduced into HTTP inspector. |
@kyessenov not surprised there would be a regression here. Feel free to revert until we sort it out. Thank you. cc @soulxu |
I looked at the PR, and it didn't change |
Given this is something that istio depends on, can you create an integration test that captures the failure? Both to help diagnose this issue, and to prevent future regressions. It may be a failure when there are two listener filters; i don't think we have much coverage for that situation at all. |
I can confirm it's this PR for the test case above. It's just one filter I think - before the socket close wasn't doing anything somehow. |
I think I understand what happened - prior version was signaling ParseError on read errors. The refactor conflated the actual parse error and continued closing the socket. |
OK if you want to fix forward we can do that or revert for now. Up to you. |
Although this looks like a one liner fix to remove socket.close() and return Continue, let's revert, and an integration test for HTTP (which is missing) and then validate again. |
@kyessenov Thanks for diagnosing this, let me fix it. |
Signed-off-by: He Jie Xu <hejie.xu@intel.com>
// As per discussion in https://github.com/envoyproxy/envoy/issues/7864 | ||
// we don't add new enum in FilterStatus so we have to signal the caller | ||
// the new condition. | ||
cb.socket().close(); | ||
cb_->socket().close(); | ||
return Network::FilterStatus::StopIteration; |
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 should be continue
, and not close the socket.
EXPECT_CALL(socket_, close()); | ||
file_event_callback_(Event::FileReadyType::Read); | ||
auto status = filter_->onData(*buffer_); | ||
EXPECT_EQ(status, Network::FilterStatus::StopIteration); |
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 is wrong, I changed the original test behavior.
Signed-off-by: He Jie Xu <hejie.xu@intel.com>
Commit Message: listener: remove the peek from the listener filters
Additional Description:
To avoid the listener filters to peek the data from the socket directly, allow the ActiveTcpSocket to peek the data
into the buffer. The ActiveTcpSocket will get the max data expected from the filters through the
filter interface
size_t maxReadBytes()
. Then ActiveTcpSocket will listen on the file event on the socket.And peek the data into the buffer. And calling the listener filter callback
onData(Buffer::Instance&)
.If the filter doesn't need any data from the connection, it should return 0 on the interface 'maxReadBytes()'.
The ActiveTcpSocket won't listen on the file event if there is no filter expecting any data.
The buffer will be shared across the listener filter. no duplicated peek need anymore.
The buffer is implemented by
ListenerFilterBufferImpl
. It will store the data peeked from the socket, and providethe const pointer to the listener filter to access the data. It also provides the interface for listener filter to drain the data,
and the data will be drained on the socket in the end.
Risk Level: high
Testing: unit tests and integration tests
Docs Changes: add document for the new stats
Release Notes: add release note for new stats
Fixes: #17229
Signed-off-by: He Jie Xu hejie.xu@intel.com