Skip to content

feat(rust): give the transport a retry policy, as a unit of its own - #732

Draft
wkirschenmann wants to merge 2 commits into
wk/feat/rust-pkcs12-v2from
wk/feat/rust-retry-v2
Draft

wkirschenmann wants to merge 2 commits into
wk/feat/rust-pkcs12-v2from
wk/feat/rust-retry-v2

Conversation

@wkirschenmann

Copy link
Copy Markdown
Contributor

Give the transport a retry policy, configured as its own unit.

RetryConfig carries what ArmoniK's other clients hand grpc-dotnet, so a deployment behaves the
same whichever client talks to it: five attempts, one second growing by 1.5 to a five-second
ceiling, replaying on Unavailable, Aborted and Unknown. It is reached as config.retry, and
applied by whoever makes the calls: a channel carries no notion of a call, so nothing in connect
reads it.

Four options travel with the rest of the vocabulary, under no prefix: MaxAttempts,
InitialBackOff, MaxBackOff, BackOffMultiplier. The unit declares plain fields and no names of
its own; config.rs states the prefix next to the field with embed_prefixed!, as it already does
for tcp, http2 and tls, so the reading and the JSON schema both come from that one
declaration. The capital O is the PascalCase rendering of "back off", produced by rename_all from
the field name alone: no option here carries a rename attribute.

One name differs from the C# client, on purpose. InitialBackOff and MaxBackOff are spelled
exactly as ArmoniK.Api.Client/Options/GrpcClient.cs spells them, and both sides read the same
GrpcClient__ prefix, so those two reach either client. BackOffMultiplier does not: C# spells it
BackoffMultiplier, with a lowercase o. Until the rename lands there, a deployment setting
GrpcClient__BackoffMultiplier is read by C# and not by this crate. No alias covers it, since an
alias would keep the misspelling alive on both sides; the README records the divergence.

RawRetry holds each value in the type that states what it accepts, so a bad one is refused while
the source's key is still known and no option name has to be written next to the check:
MaxAttempts is a NonZeroU32, and 0 - which would mean one try and no replay, exactly like 1

  • is named by the document that spelled it. config_utils gains the generic reader that serves it
    and the multiplier.

The one rule spanning two options goes through TryFrom<RawRetry>, as TlsConfig does with
RawTls: a MaxBackOff below InitialBackOff holds every wait down to the ceiling, so one of the
two does nothing and the source cannot say which was meant. Setting either alone reaches it, since
the other keeps its default.

bounds() is the schedule those options describe, one item per replay and nothing past the last
attempt. The growth goes through Duration::try_from_secs_f64 rather than mul_f64, which panics
on a multiplier a caller set to a negative, infinite or non-numeric value; anything it refuses
settles on the ceiling.

Left for their own PRs, out of #707: the retry! macro and the GrpcStatus trait, which are the
caller-facing loop, together with the fastrand jitter draw only that loop consumes;
MaxRetryBufferPerCall and MaxRetryUnarySize, which bound a replay buffer nothing here holds; and
RetryableStatusCodes as an option, which no other ArmoniK client exposes - the three codes stay a
programmatic field, fixed as C# fixes them.

10 tests. In retry_config.rs: the defaults are the ones the other clients use; the bounds grow by
the multiplier and stop at the ceiling; there is one bound per replay and none beyond; a multiplier
no Duration can hold settles on the ceiling. In config.rs, through a document: the options
default; each one is read; a fractional multiplier survives the trip through text; zero attempts is
refused by the option rather than read as one; a ceiling below the initial back off is refused and
names both options; a ceiling equal to it is a constant wait rather than an error. The existing
an_empty_option_reads_as_its_default and the_named_option_carries_the_prefix... cases gain the
four options, and tests/schema.rs pins them in the vocabulary and pins RetryableStatusCodes out
of it.

From packages/rust, all green: cargo test -p armonik-transport --all-features (90 passed, 0
failed), cargo clippy -p armonik-transport --all-features --all-targets and cargo clippy -p armonik --all-features --all-targets (0 warnings), cargo check -p armonik-transport --no-default-features (0 warnings), cargo fmt --check. cargo run -p armonik-transport --features schema --example generate_schema lists the four options as strings.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
1478 1247 84% 0% 🟢

New Files

No new covered files...

Modified Files

No covered modified files...

updated for commit: f35c83a by action🐍

@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-retry-v2 branch from 999d0bf to 90f020a Compare August 7, 2026 16:35
@wkirschenmann
wkirschenmann changed the base branch from wk/feat/rust-config-schema-v2 to wk/feat/rust-pkcs12-v2 August 7, 2026 16:36
@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-retry-v2 branch from 90f020a to 428134d Compare August 8, 2026 14:23
@wkirschenmann
wkirschenmann marked this pull request as ready for review August 8, 2026 18:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 428134dbf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +135 to +138
/// What each wait is multiplied by; empty for the default.
#[serde(deserialize_with = "crate::config_utils::optional_parsed")]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
back_off_multiplier: Option<f64>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject invalid backoff multipliers

When GrpcClient__BackOffMultiplier is 0, negative, NaN, or infinite, optional_parsed accepts it because f64::from_str accepts these spellings. The resulting policy silently produces zero delays or, via unwrap_or(ceiling) in bounds(), jumps to the maximum delay instead of rejecting the invalid configuration, so an operator typo changes retry behavior unpredictably.

Useful? React with 👍 / 👎.

pub use connect::{connect, https_connector, ConnectionError};
pub use http2_config::Http2Config;
pub use proxy::{ProxyConfig, ProxyError, ProxySource};
pub use retry_config::RetryConfig;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-export RetryConfig through the client facade

When downstream users depend on the main armonik crate, this new public configuration type is available only as armonik::transport::RetryConfig; the configuration facade in packages/rust/armonik/src/client/mod.rs:8-11 re-exports every other HttpConfig unit but omits this one. Code that needs to name or build a retry policy therefore cannot follow the existing armonik::client::* API and must reach into the transport namespace, so RetryConfig should be added to that re-export list.

Useful? React with 👍 / 👎.

@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-retry-v2 branch 2 times, most recently from 4229ce5 to b800c44 Compare August 9, 2026 09:28
`RetryConfig` carries what ArmoniK's other clients hand grpc-dotnet, so a deployment behaves the
same whichever client talks to it: five attempts, one second growing by 1.5 to a five-second
ceiling, replaying on `Unavailable`, `Aborted` and `Unknown`. It is reached as `config.retry` and
applied by whoever makes the calls, since a channel carries no notion of a call.

Four options travel with the rest of the vocabulary, under no prefix: `MaxAttempts`,
`InitialBackOff`, `MaxBackOff`, `BackOffMultiplier`. The unit declares plain fields and no names of
its own, and `config.rs` states the prefix next to the field with `embed_prefixed!`, the way `tcp`,
`http2` and `tls` are already stated, so the reading and the JSON schema both follow from that one
declaration. The capital O is the PascalCase rendering of "back off", which `rename_all` produces
from the field name alone; no option here carries a rename attribute.

`InitialBackOff` and `MaxBackOff` are spelled as the C# client spells them, so a deployment setting
`GrpcClient__InitialBackOff` reaches both. `BackOffMultiplier` differs from that client's
`BackoffMultiplier` until the rename lands there; the README says so, and no alias covers it, since
an alias would keep the misspelling alive on both sides.

`RawRetry` holds each value in the type that states what it accepts, so a bad one is refused while
the source's key is still known and no option name has to be written next to the check:
`MaxAttempts` is a `NonZeroU32`, and `0`, which would mean one try and no replay exactly like `1`,
is named by the document that spelled it. `config_utils` gains the generic reader serving it and
the multiplier.

The one rule spanning two options goes through `TryFrom<RawRetry>`, as `TlsConfig` does with
`RawTls`: a `MaxBackOff` below `InitialBackOff` holds every wait down to the ceiling, so one of the
two options does nothing and the source cannot say which was meant. Setting either alone reaches
it, since the other keeps its default.

`bounds()` multiplies through `Duration::try_from_secs_f64` rather than `mul_f64`, which panics on
a negative, infinite or non-numeric multiplier; the fields are public, so such a value is
reachable, and anything the constructor refuses settles on the ceiling instead.

From packages/rust: cargo test -p armonik-transport --all-features, 90 passed 0 failed; cargo
clippy -p armonik-transport --all-features --all-targets and cargo clippy -p armonik --all-features
--all-targets, 0 warnings; cargo check -p armonik-transport --no-default-features, 0 warnings;
cargo fmt --check and RUSTDOCFLAGS=-Dwarnings cargo doc, clean. cargo run -p armonik-transport
--features schema --example generate_schema declares MaxAttempts, InitialBackOff, MaxBackOff and
BackOffMultiplier, each as a string.
`f64` parses `nan`, `inf`, `0` and every negative as readily as a real
multiplier, so a typo in `BackOffMultiplier` changed the schedule
without a word: zero and below-one waits shrink instead of growing, and
a value no `Duration` can hold falls to the ceiling, pinning every retry
at the maximum. The reader now requires a finite number of at least 1,
and names the option.

One is allowed and is a policy: retry at a fixed interval.

`bounds()` keeps its own guard. The fields are public, so a caller can
still set a multiplier the reader would have refused, and a schedule
that cannot be computed settles on the ceiling rather than panicking.

`RetryConfig` joins the client facade's re-exports, so a caller names it
the way it names every other unit rather than reaching into the
transport crate.

cargo test -p armonik-transport --all-features: 105 passed, 0 failed.
cargo clippy -p armonik-transport -p armonik --all-features --all-targets: clean.
@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-retry-v2 branch from b800c44 to f35c83a Compare August 9, 2026 10:40
@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wkirschenmann
wkirschenmann marked this pull request as draft August 13, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant