feat: initialize Evergreen Health backend

This commit is contained in:
fuenmao
2026-09-21 14:08:22 +08:00
commit 8553df5dd2
493 changed files with 75649 additions and 0 deletions
@@ -0,0 +1,58 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 事件订阅
/// </summary>
public class AppEventSubscriber : IEventSubscriber, ISingleton, IDisposable
{
private static readonly ISugarQueryable<SysTenant> SysTenantQueryable = App.GetService<ISqlSugarClient>().Queryable<SysTenant>();
private readonly IServiceScope _serviceScope;
public AppEventSubscriber(IServiceScopeFactory scopeFactory)
{
_serviceScope = scopeFactory.CreateScope();
}
/// <summary>
/// 增加异常日志
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[EventSubscribe(CommonConst.AddExLog)]
public async Task CreateExLog(EventHandlerExecutingContext context)
{
// 切换日志独立数据库
var db = SqlSugarSetup.ITenant.IsAnyConnection(SqlSugarConst.LogConfigId)
? SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.LogConfigId)
: SqlSugarSetup.ITenant.GetConnectionScope(SqlSugarConst.MainConfigId);
await db.CopyNew().Insertable((SysLogEx)context.Source.Payload).ExecuteCommandAsync();
}
/// <summary>
/// 发送异常邮件
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[EventSubscribe(CommonConst.SendErrorMail)]
public async Task SendOrderErrorMail(EventHandlerExecutingContext context)
{
long.TryParse(App.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value, out var tenantId);
var tenant = await SysTenantQueryable.FirstAsync(t => t.Id == tenantId);
var title = $"{tenant?.Title} 系统异常";
await _serviceScope.ServiceProvider.GetRequiredService<SysEmailService>().SendEmail(JSON.Serialize(context.Source.Payload), title);
}
/// <summary>
/// 释放服务作用域
/// </summary>
public void Dispose()
{
_serviceScope.Dispose();
}
}
+118
View File
@@ -0,0 +1,118 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// Redis 消息扩展
/// </summary>
/// <typeparam name="T"></typeparam>
public class EventConsumer<T> : IDisposable
{
/// <summary>
///
/// </summary>
private Task _consumerTask;
/// <summary>
///
/// </summary>
private CancellationTokenSource _consumerCts;
/// <summary>
/// 消费者
/// </summary>
public IProducerConsumer<T> Consumer { get; }
/// <summary>
/// 消息回调
/// </summary>
public event EventHandler<T> Received;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="consumer"></param>
public EventConsumer(IProducerConsumer<T> consumer) => Consumer = consumer;
/// <summary>
/// 启动
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public void Start()
{
if (Consumer is null)
{
throw new InvalidOperationException("Subscribe first using the Consumer.Subscribe() function");
}
if (_consumerTask != null)
{
return;
}
_consumerCts = new CancellationTokenSource();
var ct = _consumerCts.Token;
_consumerTask = Task.Factory.StartNew(async () =>
{
while (!ct.IsCancellationRequested)
{
try
{
var cr = Consumer.TakeOne(10);
if (cr == null) continue;
Received?.Invoke(this, cr);
}
catch (Exception ex)
{
Console.WriteLine($"消息消费异常: {ex.Message}");
await Task.Delay(1000); // 短暂等待后继续尝试
}
}
}, ct, TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
/// <summary>
/// 停止
/// </summary>
/// <returns></returns>
public async Task Stop()
{
if (_consumerCts == null || _consumerTask == null) return;
_consumerCts.Cancel();
try
{
await _consumerTask;
}
finally
{
_consumerTask = null;
_consumerCts = null;
}
}
/// <summary>
/// 释放
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 释放
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_consumerTask != null)
{
Stop().Wait();
}
}
}
}
@@ -0,0 +1,28 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
public class EventHandlerMonitor : IEventHandlerMonitor
{
public Task OnExecutingAsync(EventHandlerExecutingContext context)
{
//_logger.LogInformation("执行之前:{EventId}", context.Source.EventId);
return Task.CompletedTask;
}
public Task OnExecutedAsync(EventHandlerExecutedContext context)
{
//_logger.LogInformation("执行之后:{EventId}", context.Source.EventId);
if (context.Exception != null)
{
Log.Error($"EventHandlerMonitor.OnExecutedAsync 执行出错啦:{context.Source.EventId}", context.Exception);
}
return Task.CompletedTask;
}
}
@@ -0,0 +1,146 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Threading.Channels;
namespace Admin.NET.Core;
/// <summary>
/// RabbitMQ自定义事件源存储器
/// </summary>
public class RabbitMQEventSourceStore : IEventSourceStorer, IDisposable
{
/// <summary>
/// 内存通道事件源存储器
/// </summary>
private Channel<IEventSource> _channelEventSource;
/// <summary>
/// 路由键
/// </summary>
private string _routeKey;
/// <summary>
/// 连接对象
/// </summary>
private IConnection _connection;
/// <summary>
/// 通道对象
/// </summary>
private IChannel _channel;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="factory">连接工厂</param>
/// <param name="routeKey">路由键</param>
/// <param name="capacity">存储器最多能够处理多少消息,超过该容量进入等待写入</param>
public RabbitMQEventSourceStore(ConnectionFactory factory, string routeKey, int capacity)
{
InitEventSourceStore(factory, routeKey, capacity).GetAwaiter().GetResult();
}
/// <summary>
/// 初始化事件源存储器
/// </summary>
/// <param name="factory">连接工厂</param>
/// <param name="routeKey">路由键</param>
/// <param name="capacity">存储器最多能够处理多少消息,超过该容量进入等待写入</param>
private async Task InitEventSourceStore(ConnectionFactory factory, string routeKey, int capacity)
{
// 配置通道(超出默认容量后进入等待)
var boundedChannelOptions = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
// 创建有限容量通道
_channelEventSource = Channel.CreateBounded<IEventSource>(boundedChannelOptions);
// 创建连接
_connection = await factory.CreateConnectionAsync();
// 路由键名
_routeKey = routeKey;
// 创建通道
_channel = await _connection.CreateChannelAsync();
// 声明路由队列
await _channel.QueueDeclareAsync(routeKey, false, false, false, null);
// 创建消息订阅者
var consumer = new AsyncEventingBasicConsumer(_channel);
// 订阅消息并写入内存 Channel
consumer.ReceivedAsync += async (ch, ea) =>
{
// 读取原始消息
var stringEventSource = Encoding.UTF8.GetString(ea.Body.ToArray());
// 转换为 IEventSource,如果自定义了 EventSource,注意属性是可读可写
var eventSource = JSON.Deserialize<ChannelEventSource>(stringEventSource);
// 写入内存管道存储器
await _channelEventSource.Writer.WriteAsync(eventSource);
// 确认该消息已被消费
await _channel.BasicAckAsync(ea.DeliveryTag, false);
};
// 启动消费者且设置为手动应答消息
await _channel.BasicConsumeAsync(routeKey, false, consumer);
}
/// <summary>
/// 将事件源写入存储器
/// </summary>
/// <param name="eventSource">事件源对象</param>
/// <param name="cancellationToken">取消任务 Token</param>
/// <returns><see cref="ValueTask"/></returns>
public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken)
{
if (eventSource == default)
throw new ArgumentNullException(nameof(eventSource));
// 判断是否是 ChannelEventSource 或自定义的 EventSource
if (eventSource is ChannelEventSource source)
{
// 序列化及发布
var data = Encoding.UTF8.GetBytes(JSON.Serialize(source));
var props = new BasicProperties();
props.ContentType = "text/plain";
props.DeliveryMode = DeliveryModes.Persistent;
await _channel.BasicPublishAsync("", _routeKey, false, props, data);
}
else
{
// 处理动态订阅
await _channelEventSource.Writer.WriteAsync(eventSource, cancellationToken);
}
}
/// <summary>
/// 从存储器中读取一条事件源
/// </summary>
/// <param name="cancellationToken">取消任务 Token</param>
/// <returns>事件源对象</returns>
public async ValueTask<IEventSource> ReadAsync(CancellationToken cancellationToken)
{
var eventSource = await _channelEventSource.Reader.ReadAsync(cancellationToken);
return eventSource;
}
/// <summary>
/// 释放非托管资源
/// </summary>
public void Dispose()
{
_channel.Dispose();
_connection.Dispose();
}
}
@@ -0,0 +1,165 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using NewLife.Caching.Queues;
using Newtonsoft.Json;
using System.Threading.Channels;
namespace Admin.NET.Core;
/// <summary>
/// Redis自定义事件源存储器
/// </summary>
/// <remarks>
/// 在集群部署时,一般每一个消息只由一个服务节点消费一次。
/// 有些特殊情情要通知到服务器群中的每一个节点(比如需要强制加载某些配置、重点服务等),
/// 在这种情况下就要以“broadcast:”开头来定义EventId,
/// 本系统会把“broadcast:”开头的事件视为“广播消息”保证集群中的每一个服务节点都能消费得到这个消息
/// </remarks>
public sealed class RedisEventSourceStorer : IEventSourceStorer, IDisposable
{
/// <summary>
/// 消费者
/// </summary>
private readonly EventConsumer<ChannelEventSource> _eventConsumer;
/// <summary>
/// 内存通道事件源存储器
/// </summary>
private readonly Channel<IEventSource> _channel;
private IProducerConsumer<ChannelEventSource> _queueSingle;
private RedisStream<string> _queueBroadcast;
private ILogger<RedisEventSourceStorer> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="cacheProvider">Redis 连接对象</param>
/// <param name="routeKey">路由键</param>
/// <param name="capacity">存储器最多能够处理多少消息,超过该容量进入等待写入</param>
public RedisEventSourceStorer(ICacheProvider cacheProvider, string routeKey, int capacity)
{
_logger = App.GetRequiredService<ILogger<RedisEventSourceStorer>>();
// 配置通道,设置超出默认容量后进入等待
var boundedChannelOptions = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
// 创建有限容量通道
_channel = Channel.CreateBounded<IEventSource>(boundedChannelOptions);
//_redis = redis as FullRedis;
// 创建广播消息订阅者,即所有服务器节点都能收到消息(用来发布重启、Reload配置等消息)
FullRedis redis = (FullRedis)cacheProvider.Cache;
var clusterOpt = App.GetConfig<ClusterOptions>("Cluster", true);
_queueBroadcast = redis.GetStream<string>(routeKey + ":broadcast");
_queueBroadcast.Group = clusterOpt.ServerId;//根据服务器标识分配到不同的分组里
_queueBroadcast.Expire = TimeSpan.FromSeconds(10);//消息10秒过期()
_queueBroadcast.ConsumeAsync(OnConsumeBroadcast);
// 创建队列消息订阅者,只要有一个服务节点消费了消息即可
_queueSingle = redis.GetQueue<ChannelEventSource>(routeKey + ":single");
_eventConsumer = new EventConsumer<ChannelEventSource>(_queueSingle);
// 订阅消息写入 Channel
_eventConsumer.Received += async (send, cr) =>
{
// var oriColor = Console.ForegroundColor;
try
{
ChannelEventSource ces = (ChannelEventSource)cr;
await ConsumeChannelEventSourceAsync(ces, ces.CancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "处理Received中的消息产生错误!");
}
};
_eventConsumer.Start();
}
private async Task OnConsumeBroadcast(string source, Message message, CancellationToken token)
{
ChannelEventSource ces = JsonConvert.DeserializeObject<ChannelEventSource>(source);
await ConsumeChannelEventSourceAsync(ces, token);
}
private async Task ConsumeChannelEventSourceAsync(ChannelEventSource ces, CancellationToken cancel = default)
{
// 打印测试事件
if (ces.EventId != null && ces.EventId.IndexOf(":Test") > 0)
{
var oriColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"有消息要处理{ces.EventId},{ces.Payload}");
Console.ForegroundColor = oriColor;
}
await _channel.Writer.WriteAsync(ces, cancel);
}
/// <summary>
/// 将事件源写入存储器
/// </summary>
/// <param name="eventSource">事件源对象</param>
/// <param name="cancellationToken">取消任务 Token</param>
/// <returns><see cref="ValueTask"/></returns>
public async ValueTask WriteAsync(IEventSource eventSource, CancellationToken cancellationToken)
{
// 空检查
if (eventSource == default)
throw new ArgumentNullException(nameof(eventSource));
// 这里判断是否是 ChannelEventSource 或者 自定义的 EventSource
if (eventSource is ChannelEventSource source)
{
// 异步发布
await Task.Factory.StartNew(() =>
{
if (source.EventId != null && source.EventId.StartsWith("broadcast:"))
{
string str = JsonConvert.SerializeObject(source);
_queueBroadcast.Add(str);
}
else
{
_queueSingle.Add(source);
}
}, cancellationToken, TaskCreationOptions.LongRunning, System.Threading.Tasks.TaskScheduler.Default);
}
else
{
// 处理动态订阅问题
await _channel.Writer.WriteAsync(eventSource, cancellationToken);
}
}
/// <summary>
/// 从存储器中读取一条事件源
/// </summary>
/// <param name="cancellationToken">取消任务 Token</param>
/// <returns>事件源对象</returns>
public async ValueTask<IEventSource> ReadAsync(CancellationToken cancellationToken)
{
// 读取一条事件源
var eventSource = await _channel.Reader.ReadAsync(cancellationToken);
return eventSource;
}
/// <summary>
/// 释放非托管资源
/// </summary>
public async void Dispose()
{
await _eventConsumer.Stop();
GC.SuppressFinalize(this);
}
}
+202
View File
@@ -0,0 +1,202 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using NewLife.Caching.Queues;
namespace Admin.NET.Core;
/// <summary>
/// Redis 消息队列
/// </summary>
public static class RedisQueue
{
private static ICacheProvider _cacheProvider = App.GetRequiredService<ICacheProvider>();
/// <summary>创建Redis消息队列。默认消费一次,指定消费者group时使用STREAM结构,支持多消费组共享消息</summary>
/// <remarks>
/// 使用队列时,可根据是否设置消费组来决定使用简单队列还是完整队列。 简单队列(如RedisQueue)可用作命令队列,Topic很多,但几乎没有消息。 完整队列(如RedisStream)可用作消息队列,Topic很少,但消息很多,并且支持多消费组。
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <param name="topic">主题</param>
/// <param name="group">消费组。未指定消费组时使用简单队列(如RedisQueue),指定消费组时使用完整队列(如RedisStream)</param>
/// <returns></returns>
public static IProducerConsumer<T> GetQueue<T>(String topic, String group = null)
{
// 队列需要单列
var key = $"myStream:{topic}";
if (_cacheProvider.InnerCache.TryGetValue<IProducerConsumer<T>>(key, out var queue)) return queue;
queue = _cacheProvider.GetQueue<T>(topic, group);
_cacheProvider.Cache.Set(key, queue);
return queue;
}
/// <summary>
/// 获取可信队列,需要确认
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="topic"></param>
/// <returns></returns>
public static RedisReliableQueue<T> GetRedisReliableQueue<T>(string topic)
{
// 队列需要单列
var key = $"myQueue:{topic}";
if (_cacheProvider.InnerCache.TryGetValue<RedisReliableQueue<T>>(key, out var queue)) return queue;
queue = (_cacheProvider.Cache as FullRedis).GetReliableQueue<T>(topic);
_cacheProvider.Cache.Set(key, queue);
return queue;
}
/// <summary>
/// 可信队列回滚
/// </summary>
/// <param name="topic"></param>
/// <param name="retryInterval"></param>
/// <returns></returns>
public static int RollbackAllAck(string topic, int retryInterval = 60)
{
var queue = GetRedisReliableQueue<string>(topic);
queue.RetryInterval = retryInterval;
return queue.RollbackAllAck();
}
/// <summary>
/// 发送一个数据列表到可信队列
/// </summary>
/// <param name="topic"></param>
/// <param name="value"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static int AddReliableQueueList<T>(string topic, List<T> value)
{
var queue = GetRedisReliableQueue<T>(topic);
var count = queue.Count;
var result = queue.Add(value.ToArray());
return result - count;
}
/// <summary>
/// 发送一条数据到可信队列
/// </summary>
/// <param name="topic"></param>
/// <param name="value"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static int AddReliableQueue<T>(string topic, T value)
{
var queue = GetRedisReliableQueue<T>(topic);
var count = queue.Count;
var result = queue.Add(value);
return result - count;
}
/// <summary>
/// 获取延迟队列
/// </summary>
/// <param name="topic"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static RedisDelayQueue<T> GetDelayQueue<T>(string topic)
{
// 队列需要单列
var key = $"myDelay:{topic}";
if (_cacheProvider.InnerCache.TryGetValue<RedisDelayQueue<T>>(key, out var queue)) return queue;
queue = (_cacheProvider.Cache as FullRedis).GetDelayQueue<T>(topic);
_cacheProvider.Cache.Set(key, queue);
return queue;
}
/// <summary>
/// 发送一条数据到延迟队列
/// </summary>
/// <param name="topic"></param>
/// <param name="value"></param>
/// <param name="delay">延迟时间。单位秒</param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static int AddDelayQueue<T>(string topic, T value, int delay)
{
var queue = GetDelayQueue<T>(topic);
return queue.Add(value, delay);
}
/// <summary>
/// 发送数据列表到延迟队列
/// </summary>
/// <param name="topic"></param>
/// <param name="value"></param>
/// <param name="delay"></param>
/// <typeparam name="T">延迟时间。单位秒</typeparam>
/// <returns></returns>
public static int AddDelayQueue<T>(string topic, List<T> value, int delay)
{
var queue = GetDelayQueue<T>(topic);
queue.Delay = delay;
return queue.Add(value.ToArray());
}
/// <summary>
/// 在可信队列获取一条数据
/// </summary>
/// <param name="topic"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T ReliableTakeOne<T>(string topic)
{
var queue = GetRedisReliableQueue<T>(topic);
return queue.TakeOne(1);
}
/// <summary>
/// 异步在可信队列获取一条数据
/// </summary>
/// <param name="topic"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static async Task<T> ReliableTakeOneAsync<T>(string topic)
{
var queue = GetRedisReliableQueue<T>(topic);
return await queue.TakeOneAsync(1);
}
/// <summary>
/// 在可信队列获取多条数据
/// </summary>
/// <param name="topic"></param>
/// <param name="count"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static List<T> ReliableTake<T>(string topic, int count)
{
var queue = GetRedisReliableQueue<T>(topic);
return queue.Take(count).ToList();
}
/// <summary>
/// 申请分布式锁
/// </summary>
/// <param name="key">要锁定的key</param>
/// <param name="msTimeout">申请锁等待的时间,单位毫秒</param>
/// <param name="msExpire">锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒</param>
/// <param name="throwOnFailure">失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败</param>
/// <returns></returns>
public static IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true)
{
try
{
return _cacheProvider.Cache.AcquireLock(key, msTimeout, msExpire, throwOnFailure);
}
catch
{
return null;
}
}
}
@@ -0,0 +1,44 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 事件执行器-超时控制、失败重试熔断等等
/// </summary>
public class RetryEventHandlerExecutor : IEventHandlerExecutor
{
public async Task ExecuteAsync(EventHandlerExecutingContext context, Func<EventHandlerExecutingContext, Task> handler)
{
var eventSubscribeAttribute = context.Attribute;
// 判断是否自定义了重试失败回调服务
var fallbackPolicyService = eventSubscribeAttribute?.FallbackPolicy == null
? null
: App.GetRequiredService(eventSubscribeAttribute.FallbackPolicy) as IEventFallbackPolicy;
await Retry.InvokeAsync(async () =>
{
try
{
await handler(context);
}
catch (Exception ex)
{
Log.Error($"Invoke EventHandler {context.Source.EventId} Error", ex);
throw;
}
}
, eventSubscribeAttribute?.NumRetries ?? 0
, eventSubscribeAttribute?.RetryTimeout ?? 1000
, exceptionTypes: eventSubscribeAttribute?.ExceptionTypes
, fallbackPolicy: fallbackPolicyService == null ? null : async (Exception ex) => { await fallbackPolicyService.CallbackAsync(context, ex); }
, retryAction: (total, times) =>
{
// 输出重试日志
Log.Warning($"Retrying {times}/{total} times for EventHandler {context.Source.EventId}");
});
}
}