Skip to content

Improve wait time in retry logic for CC calls#1878

Open
Yavor16 wants to merge 8 commits into
masterfrom
feature/LMCROSSITXSADEPLOY-2653
Open

Improve wait time in retry logic for CC calls#1878
Yavor16 wants to merge 8 commits into
masterfrom
feature/LMCROSSITXSADEPLOY-2653

Conversation

@Yavor16

@Yavor16 Yavor16 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Improves the retry/backoff behaviour of ResilientCloudOperationExecutor for Cloud Controller calls so the service degrades more gracefully when the CC is under rate limiting or is unstable.

  • Honour the Retry-After header on HTTP 429. CloudControllerResponseErrorHandler now parses the Retry-After header on 429 TOO_MANY_REQUESTS responses and propagates it via a new CloudOperationException.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.
  • Treat 429 as retryable. TOO_MANY_REQUESTS is added to the default set of statuses that are retried.
  • Randomized delay for repeated failures. After the first failed retry of a non-429 error, the executor waits a randomized interval between 30s and 90s instead of the fixed 5s, spreading out load when the CC is seriously impacted.

The random delay supplier and sleeper are injectable (package-private) purely to make the timing behaviour deterministically testable.

Acceptance criteria

  • Failed CC calls returning 429 respect the Retry-After header (capped), and fall back sensibly when it is missing.
  • Repeated retries no longer hammer the CC with a fixed short delay; they back off with a randomized wait.

Tests

Added coverage across three test classes:

  • CloudOperationExceptionTest — retry-after storage, default null, four-arg constructor leaves it null.
  • CloudControllerResponseErrorHandlerTestRetry-After parsing 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

Yavor16 added 3 commits July 15, 2026 14:38
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 Yavor16 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated code review by MTA Quality Agent — 2 finding(s).

@Yavor16

Yavor16 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

MTA Quality Report — cloudfoundry/multiapps-controller PR #1878

Jira: LMCROSSITXSADEPLOY-2653 — Improve wait time in retry logic for CC calls
Backlog alignment: PASS

  • Implements Jira scope? yes — the diff delivers all three scoped items: honour Retry-After on 429 (capped at 120s with a 60s fallback), add TOO_MANY_REQUESTS to the retryable set, and apply a randomized 30–90s wait after repeated failures.
  • Changes outside Jira scope? no — changes are confined to the retry/backoff logic (ResilientCloudOperationExecutor, CloudOperationException, CloudControllerResponseErrorHandler, Messages) and their tests.

Code Review

2 finding(s): 2 posted inline.

See the inline review on the PR diff for posted findings.


Security

  • L1 — LOW — Input validation: negative Retry-After value (CloudControllerResponseErrorHandler.java, extractRetryAfterSeconds): Long.parseLong(headerValue) does not reject negatives. A CC response with Retry-After: -1 leads to Thread.sleep(-1000) throwing IllegalArgumentException. The CC is normally a trusted internal service, but defense-in-depth warrants validating parsed > 0 — consistent with the code-review fix.

No CRITICAL / HIGH / MEDIUM findings. No hardcoded credentials, secrets, or internal URLs introduced; no new SQL, shell, deserialization, or SSRF surface; ThreadLocalRandom usage is appropriate (not security-sensitive).


SonarCloud

The "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 (pom.xml, build.gradle, lockfiles).

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 Yavor16 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated code review by MTA Quality Agent — 3 finding(s).

@Yavor16

Yavor16 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

MTA Quality Report — cloudfoundry/multiapps-controller PR #1878

Jira: LMCROSSITXSADEPLOY-2653 — Improve wait time in retry logic for CC calls
Backlog alignment: PASS

  • Implements Jira scope? yes — the diff delivers all three scoped items: honour Retry-After on 429 (capped at 120s with a 60s fallback), add TOO_MANY_REQUESTS to the retryable set, and apply a randomized 30–90s wait after repeated failures.
  • Changes outside Jira scope? no — changes are confined to the retry/backoff logic (ResilientCloudOperationExecutor, ResilientOperationExecutor, CloudOperationException, CloudControllerResponseErrorHandler, Messages) and their tests.

Code Review

3 finding(s): 3 posted inline.

See the inline review on the PR diff for posted findings.


Security

No issues found.

No CRITICAL / HIGH / MEDIUM findings. The Retry-After header is scoped to HTTP 429, parsed with Long.parseLong inside a try/catch, and guarded by parsed > 0; the server-supplied delay is bounded by Math.min(retryAfterSeconds, 120L) and floored at 1L, so a compromised Cloud Controller cannot force an arbitrarily long client-side sleep. All new log calls use SLF4J parameterized logging with long primitives (no log injection), and ThreadLocalRandom is used only for retry jitter (not security-sensitive).


SonarCloud

The "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 Yavor16 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated code review by MTA Quality Agent — 1 finding.

for (long i = 1; i < retryCount; i++) {
try {
return operation.get();
} catch (CloudOperationException e) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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:

Suggested change
} catch (CloudOperationException e) {
} catch (RuntimeException e) {

@Yavor16

Yavor16 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

MTA Quality Report — cloudfoundry/multiapps-controller PR #1878

Jira: LMCROSSITXSADEPLOY-2653 — Improve wait time in retry logic for CC calls
(⚠️ Jira backlog item could not be fetched — the Jira MCP tool is unavailable in this environment. Alignment below is derived from the PR title/description against the diff.)
Backlog alignment: PASS

  • Implements Jira scope? yes — the diff delivers all three scoped items: honour Retry-After on 429 (capped at 120s with a 60s fallback), add TOO_MANY_REQUESTS to the retryable set, and apply a randomized 30–90s wait after repeated failures.
  • Changes outside Jira scope? no — changes are confined to the retry/backoff logic (ResilientCloudOperationExecutor, ResilientOperationExecutor, CloudOperationException, CloudControllerResponseErrorHandler, Messages) and their tests.

Code Review

1 finding(s): 1 posted inline.

See the inline review on the PR diff for posted findings.


Security

No issues found.

No CRITICAL / HIGH / MEDIUM findings. The three prior concerns are confirmed resolved: negative/zero Retry-After is rejected (parsed > 0), the server-supplied backoff is bounded to [1,120]s via Math.max(1L, Math.min(…,120L)) preventing DoS-via-sleep with no overflow risk, and the catch is narrowed to CloudOperationException. All new log calls use parameterized SLF4J with numeric primitives only, so no log-injection (CWE-117) or sensitive-data exposure is introduced.


SonarCloud

The "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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

import it above org.springframework.http.HttpHeaders and reference HttpHeaders.RETRY_AFTER

e);
}

private long computeWaitMillis(RuntimeException e, long attemptIndex) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Signature show support for Runtime exceptions, but the only caller method catches CloudOperationException

@vkalapov vkalapov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Check sonarcloud comments for unit tests.

@Yavor16
Yavor16 force-pushed the feature/LMCROSSITXSADEPLOY-2653 branch from 930e59c to 92094b7 Compare July 17, 2026 11:21
stiv03
stiv03 previously approved these changes Jul 17, 2026
@sonarqubecloud

Copy link
Copy Markdown

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.

3 participants