Skip to content

Commit dfb0bb2

Browse files
committed
repo-stats: retry on non-standard HTTP 499 too
1 parent 3a1f883 commit dfb0bb2

5 files changed

Lines changed: 33 additions & 16 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/lib/docs_rs_repository_stats/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ docs_rs_test_fakes = { path = "../docs_rs_test_fakes" }
3434
docs_rs_types = { path = "../docs_rs_types", features = ["testing"] }
3535
mockito = { workspace = true }
3636
pretty_assertions = { workspace = true }
37+
test-case = { workspace = true }
3738
tokio = { workspace = true }
3839

3940
[lints]

‎crates/lib/docs_rs_repository_stats/src/github.rs‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
RateLimitReached,
33
config::Config,
4-
retry::NoRateLimitRetryStrategy,
4+
retry::RepositoryForgeRetryStrategy,
55
updater::{FetchRepositoriesResult, Repository, RepositoryForge, RepositoryName},
66
};
77
use anyhow::{Result, anyhow, bail};
@@ -84,7 +84,7 @@ impl GitHub {
8484
)
8585
.with(RetryTransientMiddleware::new_with_policy_and_strategy(
8686
ExponentialBackoff::builder().build_with_max_retries(config.github_api_retries),
87-
NoRateLimitRetryStrategy,
87+
RepositoryForgeRetryStrategy,
8888
))
8989
.build();
9090

@@ -309,6 +309,7 @@ mod tests {
309309
use anyhow::Result;
310310
use docs_rs_config::AppConfig as _;
311311
use reqwest::header::AUTHORIZATION;
312+
use test_case::test_case;
312313

313314
const TEST_TOKEN: &str = "qsjdnfqdq";
314315

@@ -426,7 +427,9 @@ mod tests {
426427
}
427428

428429
#[tokio::test]
429-
async fn retries_server_errors() -> Result<()> {
430+
#[test_case(499)]
431+
#[test_case(500)]
432+
async fn retries_server_errors(status_code: usize) -> Result<()> {
430433
const RETRIES: u32 = 2;
431434

432435
let mut config = github_config()?;
@@ -436,7 +439,7 @@ mod tests {
436439
let mock = server
437440
.mock("POST", "/graphql")
438441
.with_header("content-type", "application/json")
439-
.with_status(500)
442+
.with_status(status_code)
440443
.expect((RETRIES + 1) as usize)
441444
.create();
442445

@@ -447,7 +450,7 @@ mod tests {
447450
.await
448451
.unwrap_err();
449452

450-
assert!(err.to_string().contains("500 Internal Server Error"));
453+
assert!(err.to_string().contains(&status_code.to_string()));
451454
mock.assert();
452455

453456
Ok(())

‎crates/lib/docs_rs_repository_stats/src/gitlab.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use tracing::warn;
1212

1313
use crate::{
1414
RateLimitReached,
15-
retry::NoRateLimitRetryStrategy,
15+
retry::RepositoryForgeRetryStrategy,
1616
updater::{FetchRepositoriesResult, Repository, RepositoryForge, RepositoryName},
1717
};
1818

@@ -88,7 +88,7 @@ impl GitLab {
8888
)
8989
.with(RetryTransientMiddleware::new_with_policy_and_strategy(
9090
ExponentialBackoff::builder().build_with_max_retries(api_retries),
91-
NoRateLimitRetryStrategy,
91+
RepositoryForgeRetryStrategy,
9292
))
9393
.build();
9494

‎crates/lib/docs_rs_repository_stats/src/retry.rs‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,32 @@ use reqwest::StatusCode;
22
use reqwest_middleware::Error as MiddlewareError;
33
use reqwest_retry::{DefaultRetryableStrategy, Retryable, RetryableStrategy};
44

5-
/// Retries transient failures, except rate limits, which callers must handle immediately.
5+
/// Retry policy for repository-forge APIs.
66
///
77
/// Repo-Stats run as a scheduled task once an hour. When we reach a rate limit
88
/// we just stop handling repos for this run, and continue at the next scheduled time.
9-
pub(crate) struct NoRateLimitRetryStrategy;
9+
///
10+
/// Also, some HTTP statuses are treated as fatal in `DefaultRetryableStrategy`, while
11+
/// we think they are actually transient.
12+
pub(crate) struct RepositoryForgeRetryStrategy;
1013

11-
impl RetryableStrategy for NoRateLimitRetryStrategy {
14+
impl RetryableStrategy for RepositoryForgeRetryStrategy {
1215
fn handle(&self, result: &Result<reqwest::Response, MiddlewareError>) -> Option<Retryable> {
13-
if let Ok(response) = result
14-
&& response.status() == StatusCode::TOO_MANY_REQUESTS
15-
{
16-
Some(Retryable::Fatal)
17-
} else {
18-
DefaultRetryableStrategy.handle(result)
16+
match result {
17+
Ok(response) if response.status() == StatusCode::TOO_MANY_REQUESTS => {
18+
Some(Retryable::Fatal)
19+
}
20+
Ok(response) if response.status().as_u16() == 499 => {
21+
// NGINX defines a non-standard HTTP status code:
22+
// `NGX_HTTP_CLIENT_CLOSED_REQUEST 499`
23+
// See:
24+
// https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#nginx
25+
// https://web.archive.org/web/20170919111558/http://lxr.nginx.org/source/src/http/ngx_http_request.h
26+
//
27+
// We believe retrying is safe because the server did not complete the request.
28+
Some(Retryable::Transient)
29+
}
30+
_ => DefaultRetryableStrategy.handle(result),
1931
}
2032
}
2133
}

0 commit comments

Comments
 (0)