feat(retry): 批次3 限流类自动重试守卫豁免(让无限重试在生产真正生效)
接 16974d0(I3)。Task8 核实:限流类自动重发途经 MsgPushGuardChain 的 Quota/RateLimitChecker,per-receiver 日限远小于 ~360 次/12h,会被守卫置 rate_limited/quota_exceeded 终态 → 队列记录被删 → 无限重试静默失效。 批次1/2/I3 修对了重试编排,但不豁免守卫则功能在生产仍发不出去。 方案 A+B(用户选:完全豁免 AUTO_RETRY): - A:checkAll 加 RetrySourceEnum 参,AUTO_RETRY 跳过 quota+ratelimit (blacklist 仍执行);processSendMsg 透传。重发节奏由 120s 间隔 + 12h 兜底天然封顶,无需配额墙再约束。 - B:applySendResult 加 retrySource 参,仅首发(null)累加配额/频率计数; 重发(AUTO/MANUAL)是同一条消息再投,不重复扣额。 - MANUAL_RETRY 仍受守卫检查(只豁免 AUTO)。 checkAll 全仓仅 1 调用点;applySendResult 为 impl 内部方法、2 处 this 调用, 接口签名不变。Spring 编排+Redis 副作用无 H2 脚手架,靠编译+diff+人工冒烟 验证(spec §8):RetryDecisionLogicTest 仍 6/6 绿,全模块编译通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import com.njcn.msgpush.module.push.checker.impl.BlacklistChecker;
|
|||||||
import com.njcn.msgpush.module.push.checker.impl.QuotaChecker;
|
import com.njcn.msgpush.module.push.checker.impl.QuotaChecker;
|
||||||
import com.njcn.msgpush.module.push.checker.impl.RateLimitChecker;
|
import com.njcn.msgpush.module.push.checker.impl.RateLimitChecker;
|
||||||
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
|
import com.njcn.msgpush.module.push.dal.dataobject.message.MessageRecordDO;
|
||||||
|
import com.njcn.msgpush.module.push.enums.RetrySourceEnum;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -24,9 +25,15 @@ public class MsgPushGuardChain {
|
|||||||
private RateLimitChecker rateLimitChecker;
|
private RateLimitChecker rateLimitChecker;
|
||||||
|
|
||||||
|
|
||||||
public void checkAll(List<MessageRecordDO> messageRecordList) {
|
public void checkAll(List<MessageRecordDO> messageRecordList, RetrySourceEnum retrySource) {
|
||||||
|
// 黑名单:首发与重发(含限流自动重试)都必须执行
|
||||||
blacklistChecker.check(messageRecordList);
|
blacklistChecker.check(messageRecordList);
|
||||||
|
// 配额/接收者频率:系统自动重试(限流类无限重试)完全豁免——否则会被守卫置终态、队列记录被删,
|
||||||
|
// 无限重试静默失效。重发节奏由 120s 固定间隔 + 12h 兜底天然封顶,无需配额墙再约束。
|
||||||
|
// 首发(retrySource==null)与人工重试(MANUAL_RETRY)仍正常受这两道守卫。
|
||||||
|
if (retrySource != RetrySourceEnum.AUTO_RETRY) {
|
||||||
quotaChecker.check(messageRecordList);
|
quotaChecker.check(messageRecordList);
|
||||||
rateLimitChecker.check(messageRecordList);
|
rateLimitChecker.check(messageRecordList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class MessageRecordServiceImpl extends ServiceImpl<MessageRecordMapper, M
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<MessageSendResultVO> processSendMsg(List<MessageRecordDO> messageRecordDOList, RetrySourceEnum retrySource) {
|
public List<MessageSendResultVO> processSendMsg(List<MessageRecordDO> messageRecordDOList, RetrySourceEnum retrySource) {
|
||||||
msgPushGuardChain.checkAll(messageRecordDOList);
|
msgPushGuardChain.checkAll(messageRecordDOList, retrySource);
|
||||||
List<MessageSendResultVO> resultList = new ArrayList<>();
|
List<MessageSendResultVO> resultList = new ArrayList<>();
|
||||||
for (MessageRecordDO messageRecordDO : messageRecordDOList) {
|
for (MessageRecordDO messageRecordDO : messageRecordDOList) {
|
||||||
try {
|
try {
|
||||||
@@ -131,11 +131,11 @@ public class MessageRecordServiceImpl extends ServiceImpl<MessageRecordMapper, M
|
|||||||
if (sendResult == null) {
|
if (sendResult == null) {
|
||||||
sendResult = this.sendMessage(messageRecordDO, channelProviderConfigDO, messageProviderFactory);
|
sendResult = this.sendMessage(messageRecordDO, channelProviderConfigDO, messageProviderFactory);
|
||||||
}
|
}
|
||||||
this.applySendResult(messageRecordDO, sendResult);
|
this.applySendResult(messageRecordDO, sendResult, retrySource);
|
||||||
this.recordRetryHistory(messageRecordDO, retrySource);
|
this.recordRetryHistory(messageRecordDO, retrySource);
|
||||||
} else {
|
} else {
|
||||||
sendResult = this.validateProviderAndChannel(messageRecordDO, null, null);
|
sendResult = this.validateProviderAndChannel(messageRecordDO, null, null);
|
||||||
this.applySendResult(messageRecordDO, sendResult);
|
this.applySendResult(messageRecordDO, sendResult, retrySource);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ public class MessageRecordServiceImpl extends ServiceImpl<MessageRecordMapper, M
|
|||||||
* 主流程在这里统一做状态落库、重试编排和健康度更新。
|
* 主流程在这里统一做状态落库、重试编排和健康度更新。
|
||||||
*/
|
*/
|
||||||
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
||||||
public void applySendResult(MessageRecordDO messageRecordDO, SendResult sendResult) {
|
public void applySendResult(MessageRecordDO messageRecordDO, SendResult sendResult, RetrySourceEnum retrySource) {
|
||||||
messageRecordDO.setStatus(this.mapOutcomeToStatus(sendResult.getOutcome()));
|
messageRecordDO.setStatus(this.mapOutcomeToStatus(sendResult.getOutcome()));
|
||||||
messageRecordDO.setErrorCode(sendResult.getErrorCode());
|
messageRecordDO.setErrorCode(sendResult.getErrorCode());
|
||||||
messageRecordDO.setErrorMsg(sendResult.getMessage());
|
messageRecordDO.setErrorMsg(sendResult.getMessage());
|
||||||
@@ -194,8 +194,12 @@ public class MessageRecordServiceImpl extends ServiceImpl<MessageRecordMapper, M
|
|||||||
if (SendOutcome.ACCEPTED.equals(sendResult.getOutcome())) {
|
if (SendOutcome.ACCEPTED.equals(sendResult.getOutcome())) {
|
||||||
messageConfirmRedisDAO.addToConfirmQueue(messageRecordDO);
|
messageConfirmRedisDAO.addToConfirmQueue(messageRecordDO);
|
||||||
|
|
||||||
|
// 仅首发(retrySource==null)消耗每日配额/接收者频率计数;重发(AUTO_RETRY/MANUAL_RETRY)
|
||||||
|
// 是同一条消息的再次投递,不重复扣额——与守卫豁免配套,避免重发把自己的配额耗尽再被拦。
|
||||||
|
if (retrySource == null) {
|
||||||
systemQuotaRedisDAO.set(messageRecordDO.getChannel(), messageRecordDO.getAppName());
|
systemQuotaRedisDAO.set(messageRecordDO.getChannel(), messageRecordDO.getAppName());
|
||||||
rateLimitRedisDAO.set(messageRecordDO.getChannel(), messageRecordDO.getAppName(), messageRecordDO.getReceiver());
|
rateLimitRedisDAO.set(messageRecordDO.getChannel(), messageRecordDO.getAppName(), messageRecordDO.getReceiver());
|
||||||
|
}
|
||||||
} else if (SendOutcome.RETRYABLE_FAILED.equals(sendResult.getOutcome())) {
|
} else if (SendOutcome.RETRYABLE_FAILED.equals(sendResult.getOutcome())) {
|
||||||
messageRetryQueueService.saveOrUpdateRetryMessage(messageRecordDO);
|
messageRetryQueueService.saveOrUpdateRetryMessage(messageRecordDO);
|
||||||
channelProviderConfigService.failureUpdate(messageRecordDO.getProviderType(), messageRecordDO.getChannel());
|
channelProviderConfigService.failureUpdate(messageRecordDO.getProviderType(), messageRecordDO.getChannel());
|
||||||
|
|||||||
Reference in New Issue
Block a user