Refactor fd to accommodate more general I/O handle#4579
Refactor fd to accommodate more general I/O handle#4579sbelair2 wants to merge 1 commit intoenvoyproxy:masterfrom sbelair2:fd_refactor
Conversation
|
@lyft/network-team please review |
…s with ioHandle() calls; move IoHandle class, PostIoAction, IoResult into new file include/envoy/network/io_defs.h Signed-off-by: Stephen Belair <sbelair@cisco.com>
|
@sbelair2 thanks I will take a look. Can you check the format CI job? |
|
Regarding the failed test ci/circleci:format: ERROR: From ./include/envoy/network/io_defs.h And then: tools/check_format.py fix ./include/envoy/network/io_defs.h Note that I have clang-format installed- I don’t know how to get clang-format-6.0 (?). Looking at io_defs.h, I see no format errors, either on line 1 or line 56. @mattklein123: Suggestions? |
|
Looking at the ci/circleci:asan failure, only one of 326 failed: @envoy//test/integration:idle_timeout_integration_test FAILED in 28.9s external/envoy/test/integration/idle_timeout_integration_test.cc:174: Failure I’m not sure what that failure is about… |
|
I'm really excited about this effort; thanks for taking this on! I'm wondering, though, if it would be more general to change widespread usage of integer handles to some abstract base class. IOHandle seems like a good name for such a class, and is consistent with some of your new naming. However, the PR as it stands still uses ints to pass around these descriptors. I realize that the devil is in the details, especially when it comes to hooking it into libevent, which exposes the need for integer FDs. Nevertheless I think it might be possible to put the libevent/fd interface into virtual methods on a subclass of IOHandle (SocketIOHandle or something). @cpakulski WDYT? |
+1, this is what I had in mind. I don't think continuing to pass around integer handles is really where we want to go. What @jmarantz mentions is where I thought we were heading with this change. Thoughts? |
|
@jmarantz, @mattklein123: Thanks for the comments. To be honest, I wasn't sure what the ultimate intentions of the fd refactor were- all I had to go on was the original commit which introduced the fd. Let me get back to you on this- thanks! |
|
Few thoughts after reviewing the changes. I tried to do something similar, but not exactly the same. My goal was to hide fd and talk to a socket via a class, let us name it EnvoySocket class. Such abstraction would allow to use real socket handles or simulate ones (handy in tests). I hit a wall at some point. As @jmarantz mentioned devil is hidden in details - infrastructure code calls system routines and they require "real" fds. Passing a simulated fd to such calls caused a crash. At some point I came to conclusion that abstraction must be taken one or two levels above and abstracting just Socket is not enough. By saying one or two levels up, I mean for example Dispatcher. Right now Dispatcher just uses system calls and expects real fd handles. It would have to be changed to accept EnvoySocket class and in your case ioHandle and would have to know how to act when type of IoHandle is IoHandle::SocketFd or IoHandle::SessionId. Good example is |
|
@jmarantz, @mattklein123, @cpakulski: thanks much for the discussion. I'm not sure if everyone saw @cpakulski's response: "Few thoughts after reviewing the changes. I tried to do something similar, but not exactly the same. My goal was to hide fd and talk to a socket via a class, let us name it EnvoySocket class. Such abstraction would allow to use real socket handles or simulate ones (handy in tests). I hit a wall at some point. As @jmarantz mentioned devil is hidden in details - infrastructure code calls system routines and they require "real" fds. Passing a simulated fd to such calls caused a crash. At some point I came to conclusion that abstraction must be taken one or two levels above and abstracting just Socket is not enough. By saying one or two levels up, I mean for example Dispatcher. Right now Dispatcher just uses system calls and expects real fd handles. It would have to be changed to accept EnvoySocket class and in your case ioHandle and would have to know how to act when type of IoHandle is IoHandle::SocketFd or IoHandle::SessionId. Good example is It seems to me that the requirements can grow without end as we try to accommodate all use cases if we aren't careful. I'm wondering if maybe we should make IoHandle be really opaque and customizable at the same time, something like below. In trying to come up with a non-integer handle in a generic IoHandle class, I wondered what should the type actually be of the handle? We agree that it being an int is too restrictive, but what should it be? I came to think that an IoHandle class should accommodate: (1) a pointer to an opaque type as a generic handle and (2) an integer value that is used when we force a handle into one of its eigenvalues, so to speak. For socket types, that is an int. For VPP sessions, we'll see that the int is a session id. So the point is that the IoHandle has two generic and opaque pieces of private data: a pointer to whatever type, and an integer field, along with getters for either variable. Some parts of Envoy code might use the pointer variable, others might use the integer variable. Sample definition below, but note that I'm using void * below for discussion purposes, bear with me please... (full disclosure: I have been programming in C my whole career, primarily- coming up to speed with c++11 chops) class IoHandle { public: Ok, the void * part. Really what I think is required is a template class for IoHandle, where we take a T * pointer for a class T, and an int: template<Class T*, int> IoHandle { ... }. I haven't worked out the code yet :-), but what I don't like about this is that we'll always have to be passing in a pointer to a class when we declare and construct IoHandles. Hence the void * above- it's a placeholder for proper code, and it conveys the opacity. What I was thinking was that for future use, calling code can store context in the handle if it wants or needs to. We can even add another private datum: int data_, with a getter for it. Thus the calling code can either store an extra int for private use, or a pointer to some class. Either way, I see no need for the IoHandleType anymore, since a private data field can store the owner-specific state if it needs to, in addition to the substitute-for-fd intValue. So, the void */maybe templated class is now: class IoHandle { So, the way this would integrate into the current diffs: all references to IoHandleType will be removed, and all places in the code that have handle.ioHandle() will be replaced by handle.intValue(); The rest is open for future use, or not. Is the use of a template class for IoHandle too much a pain? I hope I'm not in left field on this :-), comments please. |
|
I think that's pretty close, but the C++-ish way to do that is make IOHandle a base class, with pure virtual methods covering all the system-calls you'd want to do with FDs (getsocketopt, read, write, libevent file-events). There would be an FD-based derived class from IOHandle that would have a single member var Then let's say you were building a Windows version and the network primitives were some kind of abstract handle provided by that OS. You'd make another derived class that implements all those methods. You wouldn't need to use a And you could do the same for a testing discipline which avoided making any OS calls. E.g. in the past I've built a MemFileSystem for use in tests, using a similar architecture, and a hash-table to represent files and their contents. One tricky part is where we are dependent on using code we don't control (libevent) that has an FD interface. One solution I'd throw out there -- not that I love it but it's the first thing that comes to mind -- is to use threads to poll for non-fd related file/network events, and then get libevent to wake up the right callback via a timer. |
|
What @jmarantz mentions is what I was envisioning. Basically, the internals of IOHandle are abstract, with methods on it to do various things. I agree there are various details here that are tricky, for example the libevent case. IMO the way to handle this is actually to build IOHandle directly into Event::Dispatcher which is how we completely hide libevent from the rest of the code. Thus, the dispatcher can spit out IOHandles, addresses, connections, etc. If we do this I suspect it mostly "just works." I think this is what we want to do anyway for VPP since I'm pretty sure that ultimately VPP would need to implement its own dispatcher and move away from libevent, right? This would be the same for a DPDK solution. |
|
This pull request has been automatically marked as stale because it has not had activity in the last 7 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
This pull request has been automatically closed because it has not had activity in the last 14 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
@mattklein123, @jmarantz, @htuch, @cpakulski: Re-opening this PR which was closed because it had become stale. My apologies for that- it took some time to understand the comments received. There is no new code, just proposed changes with the goal of continuing the discussion and the feedback. The issue with refactoring the fd is the implicit assumption that the main interface to Envoy from the application is a socket. In this PR, our purpose is to abstract away sockets and all system calls pertaining to sockets from the I/O interface. The IoHandle represents the abstract interface for all I/O operations and the details of any particular interface (socket, VPP, etc) are contained in classes derived from IoHandle. To this end, we also define the IoAddress class and some derivatives, as well as the IoDescriptor class, which will replace the fd() calls when dealing with actual sockets. These changes in this PR are a proposal and are intended to get feedback for the direction we are taking. Rather than show all the changes required to implement this new direction, we will also discuss towards the end how we anticipate further changes rippling through the code. Note also that this PR does not address events yet, as previous comments suggested. The intention is to introduce further PRs that will address events and the dispatcher. I. IoHandle and derivative classes The following definitions would be contained in a new file io_defs.h. // Generalize the SysCallResult to accommodate I/O operations that are not template struct IoCallResult : public struct IoResult { template class IoDescriptor { T descriptor() const { return(descriptor_); } operator(T) const { return(descriptor_); protected: typedef IoDescriptor IoSocketDescriptor; // fd for sockets // I/O options struct IoOptionName { }; struct IoOptionNameSimple : public IoOptionName { struct IoSocketOptionName : public IoOptionName { struct IoSocketOptionValue : public IoOptionValue { class IoHandle { // I/O Options API virtual void setTransport(IoTransportPtr transport) PURE; // Calls made from Envoy::Network::Address::Instance // Read/Write operations // Descriptor // Socket derivative IoSocketHandle(const IoDescriptor& descriptor, const IoAddress& local_addr, // I/O Options API // Operations in Envoy::Network::Address::Instance // Read/Write operations const IoDescriptor& descriptor() const override { return(descriptor_); } // Addresses (see include/network/address.h below) protected: Similarly, we would define a new class IoHandleVpp that would accommodate differences required to interface to VPP. And it would have a customized IoTransportVpp member. II. IoAddress and derivative classes For the address classes, the intention was to remove all the methods using the fd. The IoAddress class is intended to replace the Instance class defined in address.h. // in envoy/include/network/address.h: enum class IoType { Stream, Datagram }; class IoAddress // equivalent to class Instance in address,h } // Replaces InstanceBase, InstanceIpv4, InstanceIpv6, IpHelper template <typename T, typename U > class IoAddressBase : public IoAddress { virtual const std::string& logicalName() const override { return asString(); } bool operator == (const T& lhs) { protected: }; // Implementations for IoAddressType. The details for Ipv4 sockets are given class IoSockAddrIpv4 : public IoAddressBase<sockaddr_in, uint32_t> { sockaddr_in address() const { return address_; } // The following is unclear- this next method replaces socketFromSocketType() but const sockaddr_in& addressFromString(const std::string& address, Note also that the methods addressFromFd() and peerAddressFromFd() should be methods in a derived class from a base class that has virtual methods addressFromIoHandle() and peerAddressFromIoHandle(), as opposed to addressFromSockAddr() which pertains only to sockets. III. Transport Sockets The transport sockets will be redefined by the class IoTransport, with derivatives for sockets and for VPP. In envoy/include/envoy/network, replacing the file transport_socket.h with the new file io_transport.h: class IoTransportCallbacks { virtual IoDescriptor descriptor() const PURE; virtual Network::Connection& connection() PURE; class IoTransport { virtual void setTransportCallbacks(IoTransportCallbacks& callbacks) PURE; virtual bool canFlushClose() PURE; virtual IoResult doRead(Buffer::Instance& buffer) PURE; class IoTransportFactory { virtual bool implementsSecureTransport() const PURE; Definitions of IoTransport for sockets (in a file in envoy/source/common/network/io_transport_socket.h?): class IoTransportSocket : public IoTransport { And then, for example, in envoy/source/common/network/raw_buffer_socket.h; class RawBufferSocket : public IoTransportSocket, private: IV. Further Notes We anticipate that these proposed changes will require changes to the current existing classes: Socket
In ConnectionHandlerImpl::ActiveListener::newConnection: Network::ConnectionPtr new_connection = parent_.dispatcher_.createServerConnection(std::move(ioHandle)); Note that an IoTransportPtr is not passed in because the IoTransport is contained in the IoHandle. const auto filter_chain = config_.filterChainManager().findFilterChain(*ioHandle); |
|
@mattklein123, @jmarantz, @htuch, @cpakulski: I tried to re-open the PR and saw no way to do it. Your advice is welcome. |
|
@sbelair2 thanks for the proposal. Can you potentially code format it so it's easier to read? Or put into a gist or new PR that we can comment on? Even if only psuedo header files? Thank you! (I can reopen this PR but I don't think it's worth it). |
2 similar comments
|
@sbelair2 thanks for the proposal. Can you potentially code format it so it's easier to read? Or put into a gist or new PR that we can comment on? Even if only psuedo header files? Thank you! (I can reopen this PR but I don't think it's worth it). |
|
@sbelair2 thanks for the proposal. Can you potentially code format it so it's easier to read? Or put into a gist or new PR that we can comment on? Even if only psuedo header files? Thank you! (I can reopen this PR but I don't think it's worth it). |
|
@mattklein123: Sure, happy to. My apologies- I thought this was readable enough to get the idea across. I will generate a new PR today. |
Description:
This PR addresses issue #4546- refactoring the fd to accommodate a generalized I/O handle.
In preparation for Envoy integration for VPP, the fd() method is generalized beyond the assumption that there is a socket interface between Envoy and its clients.
An IoHandle class is introduced, which holds a handle and a new IoHandleType, which qualifies the handle as one of: a socket fd, a new session id, or a stream id.
Two methods are introduced: ioHandle(), which returns the actual handle and ioHandleType(), which returns the type of the handle.
The calls to fd() in the code are replaced by calls to ioHandle()- there are no calls as yet to ioHandleType(), which is available for future use.
Finally,. a new file is introduced in envoy/include/envoy/network: io_defs.h. io_defs.h has the new definitions for IoHandle, as well as those for PostIoAction and IoResult, which were removed from transport_socket.h to io_defs.h.
Risk Level: Low
Testing: Integration testing
Docs Changes:N/A
Release Notes: N/A
Fixes #4546