Skip to content

IoHandle readv and writev#6037

Merged
mattklein123 merged 22 commits intoenvoyproxy:masterfrom
danzh2010:iohandlerw
Mar 9, 2019
Merged

IoHandle readv and writev#6037
mattklein123 merged 22 commits intoenvoyproxy:masterfrom
danzh2010:iohandlerw

Conversation

@danzh2010
Copy link
Contributor

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

@danzh2010 danzh2010 requested a review from lizan as a code owner February 22, 2019 22:23
@danzh2010
Copy link
Contributor Author

/assign @sbelair2 @mattklein123

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

TEST_F(OwnedImplTest, Write) {
would have to use a mocked IoSocketHandle though

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. owned_impl_test.cc, which doesn't make any assertions on the arguments passed to readv/writev
  2. 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. owned_impl_test.cc, which doesn't make any assertions on the arguments passed to readv/writev
  2. 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly to above, would it be possible to call writev directly here?

@danzh2010
Copy link
Contributor Author

ratelimit_integration_test failed because dereferencing ENVOY_ERROR_AGAIN:
[ RUN ] IpVersionsClientType/RatelimitIntegrationTest.Ok/0
/usr/lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/unique_ptr.h:323:9: runtime error: reference binding to misaligned address 0x000000000001 for type 'Envoy::Network::IoError', which requires 8 byte alignment
0x000000000001: note: pointer points here

#0 0xa4bfd85 in std::unique_ptr<Envoy::Network::IoError, void ()(Envoy::Network::IoError)>::operator*() const /usr/lib/gcc/x86_64-linux-gnu/7.4.0/../../../../include/c++/7.4.0/bits/unique_ptr.h:323:2
#1 0xa4bd0b9 in Envoy::Network::RawBufferSocket::doRead(Envoy::Buffer::Instance&) /proc/self/cwd/source/common/network/raw_buffer_socket.cc:38:42
#2 0x983eede in Envoy::Network::ConnectionImpl::onReadReady() /proc/self/cwd/source/common/network/connection_impl.cc:482:40

Similar test has been done in io_socket_handle_impl_test.cc, I have no idea why that one didn't fail asan.

@mattklein123
Copy link
Member

mattklein123 commented Feb 26, 2019

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.

@lizan
Copy link
Member

lizan commented Feb 27, 2019

@danzh2010 io_socket_handle_impl_test.cc doesn't use ENVOY_ERROR_AGAIN which is an unaligned address, you may need to change ENVOY_ERROR_AGAIN to some aligned address. perhaps alignof(IoError)? Though I'm not sure whether ASAN will raise error on that address.

@danzh2010
Copy link
Contributor Author

@danzh2010 io_socket_handle_impl_test.cc doesn't use ENVOY_ERROR_AGAIN which is an unaligned address, you may need to change ENVOY_ERROR_AGAIN to some aligned address. perhaps alignof(IoError)? Though I'm not sure whether ASAN will raise error on that address.

@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.

@danzh2010
Copy link
Contributor Author

/assign @alyssawilk

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I suggest addressing this with a singleton in the impl? This looks like a pretty gnarly thing to put into the interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice if feasible, not sure if it is discussed in previous PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarifying: this is a pretty gnarly thing anywhere. And it's particularly gnarly IMO to put it in the interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@jmarantz jmarantz Feb 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 be void(*)(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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member

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 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this enum value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: doc comments here and below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed almost everywhere a IoHandle interface call returns. So I think a move assignment is cleaner.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks great. Few small things.

/wait


using IoCallUintResult = IoCallResult<uint64_t>;

#define IO_CALL_RESULT_NO_ERROR \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can this just be an inline free function? Does it need to be a macro?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Api::IoError::IoErrorCode IoSocketError::getErrorCode() const {
switch (errno_) {
case EAGAIN:
RELEASE_ASSERT(this == getIoSocketEagainInstance(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for normal ASSERT

inline IoSocketEagain* getIoSocketEagainInstance() {
static auto* kInstance = new IoSocketEagain();
return kInstance;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the no error free function?

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks this is great. 2 small nits and then ready to ship!

/wait


// static.
IoSocketError* IoSocketError::getIoSocketEagainInstance() {
static auto* instance = new IoSocketError(EAGAIN);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can use CONSTRUCT_ON_FIRST_USE here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not exactly same with CONSTRUCT_ON_FIRST_USE macro. We want a non-const to be returned to construct a unique_ptr.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean casting from const IoSocketError* to IoSocketError*?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove comment, same below, we don't typically do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Signed-off-by: Dan Zhang <danzh@google.com>
mattklein123
mattklein123 previously approved these changes Mar 6, 2019
Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

/retest

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: asan (failed build)

🐱

Caused by: a #6037 (review) was submitted by @mattklein123.

see: more, trace.

Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and of course use that method at call-sites rather than directly dereferencing err_.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding ok() sounds good to me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

IoErrorPtr err_;
};

using IoCallUintResult = IoCallResult<uint64_t>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: IOCallUint64Result

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about the name for struct IoCallResult then? Isn't it envoy naming style to use camel case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I meant IoCallUint64Result.

My main point was "Uint" was not clear that you meant 64 bits not 32.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOCallUint64Result

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and my main point here is that you defined a nickname but didn't use it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: write() returns ssize_t.
https://linux.die.net/man/2/write

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to ssize_t.

@mattklein123
Copy link
Member

@jmarantz I'm going to assign over to you. Can you merge when you are happy? I'm fine at this point. :)

@jmarantz
Copy link
Contributor

jmarantz commented Mar 7, 2019

@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.

@danzh2010
Copy link
Contributor Author

@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>
@danzh2010
Copy link
Contributor Author

/retest

@repokitteh-read-only
Copy link

🔨 rebuilding ci/circleci: asan (failed build)

🐱

Caused by: a #6037 (comment) was created by @danzh2010.

see: more, trace.

@danzh2010
Copy link
Contributor Author

PTAL

Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry -- I wrote this comment yesterday and managed not to hit 'send'.

return *this;
}

bool ok() const { return err_ == nullptr; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Signed-off-by: Dan Zhang <danzh@google.com>
Copy link
Contributor

@jmarantz jmarantz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks; will merge when CI finishes.

@mattklein123 mattklein123 merged commit 083e6df into envoyproxy:master Mar 9, 2019
mpuncel added a commit to mpuncel/envoy that referenced this pull request Mar 11, 2019
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants