Resiliencia,
compilada.
Anota una interfaz Java con retry, circuit breaker, timeout y compañía. Al compilar, un procesador de anotaciones escribe el Java plano que los aplica. Sin proxies, sin reflexión, sin framework.
- Java 21+
- Funciona sin Spring, CDI ni AspectJ
- Síncrono y
CompletionStage - Apache 2.0
dependencies { implementation 'dev.resilience4cata:resilience4cata-all:0.10.0' annotationProcessor 'dev.resilience4cata:resilience4cata-processor:0.10.0' }
dependencies { implementation("dev.resilience4cata:resilience4cata-all:0.10.0") annotationProcessor("dev.resilience4cata:resilience4cata-processor:0.10.0") }
<dependency> <groupId>dev.resilience4cata</groupId> <artifactId>resilience4cata-all</artifactId> <version>0.10.0</version> </dependency> <!-- in the maven-compiler-plugin <configuration> --> <annotationProcessorPaths> <path> <groupId>dev.resilience4cata</groupId> <artifactId>resilience4cata-processor</artifactId> <version>0.10.0</version> </path> </annotationProcessorPaths>
Versión 0.10.0 en Maven Central. resilience4cata-all trae core, las anotaciones y la configuración; el procesador va solo en el path de procesadores de anotaciones. Guía de instalación
PaymentClient.javapublic interface PaymentClient { @Retry(maxAttempts = 3, delay = "200ms") @CircuitBreaker @Timeout("2s") Receipt charge(Order order); } // once, at startup ResilienceRegistry registry = ResilienceRegistry.builder().build(); PaymentClient client = Resilient.of( PaymentClient.class, new HttpPaymentClient(), registry); client.charge(order); // retried, guarded, timed out
PaymentClient$Resilient.java// Generated by the resilience4cata annotation processor. Do not edit. public final class PaymentClient$Resilient implements PaymentClient { private final PaymentClient delegate; private final RetryPolicy retry0; private final CircuitBreakerPolicy circuitBreaker0; private final TimeoutPolicy timeout0; // … constructor: each policy resolved once from the registry @Override public Receipt charge(Order p0) { final RetryPolicy r4c$retry = this.retry0; final long r4c$start = r4c$retry.start(); int r4c$attempts = 0; while (true) { try { return this.r4c$circuitBreaker0(p0); } catch (Throwable r4c$e) { final long r4c$delay = r4c$retry.onFailure(++r4c$attempts, r4c$e, r4c$start); if (r4c$delay < 0 || !r4c$retry.pause(r4c$delay)) { throw r4c$e; } } } } private Receipt r4c$circuitBreaker0(Order p0) { final CircuitBreakerPolicy r4c$cb = this.circuitBreaker0; final long r4c$token = r4c$cb.acquire(); if (r4c$token < 0) { throw r4c$cb.rejection(); } final long r4c$started = r4c$cb.startTimer(); try { final Receipt r4c$result = this.r4c$timeout0(p0); r4c$cb.onSuccess(r4c$token, r4c$started); return r4c$result; } catch (Throwable r4c$e) { r4c$cb.onFailure(r4c$token, r4c$started, r4c$e); throw r4c$e; } } private Receipt r4c$timeout0(Order p0) { final TimeoutPolicy r4c$timeout = this.timeout0; final TimeoutPolicy.Scope r4c$scope = r4c$timeout.enter(); final Receipt r4c$result; try { r4c$result = this.delegate.charge(p0); } catch (Throwable r4c$e) { r4c$timeout.failed(r4c$scope, r4c$e); throw r4c$e; } r4c$timeout.succeeded(r4c$scope); return r4c$result; } }
HttpPaymentClient.charge
Abreviado: el archivo real usa nombres completamente calificados.
La protección es código que puedes leer
Resilience4j y MicroProfile envuelven tus llamadas en decoradores o proxies en tiempo de ejecución. resilience4cata hace ese trabajo en el compilador, así que lo que se ejecuta es Java común y corriente.
Entra con el debugger
El procesador escribe PaymentClient$Resilient junto a tus propias clases. Cada política son unas pocas líneas rectas en un método que puedes abrir, donde puedes poner un breakpoint y que lees en un stack trace.
at HttpPaymentClient.charge at PaymentClient$Resilient.r4c$timeout0 at PaymentClient$Resilient.r4c$circuitBreaker0 at PaymentClient$Resilient.charge at CheckoutService.pay
Nada se asigna cuando las llamadas salen bien
Las llamadas síncronas que salen bien o se rechazan asignan cero bytes. Un gate de allocations ejecuta los benchmarks en cada pull request y hace fallar el build si eso cambia.
Los errores hacen fallar el build
Una duración mal escrita, un fallback con la firma equivocada o una anotación en un método final es un error de compilación con un código estable y una solución, no una sorpresa en producción.
error: [R4C-ATR-001] @Retry.delay = "2 seconds"
is not a valid duration: … Use a number
followed by ns, us, ms, s, m, h or d …
@Retry(delay = "2 seconds")
^
Tiempo por llamada, escala logarítmica. Menos es mejor.
Ver como tabla
| Escenario | Threads | resilience4cata | Resilience4j |
|---|---|---|---|
| Retry, el primer intento sale bien | 1 | 1.2 ns, 0 B | 22.9 ns, 0 B |
| Circuit breaker, todas las llamadas salen bien | 4 | 1.9 ns, 0 B | 832 ns, 2 B |
| Timeout síncrono | 1 | 22.2 ns, 0 B | 8842 ns, 176 B |
| Bulkhead con lugares libres | 1 | 20.5 ns, 0 B | 20.4 ns, 0 B |
Ocho políticas, una anotación cada una
Ponlas en un método o en toda la interfaz. Combínalas como quieras: el procesador las anida en un orden pensado para ello, que @PolicyOrder puede cambiar.
@Retry
Vuelve a intentar con backoff exponencial y jitter, dentro de un presupuesto de tiempo opcional.
maxAttempts · delay · backoff · maxDuration@Hedge
Vuelve a lanzar la misma llamada async idempotente cuando va lenta, y se queda con el primer éxito.
delay · maxAttempts · hedgeOn · budgetPercent@CircuitBreaker
Deja de llamar a una dependencia que sigue fallando y luego deja pasar algunas llamadas de prueba.
failureRateThreshold · waitInOpen · halfOpenCalls@RateLimiter
Admite un número fijo de llamadas por período y rechaza o hace esperar al resto.
permits · period · maxWait@AdaptiveLimit
Limita las llamadas concurrentes con un límite que se ajusta solo: sube mientras la latencia se mantiene, baja cuando crece.
algorithm · minLimit · maxLimit@Timeout
Interrumpe una llamada que se pasa de su presupuesto, en el propio thread de quien llama.
value@Bulkhead
Limita cuántas llamadas corren a la vez, para que una dependencia lenta no se quede con todos los threads.
maxConcurrent · maxWait@Fallback
Devuelve una respuesta útil cuando todo lo demás falló, desde un método hermano o una clase handler.
value · methodOrden, de afuera hacia adentro: Fallback, Retry o Hedge, CircuitBreaker, RateLimiter, AdaptiveLimit, Timeout, Bulkhead y luego tu método. Cómo se combinan
De la dependencia a la primera llamada protegida
Agrega el procesador
Core en tiempo de ejecución, las anotaciones solo al compilar y el procesador en el path de procesadores de anotaciones.
annotationProcessor 'dev.resilience4cata: resilience4cata-processor'
Anota una interfaz
Las duraciones son strings como
"200ms", verificadas al compilar.@Retry(maxAttempts = 3) @Timeout("2s") Receipt charge(Order order);
Envuelve tu implementación
Un registry por aplicación comparte el estado de breakers y limiters por nombre.
Resilient.of(PaymentClient.class, new HttpPaymentClient(), registry);
Resiliencia basada en anotaciones para Java 21+ puro. Licencia Apache 2.0.
dev.resilience4cata:resilience4cata-bom:0.10.0