|
24 | 24 | import dev.failsafe.spi.Scheduler;
|
25 | 25 |
|
26 | 26 | import java.time.Duration;
|
| 27 | +import java.util.concurrent.CancellationException; |
27 | 28 | import java.util.concurrent.CompletableFuture;
|
28 | 29 | import java.util.concurrent.Future;
|
29 | 30 | import java.util.concurrent.TimeUnit;
|
@@ -62,22 +63,33 @@ protected CompletableFuture<ExecutionResult<R>> preExecuteAsync(Scheduler schedu
|
62 | 63 | CompletableFuture<ExecutionResult<R>> promise = new CompletableFuture<>();
|
63 | 64 | CompletableFuture<Void> acquireFuture = bulkhead.acquirePermitAsync();
|
64 | 65 | acquireFuture.whenComplete((result, error) -> {
|
65 |
| - // Signal for execution to proceed |
66 |
| - promise.complete(ExecutionResult.none()); |
| 66 | + if (error instanceof CancellationException) { |
| 67 | + // Cancellation of acquireFuture future means either cancellation in the scheduler (in which case we probably |
| 68 | + // do not care about the result too much), or cancellation because we reached maxWaitTime (see below) - in which case |
| 69 | + // we want to inform the user with BulkheadFullException. |
| 70 | + promise.complete(ExecutionResult.exception(new BulkheadFullException(bulkhead))); |
| 71 | + } else { |
| 72 | + // Signal for execution to proceed |
| 73 | + promise.complete(ExecutionResult.none()); |
| 74 | + } |
67 | 75 | });
|
68 | 76 |
|
69 | 77 | if (!promise.isDone()) {
|
70 | 78 | try {
|
71 | 79 | // Schedule bulkhead permit timeout
|
72 | 80 | Future<?> timeoutFuture = scheduler.schedule(() -> {
|
73 |
| - promise.complete(ExecutionResult.exception(new BulkheadFullException(bulkhead))); |
| 81 | + // Note: we cannot call `promise.complete` here directly. Doing so would result in a following race condition: |
| 82 | + // * `promise` would be considered failed (i.e. caller would think thar no permit was acquired) |
| 83 | + // * but some other thread may release permit before we call `acquireFuture.cancel` - resulting in `acquireFuture` being completed |
| 84 | + // successfully (and permit acquired). But since `promise` is already completed that fact is ignored. |
| 85 | + // This discrepancy would lead to permits 'leaking'. So instead we make `acquireFuture.whenComplete` be the only way |
| 86 | + // to complete `promise` and here we just signal to that code that `promise` should return BulkheadFullException. |
74 | 87 | acquireFuture.cancel(true);
|
75 | 88 | return null;
|
76 | 89 | }, maxWaitTime.toNanos(), TimeUnit.NANOSECONDS);
|
77 | 90 |
|
78 | 91 | // Propagate outer cancellations to the promise, bulkhead acquire future, and timeout future
|
79 | 92 | future.setCancelFn(this, (mayInterrupt, cancelResult) -> {
|
80 |
| - promise.complete(cancelResult); |
81 | 93 | acquireFuture.cancel(mayInterrupt);
|
82 | 94 | timeoutFuture.cancel(mayInterrupt);
|
83 | 95 | });
|
|
0 commit comments