Severity: CRITICAL
Bug 1: Thread.sleep() Blocks Calling Thread - No Cancellation (Line 104)
Code:
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", ie);
}
Problems:
A. No Way to Cancel Retry
Once you start retrying, you're STUCK until all retries complete:
RetryPolicy policy = RetryPolicy.aggressive(); // 5 retries, up to 30s delays
// This could block for MINUTES with no way to stop it:
policy.execute(() -> {
// Network call that keeps failing...
return slowServer.getData(); // User clicks "Cancel" - TOO BAD!
});
Total blocking time with default aggressive():
- Attempt 0: immediate
- Attempt 1: 200ms delay
- Attempt 2: 400ms delay
- Attempt 3: 800ms delay
- Attempt 4: 1,600ms delay
- Attempt 5: 3,200ms delay
- Total: 6.2 seconds minimum (plus jitter, plus actual operation time)
If operations also fail slowly:
// Each attempt times out after 30 seconds
// 5 retries × 30s = 150 seconds = 2.5 MINUTES of blocking!
B. Wastes Thread Pool Threads
ExecutorService executor = Executors.newFixedThreadPool(10);
// Submit 100 tasks that all retry
for (int i = 0; i < 100; i++) {
executor.submit(() ->
retryPolicy.execute(() -> failingOperation())
);
}
// All 10 threads are now BLOCKED sleeping!
// No new tasks can run!
Fix:
Use ScheduledExecutorService:
public <T> CompletableFuture<T> executeAsync(IOSupplier<T> operation, Executor executor) {
CompletableFuture<T> future = new CompletableFuture<>();
retryAsync(operation, 0, future, executor);
return future;
}
private <T> void retryAsync(IOSupplier<T> operation, int attempt,
CompletableFuture<T> future, Executor executor) {
try {
T result = operation.get();
future.complete(result);
} catch (IOException e) {
if (attempt >= maxRetries) {
future.completeExceptionally(e);
return;
}
long delay = calculateDelay(attempt);
scheduler.schedule(
() -> retryAsync(operation, attempt + 1, future, executor),
delay, TimeUnit.MILLISECONDS
);
}
}
Bug 2: Jitter Implementation is WRONG (Lines 136-138)
Current code:
if (jitter && delay > 0) {
long jitterAmount = (long) (delay * 0.25 * Math.random());
delay += jitterAmount; // ← ONLY ADDS, never subtracts!
}
This is NOT jitter - it's just adding random delay!
Real jitter should be ± random amount:
- Sometimes shorter delays (reduce thundering herd)
- Sometimes longer delays (spread out load)
Current behavior:
- delay = 1000ms
- jitterAmount = 0 to 250ms (always positive)
- final delay = 1000ms to 1250ms
Correct jitter:
- delay = 1000ms
- jitterAmount = -250ms to +250ms
- final delay = 750ms to 1250ms
Why this matters:
Jitter prevents thundering herd - when many clients retry at the same time after a failure.
Scenario:
Server goes down at 10:00:00
1000 clients all fail their request
All 1000 retry after exactly 1000ms (at 10:00:01)
Server gets 1000 simultaneous requests → overloaded again!
With proper jitter:
1000 clients retry between 750ms and 1250ms
Requests spread out over 500ms window
Server handles gradual ramp-up
Fix:
if (jitter && delay > 0) {
// Random value between -0.25 and +0.25
double jitterFactor = (Math.random() - 0.5) * 0.5; // -0.25 to +0.25
long jitterAmount = (long) (delay * jitterFactor);
delay = Math.max(0, delay + jitterAmount); // Don't go negative
}
Or better, use ThreadLocalRandom:
if (jitter && delay > 0) {
long maxJitter = delay / 4; // 25% jitter
long jitterAmount = ThreadLocalRandom.current().nextLong(-maxJitter, maxJitter + 1);
delay = Math.max(0, delay + jitterAmount);
}
Bug 3: Math.pow() Can Overflow (Line 130)
Code:
long delay = (long) (initialDelayMs * Math.pow(backoffMultiplier, attemptNumber));
Problem:
Math.pow() returns double, which can exceed Long.MAX_VALUE:
RetryPolicy policy = new RetryPolicy(100, 1000, Long.MAX_VALUE, 2.0, false);
// Attempt 63:
// delay = 1000 * 2^63 = 9,223,372,036,854,775,808 (overflows long!)
// Cast to long wraps around to NEGATIVE number!
// delay becomes negative → Thread.sleep(-X) throws IllegalArgumentException!
Even with maxDelayMs cap:
long delay = (long) (initialDelayMs * Math.pow(backoffMultiplier, attemptNumber));
// If this overflows, delay is negative
delay = Math.min(delay, maxDelayMs); // Math.min(negative, positive) = negative!
// Still negative!
Fix:
private long calculateDelay(int attemptNumber) {
long delay = initialDelayMs;
// Multiply iteratively with overflow protection
for (int i = 0; i < attemptNumber; i++) {
// Check for overflow before multiplying
if (delay > maxDelayMs / backoffMultiplier) {
return maxDelayMs; // Would overflow, cap it
}
delay = (long) (delay * backoffMultiplier);
}
delay = Math.min(delay, maxDelayMs);
// Add jitter
if (jitter && delay > 0) {
long maxJitter = delay / 4;
long jitterAmount = ThreadLocalRandom.current().nextLong(-maxJitter, maxJitter + 1);
delay = Math.max(0, delay + jitterAmount);
}
return delay;
}
Bug 4: Math.random() is Thread-Unsafe (Line 137)
Code:
long jitterAmount = (long) (delay * 0.25 * Math.random());
From Math.random() Javadoc:
This method is properly synchronized to allow correct use by more than one thread. However, if many threads need to generate pseudorandom numbers at a great rate, it may reduce contention for each thread to have its own pseudorandom-number generator.
Problem:
Math.random() uses a single shared Random instance with synchronized access.
Impact on concurrent retries:
// 100 threads all retrying at once
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
executor.submit(() -> retryPolicy.execute(() -> operation()));
}
// All 100 threads contend on Math.random()'s lock!
// Severe performance degradation!
Fix:
Use ThreadLocalRandom (zero contention):
long jitterAmount = ThreadLocalRandom.current().nextLong(0, delay / 4 + 1);
Bug 5: Losing Stack Traces (Lines 110-116)
Code:
} catch (Exception e) {
// Non-IOException - don't retry (likely a programming error)
if (e instanceof RuntimeException) {
throw (RuntimeException) e; // ✅ Preserves stack trace
}
throw new IOException("Unexpected exception during retry", e); // ❌ Wraps checked exceptions
}
Problem:
Checked exceptions (other than IOException) are wrapped in IOException.
Example:
retryPolicy.execute(() -> {
// JDBC code
return connection.prepareStatement("SELECT * FROM users").executeQuery();
});
// If SQLException is thrown:
// Original: SQLException with full stack trace
// After: IOException("Unexpected exception", cause: SQLException)
// Stack trace shows retry logic, not original error location!
This makes debugging MUCH harder.
Better approach:
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
// Wrap in RuntimeException, not IOException
throw new IllegalStateException(
"Unexpected checked exception during retry (was the lambda correct?)", e);
}
Or better, don't catch Exception at all:
} catch (IOException e) {
lastException = e;
// ... retry logic ...
}
// Don't catch other exceptions - let them propagate naturally
Bug 6: Interrupt Handling Loses Information (Lines 105-108)
Code:
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ✅ Restores flag
throw new IOException("Retry interrupted", ie); // ❌ Wraps in IOException
}
Problem:
InterruptedException is wrapped in IOException, making it look like an I/O error.
Caller can't distinguish:
try {
retryPolicy.execute(() -> operation());
} catch (IOException e) {
// Is this a network error or was I interrupted?
if (e.getCause() instanceof InterruptedException) {
// Have to unwrap to find out!
}
}
Fix:
Let InterruptedException propagate OR create a specific exception:
public <T> T execute(IOSupplier<T> operation) throws IOException, InterruptedException {
// ...
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw ie; // Let caller handle it
}
}
Or:
public class RetryInterruptedException extends IOException {
public RetryInterruptedException(InterruptedException cause) {
super("Retry was interrupted", cause);
}
}
Bug 7: No Logging or Observability
Code has ZERO logging!
When retries happen, you have NO idea:
- How many retries occurred
- How long each retry took
- What exceptions were thrown
- Whether jitter was applied
For production debugging, you need:
private static final Logger logger = LoggerFactory.getLogger(RetryPolicy.class);
public <T> T execute(IOSupplier<T> operation) throws IOException {
IOException lastException = null;
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (attempt > 0) {
logger.debug("Retry attempt {} of {}", attempt, maxRetries);
}
long start = System.currentTimeMillis();
T result = operation.get();
if (attempt > 0) {
logger.info("Operation succeeded after {} retries in {}ms",
attempt, System.currentTimeMillis() - start);
}
return result;
} catch (IOException e) {
lastException = e;
if (attempt < maxRetries) {
long delay = calculateDelay(attempt);
logger.warn("Attempt {} failed ({}), retrying in {}ms",
attempt, e.getMessage(), delay);
// ... sleep ...
} else {
logger.error("All {} retry attempts exhausted", maxRetries + 1, e);
}
}
}
throw new IOException("Failed after " + (maxRetries + 1) + " attempts", lastException);
}
Bug 8: No Metrics/Telemetry
No way to monitor retry behavior in production:
- How often do retries happen?
- What's the success rate after 1 retry? 2 retries?
- Are we hitting max retries often?
Should expose:
public class RetryMetrics {
private final AtomicLong totalExecutions = new AtomicLong();
private final AtomicLong successWithoutRetry = new AtomicLong();
private final AtomicLong successAfterRetry = new AtomicLong();
private final AtomicLong totalRetries = new AtomicLong();
private final AtomicLong exhaustedRetries = new AtomicLong();
// Histogram of retry counts
private final ConcurrentHashMap<Integer, AtomicLong> retriesByAttempt = new ConcurrentHashMap<>();
}
Summary
Current RetryPolicy:
- ❌ Blocks threads (no cancellation)
- ❌ Jitter implementation is wrong (only adds, never subtracts)
- ❌ Can overflow with Math.pow()
- ❌ Uses slow, thread-unsafe Math.random()
- ❌ Loses stack traces by wrapping exceptions
- ❌ Wraps InterruptedException in IOException
- ❌ Zero logging (impossible to debug)
- ❌ Zero metrics (can't monitor production)
This class is the CORE of network reliability and it's BROKEN.
Impact: Production failures, poor performance, hard to debug, impossible to monitor.
Priority: CRITICAL - This affects every network operation in the library.
Severity: CRITICAL
Bug 1: Thread.sleep() Blocks Calling Thread - No Cancellation (Line 104)
Code:
Problems:
A. No Way to Cancel Retry
Once you start retrying, you're STUCK until all retries complete:
Total blocking time with default aggressive():
If operations also fail slowly:
B. Wastes Thread Pool Threads
Fix:
Use ScheduledExecutorService:
Bug 2: Jitter Implementation is WRONG (Lines 136-138)
Current code:
This is NOT jitter - it's just adding random delay!
Real jitter should be ± random amount:
Current behavior:
Correct jitter:
Why this matters:
Jitter prevents thundering herd - when many clients retry at the same time after a failure.
Scenario:
With proper jitter:
Fix:
Or better, use ThreadLocalRandom:
Bug 3: Math.pow() Can Overflow (Line 130)
Code:
Problem:
Math.pow() returns double, which can exceed Long.MAX_VALUE:
Even with maxDelayMs cap:
Fix:
Bug 4: Math.random() is Thread-Unsafe (Line 137)
Code:
From Math.random() Javadoc:
Problem:
Math.random() uses a single shared Random instance with synchronized access.
Impact on concurrent retries:
Fix:
Use ThreadLocalRandom (zero contention):
Bug 5: Losing Stack Traces (Lines 110-116)
Code:
Problem:
Checked exceptions (other than IOException) are wrapped in IOException.
Example:
This makes debugging MUCH harder.
Better approach:
Or better, don't catch Exception at all:
Bug 6: Interrupt Handling Loses Information (Lines 105-108)
Code:
Problem:
InterruptedException is wrapped in IOException, making it look like an I/O error.
Caller can't distinguish:
Fix:
Let InterruptedException propagate OR create a specific exception:
Or:
Bug 7: No Logging or Observability
Code has ZERO logging!
When retries happen, you have NO idea:
For production debugging, you need:
Bug 8: No Metrics/Telemetry
No way to monitor retry behavior in production:
Should expose:
Summary
Current RetryPolicy:
This class is the CORE of network reliability and it's BROKEN.
Impact: Production failures, poor performance, hard to debug, impossible to monitor.
Priority: CRITICAL - This affects every network operation in the library.