Merge pull request #8829 from abpframework/liangshiwei/eventbus-errorhandle

Introduce event bus error handler
pull/9353/head
maliming 4 years ago committed by GitHub
commit 8bebad24a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Confluent.Kafka;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Kafka;
@ -18,6 +19,7 @@ namespace Volo.Abp.EventBus.Kafka
[ExposeServices(typeof(IDistributedEventBus), typeof(KafkaDistributedEventBus))]
public class KafkaDistributedEventBus : EventBusBase, IDistributedEventBus, ISingletonDependency
{
protected AbpEventBusOptions AbpEventBusOptions { get; }
protected AbpKafkaEventBusOptions AbpKafkaEventBusOptions { get; }
protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; }
protected IKafkaMessageConsumerFactory MessageConsumerFactory { get; }
@ -26,6 +28,7 @@ namespace Volo.Abp.EventBus.Kafka
protected ConcurrentDictionary<Type, List<IEventHandlerFactory>> HandlerFactories { get; }
protected ConcurrentDictionary<string, Type> EventTypes { get; }
protected IKafkaMessageConsumer Consumer { get; private set; }
protected string DeadLetterTopicName { get; }
public KafkaDistributedEventBus(
IServiceScopeFactory serviceScopeFactory,
@ -34,14 +37,19 @@ namespace Volo.Abp.EventBus.Kafka
IKafkaMessageConsumerFactory messageConsumerFactory,
IOptions<AbpDistributedEventBusOptions> abpDistributedEventBusOptions,
IKafkaSerializer serializer,
IProducerPool producerPool)
: base(serviceScopeFactory, currentTenant)
IProducerPool producerPool,
IEventErrorHandler errorHandler,
IOptions<AbpEventBusOptions> abpEventBusOptions)
: base(serviceScopeFactory, currentTenant, errorHandler)
{
AbpKafkaEventBusOptions = abpKafkaEventBusOptions.Value;
AbpDistributedEventBusOptions = abpDistributedEventBusOptions.Value;
AbpEventBusOptions = abpEventBusOptions.Value;
MessageConsumerFactory = messageConsumerFactory;
Serializer = serializer;
ProducerPool = producerPool;
DeadLetterTopicName =
AbpEventBusOptions.DeadLetterName ?? AbpKafkaEventBusOptions.TopicName + "_dead_letter";
HandlerFactories = new ConcurrentDictionary<Type, List<IEventHandlerFactory>>();
EventTypes = new ConcurrentDictionary<string, Type>();
@ -51,9 +59,9 @@ namespace Volo.Abp.EventBus.Kafka
{
Consumer = MessageConsumerFactory.Create(
AbpKafkaEventBusOptions.TopicName,
DeadLetterTopicName,
AbpKafkaEventBusOptions.GroupId,
AbpKafkaEventBusOptions.ConnectionName);
Consumer.OnMessageReceived(ProcessEventAsync);
SubscribeHandlers(AbpDistributedEventBusOptions.Handlers);
@ -70,7 +78,18 @@ namespace Volo.Abp.EventBus.Kafka
var eventData = Serializer.Deserialize(message.Value, eventType);
await TriggerHandlersAsync(eventType, eventData);
await TriggerHandlersAsync(eventType, eventData, errorContext =>
{
var retryAttempt = 0;
if (message.Headers.TryGetLastBytes(EventErrorHandlerBase.RetryAttemptKey, out var retryAttemptBytes))
{
retryAttempt = Serializer.Deserialize<int>(retryAttemptBytes);
}
errorContext.EventData = Serializer.Deserialize(message.Value, eventType);
errorContext.SetProperty(EventErrorHandlerBase.HeadersKey, message.Headers);
errorContext.SetProperty(EventErrorHandlerBase.RetryAttemptKey, retryAttempt);
});
}
public IDisposable Subscribe<TEvent>(IDistributedEventHandler<TEvent> handler) where TEvent : class
@ -147,20 +166,51 @@ namespace Volo.Abp.EventBus.Kafka
}
public override async Task PublishAsync(Type eventType, object eventData)
{
await PublishAsync(eventType, eventData, new Headers {{"messageId", Serializer.Serialize(Guid.NewGuid())}}, null);
}
public virtual async Task PublishAsync(Type eventType, object eventData, Headers headers, Dictionary<string, object> headersArguments)
{
await PublishAsync(AbpKafkaEventBusOptions.TopicName, eventType, eventData, headers, headersArguments);
}
public virtual async Task PublishToDeadLetterAsync(Type eventType, object eventData, Headers headers, Dictionary<string, object> headersArguments)
{
await PublishAsync(DeadLetterTopicName, eventType, eventData, headers, headersArguments);
}
private async Task PublishAsync(string topicName, Type eventType, object eventData, Headers headers, Dictionary<string, object> headersArguments)
{
var eventName = EventNameAttribute.GetNameOrDefault(eventType);
var body = Serializer.Serialize(eventData);
var producer = ProducerPool.Get(AbpKafkaEventBusOptions.ConnectionName);
SetEventMessageHeaders(headers, headersArguments);
await producer.ProduceAsync(
AbpKafkaEventBusOptions.TopicName,
topicName,
new Message<string, byte[]>
{
Key = eventName, Value = body
Key = eventName, Value = body, Headers = headers
});
}
private void SetEventMessageHeaders(Headers headers, Dictionary<string, object> headersArguments)
{
if (headersArguments == null)
{
return;
}
foreach (var header in headersArguments)
{
headers.Remove(header.Key);
headers.Add(header.Key, Serializer.Serialize(header.Value));
}
}
private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
{
return HandlerFactories.GetOrAdd(

@ -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()}});
}
}
}

@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
@ -25,6 +26,7 @@ namespace Volo.Abp.EventBus.RabbitMq
{
protected AbpRabbitMqEventBusOptions AbpRabbitMqEventBusOptions { get; }
protected AbpDistributedEventBusOptions AbpDistributedEventBusOptions { get; }
protected AbpEventBusOptions AbpEventBusOptions { get; }
protected IConnectionPool ConnectionPool { get; }
protected IRabbitMqSerializer Serializer { get; }
@ -41,12 +43,15 @@ namespace Volo.Abp.EventBus.RabbitMq
IServiceScopeFactory serviceScopeFactory,
IOptions<AbpDistributedEventBusOptions> distributedEventBusOptions,
IRabbitMqMessageConsumerFactory messageConsumerFactory,
ICurrentTenant currentTenant)
: base(serviceScopeFactory, currentTenant)
ICurrentTenant currentTenant,
IEventErrorHandler errorHandler,
IOptions<AbpEventBusOptions> abpEventBusOptions)
: base(serviceScopeFactory, currentTenant, errorHandler)
{
ConnectionPool = connectionPool;
Serializer = serializer;
MessageConsumerFactory = messageConsumerFactory;
AbpEventBusOptions = abpEventBusOptions.Value;
AbpDistributedEventBusOptions = distributedEventBusOptions.Value;
AbpRabbitMqEventBusOptions = options.Value;
@ -56,17 +61,21 @@ namespace Volo.Abp.EventBus.RabbitMq
public void Initialize()
{
const string suffix = "_dead_letter";
Consumer = MessageConsumerFactory.Create(
new ExchangeDeclareConfiguration(
AbpRabbitMqEventBusOptions.ExchangeName,
type: "direct",
durable: true
durable: true,
deadLetterExchangeName: AbpRabbitMqEventBusOptions.ExchangeName + suffix
),
new QueueDeclareConfiguration(
AbpRabbitMqEventBusOptions.ClientName,
durable: true,
exclusive: false,
autoDelete: false
autoDelete: false,
AbpEventBusOptions.DeadLetterName ?? AbpRabbitMqEventBusOptions.ClientName + suffix
),
AbpRabbitMqEventBusOptions.ConnectionName
);
@ -87,7 +96,19 @@ namespace Volo.Abp.EventBus.RabbitMq
var eventData = Serializer.Deserialize(ea.Body.ToArray(), eventType);
await TriggerHandlersAsync(eventType, eventData);
await TriggerHandlersAsync(eventType, eventData, errorContext =>
{
var retryAttempt = 0;
if (ea.BasicProperties.Headers != null &&
ea.BasicProperties.Headers.ContainsKey(EventErrorHandlerBase.RetryAttemptKey))
{
retryAttempt = (int)ea.BasicProperties.Headers[EventErrorHandlerBase.RetryAttemptKey];
}
errorContext.EventData = Serializer.Deserialize(ea.Body.ToArray(), eventType);
errorContext.SetProperty(EventErrorHandlerBase.HeadersKey, ea.BasicProperties);
errorContext.SetProperty(EventErrorHandlerBase.RetryAttemptKey, retryAttempt);
});
}
public IDisposable Subscribe<TEvent>(IDistributedEventHandler<TEvent> handler) where TEvent : class
@ -168,8 +189,14 @@ namespace Volo.Abp.EventBus.RabbitMq
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear());
}
public override Task PublishAsync(Type eventType, object eventData)
public override async Task PublishAsync(Type eventType, object eventData)
{
await PublishAsync(eventType, eventData, null);
}
public Task PublishAsync(Type eventType, object eventData, IBasicProperties properties, Dictionary<string, object> headersArguments = null)
{
var eventName = EventNameAttribute.GetNameOrDefault(eventType);
var body = Serializer.Serialize(eventData);
@ -181,11 +208,17 @@ namespace Volo.Abp.EventBus.RabbitMq
durable: true
);
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent;
if (properties == null)
{
properties = channel.CreateBasicProperties();
properties.DeliveryMode = RabbitMqConsts.DeliveryModes.Persistent;
properties.MessageId = Guid.NewGuid().ToString("N");
}
SetEventMessageHeaders(properties, headersArguments);
channel.BasicPublish(
exchange: AbpRabbitMqEventBusOptions.ExchangeName,
exchange: AbpRabbitMqEventBusOptions.ExchangeName,
routingKey: eventName,
mandatory: true,
basicProperties: properties,
@ -196,6 +229,21 @@ namespace Volo.Abp.EventBus.RabbitMq
return Task.CompletedTask;
}
private void SetEventMessageHeaders(IBasicProperties properties, Dictionary<string, object> headersArguments)
{
if (headersArguments == null)
{
return;
}
properties.Headers ??= new Dictionary<string, object>();
foreach (var header in headersArguments)
{
properties.Headers[header.Key] = header.Value;
}
}
private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
{
return HandlerFactories.GetOrAdd(
@ -213,9 +261,11 @@ namespace Volo.Abp.EventBus.RabbitMq
{
var handlerFactoryList = new List<EventTypeWithEventHandlerFactories>();
foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)))
foreach (var handlerFactory in
HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)))
{
handlerFactoryList.Add(new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value));
handlerFactoryList.Add(
new EventTypeWithEventHandlerFactories(handlerFactory.Key, handlerFactory.Value));
}
return handlerFactoryList.ToArray();

@ -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;
}
}
}

@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Rebus.Handlers;
using Rebus.Retry.Simple;
using Rebus.ServiceProvider;
using Volo.Abp.Modularity;
@ -11,23 +12,29 @@ namespace Volo.Abp.EventBus.Rebus
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var options = context.Services.ExecutePreConfiguredActions<AbpRebusEventBusOptions>();
var abpEventBusOptions = context.Services.ExecutePreConfiguredActions<AbpEventBusOptions>();
var options = context.Services.ExecutePreConfiguredActions<AbpRebusEventBusOptions>();;
context.Services.AddTransient(typeof(IHandleMessages<>), typeof(RebusDistributedEventHandlerAdapter<>));
Configure<AbpRebusEventBusOptions>(rebusOptions =>
{
rebusOptions.Configurer = options.Configurer;
rebusOptions.Publish = options.Publish;
rebusOptions.InputQueueName = options.InputQueueName;
context.Services.ExecutePreConfiguredActions(rebusOptions);
});
context.Services.AddRebus(configurer =>
context.Services.AddRebus(configure =>
{
options.Configurer?.Invoke(configurer);
return configurer;
});
if (abpEventBusOptions.RetryStrategyOptions != null)
{
configure.Options(b =>
b.SimpleRetryStrategy(
errorQueueAddress: abpEventBusOptions.DeadLetterName ?? options.InputQueueName + "_dead_letter",
maxDeliveryAttempts: abpEventBusOptions.RetryStrategyOptions.MaxRetryAttempts));
}
options.Configurer?.Invoke(configure);
return configure;
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)

