Resilience
Make an action survive slow or failing dependencies with retries, a timeout and a fallback.
An action gets resilience from three places. @Resilience declares retry and timeout. @Fallback names the method that runs when the action still fails. ResilienceExecutor wraps any operation of your own, outside an action.
What @Resilience applies
@Resilience goes on an action method or on the role class. A method-level annotation replaces the class-level one as a whole. Fields are not merged.
| Field | Applied on an action | Notes |
|---|---|---|
retry | Yes | Runs when retry.maxAttempts is above 0. |
timeout | Yes | Milliseconds. 0 means no timeout. |
circuitBreaker | No | Parsed only. Every action gets the default breaker below. |
rateLimit | No | Parsed only. |
bulkhead, maxConcurrent | No | Parsed only. |
fallback | No | Parsed only. Use @Fallback instead. |
If neither retry nor timeout is set, the action runs unwrapped.
When retry or timeout is set, the action also runs behind a circuit breaker keyed by the action name. Its settings are fixed:
- It opens at a 50% failure rate.
- It counts the last 20 calls, and waits for at least 10.
- It stays open for 15 seconds.
Retry an action and fall back
fetchQuote retries up to three times on IOException, with each attempt bounded by a 5 second timeout. If every attempt fails, cachedQuote returns a stand-in value.
package com.example.tnsai.docs;
import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.Fallback;
import com.tnsai.annotations.Resilience;
import com.tnsai.enums.ActionType;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.roles.Role;
import java.io.IOException;
public final class ResilientQuoteRole extends Role {
@Override
public RoleIdentity getIdentity() {
return new RoleIdentity(
"quotes",
"Fetch prices from an upstream quote service",
"market-data");
}
@ActionSpec(
type = ActionType.LOCAL,
description = "Fetch the latest price for a ticker symbol")
@Resilience(
retry = @Resilience.Retry(
maxAttempts = 3,
backoffMs = 500,
retryOn = IOException.class),
timeout = 5000)
public String fetchQuote(String symbol) throws IOException {
return callUpstream(symbol);
}
@Fallback(forAction = "fetchQuote")
public String cachedQuote(String symbol, Exception error) {
return symbol + ": price unavailable";
}
private String callUpstream(String symbol) throws IOException {
throw new IOException("quote service unreachable");
}
}@Resilience.Retry has these fields:
| Field | Default | Meaning |
|---|---|---|
maxAttempts | 0 | Total attempts. 0 turns retry off. |
backoffMs | 1000 | Delay before the second attempt. |
multiplier | 2.0 | Growth factor for each later delay. |
maxBackoffMs | 30000 | Upper bound on one delay. |
retryOn | all | Exception types that trigger a retry. Error types are ignored. |
noRetryOn | none | Exception types that never retry. Error types are ignored. |
jitter | true | Adds a random 0 to 25% to each delay. |
Choose the fallback method
@Fallback marks a method on the same role. The framework calls it after the action and its retries have failed.
@Fallback(forAction = "fetchQuote")handles one action.@Fallbackwith noforActionhandles every action on the role that has no fallback of its own.- The method takes the action's parameters in declaration order, then an
ExceptionorThrowable. maxRetriesreruns the action that many times, with no delay, before the fallback runs. The default is 0.
Set resilience on a programmatic role
A role built with RoleBuilder has no annotated methods. Give it RoleBuilder.resilience(ResilienceConfig) instead:
package com.example.tnsai.docs;
import com.tnsai.resilience.ResilienceConfig;
import com.tnsai.resilience.RetryPolicy;
import com.tnsai.roles.Role;
import com.tnsai.roles.RoleBuilder;
public final class ResilienceRoleBuilderExample {
private ResilienceRoleBuilderExample() {}
public static Role quotesRole() {
return RoleBuilder.create()
.name("quotes")
.resilience(ResilienceConfig.builder()
.retry(RetryPolicy.builder().maxAttempts(3).build())
.timeoutMs(5000)
.build())
.build();
}
}The config carries retry and timeout only. It applies when no @Resilience annotation does.
Wrap your own operation
ResilienceExecutor runs any Callable with the full set of layers. Use it for calls you make outside an action.
package com.example.tnsai.docs;
import com.tnsai.resilience.ResilienceExecutor;
import com.tnsai.resilience.ResilienceRequest;
import com.tnsai.resilience.RetryPolicy;
import com.tnsai.resilience.dlq.DeadLetterEntry;
import com.tnsai.resilience.dlq.InMemoryDeadLetterQueue;
import java.time.Duration;
public final class ResilienceExecutorExample {
private ResilienceExecutorExample() {}
public static String fetchWithLimits(String symbol) throws Exception {
InMemoryDeadLetterQueue deadLetters = new InMemoryDeadLetterQueue();
ResilienceExecutor executor = new ResilienceExecutor(deadLetters);
ResilienceRequest<String> request = ResilienceRequest.<String>builder()
.operationId("fetch-quote")
.operation(() -> callUpstream(symbol))
.retryPolicy(RetryPolicy.builder()
.maxAttempts(3)
.initialBackoff(Duration.ofMillis(500))
.build())
.fallbackStrategy(error -> symbol + ": price unavailable")
.rateLimit(20, 1000)
.bulkhead(5)
.timeout(5000)
.build();
String result = executor.execute(request);
for (DeadLetterEntry entry : deadLetters.getEntries("fetch-quote")) {
System.out.println(entry.getExceptionType() + ": " + entry.getExceptionMessage());
}
return result;
}
private static String callUpstream(String symbol) {
throw new IllegalStateException("quote service unreachable");
}
}The layers wrap each other from outside in:
- Timeout
- Rate limit
- Bulkhead
- Circuit breaker
- Retry
- The operation
Every failure goes to the dead-letter queue, including a failure that the fallback then recovers. The fallbackStrategy handles ordinary exceptions only. These rejections are thrown to the caller unchanged:
| Rejection | Exception |
|---|---|
| Timeout | com.tnsai.exceptions.TimeoutException |
| Circuit breaker open | io.github.resilience4j.circuitbreaker.CallNotPermittedException |
| Bulkhead full | io.github.resilience4j.bulkhead.BulkheadFullException |
| Rate limit reached | io.github.resilience4j.ratelimiter.RequestNotPermitted |
Each DeadLetterEntry has getOperationId(), getExceptionType(), getExceptionMessage(), getTimestamp() and getSpec(). getSpec() returns the request metadata plus operationId and failureType.
Related
- Actions: where resilience runs in the action pipeline.
- Exceptions: the error categories an action can raise.
- Long runs: progress timeouts and abort reasons for long work.
- Trace, meter and fall back: a full tutorial with a fallback.