Merge pull request #844 from hchen2020/master

Support distributed crontab workers
This commit is contained in:
Haiping 2025-01-28 21:34:29 +08:00 committed by GitHub
commit c2f4de14fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 104 additions and 22 deletions

View file

@ -34,6 +34,8 @@ public abstract class AgentHookBase : IAgentHook
dict["current_date"] = $"{DateTime.Now:MMM dd, yyyy}";
dict["current_time"] = $"{DateTime.Now:hh:mm tt}";
dict["current_weekday"] = $"{DateTime.Now:dddd}";
dict["current_utc_datetime"] = $"{DateTime.UtcNow}";
return true;
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>

View file

@ -1,4 +1,4 @@
namespace BotSharp.Core.Rules.Triggers;
namespace BotSharp.Abstraction.Rules;
public interface IRuleTrigger
{

View file

@ -2,6 +2,9 @@ namespace BotSharp.Core.Crontab.Abstraction;
public interface ICrontabHook
{
string[]? Triggers
=> null;
Task OnCronTriggered(CrontabItem item)
=> Task.CompletedTask;

View file

@ -34,5 +34,6 @@ public class CrontabPlugin : IBotSharpPlugin
services.AddScoped<IAgentUtilityHook, CrontabUtilityHook>();
services.AddScoped<ICrontabService, CrontabService>();
services.AddHostedService<CrontabWatcher>();
services.AddHostedService<CrontabEventSubscription>();
}
}

View file

@ -0,0 +1,59 @@
using BotSharp.Abstraction.Infrastructures.Events;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Runtime.InteropServices;
namespace BotSharp.Core.Crontab.Services;
public class CrontabEventSubscription : BackgroundService
{
private readonly ILogger _logger;
private readonly IServiceProvider _services;
public CrontabEventSubscription(IServiceProvider services, ILogger<CrontabEventSubscription> logger)
{
_logger = logger;
_services = services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Crontab event subscription background service is running.");
using (var scope = _services.CreateScope())
{
var subscriber = scope.ServiceProvider.GetRequiredService<IEventSubscriber>();
var cron = scope.ServiceProvider.GetRequiredService<ICrontabService>();
var crons = await cron.GetCrontable();
foreach (var item in crons)
{
_ = Task.Run(async () =>
{
await subscriber.SubscribeAsync($"Crontab:{item.Title}",
"Crontab",
port: 0,
priorityEnabled: false, async (sender, args) =>
{
var scope = _services.CreateScope();
cron = scope.ServiceProvider.GetRequiredService<ICrontabService>();
await cron.ScheduledTimeArrived(item);
}, stoppingToken: stoppingToken);
});
}
}
/*using (var scope = _services.CreateScope())
{
var cron = scope.ServiceProvider.GetRequiredService<ICrontabService>();
var crons = await cron.GetCrontable();
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(1000, stoppingToken);
}
}*/
}
}

View file

@ -55,12 +55,15 @@ public class CrontabService : ICrontabService
public async Task ScheduledTimeArrived(CrontabItem item)
{
_logger.LogDebug($"ScheduledTimeArrived {item}");
await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
{
await hook.OnTaskExecuting(item);
await hook.OnCronTriggered(item);
await hook.OnTaskExecuted(item);
if (hook.Triggers == null || hook.Triggers.Contains(item.Title))
{
await hook.OnTaskExecuting(item);
await hook.OnCronTriggered(item);
await hook.OnTaskExecuted(item);
}
});
}
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Infrastructures;
using BotSharp.Abstraction.Infrastructures.Events;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NCrontab;
@ -26,9 +27,9 @@ public class CrontabWatcher : BackgroundService
while (!stoppingToken.IsCancellationRequested)
{
var delay = Task.Delay(1000 * 10, stoppingToken);
var delay = Task.Delay(1000, stoppingToken);
await locker.LockAsync("CrontabWatcher", async () =>
await locker.LockAsync("CrontabWatcher:locker", async () =>
{
await RunCronChecker(scope.ServiceProvider);
});
@ -44,8 +45,13 @@ public class CrontabWatcher : BackgroundService
{
var cron = services.GetRequiredService<ICrontabService>();
var crons = await cron.GetCrontable();
var publisher = services.GetRequiredService<IEventPublisher>();
foreach (var item in crons)
{
_logger.LogDebug($"[{DateTime.UtcNow}] Cron task ({item.Title}, {item.Cron}), Last Execution Time: {item.LastExecutionTime}");
try
{
// strip seconds from cron expression
@ -80,8 +86,10 @@ public class CrontabWatcher : BackgroundService
if (matches)
{
_logger.LogDebug($"The current time matches the cron expression {item}");
cron.ScheduledTimeArrived(item);
_logger.LogInformation($"The current time matches the cron expression {item}");
await publisher.PublishAsync($"Crontab:{item.Title}", item.Cron);
// cron.ScheduledTimeArrived(item);
}
}
catch (Exception ex)
@ -97,9 +105,9 @@ public class CrontabWatcher : BackgroundService
var nextOccurrence = schedule.GetNextOccurrence(DateTime.UtcNow);
var afterNextOccurrence = schedule.GetNextOccurrence(nextOccurrence);
var interval = afterNextOccurrence - nextOccurrence;
if (interval.TotalMinutes < 10)
if (interval.TotalMinutes < 1)
{
throw new ArgumentException("The minimum interval must be at least 10 minutes.");
throw new ArgumentException("The minimum interval must be at least 1 minutes.");
}
return nextOccurrence - interval;
}

View file

@ -1,5 +1,3 @@
using BotSharp.Core.Rules.Triggers;
namespace BotSharp.Core.Rules.Engines;
public interface IRuleEngine

View file

@ -1,10 +1,7 @@
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Enums;
using BotSharp.Abstraction.Models;
using BotSharp.Abstraction.Repositories.Filters;
using BotSharp.Abstraction.Routing;
using BotSharp.Abstraction.Utilities;
using BotSharp.Core.Rules.Triggers;
using Microsoft.Extensions.Logging;
using System.Data;

View file

@ -6,4 +6,6 @@ global using BotSharp.Abstraction.Plugins;
global using BotSharp.Abstraction.Agents;
global using BotSharp.Abstraction.Conversations.Models;
global using BotSharp.Abstraction.Instructs;
global using BotSharp.Abstraction.Instructs.Models;
global using BotSharp.Abstraction.Instructs.Models;
global using BotSharp.Abstraction.Rules;

View file

@ -33,7 +33,7 @@ public class RedisPublisher : IEventPublisher
if (CheckMessageExists(db, channel, "message", message))
{
_logger.LogError($"The message already exists {channel} {message}");
_logger.LogInformation($"The message already exists {channel} {message}");
return null;
}
@ -84,7 +84,8 @@ public class RedisPublisher : IEventPublisher
return
[
new NameValueEntry("message", message),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")),
new NameValueEntry("machine", Environment.MachineName)
];
}
@ -94,6 +95,7 @@ public class RedisPublisher : IEventPublisher
[
new NameValueEntry("message", message),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o")),
new NameValueEntry("machine", Environment.MachineName),
new NameValueEntry("error", error)
];
}

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Rules;
using BotSharp.Core.Rules.Triggers;
namespace BotSharp.OpenAPI.Controllers;

View file

@ -188,6 +188,11 @@ public class HandleHttpRequestFn : IFunctionCallback
{
if (response == null) return string.Empty;
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
return response.ReasonPhrase ?? "http call has error occurred.";
}
return await response.Content.ReadAsStringAsync();
}
}

View file

@ -65,7 +65,7 @@ namespace BotSharp.Plugin.Twilio.Services
var httpContext = sp.GetRequiredService<IHttpContextAccessor>();
httpContext.HttpContext = new DefaultHttpContext();
httpContext.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
foreach (var header in message.RequestHeaders)
foreach (var header in message.RequestHeaders ?? [])
{
httpContext.HttpContext.Request.Headers[header.Key] = header.Value;
}
@ -81,8 +81,9 @@ namespace BotSharp.Plugin.Twilio.Services
InitProgressService(message, sessionManager, progressService);
InitConversation(message, inputMsg, conv, routing);
// Need to consider Inbound and Outbound call
var conversation = await conv.GetConversation(message.ConversationId);
var agentId = string.IsNullOrWhiteSpace(conversation.AgentId) ? config.AgentId : conversation.AgentId;
var agentId = string.IsNullOrWhiteSpace(conversation?.AgentId) ? config.AgentId : conversation.AgentId;
var result = await conv.SendMessage(agentId,
inputMsg,