Resilience4j 结果重试返回负间隔,却报 retry wait interval 异常
Resilience4j 2.4.0 同步 retryOnResult 分支会把负间隔继续交给 sleep,可能抛出内部 IllegalStateException。把是否重试、最多尝试次数和非负等待分开配置,不靠负数取消结果重试。
Resilience4j 的重试配置已经让 intervalFunction 返回 -1L,本来想立即停止重试、把结果交回调用方,却得到:
java.lang.IllegalStateException:
Unexpected exception during retry wait interval
在 2.4.0 中,这种表现需要检查是否走了同步 retryOnResult 路径。不能因为某条异常重试或 CompletionStage 调用能按预期停止,就认为所有装饰器对负间隔都采用同一规则。
内部等待异常可能盖住原结果
同步 ContextImpl.onResult 先判断结果谓词、增加尝试次数,再检查是否已到 maxAttempts。未达到上限时,它会调用 consumeResultBeforeRetryAttempt,随后进入等待函数。
这个等待函数拿到负间隔后,仍把它交给 sleepFunction。默认等待最终不能接受负数;而结果路径没有先前业务运行时异常可供重新抛出,便生成上面的 IllegalStateException。
因此,不能仅看最外层异常就判定被包装的业务函数自己抛了 IllegalStateException。需要保留重试配置、完整异常链和最后一份业务结果,并分清异常来自业务调用还是重试等待。
还有一个容易漏掉的顺序:结果消费回调早于间隔计算。若它已经释放响应体或关闭资源,再试图把“最后结果”返回上层,可能得到一个已被消费的对象。更改等待值不会自动恢复其内容。
用结果谓词决定资格,用次数控制预算
在这条正式实现上,不要用负间隔承担停止结果重试的职责。结果不再适合重试时,让 retryOnResult 明确返回 false;有限预算用 maxAttempts,等待函数只返回合法的非负值。
下面是仅重试指定临时状态的配置片段,类型来自 io.github.resilience4j.retry,用于现有同步 Supplier 调用:
RetryConfig config = RetryConfig.<String>custom()
.retryOnResult("TEMPORARY"::equals)
.maxAttempts(3)
.intervalFunction(attempt -> 100L)
.build();
Retry retry = Retry.of("status-check", config);
100 是示例毫秒数,3 包含首次调用,最多总共尝试三次。这里不使用 failAfterMaxAttempts(true);耗尽后如何处理最后结果,要与应用原来的配置一致。该片段也不是“任意动态负间隔算法”的自动等价替换:如果停止规则同时依赖尝试历史、结果或外部状态,需要重新明确规则所属的执行范围,而不是把共享计数器塞进全局谓词。
原意就是完全不重试时,直接绕过这层重试或采用一次尝试的配置,比继续传入一个永远返回负数的函数更清楚。不要把负数改为零后就结束检查;零通常表示立即再次尝试,仍可能重复业务操作。
结果消费与异步适配要另核对
如果保留 consumeResultBeforeRetryAttempt,它只能处理确实将被下一次调用替代的结果;还应确认最后结果仍可由调用方使用。带资源的响应对象,需要按其真实生命周期安排关闭责任,不能同时让回调和调用方各自假定对方拥有它。
截至 2026 年 10 月 25 日,当前正式 2.4.0 的同步分支仍有上述处理。异步 Retry context 与 Reactor、RxJava 等适配器还各有停止值判断,不能把本篇同步改动推广为任意负值都能安全停止。
核验应覆盖可重试结果、不可重试结果、预算耗尽、业务异常与线程中断,记录实际调用次数及结果是否仍完整。对于提交、扣款或消息发送,重试资格还必须符合幂等和结果确认契约;等待函数报错不证明前一次操作没有发生。


