透過surging的後臺託管服務編寫任務排程並支援規則引擎自定義指令碼

fanly11發表於2022-12-21

簡介

     過去,如果在業務中需要處理任務排程的時候,大家都會使用第三方的任務排程元件,而第三方元件有一套自己的規則,在微服務的中顯得那麼格格不入,這樣就會造成程式碼臃腫,耦合性高,如果有分散式還需要搭建新的分散式環境,如果把任務排程做成元件服務,這個就完全滿足了微服務的模組化,元件化,而下面談的是在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指令碼返回為真的驗證
Skip( function(lastExecTime)) 限制任務基於一個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

 

相關文章