KAFKA-20599: Improve retry logic in KafkaStatusBackingStore with exponential backoff - #22324
Conversation
|
A label of 'needs-attention' was automatically added to this PR in order to raise the |
mimaison
left a comment
There was a problem hiding this comment.
Thanks for the PR. I left a few comments
| sendRetryExecutor.submit(() -> { | ||
| time.sleep(backoffMs); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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?
| if (attemptNumber >= MAX_RETRY_ATTEMPTS) { | ||
| log.error("Failed to write status update for key {} after {} attempts. Giving up.", | ||
| key, attemptNumber + 1, exception); | ||
| return; | ||
| } |
There was a problem hiding this comment.
I'm uneasy with completely giving up and only logging it, especially on a RetriableException
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
|
@mimaison The PR has been updated with the changes. Could you please review? |
mimaison
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Is MAX_RETRY_ATTEMPTS correctly named? It seems this keeps retrying even after MAX_RETRY_ATTEMPTS.
There was a problem hiding this comment.
@mimaison The PR has been updated with the changes. Could you please review?
mimaison
left a comment
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
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.
| 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()); | ||
| } |
There was a problem hiding this comment.
I'm not sure having different messages is worth it.
There was a problem hiding this comment.
@mimaison The PR has been updated with the changes. Could you please review?
|
I think you need to rebase your branch on trunk to resolve the CI issues. |
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
…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
da6fb0e to
651f588
Compare
|
@mimaison I have rebased the branch |
This PR improves the retry logic in
KafkaStatusBackingStoreto addressthe TODO comment at line 283.
Problem
Previously, the code retried indefinitely without backoff when
encountering
RetriableException, which could:Solution
Implemented graceful retry mechanism with:
outcomes
resilience
Changes
MAX_RETRY_ATTEMPTS=10,INITIAL_RETRY_BACKOFF_MS=300,MAX_RETRY_BACKOFF_MS=60000)sendWithRetry()methods with exponential backoff forboth topic status and general status updates
calculateBackoff()using formula:min(INITIAL_BACKOFF * 2^attempt, MAX_BACKOFF)sendTopicStatus()andsend()methods to use new retrylogic
Testing
Benefits
(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