fix: 修复设备断开后通讯层会话缓存未及时清理的问题

之前设备断开连接时(channelInactive),虽然会关闭会话并通知应用层,
但通讯层的 Caffeine 缓存没有立即清除,导致:
- 下行指令仍能找到"幽灵会话",写入已关闭的 channel 后静默失败
- 应用层无法感知设备已离线,用户体验不佳

改动:
- ProtocolSession 新增 closeCallback 回调机制,close() 时自动通知注册中心
- ProtocolSession 新增 AtomicBoolean closed 状态,防止重复关闭
- DefaultProtocolSessionRegistryProvider.register() 时自动设置回调
- 新增单元测试验证回调机制

现在设备断开后,App 层下发指令时能立即感知到会话不存在,
并在日志中明确提示"充电桩会话不存在"。
This commit is contained in:
三丙
2026-01-29 20:38:48 +08:00
parent fa9524d302
commit b3270c21b0
3 changed files with 215 additions and 2 deletions

View File

@@ -0,0 +1,173 @@
/**
* 开源代码,仅供学习和交流研究使用,商用请联系三丙
* 微信mohan_88888
* 抖音:程序员三丙
* 付费课程知识星球https://t.zsxq.com/aKtXo
*/
package sanbing.jcpp.protocol.provider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sanbing.jcpp.proto.gen.DownlinkProto.DownlinkRequestMessage;
import sanbing.jcpp.proto.gen.UplinkProto.SessionCloseReason;
import sanbing.jcpp.protocol.domain.ProtocolSession;
import sanbing.jcpp.protocol.provider.impl.DefaultProtocolSessionRegistryProvider;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
/**
* 会话关闭回调单元测试
* 测试 ProtocolSession 关闭时自动清除 Caffeine 缓存功能
*
* @author 九筒
*/
class ProtocolSessionCloseCallbackTest {
private final Logger log = LoggerFactory.getLogger(this.getClass());
private DefaultProtocolSessionRegistryProvider sessionRegistryProvider;
@BeforeEach
void setUp() {
sessionRegistryProvider = new DefaultProtocolSessionRegistryProvider();
}
/**
* 测试:会话关闭后应自动从缓存中移除
*/
@Test
void testSessionRemovedFromCacheOnClose() {
log.info("开始测试:会话关闭后应自动从缓存中移除");
// 1. 创建并注册会话
TestProtocolSession session = new TestProtocolSession("testProtocol");
UUID sessionId = session.getId();
sessionRegistryProvider.register(session);
log.info("会话已注册: sessionId={}", sessionId);
// 2. 验证会话存在于缓存中
ProtocolSession sessionFromCache = sessionRegistryProvider.get(sessionId);
assertNotNull(sessionFromCache, "注册后会话应存在于缓存中");
assertEquals(sessionId, sessionFromCache.getId(), "会话ID应匹配");
log.info("验证会话存在于缓存中");
// 3. 关闭会话
session.close(SessionCloseReason.SESSION_CLOSE_ON_CHANNEL_INACTIVE);
log.info("会话已关闭");
// 4. 验证会话已从缓存中移除
ProtocolSession sessionAfterClose = sessionRegistryProvider.get(sessionId);
assertNull(sessionAfterClose, "关闭后会话应从缓存中移除");
log.info("验证会话已从缓存中移除");
// 5. 验证会话状态
assertTrue(session.isClosed(), "会话 closed 状态应为 true");
log.info("验证会话 closed 状态为 true");
log.info("测试通过:会话关闭后自动从缓存中移除");
}
/**
* 测试:重复关闭会话不应抛出异常,且回调只执行一次
*/
@Test
void testDuplicateCloseExecutesCallbackOnce() {
log.info("开始测试:重复关闭会话回调只执行一次");
// 使用计数器跟踪回调执行次数
AtomicInteger callbackCount = new AtomicInteger(0);
// 1. 创建会话并设置自定义回调
TestProtocolSession session = new TestProtocolSession("testProtocol");
session.setCloseCallback(id -> {
callbackCount.incrementAndGet();
log.info("回调执行,当前次数: {}", callbackCount.get());
});
// 2. 第一次关闭
session.close(SessionCloseReason.SESSION_CLOSE_ON_CHANNEL_INACTIVE);
assertEquals(1, callbackCount.get(), "第一次关闭后回调应执行1次");
assertTrue(session.isClosed(), "第一次关闭后应为 closed 状态");
// 3. 第二次关闭(应被忽略)
assertDoesNotThrow(() -> session.close(SessionCloseReason.SESSION_CLOSE_DESTRUCTION));
assertEquals(1, callbackCount.get(), "重复关闭后回调仍应只执行1次");
assertTrue(session.isClosed(), "重复关闭后仍应为 closed 状态");
// 4. 第三次关闭(应被忽略)
assertDoesNotThrow(() -> session.close());
assertEquals(1, callbackCount.get(), "再次重复关闭后回调仍应只执行1次");
log.info("测试通过:重复关闭只触发一次回调");
}
/**
* 测试register 时会自动设置 closeCallback
*/
@Test
void testRegisterSetsCloseCallback() {
log.info("开始测试register 时会自动设置 closeCallback");
// 1. 创建会话(此时 closeCallback 为 null
TestProtocolSession session = new TestProtocolSession("testProtocol");
assertNull(session.getCloseCallback(), "注册前 closeCallback 应为 null");
// 2. 注册会话
sessionRegistryProvider.register(session);
// 3. 验证 closeCallback 已设置
assertNotNull(session.getCloseCallback(), "注册后 closeCallback 应已设置");
log.info("验证 closeCallback 已设置");
// 4. 验证关闭时能正确清除缓存
UUID sessionId = session.getId();
assertNotNull(sessionRegistryProvider.get(sessionId), "关闭前会话应存在");
session.close();
assertNull(sessionRegistryProvider.get(sessionId), "关闭后会话应被移除");
log.info("验证关闭时正确清除缓存");
log.info("测试通过register 自动设置 closeCallback");
}
/**
* 测试isClosed 方法正确反映会话状态
*/
@Test
void testIsClosedReflectsSessionState() {
log.info("开始测试isClosed 方法正确反映会话状态");
TestProtocolSession session = new TestProtocolSession("testProtocol");
// 新建会话应未关闭
assertFalse(session.isClosed(), "新建会话应未关闭");
// 关闭后应为已关闭
session.close();
assertTrue(session.isClosed(), "关闭后应为已关闭");
log.info("测试通过isClosed 方法正确反映会话状态");
}
/**
* 测试用的 ProtocolSession 实现
*/
private static class TestProtocolSession extends ProtocolSession {
public TestProtocolSession(String protocolName) {
super(protocolName);
}
@Override
public void onDownlink(DownlinkRequestMessage downlinkMsg) {
// 测试用空实现
}
}
}