Discussion:
curl_multi_timeout and the multi_socket API
Henrik Holst via curl-library
2021-04-01 23:32:47 UTC
Permalink
Dear libcurl devs,

in the docs the curl_multi_timeout() function has this note:

"An application that uses the multi_socket API SHOULD NOT use this
function, but SHOULD instead use curl_multi_setopt and its
CURLMOPT_TIMERFUNCTION option for proper and desired behavior."

However as I see there are instances where curl_multi_timeout() have an
role to play even in the muli_socket API (unless of course there are some
internal class if one uses this function, I have not gone through the
sources that deep) when the event handler is used for not only libcurl
events.

If the event handler is only handling libcurl events then all is just fine
with the timer callback, but if other sockets or file descriptors are
handled by the same event handler and one ends up in a situation where
those other sockets/descriptors are very busy then it's quite possible that
the event handler will not return back with a timeout for quite a while,
and since libcurl can only update it's internal state and call the timer
callback if one calls curl_multi_socket_action() then one either have to
call curl_multi_socket_action() after each event handler return (and I
assume here without checking that curl_multi_socket_action() is a much
heavier function to call than curl_multi_timeout(), but I could of course
be wrong), or one have to track the time taken during the event handler
call (which is hard on e.g Linux since select/poll/epoll doesn't indicate
how long it really waited).

What I mean is that the naive implementation of the multi socket API that
works 100% of the time if curl is the only one generating events and 99% of
the time if you have other external events but not much traffic on them:

for (;;)
int ret = poll (fds, nfds, timeout);

if (ret == 0) {/* timeout */
curl_multi_socket_action (curlm, CURL_SOCKET_TIMEOUT, 0,
&running_handles);
} else if (ret != -1) { /* events */
if (fds[0].revents != 0)
curl_multi_socket_action (curlm, fds[0].fd, fds[0].revents,
&running_handles);
else if (fds[1]).revents != 0)
...
}
}

Yes I know that we cannot put fds[0].revents directly into curl but this
was just a small example of the typical event structure from someone using
poll().

Now if curl is the only one generating events here the timer function
callback will be called by curl and the "timeout" variable will be set
appropriate. However if we add some other socket here on say fds[1] above
that is extremely high in volume then it's quite possible that poll() will
very seldom return 0 even when the timeout from curl is very low leading to
a situation where curl will "hang", typically I have seen this directly
after a call to curl_multi_add_handle() where curl tends to first call the
timer function callback with a timeout of 0 and then after the first call
to curl_multi_socket_action() the timer function callback is called again
with a timeout of 1 but that 1 can be enough for a very highly stressed
other socket prohibiting poll from timing out even for 1ms.

Therefore I have noticed that the following logic and where we don't set
any timer function callback at all, works much better:

for (;;) {
long timeout;
curl_multi_timeout (curlm, &timeout);

if (timeout == 0)
curl_multi_socket_action (curlm, CURL_SOCKET_TIMEOUT, 0,
&running_handles);

int ret = poll (fds, nfds, timeout);

if (ret > 0) { /* events */
if (fds[0].revents != 0)
curl_multi_socket_action (curlm, fds[0].fd, fds[0].revents,
&running_handles);
else if (fds[1]).revents != 0)
...
}
}

So unless there is some potential lethal overhead in curl_multi_timeout() I
actually find this to work much better than the timer function callback.

Regards, Henrik Holst
Daniel Stenberg via curl-library
2021-04-02 21:05:44 UTC
Permalink
Post by Henrik Holst via curl-library
for (;;)
int ret = poll (fds, nfds, timeout);
if (ret == 0) {/* timeout */
curl_multi_socket_action (curlm, CURL_SOCKET_TIMEOUT, 0,
&running_handles);
} else if (ret != -1) { /* events */
if (fds[0].revents != 0)
curl_multi_socket_action (curlm, fds[0].fd, fds[0].revents,
&running_handles);
else if (fds[1]).revents != 0)
...
}
}
This is a typical example of an event loop that should rather use
curl_multi_perform() or perhaps even just curl_multi_poll(). And yes, for such
an event loop you want curl_multi_timeout (at least unless you use
curl_multi_poll).

If you use poll() then the multi socket API is probably the wrong choice. The
multi socket API is for event-based handling.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/eti
Henrik Holst via curl-library
2021-04-02 21:08:46 UTC
Permalink
Post by Daniel Stenberg via curl-library
Post by Henrik Holst via curl-library
for (;;)
int ret = poll (fds, nfds, timeout);
if (ret == 0) {/* timeout */
curl_multi_socket_action (curlm, CURL_SOCKET_TIMEOUT, 0,
&running_handles);
} else if (ret != -1) { /* events */
if (fds[0].revents != 0)
curl_multi_socket_action (curlm, fds[0].fd, fds[0].revents,
&running_handles);
else if (fds[1]).revents != 0)
...
}
}
This is a typical example of an event loop that should rather use
curl_multi_perform() or perhaps even just curl_multi_poll(). And yes, for such
an event loop you want curl_multi_timeout (at least unless you use
curl_multi_poll).
If you use poll() then the multi socket API is probably the wrong choice. The
multi socket API is for event-based handling.
Okey, but the very same thing happens with epoll, or are we only defining
event-based handling as say libevent, libev and libuv?

/HH
Daniel Stenberg via curl-library
2021-04-02 21:12:31 UTC
Permalink
Post by Henrik Holst via curl-library
Post by Daniel Stenberg via curl-library
If you use poll() then the multi socket API is probably the wrong choice.
The multi socket API is for event-based handling.
Okey, but the very same thing happens with epoll, or are we only defining
event-based handling as say libevent, libev and libuv?
What "very same thing" is it that happens? What do you do with the timeout
value curl_multi_timeout returns in an event-based system?
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-li
Henrik Holst via curl-library
2021-04-02 21:38:13 UTC
Permalink
Post by Daniel Stenberg via curl-library
Post by Henrik Holst via curl-library
Post by Daniel Stenberg via curl-library
If you use poll() then the multi socket API is probably the wrong
choice.
Post by Henrik Holst via curl-library
Post by Daniel Stenberg via curl-library
The multi socket API is for event-based handling.
Okey, but the very same thing happens with epoll, or are we only
defining
Post by Henrik Holst via curl-library
event-based handling as say libevent, libev and libuv?
What "very same thing" is it that happens?
For epoll, the "very same thing" is that if a non-curl socket is added to
the epoll efd then epoll_wait() would just like poll() enter a situation
where it might not return 0 to indicate timeout.
Post by Daniel Stenberg via curl-library
What do you do with the timeout value curl_multi_timeout returns in an
event-based system?
for epoll I would pass that on as the timeout argument for epoll_wait().

/HH
Post by Daniel Stenberg via curl-library
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
Daniel Stenberg via curl-library
2021-04-02 22:01:58 UTC
Permalink
Post by Henrik Holst via curl-library
Post by Daniel Stenberg via curl-library
What "very same thing" is it that happens?
For epoll, the "very same thing" is that if a non-curl socket is added to
the epoll efd then epoll_wait() would just like poll() enter a situation
where it might not return 0 to indicate timeout.
libcurl doesn't need you to invoke it because of a timeout. It gives you the
timeout for when to call it again *at the latest*. If you call it before that,
perhaps because of activity on a socket, that's fine and you'll see that it
will then often update the timeout accordingly.

If you miss the timeout moment and instead call libcurl a little late, libcurl
will deal with the timeout actions then instead and all you loose is accuracy
in things that are time based (timeouts, speed limits etc).
Post by Henrik Holst via curl-library
for epoll I would pass that on as the timeout argument for epoll_wait().
But in that case there's really no difference compared to receiving that
value with the timeout callback as then you'd just use the most recently set
timeout callback value in the epoll_wait() call. (You just have to deduct the
passed time in case you loop without the timeout having been updated etc)

This said, curl_multi_timeout() will work. It's just not intended for
socket-based event loops.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: http
Henrik Holst via curl-library
2021-04-02 22:16:09 UTC
Permalink
Post by Daniel Stenberg via curl-library
Post by Henrik Holst via curl-library
Post by Daniel Stenberg via curl-library
What "very same thing" is it that happens?
For epoll, the "very same thing" is that if a non-curl socket is added
to
Post by Henrik Holst via curl-library
the epoll efd then epoll_wait() would just like poll() enter a situation
where it might not return 0 to indicate timeout.
libcurl doesn't need you to invoke it because of a timeout. It gives you the
timeout for when to call it again *at the latest*. If you call it before that,
perhaps because of activity on a socket, that's fine and you'll see that it
will then often update the timeout accordingly.
I know, but that requires you to keep track of the time remaining yourself.
My point was that the naive implementation would be to call curl only on
read/write/error events from the event handler (epoll/poll in this case)
and then also when said event handler returns due to timeout but if the
event handler handles other events besides curl then you might end up in a
situation where you will never receive any event at all and also no timeout
event at all.

I'm not saying that there is something wrong with curl, just that the docs
as written could lead to some very naive implementation that works just
fine as long as it only handles curl events but as the application is
expanded all of the sudden curl seams to just die which is then due to the
naive implementation and not curl itself.
Post by Daniel Stenberg via curl-library
If you miss the timeout moment and instead call libcurl a little late, libcurl
will deal with the timeout actions then instead and all you loose is accuracy
in things that are time based (timeouts, speed limits etc).
Post by Henrik Holst via curl-library
for epoll I would pass that on as the timeout argument for epoll_wait().
But in that case there's really no difference compared to receiving that
value with the timeout callback as then you'd just use the most recently set
timeout callback value in the epoll_wait() call. (You just have to deduct the
passed time in case you loop without the timeout having been updated etc)
the difference is that the callback might never be called due to
epoll_wait() never returning with events for curl nor returning on timeout
(since some other socket is having events so often that epoill never times
out). So you then either have to keep track of the elapsed time yourself
(which of course isn't that hard, but calling curl_multi_timeout() before
each call to e.g epoll is way easier) or you will have to call
curl_multi_socket_action() each time epoll returns which I guess would be
quite some overhead?

I'm not really arguing anything here more than pointing out that
curl_multi_timeout() have it's place even in the multi socket API :)

/HH
Post by Daniel Stenberg via curl-library
This said, curl_multi_timeout() will work. It's just not intended for
socket-based event loops.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
Daniel Stenberg via curl-library
2021-04-02 22:25:22 UTC
Permalink
Post by Henrik Holst via curl-library
I'm not really arguing anything here more than pointing out that
curl_multi_timeout() have it's place even in the multi socket API :)
Clearly it can be useful there as well. At least when using epoll(-like) APIs,
which in my eyes is a bit of a special beast in event-based handling.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-li
Fulup Ar Foll via curl-library
2021-04-03 08:40:36 UTC
Permalink
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Henrik Holst via curl-library
2021-04-03 14:08:59 UTC
Permalink
Henrik,
I wrote an example using either sytemd or libuv main loop, it might
eventually help you https://github.com/fulup-bzh/libcurl-mainloop
Thanks, I can see that you use the timer functionatilty in libuv and
systemd to avoid the problems that I'm talking about. The main question
then is if a call to curl_multi_timeout() before each call to epoll_wait()
/ poll() / select() is more or less overhead than using timers.

/HH
Fulup
Post by Daniel Stenberg via curl-library
Post by Henrik Holst via curl-library
for (;;)
int ret = poll (fds, nfds, timeout);
if (ret == 0) {/* timeout */
curl_multi_socket_action (curlm, CURL_SOCKET_TIMEOUT, 0,
&running_handles);
} else if (ret != -1) { /* events */
if (fds[0].revents != 0)
curl_multi_socket_action (curlm, fds[0].fd, fds[0].revents,
&running_handles);
else if (fds[1]).revents != 0)
...
}
}
This is a typical example of an event loop that should rather use
curl_multi_perform() or perhaps even just curl_multi_poll(). And yes, for such
an event loop you want curl_multi_timeout (at least unless you use
curl_multi_poll).
If you use poll() then the multi socket API is probably the wrong choice. The
multi socket API is for event-based handling.
Okey, but the very same thing happens with epoll, or are we only defining
event-based handling as say libevent, libev and libuv?
/HH
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Fulup Ar Foll via curl-library
2021-04-03 16:41:15 UTC
Permalink
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Henrik Holst via curl-library
2021-04-03 17:02:18 UTC
Permalink
Henrik,
Curl timeout and mainloop timer as in my sample with libuv, epool, ... are
completely independent. Curl handle its own timer to remind you when you're
supose to do some action. But as soon transfert start you only handle
communication go through the main event loop.
In order to use epool, you only need to map what I did for livuv, nothing
more nothing less. You only need to write the 'epoll' version of
https://github.com/fulup-bzh/libcurl-mainloop/blob/main/glue-libuv.c and
you should only call one curl_multi_timeout()
As I wrote earlier, usually when you call curl_multi_add_handle(), the
Timer Function CB is called once by curl with timeout set to 0. Then when
you call curl_multi_socket_action() with CURL_SOCKET_TIMEOUT the Timer
Function CB is usually called once more but this time with a timeout set to
1. If you do not call curl_multi_socket_action() again with
CURL_SOCKET_TIMEOUT after 1ms then curl stalls indefinitely since there
will be no read or write events triggering the event handler to return
events, in fact you won't even get a call to your Socket Function CB before
you handle that first 1ms timeout.

Now I might have misread what you wrote, but some external timer is still
needed so the question at hand is if having such a timer is more or less
complexity/overhead than simply calling curl_multi_timeout().

/HH


Fulup
Henrik,
I wrote an example using either sytemd or libuv main loop, it might
eventually help you https://github.com/fulup-bzh/libcurl-mainloop
Thanks, I can see that you use the timer functionatilty in libuv and
systemd to avoid the problems that I'm talking about. The main question
then is if a call to curl_multi_timeout() before each call to epoll_wait()
/ poll() / select() is more or less overhead than using timers.
/HH
Fulup
Post by Daniel Stenberg via curl-library
Post by Henrik Holst via curl-library
for (;;)
int ret = poll (fds, nfds, timeout);
if (ret == 0) {/* timeout */
curl_multi_socket_action (curlm, CURL_SOCKET_TIMEOUT, 0,
&running_handles);
} else if (ret != -1) { /* events */
if (fds[0].revents != 0)
curl_multi_socket_action (curlm, fds[0].fd, fds[0].revents,
&running_handles);
else if (fds[1]).revents != 0)
...
}
}
This is a typical example of an event loop that should rather use
curl_multi_perform() or perhaps even just curl_multi_poll(). And yes, for such
an event loop you want curl_multi_timeout (at least unless you use
curl_multi_poll).
If you use poll() then the multi socket API is probably the wrong choice. The
multi socket API is for event-based handling.
Okey, but the very same thing happens with epoll, or are we only defining
event-based handling as say libevent, libev and libuv?
/HH
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Fulup Ar Foll via curl-library
2021-04-03 20:56:29 UTC
Permalink
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Henrik Holst via curl-library
2021-04-03 21:13:05 UTC
Permalink
Henrik,
I posted this sample on github because I lost too many hours searching for
curl logic in the documentation. You have to respect curl_timeout,
nevertheless if your mainloop works well, it should be call only once per
download. In my case I do not wait 1ms before initial call and everything
works perfectly well.
I was not speaking about waiting 1ms before the initial call, it was just
an observation that the timer function callback was very often being called
with timeout=0, timeout=1, timeout=-1 and that timeout=1 was the cause of
my initial troubles since calling epoll_wait with 1ms timeout made it never
return 0 to indicate timeout (since some other sockets that I had in the
same epoll had recv traffic more often than that). Relying on epoll_wait()
returning 0 works flawless if one only have curl sockets in the epoll so it
was faulty logic when the application was expanded beyond handling only
curl requests.

Anyway I've redone the logic anyway to minimize the overhead penalty of
calling curl_multi_timeout() on each iteration of the main event loop and
now actually uses the timer callback but instead of relying on epoll_wait()
returning 0 I calculate when the timeout have timed out against the
wallclock (clock_gettime() + timeout from the callback), and check that
instead which I of course should have done from the beginning. That way the
overhead is minimized (just a call to clock_gettime() wich for this
specific implementation is overhead that I can live with) and there is no
need for any external timers or event libs, just bog standard epoll.

/HH
Fulup Ar Foll via curl-library
2021-04-03 22:06:30 UTC
Permalink
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Henrik Holst via curl-library
2021-04-03 22:43:26 UTC
Permalink
I'm not sure to understand your logic. If you have to call
curl_multi_timeout() at each iteration of your mainloop your logic should
be wrong.
I never said that I had to, just that one could (in order to avoid having a
timer, aka if the latency requirement is not that high then logic is easier
and loc is smaller by just calling curl_multi_timeout() on each iteration).
I also mix curl-socket with multiple other classes of sockets and do no
call call curl_multi_timeout() at each iteration.
In my sample the eventloop call 'glueOnSocketCB' as soon as a curlsocket
is readable. 'glueOnSocketCB' map mainloop event-names to curl-events
name before calling 'httpOnSocketCB' that finally call
curl_multi_socket_action to do the job. If you do so, you do not have to
call curl_multi_socket_action. The secret is to map your main loop
event-name to curl-names.
In my code 'glueOnSocketCB' is attached to mainloop within glueSetSocketCB
that is called from multiSetSockCB. This last one is call from curl with "
curl_multi_setopt(httpPool->multi, CURLMOPT_SOCKETFUNCTION,
multiSetSockCB);"
For the timer you do not need an external lib, you may use timerfd and
epool is just as find as any other mainloop. In my case I use systemd
mainloop, because this what the rest of the application uses. I did libuv
to make sure my code was independent of the mainloop, in case app
developper would like to change in the future. I did not port for
epool/timerfd, but I'm sure that it would not take more that few hours.
yes you are correct that timerfd would work fine here, don't know why I
never thought of timerfd.

/HH
Henrik,
I posted this sample on github because I lost too many hours searching
for curl logic in the documentation. You have to respect curl_timeout,
nevertheless if your mainloop works well, it should be call only once per
download. In my case I do not wait 1ms before initial call and everything
works perfectly well.
I was not speaking about waiting 1ms before the initial call, it was just
an observation that the timer function callback was very often being called
with timeout=0, timeout=1, timeout=-1 and that timeout=1 was the cause of
my initial troubles since calling epoll_wait with 1ms timeout made it never
return 0 to indicate timeout (since some other sockets that I had in the
same epoll had recv traffic more often than that). Relying on epoll_wait()
returning 0 works flawless if one only have curl sockets in the epoll so it
was faulty logic when the application was expanded beyond handling only
curl requests.
Anyway I've redone the logic anyway to minimize the overhead penalty of
calling curl_multi_timeout() on each iteration of the main event loop and
now actually uses the timer callback but instead of relying on epoll_wait()
returning 0 I calculate when the timeout have timed out against the
wallclock (clock_gettime() + timeout from the callback), and check that
instead which I of course should have done from the beginning. That way the
overhead is minimized (just a call to clock_gettime() wich for this
specific implementation is overhead that I can live with) and there is no
need for any external timers or event libs, just bog standard epoll.
/HH
Daniel Stenberg via curl-library
2021-04-04 10:28:53 UTC
Permalink
On Sat, 3 Apr 2021, Henrik Holst wrote:

(Let me just add some meat to this and explain this described behavior.)
As I wrote earlier, usually when you call curl_multi_add_handle(), the Timer
Function CB is called once by curl with timeout set to 0. Then when you call
curl_multi_socket_action() with CURL_SOCKET_TIMEOUT the Timer Function CB is
usually called once more but this time with a timeout set to 1.
This description matches how libcurl *did* behave. More recent versions don't
need to poll the name resolve results from the threaded resolver so the
timeout pattern will be slightly different.

The application should of course still be prepared to deal with the timeouts
libcurl asks for.
If you do not call curl_multi_socket_action() again with CURL_SOCKET_TIMEOUT
after 1ms then curl stalls indefinitely since there will be no read or write
events triggering the event handler to return events,
... because in that scenario libcurl waits to get called so that it can poll
if the name resolve is done, and if you don't call libcurl then it won't
notice that and it can't continue to perform its transfer until that's done!
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https
Henrik Holst via curl-library
2021-04-04 14:34:17 UTC
Permalink
Post by Daniel Stenberg via curl-library
(Let me just add some meat to this and explain this described behavior.)
Post by Henrik Holst via curl-library
As I wrote earlier, usually when you call curl_multi_add_handle(), the
Timer
Post by Henrik Holst via curl-library
Function CB is called once by curl with timeout set to 0. Then when you
call
Post by Henrik Holst via curl-library
curl_multi_socket_action() with CURL_SOCKET_TIMEOUT the Timer Function
CB is
Post by Henrik Holst via curl-library
usually called once more but this time with a timeout set to 1.
This description matches how libcurl *did* behave. More recent versions don't
need to poll the name resolve results from the threaded resolver so the
timeout pattern will be slightly different.
Ah, yeah I'm on 7.68 and 7.58 (Ubuntu 20.04 and Ubuntu 18.04).
Post by Daniel Stenberg via curl-library
The application should of course still be prepared to deal with the timeouts
libcurl asks for.
Post by Henrik Holst via curl-library
If you do not call curl_multi_socket_action() again with
CURL_SOCKET_TIMEOUT
Post by Henrik Holst via curl-library
after 1ms then curl stalls indefinitely since there will be no read or
write
Post by Henrik Holst via curl-library
events triggering the event handler to return events,
... because in that scenario libcurl waits to get called so that it can poll
if the name resolve is done, and if you don't call libcurl then it won't
notice that and it can't continue to perform its transfer until that's done!
Exactly, which is why my stupid naive implementation worked fine in 100% of
all cases until I started to put other sockets into my event handler. I
hope no one has seen this as any form of critique of either curl or others
(besides my own implementation), it was basically just me having to write
it down somewhere to get a discussion going to get out of the whole "home
blindness" and be able to fix my own implementation in a better way.
Sometimes one just needs that kind of ball plank, sorry if I've wasted any
ones time due to this, but then on the other hand some day in the future
some one else with the same problem will google and find this discussion
and perhaps learn something :-)

/HH
Fulup Ar Foll via curl-library
2021-04-04 15:40:39 UTC
Permalink
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Daniel Stenberg via curl-library
2021-04-04 21:24:07 UTC
Permalink
It is clear that mainloop event model with libcurl would deserve a better
documentation/samples. It also took me a while to get a working model.
Unfortunately when googleling you find mostly lost people or worse bad
implementations.
Apart from the man pages, I've tried to make it more tutorial style in the
book here:

https://everything.curl.dev/libcurl/drive/multi-socket

But we're of course always interested in improvements and extensions for the
documentation as well.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https
Fulup Ar Foll via curl-library
2021-04-05 16:24:10 UTC
Permalink
Daniel,

Good response, anyone can improve the documentation. I wrote a mainloop
sample, but a good doc would be far better. I note your point about
improving the doc :)

Fulup
Post by Daniel Stenberg via curl-library
It is clear that mainloop event model with libcurl would deserve a
better documentation/samples. It also took me a while to get a
working model. Unfortunately when googleling you find mostly lost
people or worse bad implementations.
Apart from the man pages, I've tried to make it more tutorial style in
  https://everything.curl.dev/libcurl/drive/multi-socket
But we're of course always interested in improvements and extensions
for the documentation as well.
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
E
Henrik Holst via curl-library
2021-04-05 14:54:00 UTC
Permalink
Post by Daniel Stenberg via curl-library
(Let me just add some meat to this and explain this described behavior.)
Post by Henrik Holst via curl-library
As I wrote earlier, usually when you call curl_multi_add_handle(), the
Timer
Post by Henrik Holst via curl-library
Function CB is called once by curl with timeout set to 0. Then when you
call
Post by Henrik Holst via curl-library
curl_multi_socket_action() with CURL_SOCKET_TIMEOUT the Timer Function
CB is
Post by Henrik Holst via curl-library
usually called once more but this time with a timeout set to 1.
This description matches how libcurl *did* behave. More recent versions don't
need to poll the name resolve results from the threaded resolver so the
timeout pattern will be slightly different.
On a side note (and perhaps I should have written this in a separate mail)
I did notice that the timer function callback returns far too low values
for long polling http servers if the user haven't set CURLOPT_TIMEOUT.
Caveat here is that I have only tested this with 7.58 and 7.68 as of yet
and not with HEAD.

So one of the external services that I connect to uses long polling to
avoid having people like me polling the service several times per second in
order to get the latest update with minimal latency. Normally I set
CURLOPT_TIMEOUT for all connections, but yesterday I experimented by using
TCP Keep-alive instead so I commented out the setting of CURLOPT_TIMEOUT
and noticed that the application took 100% cpu, later examination revealed
that this was because the last timeout from the Timer Function callback was
1 (and sometimes 0).

Here is one such run with CURLOPT_VERBOSE set, unfortunately the server is
not public so replication might be hard:

For this I simply print out the given timeout with this callback:

static int CURL_timer_callback (__attribute__((unused)) CURLM *multi, long
timeout, void *userp)
{
printf ("timeout: %ld\n", timeout);
return 0;
}

