feat: initialize Evergreen Health backend
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user