Send websocket pings from both the client and the server

Remove the client-only logic for sending protobuf pings.

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
Co-Authored-By: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Max Brunsfeld 2022-03-04 13:53:40 -08:00
parent c61a1bd659
commit 9017a1363b
9 changed files with 174 additions and 92 deletions

6
Cargo.lock generated
View file

@ -998,7 +998,6 @@ dependencies = [
name = "clock" name = "clock"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"rpc",
"smallvec", "smallvec",
] ]
@ -2236,6 +2235,7 @@ dependencies = [
"tiny-skia", "tiny-skia",
"tree-sitter", "tree-sitter",
"usvg", "usvg",
"util",
"waker-fn", "waker-fn",
] ]
@ -3959,6 +3959,7 @@ dependencies = [
"async-lock", "async-lock",
"async-tungstenite", "async-tungstenite",
"base64 0.13.0", "base64 0.13.0",
"clock",
"futures", "futures",
"gpui", "gpui",
"log", "log",
@ -3972,6 +3973,7 @@ dependencies = [
"smol", "smol",
"smol-timeout", "smol-timeout",
"tempdir", "tempdir",
"util",
"zstd", "zstd",
] ]
@ -5574,7 +5576,6 @@ name = "util"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clock",
"futures", "futures",
"log", "log",
"rand 0.8.3", "rand 0.8.3",
@ -5959,6 +5960,7 @@ name = "zed-server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-io",
"async-sqlx-session", "async-sqlx-session",
"async-std", "async-std",
"async-trait", "async-trait",

View file

@ -137,8 +137,8 @@ struct ClientState {
credentials: Option<Credentials>, credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>), status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>, entity_id_extractors: HashMap<TypeId, Box<dyn Send + Sync + Fn(&dyn AnyTypedEnvelope) -> u64>>,
_maintain_connection: Option<Task<()>>, _reconnect_task: Option<Task<()>>,
heartbeat_interval: Duration, reconnect_interval: Duration,
models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>, models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>,
models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>, models_by_message_type: HashMap<TypeId, AnyWeakModelHandle>,
model_types_by_message_type: HashMap<TypeId, TypeId>, model_types_by_message_type: HashMap<TypeId, TypeId>,
@ -168,8 +168,8 @@ impl Default for ClientState {
credentials: None, credentials: None,
status: watch::channel_with(Status::SignedOut), status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(), entity_id_extractors: Default::default(),
_maintain_connection: None, _reconnect_task: None,
heartbeat_interval: Duration::from_secs(5), reconnect_interval: Duration::from_secs(5),
models_by_message_type: Default::default(), models_by_message_type: Default::default(),
models_by_entity_type_and_remote_id: Default::default(), models_by_entity_type_and_remote_id: Default::default(),
model_types_by_message_type: Default::default(), model_types_by_message_type: Default::default(),
@ -236,7 +236,7 @@ impl Client {
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn tear_down(&self) { pub fn tear_down(&self) {
let mut state = self.state.write(); let mut state = self.state.write();
state._maintain_connection.take(); state._reconnect_task.take();
state.message_handlers.clear(); state.message_handlers.clear();
state.models_by_message_type.clear(); state.models_by_message_type.clear();
state.models_by_entity_type_and_remote_id.clear(); state.models_by_entity_type_and_remote_id.clear();
@ -283,21 +283,13 @@ impl Client {
match status { match status {
Status::Connected { .. } => { Status::Connected { .. } => {
let heartbeat_interval = state.heartbeat_interval; state._reconnect_task = None;
let this = self.clone();
let foreground = cx.foreground();
state._maintain_connection = Some(cx.foreground().spawn(async move {
loop {
foreground.timer(heartbeat_interval).await;
let _ = this.request(proto::Ping {}).await;
}
}));
} }
Status::ConnectionLost => { Status::ConnectionLost => {
let this = self.clone(); let this = self.clone();
let foreground = cx.foreground(); let foreground = cx.foreground();
let heartbeat_interval = state.heartbeat_interval; let reconnect_interval = state.reconnect_interval;
state._maintain_connection = 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 = Duration::from_millis(100);
while let Err(error) = this.authenticate_and_connect(&cx).await { while let Err(error) = this.authenticate_and_connect(&cx).await {
@ -311,12 +303,12 @@ impl Client {
foreground.timer(delay).await; foreground.timer(delay).await;
delay = delay delay = delay
.mul_f32(rng.gen_range(1.0..=2.0)) .mul_f32(rng.gen_range(1.0..=2.0))
.min(heartbeat_interval); .min(reconnect_interval);
} }
})); }));
} }
Status::SignedOut | Status::UpgradeRequired => { Status::SignedOut | Status::UpgradeRequired => {
state._maintain_connection.take(); state._reconnect_task.take();
} }
_ => {} _ => {}
} }
@ -548,7 +540,11 @@ impl Client {
} }
async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) { async fn set_connection(self: &Arc<Self>, conn: Connection, cx: &AsyncAppContext) {
let (connection_id, handle_io, mut incoming) = self.peer.add_connection(conn).await; let executor = cx.background();
let (connection_id, handle_io, mut incoming) = self
.peer
.add_connection(conn, move |duration| executor.timer(duration))
.await;
cx.foreground() cx.foreground()
.spawn({ .spawn({
let cx = cx.clone(); let cx = cx.clone();
@ -940,26 +936,6 @@ mod tests {
use crate::test::{FakeHttpClient, FakeServer}; use crate::test::{FakeHttpClient, FakeServer};
use gpui::TestAppContext; use gpui::TestAppContext;
#[gpui::test(iterations = 10)]
async fn test_heartbeat(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
let user_id = 5;
let mut client = Client::new(FakeHttpClient::with_404_response());
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
cx.foreground().advance_clock(Duration::from_secs(10));
let ping = server.receive::<proto::Ping>().await.unwrap();
server.respond(ping.receipt(), proto::Ack {}).await;
cx.foreground().advance_clock(Duration::from_secs(10));
let ping = server.receive::<proto::Ping>().await.unwrap();
server.respond(ping.receipt(), proto::Ack {}).await;
client.disconnect(&cx.to_async()).unwrap();
assert!(server.receive::<proto::Ping>().await.is_err());
}
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_reconnection(cx: &mut TestAppContext) { async fn test_reconnection(cx: &mut TestAppContext) {
cx.foreground().forbid_parking(); cx.foreground().forbid_parking();

View file

@ -75,7 +75,8 @@ impl FakeServer {
} }
let (client_conn, server_conn, _) = Connection::in_memory(cx.background()); let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
let (connection_id, io, incoming) = peer.add_connection(server_conn).await; let (connection_id, io, incoming) =
peer.add_test_connection(server_conn, cx.background()).await;
cx.background().spawn(io).detach(); cx.background().spawn(io).detach();
let mut state = state.lock(); let mut state = state.lock();
state.connection_id = Some(connection_id); state.connection_id = Some(connection_id);

View file

@ -14,6 +14,7 @@ test-support = ["backtrace", "dhat", "env_logger", "collections/test-support"]
[dependencies] [dependencies]
collections = { path = "../collections" } collections = { path = "../collections" }
gpui_macros = { path = "../gpui_macros" } gpui_macros = { path = "../gpui_macros" }
util = { path = "../util" }
sum_tree = { path = "../sum_tree" } sum_tree = { path = "../sum_tree" }
async-task = "4.0.3" async-task = "4.0.3"
backtrace = { version = "0.3", optional = true } backtrace = { version = "0.3", optional = true }

View file

@ -26,7 +26,9 @@ rsa = "0.4"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
smol-timeout = "0.6" smol-timeout = "0.6"
zstd = "0.9" zstd = "0.9"
clock = { path = "../clock" }
gpui = { path = "../gpui", optional = true } gpui = { path = "../gpui", optional = true }
util = { path = "../util" }
[build-dependencies] [build-dependencies]
prost-build = "0.8" prost-build = "0.8"

View file

@ -94,6 +94,7 @@ pub struct ConnectionState {
Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, barrier::Sender)>>>>>, Arc<Mutex<Option<HashMap<u32, oneshot::Sender<(proto::Envelope, barrier::Sender)>>>>>,
} }
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(2);
const WRITE_TIMEOUT: Duration = Duration::from_secs(10); const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
impl Peer { impl Peer {
@ -104,14 +105,20 @@ impl Peer {
}) })
} }
pub async fn add_connection( pub async fn add_connection<F, Fut, Out>(
self: &Arc<Self>, self: &Arc<Self>,
connection: Connection, connection: Connection,
create_timer: F,
) -> ( ) -> (
ConnectionId, ConnectionId,
impl Future<Output = anyhow::Result<()>> + Send, impl Future<Output = anyhow::Result<()>> + Send,
BoxStream<'static, Box<dyn AnyTypedEnvelope>>, BoxStream<'static, Box<dyn AnyTypedEnvelope>>,
) { )
where
F: Send + Fn(Duration) -> Fut,
Fut: Send + Future<Output = Out>,
Out: Send,
{
// For outgoing messages, use an unbounded channel so that application code // For outgoing messages, use an unbounded channel so that application code
// can always send messages without yielding. For incoming messages, use a // can always send messages without yielding. For incoming messages, use a
// bounded channel so that other peers will receive backpressure if they send // bounded channel so that other peers will receive backpressure if they send
@ -121,7 +128,7 @@ impl Peer {
let connection_id = ConnectionId(self.next_connection_id.fetch_add(1, SeqCst)); let connection_id = ConnectionId(self.next_connection_id.fetch_add(1, SeqCst));
let connection_state = ConnectionState { let connection_state = ConnectionState {
outgoing_tx, outgoing_tx: outgoing_tx.clone(),
next_message_id: Default::default(), next_message_id: Default::default(),
response_channels: Arc::new(Mutex::new(Some(Default::default()))), response_channels: Arc::new(Mutex::new(Some(Default::default()))),
}; };
@ -131,39 +138,43 @@ impl Peer {
let this = self.clone(); let this = self.clone();
let response_channels = connection_state.response_channels.clone(); let response_channels = connection_state.response_channels.clone();
let handle_io = async move { let handle_io = async move {
let result = 'outer: loop { let _end_connection = util::defer(|| {
response_channels.lock().take();
this.connections.write().remove(&connection_id);
});
loop {
let read_message = reader.read_message().fuse(); let read_message = reader.read_message().fuse();
futures::pin_mut!(read_message); futures::pin_mut!(read_message);
loop { loop {
futures::select_biased! { futures::select_biased! {
outgoing = outgoing_rx.next().fuse() => match outgoing { outgoing = outgoing_rx.next().fuse() => match outgoing {
Some(outgoing) => { Some(outgoing) => {
match writer.write_message(&outgoing).timeout(WRITE_TIMEOUT).await { if let Some(result) = writer.write_message(&outgoing).timeout(WRITE_TIMEOUT).await {
None => break 'outer Err(anyhow!("timed out writing RPC message")), result.context("failed to write RPC message")?;
Some(Err(result)) => break 'outer Err(result).context("failed to write RPC message"), } else {
_ => {} Err(anyhow!("timed out writing message"))?;
} }
} }
None => break 'outer Ok(()), None => return Ok(()),
}, },
incoming = read_message => match incoming { incoming = read_message => {
Ok(incoming) => { let incoming = incoming.context("received invalid rpc message")?;
if incoming_tx.send(incoming).await.is_err() { if incoming_tx.send(incoming).await.is_err() {
break 'outer Ok(()); return Ok(());
}
break;
}
Err(error) => {
break 'outer Err(error).context("received invalid RPC message")
} }
break;
}, },
_ = create_timer(KEEPALIVE_INTERVAL).fuse() => {
if let Some(result) = writer.ping().timeout(WRITE_TIMEOUT).await {
result.context("failed to send websocket ping")?;
} else {
Err(anyhow!("timed out sending websocket ping"))?;
}
}
} }
} }
}; }
response_channels.lock().take();
this.connections.write().remove(&connection_id);
result
}; };
let response_channels = connection_state.response_channels.clone(); let response_channels = connection_state.response_channels.clone();
@ -191,18 +202,31 @@ impl Peer {
None None
} else { } else {
if let Some(envelope) = proto::build_typed_envelope(connection_id, incoming) { proto::build_typed_envelope(connection_id, incoming).or_else(|| {
Some(envelope)
} else {
log::error!("unable to construct a typed envelope"); log::error!("unable to construct a typed envelope");
None None
} })
} }
} }
}); });
(connection_id, handle_io, incoming_rx.boxed()) (connection_id, handle_io, incoming_rx.boxed())
} }
#[cfg(any(test, feature = "test-support"))]
pub async fn add_test_connection(
self: &Arc<Self>,
connection: Connection,
executor: Arc<gpui::executor::Background>,
) -> (
ConnectionId,
impl Future<Output = anyhow::Result<()>> + Send,
BoxStream<'static, Box<dyn AnyTypedEnvelope>>,
) {
let executor = executor.clone();
self.add_connection(connection, move |duration| executor.timer(duration))
.await
}
pub fn disconnect(&self, connection_id: ConnectionId) { pub fn disconnect(&self, connection_id: ConnectionId) {
self.connections.write().remove(&connection_id); self.connections.write().remove(&connection_id);
} }
@ -349,15 +373,21 @@ mod tests {
let (client1_to_server_conn, server_to_client_1_conn, _) = let (client1_to_server_conn, server_to_client_1_conn, _) =
Connection::in_memory(cx.background()); Connection::in_memory(cx.background());
let (client1_conn_id, io_task1, client1_incoming) = let (client1_conn_id, io_task1, client1_incoming) = client1
client1.add_connection(client1_to_server_conn).await; .add_test_connection(client1_to_server_conn, cx.background())
let (_, io_task2, server_incoming1) = server.add_connection(server_to_client_1_conn).await; .await;
let (_, io_task2, server_incoming1) = server
.add_test_connection(server_to_client_1_conn, cx.background())
.await;
let (client2_to_server_conn, server_to_client_2_conn, _) = let (client2_to_server_conn, server_to_client_2_conn, _) =
Connection::in_memory(cx.background()); Connection::in_memory(cx.background());
let (client2_conn_id, io_task3, client2_incoming) = let (client2_conn_id, io_task3, client2_incoming) = client2
client2.add_connection(client2_to_server_conn).await; .add_test_connection(client2_to_server_conn, cx.background())
let (_, io_task4, server_incoming2) = server.add_connection(server_to_client_2_conn).await; .await;
let (_, io_task4, server_incoming2) = server
.add_test_connection(server_to_client_2_conn, cx.background())
.await;
executor.spawn(io_task1).detach(); executor.spawn(io_task1).detach();
executor.spawn(io_task2).detach(); executor.spawn(io_task2).detach();
@ -440,10 +470,12 @@ mod tests {
let (client_to_server_conn, server_to_client_conn, _) = let (client_to_server_conn, server_to_client_conn, _) =
Connection::in_memory(cx.background()); Connection::in_memory(cx.background());
let (client_to_server_conn_id, io_task1, mut client_incoming) = let (client_to_server_conn_id, io_task1, mut client_incoming) = client
client.add_connection(client_to_server_conn).await; .add_test_connection(client_to_server_conn, cx.background())
let (server_to_client_conn_id, io_task2, mut server_incoming) = .await;
server.add_connection(server_to_client_conn).await; let (server_to_client_conn_id, io_task2, mut server_incoming) = server
.add_test_connection(server_to_client_conn, cx.background())
.await;
executor.spawn(io_task1).detach(); executor.spawn(io_task1).detach();
executor.spawn(io_task2).detach(); executor.spawn(io_task2).detach();
@ -538,10 +570,12 @@ mod tests {
let (client_to_server_conn, server_to_client_conn, _) = let (client_to_server_conn, server_to_client_conn, _) =
Connection::in_memory(cx.background()); Connection::in_memory(cx.background());
let (client_to_server_conn_id, io_task1, mut client_incoming) = let (client_to_server_conn_id, io_task1, mut client_incoming) = client
client.add_connection(client_to_server_conn).await; .add_test_connection(client_to_server_conn, cx.background())
let (server_to_client_conn_id, io_task2, mut server_incoming) = .await;
server.add_connection(server_to_client_conn).await; let (server_to_client_conn_id, io_task2, mut server_incoming) = server
.add_test_connection(server_to_client_conn, cx.background())
.await;
executor.spawn(io_task1).detach(); executor.spawn(io_task1).detach();
executor.spawn(io_task2).detach(); executor.spawn(io_task2).detach();
@ -649,7 +683,9 @@ mod tests {
let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background()); let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background());
let client = Peer::new(); let client = Peer::new();
let (connection_id, io_handler, mut incoming) = client.add_connection(client_conn).await; let (connection_id, io_handler, mut incoming) = client
.add_test_connection(client_conn, cx.background())
.await;
let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel(); let (mut io_ended_tx, mut io_ended_rx) = postage::barrier::channel();
executor executor
@ -683,7 +719,9 @@ mod tests {
let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background()); let (client_conn, mut server_conn, _) = Connection::in_memory(cx.background());
let client = Peer::new(); let client = Peer::new();
let (connection_id, io_handler, mut incoming) = client.add_connection(client_conn).await; let (connection_id, io_handler, mut incoming) = client
.add_test_connection(client_conn, cx.background())
.await;
executor.spawn(io_handler).detach(); executor.spawn(io_handler).detach();
executor executor
.spawn(async move { incoming.next().await }) .spawn(async move { incoming.next().await })

View file

@ -318,6 +318,13 @@ where
self.stream.send(WebSocketMessage::Binary(buffer)).await?; self.stream.send(WebSocketMessage::Binary(buffer)).await?;
Ok(()) Ok(())
} }
pub async fn ping(&mut self) -> Result<(), WebSocketError> {
self.stream
.send(WebSocketMessage::Ping(Default::default()))
.await?;
Ok(())
}
} }
impl<S> MessageStream<S> impl<S> MessageStream<S>

