Solución de problemas
Problemas con los que se encuentra la gente al usar resilience4cata, por síntoma, con la causa y la solución.
Cada error que reporta la biblioteca empieza con un código como [R4C-RT-003]; la referencia de errores tiene una sección para cada uno, con un ejemplo incorrecto y la solución.
Build y generación de código
Resilient.of falla: "No generated class … found"
[R4C-RT-003] No generated class com.acme.shop.clients.PriceClient$Resilient found. Make sure that com.acme.shop.clients.PriceClient has at least one resilience annotation, and that the annotation processor runs when it is compiled. With Gradle:
annotationProcessor 'dev.resilience4cata:resilience4cata-processor:<version>'
El procesador no se ejecutó cuando se compiló la interfaz, o la interfaz no tiene ninguna anotación de resiliencia. El procesador es obligatorio: no hay alternativa por reflexión.
- Gradle: agrega
annotationProcessor 'dev.resilience4cata:resilience4cata-processor'(yannotationProcessor platform(…bom…)si usas el BOM, porque la ruta de procesadores se resuelve por separado). Para tipos anotados bajosrc/test, usa tambiéntestAnnotationProcessorytestCompileOnly. - Maven: incluye el procesador en
<annotationProcessorPaths>demaven-compiler-plugin. Con maven-compiler-plugin 3.12.0 o posterior, un<path>sin<version>toma su versión dedependencyManagement(el BOM importado); las versiones anteriores del plugin necesitan una<version>explícita. - Procesador solo en el class path: desde JDK 23,
javacya no ejecuta los procesadores que encuentra en el class path a menos que se le pida. Ponlo en la ruta de procesadores. - Verificación: después de un build, existe
build/generated/sources/annotationProcessor/java/main/<package>/<Type>$Resilient.java(Gradle) otarget/generated-sources/annotations/…(Maven).
Consulta R4C-RT-003 e Instalación.
[R4C-RT-004]: la clase generada no coincide
resilience4cata-core y resilience4cata-processor tienen versiones distintas. Toma ambos del BOM, o dales la misma versión. Luego haz un build limpio para que desaparezcan las clases generadas antiguas. Consulta R4C-RT-004.
[R4C-RT-005] en una aplicación modular
core carga <Type>$Resilient por reflexión, así que tu módulo debe exportarle (o abrirle) el paquete: exports com.acme.shop.clients to dev.resilience4cata.core;. Consulta R4C-RT-005.
El IDE no ejecuta el procesador
El build funciona en la línea de comandos, pero en el IDE las clases $Resilient faltan o están desactualizadas.
- IntelliJ IDEA: en proyectos Gradle y Maven, IDEA lee la ruta de procesadores desde el build. Si compilas con el compilador propio de IDEA, habilita Settings → Build, Execution, Deployment → Compiler → Annotation Processors → Enable annotation processing, o deja que Gradle compile el proyecto (Build and run using: Gradle).
- Eclipse: el procesamiento de anotaciones debe estar habilitado para el proyecto (Buildship para Gradle, el soporte de procesamiento de anotaciones de m2e para Maven).
- Después de cambiar anotaciones, vuelve a compilar el módulo: el procesador escribe la fuente generada cuando se compila la interfaz anotada.
Consulta Instalación para cada IDE.
Lombok, MapStruct u otros procesadores en el mismo build
El procesador de resilience4cata puede ejecutarse junto a otros procesadores de anotaciones. Ponlos todos en la misma ruta de procesadores:
- Gradle: una línea
annotationProcessorpor procesador (org.projectlombok:lombok,org.mapstruct:mapstruct-processor,dev.resilience4cata:resilience4cata-processor). - Maven: una vez que se define
<annotationProcessorPaths>, Maven ejecuta solo lo que esa lista incluye, así que incluye ahí también Lombok y MapStruct. - Pon Lombok antes que resilience4cata, en las líneas
annotationProcessorde Gradle y en<annotationProcessorPaths>de Maven. La subclase generada de una clase anotada replica los constructores de la clase, así que debe ver los constructores que agrega Lombok. Con resilience4cata primero, una clase anotada que usa@RequiredArgsConstructor(u otro constructor de Lombok) no compila, con un error comoconstructor … in class … cannot be applied to given types.
El procesador solo lee las interfaces y clases que llevan anotaciones de resiliencia y, cuando hay otros procesadores presentes, reclama solo sus propias anotaciones, así que no se las oculta.
-Xlint:all -Werror falla con "No processor claimed any of these annotations"
resilience4cata en sí no lo causa: solo en la ruta de procesadores reclama las anotaciones que ve, y junto a otros procesadores reclama solo dev.resilience4cata.annotation.*. Si la advertencia lista otras anotaciones, otro procesador las dejó sin reclamar; agrega -Xlint:-processing a los argumentos del compilador, o corrígelo en ese procesador.
-Werror falla con una advertencia auxiliaryclass en el código generado
Una interfaz anotada declarada como segundo tipo de nivel superior, no público, en el archivo fuente de otro tipo (Clients.java con public class Clients e interface PriceClient) es una clase auxiliar. El PriceClient$Resilient generado vive en su propio archivo y la referencia, así que javac -Xlint:all advierte auxiliary class PriceClient in Clients.java should not be accessed from outside its own source file, y -Werror lo convierte en error. Mueve la interfaz a su propio archivo (PriceClient.java), o anídala como tipo miembro de la clase.
Errores de compilación del procesador
El procesador verifica las anotaciones en tiempo de compilación y reporta errores R4C-APT, R4C-ATR, R4C-CMP, R4C-ASY, R4C-CLS, R4C-FB y R4C-SUB sobre el elemento anotado. Los más comunes:
| Síntoma | Código |
|---|---|
anotación en un método static o private |
R4C-APT-002 |
| anotación en una interfaz privada | R4C-APT-003 |
una duración como "2 s" o "1.5s" |
R4C-ATR-001 |
@Timeout("0") |
R4C-ATR-002 |
un método async que devuelve algo distinto de CompletionStage<T> / CompletableFuture<T> (por ejemplo ? super T) |
R4C-ASY-001 |
| la firma del método de fallback no encaja | R4C-FB-003 |
una clase anotada que es final |
R4C-SUB-001 |
Comportamiento
Una llamada dentro de mi implementación no se reintenta
Con Resilient.of (el estilo de interfaz), las políticas están en el wrapper generado, que llama a tu implementación. Cuando la implementación llama a uno de sus propios métodos, la llamada no pasa por el wrapper:
public interface CatalogApi {
@Retry(maxAttempts = 3, delay = "0")
int price(String sku);
int total(List<String> skus); // not annotated
}
final class HttpCatalog implements CatalogApi {
@Override
public int price(String sku) {
return fetchPrice(sku); // may fail
}
@Override
public int total(List<String> skus) {
// this.price(...) calls the implementation directly, not the generated wrapper: no retry here
return skus.stream().mapToInt(this::price).sum();
}
private int failuresLeft = 1; // a flaky remote call: fails once
private int fetchPrice(String sku) {
if (failuresLeft-- > 0) {
throw new IllegalStateException("flaky");
}
return 10;
}
}
catalog.price("a") se reintenta; catalog.total(List.of("a")) falla en el primer error, porque total llama a this.price en la implementación. Es una propiedad del estilo de interfaz, no un bug, y no se intenta sortear. Para interceptar las autollamadas, anota una clase en su lugar: la subclase generada sobrescribe los métodos, así que this.price(…) pasa por las políticas. Consulta Clases anotadas.
Las anotaciones en mi clase de implementación no tienen efecto
Resilient.of(Interface.class, impl, registry) usa solo las anotaciones de la interfaz (y de sus superinterfaces). El wrapper no lee las anotaciones de la clase de implementación. Anotar la clase la convierte en una clase anotada: el procesador genera una subclase para ella, que creas con Resilient.create(Type.class, registry, args…) o new Type$Resilient(registry, args…), y la clase debe entonces cumplir las reglas de las clases anotadas (no final, no sealed, no abstracta… consulta de R4C-SUB-001 a R4C-SUB-008). Elige un estilo por tipo.
Los retries se detienen en cuanto el breaker se abre
Es el comportamiento por defecto, a propósito. El retry está fuera del circuit breaker (orden: Fallback → Retry → CircuitBreaker → RateLimiter → AdaptiveLimit → Timeout → Bulkhead), y un retry nunca reintenta una CallNotPermittedException: una vez que el breaker rechaza un intento, reintentar solo sería rechazado otra vez. El retry se rinde con GaveUp(reason = NOT_RETRYABLE) y vuelve a lanzar la CallNotPermittedException (o se ejecuta el fallback).
Esto vale digan lo que digan abortOn y retryOn, y también con un classifier; no es una entrada de abortOn, así que definir tu propia lista no lo quita. Consulta Retry y Circuit breaker.
Un timeout no detiene mi código
Un @Timeout sync ejecuta la llamada en el thread de quien llama y lo interrumpe cuando se acaba el tiempo. El código que ignora las interrupciones sigue ejecutándose hasta que retorna por sí solo, y solo entonces quien llama recibe CallTimeoutException:
- lecturas bloqueantes clásicas de
java.net.Socket/InputStream, y las bibliotecas construidas sobre ellas; - bucles activos que nunca verifican
Thread.interrupted(); - código que captura
InterruptedExceptiony sigue adelante.
Soluciones: configura los timeouts de E/S del propio cliente (timeouts de conexión y de lectura; java.net.http.HttpClient, los canales NIO y la mayoría de los clientes modernos responden a las interrupciones o tienen sus propios timeouts), verifica Thread.interrupted() en los bucles largos y restablece el flag de interrupción cuando captures InterruptedException. En los métodos async, el stage devuelto falla con CallTimeoutException a tiempo, y el stage de la llamada se cancela si es un Future; que el trabajo detrás de él se detenga depende de ese código.
Los timeouts además son poco precisos por diseño: nunca antes de tiempo, a lo sumo unos dos ticks del watchdog tarde (un tick es el timeout más corto en uso / 16, entre 1 y 50 ms). Consulta Timeout.
Mi configuración externa no se aplica
Verifica, en orden:
- El valor lo sobrescribe algo con mayor precedencia. De menor a mayor: valores por defecto integrados,
defaults.*,configs.<name>.*, atributos de la anotación,instances.<id>.*. Una clavedefaults.*no cambia un atributo que la anotación define explícitamente; usainstances.<id>.*para eso. Consulta precedencia. - La fuente no se lee.
ResilienceConfig.load()lee solo el recurso del classpathresilience4cata.properties, las variables de entorno (R4C_…) y las propiedades del sistema con el prefijor4c.oresilience4cata.; otros archivos necesitanResilienceConfig.builder().file(…)o.classpath(…). - La configuración no se aplica al registry. Envuelve tus servicios con el registry construido por
config.applyTo(ResilienceRegistry.builder()).build()(oconfig.toRegistry()). - El id de instancia no coincide. Una clave
instances.<id>cuyo id no coincide con nada queda sin usar en silencio hasta que lo preguntes.config.describe()lista cada valor efectivo con su fuente;config.verifyInstances(registry), llamado después de envolver tus servicios, falla con los ids que no coinciden con ninguna instancia y sugiere el más cercano:
ResilienceConfig config = ResilienceConfig.builder().properties("app.properties", properties).build();
System.out.println(config.describe()); // every effective value and where it comes from
ResilienceRegistry registry = config.applyTo(ResilienceRegistry.builder()).build();
Payments payments = Resilient.of(Payments.class, order -> "ok", registry);
config.verifyInstances(registry); // after every service is wrapped: fails on ids that match nothing
Los ids derivados se ven como com.acme.Api#get(java.lang.String) y deben ir entre comillas en las claves: instances."com.acme.Api#get(java.lang.String)".timeout.value=2s. Consulta R4C-CFG-013 y Claves de configuración.
- Esperabas una recarga. La configuración se lee una sola vez, cuando se construyen el registry y sus instancias; no hay recarga en caliente.
IllegalArgumentException [R4C-REG-009] desde registry.circuitBreaker(id)
El id es incorrecto, o la instancia todavía no existe: las instancias se crean cuando se envuelve la interfaz (Resilient.of, Resilient.create) o se construye el Pipeline. El mensaje sugiere el id más cercano. Consulta R4C-REG-009.
IllegalStateException [R4C-REG-008] al envolver
Dos métodos comparten un name (o un id de pipeline) pero se resuelven a configuraciones distintas. Los métodos que comparten una instancia deben coincidir en todos los atributos; dales nombres distintos, o los mismos atributos. Consulta R4C-REG-008.