-
Notifications
You must be signed in to change notification settings - Fork 137
Add additional async getaddrinfo step to outgoing tcp connections #301
Add additional async getaddrinfo step to outgoing tcp connections #301
Conversation
Signed-off-by: Norbert Heusser <[email protected]>
I've never used Basically something like this (declaration order inverted and extra comments added just for clarity): int UvTcpConnect(...) {
...
assert(!t->closing); /* Note that t->closing is false at the beginning */
...
QUEUE_PUSH(&t->connecting, &r->queue); /* So we know there's a connection attempt in progress,
and we abort it in UvTcpConnectClose() if uvTcpClose() gets
called because the engine is shutting down. */.
...
uvTcpConnectStart(r, address);
}
static int uvTcpConnectStart(struct uvTcpConnect *r, const char *address) {
...
rv = uv_tcp_init(r->t->loop, r->tcp); /* we'll need to close this handle in case of failure,
see uvTcpConnectAbort */
assert(rv == 0);
...
rv = uv_getaddrinfo(r->t->loop, &r->getaddrinfo, &uvGetAddrInfoCb, hostname,
service, &hints);
...
}
static void uvGetAddrInfoCb(uv_getaddrinfo_t *req,
int status,
struct addrinfo *res) {
...
if (t->closing) { /* This basically checks if uvTcpClose() was called, see uv_tcp.c.
Note that uvTcpConnectAbort() has been already called too, by UvTcpConnectClose */
connect->status = RAFT_CANCELED;
return;
}
if (status != 0) { /* The DNS resolution failed, explicitly abort the whole attempt */
assert(status != UV_ECANCELED); /* t->closing would have been true, because
we attempt cancelling in uvTcpAbort() */
connect->status = RAFT_NOCONNECTION;
...
goto err;
}
rv = uv_tcp_connect(&r->connect, r->tcp, (const struct sockaddr *)res->ai_addr
uvTcpConnectUvConnectCb);
if (rv != 0) {
/* UNTESTED: since parsing succeed, this should fail only because of
* lack of system resources */
connect->status = RAFT_NOCONNECTION;
goto err;
}
return;
err:
uvTcpConnectAbort(connect);
}
static void uvTcpConnectUvConnectCb(struct uv_connect_s *req, int status) {
... /* No change should be needed here */
}
static void uvTcpConnectUvWriteCb(struct uv_write_s *write, int status) {
... /* No change should be needed here */
}
static void uvTcpConnectFinish(struct uvTcpConnect *connect) {
...
/* The only change needed here should be freeing the addr structure, since we
don't need it anymore. Note that this function will be always invoked,
either by uvTcpConnectUvWriteCb or by uvTcpConnectUvCloseCb, so no leaking will happen. */
uv_freeaddrinfo(connect->getaddrinfo.addrinfo);
...
}
static void uvTcpConnectAbort(struct uvTcpConnect *connect)
{
QUEUE_REMOVE(&connect->queue);
QUEUE_PUSH(&connect->t->aborting, &connect->queue);
uv_cancel((struct uv_req_s *)&connect->getaddrinfo); /* Best effort, it doesn't matter
if it actually cancels or not, in any case the uvGetAddrInfoCb callback will
be invoked if the request is still in-flight */
uv_close((struct uv_handle_s *)connect->tcp, uvTcpConnectUvCloseCb);
} |
Just to be extra clear: the call to |
Not canceling the uv_write is totally fine as the uv_write is dependent on the same tcp handle as the tcp_connect. In this case it is fine and totally safe to simply close the tcp handle and all pending or currently processed on this handle will be stopped before the callback give to the uv_close will be invoked. This hold for any of-of-band invocation from a thread outside the libuv loop as well.
So if we simply invoke the uv_close on the tcp handle right after we have tried to cancel the getaddrinfo request the libuv will immediately close the tcp handle (as it is not used for anything up to now) and invoke the give cb. And this callback will invoke the uvTcpConnectFinish, which will cleanup all the memory structures. But depending on the current execution stack of the getaddrinfo request in the libuv our getaddrinfo callback was accessing freed memory. Additionally the finish cb will try to free memory (the getaddrinfo result), which may not been allocated. |
So the uv_cancel is not really necessary to stop the connect, but we need to make sure the structures used in uvGetAddrInfoCb are still valid until the callback execution has finished. |
Ah, you're right, I now see what you mean. What about adding a flag that tracks whether the getaddrinfo request is in-flight or not, basically along the lines of what you had suggested at the very beginning I believe. Same code as above with a little modification: struct uvTcpConnect {
bool resolving; /* will be initialized to false */
}
static int uvTcpConnectStart(struct uvTcpConnect *r, const char *address) {
...
rv = uv_tcp_init(r->t->loop, r->tcp); /* we'll need to close this handle in case of failure,
see uvTcpConnectAbort */
assert(rv == 0);
...
rv = uv_getaddrinfo(r->t->loop, &r->getaddrinfo, &uvGetAddrInfoCb, hostname,
service, &hints);
if (rv != 0) {...}
r->resolving = true; /* this means that the uv_getaddrinfo call is in progress */
...
}
static void uvGetAddrInfoCb(uv_getaddrinfo_t *req,
int status,
struct addrinfo *res) {
...
connect->resolving = false; /* run this before anything else */
if (t->closing) { /* This basically checks if uvTcpClose() was called, see uv_tcp.c.
Note that uvTcpConnectAbort() has been already called too, by UvTcpConnectClose,
but uv_close() was not called yet because the connect->resolving flag was true */
connect->status = RAFT_CANCELED;
/* call uv_close() now that the getaddrinfo request has completed */
uv_close((struct uv_handle_s *)connect->tcp, uvTcpConnectUvCloseCb);
return;
}
... /* the rest is the same as above */
}
static void uvTcpConnectAbort(struct uvTcpConnect *connect)
{
QUEUE_REMOVE(&connect->queue);
QUEUE_PUSH(&connect->t->aborting, &connect->queue);
uv_cancel((struct uv_req_s *)&connect->getaddrinfo); /* Best effort, it doesn't matter
if it actually cancels or not, in any case the uvGetAddrInfoCb callback will
be invoked if the request is still in-flight */
/* Only call uv_close() if there's no getaddrinfo request in flight. Otherwise, we
delay it until it is completed. */
if (!connect->resolving) {
uv_close((struct uv_handle_s *)connect->tcp, uvTcpConnectUvCloseCb);
}
} |
The solution you proposed was one I though about as well. But decided not to use it for several reasons:
This was the reason for the solution I proposed. As the current solution in the PR is based on the guarantees given by the libuv. |
Is there a way to retrigger the CLA check as I filed the contributor agreement now ? |
It failed again, possibly because there is some delay between you signing the agreement and the system actually knowing that you signed. I'll keep an eye on it if the problem would persist. |
This is actually a very common design throughout this raft library (and libuv itself). Basically the two callbacks that we have now can be aborted at the same time for the reason you mentioned (because they both depend on the tcp handle). This third callback we are introducing is an independent one that introduces an additional ordering requirement. Using the flag we make sure that ordering is preserved: first we let the uv_getaddrinfo call to finish and the uvGetAddrInfoCb callback to be triggered and only then we proceed with uv_close(). We already do this kind of callback ordering in various places.
The uv_close() call due to a failure of uvTcpConnectStart is pretty trivial and it's basically just a direct rollback. The role of uvTcpConnectAbort() is to trigger an orderly shutdown. The fact that we don't call uv_close() right away actually makes things very explicit in my view: there is a shutdown/abort order to honor and at this time we can't yet call uv_close().
This can't happen because of the single-threaded event-driven design of raft/libuv. There is just one thread, and any callback that is invoked by libuv is guaranteed to run from start to end without any other libuv or user callback code being executed, regardless of the OS scheduling. It's very different from multi-threaded designs were I totally agree that flags like this are usually a bad idea, or should at least be protected with very careful mutexes. There are no race conditions of that kind in event-loop based designs. In this case if libuv is right in the execution of uvGetAddrInfoCb, in particular starting the t->closing section that you mention, then there is no way that the "main thread" executes uvTcpConnectAbort in parallel. There is only one main thread, and it's executing uvGetAddrInfoCb, so nothing else can't execute until the uvGetAddrInfoCb function has returned. Again, this kind of design pattern is very common in both libraft and libuv, and I believe very solid. I'd recommend to stick with the |
Got it. As the libraft is totally running single threaded and using the same thread to invoke the uv_run periodically there is no concurrency at all. So will I will replace the uv_check with the flag you prefer and update the PR. |
Exactly! Thank you for bearing with me, that's appreciated :) |
b44f904
to
cb6ebf4
Compare
Signed-off-by: Norbert Heusser <[email protected]>
cb6ebf4
to
c75b969
Compare
Appreciate it as well. Always interested to find a good solution, which fit well into the existing code base and keep the code consistent, stable and maintainable. |
int rv; | ||
|
||
rv = uvIpParse(address, &addr); | ||
connect->resolving = | ||
false; /* Indicate we are in the name resolving phase */ |
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.
Probably better to write "Indicate that we are done with the resolving phase"
Looks good to me, thanks again. Just in case you are interested, since you mentioned that "there is no concurrency at all", there is actually concurrency in this kind of single-threaded event-based systems. However there is no parallelism, which is different. This is a nice talk about it: https://www.youtube.com/watch?v=tIrVLcUq4xE (it's not really Go-specific). |
@NorbertHeusser Is |
Yes. This is the e-mail adress I used for the launchpad account as well. |
service_ptr = strchr(address, colon); | ||
} | ||
if (!service_ptr || *service_ptr == 0 || *(++service_ptr) == 0) { | ||
service_ptr = "8080"; |
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.
What's special about 8080
? Maybe document this somewhere?
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 a question for @freeekanayaka as I kept it fully compatible with the existing uvIpParse (which was used before). And he introduced this default with the first version of the file visible to me in the repo.
struct uv_connect_s connect; /* TCP connection request */ | ||
struct uv_write_s write; /* TCP handshake request */ | ||
int status; /* Returned to the request callback */ | ||
bool resolving; /* Indicate name resolving in progress */ |
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.
Aligning with the comments above would be a bit more aesthetically pleasing.
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.
As the project has a .clang-format file in it's root folder I ran a clang-format on all files I touched. My expectation was all format will be fine after that.
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.
Interesting: Using the clang-format option AlignTrailingComments: true seems not to work on comment blocks, but just on single line comments //
Not sure, if it you are willing to use // for single line comments, which would solve this problem based on clang-format. But some people don't like using the c++ originated sigle line comment in C file (even knowing this is allowed since C99).
/* Initialize the handshake buffer. */ | ||
rv = uvTcpEncodeHandshake(t->id, t->address, &r->handshake); | ||
if (rv != 0) { | ||
assert(rv == RAFT_NOMEM); | ||
ErrMsgOom(r->t->transport->errmsg); | ||
ErrMsgOom(t->transport->errmsg); | ||
goto err; |
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.
handshake.base
is freed after err
, but never allocated. It looks safe right now, but it's not very pretty and can lead to an erroneous free in the future.
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 initialise the pointer to NULL in the beginning of the function. As the raft_free (like the posix free is safe to be invoked on a NULL pointer) it safe from my perspective.
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.
It's just the default port if none is specified. We should indeed document it, or perhaps return an error instead.
As first step to allow usage of hostnames instead of static IPs I added the required additional async getaddrinfo step to the outgoing tcp connection to the other cluster members.
Unfortunately the libuv async getaddrinfo is not connected to the TCP handle. As a consequence abort the connect got a little bit more complicated as the getaddrinfo may be in progress and cannot be stopped. In this case the data structures need to be valid until the getaddrinfo callback is finished.
In the current first implementation the getaddrinfo is restricted to IPv4 addresses in a string representation as before. This way it should behave excactly as before.