@ -32,7 +32,7 @@ namespace Volo.Abp.EventBus.Rebus
public AbpRebusEventBusOptions()
{
_publish = DefaultPublish;
_configurer = DefaultConfigurer;
_configurer = DefaultConfigure;
}
private async Task DefaultPublish(IBus bus, Type eventType, object eventData)
@ -40,10 +40,10 @@ namespace Volo.Abp.EventBus.Rebus
await bus.Advanced.Routing.Send(InputQueueName, eventData);
}
private void DefaultConfigurer(RebusConfigurer configurer)
private void DefaultConfigure(RebusConfigurer configure)
{
configurer.Subscriptions(s => s.StoreInMemory());
configurer.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), InputQueueName));
configure.Subscriptions(s => s.StoreInMemory());
configure.Transport(t => t.UseInMemoryTransport(new InMemNetwork(), InputQueueName));
}
}
}

@ -30,8 +30,9 @@ namespace Volo.Abp.EventBus.Rebus
ICurrentTenant currentTenant,
IBus rebus,
IOptions<AbpDistributedEventBusOptions> abpDistributedEventBusOptions,
IOptions<AbpRebusEventBusOptions> abpEventBusRebusOptions) :
base(serviceScopeFactory, currentTenant)
IOptions<AbpRebusEventBusOptions> abpEventBusRebusOptions,
IEventErrorHandler errorHandler) :
base(serviceScopeFactory, currentTenant, errorHandler)
{
Rebus = rebus;
AbpRebusEventBusOptions = abpEventBusRebusOptions.Value;

@ -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;
}
}
}

@ -16,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.EventBus.Abstractions\Volo.Abp.EventBus.Abstractions.csproj" />
<ProjectReference Include="..\Volo.Abp.Json\Volo.Abp.Json.csproj" />
<ProjectReference Include="..\Volo.Abp.MultiTenancy\Volo.Abp.MultiTenancy.csproj" />
</ItemGroup>

@ -4,6 +4,7 @@ using System.Collections.Generic;
using Volo.Abp.EventBus.Abstractions;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Json;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
@ -12,7 +13,8 @@ namespace Volo.Abp.EventBus
{
[DependsOn(
typeof(AbpEventBusAbstractionsModule),
typeof(AbpMultiTenancyModule))]
typeof(AbpMultiTenancyModule),
typeof(AbpJsonModule))]
public class AbpEventBusModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
@ -20,6 +22,14 @@ namespace Volo.Abp.EventBus
AddEventHandlers(context.Services);
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpEventBusOptions>(options =>
{
context.Services.ExecutePreConfiguredActions(options);
});
}
private static void AddEventHandlers(IServiceCollection services)
{
var localHandlers = new List<Type>();

@ -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;
}
}

@ -19,10 +19,16 @@ namespace Volo.Abp.EventBus
protected ICurrentTenant CurrentTenant { get; }
protected EventBusBase(IServiceScopeFactory serviceScopeFactory, ICurrentTenant currentTenant)
protected IEventErrorHandler ErrorHandler { get; }
protected EventBusBase(
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IEventErrorHandler errorHandler)
{
ServiceScopeFactory = serviceScopeFactory;
CurrentTenant = currentTenant;
ErrorHandler = errorHandler;
}
/// <inheritdoc/>
@ -89,7 +95,7 @@ namespace Volo.Abp.EventBus
/// <inheritdoc/>
public abstract Task PublishAsync(Type eventType, object eventData);
public virtual async Task TriggerHandlersAsync(Type eventType, object eventData)
public virtual async Task TriggerHandlersAsync(Type eventType, object eventData, Action<EventExecutionErrorContext> onErrorAction = null)
{
var exceptions = new List<Exception>();
@ -97,16 +103,13 @@ namespace Volo.Abp.EventBus
if (exceptions.Any())
{
if (exceptions.Count == 1)
{
exceptions[0].ReThrow();
}
throw new AggregateException("More than one error has occurred while triggering the event: " + eventType, exceptions);
var context = new EventExecutionErrorContext(exceptions, eventType, this);
onErrorAction?.Invoke(context);
await ErrorHandler.HandleAsync(context);
}
}
protected virtual async Task TriggerHandlersAsync(Type eventType, object eventData, List<Exception> exceptions)
protected virtual async Task TriggerHandlersAsync(Type eventType, object eventData , List<Exception> exceptions)
{
await new SynchronizationContextRemover();

@ -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);
}
}

@ -7,9 +7,11 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Json;
namespace Volo.Abp.EventBus.Local
{
@ -28,12 +30,17 @@ namespace Volo.Abp.EventBus.Local
protected ConcurrentDictionary<Type, List<IEventHandlerFactory>> HandlerFactories { get; }
protected IJsonSerializer Serializer { get; }
public LocalEventBus(
IOptions<AbpLocalEventBusOptions> options,
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant)
: base(serviceScopeFactory, currentTenant)
ICurrentTenant currentTenant,
IEventErrorHandler errorHandler,
IJsonSerializer serializer)
: base(serviceScopeFactory, currentTenant, errorHandler)
{
Serializer = serializer;
Options = options.Value;
Logger = NullLogger<LocalEventBus>.Instance;
@ -119,19 +126,17 @@ namespace Volo.Abp.EventBus.Local
public override async Task PublishAsync(Type eventType, object eventData)
{
var exceptions = new List<Exception>();
await TriggerHandlersAsync(eventType, eventData, exceptions);
await PublishAsync(new LocalEventMessage(Guid.NewGuid(), eventData, eventType));
}
if (exceptions.Any())
public virtual async Task PublishAsync(LocalEventMessage localEventMessage)
{
var rawEventData = Serializer.Serialize(localEventMessage.EventData);
await TriggerHandlersAsync(localEventMessage.EventType, localEventMessage.EventData, errorContext =>
{
if (exceptions.Count == 1)
{
exceptions[0].ReThrow();
}
throw new AggregateException("More than one error has occurred while triggering the event: " + eventType, exceptions);
}
errorContext.EventData = Serializer.Deserialize(localEventMessage.EventType, rawEventData);
errorContext.SetProperty(nameof(LocalEventMessage.MessageId), localEventMessage.MessageId);
});
}
protected override IEnumerable<EventTypeWithEventHandlerFactories> GetHandlerFactories(Type eventType)

@ -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;
}
}
}

@ -14,8 +14,6 @@ namespace Volo.Abp.Kafka
public Action<TopicSpecification> ConfigureTopic { get; set; }
public bool ReQueue { get; set; } = true;
public AbpKafkaOptions()
{
Connections = new KafkaConnections();

@ -8,11 +8,13 @@
/// not disposed until end of the application.
/// </summary>
/// <param name="topicName"></param>
/// <param name="deadLetterTopicName"></param>
/// <param name="groupId"></param>
/// <param name="connectionName"></param>
/// <returns></returns>
IKafkaMessageConsumer Create(
string topicName,
string deadLetterTopicName,
string groupId,
string connectionName = null);
}

@ -7,5 +7,7 @@ namespace Volo.Abp.Kafka
byte[] Serialize(object obj);
object Deserialize(byte[] value, Type type);
T Deserialize<T>(byte[] value);
}
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Confluent.Kafka;
@ -26,6 +27,8 @@ namespace Volo.Abp.Kafka
protected AbpKafkaOptions Options { get; }
protected AbpAsyncTimer Timer { get; }
protected ConcurrentBag<Func<Message<string, byte[]>, Task>> Callbacks { get; }
protected IConsumer<string, byte[]> Consumer { get; private set; }
@ -36,34 +39,43 @@ namespace Volo.Abp.Kafka
protected string TopicName { get; private set; }
protected string DeadLetterTopicName { get; private set; }
public KafkaMessageConsumer(
IConsumerPool consumerPool,
IExceptionNotifier exceptionNotifier,
IOptions<AbpKafkaOptions> options,
IProducerPool producerPool)
IProducerPool producerPool,
AbpAsyncTimer timer)
{
ConsumerPool = consumerPool;
ExceptionNotifier = exceptionNotifier;
ProducerPool = producerPool;
Timer = timer;
Options = options.Value;
Logger = NullLogger<KafkaMessageConsumer>.Instance;
Callbacks = new ConcurrentBag<Func<Message<string, byte[]>, Task>>();
Timer.Period = 5000; //5 sec.
Timer.Elapsed = Timer_Elapsed;
Timer.RunOnStart = true;
}
public virtual void Initialize(
[NotNull] string topicName,
[NotNull] string deadLetterTopicName,
[NotNull] string groupId,
string connectionName = null)
{
Check.NotNull(topicName, nameof(topicName));
Check.NotNull(deadLetterTopicName, nameof(deadLetterTopicName));
Check.NotNull(groupId, nameof(groupId));
TopicName = topicName;
DeadLetterTopicName = deadLetterTopicName;
ConnectionName = connectionName ?? KafkaConnections.DefaultConnectionName;
GroupId = groupId;
AsyncHelper.RunSync(CreateTopicAsync);
Consume();
Timer.Start();
}
public virtual void OnMessageReceived(Func<Message<string, byte[]>, Task> callback)
@ -71,26 +83,45 @@ namespace Volo.Abp.Kafka
Callbacks.Add(callback);
}
protected virtual async Task Timer_Elapsed(AbpAsyncTimer timer)
{
await CreateTopicAsync();
Consume();
Timer.Stop();
}
protected virtual async Task CreateTopicAsync()
{
using (var adminClient = new AdminClientBuilder(Options.Connections.GetOrDefault(ConnectionName)).Build())
{
var topic = new TopicSpecification
var topics = new List<TopicSpecification>
{
Name = TopicName,
NumPartitions = 1,
ReplicationFactor = 1
new()
{
Name = TopicName,
NumPartitions = 1,
ReplicationFactor = 1
},
new()
{
Name = DeadLetterTopicName,
NumPartitions = 1,
ReplicationFactor = 1
}
};
Options.ConfigureTopic?.Invoke(topic);
topics.ForEach(topic =>
{
Options.ConfigureTopic?.Invoke(topic);
});
try
{
await adminClient.CreateTopicsAsync(new[] {topic});
await adminClient.CreateTopicsAsync(topics);
}
catch (CreateTopicsException e)
{
if(e.Results.First().Error.Code != ErrorCode.TopicAlreadyExists)
if(e.Results.Any(x => x.Error.Code != ErrorCode.TopicAlreadyExists))
{
throw;
}
@ -139,8 +170,6 @@ namespace Volo.Abp.Kafka
}
catch (Exception ex)
{
await RequeueAsync(consumeResult);
Logger.LogException(ex);
await ExceptionNotifier.NotifyAsync(ex);
}
@ -150,17 +179,6 @@ namespace Volo.Abp.Kafka
}
}
protected virtual async Task RequeueAsync(ConsumeResult<string, byte[]> consumeResult)
{
if (!Options.ReQueue)
{
return;
}
var producer = ProducerPool.Get(ConnectionName);
await producer.ProduceAsync(consumeResult.Topic, consumeResult.Message);
}
public virtual void Dispose()
{
if (Consumer == null)

@ -16,11 +16,12 @@ namespace Volo.Abp.Kafka
public IKafkaMessageConsumer Create(
string topicName,
string deadLetterTopicName,
string groupId,
string connectionName = null)
{
var consumer = ServiceScope.ServiceProvider.GetRequiredService<KafkaMessageConsumer>();
consumer.Initialize(topicName, groupId, connectionName);
consumer.Initialize(topicName, deadLetterTopicName, groupId, connectionName);
return consumer;
}

@ -23,5 +23,10 @@ namespace Volo.Abp.Kafka
{
return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value));
}
public T Deserialize<T>(byte[] value)
{
return _jsonSerializer.Deserialize<T>(Encoding.UTF8.GetString(value));
}
}
}

@ -6,6 +6,8 @@ namespace Volo.Abp.RabbitMQ
{
public string ExchangeName { get; }
public string DeadLetterExchangeName { get; set; }
public string Type { get; }
public bool Durable { get; set; }
@ -15,16 +17,18 @@ namespace Volo.Abp.RabbitMQ
public IDictionary<string, object> Arguments { get; }
public ExchangeDeclareConfiguration(
string exchangeName,
string type,
bool durable = false,
bool autoDelete = false)
string exchangeName,
string type,
bool durable = false,
bool autoDelete = false,
string deadLetterExchangeName = null)
{
ExchangeName = exchangeName;
DeadLetterExchangeName = deadLetterExchangeName;
Type = type;
Durable = durable;
AutoDelete = autoDelete;
Arguments = new Dictionary<string, object>();
}
}
}
}

@ -7,5 +7,7 @@ namespace Volo.Abp.RabbitMQ
byte[] Serialize(object obj);
object Deserialize(byte[] value, Type type);
T Deserialize<T>(byte[] value);
}
}

@ -6,8 +6,9 @@ namespace Volo.Abp.RabbitMQ
{
public class QueueDeclareConfiguration
{
[NotNull]
public string QueueName { get; }
[NotNull] public string QueueName { get; }
public string DeadLetterQueueName { get; set; }
public bool Durable { get; set; }
@ -18,12 +19,14 @@ namespace Volo.Abp.RabbitMQ
public IDictionary<string, object> Arguments { get; }
public QueueDeclareConfiguration(
[NotNull] string queueName,
bool durable = true,
bool exclusive = false,
bool autoDelete = false)
[NotNull] string queueName,
bool durable = true,
bool exclusive = false,
bool autoDelete = false,
string deadLetterQueueName = null)
{
QueueName = queueName;
DeadLetterQueueName = deadLetterQueueName;
Durable = durable;
Exclusive = exclusive;
AutoDelete = autoDelete;
@ -41,4 +44,4 @@ namespace Volo.Abp.RabbitMQ
);
}
}
}
}

@ -6,6 +6,7 @@ using RabbitMQ.Client.Events;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using RabbitMQ.Client.Exceptions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Threading;
@ -147,6 +148,7 @@ namespace Volo.Abp.RabbitMQ
Channel = ConnectionPool
.Get(ConnectionName)
.CreateModel();
Channel.ExchangeDeclare(
exchange: Exchange.ExchangeName,
type: Exchange.Type,
@ -155,7 +157,29 @@ namespace Volo.Abp.RabbitMQ
arguments: Exchange.Arguments
);
Channel.QueueDeclare(
if (!Exchange.DeadLetterExchangeName.IsNullOrWhiteSpace() &&
!Queue.DeadLetterQueueName.IsNullOrWhiteSpace())
{
Channel.ExchangeDeclare(
Exchange.DeadLetterExchangeName,
Exchange.Type,
Exchange.Durable,
Exchange.AutoDelete
);
Channel.QueueDeclare(
Queue.DeadLetterQueueName,
Queue.Durable,
Queue.Exclusive,
Queue.AutoDelete);
Queue.Arguments["x-dead-letter-exchange"] = Exchange.DeadLetterExchangeName;
Queue.Arguments["x-dead-letter-routing-key"] = Queue.DeadLetterQueueName;
Channel.QueueBind(Queue.DeadLetterQueueName, Exchange.DeadLetterExchangeName, Queue.DeadLetterQueueName);
}
var result = Channel.QueueDeclare(
queue: Queue.QueueName,
durable: Queue.Durable,
exclusive: Queue.Exclusive,
@ -174,6 +198,17 @@ namespace Volo.Abp.RabbitMQ
}
catch (Exception ex)
{
if (ex is OperationInterruptedException operationInterruptedException &&
operationInterruptedException.ShutdownReason.ReplyCode == 406 &&
operationInterruptedException.Message.Contains("arg 'x-dead-letter-exchange'"))
{
Exchange.DeadLetterExchangeName = null;
Queue.DeadLetterQueueName = null;
Queue.Arguments.Remove("x-dead-letter-exchange");
Queue.Arguments.Remove("x-dead-letter-routing-key");
Logger.LogWarning("Unable to bind the dead letter queue to an existing queue. You can delete the queue or add policy. See: https://www.rabbitmq.com/parameters.html");
}
Logger.LogException(ex, LogLevel.Warning);
await ExceptionNotifier.NotifyAsync(ex, logLevel: LogLevel.Warning);
}
@ -194,14 +229,10 @@ namespace Volo.Abp.RabbitMQ
{
try
{
Channel.BasicNack(
basicDeliverEventArgs.DeliveryTag,
multiple: false,
requeue: true
);
Channel.BasicReject(basicDeliverEventArgs.DeliveryTag, false);
}
catch { }
Logger.LogException(ex);
await ExceptionNotifier.NotifyAsync(ex);
}

@ -23,5 +23,10 @@ namespace Volo.Abp.RabbitMQ
{
return _jsonSerializer.Deserialize(type, Encoding.UTF8.GetString(value));
}
public T Deserialize<T>(byte[] value)
{
return _jsonSerializer.Deserialize<T>(Encoding.UTF8.GetString(value));
}
}
}
}

@ -5,6 +5,17 @@ namespace Volo.Abp.EventBus
[DependsOn(typeof(AbpEventBusModule))]
public class EventBusTestModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpEventBusOptions>(options =>
{
options.UseRetryStrategy(retryStrategyOptions =>
{
retryStrategyOptions.IntervalMillisecond = 0;
});
options.ErrorHandleSelector = type => type == typeof(MyExceptionHandleEventData);
});
}
}
}
}

@ -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…
Cancel
Save