From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Christian Ebner <c.ebner@proxmox.com>, pbs-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-backup 1/2] fix #6373: HTTP level reader heartbeat for proxy connection keepalive
Date: Wed, 15 Apr 2026 10:33:46 +0200 [thread overview]
Message-ID: <1776240724.q4ii7heihr.astroid@yuna.none> (raw)
In-Reply-To: <20260129122700.448448-3-c.ebner@proxmox.com>
On January 29, 2026 1:26 pm, Christian Ebner wrote:
> Backup readers can have long periods of idle connections, e.g. if a
> backup snapshot has been mounted and all relevant chunks are locally
> cached or a backup session with previous metadata archive not needing
> to fetch new contents while the backup is ongoing.
>
> Proxies like e.g. HAProxy might however close idle connections for
> better resource handling [0,1], even multiplexed HTTP/2 connections as
> are being used for the Proxmox Backup Sever backup/reader protocol.
>
> This mainly affects the backup reader, while the backup writer will
> do indexing and chunk uploads anyways.
but if the storage is slow, there might not be chunk traffic for a few
seconds as well?
> Therefore, perform heartbeat traffic in the backup reader http2 client
> when no other requests are being send. To do so, an new `heartbeat`
> API endpoint is introduced as part of the backup reader protocol,
> returning empty on GET requests. By making this part of the backup
> reader protocol, this is limited to active reader HTTP/2 sessions.
>
> On the client side the heartbeat is send out periodically whenever a
> timeout is being reached, the timeout however being reset if other
> requests are being performed via the http2 client.
>
> Since older servers do not provide the new API path, ignore errors
> as the response is not strictly necessary for the connection to
> remain established.
>
> The timeout is currently only being performed if the timeout value
> in seconds is given via the PBS_READER_HEARTBEAT_TIMEOUT.
s/timeout/heartbeat/ ?
>
> Testing was performed using HAProxy with the Proxmox Backup Server
> as backend using the following 5 second connection idle timeouts
> as configuration parameters in haproxy.cfg:
> ```
> ...
> defaults
> ...
> timeout connect 5000
> timeout client 5000
> timeout server 5000
> ..
>
> frontend http-in
> bind *:8007
> mode tcp
> default_backend pbs
>
> backend pbs
> mode tcp
> http-reuse always
> server pbs <PBS-IP>:8007 verify none
> ```
>
> As command invocation:
> ```
> PBS_READER_HEARTBEAT_TIMEOUT=1 proxmox-backup-client mount <snapshot> <archive> \
> <mountpoint> --repository <user-and-realm>@<PROXY-IP>:<datastore> --verbose
> ```
>
> [0] https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/#4-timeout%20client
> [1] https://www.haproxy.com/documentation/haproxy-configuration-manual/latest/#4.2-timeout%20http-keep-alive
>
> Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=6373
> Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
> ---
> pbs-client/src/backup_reader.rs | 69 ++++++++++++++++++++++++++++++---
> src/api2/reader/mod.rs | 9 ++++-
> 2 files changed, 72 insertions(+), 6 deletions(-)
>
> diff --git a/pbs-client/src/backup_reader.rs b/pbs-client/src/backup_reader.rs
> index 88cba599b..95236bef2 100644
> --- a/pbs-client/src/backup_reader.rs
> +++ b/pbs-client/src/backup_reader.rs
> @@ -1,10 +1,13 @@
> use anyhow::{format_err, Error};
> use std::fs::File;
> use std::io::{Seek, SeekFrom, Write};
> +use std::str::FromStr;
> use std::sync::Arc;
> +use std::time::Duration;
>
> use futures::future::AbortHandle;
> use serde_json::{json, Value};
> +use tokio::sync::mpsc;
>
> use pbs_api_types::{BackupArchiveName, BackupDir, BackupNamespace, MANIFEST_BLOB_NAME};
> use pbs_datastore::data_blob::DataBlob;
> @@ -18,26 +21,65 @@ use pbs_tools::sha::sha256;
>
> use super::{H2Client, HttpClient};
>
> +enum HeartBeatMsg {
> + Reset,
> + Abort,
> +}
> +
> /// Backup Reader
> pub struct BackupReader {
> h2: H2Client,
> abort: AbortHandle,
> crypt_config: Option<Arc<CryptConfig>>,
> + heartbeat: Option<mpsc::Sender<HeartBeatMsg>>,
> }
>
> impl Drop for BackupReader {
> fn drop(&mut self) {
> + self.send_msg_heartbeat(HeartBeatMsg::Abort);
> self.abort.abort();
> }
> }
>
> impl BackupReader {
> fn new(h2: H2Client, abort: AbortHandle, crypt_config: Option<Arc<CryptConfig>>) -> Arc<Self> {
> - Arc::new(Self {
> - h2,
> - abort,
> - crypt_config,
> - })
> + let timeout = match std::env::var("PBS_READER_HEARTBEAT_TIMEOUT") {
> + Ok(val) => u64::from_str(&val).map(Duration::from_secs).ok(),
if the var is set but contains a bogus value, shouldn't that be an
error?
> + Err(_err) => None,
> + };
> +
> + if let Some(timeout) = timeout {
> + let (send, mut recv) = mpsc::channel(1);
> + let backup_reader = Arc::new(Self {
> + h2,
> + abort,
> + crypt_config,
> + heartbeat: Some(send),
> + });
> + let reader_cloned = Arc::clone(&backup_reader);
> +
> + tokio::spawn(async move {
> + loop {
> + match tokio::time::timeout(timeout, recv.recv()).await {
> + Ok(Some(HeartBeatMsg::Reset)) => (),
> + Ok(Some(HeartBeatMsg::Abort)) | Ok(None) => break,
> + Err(_elapsed) => {
> + // connection idle timeout reached, send heatbeat
> + let _ = reader_cloned.h2.get("heartbeat", None).await;
> + }
> + }
> + }
> + });
> +
> + backup_reader
> + } else {
> + Arc::new(Self {
> + h2,
> + abort,
> + crypt_config,
> + heartbeat: None,
> + })
> + }
> }
>
> /// Create a new instance by upgrading the connection at '/api2/json/reader'
> @@ -79,21 +121,25 @@ impl BackupReader {
>
> /// Execute a GET request
> pub async fn get(&self, path: &str, param: Option<Value>) -> Result<Value, Error> {
> + self.send_msg_heartbeat(HeartBeatMsg::Reset);
> self.h2.get(path, param).await
> }
>
> /// Execute a PUT request
> pub async fn put(&self, path: &str, param: Option<Value>) -> Result<Value, Error> {
> + self.send_msg_heartbeat(HeartBeatMsg::Reset);
> self.h2.put(path, param).await
> }
>
> /// Execute a POST request
> pub async fn post(&self, path: &str, param: Option<Value>) -> Result<Value, Error> {
> + self.send_msg_heartbeat(HeartBeatMsg::Reset);
> self.h2.post(path, param).await
> }
>
> /// Execute a GET request and send output to a writer
> pub async fn download<W: Write + Send>(&self, file_name: &str, output: W) -> Result<(), Error> {
> + self.send_msg_heartbeat(HeartBeatMsg::Reset);
> let path = "download";
> let param = json!({ "file-name": file_name });
> self.h2.download(path, Some(param), output).await
> @@ -103,6 +149,7 @@ impl BackupReader {
> ///
> /// This writes random data, and is only useful to test download speed.
> pub async fn speedtest<W: Write + Send>(&self, output: W) -> Result<(), Error> {
> + self.send_msg_heartbeat(HeartBeatMsg::Reset);
> self.h2.download("speedtest", None, output).await
> }
>
> @@ -112,12 +159,14 @@ impl BackupReader {
> digest: &[u8; 32],
> output: W,
> ) -> Result<(), Error> {
> + self.send_msg_heartbeat(HeartBeatMsg::Reset);
> let path = "chunk";
> let param = json!({ "digest": hex::encode(digest) });
> self.h2.download(path, Some(param), output).await
> }
>
> pub fn force_close(self) {
> + self.send_msg_heartbeat(HeartBeatMsg::Abort);
> self.abort.abort();
> }
>
> @@ -205,4 +254,14 @@ impl BackupReader {
>
> Ok(index)
> }
> +
> + /// Send given message to the heartbeat closure.
> + ///
> + /// All errors are being ignored, since they cannot be handled anyways.
> + fn send_msg_heartbeat(&self, msg: HeartBeatMsg) {
> + let _ = self
> + .heartbeat
> + .as_ref()
> + .map(|heartbeat| heartbeat.try_send(msg));
> + }
> }
> diff --git a/src/api2/reader/mod.rs b/src/api2/reader/mod.rs
> index f7adc366f..1c673d7a6 100644
> --- a/src/api2/reader/mod.rs
> +++ b/src/api2/reader/mod.rs
> @@ -18,7 +18,7 @@ use proxmox_router::{
> http_err, list_subdirs_api_method, ApiHandler, ApiMethod, ApiResponseFuture, Permission,
> Router, RpcEnvironment, SubdirMap,
> };
> -use proxmox_schema::{BooleanSchema, ObjectSchema};
> +use proxmox_schema::{api, BooleanSchema, ObjectSchema};
> use proxmox_sortable_macro::sortable;
>
> use pbs_api_types::{
> @@ -227,6 +227,7 @@ const READER_API_SUBDIRS: SubdirMap = &[
> "download",
> &Router::new().download(&API_METHOD_DOWNLOAD_FILE),
> ),
> + ("heartbeat", &Router::new().download(&API_METHOD_HEARTBEAT)),
shouldn't this be `.get`
> ("speedtest", &Router::new().download(&API_METHOD_SPEEDTEST)),
> ];
>
> @@ -433,3 +434,9 @@ fn speedtest(
>
> future::ok(response).boxed()
> }
> +
> +#[api()]
> +/// HTTP level heartbeat to avoid proxies closing long running idle backup reader connections.
> +pub async fn heartbeat(_rpcenv: &mut dyn RpcEnvironment) -> Result<(), Error> {
and this a regular `fn`?
> + Ok(())
> +}
> --
> 2.47.3
>
>
>
>
>
>
next prev parent reply other threads:[~2026-04-15 8:33 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-01-29 12:26 [RFC proxmox{,-backup} 0/3] fix #6373: HTTP level keepalive for http2 backup reader connection Christian Ebner
2026-01-29 12:26 ` [PATCH proxmox 1/1] rest-server: add request logfilter by method and path in h2 service Christian Ebner
2026-01-29 12:26 ` [PATCH proxmox-backup 1/2] fix #6373: HTTP level reader heartbeat for proxy connection keepalive Christian Ebner
2026-04-15 8:33 ` Fabian Grünbichler [this message]
2026-04-15 8:45 ` Christian Ebner
2026-04-15 11:00 ` Fabian Grünbichler
2026-04-15 11:22 ` Christian Ebner
2026-04-15 11:48 ` Fabian Grünbichler
2026-04-15 11:56 ` Christian Ebner
2026-01-29 12:27 ` [PATCH proxmox-backup 2/2] api: h2service: avoid logging heartbeat requests to task log Christian Ebner
2026-04-10 17:11 ` [RFC proxmox{,-backup} 0/3] fix #6373: HTTP level keepalive for http2 backup reader connection Christian Ebner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=1776240724.q4ii7heihr.astroid@yuna.none \
--to=f.gruenbichler@proxmox.com \
--cc=c.ebner@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox