sepuri sai krishna created KAFKA-20968:
------------------------------------------
Summary: Connect retry backoff overflows with infinite retries,
dropping the delay to zero and restarting the exponential ramp
Key: KAFKA-20968
URL: https://issues.apache.org/jira/browse/KAFKA-20968
Project: Kafka
Issue Type: Bug
Components: connect
Reporter: sepuri sai krishna
Assignee: sepuri sai krishna
RetryWithToleranceOperator.backoff()
(connect/runtime/src/main/java/org/apache/kafka/connect/runtime/errors/RetryWithToleranceOperator.java,
lines 299-315) computes the exponential backoff with an unchecked long shift:
void backoff(int attempt, long deadline) {
int numRetry = attempt - 1;
long delay = RETRIES_DELAY_MIN_MS << numRetry; // 300L <<
numRetry
if (delay > errorMaxDelayInMillis) {
delay = ThreadLocalRandom.current().nextLong(errorMaxDelayInMillis);
}
long currentTime = time.milliseconds();
if (delay + currentTime > deadline) {
delay = Math.max(0, deadline - currentTime);
}
log.debug("Sleeping for up to {} millis", delay);
try {
stopRequestedLatch.await(delay, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// ignore
}
}
"errors.retry.timeout = -1" is a documented configuration value --
ConnectorConfig.java:180 states "Use -1 for infinite retries." With it,
execAndRetry() sets deadline = Long.MAX_VALUE (line 203), so "attempt" grows
without bound for as long as the operation keeps throwing RetriableException.
That is precisely the scenario infinite retries exists for: a sink connector
whose downstream system is unavailable for an extended outage.
Because "numRetry" is unbounded, "300L << numRetry" eventually overflows. Two
distinct failures follow.
1. The delay becomes non-positive, and the jitter clamp is bypassed.
At numRetry = 55 (attempt 56), 300L << 55 overflows to -7638104968020361216. A
negative delay is not greater than errorMaxDelayInMillis, so the "delay >
errorMaxDelayInMillis" branch that applies jitter is skipped. With an infinite
deadline the second clamp is skipped as well, so the negative value reaches
CountDownLatch.await(), which returns immediately for a non-positive timeout.
The backoff disappears entirely and the connector retries in a tight loop
against the already-failing resource.
The same happens at numRetry = 58, 60, 61 (negative) and numRetry = 62, 63
(exactly 0).
2. Beyond numRetry = 63 the ramp silently restarts.
Java masks the shift distance for a long to its six lowest-order bits, so at
numRetry = 64 the shift is 0 and the delay returns to 300 ms, then 600 ms, 1200
ms, and so on. The backoff therefore cycles with period 64 instead of remaining
capped at errors.retry.delay.max.ms, repeatedly dropping back to sub-second
retries no matter how long the outage has lasted.
Both outcomes defeat the protection that errors.retry.delay.max.ms is
documented to provide -- ConnectorConfig.java:185-186 states "Jitter will be
added to the delay once this limit is reached to prevent thundering herd
issues."
The overflow is reached in ordinary operating time, not at some unreachable
extreme. With the default errors.retry.delay.max.ms of 60000, attempts 1
through 8 use the real exponential values (300 ms through 38400 ms) and
attempts 9 through 55 each wait a jittered interval uniformly distributed in
[0, 60000) ms, averaging 30 seconds. A connector retrying a downstream system
that is down therefore reaches the first zero-backoff attempt after roughly 25
minutes, and then re-enters the degraded region every 64 attempts thereafter.
RetryWithToleranceOperatorTest already contains a case asserting that the
deadline clamp does not produce a negative wait ("We may try to begin backing
off after the deadline has already passed; make sure that we don't wait with a
negative timeout"), but no case covers a negative wait arising from the
overflow itself.
Proposed fix: treat a shift that would overflow as an effectively infinite
delay so it falls into the existing jitter branch, keeping the delay within [0,
errors.retry.delay.max.ms] for every attempt number.
I'm happy to submit a PR for this fix.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)