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
@@ -2,4 +2,48 @@ package com.baiyue.iam.notification.mapper;
import com.baiyue.iam.notification.entity.NotificationTask;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface NotificationTaskMapper extends BaseMapper<NotificationTask> { }
import org.apache.ibatis.annotations.Update;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
@Mapper public interface NotificationTaskMapper extends BaseMapper<NotificationTask> {
@Update("""
UPDATE iam_notification_task task
SET task.status = 'DEAD',
task.last_error = 'DISPATCH_TIMEOUT_WITH_DELIVERY_EVIDENCE',
task.updated_at = #{now}
WHERE task.status = 'DISPATCHING'
AND task.updated_at <= #{staleBefore}
AND task.sent_at IS NULL
AND (
EXISTS (
SELECT 1 FROM iam_notification_recipient recipient
WHERE recipient.task_id = task.id AND recipient.status = 'DELIVERED'
)
OR EXISTS (
SELECT 1 FROM iam_notification_inbox inbox WHERE inbox.task_id = task.id
)
)
""")
int deadTimedOutDispatchingWithDeliveryEvidence(@Param("staleBefore") LocalDateTime staleBefore,
@Param("now") LocalDateTime now);
@Update("""
UPDATE iam_notification_task task
SET task.status = 'RETRYING',
task.attempt_count = task.attempt_count + 1,
task.last_error = 'DISPATCH_TIMEOUT_RECOVERED',
task.updated_at = #{now}
WHERE task.status = 'DISPATCHING'
AND task.updated_at <= #{staleBefore}
AND task.sent_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM iam_notification_recipient recipient
WHERE recipient.task_id = task.id AND recipient.status = 'DELIVERED'
)
AND NOT EXISTS (
SELECT 1 FROM iam_notification_inbox inbox WHERE inbox.task_id = task.id
)
""")
int retryTimedOutDispatchingWithoutDeliveryEvidence(@Param("staleBefore") LocalDateTime staleBefore,
@Param("now") LocalDateTime now);
}
@@ -54,6 +54,15 @@ public class NotificationRepository {
public boolean claimScheduled(Long taskId) { return tasks.update(null,new LambdaUpdateWrapper<NotificationTask>()
.eq(NotificationTask::getId,taskId).eq(NotificationTask::getStatus,"SCHEDULED")
.set(NotificationTask::getStatus,"DISPATCHING").set(NotificationTask::getUpdatedAt,java.time.LocalDateTime.now()))==1; }
public int recoverTimedOutDispatching(java.time.LocalDateTime staleBefore,java.time.LocalDateTime now) {
int dead = tasks.deadTimedOutDispatchingWithDeliveryEvidence(staleBefore,now);
return dead + tasks.retryTimedOutDispatchingWithoutDeliveryEvidence(staleBefore,now);
}
public List<NotificationTask> retryingTasks() { return tasks.selectList(new LambdaQueryWrapper<NotificationTask>()
.eq(NotificationTask::getStatus,"RETRYING").orderByAsc(NotificationTask::getUpdatedAt).last("LIMIT 100")); }
public boolean claimRetrying(Long taskId) { return tasks.update(null,new LambdaUpdateWrapper<NotificationTask>()
.eq(NotificationTask::getId,taskId).eq(NotificationTask::getStatus,"RETRYING")
.set(NotificationTask::getStatus,"DISPATCHING").set(NotificationTask::getUpdatedAt,java.time.LocalDateTime.now()))==1; }
public void insertInbox(NotificationInbox value) { inbox.insert(value); }
public PageResult<NotificationInbox> pageInbox(long tenantId,long membershipId,String app,long page,long size,String status,String category) {
return PageResult.from(inbox.selectPage(new Page<>(page,size), new LambdaQueryWrapper<NotificationInbox>()
@@ -1,10 +1,29 @@
package com.baiyue.iam.notification.service;
import jakarta.annotation.Resource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.function.Function;
@Component @ConditionalOnProperty(name="baiyue.notification.scheduler-enabled",havingValue="true")
public class NotificationScheduler {
private static final Logger log = LoggerFactory.getLogger(NotificationScheduler.class);
@Resource private NotificationService service;
@Scheduled(fixedDelayString="${baiyue.notification.scheduler-delay-ms:5000}") public void dispatch(){service.dispatchDue();}
@Value("${baiyue.notification.dispatch-timeout-seconds:300}") private long dispatchTimeoutSeconds;
@Scheduled(fixedDelayString="${baiyue.notification.scheduler-delay-ms:5000}") public void dispatch(){
service.recoverTimedOutDispatching(dispatchTimeoutSeconds);
dispatchTasks("scheduled", service.dueScheduledTaskIds(), service::dispatchScheduled);
dispatchTasks("retrying", service.retryingTaskIds(), service::dispatchRetrying);
}
private void dispatchTasks(String source, List<Long> taskIds, Function<Long, Boolean> dispatcher) {
for (Long taskId : taskIds) {
try { dispatcher.apply(taskId); }
catch (RuntimeException failure) {
log.error("Notification {} task dispatch failed; taskId={}", source, taskId, failure);
}
}
}
}
@@ -180,11 +180,38 @@ public class NotificationService {
}
@Transactional
public void dispatchDue() {
for (NotificationTask task : repository.dueTasks()) {
if (!repository.claimScheduled(task.getId())) continue;
task.setStatus("DISPATCHING"); deliver(task, LocalDateTime.now());
}
public int recoverTimedOutDispatching(long timeoutSeconds) {
if (timeoutSeconds < 1) throw new IllegalArgumentException("Dispatch timeout must be positive");
LocalDateTime now = eventNow();
return repository.recoverTimedOutDispatching(now.minusSeconds(timeoutSeconds), now);
}
@Transactional(readOnly = true)
public List<Long> dueScheduledTaskIds() {
return repository.dueTasks().stream().map(NotificationTask::getId).toList();
}
@Transactional(readOnly = true)
public List<Long> retryingTaskIds() {
return repository.retryingTasks().stream().map(NotificationTask::getId).toList();
}
@Transactional
public boolean dispatchScheduled(Long taskId) {
if (!repository.claimScheduled(taskId)) return false;
NotificationTask task = requiredTask(taskId);
task.setStatus("DISPATCHING");
deliver(task, LocalDateTime.now());
return true;
}
@Transactional
public boolean dispatchRetrying(Long taskId) {
if (!repository.claimRetrying(taskId)) return false;
NotificationTask task = requiredTask(taskId);
task.setStatus("DISPATCHING");
deliver(task, LocalDateTime.now());
return true;
}
@Transactional(readOnly = true)
+1
View File
@@ -25,6 +25,7 @@ spring:
baiyue:
notification:
scheduler-enabled: ${NOTIFICATION_SCHEDULER_ENABLED:false}
dispatch-timeout-seconds: ${NOTIFICATION_DISPATCH_TIMEOUT_SECONDS:300}
mq-consumer-enabled: ${NOTIFICATION_MQ_CONSUMER_ENABLED:false}
clients:
auth: