From: Sean McAllister <smcallis@google.com>
To: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Cc: git@vger.kernel.org, Junio C Hamano <gitster@pobox.com>,
peff@peff.net, Masaya Suzuki <masayasuzuki@google.com>
Subject: Re: [PATCH 3/3] http: automatically retry some requests
Date: Tue, 13 Oct 2020 09:03:15 -0600 [thread overview]
Message-ID: <CAM4o00fL4oGNG_Z7tF5bL=Kp===683LBo1RhmZ=vZ6Kie=-jzA@mail.gmail.com> (raw)
In-Reply-To: <nycvar.QRO.7.76.6.2010122126280.50@tvgsbejvaqbjf.bet>
> >
> > +http.retryLimit::
> > + Some HTTP error codes (eg: 429,503) can reasonably be retried if
>
> Please have a space after the comma so that it is not being mistaken for a
> 6-digit number. Also, aren't they called "status codes"? Not all of them
> indicate errors, after all.
Done for v2, good point on the nomenclature.
> > diff --git a/http.c b/http.c
> > index b3c1669388..e41d7c5575 100644
> > --- a/http.c
> > +++ b/http.c
> > @@ -92,6 +92,9 @@ static const char *http_proxy_ssl_key;
> > static const char *http_proxy_ssl_ca_info;
> > static struct credential proxy_cert_auth = CREDENTIAL_INIT;
> > static int proxy_ssl_cert_password_required;
> > +static int http_retry_limit = 3;
> > +static int http_default_delay = 2;
>
> Should there be a config option for that? Also, it took me some time to
> find the code using this variable in order to find out what unit to use:
> it is seconds (not microseconds, as I had expected). Maybe this can be
> documented in the variable name, or at least in a comment?
>
Junio tossed that out during our private review and I think we decided to just
leave them as non-const so we have that option going forward. I'm not
sure there's
a strong story for configuring the default delay.
I went through and changed all the _delay variables to be more explicit they're
in seconds.
> > @@ -219,6 +222,51 @@ size_t fwrite_null(char *ptr, size_t eltsize, size_t nmemb, void *strbuf)
> > return nmemb;
> > }
> >
> > +
> > +/* return 1 for a retryable HTTP code, 0 otherwise */
> > +static int retryable_code(int code)
> > +{
> > + switch(code) {
> > + case 429: /* fallthrough */
> > + case 502: /* fallthrough */
> > + case 503: /* fallthrough */
> > + case 504: return 1;
> > + default: return 0;
> > + }
> > +}
> > +
> > +size_t http_header_value(
>
> In the part of Git's code with which I am familiar, we avoid trying to
> break the line after an opening parenthesis, instead preferring to break
> after a comma.
>
> Also, shouldn't we make the return type `ssize_t` to allow for a negative
> value to indicate an error/missing header?
I return zero to indicate no header found (or zero-length value), so this should
never return a negative value. One can check value to see if a string
was allocated
for a zero-length header.
>
> > + const struct strbuf headers, const char *header, char **value)
> > +{
> > + size_t len = 0;
> > + struct strbuf **lines, **line;
> > + char *colon = NULL;
> > +
> > + lines = strbuf_split(&headers, '\n');
> > + for (line = lines; *line; line++) {
> > + strbuf_trim(*line);
> > +
> > + /* find colon and null it out to 'split' string */
> > + colon = strchr((*line)->buf, ':');
> > + if (colon) {
> > + *colon = '\0';
> > +
> > + if (!strcasecmp(header, (*line)->buf)) {
>
> If all we want is to find the given header, splitting lines seems to be a
> bit wasteful to me. We could instead search for the header directly:
>
> const char *p = strcasestr(headers.buf, header), *eol;
> size_t header_len = strlen(header);
>
> while (p) {
> if ((p != headers.buf && p[-1] != '\n') ||
> p[header_len] != ':') {
> p = strcasestr(p + header_len, header);
> continue;
> }
>
> p += header_len + 1;
> while (isspace(*p) && *p != '\n')
> p++;
> eol = strchrnul(p, '\n');
> len = eol - p;
> *value = xstrndup(p, len);
> return len;
> }
>
I've been writing a lot of python code lately =D So splitting into
lines was a natural paradigm for me. You're right, I like yours more. I've
refactored it to be closer to that. Little bit of fiddling to deal with header
whitespace properly, but it's pretty close.
I also modified to just return the value pointer directly, then it's clear when
we get a zero-length header or don't find it completely, and it fixes the
leak issue below.
> > @@ -1903,20 +1958,55 @@ static void http_opt_request_remainder(CURL *curl, off_t pos)
> > #define HTTP_REQUEST_STRBUF 0
> > #define HTTP_REQUEST_FILE 1
> >
> > +/* check for a retry-after header in the given headers string, if found, then
> > +honor it, otherwise do an exponential backoff up to the max on the current delay
> > +*/
>
> Multi-line comments should be of this form:
>
> /*
> * Check for a retry-after header in the given headers string, if
> * found, then honor it, otherwise do an exponential backoff up to
> * the maximum on the current delay.
> */
>
Done.
> > +static int http_retry_after(const struct strbuf headers, int cur_delay) {
>
> For functions, the initial opening curly bracket goes on its own line.
>
Done.
> > + int len, delay;
> > + char *end;
> > + char *value;
>
> Why not declare `char *end, *value;`, just like `len` and `delay` are
> declared on the same line?
>
Done.
> > +
> > + len = http_header_value(headers, "retry-after", &value);
> > + if (len) {
> > + delay = strtol(value, &end, 0);
> > + if (*value && *end == 0 && delay >= 0) {
>
> Better: `*end == '\0'`
>
> And why `*value` here? We already called `strtol()` on it.
>
Fixed, and I check value per the man page on strtol:
> In particular, if *nptr is not '\0' but **endptr is '\0' on
return, the entire string is valid.
I wanted to make sure I converted the entire header value, so this
seemed correct?
> > + if (delay > http_max_delay) {
> > + die(Q_(
>
> Let's not end the line in an opening parenthesis. Instead, use C's string
> continuation like so:
>
> die(Q_("server requested retry after %d second,"
> " which is longer than max allowed\n",
> "server requested retry after %d "
> "seconds, which is longer than max "
> "allowed\n", delay), delay);
>
Done.
> > + "server requested retry after %d second, which is longer than max allowed\n",
> > + "server requested retry after %d seconds, which is longer than max allowed\n", delay), delay);
> > + }
> > + free(value);
>
> `value` is not actually used after that `strtol()` call above, so let's
> release it right then and there.
>
Good call, done.
> > + return delay;
> > + }
> > + free(value);
> > + }
>
> If the header was found, but for some reason had an empty value, we're
> leaking `value` here.
>
I return the pointer directly and check that now, so if it's allocated, we'll
always call free, even if it's an empty string.
> > +
> > + cur_delay *= 2;
> > + return cur_delay >= http_max_delay ? http_max_delay : cur_delay;
> > +}
> > +
> > static int http_request(const char *url,
> > void *result, int target,
> > const struct http_get_options *options)
> > {
> > struct active_request_slot *slot;
> > struct slot_results results;
> > - struct curl_slist *headers = http_copy_default_headers();
> > + struct curl_slist *headers;
> > struct strbuf buf = STRBUF_INIT;
> > + struct strbuf result_headers = STRBUF_INIT;
> > const char *accept_language;
> > int ret;
> > + int retry_cnt = 0;
> > + int retry_delay = http_default_delay;
> > + int http_code;
> >
> > +retry:
> > slot = get_active_slot();
> > curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1);
> >
> > + curl_easy_setopt(slot->curl, CURLOPT_HEADERDATA, &result_headers);
> > + curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, fwrite_buffer);
> > +
> > if (result == NULL) {
> > curl_easy_setopt(slot->curl, CURLOPT_NOBODY, 1);
> > } else {
> > @@ -1936,6 +2026,7 @@ static int http_request(const char *url,
> >
> > accept_language = get_accept_language();
> >
> > + headers = http_copy_default_headers();
> > if (accept_language)
> > headers = curl_slist_append(headers, accept_language);
> >
> > @@ -1961,7 +2052,31 @@ static int http_request(const char *url,
> > curl_easy_setopt(slot->curl, CURLOPT_ENCODING, "");
> > curl_easy_setopt(slot->curl, CURLOPT_FAILONERROR, 0);
> >
> > - ret = run_one_slot(slot, &results);
> > + http_code = 0;
> > + ret = run_one_slot(slot, &results, &http_code);
> > +
> > + if (ret != HTTP_OK) {
> > + if (retryable_code(http_code) && (retry_cnt < http_retry_limit)) {
>
> The parentheses around the second condition should be dropped.
>
Done.
> > + retry_cnt++;
> > + retry_delay = http_retry_after(result_headers, retry_delay);
> > + fprintf(stderr,
>
> Should this be a `warning()` instead? I see 5 instances in `http.c` that
> use `fprintf(stderr, ...)`, but 12 that use `warning()`, making me believe
> that at least some of those 5 instances should call `warning()` instead,
> too.
>
At your discretion. I'm not familiar with the logging framework.
Only one of those 5 fprintf
is in my patch, so I changed that one to use warning().
> > + Q_("got HTTP response %d, retrying after %d second (%d/%d)\n",
> > + "got HTTP response %d, retrying after %d seconds (%d/%d)\n",
> > + retry_delay),
> > + http_code, retry_delay, retry_cnt, http_retry_limit);
> > + sleep(retry_delay);
> > +
> > + // remove header data fields since not all slots will use them
>
> No C++-style comments, please: use /* ... */ instead.
>
Thought I got them all. Done.
> > + curl_easy_setopt(slot->curl, CURLOPT_HEADERDATA, NULL);
> > + curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, NULL);
> > +
> > + goto retry;
> > + }
> > + }
> > +
> > + // remove header data fields since not all slots will use them
>
> No C++-style comments, please.
>
Done.
> > + curl_easy_setopt(slot->curl, CURLOPT_HEADERDATA, NULL);
> > + curl_easy_setopt(slot->curl, CURLOPT_HEADERFUNCTION, NULL);
>
> Shouldn't we just perform this assignment before the `if (ret != HTTP_OK)`
> condition? I do not see anything inside that block that needs it,
> therefore this could be DRY'd up.
>
Excellent idea, done.
> > +/*
> > + * Query the value of an HTTP header.
> > + *
> > + * If the header is found, then a newly allocate string is returned through
> > + * the value parameter, and the length is returned.
> > + *
> > + * If not found, returns 0
> > + */
> > +size_t http_header_value(
> > + const struct strbuf headers, const char *header, char **value);
>
> Do we really need to export this function? It could stay file-local, at
> least for now (i.e. be defined `static` inside `http.c`), no?
>
I originally thought I'd need it elsewhere, so a big of a relic there. I made
it static for now.
> > --- a/t/t5539-fetch-http-shallow.sh
> > +++ b/t/t5539-fetch-http-shallow.sh
> > @@ -30,20 +30,39 @@ test_expect_success 'clone http repository' '
> > git clone --bare --no-local shallow "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
> > git clone $HTTPD_URL/smart/repo.git clone &&
> > (
> > - cd clone &&
> > - git fsck &&
> > - git log --format=%s origin/master >actual &&
> > - cat <<EOF >expect &&
> > -7
> > -6
> > -5
> > -4
> > -3
> > -EOF
> > - test_cmp expect actual
> > + cd clone &&
> > + git fsck &&
> > + git log --format=%s origin/master >actual &&
> > + cat <<-\EOF >expect &&
> > + 7
> > + 6
> > + 5
> > + 4
> > + 3
> > + EOF
> > + test_cmp expect actual
>
> This just changes the indentation, right?
>
> I _guess_ this is a good change, but it should live in its own patch.
>
This was in combination with my adding a new test, we cleaned up the
whitespace too per Junio rather than copy-and-pasting ill formatted code.
It's OBE now because this change has been removed in v2.
> > +test_expect_success 'clone http repository with flaky http' '
> > + rm -rf clone &&
>
> Let's consistently use horizontal tab characters for indentation. (There
> are more instances of lines indented by spaces below.)
>
*sigh* thought I got them all, fixed.
> > + git clone $HTTPD_URL/error_ntime/`gen_nonce`/3/429/1/smart/repo.git clone 2>err &&
>
> Let's use `$(gen_nonce)`. Also: where is the `gen_nonce` defined? I do not
> see the definition in this patch (but it could be 1/3, which for some
> reason did not make it to the mailing list:
> https://lore.kernel.org/git/20201012184806.166251-1-smcallis@google.com/).
>
Done, and it is indeed in 1/3 which got caught by the spam filter
(should be available now).
> Another suggestion: rather than deleting `clone/`, use a separate
> directory to clone into, say, `flaky/`. That will make it easier to debug
> when the entire "trash" directory is tar'ed up in a failed CI build, for
> example.
>
Done.
> > diff --git a/t/t5551-http-fetch-smart.sh b/t/t5551-http-fetch-smart.sh
> > index e40e9ed52f..85d2a0e8b8 100755
> > --- a/t/t5551-http-fetch-smart.sh
> > +++ b/t/t5551-http-fetch-smart.sh
> > @@ -45,6 +45,7 @@ test_expect_success 'clone http repository' '
> > EOF
> > GIT_TRACE_CURL=true GIT_TEST_PROTOCOL_VERSION=0 \
> > git clone --quiet $HTTPD_URL/smart/repo.git clone 2>err &&
> > + cd clone && git config pull.rebase false && cd .. &&
>
> Better: test_config -C clone pull.rebase false
>
Done, but also OBE by removing tests.
>
> I wonder whether it is really necessary to add _that_ many test cases. The
> test suite already takes so long to run that we had cases where
> contributors simply did not run it before sending their contributions.
>
> In this instance, I would think that it would be plenty sufficient to have
> a single new test case that exercizes the added code path (and verifies
> that it can see the message).
>
Done, down to a single test (clone) that should exercise everything.
> Thanks,
> Dscho
>
> >
> > # DO NOT add non-httpd-specific tests here, because the last part of this
> > # test script is only executed when httpd is available and enabled.
> > --
> > 2.28.0.1011.ga647a8990f-goog
> >
> >
next prev parent reply other threads:[~2020-10-13 15:03 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
[not found] <20201012184806.166251-1-smcallis@google.com>
2020-10-12 18:48 ` [PATCH 2/3] replace CURLOPT_FILE With CURLOPT_WRITEDATA Sean McAllister
2020-10-12 19:26 ` Johannes Schindelin
2020-10-12 18:48 ` [PATCH 3/3] http: automatically retry some requests Sean McAllister
2020-10-12 20:15 ` Johannes Schindelin
2020-10-12 21:00 ` Junio C Hamano
2020-10-13 15:03 ` Sean McAllister [this message]
2020-10-13 17:45 ` Junio C Hamano
2020-10-12 20:19 ` [PATCH] remote-curl: add testing for intelligent retry for HTTP Sean McAllister
2020-10-12 20:51 ` Junio C Hamano
2020-10-12 22:20 ` Sean McAllister
2020-10-12 22:30 ` Junio C Hamano
2020-10-13 14:25 ` Johannes Schindelin
2020-10-13 17:29 ` Junio C Hamano
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: http://vger.kernel.org/majordomo-info.html
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to='CAM4o00fL4oGNG_Z7tF5bL=Kp===683LBo1RhmZ=vZ6Kie=-jzA@mail.gmail.com' \
--to=smcallis@google.com \
--cc=Johannes.Schindelin@gmx.de \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=masayasuzuki@google.com \
--cc=peff@peff.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://80x24.org/mirrors/git.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).