mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
Live join: a large Welcome still downloading is not given up on at the idle timeout (#1664)
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
use figcraft_collab::message::{self, ClientMsg, PROTOCOL, Presence, Resolve, ServerMsg};
|
||||
use figcraft_engine::live::{Event, Status};
|
||||
use std::time::Duration;
|
||||
// `std::time::Instant` on native builds; the browser's clock in the web build.
|
||||
use web_time::Instant;
|
||||
|
||||
@@ -25,6 +26,7 @@ use crate::exchange::{self, Exchange, Then};
|
||||
use crate::inbox::Inbox;
|
||||
use crate::invite::Endpoint;
|
||||
use crate::join::{GuestConfig, Next, Outcome, Resuming, Seen, Taken, after, closed, hello, resolved, take, too_large, unresolved};
|
||||
use crate::limits::NetLimits;
|
||||
use crate::requests::Publish;
|
||||
use crate::throttle::{PRESENCE_INTERVAL, Throttle, earliest};
|
||||
|
||||
@@ -55,6 +57,13 @@ impl Conn {
|
||||
fn new(now: Instant) -> Self {
|
||||
Self { started: now, opened: false, welcomed: false, seen: Seen::default(), last_ping: now, last_heard: now }
|
||||
}
|
||||
|
||||
/// How long the host may stay silent. Until the welcome is whole, the document is arriving as
|
||||
/// one message that doesn't count as heard before it is complete, so a large file on a slow
|
||||
/// link gets the long reply timeout instead of the idle one.
|
||||
fn silence_limit(&self, l: &NetLimits) -> Duration {
|
||||
if self.welcomed { l.idle_timeout } else { l.request_reply_timeout }
|
||||
}
|
||||
}
|
||||
|
||||
/// A joined (or joining) session over a [`Socket`].
|
||||
@@ -315,7 +324,7 @@ impl<S: Socket> Driver<S> {
|
||||
// A managing connection waits for each answer.
|
||||
(true, true) => [Some(c.last_heard + l.request_reply_timeout), None, None],
|
||||
(true, false) => {
|
||||
[Some(c.last_ping + l.keepalive), Some(c.last_heard + l.idle_timeout), if c.welcomed { self.throttle.due() } else { None }]
|
||||
[Some(c.last_ping + l.keepalive), Some(c.last_heard + c.silence_limit(l)), if c.welcomed { self.throttle.due() } else { None }]
|
||||
}
|
||||
});
|
||||
earliest(conn.into_iter().flatten().chain([self.reconnect_at]))
|
||||
@@ -332,7 +341,7 @@ impl<S: Socket> Driver<S> {
|
||||
}
|
||||
let l = self.cfg.limits;
|
||||
let Some(c) = self.conn.as_ref() else { return };
|
||||
let (opened, started, last_heard, last_ping) = (c.opened, c.started, c.last_heard, c.last_ping);
|
||||
let (opened, started, last_heard, last_ping, silence) = (c.opened, c.started, c.last_heard, c.last_ping, c.silence_limit(&l));
|
||||
if !opened {
|
||||
if now >= started + self.cfg.connect_timeout {
|
||||
self.dropped(format!("no answer from {}", self.server().display()), now);
|
||||
@@ -345,7 +354,7 @@ impl<S: Socket> Driver<S> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if now >= last_heard + l.idle_timeout {
|
||||
if now >= last_heard + silence {
|
||||
return self.end(Outcome::Retry("the host stopped answering".into()), now);
|
||||
}
|
||||
let ok = self.throttle.take_due(now).is_none_or(|p| self.write(&ClientMsg::Presence(p)));
|
||||
|
||||
@@ -184,6 +184,38 @@ fn pings_go_out_while_presence_does_and_the_pongs_keep_the_connection() {
|
||||
assert!(pings >= 5, "a ping every 15 s ({pings})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_welcome_still_downloading_gets_the_long_reply_timeout() {
|
||||
let t0 = Instant::now();
|
||||
let (mut d, inbox) = start(Script::default(), t0);
|
||||
d.opened(t0);
|
||||
let limits = GuestConfig::new(invite(), "Ada".into()).limits;
|
||||
// The welcome (the whole document) is one message that is not heard until complete: past the
|
||||
// idle timeout the join must not be given up.
|
||||
let past_idle = t0 + limits.idle_timeout + Duration::from_secs(1);
|
||||
d.tick(past_idle);
|
||||
assert!(!d.done());
|
||||
assert_eq!(d.sock.closes, 0);
|
||||
assert_eq!(statuses(&inbox.drain()), vec![Status::Connecting]);
|
||||
// It does arrive; from then on a silent host is dropped at the idle timeout.
|
||||
d.message(&welcome(), past_idle);
|
||||
let silent = past_idle + limits.idle_timeout;
|
||||
d.tick(silent - ms(1));
|
||||
assert_eq!(d.sock.closes, 0);
|
||||
d.tick(silent);
|
||||
assert_eq!(d.sock.closes, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_host_that_never_welcomes_is_given_up_on_at_the_reply_timeout() {
|
||||
let t0 = Instant::now();
|
||||
let (mut d, _inbox) = start(Script::default(), t0);
|
||||
d.opened(t0);
|
||||
let limit = t0 + GuestConfig::new(invite(), "Ada".into()).limits.request_reply_timeout;
|
||||
d.tick(limit);
|
||||
assert_eq!(d.sock.closes, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_silent_host_counts_as_dropped_and_the_client_reconnects() {
|
||||
let t0 = Instant::now();
|
||||
|
||||
@@ -695,7 +695,9 @@ resuming with its offline edits, and leaving the session going back to snapshot
|
||||
(resuming as the same client after a drop), presence held until the welcome and changes made
|
||||
for an earlier connection dropped (the sync client sends them again once caught up), presence throttled to about 30 updates a second
|
||||
(latest wins), a `Ping` after 15 s without sending (browsers don't expose WebSocket ping frames;
|
||||
the host answers `Pong`), a connection silent for 45 s counted as dropped, reconnects with the
|
||||
the host answers `Pong`), a connection silent for 45 s counted as dropped (5 minutes before the
|
||||
welcome: it carries the whole document as one message, which a large file on a slow link takes
|
||||
a while to download, and nothing counts as heard until it is complete), reconnects with the
|
||||
same growing pauses after a live connection drops, refusals and the end of the session final,
|
||||
and a first join that can't reach the host failing at once. The browser layer only feeds it the
|
||||
socket's events and one `setTimeout` for its next deadline, and every arrival wakes egui
|
||||
|
||||
Reference in New Issue
Block a user