poc for fileconfiguration failover#55
Conversation
* fix all issues caught by sonarqube * error on non-existing failoverStrategy, refactor doFailoverAttempt, better logging * simplify doFailoverAttempt, check for NetworkError in orderedRoundTrip
There was a problem hiding this comment.
Pull request overview
This PR introduces transport-level endpoint failover (RoundTripper-based) and extracts the per-product HTTP retry logic into a shared shared/retry package, with initial YAML deserialization support for failover options in shared/fileconfiguration.
Changes:
- Add
shared/failoverRoundTripper with RoundRobin strategy, backoff, and extensive unit/E2E tests. - Centralize application-level HTTP retry into
shared/retry.DoWithApplicationRetryand switch compute client to use it. - Extend file configuration to deserialize a top-level
failover:block intofailover.Options.
Reviewed changes
Copilot reviewed 12 out of 14 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| shared/retry/retry.go | New shared application-level retry implementation used by product clients. |
| shared/logger.go | Adds LogDebug/LogTrace helpers for log-level-gated logging. |
| shared/go.mod | Adds dependencies needed by failover/retry + (currently) compute for E2E test. |
| shared/go.sum | Updates checksums for newly required modules. |
| shared/fileconfiguration/fileconfiguration.go | Adds Failover *failover.Options field and accessor. |
| shared/fileconfiguration/fileconfiguration_test.go | Tests YAML deserialization and nil behavior for failover options. |
| shared/failover/helpers_test.go | Test helpers for RoundTripper/network error simulation and backoff. |
| shared/failover/failover_roundtripper.go | New failover RoundTripper implementation and option types. |
| shared/failover/failover_roundtripper_test.go | Unit tests covering failover behavior across error/status scenarios. |
| shared/failover/failover_e2e_test.go | E2E test wiring compute client to failover transport via shared module. |
| shared/configuration.go | Exports DefaultMaxRetries and updates default initializations. |
| products/compute/go.mod | Adds local replace to shared (duplicated) and indirect deps for failover tests. |
| products/compute/go.sum | Updates module checksums for new deps. |
| products/compute/client.go | Replaces inline retry logic with shared/retry.DoWithApplicationRetry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 13 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| logRequest(clonedRequest, 0) | ||
|
|
||
| httpRequestStartTime := time.Now() | ||
| clonedRequest.Close = true |
There was a problem hiding this comment.
This retry implementation sets clonedRequest.Close = true, which disables connection reuse, but later drains the response body “so the connection can be reused”. Either remove Close = true to allow keep-alives (and keep draining), or drop the drain logic if connections are intentionally closed each attempt.
| clonedRequest.Close = true |
| if !shared.SdkLogLevel.Satisfies(shared.Trace) { | ||
| logReq.Header.Del("Authorization") | ||
| } | ||
| dump, err := httputil.DumpRequestOut(logReq, true) |
There was a problem hiding this comment.
request.Clone() shares the same Body reader with the original request. httputil.DumpRequestOut(..., true) reads the body; with the current flow (logging the same cloned request you later send), debug logging can consume the body and cause the real request to be sent with an empty/truncated payload. To avoid this, ensure the request being dumped has an independent body reader (e.g., set logReq.Body from request.GetBody() when available) rather than reusing the same Body instance.
| dump, err := httputil.DumpRequestOut(logReq, true) | |
| dumpBody := false | |
| if request.GetBody != nil { | |
| if bodyCopy, err := request.GetBody(); err == nil && bodyCopy != nil { | |
| defer bodyCopy.Close() | |
| logReq.Body = bodyCopy | |
| dumpBody = true | |
| } | |
| } | |
| dump, err := httputil.DumpRequestOut(logReq, dumpBody) |
| // MaxRetries controls how many times the transport will attempt a request using the set strategy before giving up | ||
| // and returning the last error. If zero, it defaults to 3. |
There was a problem hiding this comment.
The Options.MaxRetries docs are inconsistent with the implementation: orderedRoundTrip uses totalAttempts := maxRetries + 1, but Options.MaxRetries is described as “how many times the transport will attempt a request”. Please clarify whether MaxRetries is “retries” or “attempts” and align the comment (and the worst-case attempts formula) accordingly.
| // MaxRetries controls how many times the transport will attempt a request using the set strategy before giving up | |
| // and returning the last error. If zero, it defaults to 3. | |
| // MaxRetries controls how many times the transport will retry a request using the set strategy | |
| // after the initial attempt before giving up and returning the last error. In other words, the | |
| // worst-case total number of attempts is MaxRetries+1. If zero, it defaults to 3. |
products/compute/go.mod
Outdated
|
|
||
| replace github.com/ionos-cloud/sdk-go-bundle/shared => ../../shared |
There was a problem hiding this comment.
Duplicate replace github.com/ionos-cloud/sdk-go-bundle/shared => ../../shared: this file declares it twice, which will make go mod tidy fail. Remove one of the duplicate replace directives.
| replace github.com/ionos-cloud/sdk-go-bundle/shared => ../../shared |
| // DoWithApplicationRetry executes an HTTP request with application-level retry | ||
| // logic. This is the shared equivalent of the per-product callAPI method. | ||
| // | ||
| // Retry behaviour: | ||
| // - 502/503/504: retries with backoff (skips POST) | ||
| // - 429: retries honoring Retry-After header, falls back to waitTime | ||
| // - Other status codes or transport errors: returns immediately | ||
| // - Respects context cancellation during backoff |
There was a problem hiding this comment.
This PR introduces a new shared retry helper, but there are no unit tests covering the key behaviors (e.g., 429 Retry-After parsing, 502/503/504 retry rules, and POST not being retried). Please add focused tests under shared/retry to protect future migrations from regressions.
| ) (*http.Response, time.Duration, error) { | ||
| if cfg == nil { | ||
| return nil, 0, errors.New("nil configuration") | ||
| } |
There was a problem hiding this comment.
DoWithApplicationRetry validates cfg but not request. A nil request will panic when accessing request.Method / request.Context(). Add an early request == nil check and return a clear error.
| } | |
| } | |
| if request == nil { | |
| return nil, 0, errors.New("nil request") | |
| } |
|




No description provided.