Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(asyncification) #2035

Merged
merged 80 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 73 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
f4032c4
temporarily make build scripts faster
Geal Mar 14, 2024
9d19548
move to an async reqwest client
Geal Mar 14, 2024
a144232
more fixes
Geal Mar 14, 2024
9d0a3fd
fix issues
Geal Mar 15, 2024
120a17d
lint
Geal Mar 15, 2024
1b2d6e6
use futures aware channel
Geal Mar 15, 2024
6cc15fe
fix
Geal Mar 15, 2024
abe8dee
sleep
Geal Mar 15, 2024
40b35ab
lint
Geal Mar 15, 2024
b75a531
backtrace
Geal Mar 15, 2024
bc95dd3
fixes
Geal Mar 15, 2024
c6c4d03
fixes
Geal Mar 15, 2024
73ad050
fmt
Geal Mar 15, 2024
f6c98d3
fix
Geal Mar 15, 2024
d8c7d2e
change back CI
Geal Mar 15, 2024
20ce999
Merge branch 'main' into geal/what-color-is-your-rover
Geal Mar 27, 2024
c5f68aa
temporarily make build scripts faster
Geal Mar 14, 2024
09d409f
move to an async reqwest client
Geal Mar 14, 2024
3553d6d
more fixes
Geal Mar 14, 2024
91c8b4d
fix issues
Geal Mar 15, 2024
bfd3260
lint
Geal Mar 15, 2024
5f62b5b
use futures aware channel
Geal Mar 15, 2024
f7aaf69
fix
Geal Mar 15, 2024
f5de9e0
sleep
Geal Mar 15, 2024
16b0aa2
lint
Geal Mar 15, 2024
1d9b26c
backtrace
Geal Mar 15, 2024
d9250e8
fixes
Geal Mar 15, 2024
737473e
fixes
Geal Mar 15, 2024
877fae7
fmt
Geal Mar 15, 2024
34d008e
fix
Geal Mar 15, 2024
8b3eb9f
change back CI
Geal Mar 15, 2024
e01259b
fix(LeaderSesson): impl Drop
aaronArinder Apr 30, 2024
3f8cf95
Merge branch 'main' into geal/what-color-is-your-rover
aaronArinder Apr 30, 2024
e3288af
Merge branch 'geal/what-color-is-your-rover' into aaron/my-rover-is-p…
aaronArinder Apr 30, 2024
aee9b3c
Merge branch 'main' into aaron/my-rover-is-purple
aaronArinder Jul 17, 2024
f46bb51
fix(errors, conflicts): merging in main
aaronArinder Jul 18, 2024
a8a7f9a
Merge branch 'main' into aaron/my-rover-is-purple
aaronArinder Jul 18, 2024
d455082
fix(xtask): clippy complaints
aaronArinder Jul 18, 2024
5940d36
fix(tests): asyncification of router runner test
aaronArinder Jul 18, 2024
db37827
Merge branch 'main' into aaron/my-rover-is-purple
aaronArinder Jul 18, 2024
0b3ecd7
fix(asyncification): appease clippy, dev shutdown() async'd
aaronArinder Jul 18, 2024
f241400
fix(xtask): runtime within a runtime within a runtime within a runtime
aaronArinder Jul 18, 2024
b31c511
fix(xtask): asyncify tests
aaronArinder Jul 18, 2024
e2ee38a
fix(xtask): asyncify tests command running
aaronArinder Jul 18, 2024
2c49c5d
fix(xtask): tokio fflag, rt-multi-thread
aaronArinder Jul 18, 2024
2bd78c2
fix(sputnik): block if telemetry disabled
aaronArinder Jul 18, 2024
fc5a675
fix(sputnik): remove dead-code macro, inaccurate
aaronArinder Jul 18, 2024
eb9a1ba
Merge branch 'main' into aaron/my-rover-is-purple
aaronArinder Jul 19, 2024
ecbd83f
fix(supergraph compose): make faster!
aaronArinder Jul 19, 2024
747584a
wip
aaronArinder Jul 19, 2024
46b7008
rebase me: tests failing
aaronArinder Jul 19, 2024
f200692
rebase
aaronArinder Jul 22, 2024
b0d02c4
rebase
aaronArinder Jul 22, 2024
f7efa70
rebase
aaronArinder Jul 22, 2024
9e864fd
rebase
aaronArinder Jul 22, 2024
5e12ef2
rebase
aaronArinder Jul 22, 2024
d22e976
Merge branch 'main' into aaron/my-rover-is-purple
aaronArinder Jul 29, 2024
ac69e93
docs(rover client): add readme
aaronArinder Jul 29, 2024
18143aa
ROVER-69 Unify supergraph_config parsing
jonathanrainer Jul 24, 2024
4cb408a
ROVER-69 Migrate tests from #1977
jonathanrainer Jul 25, 2024
3b9aff7
ROVER-69 Fix Federation Versions to remove WARNs
jonathanrainer Jul 25, 2024
1792645
ROVER-69 Fix docs
jonathanrainer Jul 25, 2024
49e1ef7
Merge branch 'main' into aaron/my-rover-is-purple
aaronArinder Jul 30, 2024
82ef6d5
ROVER-69 Optimise --graph-ref uses
jonathanrainer Jul 25, 2024
95dd8d6
ROVER-69 Update to account for #2004
jonathanrainer Jul 30, 2024
f33a29a
ROVER-69 Fix argument parsing
jonathanrainer Jul 31, 2024
05276fd
ROVER-69 Respond to PR comments
jonathanrainer Jul 31, 2024
b8676cc
Merge branch 'jr/story/ROVER-69' into aaron/my-rover-is-purple-and-ba…
aaronArinder Jul 31, 2024
ddda490
fix(async): missed asyncifications during merge
aaronArinder Jul 31, 2024
0cdb3e8
Merge branch 'main' into aaron/my-rover-is-purple-and-batchy
aaronArinder Aug 7, 2024
a447870
fix(merge): bad merge resolutions
aaronArinder Aug 7, 2024
8e650db
appease clippy
aaronArinder Aug 7, 2024
1bfc449
fix(tests): kill println
aaronArinder Aug 7, 2024
a5b662f
Update Cargo.toml
aaronArinder Aug 9, 2024
655809e
fix(bin/main): make async
aaronArinder Aug 9, 2024
fe13a64
fix(introspect): warnings invalidated; toggling booleans
aaronArinder Aug 9, 2024
c96b715
fix(leader): remove unnecessary warning
aaronArinder Aug 9, 2024
cb3f6c9
fix(deps): remove reqwest blocking feature
aaronArinder Aug 9, 2024
51364e4
Merge branch 'main' into asyncification
aaronArinder Aug 12, 2024
224046d
fix(log): bad merge
aaronArinder Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ assert_cmd = "2"
assert-json-diff = "2"
anyhow = "1"
backtrace = "0.3"
backoff = "0.4"
backoff = { version = "0.4", features = [ "tokio" ]}
base64 = "0.22"
billboard = "0.2"
buildstructor = "0.5.4"
Expand Down Expand Up @@ -195,6 +195,8 @@ tracing = { workspace = true }
which = { workspace = true }
uuid = { workspace = true }
url = { workspace = true, features = ["serde"] }
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] }
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 highlight the line, but, seems like maybe we can get rid of all the reqwest "blocking" features now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call out! removing