View file

@ -16,6 +16,7 @@ required-features = ["seed-support"]
collections = { path = "../collections" } collections = { path = "../collections" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
anyhow = "1.0.40" anyhow = "1.0.40"
async-io = "1.3"
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
async-trait = "0.1.50" async-trait = "0.1.50"
async-tungstenite = "0.16" async-tungstenite = "0.16"

View file

@ -6,6 +6,7 @@ use super::{
AppState, AppState,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use async_io::Timer;
use async_std::task; use async_std::task;
use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream}; use async_tungstenite::{tungstenite::protocol::Role, WebSocketStream};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
@ -16,7 +17,12 @@ use rpc::{
Connection, ConnectionId, Peer, TypedEnvelope, Connection, ConnectionId, Peer, TypedEnvelope,
}; };
use sha1::{Digest as _, Sha1}; use sha1::{Digest as _, Sha1};
use std::{any::TypeId, future::Future, sync::Arc, time::Instant}; use std::{
any::TypeId,
future::Future,
sync::Arc,
time::{Duration, Instant},
};
use store::{Store, Worktree}; use store::{Store, Worktree};
use surf::StatusCode; use surf::StatusCode;
use tide::log; use tide::log;
@ -40,10 +46,13 @@ pub struct Server {
notifications: Option<mpsc::UnboundedSender<()>>, notifications: Option<mpsc::UnboundedSender<()>>,
} }
pub trait Executor { pub trait Executor: Send + Clone {
type Timer: Send + Future;
fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F); fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F);
fn timer(&self, duration: Duration) -> Self::Timer;
} }
#[derive(Clone)]
pub struct RealExecutor; pub struct RealExecutor;
const MESSAGE_COUNT_PER_PAGE: usize = 100; const MESSAGE_COUNT_PER_PAGE: usize = 100;
@ -167,8 +176,18 @@ impl Server {
) -> impl Future<Output = ()> { ) -> impl Future<Output = ()> {
let mut this = self.clone(); let mut this = self.clone();
async move { async move {
let (connection_id, handle_io, mut incoming_rx) = let (connection_id, handle_io, mut incoming_rx) = this
this.peer.add_connection(connection).await; .peer
.add_connection(connection, {
let executor = executor.clone();
move |duration| {
let timer = executor.timer(duration);
async move {
timer.await;
}
}
})
.await;
if let Some(send_connection_id) = send_connection_id.as_mut() { if let Some(send_connection_id) = send_connection_id.as_mut() {
let _ = send_connection_id.send(connection_id).await; let _ = send_connection_id.send(connection_id).await;
@ -883,9 +902,15 @@ impl Server {
} }
impl Executor for RealExecutor { impl Executor for RealExecutor {
type Timer = Timer;
fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) { fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
task::spawn(future); task::spawn(future);
} }
fn timer(&self, duration: Duration) -> Self::Timer {
Timer::after(duration)
}
} }
fn broadcast<F>( fn broadcast<F>(
@ -1759,7 +1784,7 @@ mod tests {
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_peer_disconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking(); cx_a.foreground().forbid_parking();
let lang_registry = Arc::new(LanguageRegistry::new()); let lang_registry = Arc::new(LanguageRegistry::new());
let fs = FakeFs::new(cx_a.background()); let fs = FakeFs::new(cx_a.background());
@ -1817,16 +1842,39 @@ mod tests {
.await .await
.unwrap(); .unwrap();
// See that a guest has joined as client A. // Client A sees that a guest has joined.
project_a project_a
.condition(&cx_a, |p, _| p.collaborators().len() == 1) .condition(&cx_a, |p, _| p.collaborators().len() == 1)
.await; .await;
// Drop client B's connection and ensure client A observes client B leaving the worktree. // Drop client B's connection and ensure client A observes client B leaving the project.
client_b.disconnect(&cx_b.to_async()).unwrap(); client_b.disconnect(&cx_b.to_async()).unwrap();
project_a project_a
.condition(&cx_a, |p, _| p.collaborators().len() == 0) .condition(&cx_a, |p, _| p.collaborators().len() == 0)
.await; .await;
// Rejoin the project as client B
let _project_b = Project::remote(
project_id,
client_b.clone(),
client_b.user_store.clone(),
lang_registry.clone(),
fs.clone(),
&mut cx_b.to_async(),
)
.await
.unwrap();
// Client A sees that a guest has re-joined.
project_a
.condition(&cx_a, |p, _| p.collaborators().len() == 1)
.await;
// Simulate connection loss for client B and ensure client A observes client B leaving the project.
server.disconnect_client(client_b.current_user_id(cx_b));
project_a
.condition(&cx_a, |p, _| p.collaborators().len() == 0)
.await;
} }
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
@ -5031,9 +5079,15 @@ mod tests {
} }
impl Executor for Arc<gpui::executor::Background> { impl Executor for Arc<gpui::executor::Background> {
type Timer = BoxFuture<'static, ()>;
fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) { fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
self.spawn(future).detach(); self.spawn(future).detach();
} }
fn timer(&self, duration: Duration) -> Self::Timer {
self.as_ref().timer(duration).boxed()
}
} }
fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> { fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {