From: Robert Obkircher <r.obkircher@proxmox.com>
To: Christian Ebner <c.ebner@proxmox.com>, pbs-devel@lists.proxmox.com
Subject: Re: [PATCH v1 2/6] s3-client: add persistent shared request counters for client
Date: Wed, 11 Feb 2026 13:13:41 +0100 [thread overview]
Message-ID: <2a88c886-9146-4448-9c5a-d27097f48eeb@proxmox.com> (raw)
In-Reply-To: <20260209091533.156902-3-c.ebner@proxmox.com>
On 2/9/26 10:14, Christian Ebner wrote:
> Implements atomic counters for api requests successfully send to the
> S3 API endpoint via the client, accounting for individual requests
> discriminating based on their method.
>
> Since multiple client instances might exists accessing the API
> concurrently, even in different processes, provide the atomic
> counters via shared memory mapping. This follows along the lines of
> the shared traffic limiter implementation.
>
> The counter mappings are conditionally constructed on client
> instantiation by caller give configuration options.
>
> Keep sequential ordering of the counters with the intent to use them
> for statistics, soft limits and/or notifications within Proxmox
> Backup Server.
>
> Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
> ---
> proxmox-s3-client/Cargo.toml | 4 +
> proxmox-s3-client/debian/control | 2 +
> proxmox-s3-client/examples/s3_client.rs | 1 +
> proxmox-s3-client/src/client.rs | 48 +++++-
> proxmox-s3-client/src/lib.rs | 7 +-
> .../src/shared_request_counters.rs | 152 ++++++++++++++++++
> 6 files changed, 212 insertions(+), 2 deletions(-)
> create mode 100644 proxmox-s3-client/src/shared_request_counters.rs
>
> diff --git a/proxmox-s3-client/Cargo.toml b/proxmox-s3-client/Cargo.toml
> index a50fa715..1e31bca4 100644
> --- a/proxmox-s3-client/Cargo.toml
> +++ b/proxmox-s3-client/Cargo.toml
> @@ -38,7 +38,9 @@ proxmox-base64 = { workspace = true, optional = true }
> proxmox-http = { workspace = true, features = [ "body", "client", "client-trait" ], optional = true }
> proxmox-human-byte.workspace = true
> proxmox-rate-limiter = { workspace = true, features = [ "rate-limiter", "shared-rate-limiter" ], optional = true }
> +proxmox-shared-memory = { workspace = true, optional = true }
> proxmox-schema = { workspace = true, features = [ "api-macro", "api-types" ] }
> +proxmox-sys = { workspace = true, optional = true }
> proxmox-serde.workspace = true
> proxmox-time = {workspace = true, optional = true }
>
> @@ -65,6 +67,8 @@ impl = [
> "dep:proxmox-base64",
> "dep:proxmox-http",
> "dep:proxmox-rate-limiter",
> + "dep:proxmox-shared-memory",
> + "dep:proxmox-sys",
> "dep:proxmox-time",
> ]
>
> diff --git a/proxmox-s3-client/debian/control b/proxmox-s3-client/debian/control
> index 33418881..a534a107 100644
> --- a/proxmox-s3-client/debian/control
> +++ b/proxmox-s3-client/debian/control
> @@ -85,6 +85,8 @@ Depends:
> librust-proxmox-rate-limiter-1+default-dev,
> librust-proxmox-rate-limiter-1+rate-limiter-dev,
> librust-proxmox-rate-limiter-1+shared-rate-limiter-dev,
> + librust-proxmox-shared-memory-1+default-dev,
> + librust-proxmox-sys-1+default-dev,
> librust-proxmox-time-2+default-dev (>= 2.1.0-~~),
> librust-quick-xml-0.36+async-tokio-dev (>= 0.36.1-~~),
> librust-quick-xml-0.36+default-dev (>= 0.36.1-~~),
> diff --git a/proxmox-s3-client/examples/s3_client.rs b/proxmox-s3-client/examples/s3_client.rs
> index ca69971c..329de47a 100644
> --- a/proxmox-s3-client/examples/s3_client.rs
> +++ b/proxmox-s3-client/examples/s3_client.rs
> @@ -40,6 +40,7 @@ async fn run() -> Result<(), anyhow::Error> {
> put_rate_limit: None,
> provider_quirks: Vec::new(),
> rate_limiter_config: None,
> + request_counter_config: None,
> };
>
> // Creating a client instance and connect to api endpoint
> diff --git a/proxmox-s3-client/src/client.rs b/proxmox-s3-client/src/client.rs
> index 83176b39..4a6a702b 100644
> --- a/proxmox-s3-client/src/client.rs
> +++ b/proxmox-s3-client/src/client.rs
> @@ -1,5 +1,6 @@
> use std::path::{Path, PathBuf};
> use std::str::FromStr;
> +use std::sync::atomic::Ordering;
> use std::sync::{Arc, Mutex};
> use std::time::{Duration, Instant};
>
> @@ -32,6 +33,7 @@ use crate::response_reader::{
> CopyObjectResponse, DeleteObjectsResponse, GetObjectResponse, HeadObjectResponse,
> ListBucketsResponse, ListObjectsV2Response, PutObjectResponse, ResponseReader,
> };
> +use crate::shared_request_counters::SharedRequestCounters;
>
> /// Default timeout for s3 api requests.
> pub const S3_HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(30 * 60);
> @@ -74,6 +76,21 @@ pub struct S3RateLimiterConfig {
> burst_out: Option<u64>,
> }
>
> +/// Options for the s3 client's shared request counters
> +pub struct S3RequestCounterOptions {
> + /// ID for the memory mapped file
> + pub id: String,
> + /// Base path for the shared memory mapped file
> + pub base_path: PathBuf,
> + /// User for the to be created shared memory mapped file and folders
> + pub user: User,
> +}
> +
> +/// Configuration for the s3 client's shared request counters
> +pub struct S3RequestCounterConfig {
> + options: S3RequestCounterOptions,
> +}
> +
> /// Configuration options for client
> pub struct S3ClientOptions {
> /// Endpoint to access S3 object store.
> @@ -100,6 +117,8 @@ pub struct S3ClientOptions {
> pub provider_quirks: Vec<ProviderQuirks>,
> /// Configuration options for the shared rate limiter.
> pub rate_limiter_config: Option<S3RateLimiterConfig>,
> + /// Configuration options for the client's shared request counters.
> + pub request_counter_config: Option<S3RequestCounterConfig>,
> }
>
> impl S3ClientOptions {
> @@ -110,6 +129,7 @@ impl S3ClientOptions {
> bucket: Option<String>,
> common_prefix: String,
> rate_limiter_options: Option<S3RateLimiterOptions>,
> + request_counter_options: Option<S3RequestCounterOptions>,
> ) -> Self {
> let rate_limiter_config = rate_limiter_options.map(|options| S3RateLimiterConfig {
> options,
> @@ -118,6 +138,8 @@ impl S3ClientOptions {
> rate_out: config.rate_out.map(|human_bytes| human_bytes.as_u64()),
> burst_out: config.burst_out.map(|human_bytes| human_bytes.as_u64()),
> });
> + let request_counter_config =
> + request_counter_options.map(|options| S3RequestCounterConfig { options });
> Self {
> endpoint: config.endpoint,
> port: config.port,
> @@ -131,6 +153,7 @@ impl S3ClientOptions {
> put_rate_limit: config.put_rate_limit,
> provider_quirks: config.provider_quirks.unwrap_or_default(),
> rate_limiter_config,
> + request_counter_config,
> }
> }
> }
> @@ -141,6 +164,7 @@ pub struct S3Client {
> options: S3ClientOptions,
> authority: Authority,
> put_rate_limiter: Option<Arc<Mutex<RateLimiter>>>,
> + request_counters: Option<Arc<SharedRequestCounters>>,
> }
>
> impl S3Client {
> @@ -213,6 +237,21 @@ impl S3Client {
> }
> }
>
> + let request_counters = if let Some(config) = options.request_counter_config.as_ref() {
> + let path = config
> + .options
> + .base_path
> + .join(format!("{}.shmem", config.options.id));
> + let request_counters = SharedRequestCounters::open_shared_memory_mapped(
> + &path,
> + config.options.user.clone(),
> + )
> + .context("failed to mmap shared S3 request counters")?;
> + Some(Arc::new(request_counters))
> + } else {
> + None
> + };
> +
> let client = Client::builder(TokioExecutor::new()).build::<_, Body>(https_connector);
>
> let authority_template = if let Some(port) = options.port {
> @@ -241,6 +280,7 @@ impl S3Client {
> options,
> authority,
> put_rate_limiter,
> + request_counters,
> })
> }
>
> @@ -392,7 +432,13 @@ impl S3Client {
> };
>
> match response {
> - Ok(Ok(response)) => return Ok(response),
> + Ok(Ok(response)) => {
> + if let Some(counters) = self.request_counters.as_ref() {
> + let _prev = counters.increment(parts.method.clone(), Ordering::SeqCst);
> + }
> +
> + return Ok(response);
> + }
> Ok(Err(err)) => {
> if retry >= MAX_S3_HTTP_REQUEST_RETRY - 1 {
> return Err(err.into());
> diff --git a/proxmox-s3-client/src/lib.rs b/proxmox-s3-client/src/lib.rs
> index d02fd0dc..ceee41a2 100644
> --- a/proxmox-s3-client/src/lib.rs
> +++ b/proxmox-s3-client/src/lib.rs
> @@ -21,7 +21,8 @@ pub use aws_sign_v4::uri_decode;
> mod client;
> #[cfg(feature = "impl")]
> pub use client::{
> - S3Client, S3ClientOptions, S3PathPrefix, S3RateLimiterOptions, S3_HTTP_REQUEST_TIMEOUT,
> + S3Client, S3ClientOptions, S3PathPrefix, S3RateLimiterOptions, S3RequestCounterOptions,
> + S3_HTTP_REQUEST_TIMEOUT,
> };
> #[cfg(feature = "impl")]
> mod timestamps;
> @@ -33,3 +34,7 @@ mod object_key;
> pub use object_key::S3ObjectKey;
> #[cfg(feature = "impl")]
> mod response_reader;
> +#[cfg(feature = "impl")]
> +mod shared_request_counters;
> +#[cfg(feature = "impl")]
> +pub use shared_request_counters::SharedRequestCounters;
> diff --git a/proxmox-s3-client/src/shared_request_counters.rs b/proxmox-s3-client/src/shared_request_counters.rs
> new file mode 100644
> index 00000000..b236490b
> --- /dev/null
> +++ b/proxmox-s3-client/src/shared_request_counters.rs
> @@ -0,0 +1,152 @@
> +use std::mem::MaybeUninit;
> +use std::path::Path;
> +use std::sync::atomic::{AtomicU64, Ordering};
> +
> +use anyhow::{bail, Error};
> +use hyper::http::method::Method;
> +use nix::sys::stat::Mode;
> +use nix::unistd::User;
> +
> +use proxmox_shared_memory::{Init, SharedMemory};
> +use proxmox_sys::fs::CreateOptions;
> +
> +const MEMORY_PAGE_SIZE: usize = 4096;
> +/// Generated via openssl::sha::sha256(b"Proxmox shared request counters v1.0")[0..8]
> +const PROXMOX_SHARED_REQUEST_COUNTERS_1_0: [u8; 8] = [224, 110, 88, 252, 26, 77, 180, 5];
> +
> +#[repr(C)]
> +#[derive(Default)]
> +struct RequestCounters {
> + delete: AtomicU64,
> + get: AtomicU64,
> + head: AtomicU64,
> + post: AtomicU64,
> + put: AtomicU64,
It probably doesn't matter (especially not with SeqCst), but aligning
each field to 64 bytes would avoid false sharing.
> +}
> +
> +impl Init for RequestCounters {
> + fn initialize(this: &mut MaybeUninit<Self>) {
> + unsafe {
> + let this = &mut *this.as_mut_ptr();
> + *this = RequestCounters::default();
If the memory is actually uninitialized this is undefined behavior
because `*this =` drops the previous value. The memory from mmap is
initialized, but I think it would still be better to use ptr::write.
> + }
> + }
> +}
> +
> +impl RequestCounters {
> + /// Increment the counter for given method, following the provided memory ordering constrains.
> + ///
> + /// Returns the previously stored value.
> + pub fn increment(&self, method: Method, ordering: Ordering) -> u64 {
> + match method {
> + Method::DELETE => self.delete.fetch_add(1, ordering),
> + Method::GET => self.get.fetch_add(1, ordering),
> + Method::HEAD => self.head.fetch_add(1, ordering),
> + Method::POST => self.post.fetch_add(1, ordering),
> + Method::PUT => self.put.fetch_add(1, ordering),
> + _ => 0,
> + }
> + }
> +
> + /// Load current counter state for given method, following the provided memory ordering constrains
> + pub fn load(&self, method: Method, ordering: Ordering) -> u64 {
> + match method {
> + Method::DELETE => self.delete.load(ordering),
> + Method::GET => self.get.load(ordering),
> + Method::HEAD => self.head.load(ordering),
> + Method::POST => self.post.load(ordering),
> + Method::PUT => self.put.load(ordering),
> + _ => 0,
> + }
> + }
> +
> + /// Reset all counters, following the provided memory ordering constrains
> + pub fn reset(&self, ordering: Ordering) {
> + self.delete.store(0, ordering);
> + self.get.store(0, ordering);
> + self.head.store(0, ordering);
> + self.post.store(0, ordering);
> + self.put.store(0, ordering);
> + }
> +}
> +
> +#[repr(C)]
> +struct MappableRequestCounters {
> + magic: [u8; 8],
> + counters: RequestCounters,
> + _page_size_padding: [u8; MEMORY_PAGE_SIZE
> + - PROXMOX_SHARED_REQUEST_COUNTERS_1_0.len()
> + - std::mem::size_of::<RequestCounters>()],
Why do we need to hard-code a possibly incorrect assumption about the
page size? I know `SharedMemory` claims that the size needs to be a
multiple of 4096, but I don't understand why.
I guess this implicitly prevents unused padding at the end of the
struct which would be problematic if we add a field and run two
processes with different versions at the same time, but that doesn't
explain the page size.
> +}
> +
> +impl Init for MappableRequestCounters {
> + fn initialize(this: &mut MaybeUninit<Self>) {
> + unsafe {
> + let this = &mut *this.as_mut_ptr();
> + this.magic = PROXMOX_SHARED_REQUEST_COUNTERS_1_0;
> + this.counters = RequestCounters::default();
> + }
> + }
> +
> + fn check_type_magic(this: &MaybeUninit<Self>) -> Result<(), Error> {
> + unsafe {
> + let this = &*this.as_ptr();
> + if this.magic != PROXMOX_SHARED_REQUEST_COUNTERS_1_0 {
> + bail!("incorrect magic number for request counters detected");
> + }
> + proxmox_shared_memory::check_subtype(&this.counters)?;
> + }
> + Ok(())
> + }
> +}
> +
> +/// Atomic counters storing per-request method counts for the client.
> +///
> +/// If set, the counts can be filtered based on a path prefix.
> +pub struct SharedRequestCounters {
> + shared_memory: SharedMemory<MappableRequestCounters>,
> +}
> +
> +impl SharedRequestCounters {
> + /// Create a new shared counter instance.
> + ///
> + /// Opens or creates mmap file and accesses it via shared memory mapping.
> + pub fn open_shared_memory_mapped<P: AsRef<Path>>(path: P, user: User) -> Result<Self, Error> {
> + let path = path.as_ref();
> + if let Some(parent) = path.parent() {
> + let dir_opts = CreateOptions::new()
> + .perm(Mode::from_bits_truncate(0o770))
> + .owner(user.uid)
> + .group(user.gid);
> +
> + proxmox_sys::fs::create_path(parent, Some(dir_opts), Some(dir_opts))?;
> + }
> +
> + let file_opts = CreateOptions::new()
> + .perm(Mode::from_bits_truncate(0o660))
> + .owner(user.uid)
> + .group(user.gid);
> + let shared_memory = SharedMemory::open(path, file_opts)?;
> + Ok(Self { shared_memory })
> + }
> +
> + /// Increment the counter for given method, following the provided memory ordering constrains
> + ///
> + /// Returns the previously stored value.
> + pub fn increment(&self, method: Method, ordering: Ordering) -> u64 {
> + self.shared_memory
> + .data()
> + .counters
> + .increment(method, ordering)
> + }
> +
> + /// Load current counter state for given method, following the provided memory ordering constrains
> + pub fn load(&self, method: Method, ordering: Ordering) -> u64 {
> + self.shared_memory.data().counters.load(method, ordering)
> + }
> +
> + /// Reset all counters, following the provided memory ordering constrains
> + pub fn reset(&self, ordering: Ordering) {
> + self.shared_memory.data().counters.reset(ordering)
> + }
> +}
next prev parent reply other threads:[~2026-02-11 12:13 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-09 9:15 [PATCH v1 00/17] partially fix #6563: add s3 request and traffic counter statistics Christian Ebner
2026-02-09 9:15 ` [PATCH v1 1/6] shared-memory: drop check for mmap file being located on tmpfs Christian Ebner
2026-02-09 9:15 ` [PATCH v1 2/6] s3-client: add persistent shared request counters for client Christian Ebner
2026-02-11 12:13 ` Robert Obkircher [this message]
2026-02-11 12:41 ` Christian Ebner
2026-02-12 9:55 ` Robert Obkircher
2026-02-12 10:19 ` Christian Ebner
2026-02-13 13:37 ` Christian Ebner
2026-02-09 9:15 ` [PATCH v1 3/6] s3-client: add counters for upload/download traffic Christian Ebner
2026-02-09 9:15 ` [PATCH v1 4/6] s3-client: account for upload traffic on successful request sending Christian Ebner
2026-02-09 9:15 ` [PATCH v1 5/6] s3-client: account for downloaded bytes in incoming response body Christian Ebner
2026-02-09 9:15 ` [PATCH v1 6/6] pbs-api-types: define api type for s3 request statistics Christian Ebner
2026-02-09 9:15 ` [PATCH v1 01/11] datastore: collect request statistics for s3 backed datastores Christian Ebner
2026-02-09 9:15 ` [PATCH v1 02/11] datastore: expose request counters " Christian Ebner
2026-02-09 9:15 ` [PATCH v1 03/11] api: s3: add endpoint to reset s3 request counters Christian Ebner
2026-02-09 9:15 ` [PATCH v1 04/11] bin: s3: expose request counter reset method as cli command Christian Ebner
2026-02-09 9:15 ` [PATCH v1 05/11] datastore: add helper method to get datastore backend type Christian Ebner
2026-02-09 9:15 ` [PATCH v1 06/11] ui: improve variable name indirectly fixing typo Christian Ebner
2026-02-09 9:15 ` [PATCH v1 07/11] ui: datastore summary: move store to be part of summary panel Christian Ebner
2026-02-09 9:15 ` [PATCH v1 08/11] ui: expose s3 request counter statistics in the datastore summary Christian Ebner
2026-02-09 9:15 ` [PATCH v1 09/11] metrics: collect s3 datastore statistics as rrd metrics Christian Ebner
2026-02-11 16:29 ` Christian Ebner
2026-02-09 9:15 ` [PATCH v1 10/11] api: admin: expose s3 statistics in datastore rrd data Christian Ebner
2026-02-09 9:15 ` [PATCH v1 11/11] partially fix #6563: ui: expose s3 rrd charts in datastore summary Christian Ebner
2026-02-09 9:39 ` [PATCH v1 00/17] partially fix #6563: add s3 request and traffic counter statistics Christian Ebner
2026-02-16 12:15 ` superseded: " 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=2a88c886-9146-4448-9c5a-d27097f48eeb@proxmox.com \
--to=r.obkircher@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