From ee0e02fc1345e55b7c24eb014366a8a18aceadff Mon Sep 17 00:00:00 2001 From: Remco Jongschaap Date: Sat, 20 Jun 2026 03:07:06 +0200 Subject: [PATCH] send keepalive pings on user websockets to survive idle proxy timeouts (#1486) --- bin/core/src/api/ws/mod.rs | 3 +++ bin/core/src/api/ws/terminal.rs | 18 +++++++++++++++++- bin/core/src/api/ws/update.rs | 12 +++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/bin/core/src/api/ws/mod.rs b/bin/core/src/api/ws/mod.rs index 7c986a7ed..9d71c7369 100644 --- a/bin/core/src/api/ws/mod.rs +++ b/bin/core/src/api/ws/mod.rs @@ -24,6 +24,9 @@ use crate::{ mod terminal; mod update; +pub(super) const WS_KEEP_ALIVE_INTERVAL: std::time::Duration = + std::time::Duration::from_secs(30); + pub fn router() -> Router { Router::new() // Periphery facing diff --git a/bin/core/src/api/ws/terminal.rs b/bin/core/src/api/ws/terminal.rs index a6534d45f..e7959c9a3 100644 --- a/bin/core/src/api/ws/terminal.rs +++ b/bin/core/src/api/ws/terminal.rs @@ -117,9 +117,25 @@ async fn forward_ws_channel( }; let periphery_to_core = async { + let mut keep_alive = + tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL); loop { // Already adheres to cancellation token - match periphery_receiver.recv().await { + let recv = tokio::select! { + // Websocket keep-alive ping + _ = keep_alive.tick() => { + if client_send + .send(ws::Message::Ping(Bytes::new())) + .await + .is_err() + { + break; + } + continue; + } + recv = periphery_receiver.recv() => recv, + }; + match recv { Ok(Ok(bytes)) => { if let Err(e) = client_send.send(ws::Message::Binary(bytes.into())).await diff --git a/bin/core/src/api/ws/update.rs b/bin/core/src/api/ws/update.rs index 74d751917..b5503e9a5 100644 --- a/bin/core/src/api/ws/update.rs +++ b/bin/core/src/api/ws/update.rs @@ -3,6 +3,7 @@ use axum::{ extract::{WebSocketUpgrade, ws::Message}, response::IntoResponse, }; +use bytes::Bytes; use futures_util::{SinkExt, StreamExt}; use komodo_client::entities::{ ResourceTarget, permission::PermissionLevel, user::User, @@ -38,10 +39,19 @@ pub async fn handler( let cancel_clone = cancel.clone(); tokio::spawn(async move { + let mut keep_alive = + tokio::time::interval(super::WS_KEEP_ALIVE_INTERVAL); loop { - // poll for updates off the receiver / await cancel. + // poll for updates off the receiver / await cancel / send keep-alive ping. let update = select! { _ = cancel_clone.cancelled() => break, + _ = keep_alive.tick() => { + if ws_sender.send(Message::Ping(Bytes::new())).await.is_err() { + cancel_clone.cancel(); + break; + } + continue; + } update = receiver.recv() => {update.expect("failed to recv update msg")} };