IoHandle readv and writev#6037
Conversation
|
/assign @sbelair2 @mattklein123 |
There was a problem hiding this comment.
Would it be possible to call readv directly here? I ask, since for our Windows implementation, we'll be using _WSABUF / WSARecv instead of iovec / readv, and we don't to use platform-specific typedefs if at all possible (which we'd have to do to have a readv/writev implementation in OsSysCalls)
I think these tests:
envoy/test/common/buffer/owned_impl_test.cc
Line 115 in 99696cd
There was a problem hiding this comment.
As you see, IoSocketHandleImpl::readv/writev now does more than calling a system api. If we mock out the IoHandle code, many of the existing unit tests won't be able to cover the actually IO logic.
One thing I don't really understand about the problem: I thought windows implementation will have its own IoHandle implementation,, why would using os_sys_call here matter at all to a totally different implementation?
There was a problem hiding this comment.
Unless I'm missing something, there aren't actually any unit tests that cover this IO logic. The only two test files that use mocked readv / writev are:
- owned_impl_test.cc, which doesn't make any assertions on the arguments passed to readv/writev
- proxy_protocol_test.cc, which just passes the arguments through to the actual syscall.
So I do not think that using a mocked IoSocketHandle would affect the current test coverage. I would also argue that the Buffer::OwnedImpl tests shouldn't "know" the implementation of IoSocketHandle
If we do want to add tests that cover how IoSocketHandleImpl::readv/writev implement the IO logic, they should probably test the class directly
As to why (from a Windows perspective), we care about this separate implementation: we would like to remove writev and readv from the OsSysCalls interface, since their function signatures cannot be compiled on Windows (iovec is not a type on Windows).
If we did need to keep writev + readv in the OsSysCalls interface, we would probably just end up with something like
class OsSysCalls {
#ifdef WIN32
// Windows system calls
#else
// POSIX system calls
#endif
}
which isn't the worst thing in the world, but it's not ideal.
There was a problem hiding this comment.
Unless I'm missing something, there aren't actually any unit tests that cover this IO logic. The only two test files that use mocked readv / writev are:
- owned_impl_test.cc, which doesn't make any assertions on the arguments passed to readv/writev
- proxy_protocol_test.cc, which just passes the arguments through to the actual syscall.
So I do not think that using a mocked IoSocketHandle would affect the current test coverage. I
IoSocketHandleImple::readv/writev is mostly a partial copy from Buffer::OwnedImpl::read/write. owned_impl_test.cc should have had coverage on them. If we mocked out IoSocketHandle, we will lose coverage on the IoSocketHandleImpl::readv/writev. It is true that we could test this class independently. But this PR is just a refactory, I don't want to add more complexity to it. Plus I could see more than just these 2 tests are using MockOsSysCalls, i.e. proxy_protocol_test.cc which using mocked IoSocketHandleImpl will require some plumbing through connection and buffer.
Do you think it's okay to keep the usage of OsSysCalls as is for now and add a TODO for future change if needed.
And just FYI, I think the get/setsockopt will use OsSysCalls API too.
There was a problem hiding this comment.
Right, there are other tests that use MockOsSysCalls, but owned_impl_test.cc and proxy_protocol_test.cc are the only 2 that actually care about readv / writev. And since neither test makes any assertions on the arguments to readv / writev, I don't think we actually have any coverage on the bits of IoSocketHandleImpl::readv/writev that were copied from Buffer::OwnedImpl::read/write.
Anyways, we do not need to solve this right now, we will come back to it when we start PR'ing the Windows implementation of IoSocketHandle.
There was a problem hiding this comment.
similarly to above, would it be possible to call writev directly here?
|
ratelimit_integration_test failed because dereferencing ENVOY_ERROR_AGAIN: Similar test has been done in io_socket_handle_impl_test.cc, I have no idea why that one didn't fail asan. |
That's sad, it's being too smart! Feel free to go back to pointer checks if needed, sorry for the churn. |
|
@danzh2010 io_socket_handle_impl_test.cc doesn't use |
@lizan thanks for looking into this ASAN failure. Yes, it's fixed after ENVOY_ERROR_AGAIN is aligned. Though ENVOY_ERROR_AGAIN is tested in io_socket_handle_impl_test.cc, it's not wrapped in a unique_ptr. unique_ptr::operator*() does some extra verification I think which failed ASAN here. |
|
/assign @alyssawilk |
include/envoy/network/io_handle.h
Outdated
There was a problem hiding this comment.
Can I suggest addressing this with a singleton in the impl? This looks like a pretty gnarly thing to put into the interface.
There was a problem hiding this comment.
That would be nice if feasible, not sure if it is discussed in previous PR.
There was a problem hiding this comment.
Clarifying: this is a pretty gnarly thing anywhere. And it's particularly gnarly IMO to put it in the interface.
There was a problem hiding this comment.
Yeah, it looks ugly. But IoError is usually wrapped by unique_ptr. In order to use singleton, the unique_ptr has to be declared with a customized deleter type which is pretty much similar like here. And I assume IoSocketHandle is not the only impl that needs EAGAIN to be specialized (at least quic will need it too). So we probably will end up to have different impl's to have same EAGAIN singleton. Then I would argue that we should put this singleton in interface. This will end up to be something similar as what we do with special address currently, except that 'err_' always points to a valid address. As a result, member access like errorCode() and errorDetails() don't need to be wrapped by static methods.
I'm also not a big fan of singleton, a const static local variable can do the same. But I feel neither of these can a great advantage over ENVOY_ERROR_AGAIN.
There was a problem hiding this comment.
For the record I don't usually like mutable singletons conceptually in general in all forms (static locals, registries etc). But actually const singletons to me make sense -- just a C++ extension of statically initialized constant tables, string literals, etc. I'd definitely implement it using the static local initializer pattern in the envoy style guide, which cleanly and easily avoids the init-order fiasco.
RE interface vs impl -- a good pattern here is to have a 2-level hierarchy of Interface -> SharedImpl -> platform-specific impl.
There was a problem hiding this comment.
RE custom-deleter: that's fine. Just have it call a free() method on the class, which allows the derived impl to decide what to do.
I think you might as well use singletons for all the known types, and only allocate instances when its some error not covered in the enum, and the captured and allocated string is needed to propagate a usable error message.
There was a problem hiding this comment.
RE custom-deleter: that's fine. Just have it call a free() method on the class, which allows the derived impl to decide what to do.
In interface, only the deleter type needs to be declared which is most like to bevoid(*)(IoError*). And each impl can be provided their own deleter function. Adding IoError::free() isn't that necessary to me.
I think you might as well use singletons for all the known types, and only allocate instances when its some error not covered in the enum, and the captured and allocated string is needed to propagate a usable error message.
The enum might expand and errors other than EAGAIN aren't expected to appear often. Having singletons for each types somewhat defeat the purpose of having enum.
There was a problem hiding this comment.
RE the deleter -- sounds reasonable. If you can sort out something that isn't too horrifying I'm fine whatever you decide :)
RE singletons for all enums? Sure that's fine; just use a singleton for Again and not for the others if you prefer. In either case the enum is still valuable as it lets client code make semantic decisions about what to do that work across operating systems.
mattklein123
left a comment
There was a problem hiding this comment.
Thanks looks good at a high level. I dropped a few comments from a quick skim will look harder once some of the discussion is resolved. Thank you!
/wait
There was a problem hiding this comment.
I don't think this should ever happen in real code, so I don't think it's worth actually mapping this. Can we cover this with asserts or just fix the tests somehow if this is being hit?
There was a problem hiding this comment.
removed this enum value.
include/envoy/network/io_handle.h
Outdated
There was a problem hiding this comment.
nit: doc comments here and below.
include/envoy/network/io_handle.h
Outdated
There was a problem hiding this comment.
Would prefer to not have any magic C++ generated for stuff like this. Can you add an explicit move method or just remove cases where this is needed?
There was a problem hiding this comment.
This is needed almost everywhere a IoHandle interface call returns. So I think a move assignment is cleaner.
source/common/buffer/buffer_impl.cc
Outdated
There was a problem hiding this comment.
This is just "no error", right? Can we add a "no error" constructor variant to IoHandleCallUintResult or maybe some other short cut to hide this at every call site?
There was a problem hiding this comment.
IoHandleCallResult is a template class. It's not straight forward to add a constructor to initialize rc_ to be 0. Instead I defined a IO_CALL_RESULT_NO_ERROR macro there.
…cketHandleImpl. Replace direct call to ::readv() and ::write() with these 2 methods. Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
mattklein123
left a comment
There was a problem hiding this comment.
Thanks, looks great. Few small things.
/wait
include/envoy/api/io_error.h
Outdated
|
|
||
| using IoCallUintResult = IoCallResult<uint64_t>; | ||
|
|
||
| #define IO_CALL_RESULT_NO_ERROR \ |
There was a problem hiding this comment.
nit: can this just be an inline free function? Does it need to be a macro?
| Api::IoError::IoErrorCode IoSocketError::getErrorCode() const { | ||
| switch (errno_) { | ||
| case EAGAIN: | ||
| RELEASE_ASSERT(this == getIoSocketEagainInstance(), |
| inline IoSocketEagain* getIoSocketEagainInstance() { | ||
| static auto* kInstance = new IoSocketEagain(); | ||
| return kInstance; | ||
| } |
There was a problem hiding this comment.
Can't you define in header and implement in cc? Same below.
| Api::IoCallUintResult | ||
| IoSocketHandleImpl::sysCallResultToIoCallResult(const Api::SysCallSizeResult& result) { | ||
| if (result.rc_ >= 0) { | ||
| // Return nullptr as IoError upon success. |
There was a problem hiding this comment.
Can you use the no error free function?
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
mattklein123
left a comment
There was a problem hiding this comment.
Thanks this is great. 2 small nits and then ready to ship!
/wait
|
|
||
| // static. | ||
| IoSocketError* IoSocketError::getIoSocketEagainInstance() { | ||
| static auto* instance = new IoSocketError(EAGAIN); |
There was a problem hiding this comment.
nit: can use CONSTRUCT_ON_FIRST_USE here
There was a problem hiding this comment.
not exactly same with CONSTRUCT_ON_FIRST_USE macro. We want a non-const to be returned to construct a unique_ptr.
There was a problem hiding this comment.
You could just take the address of what is returned, which I think would be better, but it's not a big deal either way.
There was a problem hiding this comment.
do you mean casting from const IoSocketError* to IoSocketError*?
There was a problem hiding this comment.
I mean have it return IoSocketError& and then do &getIoSocketEagainInstance() but I see that we have a const difference, so I guess that won't work.
|
|
||
| std::string IoSocketError::getErrorDetails() const { return ::strerror(errno_); } | ||
|
|
||
| // static. |
There was a problem hiding this comment.
nit: remove comment, same below, we don't typically do this.
Signed-off-by: Dan Zhang <danzh@google.com>
|
🔨 rebuilding |
jmarantz
left a comment
There was a problem hiding this comment.
Looking great. A few minor nits (comments, naming, etc).
| * according to different implementations. | ||
| * If the call succeeds, |err_| is nullptr and |rc_| is valid. Otherwise |err_| | ||
| * can be passed into IoError::getErrorCode() to extract the error. In this | ||
| * case, |rc_| is invalid. |
There was a problem hiding this comment.
given this interface can you add method:
bool ok() const { return err_ == nullptr; }
I had to also read below at usages to understand that rc is really the interface-dependent return value. Can you call it return_value_ instead of rc_ (which sounds like it might be errno or something), and use ReturnValue rather than T as your template param?
There was a problem hiding this comment.
and of course use that method at call-sites rather than directly dereferencing err_.
There was a problem hiding this comment.
given this interface can you add method:
bool ok() const { return err_ == nullptr; }I had to also read below at usages to understand that rc is really the interface-dependent return value. Can you call it return_value_ instead of rc_ (which sounds like it might be errno or something), and use ReturnValue rather than T as your template param?
This renaming will touch even larger scope of files which is not related to socket read/write, since IoError is used in filesystem now. I would prefer do it separately so that this PR doesn't looks confusing.
There was a problem hiding this comment.
Can we add the ok() method now and change usage in files already in this PR, and follow up with the file-system changes later?
There was a problem hiding this comment.
Adding ok() sounds good to me
include/envoy/api/io_error.h
Outdated
| IoErrorPtr err_; | ||
| }; | ||
|
|
||
| using IoCallUintResult = IoCallResult<uint64_t>; |
There was a problem hiding this comment.
How about the name for struct IoCallResult then? Isn't it envoy naming style to use camel case?
There was a problem hiding this comment.
Sorry I meant IoCallUint64Result.
My main point was "Uint" was not clear that you meant 64 bits not 32.
There was a problem hiding this comment.
switched to IoCallUint64Result
| const int rc = ::close(fd_); | ||
| fd_ = -1; | ||
| return Api::IoCallResult<uint64_t>(rc, Api::IoErrorPtr(nullptr, deleteIoError)); | ||
| return Api::IoCallResult<uint64_t>(rc, Api::IoErrorPtr(nullptr, IoSocketError::deleteIoError)); |
There was a problem hiding this comment.
and my main point here is that you defined a nickname but didn't use it here.
There was a problem hiding this comment.
I vaguely remember constructing a unique_ptr using alias SomeTypePtr is not preferred. But since you requested, done.
| FUZZ_ASSERT(::fcntl(pipe_fds[0], F_SETFL, O_NONBLOCK) == 0); | ||
| FUZZ_ASSERT(::fcntl(pipe_fds[1], F_SETFL, O_NONBLOCK) == 0); | ||
| std::string data(max_length, insert_value); | ||
| const int rc = ::write(pipe_fds[1], data.data(), max_length); |
There was a problem hiding this comment.
nit: write() returns ssize_t.
https://linux.die.net/man/2/write
There was a problem hiding this comment.
changed to ssize_t.
|
@jmarantz I'm going to assign over to you. Can you merge when you are happy? I'm fine at this point. :) |
|
@danzh2010 just a few interface nits remaining; any chance we can polish this off and get it in? I think this will help clean up other PRs as well. |
I've been on-call for this week, just get back to this PR. I'll address your comments today or tomorrow. |
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
|
/retest |
|
🔨 rebuilding |
|
PTAL |
jmarantz
left a comment
There was a problem hiding this comment.
sorry -- I wrote this comment yesterday and managed not to hit 'send'.
| return *this; | ||
| } | ||
|
|
||
| bool ok() const { return err_ == nullptr; } |
There was a problem hiding this comment.
add doxygen doc, & also reference ok() as the canonical way to determine if a response succeeded to the comment above, rather than looking at err != nullptr.
I had recommended also in my earlier comment renaming rc_ to make it more meaningful, e.g.return_value. If you are concerned about CL growth changing other references, can you add a TODO and then do a follow-up?
Also, can you rename template name T to ReturnValue?
Once you do these I'll go ahead & merge. Thanks!
Signed-off-by: Dan Zhang <danzh@google.com>
jmarantz
left a comment
There was a problem hiding this comment.
thanks; will merge when CI finishes.
* master: token bucket: several fixes (envoyproxy#6235) config: move logging of full response to trace logging (envoyproxy#6226) mysql_filter: add a warning about compatibility (envoyproxy#6234) upstream: add transport socket failure reason to stream info and log (envoyproxy#6018) IoHandle readv and writev (envoyproxy#6037) Signed-off-by: Michael Puncel <mpuncel@squareup.com>
Add IoHandle::readv() and writev(). Implement them in IoSocketHandleImpl.
And replace direct syscall with call to this interface.
Risk Level: low, mostly refactor
Testing: existing unit and e2e tests
Part of #4546 #2557