Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

jetstream: time out recv after a ttl

sometimes the socket gets disrupted and we otherwise just wait forever

this *could* (and maybe should) be done by detecting missed pongs instead.

Changed files
+31 -4
jetstream
+2
jetstream/src/error.rs
···
CompressionDictionaryError(io::Error),
#[error("failed to send ping or pong: {0}")]
PingPongError(#[from] tokio_tungstenite::tungstenite::Error),
+
#[error("no messages received within ttl")]
+
NoMessagesReceived,
#[error("jetstream event receiver closed")]
ReceiverClosedError,
}
+29 -4
jetstream/src/lib.rs
···
Receiver,
Sender,
},
+
time::timeout,
};
use tokio_tungstenite::{
connect_async,
···
/// can help prevent that if your consumer sometimes pauses, at a cost of higher memory
/// usage while events are buffered.
pub channel_size: usize,
+
/// How long since the last jetstream message before we consider the connection dead
+
///
+
/// Default: 15s
+
pub liveliness_ttl: Duration,
}
impl Default for JetstreamConfig {
···
omit_user_agent_jetstream_info: false,
replay_on_reconnect: false,
channel_size: 4096, // a few seconds of firehose buffer
+
liveliness_ttl: Duration::from_secs(15),
}
}
}
···
let (send_channel, receive_channel) = channel(self.config.channel_size);
let replay_on_reconnect = self.config.replay_on_reconnect;
+
let liveliness_ttl = self.config.liveliness_ttl;
let build_request = self.config.get_request_builder();
tokio::task::spawn(async move {
···
if let Ok((ws_stream, _)) = connect_async(req).await {
let t_connected = Instant::now();
log::info!("jetstream connected. starting websocket task...");
-
if let Err(e) =
-
websocket_task(dict, ws_stream, send_channel.clone(), &mut last_cursor)
-
.await
+
if let Err(e) = websocket_task(
+
dict,
+
ws_stream,
+
send_channel.clone(),
+
&mut last_cursor,
+
liveliness_ttl,
+
)
+
.await
{
match e {
JetstreamEventError::ReceiverClosedError => {
···
JetstreamEventError::CompressionDictionaryError(_) => {
#[cfg(feature="metrics")]
counter!("jetstream_disconnects", "reason" => "zstd", "fatal" => "no").increment(1);
+
}
+
JetstreamEventError::NoMessagesReceived => {
+
#[cfg(feature="metrics")]
+
counter!("jetstream_disconnects", "reason" => "ttl", "fatal" => "no").increment(1);
}
JetstreamEventError::PingPongError(_) => {
#[cfg(feature="metrics")]
···
ws: WebSocketStream<MaybeTlsStream<TcpStream>>,
send_channel: JetstreamSender,
last_cursor: &mut Option<Cursor>,
+
liveliness_ttl: Duration,
) -> Result<(), JetstreamEventError> {
// TODO: Use the write half to allow the user to change configuration settings on the fly.
let (mut socket_write, mut socket_read) = ws.split();
let mut closing_connection = false;
loop {
-
match socket_read.next().await {
+
let next = match timeout(liveliness_ttl, socket_read.next()).await {
+
Ok(n) => n,
+
Err(_) => {
+
log::warn!("jetstream no events for {liveliness_ttl:?}, closing");
+
_ = socket_write.close().await;
+
return Err(JetstreamEventError::NoMessagesReceived);
+
}
+
};
+
match next {
Some(Ok(message)) => match message {
Message::Text(json) => {
#[cfg(feature = "metrics")]