This commit is contained in:
2026-08-12 19:42:08 +08:00
parent db86e97c0d
commit 47f99befb7
21 changed files with 307 additions and 48 deletions
@@ -0,0 +1,67 @@
package com.baiyue.iam.notification;
import com.baiyue.iam.notification.mapper.NotificationTaskMapper;
import com.baiyue.iam.notification.service.NotificationScheduler;
import com.baiyue.iam.notification.service.NotificationService;
import org.apache.ibatis.annotations.Update;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
class NotificationSchedulerRecoveryTest {
@Test void recoverySqlRequiresTimeoutAndNoSuccessfulDeliveryEvidence() throws Exception {
Update retry = NotificationTaskMapper.class
.getMethod("retryTimedOutDispatchingWithoutDeliveryEvidence", java.time.LocalDateTime.class, java.time.LocalDateTime.class)
.getAnnotation(Update.class);
String retrySql = String.join(" ", retry.value()).replaceAll("\\s+", " ").toUpperCase();
assertThat(retrySql).contains("TASK.STATUS = 'DISPATCHING'", "TASK.UPDATED_AT <= #{STALEBEFORE}",
"TASK.SENT_AT IS NULL", "RECIPIENT.STATUS = 'DELIVERED'", "IAM_NOTIFICATION_INBOX")
.contains("SET TASK.STATUS = 'RETRYING'");
Update dead = NotificationTaskMapper.class
.getMethod("deadTimedOutDispatchingWithDeliveryEvidence", java.time.LocalDateTime.class, java.time.LocalDateTime.class)
.getAnnotation(Update.class);
String deadSql = String.join(" ", dead.value()).replaceAll("\\s+", " ").toUpperCase();
assertThat(deadSql).contains("SET TASK.STATUS = 'DEAD'", "TASK.STATUS = 'DISPATCHING'",
"TASK.UPDATED_AT <= #{STALEBEFORE}", "RECIPIENT.STATUS = 'DELIVERED'", "IAM_NOTIFICATION_INBOX",
"DISPATCH_TIMEOUT_WITH_DELIVERY_EVIDENCE");
}
@Test void schedulerRecoversBeforeClaimingScheduledAndRetryingTasks() {
NotificationService service = mock(NotificationService.class);
NotificationScheduler scheduler = new NotificationScheduler();
ReflectionTestUtils.setField(scheduler, "service", service);
ReflectionTestUtils.setField(scheduler, "dispatchTimeoutSeconds", 300L);
when(service.dueScheduledTaskIds()).thenReturn(List.of(11L));
when(service.retryingTaskIds()).thenReturn(List.of(12L));
scheduler.dispatch();
var ordered = inOrder(service);
ordered.verify(service).recoverTimedOutDispatching(300L);
ordered.verify(service).dueScheduledTaskIds();
ordered.verify(service).dispatchScheduled(11L);
ordered.verify(service).retryingTaskIds();
ordered.verify(service).dispatchRetrying(12L);
}
@Test void oneTaskFailureDoesNotBlockRemainingTasksOrRetryQueue() {
NotificationService service = mock(NotificationService.class);
NotificationScheduler scheduler = new NotificationScheduler();
ReflectionTestUtils.setField(scheduler, "service", service);
ReflectionTestUtils.setField(scheduler, "dispatchTimeoutSeconds", 300L);
when(service.dueScheduledTaskIds()).thenReturn(List.of(11L, 12L));
when(service.dispatchScheduled(11L)).thenThrow(new IllegalStateException("failed"));
when(service.retryingTaskIds()).thenReturn(List.of(13L));
scheduler.dispatch();
verify(service).dispatchScheduled(12L);
verify(service).dispatchRetrying(13L);
}
}