简化
This commit is contained in:
parent
98664aad3b
commit
32753e7606
@ -8,7 +8,8 @@ using Oms.Application.Services.Orders.Batch;
|
|||||||
namespace Oms.Application.Messaging;
|
namespace Oms.Application.Messaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 订单批量集成事件处理器。消费通道/Ack/Nack/DLX/重试由 Fake.EventBus.RabbitMQ 负责。
|
/// 订单批量集成事件处理器。
|
||||||
|
/// 正常返回 Ack;抛异常由 Fake.EventBus.RabbitMQ 重试后 Nack 进 DLX。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class OrderBatchJobEventHandler(
|
public sealed class OrderBatchJobEventHandler(
|
||||||
OrderBatchJobProcessor processor,
|
OrderBatchJobProcessor processor,
|
||||||
@ -29,15 +30,7 @@ public sealed class OrderBatchJobEventHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
using var unitOfWork = unitOfWorkManager.Begin();
|
using var unitOfWork = unitOfWorkManager.Begin();
|
||||||
var message = @event.ToMessage();
|
await processor.ProcessAsync(@event.ToMessage(), cancellationToken);
|
||||||
var result = await processor.ProcessAsync(message, cancellationToken);
|
|
||||||
await unitOfWork.CompleteAsync(cancellationToken);
|
await unitOfWork.CompleteAsync(cancellationToken);
|
||||||
|
|
||||||
// Fake EventBus:成功返回则 Ack;抛异常则框架内重试后 Nack(requeue=false) 进 DLX。
|
|
||||||
if (result.Kind == OrderBatchJobProcessor.ProcessResultKind.Reject && result.Requeue)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Order batch system failure, allow Fake EventBus retry. action={message.Action} jobItemId={message.JobItemId} messageId={message.MessageId}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
namespace Oms.Application.Messaging;
|
namespace Oms.Application.Messaging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 订单批量消息业务开关。连接/消费/QoS/DLX 由 Fake.EventBus.RabbitMQ(RabbitMqEventBus)配置。
|
/// 订单批量消息业务开关。连接/消费/QoS/DLX/重试由 Fake.EventBus.RabbitMQ 配置。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OrderBatchRabbitMqOptions
|
public class OrderBatchRabbitMqOptions
|
||||||
{
|
{
|
||||||
@ -11,9 +11,4 @@ public class OrderBatchRabbitMqOptions
|
|||||||
/// 是否启用订单批量消息发布与处理。
|
/// 是否启用订单批量消息发布与处理。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 系统失败时任务项最大重试次数(业务侧),与 Fake EventBus 进程内重试相互独立。
|
|
||||||
/// </summary>
|
|
||||||
public int MaxRetryCount { get; set; } = 3;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
using Fake.Application;
|
|
||||||
using Fake.DependencyInjection;
|
using Fake.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Messaging;
|
using Oms.Application.Messaging;
|
||||||
@ -13,15 +11,15 @@ namespace Oms.Application.Services.Orders.Batch;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 消费 RabbitMQ 消息后更新任务状态并执行业务。
|
/// 消费 RabbitMQ 消息后更新任务状态并执行业务。
|
||||||
|
/// 正常返回由 Fake EventBus Ack;抛异常则框架重试后进 DLX。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class OrderBatchJobProcessor(
|
public sealed class OrderBatchJobProcessor(
|
||||||
IOrderBatchJobRepository batchJobRepository,
|
IOrderBatchJobRepository batchJobRepository,
|
||||||
IOrderTaskRepository orderTaskRepository,
|
IOrderTaskRepository orderTaskRepository,
|
||||||
IOrderBatchActionExecutor actionExecutor,
|
IOrderBatchActionExecutor actionExecutor,
|
||||||
IOptions<OrderBatchRabbitMqOptions> rabbitMqOptions,
|
|
||||||
ILogger<OrderBatchJobProcessor> logger) : IScopedDependency
|
ILogger<OrderBatchJobProcessor> logger) : IScopedDependency
|
||||||
{
|
{
|
||||||
public async Task<ProcessResult> ProcessAsync(
|
public async Task ProcessAsync(
|
||||||
OrderBatchJobMessage message,
|
OrderBatchJobMessage message,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@ -32,18 +30,18 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Job item {JobItemId} not found, ack message {MessageId}", message.JobItemId, message.MessageId);
|
logger.LogWarning("Job item {JobItemId} not found, ack message {MessageId}", message.JobItemId, message.MessageId);
|
||||||
return ProcessResult.Ack;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Status == OrderBatchJobItemStatusCodes.Success)
|
if (item.Status == OrderBatchJobItemStatusCodes.Success)
|
||||||
{
|
{
|
||||||
return ProcessResult.Ack;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.Status == OrderBatchJobItemStatusCodes.Cancelled)
|
if (item.Status == OrderBatchJobItemStatusCodes.Cancelled)
|
||||||
{
|
{
|
||||||
logger.LogInformation("Skip cancelled job item {JobItemId}", item.Id);
|
logger.LogInformation("Skip cancelled job item {JobItemId}", item.Id);
|
||||||
return ProcessResult.Ack;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Status = OrderBatchJobItemStatusCodes.Running;
|
item.Status = OrderBatchJobItemStatusCodes.Running;
|
||||||
@ -56,7 +54,7 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
|
|
||||||
if (message.JobItemId <= 0)
|
if (message.JobItemId <= 0)
|
||||||
{
|
{
|
||||||
// fire-and-forget(如轨迹同步):失败也不 requeue,避免空转打爆 DB
|
// fire-and-forget(如轨迹同步):失败只记日志,不抛、不进 DLX
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
@ -66,13 +64,13 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
result.FailReason);
|
result.FailReason);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ProcessResult.Ack;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item = await batchJobRepository.GetItemByIdAsync((int)message.JobItemId, cancellationToken);
|
item = await batchJobRepository.GetItemByIdAsync((int)message.JobItemId, cancellationToken);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
{
|
{
|
||||||
return ProcessResult.Ack;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.FinishedAt = DateTime.Now;
|
item.FinishedAt = DateTime.Now;
|
||||||
@ -98,7 +96,7 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
|
|
||||||
await batchJobRepository.UpdateItemAsync(item, cancellationToken);
|
await batchJobRepository.UpdateItemAsync(item, cancellationToken);
|
||||||
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
||||||
return ProcessResult.Ack;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.IsBusinessFailure)
|
if (result.IsBusinessFailure)
|
||||||
@ -112,39 +110,7 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
||||||
return ProcessResult.Ack;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.RetryCount++;
|
|
||||||
var maxRetryCount = Math.Max(1, rabbitMqOptions.Value.MaxRetryCount);
|
|
||||||
if (item.RetryCount >= maxRetryCount)
|
|
||||||
{
|
|
||||||
item.Status = OrderBatchJobItemStatusCodes.Failed;
|
|
||||||
item.FailReason = string.IsNullOrWhiteSpace(result.FailReason)
|
|
||||||
? "系统重试次数已耗尽"
|
|
||||||
: result.FailReason;
|
|
||||||
item.FinishedAt = DateTime.Now;
|
|
||||||
item.UpdateTime = DateTime.Now;
|
|
||||||
await batchJobRepository.UpdateItemAsync(item, cancellationToken);
|
|
||||||
if (message.Action == OrderBatchActionCodes.ImportRow)
|
|
||||||
{
|
|
||||||
await RecordImportTaskFailAsync(message, item.FailReason, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
await RefreshJobSummaryAsync(item.JobId, cancellationToken);
|
|
||||||
logger.LogWarning(
|
|
||||||
"Job item {JobItemId} retry exhausted after {RetryCount} attempts",
|
|
||||||
item.Id,
|
|
||||||
item.RetryCount);
|
|
||||||
return ProcessResult.Ack;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Status = OrderBatchJobItemStatusCodes.Pending;
|
|
||||||
item.FailReason = null;
|
|
||||||
item.FinishedAt = null;
|
|
||||||
item.UpdateTime = DateTime.Now;
|
|
||||||
await batchJobRepository.UpdateItemAsync(item, cancellationToken);
|
|
||||||
return ProcessResult.Reject(isBusinessFailure: false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshJobSummaryAsync(int jobId, CancellationToken cancellationToken)
|
private async Task RefreshJobSummaryAsync(int jobId, CancellationToken cancellationToken)
|
||||||
@ -251,22 +217,4 @@ public sealed class OrderBatchJobProcessor(
|
|||||||
}
|
}
|
||||||
], cancellationToken);
|
], cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ProcessResultKind
|
|
||||||
{
|
|
||||||
Ack,
|
|
||||||
Reject,
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly struct ProcessResult
|
|
||||||
{
|
|
||||||
public ProcessResultKind Kind { get; init; }
|
|
||||||
|
|
||||||
public bool Requeue { get; init; }
|
|
||||||
|
|
||||||
public static ProcessResult Ack => new() { Kind = ProcessResultKind.Ack };
|
|
||||||
|
|
||||||
public static ProcessResult Reject(bool isBusinessFailure) =>
|
|
||||||
new() { Kind = ProcessResultKind.Reject, Requeue = !isBusinessFailure };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Moq;
|
using Moq;
|
||||||
using Oms.Application.Messaging;
|
using Oms.Application.Messaging;
|
||||||
using Oms.Application.Services.Orders.Batch;
|
using Oms.Application.Services.Orders.Batch;
|
||||||
@ -23,12 +22,11 @@ public class OrderBatchJobProcessorTests
|
|||||||
_batchJobRepository.Object,
|
_batchJobRepository.Object,
|
||||||
_orderTaskRepository.Object,
|
_orderTaskRepository.Object,
|
||||||
_actionExecutor.Object,
|
_actionExecutor.Object,
|
||||||
Options.Create(new OrderBatchRabbitMqOptions { MaxRetryCount = 3 }),
|
|
||||||
NullLogger<OrderBatchJobProcessor>.Instance);
|
NullLogger<OrderBatchJobProcessor>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProcessAsync_BusinessFailure_ShouldMarkFailedAndAck()
|
public async Task ProcessAsync_BusinessFailure_ShouldMarkFailed()
|
||||||
{
|
{
|
||||||
var item = CreateItem(id: 10, status: OrderBatchJobItemStatusCodes.Pending);
|
var item = CreateItem(id: 10, status: OrderBatchJobItemStatusCodes.Pending);
|
||||||
var message = CreateMessage(item.Id);
|
var message = CreateMessage(item.Id);
|
||||||
@ -38,54 +36,30 @@ public class OrderBatchJobProcessorTests
|
|||||||
.ReturnsAsync(OrderBatchActionExecuteResult.BusinessFail("订单不存在"));
|
.ReturnsAsync(OrderBatchActionExecuteResult.BusinessFail("订单不存在"));
|
||||||
SetupJobRefresh(item.JobId, [item]);
|
SetupJobRefresh(item.JobId, [item]);
|
||||||
|
|
||||||
var result = await _processor.ProcessAsync(message);
|
await _processor.ProcessAsync(message);
|
||||||
|
|
||||||
result.Kind.Should().Be(OrderBatchJobProcessor.ProcessResultKind.Ack);
|
|
||||||
item.Status.Should().Be(OrderBatchJobItemStatusCodes.Failed);
|
item.Status.Should().Be(OrderBatchJobItemStatusCodes.Failed);
|
||||||
item.FailReason.Should().Be("订单不存在");
|
item.FailReason.Should().Be("订单不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProcessAsync_SystemFailure_WithRetriesRemaining_ShouldResetPendingAndReject()
|
public async Task ProcessAsync_SystemFailure_ShouldThrow()
|
||||||
{
|
{
|
||||||
var item = CreateItem(id: 11, status: OrderBatchJobItemStatusCodes.Pending, retryCount: 1);
|
var item = CreateItem(id: 11, status: OrderBatchJobItemStatusCodes.Pending);
|
||||||
var message = CreateMessage(item.Id);
|
var message = CreateMessage(item.Id);
|
||||||
SetupItemLookup(item);
|
SetupItemLookup(item);
|
||||||
_actionExecutor
|
_actionExecutor
|
||||||
.Setup(x => x.ExecuteAsync(message, It.IsAny<CancellationToken>()))
|
.Setup(x => x.ExecuteAsync(message, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(OrderBatchActionExecuteResult.SystemFail("下游超时"));
|
.ReturnsAsync(OrderBatchActionExecuteResult.SystemFail("下游超时"));
|
||||||
|
|
||||||
var result = await _processor.ProcessAsync(message);
|
var act = () => _processor.ProcessAsync(message);
|
||||||
|
|
||||||
result.Kind.Should().Be(OrderBatchJobProcessor.ProcessResultKind.Reject);
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||||
result.Requeue.Should().BeTrue();
|
.WithMessage("*下游超时*");
|
||||||
item.Status.Should().Be(OrderBatchJobItemStatusCodes.Pending);
|
|
||||||
item.RetryCount.Should().Be(2);
|
|
||||||
item.FailReason.Should().BeNull();
|
|
||||||
item.FinishedAt.Should().BeNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProcessAsync_SystemFailure_WhenRetryExhausted_ShouldMarkFailedAndAck()
|
public async Task ProcessAsync_CancelledItem_ShouldSkipWithoutExecuting()
|
||||||
{
|
|
||||||
var item = CreateItem(id: 12, status: OrderBatchJobItemStatusCodes.Pending, retryCount: 2);
|
|
||||||
var message = CreateMessage(item.Id);
|
|
||||||
SetupItemLookup(item);
|
|
||||||
_actionExecutor
|
|
||||||
.Setup(x => x.ExecuteAsync(message, It.IsAny<CancellationToken>()))
|
|
||||||
.ReturnsAsync(OrderBatchActionExecuteResult.SystemFail("下游超时"));
|
|
||||||
SetupJobRefresh(item.JobId, [item]);
|
|
||||||
|
|
||||||
var result = await _processor.ProcessAsync(message);
|
|
||||||
|
|
||||||
result.Kind.Should().Be(OrderBatchJobProcessor.ProcessResultKind.Ack);
|
|
||||||
item.Status.Should().Be(OrderBatchJobItemStatusCodes.Failed);
|
|
||||||
item.RetryCount.Should().Be(3);
|
|
||||||
item.FailReason.Should().Be("下游超时");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ProcessAsync_CancelledItem_ShouldAckWithoutExecuting()
|
|
||||||
{
|
{
|
||||||
var item = CreateItem(id: 13, status: OrderBatchJobItemStatusCodes.Cancelled);
|
var item = CreateItem(id: 13, status: OrderBatchJobItemStatusCodes.Cancelled);
|
||||||
var message = CreateMessage(item.Id);
|
var message = CreateMessage(item.Id);
|
||||||
@ -93,16 +67,15 @@ public class OrderBatchJobProcessorTests
|
|||||||
.Setup(x => x.GetItemByIdAsync(item.Id, It.IsAny<CancellationToken>()))
|
.Setup(x => x.GetItemByIdAsync(item.Id, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(item);
|
.ReturnsAsync(item);
|
||||||
|
|
||||||
var result = await _processor.ProcessAsync(message);
|
await _processor.ProcessAsync(message);
|
||||||
|
|
||||||
result.Kind.Should().Be(OrderBatchJobProcessor.ProcessResultKind.Ack);
|
|
||||||
_actionExecutor.Verify(
|
_actionExecutor.Verify(
|
||||||
x => x.ExecuteAsync(It.IsAny<OrderBatchJobMessage>(), It.IsAny<CancellationToken>()),
|
x => x.ExecuteAsync(It.IsAny<OrderBatchJobMessage>(), It.IsAny<CancellationToken>()),
|
||||||
Times.Never);
|
Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ProcessAsync_FireAndForgetFailure_ShouldAckWithoutRequeue()
|
public async Task ProcessAsync_FireAndForgetFailure_ShouldNotThrow()
|
||||||
{
|
{
|
||||||
var message = new OrderBatchJobMessage
|
var message = new OrderBatchJobMessage
|
||||||
{
|
{
|
||||||
@ -116,10 +89,8 @@ public class OrderBatchJobProcessorTests
|
|||||||
.Setup(x => x.ExecuteAsync(message, It.IsAny<CancellationToken>()))
|
.Setup(x => x.ExecuteAsync(message, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(OrderBatchActionExecuteResult.SystemFail("下游超时"));
|
.ReturnsAsync(OrderBatchActionExecuteResult.SystemFail("下游超时"));
|
||||||
|
|
||||||
var result = await _processor.ProcessAsync(message);
|
await _processor.ProcessAsync(message);
|
||||||
|
|
||||||
result.Kind.Should().Be(OrderBatchJobProcessor.ProcessResultKind.Ack);
|
|
||||||
result.Requeue.Should().BeFalse();
|
|
||||||
_batchJobRepository.Verify(
|
_batchJobRepository.Verify(
|
||||||
x => x.GetItemByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()),
|
x => x.GetItemByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()),
|
||||||
Times.Never);
|
Times.Never);
|
||||||
|
|||||||
@ -31,10 +31,6 @@
|
|||||||
"RefreshExpiryInDays": "7",
|
"RefreshExpiryInDays": "7",
|
||||||
"ClockSkewMinutes": "5"
|
"ClockSkewMinutes": "5"
|
||||||
},
|
},
|
||||||
"FeiShu": {
|
|
||||||
"Webhook": "https://example.com/webhook",
|
|
||||||
"EnableFeiShuExceptionSubscribe": false
|
|
||||||
},
|
|
||||||
"FeiShuNotice": {
|
"FeiShuNotice": {
|
||||||
"Webhook": "https://example.com/webhook",
|
"Webhook": "https://example.com/webhook",
|
||||||
"EnableFeiShuExceptionSubscribe": false
|
"EnableFeiShuExceptionSubscribe": false
|
||||||
@ -112,8 +108,7 @@
|
|||||||
},
|
},
|
||||||
"OrderBatch": {
|
"OrderBatch": {
|
||||||
"RabbitMq": {
|
"RabbitMq": {
|
||||||
"Enabled": false,
|
"Enabled": false
|
||||||
"MaxRetryCount": 3
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user