network: refactor tcp listener to use file events#12547
network: refactor tcp listener to use file events#12547mattklein123 merged 7 commits intoenvoyproxy:masterfrom
Conversation
IoHandle specific, implicitly, socket interface specific, implementations of listen and accept are now used to accept new IoHandles, instead of the os specific sycalls libevent leverages internally for listeners. Signed-off-by: Florin Coras <fcoras@cisco.com>
|
@mattklein123 this is practically point 4 here. |
Signed-off-by: Florin Coras <fcoras@cisco.com>
Signed-off-by: Florin Coras <fcoras@cisco.com>
antoniovicente
left a comment
There was a problem hiding this comment.
Looks like a nice cleanup. Thanks for doing this!
| os_fd_t rc; | ||
|
|
||
| #if defined(__linux__) | ||
| rc = ::accept4(sockfd, addr, addrlen, flags); |
There was a problem hiding this comment.
What other flags can be passed in to this version of accept?
There was a problem hiding this comment.
SOCK_CLOEXEC, but as far as I understood, we're not currently using that.
| void ListenerImpl::onSocketEvent(short flags) { | ||
| ASSERT(flags & (Event::FileReadyType::Read)); | ||
|
|
||
| while (1) { |
There was a problem hiding this comment.
It would be good to have a limit on the number of accept calls done per wakeup and warn if we had to do too many accepts in a row as it indicates that the proxy may have fallen behind for some reason.
There was a problem hiding this comment.
As far as I know, libevent is not doing anything more complicated than this. But we could definitely improve it going forward.
| #ifdef WIN32 | ||
| auto trigger = Event::FileTriggerType::Level; | ||
| #else | ||
| auto trigger = Event::FileTriggerType::Edge; |
There was a problem hiding this comment.
I think listeners should always operate on Level mode.
Operating in level mode allows us to safely accept only the first N connections available and return without having to set explicit resumption conditions.
There was a problem hiding this comment.
This is related to the comment above. We currently consume all the accepted sessions, before returning. If we plan to add a more sophisticated admissions mechanism, then we might want to switch.
There was a problem hiding this comment.
Use of level mode here always feels slightly safer. I agree that use of edge trigger should mostly be ok since we drain to completion. I worry about some transient error on accept resulting in us losing the trigger and then never recovering. Plus, doing the same on all platforms is simpler.
| ListenerImpl::~ListenerImpl() { | ||
| if (file_event_.get()) { | ||
| file_event_->setEnabled(0); | ||
| file_event_.reset(); |
There was a problem hiding this comment.
I'm fairly sure that you can just delete the file_event_, no need to setEnabled(0) first. You should be able to remove this explicit destructor.
There was a problem hiding this comment.
Fair point. Let me run the whole test suite to see if it hits any problems.
There was a problem hiding this comment.
Out of the 687 tests, //test/integration:h1_capture_fuzz_test crashed at source/common/http/conn_manager_impl.cc:639 on first suite run but always passed afterwards (if run as part of the suite or independently). Not sure what to make of it as it could be unrelated ...
| socklen_t remote_addr_len = sizeof(remote_addr); | ||
|
|
||
| IoHandlePtr io_handle = socket_->ioHandle().accept(reinterpret_cast<sockaddr*>(&remote_addr), | ||
| &remote_addr_len, ENVOY_SOCK_NONBLOCK); |
There was a problem hiding this comment.
ENVOY_SOCK_NONBLOCK seems to be hard coded.
Do we need the flags argument? Should the implementations set non-block unconditionally on their own?
There was a problem hiding this comment.
ENVOY_SOCK_NONBLOCK is just an os independent flag that we can pass to accept since SOCK_NONBLOCK does not exist on windows and darwin. The traditional POSIX accept syscall, on linux at least, does not inherit the O_NONBLOCK flag (see SOCK_NONBLOCK here).
There was a problem hiding this comment.
The question is if you should hard code use of SOCK_NONBLOCK in OsSysCallsImpl::accept and drop the flags argument to the accept interface method.
There was a problem hiding this comment.
Apologies, I thought you meant the underlying syscalls, not our accept implementations. I guess we can do that and then if at a later point the need for flags arises, we can re-add the parameter. Reasonable?
test/mocks/network/io_handle.h
Outdated
| MOCK_METHOD(bool, supportsUdpGro, (), (const)); | ||
| MOCK_METHOD(Api::SysCallIntResult, bind, (Address::InstanceConstSharedPtr address)); | ||
| MOCK_METHOD(Api::SysCallIntResult, listen, (int backlog)); | ||
| MOCK_METHOD(IoHandlePtr, accept, (struct sockaddr * addr, socklen_t* addrlen, int flags)); |
There was a problem hiding this comment.
Is there some additional testing that we should add for your changes? Are there specific reasons why additional testing is not needed?
There was a problem hiding this comment.
Up until now, this accept was hidden within libevent. So, whatever we tested until now should still be tested.
| #else | ||
| auto trigger = Event::FileTriggerType::Edge; | ||
| #endif | ||
| socket.ioHandle().listen(128); |
There was a problem hiding this comment.
Use a constant for the 128 backlog.
What was the listen backlog prior to these changes? Should the backlog be configurable?
There was a problem hiding this comment.
See #9501. @florincoras can you fix that as part of a follow up change? Would really appreciate it. It should be much easier now. In the interim we should replicate what libevent does which IIRC is 128 as you did here. Please add a TODO to follow up and fix this.
There was a problem hiding this comment.
Actually, that's what I found by digging into libevent.
There was a problem hiding this comment.
@mattklein123 just realized I missed your comment. Adding the TODO and yes, this should be pretty easy to customize now, although I'll need a bit of guidance wrt to where we should be configuring this from. Later edit: I can go with the suggestion from the issue, i.e., tcp_backlog_size and listener.proto if that's okay with you.
There was a problem hiding this comment.
A TODO should be good enough for now.
tcp_backlog_size would be a nice future addition in a followup change by either you or someone else.
There was a problem hiding this comment.
Yup, added it to my TODO list.
There was a problem hiding this comment.
Can you flesh out the comment more to explain where the magic 128 comes from (libevent)?
mattklein123
left a comment
There was a problem hiding this comment.
Thanks for working on this. Just flushing a few quick comments.
/wait
| void ListenerImpl::onSocketEvent(short flags) { | ||
| ASSERT(flags & (Event::FileReadyType::Read)); | ||
|
|
||
| while (1) { |
| #else | ||
| auto trigger = Event::FileTriggerType::Edge; | ||
| #endif | ||
| socket.ioHandle().listen(128); |
There was a problem hiding this comment.
See #9501. @florincoras can you fix that as part of a follow up change? Would really appreciate it. It should be much easier now. In the interim we should replicate what libevent does which IIRC is 128 as you did here. Please add a TODO to follow up and fix this.
| // rejected/closed. If the accepted socket is to be admitted, false is returned. | ||
| static bool rejectCxOverGlobalLimit(); | ||
|
|
||
| Event::Libevent::ListenerPtr listener_; |
There was a problem hiding this comment.
This type I think can be deleted now wherever it is defined.
- added TODOs for accept admission and listen backlog config - removed explicit Listener destructor - removed ListenerPtr type - removed accept flags, i.e., SOCK_NONBLOCK is now implicit on accepted fds Signed-off-by: Florin Coras <fcoras@cisco.com>
Signed-off-by: Florin Coras <fcoras@cisco.com>
| #else | ||
| auto trigger = Event::FileTriggerType::Edge; | ||
| #endif | ||
| socket.ioHandle().listen(128); |
There was a problem hiding this comment.
A TODO should be good enough for now.
tcp_backlog_size would be a nice future addition in a followup change by either you or someone else.
| #ifdef WIN32 | ||
| auto trigger = Event::FileTriggerType::Level; | ||
| #else | ||
| auto trigger = Event::FileTriggerType::Edge; |
There was a problem hiding this comment.
Use of level mode here always feels slightly safer. I agree that use of edge trigger should mostly be ok since we drain to completion. I worry about some transient error on accept resulting in us losing the trigger and then never recovering. Plus, doing the same on all platforms is simpler.
include/envoy/api/os_sys_calls.h
Outdated
| virtual SysCallSizeResult write(os_fd_t socket, const void* buffer, size_t length) PURE; | ||
|
|
||
| /** | ||
| * @see man 2 accept |
There was a problem hiding this comment.
We may want to say something about it also marking the socket as non-blocking.
- listener FileEvent uses level trigger mode - comment about non-blocking accepted fds Signed-off-by: Florin Coras <fcoras@cisco.com>
mattklein123
left a comment
There was a problem hiding this comment.
Thanks LGTM with small comments.
/wait
| #else | ||
| auto trigger = Event::FileTriggerType::Edge; | ||
| #endif | ||
| socket.ioHandle().listen(128); |
There was a problem hiding this comment.
Can you flesh out the comment more to explain where the magic 128 comes from (libevent)?
| socket.ioHandle().listen(128); | ||
| file_event_ = dispatcher.createFileEvent( | ||
| socket.ioHandle().fd(), [this](uint32_t events) -> void { onSocketEvent(events); }, | ||
| Event::FileTriggerType::Level, Event::FileReadyType::Read); |
There was a problem hiding this comment.
Can you add a comment specific to why we are using level trigger here until if/when we change it?
| throw CreateListenerException( | ||
| fmt::format("cannot listen on socket: {}", socket.localAddress()->asString())); | ||
| } | ||
| RELEASE_ASSERT(file_event_, "file event must be valid"); |
There was a problem hiding this comment.
nit: remove, we don't check for OOM type errors like this.
- more context for 128 listener backlog - explain why we use level trigger, as opposed to edge trigger, for listener file event - removed OOM release assert Signed-off-by: Florin Coras <fcoras@cisco.com>
|
Is there any way to retry only Linux-x64 release as it failed with: |
IoHandle specific, implicitly, socket interface specific,
implementations of listen and accept are now used to accept new
IoHandles, instead of the os specific sycalls libevent leverages
internally for listeners.
Signed-off-by: Florin Coras fcoras@cisco.com
Risk Level: Medium
Testing: unit tests
Docs Changes: n/a
Release Notes: n/a