Micronaut 自定义 scope 关闭时不停重复销毁,检查 per-bean 模式的移除状态
Micronaut 自定义 scope 的 per-bean 模式可能反复关闭同一实例。Core 5.2.15 修正实际条目移除与关闭标记,临时退回 scope 共用锁须保留并发验收。
Micronaut 自定义 scope 开启 lockPerBean 后,关闭上下文不返回,线程持续落在 destroyScopeLockingPerBean,同时同一 bean 的销毁计数不断增加,这种情况需要检查“关闭后有没有从 scopeMap 移除”。它与某个 @PreDestroy 阻塞在网络调用中的情况不同:这里线程还在循环工作。
具体故障出现在一个 scope 按自己的 key 保存 CreatedBean,而这个 key 不等于 CreatedBean.id() 的组合。5.2.x 的受影响销毁逻辑会先关闭 bean,再按 bean 自身 id 查找并删除:
if (scopeMap.get(closedBean.id()) == closedBean) {
scopeMap.remove(closedBean.id());
}
closing.remove(new IdentityKey(closedBean));
如果 map 中实际使用另一个 key,删除没有发生,closing 标记却被清掉。下一轮仍能看到同一个对象,于是再关闭一次;循环一直等“这一轮没有关闭任何 bean”才结束,条件始终达不到。给 map.values() 做快照也没用,因为这里本来就在快照上遍历,缺少的是销毁进度和标记状态。
Core 5.2.15 已包含两处处理:按 entry 的 value 对象身份寻找并移除实际条目;移除后若 map 仍持有该实例,保留它的 closing 标记,避免下一轮重复关闭。比较对象使用身份而不是 equals(),不能顺手改成按相等值删除,否则可能删掉另一个后来创建的 bean。
通过项目已有版本管理入口更新 micronaut-inject 及 Core 相关组件到包含修补的版本,再重新跑 scope 生命周期测试。检查依赖时要区分运行时与注解处理器,避免测试使用旧运行 jar。
短期无法升级时,自定义 scope 可以退回整个 scope 共用锁的模式。构造器中的第二个参数要明确改为 false,例如在自己的 AbstractConcurrentCustomScope<MyScope> 子类中:
MyCustomScope() {
super(MyScope.class, false);
}
这会改变创建与销毁期间的并发方式,不能只替换开关就跳过原有并发测试;已有代码若使用单参数构造器,也应确认它所选择的模式,不再额外开启 per-bean 锁。
验收用原来能触发问题的 scope:把一个 CreatedBean 放在不同于其 id 的 key 下,在线程中调用 stop(),等待一个有限超时,并同时检查三件事:销毁线程已经结束、该实例只关闭一次、map 不再保存它。再覆盖销毁期间重新创建 bean 的情况,确认新实例不会被当成已经关闭的旧实例删掉。只看到进程退出或 CPU 降下来,会漏掉重复销毁和误删新对象。
这些判断针对 per-bean 锁的移除状态。若堆栈停在业务关闭方法、互斥锁等待或数据库连接释放,不能据此认定是同一个问题。


