fix(core, go): use uint32 for Go BackoffStrategy fields and harden reconnect RetryStrategy against out-of-range values - #6680
fix(core, go): use uint32 for Go BackoffStrategy fields and harden reconnect RetryStrategy against out-of-range values#6680xShinnRyuu wants to merge 12 commits into
Conversation
…e values The core represents every retry-strategy value as a u32, so store them that way in Go instead of int. Narrowing at the public boundary previously wrapped around, turning NewBackoffStrategy(-1, ...) into 4294967295 retries with no error anywhere. Out-of-range values are now recorded and surfaced when the client configuration is converted, matching how the rest of the package reports invalid input. The public signatures of NewBackoffStrategy and WithJitterPercent are unchanged. Closes #3929 Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…and panic Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…across bindings Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…across bindings Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…ange Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
go/config/config.go (1)
338-421: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winValidation logic and uint32 narrowing are correct.
The bounds check in
toUint32(Line 395) runs before theuint32(value)conversion, so the static-analysis narrowing warning on that line is a false positive here. The design correctly matches the protobufuint32contract inglide-core/src/protobuf/connection_request.protofornumber_of_retries,factor,exponent_base, andjitter_percent.One nuance worth a light suggestion:
strategy.erronly records the first invalid setter call (Line 396-398). If a caller fixes an earlier mistake by calling the same setter again with a valid value (e.g., callingWithJitterPercenttwice), the corrected value is stored, but the stale first error still causestoProtobufto fail. Consider collecting all validation errors witherrors.Joininstead of keeping only the first one, so the reported error always reflects the final state of the strategy and callers see every invalid field in one report instead of iterating one error at a time.♻️ Proposed refactor to aggregate validation errors
type BackoffStrategy struct { numOfRetries uint32 factor uint32 exponentBase uint32 jitterPercent *uint32 - // The first out-of-range value seen by a setter, if any. - err error + // Out-of-range values seen by setters, if any. + errs []error } func (strategy *BackoffStrategy) toUint32(name string, value int, maxValue int64) uint32 { if value < 0 || int64(value) > maxValue { - if strategy.err == nil { - strategy.err = fmt.Errorf("%s must be between 0 and %d, got %d", name, maxValue, value) - } + strategy.errs = append(strategy.errs, fmt.Errorf("%s must be between 0 and %d, got %d", name, maxValue, value)) return 0 } return uint32(value) } func (strategy *BackoffStrategy) toProtobuf() (*protobuf.ConnectionRetryStrategy, error) { - if strategy.err != nil { - return nil, strategy.err + if err := errors.Join(strategy.errs...); err != nil { + return nil, err } ... }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@go/config/config.go` around lines 338 - 421, Update BackoffStrategy validation so repeated setters do not leave stale errors after a value is corrected, while preserving errors for fields that remain invalid. Replace the single strategy.err approach used by toUint32 with per-field or equivalent accumulated validation state, aggregate remaining validation errors in toProtobuf, and ensure all invalid fields are reported together.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@glide-core/redis-rs/redis/src/retry_strategies.rs`:
- Around line 84-94: Update the fallback duration calculation in the retry
strategy around last_attempt and last_duration to exponentiate using
last_attempt + 1, matching ExponentialBackoff’s nth(k) sequence. Preserve the
existing saturating arithmetic and fallback behavior while ensuring the repeated
tail uses the correct duration.
---
Nitpick comments:
In `@go/config/config.go`:
- Around line 338-421: Update BackoffStrategy validation so repeated setters do
not leave stale errors after a value is corrected, while preserving errors for
fields that remain invalid. Replace the single strategy.err approach used by
toUint32 with per-field or equivalent accumulated validation state, aggregate
remaining validation errors in toProtobuf, and ensure all invalid fields are
reported together.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: eb38bb94-f0d0-48c8-9ee3-bdb63b4a95ac
📒 Files selected for processing (9)
CHANGELOG.mdglide-core/redis-rs/redis/src/retry_strategies.rsgo/config/config.gogo/config/config_test.gojava/client/src/main/java/glide/api/models/configuration/BackoffStrategy.javanode/src/BaseClient.tsnode/src/GlideClient.tsnode/src/GlideClusterClient.tspython/glide-shared/glide_shared/config.py
There was a problem hiding this comment.
The Go narrowing itself reads correctly and the deferred-error plumbing matches the existing invalid compression configuration / invalid circuit breaker configuration pattern. Two points on the parts of the change that reach past Go.
The jitter contract now differs between Go (hard error above 100) and the core plus the three other bindings this PR documents (silent clamp to 100) — worth settling on one contract before this lands, since it is a user-visible divergence introduced in a single change. Separately, the retry-count hardening only covers the 0 end of the range: nth() still walks the iterator, so the 2^32-1 end that the new Go test blesses stalls the shared runtime on the reconnect path.
Unrelated to the code: CONTRIBUTING.md:42 requires a DCO sign-off on every commit and Conventional Commits formatting. Five of the six commits here (a5c977f, cf0d56a, ec912b5, 6c49ece, 4389e7b) carry no Signed-off-by trailer and use a no-mistakes(review) / no-mistakes(document) type; only 8b5f9af is signed off.
get_infinite_backoff_dur_iterator walked ExponentialBackoff with nth() to find the last bounded delay, which costs one step per retry. The reconnect tasks call this synchronously on the single-threaded runtime shared by every client in the process, so a retry count in the billions stalled that thread with all other clients queued behind it. The delay is now derived arithmetically. ExponentialBackoff yields factor * exponent_base^(k+1) at index k, so the exponent is the retry count itself, floored at 1 so a retry count of 0 still reports the first delay. Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
The core clamps a reconnect jitterPercent above 100 to 100, and Java, Python and Node all accept such a value. Go rejected it, so WithJitterPercent(150) failed client creation in Go and succeeded in the other three bindings. Go now clamps too, making maxJitterPercent a ceiling rather than a rejection bound. Negatives are still rejected, since they have no representation in the protobuf uint32. Validation is also tracked per field rather than as the first error seen, so calling a setter again with a corrected value clears its error and every invalid field is reported at once. Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…ping it Clamping silently changed a caller-supplied value, so a misconfiguration looked like it had been accepted. RetryStrategy::new is now fallible and rejects a jitter above MAX_JITTER_PERCENT with an InvalidClientConfig error, which the standalone and cluster client builders propagate as a client-creation failure. That gives one contract across the core and all four bindings: above 100 is an error everywhere, with no silent capping. Negatives remain unrepresentable in the protobuf uint32 and are still rejected at each binding boundary. Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…cted Go rejects a jitterPercent above 100 again, so maxJitterPercent is a validation bound rather than a ceiling, and the Java, Python and Node docs say the value is rejected rather than capped. All four bindings now describe the contract the core enforces. Per-field validation tracking is kept, so a setter called again with a valid value clears its own error and every invalid field is reported at once. Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
…ustfmt The closure body fits on one line at 98 columns, so cargo fmt --check rejected the braced form. Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
Signed-off-by: Thomas Zhou <thomaszhou64@gmail.com>
77ddbfe to
2dc0271
Compare
Summary
The Go
BackoffStrategystored its reconnect parameters asint, while the Rust core represents every retry-strategy value asu32(ConnectionRetryStrategyinglide-core/src/client/types.rs). This change stores the Go fields asuint32so they map cleanly into the core, and narrows at the public boundary so an out-of-range value can no longer wrap silently. Beyond that Go-side narrowing, it hardens the coreRetryStrategyagainst the three out-of-range hazards listed below, all of which affect every wrapper, not just Go.Issue link
This Pull Request is linked to issues:
Closes #3929
Closes #6675
Closes #6676
Features / Behaviour Changes
2^32-1value passed toNewBackoffStrategy, or a negative value passed toWithJitterPercent, now fails client creation with a named error instead of silently wrapping. PreviouslyNewBackoffStrategy(-1, ...)produced a retry count of 4294967295.jitterPercentabove 100 no longer aborts the process. A value above 100 made the lower jitter bound negative andDuration::mul_f64panicked (withpanic = "abort"in the FFI release profile). Such a value is now rejected as a configuration error when the client is created, in the core, so every wrapper reports it rather than aborting.jitterPercentof exactly 100 is still valid and means full jitter.get_infinite_backoff_dur_iterator(previouslynumber_of_retries as usize - 1wrapped tousize::MAX).u32::MAXno longer stalls the shared runtime. The last bounded delay was found by walkingExponentialBackoffwithIterator::nth, one step per retry, called synchronously from the reconnect tasks inreconnecting_connection.rsandcluster_async/mod.rson the single-threaded Tokio runtime shared by every client in the process. It is now computed arithmetically.Implementation
go/config/config.go: the four privateBackoffStrategyfields becomenumOfRetries uint32,factor uint32,exponentBase uint32,jitterPercent *uint32, matching the core. The public signaturesNewBackoffStrategy(int, int, int) *BackoffStrategyandWithJitterPercent(int) *BackoffStrategyare unchanged, including their return types, so this is not a breaking API change. Narrowing happens at the public boundary in atoUint32helper: an out-of-range value is recorded per field and the field stores 0 rather than wrapping.WithJitterPercentvalidates against 100.toProtobuf()reports every invalid field at once, andbaseClientConfiguration.toProtobuf()surfaces it asinvalid reconnect strategy: %w, matching the existing circuit-breaker and compression error wrapping. Because the error is tracked per field, calling a setter again with a corrected value clears it, so the reported error always describes the current state.glide-core/redis-rs/redis/src/retry_strategies.rs:RetryStrategy::newis fallible and rejects ajitter_percentabove a newMAX_JITTER_PERCENTof 100 with anInvalidClientConfigerror.standalone_client.rsandclient/mod.rspropagate it, so an out-of-range jitter fails client creation instead of aborting on the first reconnect.get_infinite_backoff_dur_iteratorderives its tail delay in closed form:ExponentialBackoff::from_millis(base).factor(factor)yieldsfactor * base^(k+1)at indexk, so the exponent is the retry count itself, floored at 1 so a retry count of 0 still reports the first delay. That removes both thenth()walk and the underflow, and needs no fallback branch.jitterPercentabove 100 is rejected and that the core substitutes its own defaults whenfactororexponentBaseis 0. The jitter bound, and that a value above it is rejected, is also documented on the Node, Python, and JavaBackoffStrategysurfaces, and a stalejitterkey in the NodecreateClientexamples is corrected tojitterPercent.Testing
go/config/config_test.go: cases cover zero values,2^32 - 1maxima, per-field negative and above-max inputs (asserting a named error and a stored 0 rather than a wrapped value), jitter above 100 being rejected, jitter of exactly 100 being accepted, a corrected setter clearing its earlier error, every invalid field being reported together, and error propagation through both standalone and clusterToProtobuf(). The negative-value cases run unconditionally; only the above-2^32-1cases are gated behind a skip on platforms whereintis 32 bits.glide-core/redis-rs/redis/src/retry_strategies.rs: unit tests cover a jitter above 100 being rejected with a field-namingInvalidClientConfigerror, a jitter of exactly 100 being accepted, the zero-retry case, a retry count ofu32::MAXreturning promptly rather than spinning, and the infinite tail matching the last bounded delay.go test ./config/(pass),cargo test --features tokio-comp --libinglide-core/redis-rs/redis(316 pass),cargo test --libinglide-core(161 pass), pluscargo build --testsin both crates andcargo buildinffi.NewBackoffStrategy/WithJitterPercentand callingToProtobuf()shows a negative retry count fails withinvalid reconnect strategy: numOfRetries must be between 0 and 4294967295, got -1, where it previously wrapped to 4294967295, whileWithJitterPercent(150)fails withinvalid reconnect strategy: jitterPercent must be between 0 and 100, got 150.Checklist
Before submitting the PR make sure the following are checked:
make *-linttargets) and Prettier has been run (make prettier-fix).