Improve wait time in retry logic for CC calls#1878
Conversation
Extend CloudOperationException with a nullable retryAfterSeconds field so that the error handler can propagate the Retry-After response header to the retry executor. CloudControllerResponseErrorHandler now reads the Retry-After header on 429 responses and passes the parsed value (or null when absent or unparseable) to the exception constructor. ResilientCloudOperationExecutor overrides execute(Supplier) to compute wait time per-attempt: 429 errors honour the header value capped at 120s (or fall back to 60s when the header is absent); subsequent non-429 retries (attempt >= 2) use a random delay in the range 30–90s to spread load. The LongConsumer sleeper field enables test injection without requiring static mocking. TOO_MANY_REQUESTS is added to DEFAULT_STATUSES_TO_IGNORE so the executor retries 429 responses automatically. JIRA:LMCROSSITXSADEPLOY-2653
CloudOperationExceptionTest: verify retryAfterSeconds is null via existing constructors and stored correctly via the new five-arg form. CloudControllerResponseErrorHandlerTest: fix the ClientHttpResponseMock getHeaders() stub (was UnsupportedOperationException) and add four cases covering 429 with/without/unparseable Retry-After and non-429 headers. ResilientCloudOperationExecutorTest: add five new tests covering the Retry-After cap, header-absent fallback, fixed delay for non-429 first retry, and deterministic random delay from the second retry onward. Uses withSleeper and withRandomDelaySupplier testability seams to avoid real sleeps and non-deterministic assertions. Also adds withRandomDelaySupplier package-private builder to ResilientCloudOperationExecutor to support deterministic test seeding. JIRA:LMCROSSITXSADEPLOY-2653
Cover Retry-After cap boundary, retry exhaustion, Runnable-overload routing, 429 body+header passthrough, and four-arg constructor null retry-after. JIRA:LMCROSSITXSADEPLOY-2653
Yavor16
left a comment
There was a problem hiding this comment.
Automated code review by MTA Quality Agent — 2 finding(s).
MTA Quality Report — cloudfoundry/multiapps-controller PR #1878Jira: LMCROSSITXSADEPLOY-2653 — Improve wait time in retry logic for CC calls
Code Review2 finding(s): 2 posted inline. See the inline review on the PR diff for posted findings. Security
No CRITICAL / HIGH / MEDIUM findings. No hardcoded credentials, secrets, or internal URLs introduced; no new SQL, shell, deserialization, or SSRF surface; SonarCloudThe "Build and analyze" check run for the head commit is currently in_progress. Quality gate status is UNKNOWN — SonarCloud has not yet completed analysis for this PR. No per-line annotations are available yet. Re-check Sonar status once the CI run completes. Dependency CVEs✅ No new CVEs — this PR does not modify any dependency files ( |
Negative or zero Retry-After values caused either an IllegalArgumentException in Thread.sleep (negative case) or a busy-wait tight loop against the CC (zero case). Fix both: - extractRetryAfterSeconds now returns null for values <= 0 - computeWaitMillis floors cappedSeconds at 1 via Math.max(1L, ...) JIRA:LMCROSSITXSADEPLOY-2653
Yavor16
left a comment
There was a problem hiding this comment.
Automated code review by MTA Quality Agent — 3 finding(s).
MTA Quality Report — cloudfoundry/multiapps-controller PR #1878Jira: LMCROSSITXSADEPLOY-2653 — Improve wait time in retry logic for CC calls
Code Review3 finding(s): 3 posted inline. See the inline review on the PR diff for posted findings. SecurityNo issues found. No CRITICAL / HIGH / MEDIUM findings. The SonarCloudThe "Build and analyze" check run for the head commit is currently in_progress. Quality gate status is UNKNOWN — SonarCloud has not yet completed analysis for this PR. No per-line annotations are available yet. Re-check Sonar status once the CI run completes. Dependency CVEs⏭️ CVE scan skipped on this review-fix re-run — dependencies were scanned on the initial quality run. |
- Narrow catch in execute() from RuntimeException to CloudOperationException so non-retriable exceptions (NPE, ISE) are not silently swallowed - Promote loop counter i from int to long to match the protected long retryCount field and avoid integer overflow on large retry counts - Promote computeWaitMillis attemptIndex parameter from int to long to match the updated loop variable without a narrowing cast JIRA:LMCROSSITXSADEPLOY-2653
Yavor16
left a comment
There was a problem hiding this comment.
Automated code review by MTA Quality Agent — 1 finding.
| for (long i = 1; i < retryCount; i++) { | ||
| try { | ||
| return operation.get(); | ||
| } catch (CloudOperationException e) { |
There was a problem hiding this comment.
Severity: CRITICAL · Confidence: 93
The overridden execute narrows the catch from the parent's RuntimeException to CloudOperationException. Any non-CloudOperationException RuntimeException thrown by the operation (e.g. a NullPointerException or Spring RestClientException that bypasses the error handler) now escapes immediately on the first attempt without retry, silently regressing the fault-tolerance guarantee that ResilientOperationExecutor provides. The parent's handle(RuntimeException) dispatch already exists in this class to handle the non-CloudOperationException path correctly — the catch clause just needs to be widened to match it.
Evidence: } catch (CloudOperationException e) {
Fix:
| } catch (CloudOperationException e) { | |
| } catch (RuntimeException e) { |
MTA Quality Report — cloudfoundry/multiapps-controller PR #1878Jira: LMCROSSITXSADEPLOY-2653 — Improve wait time in retry logic for CC calls
Code Review1 finding(s): 1 posted inline. See the inline review on the PR diff for posted findings. SecurityNo issues found. No CRITICAL / HIGH / MEDIUM findings. The three prior concerns are confirmed resolved: negative/zero SonarCloudThe "Build and analyze" check run for the head commit is currently in_progress. Quality gate status is UNKNOWN — SonarCloud has not yet completed analysis for this PR. No per-line annotations are available yet. Re-check Sonar status once the CI run completes. Dependency CVEs⏭️ CVE scan skipped on this review-fix re-run — dependencies were scanned on the initial quality run. |
| for (long i = 1; i < retryCount; i++) { | ||
| try { | ||
| return operation.get(); | ||
| } catch (CloudOperationException e) { |
There was a problem hiding this comment.
Timeouts always surface as CloudOperationException? Maybe we can catch it more gracefully -RuntimeException?
| // INFO messages | ||
| public static final String WAITING_MS_BEFORE_RETRYING_WITH_TIMEOUT_OF_MS = "Waiting: {} ms before retrying with timeout of: {} ms"; | ||
| public static final String RATE_LIMITED_BY_CC_WAITING_S = "CC returned 429 with Retry-After: {} s. Waiting {} s (capped) before retrying."; | ||
| public static final String RATE_LIMITED_BY_CC_NO_HEADER_WAITING_S = "CC returned 429 without Retry-After header. Waiting {} ms before retrying."; |
There was a problem hiding this comment.
maybe to be RATE_LIMITED_BY_CC_NO_HEADER_WAITING_MS, cause its logging ms
| return null; | ||
| } | ||
| String headerValue = response.getHeaders() | ||
| .getFirst(org.springframework.http.HttpHeaders.RETRY_AFTER); |
There was a problem hiding this comment.
import it above org.springframework.http.HttpHeaders and reference HttpHeaders.RETRY_AFTER
| e); | ||
| } | ||
|
|
||
| private long computeWaitMillis(RuntimeException e, long attemptIndex) { |
There was a problem hiding this comment.
Signature show support for Runtime exceptions, but the only caller method catches CloudOperationException
vkalapov
left a comment
There was a problem hiding this comment.
Check sonarcloud comments for unit tests.
930e59c to
92094b7
Compare
|



Summary
Improves the retry/backoff behaviour of
ResilientCloudOperationExecutorfor Cloud Controller calls so the service degrades more gracefully when the CC is under rate limiting or is unstable.Retry-Afterheader on HTTP 429.CloudControllerResponseErrorHandlernow parses theRetry-Afterheader on429 TOO_MANY_REQUESTSresponses and propagates it via a newCloudOperationException.getRetryAfterSeconds(). When present, the executor waits for that duration, capped at 120s to avoid excessively long stalls; when the header is absent, it falls back to a 60s wait.TOO_MANY_REQUESTSis added to the default set of statuses that are retried.The random delay supplier and sleeper are injectable (package-private) purely to make the timing behaviour deterministically testable.
Acceptance criteria
429respect theRetry-Afterheader (capped), and fall back sensibly when it is missing.Tests
Added coverage across three test classes:
CloudOperationExceptionTest— retry-after storage, default null, four-arg constructor leaves it null.CloudControllerResponseErrorHandlerTest—Retry-Afterparsing on 429 (present, absent, non-numeric), non-429 does not set it, body + header passthrough.ResilientCloudOperationExecutorTest— retry-after honoured, cap boundary, fallback wait, fixed vs randomized delay routing, retry exhaustion rethrow, Runnable-overload routing.Test plan
mvn -pl multiapps-controller-client -am test -Dtest=CloudOperationExceptionTest,CloudControllerResponseErrorHandlerTest,ResilientCloudOperationExecutorTest