feat: auth_sessions 死亡行定时清理任务(报告 16 遗留 §9.3)
CI / backend-test (push) Failing after 8m10s

- SessionCleanupJob(@Scheduled,间隔/初始延迟/保留期均配置项,默认 6h/10m/30d)删除撤销或过期超过保留期的会话行;保留期即重用检测窗口(已撤销行是重用比对对象),注释与 sample 已说明
- 多实例并发安全(幂等 DELETE);轮换链 FK 由 ON DELETE SET NULL 释放
- 门禁:./mvnw clean test → BUILD SUCCESS,75 测试 0 失败(新增 SessionCleanupIntegrationTest:仅删超期死亡行,存活与近期撤销行保留)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 15:45:38 +08:00
parent 3f6e8187cf
commit 6528a06c28
6 changed files with 163 additions and 0 deletions
@@ -0,0 +1,79 @@
package com.patbond.patbond.user.session;
import com.patbond.patbond.common.session.CreateSessionRequest;
import com.patbond.patbond.common.session.SessionTokens;
import com.patbond.patbond.common.user.CreateUserRequest;
import com.patbond.patbond.user.TestcontainersConfiguration;
import com.patbond.patbond.user.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.simple.JdbcClient;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
/**
* The cleanup job removes rows dead longer than the retention window and
* nothing else: live sessions and recently revoked rows (still needed for
* reuse detection) survive.
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
class SessionCleanupIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private SessionService sessionService;
@Autowired
private SessionCleanupJob cleanupJob;
@Autowired
private JdbcClient jdbcClient;
@Test
void removesOnlySessionsDeadLongerThanRetention() {
UUID userId = userService.createUser(
new CreateUserRequest("cleanup_user", "secret123", null, null)).getId();
SessionTokens live = createSession(userId);
SessionTokens recentlyRevoked = createSession(userId);
SessionTokens longRevoked = createSession(userId);
SessionTokens longExpired = createSession(userId);
sessionService.revoke(userId, recentlyRevoked.getRefreshToken());
sessionService.revoke(userId, longRevoked.getRefreshToken());
// Backdate beyond the 30-day retention (created_at moves along so the
// ck_sessions_expiry / ck_sessions_revoked_at ordering checks hold).
backdate("created_at = now() - interval '40 days', revoked_at = now() - interval '31 days'",
longRevoked.getSessionId());
backdate("created_at = now() - interval '61 days', expires_at = now() - interval '31 days'",
longExpired.getSessionId());
cleanupJob.cleanUp();
List<UUID> remaining = jdbcClient.sql(
"SELECT id FROM identity.auth_sessions WHERE user_id = :userId")
.param("userId", userId)
.query(UUID.class)
.list();
assertThat(remaining)
.containsExactlyInAnyOrder(live.getSessionId(), recentlyRevoked.getSessionId());
}
private SessionTokens createSession(UUID userId) {
return sessionService.create(new CreateSessionRequest(userId, null, null, null));
}
private void backdate(String setClause, UUID sessionId) {
jdbcClient.sql("UPDATE identity.auth_sessions SET " + setClause + " WHERE id = :id")
.param("id", sessionId)
.update();
}
}