Introduce client-side timeout when trying to connect

This commit is contained in:
Antonio Scandurra 2022-10-12 15:11:07 +02:00
parent a19783919c
commit cc56fa9ea6
2 changed files with 112 additions and 29 deletions

View File

@ -53,6 +53,8 @@ lazy_static! {
} }
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(client, [Authenticate]); actions!(client, [Authenticate]);
@ -330,7 +332,7 @@ impl Client {
let reconnect_interval = state.reconnect_interval; let reconnect_interval = state.reconnect_interval;
state._reconnect_task = Some(cx.spawn(|cx| async move { state._reconnect_task = Some(cx.spawn(|cx| async move {
let mut rng = StdRng::from_entropy(); let mut rng = StdRng::from_entropy();
let mut delay = Duration::from_millis(100); let mut delay = INITIAL_RECONNECTION_DELAY;
while let Err(error) = this.authenticate_and_connect(true, &cx).await { while let Err(error) = this.authenticate_and_connect(true, &cx).await {
log::error!("failed to connect {}", error); log::error!("failed to connect {}", error);
if matches!(*this.status().borrow(), Status::ConnectionError) { if matches!(*this.status().borrow(), Status::ConnectionError) {
@ -661,33 +663,42 @@ impl Client {
self.set_status(Status::Reconnecting, cx); self.set_status(Status::Reconnecting, cx);
} }
match self.establish_connection(&credentials, cx).await { let mut timeout = cx.background().timer(CONNECTION_TIMEOUT).fuse();
Ok(conn) => { futures::select_biased! {
self.state.write().credentials = Some(credentials.clone()); connection = self.establish_connection(&credentials, cx).fuse() => {
if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { match connection {
write_credentials_to_keychain(&credentials, cx).log_err(); Ok(conn) => {
} self.state.write().credentials = Some(credentials.clone());
self.set_connection(conn, cx).await; if !read_from_keychain && IMPERSONATE_LOGIN.is_none() {
Ok(()) write_credentials_to_keychain(&credentials, cx).log_err();
} }
Err(EstablishConnectionError::Unauthorized) => { self.set_connection(conn, cx).await;
self.state.write().credentials.take(); Ok(())
if read_from_keychain { }
cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); Err(EstablishConnectionError::Unauthorized) => {
self.set_status(Status::SignedOut, cx); self.state.write().credentials.take();
self.authenticate_and_connect(false, cx).await if read_from_keychain {
} else { cx.platform().delete_credentials(&ZED_SERVER_URL).log_err();
self.set_status(Status::ConnectionError, cx); self.set_status(Status::SignedOut, cx);
Err(EstablishConnectionError::Unauthorized)? self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
Err(EstablishConnectionError::Unauthorized)?
}
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
self.set_status(Status::ConnectionError, cx);
Err(error)?
}
} }
} }
Err(EstablishConnectionError::UpgradeRequired) => { _ = timeout => {
self.set_status(Status::UpgradeRequired, cx);
Err(EstablishConnectionError::UpgradeRequired)?
}
Err(error) => {
self.set_status(Status::ConnectionError, cx); self.set_status(Status::ConnectionError, cx);
Err(error)? Err(anyhow!("timed out trying to establish connection"))
} }
} }
} }
@ -1169,6 +1180,76 @@ mod tests {
assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token
} }
#[gpui::test(iterations = 10)]
async fn test_connection_timeout(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
deterministic.forbid_parking();
let user_id = 5;
let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
let mut status = client.status();
// Time out when client tries to connect.
client.override_authenticate(move |cx| {
cx.foreground().spawn(async move {
Ok(Credentials {
user_id,
access_token: "token".into(),
})
})
});
client.override_establish_connection(|_, cx| {
cx.foreground().spawn(async move {
future::pending::<()>().await;
unreachable!()
})
});
let auth_and_connect = cx.spawn({
let client = client.clone();
|cx| async move { client.authenticate_and_connect(false, &cx).await }
});
deterministic.run_until_parked();
assert!(matches!(status.next().await, Some(Status::Connecting)));
deterministic.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
status.next().await,
Some(Status::ConnectionError { .. })
));
auth_and_connect.await.unwrap_err();
// Allow the connection to be established.
let server = FakeServer::for_client(user_id, &client, cx).await;
assert!(matches!(
status.next().await,
Some(Status::Connected { .. })
));
// Disconnect client.
server.forbid_connections();
server.disconnect();
while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {}
// Time out when re-establishing the connection.
server.allow_connections();
client.override_establish_connection(|_, cx| {
cx.foreground().spawn(async move {
future::pending::<()>().await;
unreachable!()
})
});
deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY);
assert!(matches!(
status.next().await,
Some(Status::Reconnecting { .. })
));
deterministic.advance_clock(CONNECTION_TIMEOUT);
assert!(matches!(
status.next().await,
Some(Status::ReconnectionError { .. })
));
}
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_authenticating_more_than_once( async fn test_authenticating_more_than_once(
cx: &mut TestAppContext, cx: &mut TestAppContext,

View File

@ -101,10 +101,12 @@ impl FakeServer {
} }
pub fn disconnect(&self) { pub fn disconnect(&self) {
self.peer.disconnect(self.connection_id()); if self.state.lock().connection_id.is_some() {
let mut state = self.state.lock(); self.peer.disconnect(self.connection_id());
state.connection_id.take(); let mut state = self.state.lock();
state.incoming.take(); state.connection_id.take();
state.incoming.take();
}
} }
pub fn auth_count(&self) -> usize { pub fn auth_count(&self) -> usize {