簡介
過去,如果在業務中需要處理任務排程的時候,大家都會使用第三方的任務排程元件,而第三方元件有一套自己的規則,在微服務的中顯得那麼格格不入,這樣就會造成程式碼臃腫,耦合性高,如果有分散式還需要搭建新的分散式環境,如果把任務排程做成元件服務,這個就完全滿足了微服務的模組化,元件化,而下面談的是在surging 中如何支援規則引擎自定義指令碼。
排程頻率設定
首先在開始之前,先看看如何透過指令碼分配多種排程計劃,先看下錶:
方法 | 描述 |
---|---|
EveryMinute() | 每分鐘執行一次任務 |
EveryFiveMinutes(); | 每五分鐘執行一次任務 |
EveryTenMinutes(); | 每十分鐘執行一次任務 |
EveryThirtyMinutes() | 每半小時執行一次任務 |
Hourly(); | 每小時執行一次任務 |
HourlyAt(10) |
每一個小時的第 10 分鐘執行一次 |
Daily() |
每到午夜執行一次任務 |
DailyAt("3:00") | 在 3:00 執行一次任務 |
TwiceDaily(1, 3) | 在 1:00 和 3:00 分別執行一次任務 |
Weekly() |
每週執行一次任務 |
Monthly() |
每月執行一次任務 |
MonthlyOn(4, "3:00") | 在每個月的第四天的 3:00 執行一次任務 |
Quarterly() |
每季度執行一次任務 |
Yearly() |
每年執行一次任務 |
Timezone("utc") |
設定utc時區 |
舉個例子,在工作日每三秒在時間8:00-23:30內執行任務。指令碼如下:
parser.TimeZone(""utc"")
.Weekdays()
.SecondAt(3)
.Between(""8:00"", ""23:30"")
額外的限制條件列表如下:
方法 | 描述 |
---|---|
Weekdays() |
限制任務在工作日 |
Sundays() |
限制任務在星期日 |
Mondays() |
限制任務在星期一 |
Tuesdays() |
限制任務在星期二 |
Wednesdays() |
限制任務在星期三 |
Thursdays() |
限制任務在星期四 |
Fridays() |
限制任務在星期五 |
Saturdays() |
限制任務在星期六 |
When( function(lastExecTime)) |
限制任務基於一個script指令碼返回為真的驗證 |
|
限制任務基於一個script指令碼返回為假的驗證 |
舉個例子,在工作日每三秒在時間8:00-23:30內執行任務。如果設定When返回為true,skip返回false 就會執行,指令碼如下:
parser.TimeZone(""utc"") .When(function(lastExecTime){ return true; }) .Skip( function(lastExecTime){ return false; }) .Weekdays() .SecondAt(3) .Between(""8:00"", ""23:30"")
然後在function 指令碼中支援DateUtils物件,可以針對lastExecTime進行邏輯判斷,比如是否是週末:DateUtils.IsWeekend(lastExecTime), 是否是今天DateUtils.IsToday(lastExecTime),程式碼如下:
parser.TimeZone(""utc"") .When(function(lastExecTime){ return DateUtils.IsToday(lastExecTime); }) .Skip( function(lastExecTime){ return DateUtils.IsWeekend(lastExecTime); }) .Weekdays() .SecondAt(3) .Between(""8:00"", ""23:30"")
編寫排程服務
surging微服務引擎是支援後臺管理託管服務的,如果要基於BackgroundService編寫任務排程,那服務就要繼承BackgroundServiceBehavior,還要繼承ISingleInstance以設定注入單例模式,
首先,建立介面服務,這樣就可以遠端新增任務,開啟關閉服務了,程式碼如下:
[ServiceBundle("Background/{Service}")] public interface IWorkService : IServiceKey { Task<bool> AddWork(Message message); Task StartAsync(); Task StopAsync(); }
然後建立業務領域服務,以下程式碼是透過規則引擎自定義指令碼設定執行頻率,並且可以設定execsize 以標識同時執行任務的大小,透過以下業務邏輯程式碼大家可以擴充套件支援持久化。
public class WorkService : BackgroundServiceBehavior, IWorkService, ISingleInstance { private readonly ILogger<WorkService> _logger; private readonly Queue<Tuple<Message, RulesEngine.RulesEngine, SchedulerRuleWorkflow>> _queue = new Queue<Tuple<Message, RulesEngine.RulesEngine, SchedulerRuleWorkflow>>(); private readonly ConcurrentDictionary<string, DateTime> _keyValuePairs = new ConcurrentDictionary<string, DateTime>(); private readonly IServiceProxyProvider _serviceProxyProvider; private AtomicLong _atomic=new AtomicLong(1); private const int EXECSIZE = 1; private CancellationToken _token; public WorkService(ILogger<WorkService> logger, IServiceProxyProvider serviceProxyProvider) { _logger = logger; _serviceProxyProvider = serviceProxyProvider; /* var script = @"parser .Weekdays().SecondAt(3).Between(""8:00"", ""22:00"")";*/ var script = @"parser .TimeZone(""utc"") .When( function(lastExecTime){ return DateUtils.IsToday(lastExecTime); }).Skip( function(lastExecTime){ return DateUtils.IsWeekend(lastExecTime); }).Weekdays().SecondAt(3).Between(""8:00"", ""23:30"")"; var ruleWorkflow = GetSchedulerRuleWorkflow(script); var messageId = Guid.NewGuid().ToString(); _keyValuePairs.AddOrUpdate(messageId, DateTime.Now, (key, value) => DateTime.Now); _queue.Enqueue(new Tuple<Message, RulesEngine.RulesEngine, SchedulerRuleWorkflow>(new Message() { MessageId= messageId,Config=new SchedulerConfig() { IsPersistence=true} }, GetRuleEngine(ruleWorkflow), ruleWorkflow)); } public Task<bool> AddWork(Message message) { var ruleWorkflow = GetSchedulerRuleWorkflow(message.Config.Script); _keyValuePairs.AddOrUpdate(message.MessageId, DateTime.Now, (key, value) => DateTime.Now); _queue.Enqueue(new Tuple<Message, RulesEngine.RulesEngine, SchedulerRuleWorkflow>(message, GetRuleEngine(ruleWorkflow), ruleWorkflow)); return Task.FromResult(true); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { try { _token = stoppingToken; _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); _queue.TryDequeue(out Tuple<Message, RulesEngine.RulesEngine, SchedulerRuleWorkflow>? message); if (message != null) { var parser = await GetParser(message.Item3, message.Item2); await PayloadSubscribe(parser, message.Item1, message.Item2, message.Item3); _keyValuePairs.TryGetValue(message.Item1.MessageId, out DateTime dateTime); parser.Build(dateTime == DateTime.MinValue ? DateTime.Now : dateTime); } if (!_token.IsCancellationRequested && (message == null || _atomic.GetAndAdd(1) == EXECSIZE)) { _atomic = new AtomicLong(1); await Task.Delay(1000, stoppingToken); } } catch (Exception ex){ _logger.LogError("WorkService execute error, message:{message} ,trace info:{trace} ", ex.Message, ex.StackTrace); } } public async Task StartAsync() { if (_token.IsCancellationRequested) { await base.StartAsync(_token); } } public async Task StopAsync() { if (!_token.IsCancellationRequested) { await base.StopAsync(_token); } } private async Task PayloadSubscribe(RulePipePayloadParser parser, Message message, RulesEngine.RulesEngine rulesEngine, SchedulerRuleWorkflow ruleWorkflow) { parser.HandlePayload().Subscribe(async (temperature) => { try { if (temperature) { await ExecuteByPlanAsyn(message); _logger.LogInformation("Worker exec at: {time}", DateTimeOffset.Now); } } catch (Exception ex) { } finally { if (message.Config.IsPersistence || (!temperature && !message.Config.IsPersistence)) _queue.Enqueue(new Tuple<Message, RulesEngine.RulesEngine, SchedulerRuleWorkflow>(message, rulesEngine, ruleWorkflow)); } }); } private async Task<bool> ExecuteByPlanAsyn(Message message) { var result = false; var isExec = true; try { if (!string.IsNullOrEmpty(message.RoutePath)) { var serviceResult = await _serviceProxyProvider.Invoke<object>(message.Parameters, message.RoutePath, message.ServiceKey); bool.TryParse(serviceResult?.ToString(), out result); isExec = true; } } catch { } finally { if (isExec && message.Config.IsPersistence) _keyValuePairs.AddOrUpdate(message.MessageId, DateTime.Now, (key, value) => DateTime.Now); else if (!message.Config.IsPersistence) _keyValuePairs.TryRemove(message.MessageId, out DateTime dateTime); } return result; } private async Task<RulePipePayloadParser> GetParser(SchedulerRuleWorkflow ruleWorkflow, RulesEngine.RulesEngine engine) { var payloadParser = new RulePipePayloadParser(); var ruleResult = await engine.ExecuteActionWorkflowAsync(ruleWorkflow.WorkflowName, ruleWorkflow.RuleName, new RuleParameter[] { new RuleParameter("parser", payloadParser) }); if (ruleResult.Exception != null && _logger.IsEnabled(LogLevel.Error)) _logger.LogError(ruleResult.Exception, ruleResult.Exception.Message); return payloadParser; } private RulesEngine.RulesEngine GetRuleEngine(SchedulerRuleWorkflow ruleWorkFlow) { var reSettingsWithCustomTypes = new ReSettings { CustomTypes = new Type[] { typeof(RulePipePayloadParser) } }; var result = new RulesEngine.RulesEngine(new Workflow[] { ruleWorkFlow.GetWorkflow() }, null, reSettingsWithCustomTypes); return result; } private SchedulerRuleWorkflow GetSchedulerRuleWorkflow(string script) { var result = new SchedulerRuleWorkflow("1==1"); if (!string.IsNullOrEmpty(script)) { result = new SchedulerRuleWorkflow(script); } return result; } }
總結
因為工作繁忙,微服務平臺暫時擱置,等公司基於surging 的物聯網專案上線後,再投入時間研發,surging 一直開發中未曾放棄,也許你沒看到的版本才是最強大的。之前的QQ群被封了,如果感興趣可以加:744677125
開源地址:https://github.com/fanliang11/surging