timeout: -1
timeout: 0
timeout: 1
timeout: 1
timeout: 1
timeout: 4
timeout: 7
timeout: 1
timeout: 15
timeout: 1
timeout: 31
timeout: 1
* Trying xxx.170.239.163:80...
* TCP_NODELAY set
timeout: 49
* Connected to press.xxx.com (xxx.170.239.163) port 80 (#0)
Post by Daniel Stenberg via curl-library
GET /items&lastId=126599 HTTP/1.1
Host: press.xxx.com
Accept: */*

timeout: 119
timeout: 1
^C* Closing connection 0

As you can see the last timeout given here was 1 and then about 10s later I
hit ctrl-c to quit. I was rather expecting it to be -1 since the default
timeout should be infinite.

If I however set CURLOPT_TIMEOUT to say 1800L then:
timeout: -1
timeout: 0
timeout: 1
timeout: 2
timeout: 3
timeout: 1
timeout: 8
timeout: 15
timeout: 1
timeout: 31
* Trying xxx.170.239.163:80...
* TCP_NODELAY set
timeout: 10
timeout: 1
timeout: 188
* Connected to press.xxx.com (xxx.170.239.163) port 80 (#0)
Post by Daniel Stenberg via curl-library
GET /items&lastId=126599 HTTP/1.1
Host: press.xxx.com
Accept: */*

timeout: 1799747
^C* Closing connection 0

Here the timeout it set to the configured value of ~1800000ms

/HH
Daniel Stenberg via curl-library
2021-04-05 15:33:18 UTC
Permalink
On a side note (and perhaps I should have written this in a separate mail) I
did notice that the timer function callback returns far too low values for
long polling http servers if the user haven't set CURLOPT_TIMEOUT. Caveat
here is that I have only tested this with 7.58 and 7.68 as of yet and not
with HEAD.
As I mentioned before, we've removed the need for polling since then so a lot
of the really low-value timeouts are gone for a normal build. Now libcurl
waits on a socket even during a threaded name resolve.
Here is one such run with CURLOPT_VERBOSE set, unfortunately the server is
If this problem was widespread/common, then you wouldn't need to use a private
server to reproduce surely?

Simply based on the fact that libcurl is fairly well used and nobody (else) is
reporting "100% cpu use", I don't think this is a common problem. But I'm also
sure there are bugs still to fix and I'm also sure that we've fixed bugs since
the versions you run. Some of those might change these things.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette:
Fulup Ar Foll via curl-library
2021-04-05 16:39:16 UTC
Permalink
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-library
Etiquette: https://curl.se/mail/etiquette.html
Henrik Holst via curl-library
2021-04-05 18:06:11 UTC
Permalink
Henrik,
I confirm what Daniel said. If you take 100% of CPU your logic is
obviously wrong.
If you run with a main loop, it is up to your your code to select which
socket is ready for reading. From your mainloop callback your should call curl_multi_socket_action
with the corresponding socket-fd and action. If you do so timeout callback
will only be called once per get resquest. You may run my sample on your
private server, you will see that it takes only few % of CPUs, this even
when you have 50 parallel ongoing requests.
hmm, I tried it with your example and yes that does not take 100% cpu but
your code gets a timeout of 2ms from multiSetTimerCB so shouldn't that mean
that your code would call curl_multi_socket_action (curlm,
CURL_SOCKET_TIMEOUT, 0, &running_handles); every 2 milliseconds? That your
code takes 0% cpu indicates that you don't call it every 2 milliseconds so
am I understanding the timeout given by libcurl somehow or are (in this
case the systemd event loop) the event loop used imposing some limit on how
often it calls or rather convert it to usleep (2000) then that would also
lead to 0% cpu since such a small value for usleep would be the full quanta
(10ms) unless running under a realtime scheduler.

[httpPool-create-async] multi curl pool initialized
[request-sent] reqId=0 http://press.xxx.com/items&lastId=126599
-- multiSetTimerCB timeout=0
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=1
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=1
[multi-sock-remove] sock=7 (multiSetSockCB)
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=2
-- waiting 1 pending request(s)
-- multiSetTimerCB timeout=2
-- waiting 1 pending request(s)
*-- multiSetTimerCB timeout=2 <-- here is the last Timer Function CB
setting the timeout to 2 milliseconds*
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
^C

/HH
Fulup
On a side note (and perhaps I should have written this in a separate mail)
I did notice that the timer function callback returns far too low values
for long polling http servers if the user haven't set CURLOPT_TIMEOUT.
Caveat here is that I have only tested this with 7.58 and 7.68 as of yet
and not with HEAD.
As I mentioned before, we've removed the need for polling since then so a
lot of the really low-value timeouts are gone for a normal build. Now
libcurl waits on a socket even during a threaded name resolve.
Here is one such run with CURLOPT_VERBOSE set, unfortunately the server is
If this problem was widespread/common, then you wouldn't need to use a
private server to reproduce surely?
Simply based on the fact that libcurl is fairly well used and nobody
(else) is reporting "100% cpu use", I don't think this is a common problem.
But I'm also sure there are bugs still to fix and I'm also sure that we've
fixed bugs since the versions you run. Some of those might change these
things.
Henrik Holst via curl-library
2021-04-05 18:51:44 UTC
Permalink
Den mån 5 apr. 2021 kl 20:06 skrev Henrik Holst <
Post by Henrik Holst via curl-library
Henrik,
I confirm what Daniel said. If you take 100% of CPU your logic is
obviously wrong.
If you run with a main loop, it is up to your your code to select which
socket is ready for reading. From your mainloop callback your should call curl_multi_socket_action
with the corresponding socket-fd and action. If you do so timeout callback
will only be called once per get resquest. You may run my sample on your
private server, you will see that it takes only few % of CPUs, this even
when you have 50 parallel ongoing requests.
hmm, I tried it with your example and yes that does not take 100% cpu but
your code gets a timeout of 2ms from multiSetTimerCB so shouldn't that mean
that your code would call curl_multi_socket_action (curlm,
CURL_SOCKET_TIMEOUT, 0, &running_handles); every 2 milliseconds? That your
code takes 0% cpu indicates that you don't call it every 2 milliseconds so
am I understanding the timeout given by libcurl somehow or are (in this
case the systemd event loop) the event loop used imposing some limit on how
often it calls or rather convert it to usleep (2000) then that would also
lead to 0% cpu since such a small value for usleep would be the full quanta
(10ms) unless running under a realtime scheduler.
[httpPool-create-async] multi curl pool initialized
[request-sent] reqId=0 http://press.xxx.com/items&lastId=126599
-- multiSetTimerCB timeout=0
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=1
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=1
[multi-sock-remove] sock=7 (multiSetSockCB)
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=2
-- waiting 1 pending request(s)
-- multiSetTimerCB timeout=2
-- waiting 1 pending request(s)
*-- multiSetTimerCB timeout=2 <-- here is the last Timer Function CB
setting the timeout to 2 milliseconds*
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
^C
Ok so I added logging in httpOnTimerCB to output the time whenever it was
called (btw I have problems with your Makefile but perhaps we should take
that in private) and it appears to only be called once per
CURLMOPT_TIMERFUNCTION so is that where I'm thinking this wrong?

Aka I have seen the value returned from CURLMOPT_TIMERFUNCTION as "call
curl with CURL_SOCKET_TIMEOUT each timeout period" but by going by your
code it instead looks to be "call me again after timeout milliseconds once,
but only once" and if so then I definitely understand where I got wrong
this entire time.

-- multiSetTimerCB timeout=0
18:43:20.923012
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=1
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=1
[multi-sock-remove] sock=7 (multiSetSockCB)
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
18:43:20.983453
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=2
-- waiting 1 pending request(s)
18:43:21.173004
-- multiSetTimerCB timeout=10
-- waiting 1 pending request(s)
18:43:21.173030
-- multiSetTimerCB timeout=10
-- waiting 1 pending request(s)
18:43:21.423003
-- waiting 1 pending request(s)
18:43:21.423028
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
^C

/HH
Post by Henrik Holst via curl-library
/HH
Fulup
On a side note (and perhaps I should have written this in a separate
mail) I did notice that the timer function callback returns far too low
values for long polling http servers if the user haven't set
CURLOPT_TIMEOUT. Caveat here is that I have only tested this with 7.58 and
7.68 as of yet and not with HEAD.
As I mentioned before, we've removed the need for polling since then so a
lot of the really low-value timeouts are gone for a normal build. Now
libcurl waits on a socket even during a threaded name resolve.
Here is one such run with CURLOPT_VERBOSE set, unfortunately the server
If this problem was widespread/common, then you wouldn't need to use a
private server to reproduce surely?
Simply based on the fact that libcurl is fairly well used and nobody
(else) is reporting "100% cpu use", I don't think this is a common problem.
But I'm also sure there are bugs still to fix and I'm also sure that we've
fixed bugs since the versions you run. Some of those might change these
things.
Henrik Holst via curl-library
2021-04-05 18:57:08 UTC
Permalink
and of course now that I reread the docs on CURLMOPT_TIMERFUNCTION I do see
the "non-repeating timer" text....

Sorry for wasting everybody's time here, I'll learn to read before posting
again...

/HH

Den mån 5 apr. 2021 kl 20:51 skrev Henrik Holst <
Post by Henrik Holst via curl-library
Den mån 5 apr. 2021 kl 20:06 skrev Henrik Holst <
Post by Henrik Holst via curl-library
Henrik,
I confirm what Daniel said. If you take 100% of CPU your logic is
obviously wrong.
If you run with a main loop, it is up to your your code to select which
socket is ready for reading. From your mainloop callback your should call curl_multi_socket_action
with the corresponding socket-fd and action. If you do so timeout callback
will only be called once per get resquest. You may run my sample on
your private server, you will see that it takes only few % of CPUs, this
even when you have 50 parallel ongoing requests.
hmm, I tried it with your example and yes that does not take 100% cpu but
your code gets a timeout of 2ms from multiSetTimerCB so shouldn't that mean
that your code would call curl_multi_socket_action (curlm,
CURL_SOCKET_TIMEOUT, 0, &running_handles); every 2 milliseconds? That your
code takes 0% cpu indicates that you don't call it every 2 milliseconds so
am I understanding the timeout given by libcurl somehow or are (in this
case the systemd event loop) the event loop used imposing some limit on how
often it calls or rather convert it to usleep (2000) then that would also
lead to 0% cpu since such a small value for usleep would be the full quanta
(10ms) unless running under a realtime scheduler.
[httpPool-create-async] multi curl pool initialized
[request-sent] reqId=0 http://press.xxx.com/items&lastId=126599
-- multiSetTimerCB timeout=0
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=1
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=1
[multi-sock-remove] sock=7 (multiSetSockCB)
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=2
-- waiting 1 pending request(s)
-- multiSetTimerCB timeout=2
-- waiting 1 pending request(s)
*-- multiSetTimerCB timeout=2 <-- here is the last Timer Function CB
setting the timeout to 2 milliseconds*
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
^C
Ok so I added logging in httpOnTimerCB to output the time whenever it was
called (btw I have problems with your Makefile but perhaps we should take
that in private) and it appears to only be called once per
CURLMOPT_TIMERFUNCTION so is that where I'm thinking this wrong?
Aka I have seen the value returned from CURLMOPT_TIMERFUNCTION as "call
curl with CURL_SOCKET_TIMEOUT each timeout period" but by going by your
code it instead looks to be "call me again after timeout milliseconds once,
but only once" and if so then I definitely understand where I got wrong
this entire time.
-- multiSetTimerCB timeout=0
18:43:20.923012
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=1
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=1
[multi-sock-remove] sock=7 (multiSetSockCB)
[multi-sock-insert] sock=7 (multiSetSockCB)
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
18:43:20.983453
-- multiSetTimerCB timeout=49
-- waiting 1 pending request(s)
httpOnSocketCB: sock=7 action=2
-- waiting 1 pending request(s)
18:43:21.173004
-- multiSetTimerCB timeout=10
-- waiting 1 pending request(s)
18:43:21.173030
-- multiSetTimerCB timeout=10
-- waiting 1 pending request(s)
18:43:21.423003
-- waiting 1 pending request(s)
18:43:21.423028
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
-- waiting 1 pending request(s)
^C
/HH
Post by Henrik Holst via curl-library
/HH
Fulup
On a side note (and perhaps I should have written this in a separate
mail) I did notice that the timer function callback returns far too low
values for long polling http servers if the user haven't set
CURLOPT_TIMEOUT. Caveat here is that I have only tested this with 7.58 and
7.68 as of yet and not with HEAD.
As I mentioned before, we've removed the need for polling since then so
a lot of the really low-value timeouts are gone for a normal build. Now
libcurl waits on a socket even during a threaded name resolve.
Here is one such run with CURLOPT_VERBOSE set, unfortunately the server
If this problem was widespread/common, then you wouldn't need to use a
private server to reproduce surely?
Simply based on the fact that libcurl is fairly well used and nobody
(else) is reporting "100% cpu use", I don't think this is a common problem.
But I'm also sure there are bugs still to fix and I'm also sure that we've
fixed bugs since the versions you run. Some of those might change these
things.
Henrik Holst via curl-library
2021-04-05 18:26:57 UTC
Permalink
Post by Henrik Holst via curl-library
On a side note (and perhaps I should have written this in a separate
mail) I
Post by Henrik Holst via curl-library
did notice that the timer function callback returns far too low values
for
Post by Henrik Holst via curl-library
long polling http servers if the user haven't set CURLOPT_TIMEOUT.
Caveat
Post by Henrik Holst via curl-library
here is that I have only tested this with 7.58 and 7.68 as of yet and
not
Post by Henrik Holst via curl-library
with HEAD.
As I mentioned before, we've removed the need for polling since then so a lot
of the really low-value timeouts are gone for a normal build. Now libcurl
waits on a socket even during a threaded name resolve.
Post by Henrik Holst via curl-library
Here is one such run with CURLOPT_VERBOSE set, unfortunately the server
is
If this problem was widespread/common, then you wouldn't need to use a private
server to reproduce surely?
I don't know of any public http server doing long poll and this particular
server that does employ long polling was what I was working with at the
moment and unfortunately it's private service (not under my control since
it's an external service) but the server is most likely not the issue here,
just that it employs long poll so that there is no read event quickly after
connect.
Simply based on the fact that libcurl is fairly well used and nobody (else) is
reporting "100% cpu use", I don't think this is a common problem. But I'm also
sure there are bugs still to fix and I'm also sure that we've fixed bugs since
the versions you run. Some of those might change these things.
I can agree with that, but as you could see from my output, curl did give
me a 1ms timeout as the last timeout to use. So every 1ms I did call
curl_multi_socket_action() with CURL_SOCKET_TIMEOUT, isn't that what I'm
supposed to do when the callback tells me that the timeout is 1ms?

And this particular run with 1ms did not lead to 100% cpu, it was when curl
gave me 0ms as timeout as the last timeout which lead me to call
curl_multi_socket_action() every 0ms, which turned it into a busy wait loop
and hence the 100% cpu. Shouldn't curl return -1 as infinte timeout instead
there since it's waiting for read on a connected socket (after having sent
the request in full) with no defined CURLOPT_TIMEOUT?

The 100% cpu was just a side effect of the 0 timeout in my particular code,
my main question is why curl told me that the timeout should be 1/0 and not
-1, unless I set CURLOPT_TIMEOUT and I get a "proper value" instead?

/HH
Daniel Stenberg via curl-library
2021-04-05 20:43:09 UTC
Permalink
Post by Henrik Holst via curl-library
And this particular run with 1ms did not lead to 100% cpu, it was when curl
gave me 0ms as timeout as the last timeout which lead me to call
curl_multi_socket_action() every 0ms, which turned it into a busy wait loop
and hence the 100% cpu. Shouldn't curl return -1 as infinte timeout instead
there since it's waiting for read on a connected socket (after having sent
the request in full) with no defined CURLOPT_TIMEOUT?
0 means that there's a timeout pending that libcurl wants to take care of at
once so then it can't return -1 since -1 means no timeout at all.

So no, it cannot return -1 when it means 0.
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
-------------------------------------------------------------------
Unsubscribe: https://cool.haxx.se/list/listinfo/curl-libra
Henrik Holst via curl-library
2021-04-05 20:45:02 UTC
Permalink
Post by Henrik Holst via curl-library
Post by Henrik Holst via curl-library
And this particular run with 1ms did not lead to 100% cpu, it was when
curl
Post by Henrik Holst via curl-library
gave me 0ms as timeout as the last timeout which lead me to call
curl_multi_socket_action() every 0ms, which turned it into a busy wait
loop
Post by Henrik Holst via curl-library
and hence the 100% cpu. Shouldn't curl return -1 as infinte timeout
instead
Post by Henrik Holst via curl-library
there since it's waiting for read on a connected socket (after having
sent
Post by Henrik Holst via curl-library
the request in full) with no defined CURLOPT_TIMEOUT?
0 means that there's a timeout pending that libcurl wants to take care of at
once so then it can't return -1 since -1 means no timeout at all.
So no, it cannot return -1 when it means 0.
Yeah, it turned out that my whole problem was that i misread the docs
"non-repeating timer" as "repeating timer" and that was the whole basis for
my confusion.

Sorry for this.

/HH
Post by Henrik Holst via curl-library
--
/ daniel.haxx.se
| Commercial curl support up to 24x7 is available!
| Private help, bug fixes, support, ports, new features
| https://www.wolfssl.com/contact/
Loading...