public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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: Thu, 12 Feb 2026 10:55:59 +0100	[thread overview]
Message-ID: <076ebe87-db27-47a0-b3fc-893254fac0db@proxmox.com> (raw)
In-Reply-To: <441d3c9b-8987-46c1-8da9-e9cde4429aab@proxmox.com>


On 2/11/26 13:40, Christian Ebner wrote:
> On 2/11/26 1:12 PM, Robert Obkircher wrote:
>>
>> 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.
>
> Can you elaborate a bit on this? Not sure if I understand what the
> benefits for this would be. 
If those 5 variables share the same cache line then only one core can
write to any of them at a time because they all share a single lock
that bounces back and forth. I don't think it makes a measurable
difference for counting requests though.
>
>>> +}
>>> +
>>> +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.
>
> Okay, can adapt this accordingly, thanks!
>
>>> +        }
>>> +    }
>>> +}
>>> +
>>> +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.
>
> This padding was chosen exactly because of that doc comment and
> since other users of `SharedMemory` do the same 4k page size padding.
>
> Will however have a look at the implementation details there, now
> that you sparked my interest, maybe that sheds some light.
>
> Or maybe you already have further details since you are working on
> the patches to make the code compatible with kernels with
> non-default page sizes?
That's why I was thinking about it, but I don't have further details yet.
>> 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.
>
> Allowing to extend with further fields in the future has proven to
> be beneficial, yes. E.g. for the token shadow config version as
> mentioned in
> https://lore.proxmox.com/pbs-devel/20260121151408.731516-2-s.rufinatscha@proxmox.com/T/
>
>>> +}
>>> +
>>> +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)
>>> +    }
>>> +}
>




  reply	other threads:[~2026-02-12  9:55 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
2026-02-11 12:41     ` Christian Ebner
2026-02-12  9:55       ` Robert Obkircher [this message]
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=076ebe87-db27-47a0-b3fc-893254fac0db@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal