forked from ReactiveX/RxJava
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue ReactiveX#43: Added CircuitBreaker and Retry decorations for Co…
…mpletionStage
- Loading branch information
Showing
8 changed files
with
559 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ | |
*.iml | ||
.gradle | ||
build | ||
classes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
package io.github.robwin.retry; | ||
|
||
import io.github.robwin.retry.event.RetryEvent; | ||
import io.github.robwin.retry.internal.AsyncRetryContext; | ||
import io.reactivex.Flowable; | ||
|
||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.CompletionStage; | ||
import java.util.concurrent.ScheduledExecutorService; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.function.Supplier; | ||
|
||
public interface AsyncRetry { | ||
|
||
/** | ||
* Returns the ID of this Retry. | ||
* | ||
* @return the ID of this Retry | ||
*/ | ||
String getId(); | ||
|
||
/** | ||
* Records a successful call. | ||
*/ | ||
void onSuccess(); | ||
|
||
/** | ||
* Records an failed call. | ||
* @return delay in milliseconds until the next try | ||
*/ | ||
long onError(Throwable throwable); | ||
|
||
/** | ||
* Returns a reactive stream of RetryEvents. | ||
* | ||
* @return a reactive stream of RetryEvents | ||
*/ | ||
Flowable<RetryEvent> getEventStream(); | ||
|
||
/** | ||
* Creates a Retry with a custom Retry configuration. | ||
* | ||
* @param id the ID of the Retry | ||
* @param retryConfig a custom Retry configuration | ||
* | ||
* @return a Retry with a custom Retry configuration. | ||
*/ | ||
static AsyncRetry of(String id, RetryConfig retryConfig){ | ||
return new AsyncRetryContext(id, retryConfig); | ||
} | ||
|
||
/** | ||
* Creates a Retry with a custom Retry configuration. | ||
* | ||
* @param id the ID of the Retry | ||
* @param retryConfigSupplier a supplier of a custom Retry configuration | ||
* | ||
* @return a Retry with a custom Retry configuration. | ||
*/ | ||
static AsyncRetry of(String id, Supplier<RetryConfig> retryConfigSupplier){ | ||
return of(id, retryConfigSupplier.get()); | ||
} | ||
|
||
/** | ||
* Creates a Retry with default configuration. | ||
* | ||
* @param id the ID of the Retry | ||
* @return a Retry with default configuration | ||
*/ | ||
static AsyncRetry ofDefaults(String id){ | ||
return of(id, RetryConfig.ofDefaults()); | ||
} | ||
|
||
/** | ||
* Decorates CompletionStageSupplier with Retry | ||
* | ||
* @param retryContext retry context | ||
* @param scheduler execution service to use to schedule retries | ||
* @param supplier completion stage supplier | ||
* @param <T> type of completion stage result | ||
* @return decorated supplier | ||
*/ | ||
static <T> Supplier<CompletionStage<T>> decorateCompletionStage( | ||
AsyncRetry retryContext, | ||
ScheduledExecutorService scheduler, | ||
Supplier<CompletionStage<T>> supplier | ||
) { | ||
return () -> { | ||
|
||
final CompletableFuture<T> promise = new CompletableFuture<>(); | ||
final Runnable block = new AsyncRetryBlock<>(scheduler, retryContext, supplier, promise); | ||
block.run(); | ||
|
||
return promise; | ||
}; | ||
} | ||
} | ||
|
||
class AsyncRetryBlock<T> implements Runnable { | ||
private final ScheduledExecutorService scheduler; | ||
private final AsyncRetry retryContext; | ||
private final Supplier<CompletionStage<T>> supplier; | ||
private final CompletableFuture<T> promise; | ||
|
||
AsyncRetryBlock( | ||
ScheduledExecutorService scheduler, | ||
AsyncRetry retryContext, | ||
Supplier<CompletionStage<T>> supplier, | ||
CompletableFuture<T> promise | ||
) { | ||
this.scheduler = scheduler; | ||
this.retryContext = retryContext; | ||
this.supplier = supplier; | ||
this.promise = promise; | ||
} | ||
|
||
@Override | ||
public void run() { | ||
final CompletionStage<T> stage; | ||
|
||
try { | ||
stage = supplier.get(); | ||
} catch (Throwable t) { | ||
onError(t); | ||
return; | ||
} | ||
|
||
stage.whenComplete((result, t) -> { | ||
if (t != null) { | ||
onError(t); | ||
} else { | ||
promise.complete(result); | ||
retryContext.onSuccess(); | ||
} | ||
}); | ||
} | ||
|
||
private void onError(Throwable t) { | ||
final long delay = retryContext.onError(t); | ||
|
||
if (delay < 1) { | ||
promise.completeExceptionally(t); | ||
} else { | ||
scheduler.schedule(this, delay, TimeUnit.MILLISECONDS); | ||
} | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
src/main/java/io/github/robwin/retry/internal/AsyncRetryContext.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package io.github.robwin.retry.internal; | ||
|
||
import io.github.robwin.retry.AsyncRetry; | ||
import io.github.robwin.retry.RetryConfig; | ||
import io.github.robwin.retry.event.RetryEvent; | ||
import io.github.robwin.retry.event.RetryOnErrorEvent; | ||
import io.github.robwin.retry.event.RetryOnSuccessEvent; | ||
import io.reactivex.Flowable; | ||
import io.reactivex.processors.FlowableProcessor; | ||
import io.reactivex.processors.PublishProcessor; | ||
import javaslang.collection.Stream; | ||
|
||
import java.time.Duration; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
import java.util.function.Function; | ||
import java.util.function.Supplier; | ||
|
||
public class AsyncRetryContext implements AsyncRetry { | ||
|
||
private final String id; | ||
private final int maxAttempts; | ||
private Duration waitDuration; | ||
private final Function<Duration, Duration> backoffFunction; | ||
private final FlowableProcessor<RetryEvent> eventPublisher; | ||
|
||
private final AtomicInteger numOfAttempts = new AtomicInteger(0); | ||
|
||
public AsyncRetryContext(String id, RetryConfig config) { | ||
this.id = id; | ||
this.maxAttempts = config.getMaxAttempts(); | ||
this.backoffFunction = config.getBackoffFunction(); | ||
this.waitDuration = config.getWaitDuration(); | ||
|
||
PublishProcessor<RetryEvent> publisher = PublishProcessor.create(); | ||
this.eventPublisher = publisher.toSerialized(); | ||
} | ||
|
||
@Override | ||
public String getId() { | ||
return id; | ||
} | ||
|
||
@Override | ||
public void onSuccess() { | ||
int currentNumOfAttempts = numOfAttempts.get(); | ||
publishRetryEvent(() -> new RetryOnSuccessEvent(id, currentNumOfAttempts, null)); | ||
} | ||
|
||
@Override | ||
public long onError(Throwable throwable) { | ||
int attempt = numOfAttempts.addAndGet(1); | ||
publishRetryEvent(() -> new RetryOnErrorEvent(id, attempt, throwable)); | ||
return calculateInterval(attempt); | ||
} | ||
|
||
@Override | ||
public Flowable<RetryEvent> getEventStream() { | ||
return eventPublisher; | ||
} | ||
|
||
|
||
private long calculateInterval(int attempt) { | ||
|
||
if (attempt > maxAttempts) { | ||
return -1; | ||
} else { | ||
return Stream.iterate(waitDuration, backoffFunction) | ||
.get(attempt - 1) | ||
.toMillis(); | ||
} | ||
} | ||
|
||
private void publishRetryEvent(Supplier<RetryEvent> event) { | ||
if(eventPublisher.hasSubscribers()) { | ||
eventPublisher.onNext(event.get()); | ||
} | ||
} | ||
} |
Oops, something went wrong.