Skip to content

Commit 431e4f9

Browse files
committed
Guard against potential race between Timeout interrupt and execution completion
1 parent e00731b commit 431e4f9

File tree

3 files changed

+32
-17
lines changed

3 files changed

+32
-17
lines changed

src/main/java/net/jodah/failsafe/AbstractExecution.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,16 @@ public abstract class AbstractExecution extends ExecutionContext {
3535
final List<PolicyExecutor<Policy<Object>>> policyExecutors;
3636

3737
// Internally mutable state
38+
/* Whether a result has been post-executed */
3839
volatile boolean resultHandled;
39-
/** The wait time in nanoseconds. */
40+
/* Whether the execution can be interrupted */
41+
volatile boolean canInterrupt;
42+
/* Whether the execution has been interrupted */
43+
volatile boolean interrupted;
44+
/* The wait time in nanoseconds. */
4045
private volatile long waitNanos;
46+
/* Whether the execution has been completed */
4147
volatile boolean completed;
42-
volatile boolean interrupted;
4348

4449
/**
4550
* Creates a new AbstractExecution for the {@code executor}.
@@ -72,9 +77,10 @@ void preExecute() {
7277
attemptStartTime = Duration.ofNanos(System.nanoTime());
7378
if (startTime == Duration.ZERO)
7479
startTime = attemptStartTime;
80+
resultHandled = false;
7581
cancelled = false;
82+
canInterrupt = true;
7683
interrupted = false;
77-
resultHandled = false;
7884
}
7985

8086
boolean isAsyncExecution() {

src/main/java/net/jodah/failsafe/Functions.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,21 @@ static <T> Supplier<ExecutionResult> resultSupplierOf(CheckedSupplier<T> supplie
285285
result = ExecutionResult.success(supplier.get());
286286
} catch (Throwable t) {
287287
throwable = t;
288-
// Propogate InterruptedException if not intentionally interrupted by a timeout
289-
if (!execution.interrupted && t instanceof InterruptedException)
290-
Thread.currentThread().interrupt();
291288
result = ExecutionResult.failure(t);
292289
} finally {
293-
// Clear interrupt flag if execution was meant to be interrupted
294-
if (execution.interrupted && (throwable == null || !(throwable instanceof InterruptedException)))
295-
Thread.interrupted();
290+
// Guard against race with Timeout interruption
291+
synchronized (execution) {
292+
execution.canInterrupt = false;
293+
if (execution.interrupted)
294+
// Clear interrupt flag if interruption was intended
295+
Thread.interrupted();
296+
else if (throwable instanceof InterruptedException)
297+
// Set interrupt flag if interrupt occurred but was not intentional
298+
Thread.currentThread().interrupt();
296299

297-
if (result != null)
298-
execution.record(result);
300+
if (result != null)
301+
execution.record(result);
302+
}
299303
}
300304
return result;
301305
};

src/main/java/net/jodah/failsafe/TimeoutExecutor.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,18 @@ protected Supplier<ExecutionResult> supply(Supplier<ExecutionResult> supplier, S
6666
if (result.getAndUpdate(v -> v != null ? v : ExecutionResult.failure(new TimeoutExceededException(policy)))
6767
== null) {
6868
if (policy.canCancel()) {
69-
boolean canInterrupt = policy.canInterrupt();
70-
if (canInterrupt)
71-
execution.record(result.get());
69+
// Cancel and interrupt
7270
execution.cancelled = true;
73-
execution.interrupted = canInterrupt;
74-
if (canInterrupt)
75-
executionThread.interrupt();
71+
if (policy.canInterrupt()) {
72+
// Guard against race with the execution completing
73+
synchronized (execution) {
74+
if (execution.canInterrupt) {
75+
execution.record(result.get());
76+
execution.interrupted = true;
77+
executionThread.interrupt();
78+
}
79+
}
80+
}
7681
}
7782
}
7883
return null;

0 commit comments

Comments
 (0)