futures.workspace = true
aaronArinder marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
assert_cmd = { workspace = true }
Expand Down
3 changes: 2 additions & 1 deletion crates/rover-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ariadne = { workspace = true }
apollo-federation-types = { workspace = true }
apollo-parser = { workspace = true }
apollo-encoder = { workspace = true }
backoff = { workspace = true }
backoff = { workspace = true, features = ["tokio", "futures"] }
buildstructor = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
derive-getters = { workspace = true }
Expand Down Expand Up @@ -40,6 +40,7 @@ serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
regex = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }

[build-dependencies]
anyhow = { workspace = true }
Expand Down
42 changes: 42 additions & 0 deletions crates/rover-client/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Rover Client

This is the client used by rover to make network requests. This README covers aspects of the client that are useful to know when developing and using it.

The rover client uses [Reqwest](https://docs.rs/reqwest/latest/reqwest/) and some familiarity with that crate is useful in both developing and using it.

# Development :: WIP

We're in the midst of undergoing a transition from a synchronous, blocking client used by threads to an asynchronous one used by an event loop (a tokio runtime that also uses threads, but is non-blocking). Because of that, some of the naming and ergonmics might feel weird.

# Using the client

## Timeouts

By default, the timeout is 10s. This is set _not_ by the `MAX_ELAPSED_TIME` const in the `rover-client/src/blocking/client.rs` file, but in the `Default` implementation for `ClientTimeout` in `rover/src/utils/client.rs`. Users can pass the flag `--client-timeout` with an integer representing seconds to control the overall client timeout.

## Retries

### Overview
Retries can happen for two broad reasons: either the client failed or the server failed. Retries are also enabled by default, but can be disabled by passing an argument to the client's `execute` method called `should_retry` (a boolean).

Retries are also only part of the story. The interval of time you place between retries matters. If you retry all at once, you might get rate-limited or otherwise fail. It's best to spread them out exponentially, adding big chunks of time so that the server can complete its work and get ready for more work. Spreading out retries is a good idea, but if you have multiple calls happening at the same time with the same spread, they might fail if the server is overloaded. It's better to spread them out with some added noise, meaning that you spread them out with some randomly generated bit of time added or subtracted so that calls are received by the server in a somewhat distributed fashion.

#### `backoff` crate

We use the [backoff](https://docs.rs/backoff/latest/backoff/) crate for retries. It builds in both the spreading-out of retries in an exponential way, but also the little bit of jitter that helps the server handle many requests.

The crate is interesting in handling retries not by a total amount of retries, but total amount of time. The `MAX_ELAPSED_TIME` in the client file sets this value and defaults to 10s.

#### Client failures

Retries happen when either the client times out (there's a flag for setting the timeout, but by default it's 10s), when there's a connection error, or when incomplete messages are received. Errors about the request or response body, decoding, building the client, or redirecting the network call aren't retried.

#### Server failures

Retries happen for general server errors (noteably, _all_ statuses between 500-99), but not when the request is ill-formed as identified by the server (that is, a 400).






124 changes: 66 additions & 58 deletions crates/rover-client/src/blocking/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ use std::time::Duration;

use graphql_client::{Error as GraphQLError, GraphQLQuery, Response as GraphQLResponse};
use reqwest::{
blocking::{Client as ReqwestClient, Response},
header::{HeaderMap, HeaderValue},
StatusCode,
Client as ReqwestClient, Response, StatusCode,
};

use crate::error::{EndpointKind, RoverClientError};
Expand Down Expand Up @@ -37,7 +36,7 @@ impl GraphQLClient {
///
/// Takes one argument, `variables`. Returns an optional response.
/// Automatically retries requests.
pub fn post<Q>(
pub async fn post<Q>(
Copy link
Member

Choose a reason for hiding this comment

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

Is the blocking module containing async functions part of the "this is WIP" disclaimer in the README?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep! we're going to have more clean up after the next rc, which will probably make it into the actual next release

&self,
variables: Q::Variables,
header_map: &mut HeaderMap,
Expand All @@ -48,15 +47,17 @@ impl GraphQLClient {
{
let request_body = self.get_request_body::<Q>(variables)?;
header_map.append("Content-Type", HeaderValue::from_str(JSON_CONTENT_TYPE)?);
let response = self.execute(request_body, header_map, true, endpoint_kind);
GraphQLClient::handle_response::<Q>(response?, endpoint_kind)
let response = self
.execute(request_body, header_map, true, endpoint_kind)
.await;
GraphQLClient::handle_response::<Q>(response?, endpoint_kind).await
}

/// Client method for making a GraphQL request.
///
/// Takes one argument, `variables`. Returns an optional response.
/// Does not automatically retry requests.
pub fn post_no_retry<Q>(
pub async fn post_no_retry<Q>(
&self,
variables: Q::Variables,
header_map: &mut HeaderMap,
Expand All @@ -67,8 +68,10 @@ impl GraphQLClient {
{
let request_body = self.get_request_body::<Q>(variables)?;
header_map.append("Content-Type", HeaderValue::from_str(JSON_CONTENT_TYPE)?);
let response = self.execute(request_body, header_map, false, endpoint_kind);
GraphQLClient::handle_response::<Q>(response?, endpoint_kind)
let response = self
.execute(request_body, header_map, false, endpoint_kind)
.await;
GraphQLClient::handle_response::<Q>(response?, endpoint_kind).await
}

fn get_request_body<Q: GraphQLQuery>(
Expand All @@ -79,24 +82,25 @@ impl GraphQLClient {
Ok(serde_json::to_string(&body)?)
}

fn execute(
async fn execute(
&self,
request_body: String,
header_map: &HeaderMap,
should_retry: bool,
endpoint_kind: EndpointKind,
) -> Result<Response, RoverClientError> {
use backoff::{retry, Error as BackoffError, ExponentialBackoff};
use backoff::{future::retry, Error as BackoffError, ExponentialBackoff};

tracing::trace!(request_headers = ?header_map);
tracing::debug!("Request Body: {}", request_body);
let graphql_operation = || {
let graphql_operation = || async {
let response = self
.client
.post(&self.graphql_endpoint)
.headers(header_map.clone())
.body(request_body.clone())
.send();
.send()
.await;

match response {
Err(client_error) => {
Expand Down Expand Up @@ -132,7 +136,7 @@ impl GraphQLClient {
|| response_status.is_redirection()
{
if matches!(response_status, StatusCode::BAD_REQUEST) {
if let Ok(text) = success.text() {
if let Ok(text) = success.text().await {
tracing::debug!("{}", text);
}
Err(BackoffError::Permanent(status_error))
Expand All @@ -158,18 +162,14 @@ impl GraphQLClient {
..Default::default()
};

retry(backoff_strategy, graphql_operation).map_err(|e| match e {
BackoffError::Permanent(reqwest_error)
| BackoffError::Transient {
err: reqwest_error,
retry_after: _,
} => RoverClientError::SendRequest {
source: reqwest_error,
retry(backoff_strategy, graphql_operation)
.await
.map_err(|e| RoverClientError::SendRequest {
source: e,
endpoint_kind,
},
})
})
} else {
graphql_operation().map_err(|e| match e {
graphql_operation().await.map_err(|e| match e {
BackoffError::Permanent(reqwest_error)
| BackoffError::Transient {
err: reqwest_error,
Expand All @@ -190,13 +190,13 @@ impl GraphQLClient {
/// body.data, it will also error, as this shouldn't be possible.
///
/// If successful, it will return body.data, unwrapped
pub(crate) fn handle_response<Q: GraphQLQuery>(
pub(crate) async fn handle_response<Q: GraphQLQuery>(
response: Response,
endpoint_kind: EndpointKind,
) -> Result<Q::ResponseData, RoverClientError> {
let response_status = response.status();
tracing::debug!(response_status = ?response_status, response_headers = ?response.headers());
match response.json::<GraphQLResponse<Q::ResponseData>>() {
match response.json::<GraphQLResponse<Q::ResponseData>>().await {
Ok(response_body) => {
if let Some(response_body_errors) = response_body.errors {
handle_graphql_body_errors(response_body_errors)?;
Expand Down Expand Up @@ -316,8 +316,8 @@ mod tests {
assert_eq!(actual_error, expected_error);
}

#[test]
fn test_successful_response() {
#[tokio::test]
async fn test_successful_response() {
let server = MockServer::start();
let success_path = "/throw-me-a-frickin-bone-here";
let success_mock = server.mock(|when, then| {
Expand All @@ -332,21 +332,23 @@ mod tests {
Some(Duration::from_secs(3)),
);

let response = graphql_client.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
);
let response = graphql_client
.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
)
.await;

let mock_hits = success_mock.hits();

assert_eq!(mock_hits, 1);
assert!(response.is_ok())
}

#[test]
fn test_unrecoverable_server_error() {
#[tokio::test]
async fn test_unrecoverable_server_error() {
let server = MockServer::start();
let internal_server_error_path = "/this-is-me-in-a-nutshell";
let internal_server_error_mock = server.mock(|when, then| {
Expand All @@ -361,21 +363,23 @@ mod tests {
Some(Duration::from_secs(3)),
);

let response = graphql_client.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
);
let response = graphql_client
.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
)
.await;

let mock_hits = internal_server_error_mock.hits();

assert!(mock_hits > 1);
assert!(response.is_err());
}

#[test]
fn test_unrecoverable_client_error() {
#[tokio::test]
async fn test_unrecoverable_client_error() {
let server = MockServer::start();
let not_found_path = "/austin-powers-the-musical";
let not_found_mock = server.mock(|when, then| {
Expand All @@ -390,12 +394,14 @@ mod tests {
Some(Duration::from_secs(3)),
);

let response = graphql_client.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
);
let response = graphql_client
.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
)
.await;

let mock_hits = not_found_mock.hits();

Expand All @@ -405,8 +411,8 @@ mod tests {
assert!(error.to_string().contains("Not Found"));
}

#[test]
fn test_timeout_error() {
#[tokio::test]
async fn test_timeout_error() {
let server = MockServer::start();
let timeout_path = "/i-timeout-easily";
let timeout_mock = server.mock(|when, then| {
Expand All @@ -416,7 +422,7 @@ mod tests {
.delay(Duration::from_secs(3));
});

let client = reqwest::blocking::ClientBuilder::new()
let client = reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(1))
.build()
.unwrap();
Expand All @@ -426,12 +432,14 @@ mod tests {
Some(Duration::from_secs(3)),
);

let response = graphql_client.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
);
let response = graphql_client
.execute(
"{}".to_string(),
&HeaderMap::new(),
true,
EndpointKind::ApolloStudio,
)
.await;

let mock_hits = timeout_mock.hits();

Expand Down
Loading
Loading