Code review comment: The test itself is easy to expire due to the too short timeout duration. 

Code before review:
public void testDiscardOnTimeout() throws Exception {
int timeoutMs = 5;
int numAttempts = 7;
int successfulAttempt = numAttempts - 1;
List<Integer> completed = new CopyOnWriteArrayList<>();
List<Integer> discarded = new CopyOnWriteArrayList<>();
AtomicBoolean executionBlocked = new AtomicBoolean(true);
Deadline deadline = Deadline.fromNow(Duration.ofMillis(timeoutMs * numAttempts * 2));
try (RetryingExecutor executor =
new RetryingExecutor(
numAttempts,
createUnregisteredChangelogStorageMetricGroup().getAttemptsPerUpload())) {
executor.execute(
RetryPolicy.fixed(numAttempts, timeoutMs, 0),
new RetriableAction<Integer>() {
private final AtomicInteger attemptsCounter = new AtomicInteger(0);
@Override
public Integer tryExecute() throws Exception {
int attempt = attemptsCounter.getAndIncrement();
if (attempt < successfulAttempt) {
while (executionBlocked.get()) {
Thread.sleep(10);
}
}
return attempt;
}
@Override
public void completeWithResult(Integer result) {
completed.add(result);
}
@Override
public void discardResult(Integer result) {
discarded.add(result);
}
@Override
public void handleFailure(Throwable throwable) {}
});
while (completed.isEmpty() && deadline.hasTimeLeft()) {
Thread.sleep(10);
}
executionBlocked.set(false);
while (discarded.size() < successfulAttempt && deadline.hasTimeLeft()) {
Thread.sleep(10);
}
}
assertEquals(singletonList(successfulAttempt), completed);
assertEquals(
IntStream.range(0, successfulAttempt).boxed().collect(toList()),
discarded.stream().sorted().collect(toList()));
}

Line under review: Deadline deadline = Deadline.fromNow(Duration.ofMillis(timeoutMs * numAttempts * 2));

Code after addressing the review:
public void testDiscardOnTimeout() throws Exception {
int timeoutMs = 5;
int numAttempts = 7;
int successfulAttempt = numAttempts - 1;
List<Integer> completed = new CopyOnWriteArrayList<>();
List<Integer> discarded = new CopyOnWriteArrayList<>();
AtomicBoolean executionBlocked = new AtomicBoolean(true);
Deadline deadline = Deadline.fromNow(Duration.ofMinutes(5));
try (RetryingExecutor executor =
new RetryingExecutor(
numAttempts,
createUnregisteredChangelogStorageMetricGroup().getAttemptsPerUpload())) {
executor.execute(
RetryPolicy.fixed(numAttempts, timeoutMs, 0),
new RetriableAction<Integer>() {
private final AtomicInteger attemptsCounter = new AtomicInteger(0);
@Override
public Integer tryExecute() throws Exception {
int attempt = attemptsCounter.getAndIncrement();
if (attempt < successfulAttempt) {
while (executionBlocked.get()) {
Thread.sleep(10);
}
}
return attempt;
}
@Override
public void completeWithResult(Integer result) {
completed.add(result);
}
@Override
public void discardResult(Integer result) {
discarded.add(result);
}
@Override
public void handleFailure(Throwable throwable) {}
});
while (completed.isEmpty() && deadline.hasTimeLeft()) {
Thread.sleep(10);
}
executionBlocked.set(false);
while (discarded.size() < successfulAttempt && deadline.hasTimeLeft()) {
Thread.sleep(10);
}
}
assertEquals(singletonList(successfulAttempt), completed);
assertEquals(
IntStream.range(0, successfulAttempt).boxed().collect(toList()),
discarded.stream().sorted().collect(toList()));
}