Skip to content

KAFKA-20599: Improve retry logic in KafkaStatusBackingStore with exponential backoff - #22324

Open
akhil-sabu-ibm wants to merge 4 commits into
apache:trunkfrom
akhil-sabu-ibm:KAFKA-20599-improve-status-store-retry-logic
Open

KAFKA-20599: Improve retry logic in KafkaStatusBackingStore with exponential backoff#22324
akhil-sabu-ibm wants to merge 4 commits into
apache:trunkfrom
akhil-sabu-ibm:KAFKA-20599-improve-status-store-retry-logic

Conversation

@akhil-sabu-ibm

@akhil-sabu-ibm akhil-sabu-ibm commented May 19, 2026

Copy link
Copy Markdown
Contributor

This PR improves the retry logic in KafkaStatusBackingStore to address
the TODO comment at line 283.

Problem

Previously, the code retried indefinitely without backoff when
encountering RetriableException, which could:

  • Exhaust system resources during prolonged outages
  • Overwhelm Kafka brokers with rapid retry attempts
  • Lack visibility into retry behavior

Solution

Implemented graceful retry mechanism with:

  • Exponential backoff: 300ms → 600ms → 1.2s → 2.4s → ... → 60s (max)
  • Retry limit: Maximum 10 attempts before giving up
  • Enhanced logging: Tracks retry attempts, backoff times, and final
    outcomes
  • Resource protection: Prevents infinite loops while maintaining
    resilience

Changes

  • Added retry configuration constants (MAX_RETRY_ATTEMPTS=10,
    INITIAL_RETRY_BACKOFF_MS=300, MAX_RETRY_BACKOFF_MS=60000)
  • Implemented sendWithRetry() methods with exponential backoff for
    both topic status and general status updates
  • Implemented calculateBackoff() using formula: min(INITIAL_BACKOFF * 2^attempt, MAX_BACKOFF)
  • Updated sendTopicStatus() and send() methods to use new retry
    logic
  • Added comprehensive JavaDoc documentation
  • Preserved existing safe-write checks and generation validation

Testing

  • All 24 existing tests pass without modification
  • Compilation successful with Java 21
  • Checkstyle and SpotBugs checks passed
  • Maintains backward compatibility
  • No breaking changes

Benefits

  • Prevents resource exhaustion during Kafka broker outages
  • Reduces load on brokers with exponential backoff
  • Improves observability with detailed retry logging
  • Aligns with best practices used in other Kafka Connect components
    (e.g., RetryUtil, DistributedHerder)

JIRA: https://issues.apache.org/jira/browse/KAFKA-20599

This is an improvement to system reliability with no breaking changes.

Reviewers: Mickael Maison mickael.maison@gmail.com

@github-actions github-actions Bot added connect triage PRs from the community labels May 19, 2026
@github-actions

Copy link
Copy Markdown

A label of 'needs-attention' was automatically added to this PR in order to raise the
attention of the committers. Once this issue has been triaged, the triage label
should be removed to prevent this automation from happening again.

@mimaison mimaison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the PR. I left a few comments

Comment on lines +318 to +319
sendRetryExecutor.submit(() -> {
time.sleep(backoffMs);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we use a ScheduledExecutor instead of doing sleep()?

long backoffMs = calculateBackoff(attemptNumber);
log.warn("Failed to write status update for key {} (attempt {}/{}). " +
"Retrying after {}ms. Reason: {}",
key, attemptNumber + 1, MAX_RETRY_ATTEMPTS + 1, backoffMs, exception.getMessage());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The message does not seem correct. The first attempt will print 1/11 and the last one will print 10/11, shouldn't it be 1/10 to 10/10?

Comment on lines +307 to +311
if (attemptNumber >= MAX_RETRY_ATTEMPTS) {
log.error("Failed to write status update for key {} after {} attempts. Giving up.",
key, attemptNumber + 1, exception);
return;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm uneasy with completely giving up and only logging it, especially on a RetriableException

akhil-sabu-ibm added a commit to akhil-sabu-ibm/kafka that referenced this pull request Jun 18, 2026
1. Use ScheduledExecutorService instead of ExecutorService with sleep()
   - Changed sendRetryExecutor to ScheduledExecutorService
   - Use schedule() method for delayed execution instead of blocking with sleep()
   - More efficient and cleaner implementation

2. Fix confusing retry attempt log messages
   - Simplified messages to show 'attempt X' instead of 'X/Y'
   - Clear message after MAX_RETRY_ATTEMPTS indicating continued retries

3. Continue retrying indefinitely on RetriableException
   - Removed logic that gave up after MAX_RETRY_ATTEMPTS
   - RetriableException indicates temporary failures that should eventually succeed
   - Backoff is capped at MAX_RETRY_BACKOFF_MS after MAX_RETRY_ATTEMPTS
   - Existing safeguards (stale checks, generation checks) prevent issues
@github-actions github-actions Bot removed needs-attention triage PRs from the community labels Jun 19, 2026
@akhil-sabu-ibm

Copy link
Copy Markdown
Contributor Author

@mimaison The PR has been updated with the changes. Could you please review?

@mimaison mimaison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the updates. I made another pass and left another question. I think it would also be nice to have tests for the new logic.

if (exception instanceof RetriableException) {
sendRetryExecutor.submit(() -> kafkaLog.send(key, value, this));
long backoffMs = calculateBackoff(attemptNumber);
if (attemptNumber < MAX_RETRY_ATTEMPTS) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is MAX_RETRY_ATTEMPTS correctly named? It seems this keeps retrying even after MAX_RETRY_ATTEMPTS.

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.

@mimaison The PR has been updated with the changes. Could you please review?

@mimaison mimaison left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the updates! I took another look and left a few more comments.

Also you need to rebase your branch on trunk to resolve the CI issues.

Comment on lines +131 to +135
// Retry configuration constants
// After this many attempts, backoff will be capped at MAX_RETRY_BACKOFF_MS
private static final int BACKOFF_ESCALATION_THRESHOLD = 10;
private static final long INITIAL_RETRY_BACKOFF_MS = 300;
private static final long MAX_RETRY_BACKOFF_MS = 60000; // 60 seconds

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than rolling a custom exponential backoff, could we reuse org.apache.kafka.common.utils.internals.ExponentialBackoff? For example it's already used in DistributedHerder.

Comment on lines +309 to +317
if (attemptNumber < BACKOFF_ESCALATION_THRESHOLD) {
log.warn("Failed to write status update for key {} (attempt {}). " +
"Retrying after {}ms. Reason: {}",
key, attemptNumber + 1, backoffMs, exception.getMessage());
} else {
log.warn("Failed to write status update for key {} after {} attempts. " +
"Will continue retrying with {}ms backoff. Reason: {}",
key, attemptNumber + 1, backoffMs, exception.getMessage());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure having different messages is worth it.

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.

@mimaison The PR has been updated with the changes. Could you please review?

@github-actions github-actions Bot added the small Small PRs label Aug 5, 2026
@mimaison

mimaison commented Aug 7, 2026

Copy link
Copy Markdown
Member

I think you need to rebase your branch on trunk to resolve the CI issues.

akhil-sabu-ibm added a commit to akhil-sabu-ibm/kafka that referenced this pull request Aug 7, 2026
1. Use ScheduledExecutorService instead of ExecutorService with sleep()
   - Changed sendRetryExecutor to ScheduledExecutorService
   - Use schedule() method for delayed execution instead of blocking with sleep()
   - More efficient and cleaner implementation

2. Fix confusing retry attempt log messages
   - Simplified messages to show 'attempt X' instead of 'X/Y'
   - Clear message after MAX_RETRY_ATTEMPTS indicating continued retries

3. Continue retrying indefinitely on RetriableException
   - Removed logic that gave up after MAX_RETRY_ATTEMPTS
   - RetriableException indicates temporary failures that should eventually succeed
   - Backoff is capped at MAX_RETRY_BACKOFF_MS after MAX_RETRY_ATTEMPTS
   - Existing safeguards (stale checks, generation checks) prevent issues
akhil-sabu-ibm and others added 4 commits August 8, 2026 01:33
…nential backoff

- Implemented exponential backoff for retry attempts (300ms initial, 60s max)
- Added maximum retry attempt limit (10 attempts)
- Added proper logging for retry attempts and failures
- Prevents infinite retry loops during prolonged outages
- Improved observability with structured logging

The retry logic now uses exponential backoff starting at 300ms and doubling
with each attempt up to a maximum of 60 seconds. After 10 failed attempts,
the system will give up and log an error. This prevents resource exhaustion
during prolonged Kafka broker outages while still providing resilience for
transient failures.
1. Use ScheduledExecutorService instead of ExecutorService with sleep()
   - Changed sendRetryExecutor to ScheduledExecutorService
   - Use schedule() method for delayed execution instead of blocking with sleep()
   - More efficient and cleaner implementation

2. Fix confusing retry attempt log messages
   - Simplified messages to show 'attempt X' instead of 'X/Y'
   - Clear message after MAX_RETRY_ATTEMPTS indicating continued retries

3. Continue retrying indefinitely on RetriableException
   - Removed logic that gave up after MAX_RETRY_ATTEMPTS
   - RetriableException indicates temporary failures that should eventually succeed
   - Backoff is capped at MAX_RETRY_BACKOFF_MS after MAX_RETRY_ATTEMPTS
   - Existing safeguards (stale checks, generation checks) prevent issues
The constant name was misleading since retries continue indefinitely
after this threshold. The new name better reflects its actual purpose:
controlling when the exponential backoff reaches its maximum value.

After BACKOFF_ESCALATION_THRESHOLD attempts, the backoff is capped at
MAX_RETRY_BACKOFF_MS, but retries continue indefinitely for
RetriableException.
- Replace custom calculateBackoff() + three constants with the existing
  ExponentialBackoff utility class (already used in DistributedHerder),
  as suggested by reviewer mimaison
- Merge the two separate log.warn branches (below/above threshold) into
  a single unified message, as suggested by reviewer mimaison
@akhil-sabu-ibm
akhil-sabu-ibm force-pushed the KAFKA-20599-improve-status-store-retry-logic branch from da6fb0e to 651f588 Compare August 7, 2026 20:04
@akhil-sabu-ibm

Copy link
Copy Markdown
Contributor Author

@mimaison I have rebased the branch

@github-actions github-actions Bot added triage PRs from the community and removed triage PRs from the community labels Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants