Plan hacia 1.0
Lo que falta antes de publicar 1.0.0, según una revisión de 0.13.0 hecha por cuatro revisores independientes, y lo que espera hasta después.
Esta página es el plan de trabajo: cada tarea dice qué cambiar, dónde y cómo comprobarlo, para poder retomar el trabajo desde aquí sin más contexto. Marca una tarea como hecha ([x]) en el mismo pull request que la hace, y añade su número.
Estado
- Versión en Maven Central: 0.13.0.
maines 0.14.0-SNAPSHOT. - El plan de rendimiento de las corridas en AWS está terminado (pull requests #53–#59, ver benchmarks).
- Los títulos del Javadoc llevan el nombre del artefacto (#60); javadoc.io se sincronizó para los 11 módulos.
- Siguiente: fase 1 (API), luego las fases 2 y 3, luego el release (fase 4).
La revisión
Cuatro revisores leyeron el repositorio el 2026-11-02, cada uno desde un punto de vista. Ninguno ejecutó el build. Sus hallazgos son afirmaciones por comprobar, no hechos: cada tarea de abajo empieza confirmando el hallazgo.
| Revisor | Veredicto |
|---|---|
| Un usuario de Resilience4j que decide si adoptarla | Todavía no para producción en general; sí para un piloto en un servicio. Ingeniería sólida; el riesgo es la edad del proyecto y un solo mantenedor. |
| Un diseñador de APIs | Casi lista para congelarse; cinco arreglos baratos ahora que serían permanentes después. |
| Un escéptico de rendimiento y concurrencia | Ingeniería cuidadosa, ningún bug confirmado; los titulares son el mejor caso; x86 nunca se midió; cuatro sospechas. |
| Alguien nuevo en librerías de resiliencia | Una llamada protegida en unos 15 minutos; algunas frases viejas o contradictorias; algunas páginas demasiado internas. |
Lo que valoraron, para conservarlo mientras se cambia lo demás:
- Errores en compilación con códigos
R4C-…estables, cada uno documentado con un ejemplo incorrecto y su arreglo, donde Resilience4j resuelvefallbackMethodpor reflexión en ejecución. - Un coste por llamada casi nulo, medido abiertamente: se muestran empates y regresiones, los ajustes de equidad están en el Javadoc de cada benchmark, Resilience4j recibe excepciones sin stack trace y no hay código muerto, y la barrera de asignaciones corre en CI.
- Código generado que es Java plano: los stack traces pasan por
X$Resilient, no por cadenas de proxies, y las llamadas athisse interceptan en el estilo de subclase. - Una guía de migración seria (tablas de configuración, valores por defecto distintos, cuándo quedarse en Resilience4j) y una promesa de compatibilidad explícita respaldada por japicmp y la regla del código generado.
- Una API que puede crecer: las configs son clases final con builders,
ResilienceSnapshotes una clase, los handles yFallbackContextlos implementa la librería, los rechazos comparten una base sealed sin stack trace, los enums documentan que crecen,@Internal/@Experimentalse hacen cumplir,PipelineCallconserva una excepción checked precisa. - Código sin locks correcto, hasta donde lo leyó el revisor: el protocolo
quietdel breaker, el CAS acquireRUNNING → IDLEdel timeout, la liberación y relectura deBulkheadSemaphore, el ordencount++ → offer → drainde los waiters, la marca de cancelación deLinkedFuture, y el presupuesto de retry que se equivoca hacia menos reintentos. - El quickstart (un programa probado, su salida, "What just happened") y la página de instalación (BOM en
annotationProcessor, Maven viejo, descubrimiento en JDK 23, IDEs, orden con Lombok). Los árboles en/es coinciden y el español se lee con naturalidad.
Fase 1: API, antes de congelarla
Un pull request. Todo esto cambia la API pública, lo que es gratis antes de 1.0 e imposible después. Cada cambio lleva una línea en el changelog (en/es) y, si toca el código generado, una comprobación de que examples sigue pasando.
- [x] 1.1 Parámetros de tipo que admiten null. Con
@NullMarked,Pipeline<T>,PipelineCall<T, X>,FallbackHandler<T>yClassifier<T>limitanTa no nulo, aunquePipelineRunnable extends PipelineCall<@Nullable Void, X>ya rompe ese límite y los fallbacks de métodosvoiddevuelven null. Declarar<T extends @Nullable Object>en los cuatro, y en lo demás que reciba el tipo del resultado del usuario (AsyncCall,Recovery,PipelineRunnable: comprobar). Comprobación:./gradlew build; los tests del procesador compilan el código generado con-Werror.- Resultado: (#62) Hecho: los cuatro tipos, más
AsyncCall,Recoveryy losexecuteAsyncde las políticas; los tiposMono/Fluxde reactor siguen sin admitir null (Reactor prohíbe elementos null).
- Resultado: (#62) Hecho: los cuatro tipos, más
- [x] 1.2 Records que crecen.
reference/compatibility.mddice que los records de eventos y snapshots pueden recibir componentes, pero añadir uno elimina el constructor canónico (incompatible en binario, japicmp falla). Decidir: o construir eventos y snapshots no está soportado (decirlo encompatibility.mdy en el Javadoc de los records), o cada componente nuevo conserva el constructor viejo (decirlo, y añadir la regla a CLAUDE.md). La segunda es más amable con los tests de listeners de los usuarios; preferirla.- Resultado: (#62) Hecho: se conserva el constructor viejo (
compatibility.md,ResilienceEvent, CLAUDE.md).
- Resultado: (#62) Hecho: se conserva el constructor viejo (
- [x] 1.3 Tipos sealed que crecen. Añadir
PolicyConfigyCallRejectedException(suspermitscrecen con cada política nueva) a la lista de "puede crecer" decompatibility.mdy a su Javadoc, como con los eventos: unswitchexhaustivo sobre ellos necesita una rama por defecto.- Resultado: (#62) Hecho (
compatibility.md, Javadoc de las dos clases).
- Resultado: (#62) Hecho (
- [x] 1.4 Un nombre para el evento de rechazo.
CircuitBreakerEvent.CallNotPermitted→Rejected, como en los eventos del bulkhead, el rate limiter y el adaptive limit. La excepción conserva su nombre. Alinear también el enum de la anotaciónLimitAlgorithmconAdaptiveLimitConfig.Algorithm(los otros enums,Backoff,Jitter,WindowType, coinciden con el de su config). Actualizar los módulos de métricas, la tabla de eventos deguides/observability.md(en/es) y los snippets deexamples/src/test/.../docs/.- Resultado: (#62) Hecho para el evento.
LimitAlgorithmse mantiene: ver diseño.
- Resultado: (#62) Hecho para el evento.
- [x] 1.5 Constantes que se copian.
HedgeConfig.MIN_ATTEMPTS/MAX_ATTEMPTSson constantes de compilación: se copian en los class files de quien las usa, así que subir el tope de 10 no llegaría al código compilado antes. Hacerlas privadas (los límites están documentados) o no constantes.- Resultado: (#62) Hecho: privadas.
- [x] 1.6 Renombres pequeños.
ResilienceRegistry.Builder.listen(String instance, …)→id.Resilient.create(type, registry, Object... args): los argumentos pueden ser null, así que@Nullable Object.... El formato del id derivado es<interface>#…enResilienceRegistryyResilienceEventpero<type>#…en los handles: usar<type>(clases anotadas).- Resultado: (#62) Hecho.
- [x] 1.7 Decidir, no necesariamente cambiar. Escribir cada decisión en
design.md:@Timeoutno tienename(las otras seis sí). Añadirlo después es aditivo.- Los parámetros de lista de los builders (
retryOn(List<Class<? extends Throwable>>)) rechazanList<Class<? extends Exception>>;Collection<? extends Class<? extends Throwable>>lo aceptaría, pero solo antes de 1.0. - Orden de argumentos:
Resilient.of(type, delegate, registry),create(type, registry, args…),Pipeline.builder(registry, id). ResilienceHealthIndicatoryResilienceEndpointde Micronaut tienen constructores públicos: marcarlos@Internal.- Los setters de ajuste de
AdaptiveLimitConfig.Buildertienen un Javadoc de una línea sin descripción principal: escribirla. - Los snapshots tienen formas distintas: los de retry, timeout y hedge incluyen
config; los de bulkhead, rate limiter y adaptive limit copian algunos campos (maxConcurrent,permits,minLimit/maxLimit); el del circuit breaker no tiene ninguno de los dos. Elegir una forma, o escribir por qué difieren. CircuitBreakerSnapshotusa-1para "todavía no hay tasa": mantener el centinela (documentado) o prever un accesorhasRates().Pipeline.Builder.retry(String)junto aretry(Consumer)(y los mismos pares en cada política):retry(null)es ambiguo. Aceptarlo o renombrar uno del par.LockedOverride.policyes unStringcon un conjunto cerrado de valores: un enum sería más seguro; el string coincide con las claves de configuración.- Dos enums llamados
ExhaustedResult(Pipeline.ExhaustedResulty el de la anotación, que además tieneINHERIT): importar los dos es incómodo. Aceptarlo o renombrar. - Resultado: (#62) Hecho: cada punto decidido en diseño; los constructores de Micronaut son
@Internaly los 51 setters de los builders tienen descripción principal.
Comprobación de toda la fase: ./gradlew build :stress:compileJava, ./gradlew :quarkus:it:check, ./gradlew :core:apiCompatibility (el informe lista los cambios buscados y nada más).
Fase 2: documentación
Un pull request, en y es juntos. Casi todo son cifras viejas o frases que contradicen el código.
- [x] 2.1 Cifras de rendimiento viejas.
- Las portadas (
docs/{en,es}/README.md) rotulan su gráfico "resilience4cata 0.1.0" y muestran el bulkhead en 20.5 ns; elREADME.mdraíz también. Actual: 17.3 ns (benchmarks.md#release-counters). performance.md: la fila del bulkhead en el titular (20.5 / 264 ns, "empate") es anterior a 17.3 / 195.5 ns; "acquire/release … 23.2 → 9.3 ns" contradice la devolución congetAndAddvolátil; la tabla principal se midió con 0.1.0.faq.md"How fast is it?" pone 1.9 ns (4 hilos) junto a los 89.2 ns de Resilience4j (1 hilo). Usar una sola fila.- El
README.mdraíz cita el breaker con cuatro hilos (832 ns para Resilience4j) donde la FAQ usa un hilo (89 ns): el README, las portadas, la FAQ yperformance.mddeben citar las mismas filas. - Decidir si se vuelve a correr la tabla del titular en AWS con el código de 1.0 (
infra/bench.sh), para que cada cifra citada tenga una sola fuente. - Resultado: (#63) Hecho: las filas del bulkhead vienen de contadores al devolver (17.3 / 195.5 ns) en
performance.md, las portadas (también la leyenda y la fecha) y el README, que ahora cita además el breaker con un thread. La frase de la FAQ era correcta: el breaker cuesta 1.9 ns con uno y con cuatro threads. Volver a correr la tabla del titular con el código de 1.0 queda para la corrida en AWS de 2.5.
- Las portadas (
- [x] 2.2 Titulares honestos.
- Los 1.9 ns del breaker con cuatro hilos solo valen mientras la ventana no tiene fallos (el camino
quiet). Desde alrededor de 1 % de fallos cada llamada escribe el cursor compartido: "mixed", 274 frente a 910 ns. Decirlo junto al titular. - Decir que Resilience4j actualiza sus métricas internas en cada llamada y resilience4cata no mantiene contadores por llamada, lo que explica parte de la diferencia.
- La fila del timeout (22 frente a 8842 ns) compara un watchdog con un traspaso a un executor: sacarla del titular en negrita o matizarla.
- "One clock-read pair per call, whatever the number of policies" es falso para pilas (el rate limiter, el adaptive limit, un breaker temporizado y un retry con
maxDurationleen el reloj cada uno). Corregirlo. - Los benchmarks miden el tiempo medio en un bucle cerrado, lo que oculta las colas: decirlo donde se citan el rate limiter y el bulkhead con contención (los que pierden se aparcan), hasta que existan las corridas de percentiles.
- x86 nunca se ha medido: decirlo en
performance.md("x86-64 pending" ya está; dejarlo visible junto al titular). - La ventana por conteo del circuit breaker es aproximada con contención (una lectura opaca vieja de
quietpuede no registrar un éxito justo después de un fallo; saltarse el slot con el mismo resultado puede reordenar escrituras cuando la ventana da la vuelta). Escribirlo enpolicies/circuit-breaker.mdyarchitecture.md. - Resultado: (#63) Hecho: el camino quiet del breaker, los contadores por llamada del retry de Resilience4j (comprobado en su jar 2.4.0: campos
LongAdderenRetryImpl; no se afirma para su breaker), medias en bucle cerrado, solo ARM, la fila de lecturas del reloj y la ventana por conteo aproximada (las dos carreras comprobadas en el código). La fila del timeout ya estaba matizada.
- Los 1.9 ns del breaker con cuatro hilos solo valen mientras la ventana no tiene fallos (el camino
- [ ] 2.5 Benchmarks detrás de los titulares honestos. Añadir un caso del breaker con una tasa de fallos realista (0.1 % y 1 %) junto a "success" y "mixed", con uno y cuatro hilos, con Resilience4j a la misma tasa. Correrlo en AWS y citarlo en 2.2.
- [x] 2.3 Contradicciones.
start/how-it-works.md, "Policy order": "The order is not configurable yet" (falso desde@PolicyOrder).- Las tiras de datos de las páginas de políticas dicen "x of 7"; hay 8 políticas.
README.mdraíz "names can still change before 1.0" frente a la FAQ "no more API changes planned". Quedarse con la de la FAQ.policies/retry.md"Small print" cita R4C-ATR-006 paraenabled: está retirado.- La tabla de prefijos de
errors.mdno tiene filaR4C-ORD. guides/troubleshooting.md[R4C-RT-004]: reescribir con la regla actual (core puede ser más nuevo que el procesador; un core más viejo es RT-021).- Tabla de artefactos de
start/installation.md: a la fila de annotations le faltan@Hedge,@AdaptiveLimit,@PolicyOrder. - El código generado de ejemplo de
policies/circuit-breaker.mdmuestra50.0f; el código generado emite50.0. Regenerarlo desdeexamples/build/generated/.... - Las etiquetas de la portada dicen "Sync and CompletionStage": añadir
Mono/Flux. - El roadmap de la FAQ dice "YAML configuration: v1" aunque Spring, Micronaut y Quarkus ya leen YAML: aclarar qué significa (archivos YAML en Java puro) o quitarlo.
- Resultado: (#63) Hecho. "x de 7" era correcto (Retry y Hedge comparten una capa): ahora dice "capa x de 7", y los tipos de retorno incluyen
MonoyFlux.
- [x] 2.4 El camino de alguien nuevo.
- Paso 6 del quickstart, "See the breaker open": bajar
minimumCalls, mostrarOPEN, laCallNotPermittedExceptiony la respuesta del fallback (snippet probado enexamples). - Una guía "Retry and circuit breaker for an HTTP call": qué excepciones reintentar o abortar, un
@Timeoutdentro, unnamepara el breaker, números sensatos, idempotencia. Página nueva en los dos sidebars. - Un glosario corto, como definiciones en
policies/README.mdo en su propia página: jitter y "full jitter", half-open y sondas, id de instancia e id derivado, registry, bulkhead, hedge, excepción sin stack trace, código en línea recta, y la comprobación de ABI (Abi.requireen los ejemplos generados). - Páginas de políticas: mover el bloque de código generado debajo de "How it behaves" (o plegarlo).
- R4C-RT-003: el mensaje,
errors.mdy el "Verify that it works" de installation muestran solo Gradle; añadir Maven. guides/spring.md: la pestaña de Maven añadespring-boot-configuration-processorsin versión ni motivo; explicarlo o quitarlo. Mover el detalle para mantenedores (versiones probadas en 4.1.0 y 4.1.2, internos de AOT, la comprobación de transacciones que lee el bytecode) a una sección final "Under the hood". La sección de transacciones (R4C-SPR-004) va antes de "Configuration": poner "Configuration" primero.- Sidebar: rotular el grupo "Proyecto" como para contribuidores.
errors.mdtiene unas 3200 líneas en una página: añadir una pista arriba (buscar por el código, la tabla de prefijos enlaza cada área), o dividirla por área si el generador del sitio yErrorCodeTestlo permiten.- Maven:
annotationProcessorPathssustituye el descubrimiento de todos los demás procesadores (Lombok, MapStruct), así que añadir el nuestro puede quitar los suyos sin aviso. Comprobar questart/installation.mdlo dice claramente junto al snippet de Maven, con el arreglo (listarlos todos). - Valores por defecto que sorprenden a quien viene de Resilience4j: una lista corta en
migrate/from-resilience4j.md(enlazada desde la FAQ). Candidatos que nombró el revisor: un retry espera de forma exponencial con jitter desde 200 ms por defecto; el rate limiter rechaza al instante (maxWait = 0); con clases anotadas@Transactionalqueda fuera de las políticas, y la comprobación de arranque lo rechaza, así que hay que reestructurar el código. También la afirmación del revisor de que unErrorcuenta como éxito para el breaker: comprobarla primero enCircuitBreakerPolicyy en las capas generadas; si es cierta, documentarla (y decidir en la fase 1 si es el comportamiento correcto); si no, no hay nada que hacer. - Nombres de métricas: quien migra desde Resilience4j tiene que rehacer dashboards y alertas (
r4c.*). Añadir a la página de migración una tabla de los nombres de métricas de Resilience4j a los nuestros. - Lo que Resilience4j tiene y nosotros no, en un solo sitio de la página de migración (parte ya está en "when to stay"): bulkhead con thread pool, cache, RxJava, Kotlin, reconfiguración en caliente (por decisión), una transición automática de open a half-open, endpoints de eventos, métricas de éxito y latencia, y que los hedges no llevan el contexto de quien llama a los intentos lanzados. Comprobar cada uno contra el código antes de escribirlo.
- Resultado: (#63) Hecho, con tres cosas que se mantienen a propósito: los bloques de código generado siguen junto a los snippets (la vista lado a lado es la razón del componente
compare, y su panel ya tiene scroll); los detalles de la guía de Spring siguen donde están (el orden de los post-processors, AOT y el límite en imágenes nativas son para usuarios; solo se simplificó la línea de versiones); el párrafo de transacciones sigue en "Clases anotadas", donde aplica. La afirmación delErrores cierta (recordOnvaleExceptionpor defecto), ya estaba documentada, y ahora se enlaza desde la FAQ.
- Paso 6 del quickstart, "See the breaker open": bajar
Comprobación: ./gradlew :examples:test --tests '*DocsTest*' :site:test, y ./gradlew :site:serve para mirar las páginas cambiadas. 2.5 es un cambio de benchmark (su propio pull request, con una corrida en AWS).
Fase 3: bugs sospechados
Un pull request por bug confirmado. Para cada uno: primero escribir un test que lo reproduzca (seam determinista o jcstress, nunca tiempos), verlo fallar, luego arreglar, luego hacer el mutation check del test. Si no se reproduce, escribir aquí por qué y cerrar la tarea.
- [ ] 3.1 Bulkhead: un slot libre con un waiter async en cola.
BulkheadPolicy.exit(cerca de la línea 323): cuandohasQueuedThreads()yyieldToSync(), libera y vuelve sin despachar a los waiters async. Si el hilo síncrono al que cedió luego vence, es interrumpido o pierde contra uno que se cuela, el slot queda libre mientras un waiter async espera, hasta la siguiente llamada; sin tráfico ese waiter se rechaza al llegar amaxWait. Test conenqueueForTest/drainForTesty el semáforo. Arreglo posible: después de ceder, despachar si el semáforo sigue teniendo un permiso y ningún hilo en cola lo tomó. Hoy jcstress no cubre el traspaso al ceder a un hilo síncrono: añadir un caso aBulkheadWaitersStress(o una clase nueva) y hacerle el mutation check. - [ ] 3.2 Adaptive limit: muestras inconsistentes.
AdaptiveLimitPolicy(cerca de la línea 281):SUMyCOUNTde una franja son escrituras con pérdida separadas, así que una carrera puede conservar una y perder la otra; una franja que retrocede (un hilo expulsado entre la lectura y la escritura) puede dejarsum - usedSumnegativo, que se recorta a un RTT de 1 ns y empuja Gradient2 hacia arriba. Arreglo mínimo: un delta negativo o cero salta la ventana en vez de volverse 1 ns; mejor, escribir suma y conteo de forma consistente (una escritura empaquetada, o un número de secuencia). Test con el constructor(id, config, env, stripes)y los seams; añadir la carrera de franjas aAdaptiveLimitStress, que hoy no la cubre. - [ ] 3.3 Rate limiter: un
nowviejo después de la espera.RateLimiterPolicy(cerca de la línea 134):nowse lee una vez antes del bucle; tras perder el CAS y la espera de 1 ns (decenas de µs en Linux), la espera se calcula desde un tiempo viejo: quien llama duerme de más, o conmaxWait = 0puede ser rechazado cuando ya le tocaba un permiso. Comprobar si releer el reloj tras la espera cambia el benchmark (medir con uno y cuatro hilos). La misma espera da una cola larga a quienes pierden (unos 50 µs de holgura del timer en Linux) que los benchmarks de tiempo medio no muestran: medirla con las corridas de percentiles (después de 1.0, o aquí si 3.3 cambia el bucle). - [ ] 3.4 Timeout: se borra la interrupción del propio usuario.
TimeoutPolicy(cerca de la línea 218): después de que el watchdog interrumpe,Thread.interrupted()borra también una interrupción que quien llama ya había puesto. Decidir la semántica (¿conservar una interrupción previa, volviendo a poner la marca si estaba antes de la llamada?) y probarla conWatchdog(0). - [ ] 3.5 Presupuesto de retry: pérdida sin límite.
RetryBudget(cerca de la línea 125): un hilo expulsado entre su lectura opaca y su escritura puede borrar llamadas de otros hilos en su franja; las franjas se comparten cuando hay más hilos que 2 × CPUs. La dirección es segura (menos reintentos). Probablemente documentar, no arreglar: escribir el límite (o que no lo hay) en el Javadoc de la clase y enarchitecture.md. - [ ] 3.6 Timeout con hilos virtuales. La primera llamada temporizada de cada hilo nuevo registra un
ThreadScopesen una cola compartida y el watchdog recorre cada hilo registrado en cada tick. Con un hilo virtual por petición eso es una asignación por petición y un recorrido proporcional a los hilos vivos. Medirlo (un caso JMH que arranque un hilo virtual por llamada); luego decidir si arreglar o documentar.
Fase 4: release
- [ ] Decidir si hay release candidate:
1.0.0-RC1en Central, una o dos semanas de uso, luego 1.0.0. (Decisión del dueño.) - [ ] Seguir publicar una versión:
./gradlew :examples:check -Pr4c.generatedBy=0.13.0, subir la versión en los snippets de build (no en el blog), tag,release.yml, publicar a mano en el portal de Central. - [ ] Después del release:
r4c.apiBaseline= la versión publicada; sincronizar javadoc.io para cada módulo (los botones de sync y upload en la página de versiones de cada artefacto).
Después de 1.0
Aditivo, así que no bloquea congelar la API. Más o menos en orden de valor. Los puntos marcados (pedido antes de 1.0) son los que algún revisor quería antes del release: el dueño decide si adelantarlos.
- Correr la comparación con Resilience4j en x86 (
c7i) y publicarla junto a Graviton; en x86 cada atómico es una barrera completa ynanoTimees barato, así que las decisiones afinadas para ARM pueden quedar en otro orden. (pedido antes de 1.0) - jcstress en modo por defecto en un runner ARM (
ubuntu-24.04-armo Graviton) de forma periódica, y repetir allí los mutantes: hoy el workflowstresscorre el modo quick solo en x86, que es TSO, así que un orden debilitado no puede fallar allí. (pedido antes de 1.0) - Benchmarks de percentiles y de bucle abierto (
SampleTime) para el rate limiter y el bulkhead con contención. (pedido antes de 1.0) - Un fallo visible en Spring, Micronaut y Quarkus cuando hay clases anotadas sin clase generada: hoy los beans quedan sin protección y sin aviso, y solo un
/actuator/resilience4catavacío lo delata. (pedido antes de 1.0) - Métricas opcionales de éxito y latencia, medidas antes de añadirlas (escrituras compartidas por llamada), para tener paridad con los dashboards de Resilience4j. (pedido antes de 1.0)
- Soporte de Spring Boot 3, o una razón documentada y una ruta de migración. (pedido antes de 1.0)
- Salud del proyecto: un segundo mantenedor con permisos de release, una política de seguridad (
SECURITY.md) y una política de soporte. (pedido antes de 1.0) - Huecos aditivos de la API que listó su revisor:
Ticker.onAdvanceno tiene forma de darse de baja.- Los eventos solo llevan
nanoTime, no la hora de reloj. ConfigException.problems()devuelveList<String>, no problemas estructurados (código, clave, origen).- Solo los circuit breakers tienen un método de listado en el registry; retry, hedge y timeout no tienen handles.
- Funciones que Resilience4j tiene, cada una por decidir (construirla, o documentar por qué no): bulkhead con thread pool, cache, RxJava, soporte de Kotlin, una transición automática de open a half-open, endpoints de eventos, llevar el contexto de quien llama a los intentos de un hedge.
- Revisar las señales de adopción en unos seis meses, como haría el revisor que evaluó adoptarla: actividad, mantenedores, issues cerrados. Un piloto en un servicio Java puro con mucho tráfico es lo que recomendaría primero.