mirror of https://github.com/abpframework/abp.git
committed by
GitHub
33 changed files with 764 additions and 102 deletions
@ -0,0 +1,53 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Confluent.Kafka; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Logging.Abstractions; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Kafka |
||||
|
{ |
||||
|
public class KafkaEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
protected ILogger<KafkaEventErrorHandler> Logger { get; set; } |
||||
|
|
||||
|
public KafkaEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) : base(options) |
||||
|
{ |
||||
|
Logger = NullLogger<KafkaEventErrorHandler>.Instance; |
||||
|
} |
||||
|
|
||||
|
protected override async Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
||||
|
{ |
||||
|
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
||||
|
} |
||||
|
|
||||
|
context.TryGetRetryAttempt(out var retryAttempt); |
||||
|
|
||||
|
await context.EventBus.As<KafkaDistributedEventBus>().PublishAsync( |
||||
|
context.EventType, |
||||
|
context.EventData, |
||||
|
context.GetProperty(HeadersKey).As<Headers>(), |
||||
|
new Dictionary<string, object> {{RetryAttemptKey, ++retryAttempt}}); |
||||
|
} |
||||
|
|
||||
|
protected override async Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
Logger.LogException( |
||||
|
context.Exceptions.Count == 1 ? context.Exceptions.First() : new AggregateException(context.Exceptions), |
||||
|
LogLevel.Error); |
||||
|
|
||||
|
await context.EventBus.As<KafkaDistributedEventBus>().PublishToDeadLetterAsync( |
||||
|
context.EventType, |
||||
|
context.EventData, |
||||
|
context.GetProperty(HeadersKey).As<Headers>(), |
||||
|
new Dictionary<string, object> {{"exceptions", context.Exceptions.Select(x => x.ToString()).ToList()}}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using RabbitMQ.Client; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.RabbitMq |
||||
|
{ |
||||
|
public class RabbitMqEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
public RabbitMqEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
||||
|
{ |
||||
|
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
||||
|
} |
||||
|
|
||||
|
context.TryGetRetryAttempt(out var retryAttempt); |
||||
|
|
||||
|
await context.EventBus.As<RabbitMqDistributedEventBus>().PublishAsync( |
||||
|
context.EventType, |
||||
|
context.EventData, |
||||
|
context.GetProperty(HeadersKey).As<IBasicProperties>(), |
||||
|
new Dictionary<string, object> |
||||
|
{ |
||||
|
{RetryAttemptKey, ++retryAttempt}, |
||||
|
{"exceptions", context.Exceptions.Select(x => x.ToString()).ToList()} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Rebus |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Rebus will automatic retries and error handling: https://github.com/rebus-org/Rebus/wiki/Automatic-retries-and-error-handling
|
||||
|
/// </summary>
|
||||
|
public class RebusEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
public RebusEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class AbpEventBusOptions |
||||
|
{ |
||||
|
public bool EnabledErrorHandle { get; set; } |
||||
|
|
||||
|
public Func<Type, bool> ErrorHandleSelector { get; set; } |
||||
|
|
||||
|
public string DeadLetterName { get; set; } |
||||
|
|
||||
|
public AbpEventBusRetryStrategyOptions RetryStrategyOptions { get; set; } |
||||
|
|
||||
|
public void UseRetryStrategy(Action<AbpEventBusRetryStrategyOptions> action = null) |
||||
|
{ |
||||
|
EnabledErrorHandle = true; |
||||
|
RetryStrategyOptions = new AbpEventBusRetryStrategyOptions(); |
||||
|
action?.Invoke(RetryStrategyOptions); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class AbpEventBusRetryStrategyOptions |
||||
|
{ |
||||
|
public int IntervalMillisecond { get; set; } = 3000; |
||||
|
|
||||
|
public int MaxRetryAttempts { get; set; } = 3; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public abstract class EventErrorHandlerBase : IEventErrorHandler |
||||
|
{ |
||||
|
public const string HeadersKey = "headers"; |
||||
|
public const string RetryAttemptKey = "retryAttempt"; |
||||
|
|
||||
|
protected AbpEventBusOptions Options { get; } |
||||
|
|
||||
|
protected EventErrorHandlerBase(IOptions<AbpEventBusOptions> options) |
||||
|
{ |
||||
|
Options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public virtual async Task HandleAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (!await ShouldHandleAsync(context)) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
} |
||||
|
|
||||
|
if (await ShouldRetryAsync(context)) |
||||
|
{ |
||||
|
await RetryAsync(context); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await MoveToDeadLetterAsync(context); |
||||
|
} |
||||
|
|
||||
|
protected abstract Task RetryAsync(EventExecutionErrorContext context); |
||||
|
|
||||
|
protected abstract Task MoveToDeadLetterAsync(EventExecutionErrorContext context); |
||||
|
|
||||
|
protected virtual Task<bool> ShouldHandleAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (!Options.EnabledErrorHandle) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(Options.ErrorHandleSelector == null || Options.ErrorHandleSelector.Invoke(context.EventType)); |
||||
|
} |
||||
|
|
||||
|
protected virtual Task<bool> ShouldRetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions == null) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
if (!context.TryGetRetryAttempt(out var retryAttempt)) |
||||
|
{ |
||||
|
return Task.FromResult(false); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(Options.RetryStrategyOptions.MaxRetryAttempts > retryAttempt); |
||||
|
} |
||||
|
|
||||
|
protected virtual void ThrowOriginalExceptions(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (context.Exceptions.Count == 1) |
||||
|
{ |
||||
|
context.Exceptions[0].ReThrow(); |
||||
|
} |
||||
|
|
||||
|
throw new AggregateException( |
||||
|
"More than one error has occurred while triggering the event: " + context.EventType, |
||||
|
context.Exceptions); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.ObjectExtending; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class EventExecutionErrorContext : ExtensibleObject |
||||
|
{ |
||||
|
public IReadOnlyList<Exception> Exceptions { get; } |
||||
|
|
||||
|
public object EventData { get; set; } |
||||
|
|
||||
|
public Type EventType { get; } |
||||
|
|
||||
|
public IEventBus EventBus { get; } |
||||
|
|
||||
|
public EventExecutionErrorContext(List<Exception> exceptions, Type eventType, IEventBus eventBus) |
||||
|
{ |
||||
|
Exceptions = exceptions; |
||||
|
EventType = eventType; |
||||
|
EventBus = eventBus; |
||||
|
} |
||||
|
|
||||
|
public bool TryGetRetryAttempt(out int retryAttempt) |
||||
|
{ |
||||
|
retryAttempt = 0; |
||||
|
if (!this.HasProperty(EventErrorHandlerBase.RetryAttemptKey)) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
retryAttempt = this.GetProperty<int>(EventErrorHandlerBase.RetryAttemptKey); |
||||
|
return true; |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public interface IEventErrorHandler |
||||
|
{ |
||||
|
Task HandleAsync(EventExecutionErrorContext context); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Local |
||||
|
{ |
||||
|
[ExposeServices(typeof(LocalEventErrorHandler), typeof(IEventErrorHandler))] |
||||
|
public class LocalEventErrorHandler : EventErrorHandlerBase, ISingletonDependency |
||||
|
{ |
||||
|
protected Dictionary<Guid, int> RetryTracking { get; } |
||||
|
|
||||
|
public LocalEventErrorHandler( |
||||
|
IOptions<AbpEventBusOptions> options) |
||||
|
: base(options) |
||||
|
{ |
||||
|
RetryTracking = new Dictionary<Guid, int>(); |
||||
|
} |
||||
|
|
||||
|
protected override async Task RetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
if (Options.RetryStrategyOptions.IntervalMillisecond > 0) |
||||
|
{ |
||||
|
await Task.Delay(Options.RetryStrategyOptions.IntervalMillisecond); |
||||
|
} |
||||
|
|
||||
|
var messageId = context.GetProperty<Guid>(nameof(LocalEventMessage.MessageId)); |
||||
|
|
||||
|
context.TryGetRetryAttempt(out var retryAttempt); |
||||
|
RetryTracking[messageId] = ++retryAttempt; |
||||
|
|
||||
|
await context.EventBus.As<LocalEventBus>().PublishAsync(new LocalEventMessage(messageId, context.EventData, context.EventType)); |
||||
|
|
||||
|
RetryTracking.Remove(messageId); |
||||
|
} |
||||
|
|
||||
|
protected override Task MoveToDeadLetterAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
ThrowOriginalExceptions(context); |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
protected override async Task<bool> ShouldRetryAsync(EventExecutionErrorContext context) |
||||
|
{ |
||||
|
var messageId = context.GetProperty<Guid>(nameof(LocalEventMessage.MessageId)); |
||||
|
context.SetProperty(RetryAttemptKey, RetryTracking.GetOrDefault(messageId)); |
||||
|
|
||||
|
if (await base.ShouldRetryAsync(context)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
RetryTracking.Remove(messageId); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Local |
||||
|
{ |
||||
|
public class LocalEventMessage |
||||
|
{ |
||||
|
public Guid MessageId { get; } |
||||
|
|
||||
|
public object EventData { get; } |
||||
|
|
||||
|
public Type EventType { get; } |
||||
|
|
||||
|
public LocalEventMessage(Guid messageId, object eventData, Type eventType) |
||||
|
{ |
||||
|
MessageId = messageId; |
||||
|
EventData = eventData; |
||||
|
EventType = eventType; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,75 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Shouldly; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.Abp.EventBus.Local |
||||
|
{ |
||||
|
public class EventBus_Exception_Handler_Tests : EventBusTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public async Task Should_Not_Handle_Exception() |
||||
|
{ |
||||
|
var retryAttempt = 0; |
||||
|
LocalEventBus.Subscribe<MySimpleEventData>(eventData => |
||||
|
{ |
||||
|
retryAttempt++; |
||||
|
throw new Exception("This exception is intentionally thrown!"); |
||||
|
}); |
||||
|
|
||||
|
var appException = await Assert.ThrowsAsync<Exception>(async () => |
||||
|
{ |
||||
|
await LocalEventBus.PublishAsync(new MySimpleEventData(1)); |
||||
|
}); |
||||
|
|
||||
|
retryAttempt.ShouldBe(1); |
||||
|
appException.Message.ShouldBe("This exception is intentionally thrown!"); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Handle_Exception() |
||||
|
{ |
||||
|
var retryAttempt = 0; |
||||
|
LocalEventBus.Subscribe<MyExceptionHandleEventData>(eventData => |
||||
|
{ |
||||
|
eventData.Value.ShouldBe(0); |
||||
|
|
||||
|
retryAttempt++; |
||||
|
eventData.Value++; |
||||
|
if (retryAttempt < 2) |
||||
|
{ |
||||
|
throw new Exception("This exception is intentionally thrown!"); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
await LocalEventBus.PublishAsync(new MyExceptionHandleEventData(0)); |
||||
|
retryAttempt.ShouldBe(2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_Throw_Exception_After_Error_Handle() |
||||
|
{ |
||||
|
var retryAttempt = 0; |
||||
|
LocalEventBus.Subscribe<MyExceptionHandleEventData>(eventData => |
||||
|
{ |
||||
|
eventData.Value.ShouldBe(0); |
||||
|
|
||||
|
retryAttempt++; |
||||
|
eventData.Value++; |
||||
|
|
||||
|
throw new Exception("This exception is intentionally thrown!"); |
||||
|
}); |
||||
|
|
||||
|
var appException = await Assert.ThrowsAsync<Exception>(async () => |
||||
|
{ |
||||
|
await LocalEventBus.PublishAsync(new MyExceptionHandleEventData(0)); |
||||
|
}); |
||||
|
|
||||
|
retryAttempt.ShouldBe(4); |
||||
|
appException.Message.ShouldBe("This exception is intentionally thrown!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
namespace Volo.Abp.EventBus |
||||
|
{ |
||||
|
public class MyExceptionHandleEventData |
||||
|
{ |
||||
|
public int Value { get; set; } |
||||
|
|
||||
|
public MyExceptionHandleEventData(int value) |
||||
|
{ |
||||
|
Value = value; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue