说下hangfire吧

最近因工作需要开发计划任务模块(严格来说应该是修改bug吧,其他同事负责的)接触到了Hangfire。早前听同事说hangfire有点坑,怀着好奇,趁这两天bug改的差不多了,在github上面down了hangfire源码,下面分享一下,自己读hangfire源码的一些理解,和工作中需要注意的地方。介绍大概分为以下几个部分吧。1.准备工作,2.简单使用,3.源码分析,4.避坑。需要说明一下接触hangfire源码的时间不长,也就几天时间理解不到位,或者说错了的,希望在评论指正。
准备工作:hangfire源代码的代码量不多,github地址: https://github.com/HangfireIO/Hangfire,有兴趣的朋友可以自己下载瞅瞅源码。功能上大概可以分为客户端模式和服务端模式。用到的技术大概有Multi Thread、Expression、Dapper、Cron等。可以这么说,它的定时任务完全就是基于多线程协作实现的。因为是多线程环境,所以个人觉得看起来有点费力。
简单使用:.Net&.Net Core环境都可以使用,下面就以.Net Core的使用为例。
1.客户端和服务端独立部署
client端
 1 public IServiceProvider ConfigureServices(IServiceCollection services)
 2         {
 3             // 其他代码
 4              services.AddHangfire(config =>
 5             {
 6                  config.UseSqlServerStorage(...);
 7             });
 8         }
 9  
10 public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
11         {
12             // 其他代码...
13             // 启用Dashboard看板
14             app.UseHangfireDashboard();
15         }

 

server端
 1 public void Configuration(IAppBuilder app)
 2         {
 3             GlobalConfiguration.Configuration
 4                  .UseSqlServerStorage("连接字符串", new SqlServerStorageOptions
 5                 {
 6                     // options
 7                 });
 8             app.UseHangfireServer(new BackgroundJobServerOptions
 9             {
10             });
11         }
12  
13  
或者
1 services.AddHangfireServer(options =>
2                     {
3                         // 基于IHostedService接口实现
4                     });
PS:server端还有一种实现方式,实现IHostedService接口 其实跟上面的使用方法一样的,注入到服务就ok,在程序启动阶段会自动执行IHostedService接口的两个方法,可以简单看下IHostedService接口的定义。
1   public interface IHostedService
2   {
3     Task StartAsync(CancellationToken cancellationToken);
4     Task StopAsync(CancellationToken cancellationToken);
5   }
接口就定义了两个方法,start是在程序启动的时候执行,当然stop就是在程序停止的时候执行。我们用一张图简单描绘一下它的执行时机,图是盗的。
以上就是hangfire的client端和server端分开部署的一个简单应用,下面我们看下第二种,client&server部署在同一台机器上。
2.客户端和服务端统一部署
1 public void Configuration(IAppBuilder app)
2 {
3     GlobalConfiguration.Configuration.UseSqlServerStorage(); // 配置数据库连接
4     
5     app.UseHangfireServer(); // 启用server
6     app.UseHangfireDashboard(); // 启用看板
7 }

 

简单的几行代码,当然我也只会简单的用法。以上服务注入并执行,接下来就是往hangfire里面添加任务。
1 BackgroundJob.Enqueue(() => Console.WriteLine("Simple!")); // 立即执行
2 BackgroundJob.Schedule(() => Console.WriteLine("Reliable!"), TimeSpan.FromDays(7)); // 延后执行
3 RecurringJob.AddOrUpdate(() => Console.WriteLine("Transparent!"), Cron.Daily); // 循环执行,支持cron表达式
简单使用就到这吧,我们继续大纲的第三部分,源码分析。
 
源码分析
客户端模式就不用说了,说白了就是往hangfire数据库里面写任务,我们主要是看看服务端的执行原理。我们先找到入口,也可以看做是NetCore里面的一个中间件吧。看代码
1 app.UseHangfireServer(); // 启用server
UseHangfireServer实现
 1 public static IAppBuilder UseHangfireServer(
 2             [NotNull] this IAppBuilder builder,
 3             [NotNull] JobStorage storage,
 4             [NotNull] BackgroundJobServerOptions options,
 5             [NotNull] params IBackgroundProcess[] additionalProcesses)
 6         {
 7             // 其他代码...
 8             var server = new BackgroundJobServer(options, storage,  additionalProcesses); 
 9             
10             return builder;
11         }

 

UseHangfireServer扩展方法实现里面,比较重要的一行代码就是创建BackgroundJobServer,BackgroundJobServer实现了IBackgroundProcessingServer接口,server的启动也就是间接在它的构造器里面完成的。我们不妨先瞅瞅IBackgroundProcessingServer接口和BackgroundJobServer类的定义。
 1 // IBackgroundProcessingServer
 2 public interface IBackgroundProcessingServer : IDisposable
 3     {
 4         void SendStop();
 5         bool WaitForShutdown(TimeSpan timeout);
 6         Task WaitForShutdownAsync(CancellationToken cancellationToken);
 7     }
 8  
 9 // BackgroundJobServer
10 public class BackgroundJobServer : IBackgroundProcessingServer
11     {
12         // 其他成员...
13         public BackgroundJobServer(
14             [NotNull] BackgroundJobServerOptions options,
15             [NotNull] JobStorage storage,
16             [NotNull] IEnumerable<IBackgroundProcess> additionalProcesses,
17             [CanBeNull] IJobFilterProvider filterProvider,
18             [CanBeNull] JobActivator activator,
19             [CanBeNull] IBackgroundJobFactory factory,
20             [CanBeNull] IBackgroundJobPerformer performer,
21             [CanBeNull] IBackgroundJobStateChanger stateChanger)
22         {
23             // 其他代码
24             var processes = new List<IBackgroundProcessDispatcherBuilder>();
25             processes.AddRange(GetRequiredProcesses(filterProvider, activator,  factory, performer, stateChanger));
26             processes.AddRange(additionalProcesses.Select(x =>  x.UseBackgroundPool(1)));
27             var properties = new Dictionary<string, object>
28             {
29                 { "Queues", options.Queues },
30                 { "WorkerCount", options.WorkerCount }
31             };
32             
33             _processingServer = new BackgroundProcessingServer(
34                 storage,
35                 processes,
36                 properties,
37                 GetProcessingServerOptions());
38         }
39         public void SendStop()
40         {
41         }
42         public void Dispose()
43         {
44         }
45         [Obsolete("This method is a stub. There is no need to call the `Start`  method. Will be removed in version 2.0.0.")]
46         public void Start()
47         {
48         }
49         [Obsolete("Please call the `Shutdown` method instead. Will be removed in  version 2.0.0.")]
50         public void Stop()
51         {
52         }
53         [Obsolete("Please call the `Shutdown` method instead. Will be removed in  version 2.0.0.")]
54         public void Stop(bool force)
55         {
56         }
57         public bool WaitForShutdown(TimeSpan timeout)
58         {
59         }
60         public Task WaitForShutdownAsync(CancellationToken cancellationToken)
61         {
62         }

 

IBackgroundProcessingServer接口里面的这几个方法都是跟停用server,取消任务清理资源相关的。BackgroundJobServer类里面真正完成接口的实现是由BackgroundProcessingServer类型的同名函数实现,这个对象是在构造函数里面初始化的,在初始化BackgroundProcessingServer类型的同时,创建了若干IBackgroundProcessDispatcherBuilder实现类BackgroundProcessDispatcherBuilder的实例,hangfire默认实现了7种dispatcher,我们任务、日志、心跳等等独立线程都是由它的Create方法完成,这个地方不算server启动主线,会在后面细说。我们继续看看BackgroundProcessingServer这个类型。这里需要注意的是里面有几个方法好像是被停用了,start、stop等方法,官方也注释了,被删除了。start方法被停用了,难道我们的server启动是在BackgroundProcessingServer类型里面?继续看BackgroundProcessingServer的定义。
 1 public sealed class BackgroundProcessingServer : IBackgroundProcessingServer
 2     {
 3         // 其他成员
 4         internal BackgroundProcessingServer(
 5             [NotNull] BackgroundServerProcess process,
 6             [NotNull] BackgroundProcessingServerOptions options)
 7         {
 8             _process = process ?? throw new ArgumentNullException(nameof(process));
 9             _options = options ?? throw new ArgumentNullException(nameof(options));
10             _dispatcher = CreateDispatcher();
11 #if !NETSTANDARD1_3
12             AppDomain.CurrentDomain.DomainUnload += OnCurrentDomainUnload;
13             AppDomain.CurrentDomain.ProcessExit += OnCurrentDomainUnload;
14 #endif
15         }
16         public void SendStop()
17         {
18         }
19         public bool WaitForShutdown(TimeSpan timeout)
20         {
21         }
22         public async Task WaitForShutdownAsync(CancellationToken cancellationToken)
23         {
24         }
25         public void Dispose()
26         {
27             
28         }
29         private void OnCurrentDomainUnload(object sender, EventArgs args)
30         {
31             
32         }
33         private IBackgroundDispatcher CreateDispatcher()
34         {
35             var execution = new BackgroundExecution(
36                 _stoppingCts.Token,
37                 new BackgroundExecutionOptions
38                 {
39                     Name = nameof(BackgroundServerProcess),
40                     ErrorThreshold = TimeSpan.Zero,
41                     StillErrorThreshold = TimeSpan.Zero,
42                     RetryDelay = retry => _options.RestartDelay
43                 });
44             return new BackgroundDispatcher(
45                 execution,
46                 RunServer,
47                 execution,
48                 ThreadFactory);
49         }
50         private void RunServer(Guid executionId, object state)
51         {
52             _process.Execute(executionId, (BackgroundExecution)state,  _stoppingCts.Token, _stoppedCts.Token, _shutdownCts.Token);
53         }
54         private static IEnumerable<Thread> ThreadFactory(ThreadStart threadStart)
55         {
56             yield return new Thread(threadStart)
57             {
58                 IsBackground = true,
59                 Name = $"{nameof(BackgroundServerProcess)}  #{Interlocked.Increment(ref _lastThreadId)}",
60             };
61         }
62     }

 

果不其然,server的启动快要揭开神秘的面纱了,RunServer?翻译过来应该是启动服务吧,我们暂且不去管他,先记一下这个有个runserver,我们继续跟踪。在构造函数里面调用了一个CreateDispatcher()的方法,我们看下它的实现
 1 private IBackgroundDispatcher CreateDispatcher()
 2         {
 3             var execution = new BackgroundExecution(
 4                 _stoppingCts.Token,
 5                 new BackgroundExecutionOptions
 6                 {
 7                     Name = nameof(BackgroundServerProcess),
 8                     ErrorThreshold = TimeSpan.Zero,
 9                     StillErrorThreshold = TimeSpan.Zero,
10                     RetryDelay = retry => _options.RestartDelay
11                 });
12             return new BackgroundDispatcher(
13                 execution,
14                 RunServer,
15                 execution,
16                 ThreadFactory);
17         }

 

在CreateDispatcher方法里面返回了一个BackgroundDispatcher,字面意思好像是后台分发器,并且指定了回调runserver,BackgroundDispatcher实现了IBackgroundDispatcher接口,我们先看下它们的定义。
 1 // IBackgroundDispatcher
 2 public interface IBackgroundDispatcher : IDisposable
 3     {
 4         bool Wait(TimeSpan timeout);
 5         Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken);
 6     }
 7  
 8 // BackgroundDispatcher
 9 internal sealed class BackgroundDispatcher : IBackgroundDispatcher
10     {
11         // 其他成员
12         public BackgroundDispatcher(
13             [NotNull] IBackgroundExecution execution,
14             [NotNull] Action<Guid, object> action,
15             [CanBeNull] object state,
16             [NotNull] Func<ThreadStart, IEnumerable<Thread>> threadFactory)
17         {
18             if (threadFactory == null) throw new  ArgumentNullException(nameof(threadFactory));
19             _execution = execution ?? throw new  ArgumentNullException(nameof(execution));
20             _action = action ?? throw new ArgumentNullException(nameof(action));
21             _state = state;
22 #if !NETSTANDARD1_3
23             AppDomainUnloadMonitor.EnsureInitialized();
24 #endif
25             var threads = threadFactory(DispatchLoop)?.ToArray();
26             if (threads == null || threads.Length == 0)
27             {
28                 throw new ArgumentException("At least one unstarted thread should be  created.", nameof(threadFactory));
29             }
30             if (threads.Any(thread => thread == null || (thread.ThreadState &  ThreadState.Unstarted) == 0))
31             {
32                 throw new ArgumentException("All the threads should be non-null and  in the ThreadState.Unstarted state.", nameof(threadFactory));
33             }
34             _stopped = new CountdownEvent(threads.Length);
35             foreach (var thread in threads)
36             {
37                 thread.Start();
38             }
39         }
40         public bool Wait(TimeSpan timeout)
41         {
42             return _stopped.WaitHandle.WaitOne(timeout);
43         }
44         public async Task WaitAsync(TimeSpan timeout, CancellationToken  cancellationToken)
45         {
46             await _stopped.WaitHandle.WaitOneAsync(timeout,  cancellationToken).ConfigureAwait(false);
47         }
48         public void Dispose()
49         {
50         }
51         public override string ToString()
52         {
53         }
54         private void DispatchLoop()
55         {
56             try
57             {
58                 _execution.Run(_action, _state);
59             }
60             catch (Exception ex)
61             {
62  
63             }
64             finally
65             {
66                 try
67                 {
68                     _stopped.Signal();
69                 }
70                 catch (ObjectDisposedException)
71                 {
72  
73                 }
74             }
75         }
76     }

 

从IBackgroundDispatcher接口的定义来看,分发器应该是负责协调资源处理,我们具体看看BackgroundDispatcher的实现。以上代码就是server的启动执行核心代码并且我以加粗,其实就是启动线程Loop执行。在DispatchLoop方法里面间接调用了我上面说的runserver方法。在runserver方法里面实现了整个server端的初始化工作。我们接着看DispatchLoop方法的实现 ,在这个方法里面调用了IBackgroundExecution接口的run方法,继续IBackgroundExecution接口的定义。
1 public interface IBackgroundExecution : IDisposable
2     {
3         void Run([NotNull] Action<Guid, object> callback, [CanBeNull] object  state);
4         Task RunAsync([NotNull] Func<Guid, object, Task> callback, [CanBeNull]  object state);
5     }

 

就两方法,run包含同步和异步,看看它的唯一实现类BackgroundExecution。
 1   internal sealed class BackgroundExecution : IBackgroundExecution
 2     {
 3                 // 其他成员
 4         public void Run(Action<Guid, object> callback, object state)
 5         {
 6             if (callback == null) throw new ArgumentNullException(nameof(callback));
 7             var executionId = Guid.NewGuid();
 8            
 9             {
10 #if !NETSTANDARD1_3
11                 try
12 #endif
13                 {
14                     HandleStarted(executionId, out var nextDelay);
15                     while (true)
16                     {
17                         // Don't place anything here.
18                         try
19                         {
20                            
21                             if (StopRequested) break;
22                             if (nextDelay > TimeSpan.Zero)
23                             {
24                                 HandleDelay(executionId, nextDelay);
25                             }
26                             callback(executionId, state);
27                             HandleSuccess(out nextDelay);
28                         }
29 #if !NETSTANDARD1_3
30                         catch (ThreadAbortException) when  (AppDomainUnloadMonitor.IsUnloading)
31                         {
32                             // Our thread is aborted due to AppDomain unload. It's  better to give up to
33                             // not to cause the host to be more aggressive.
34                             throw;
35                         }
36 #endif
37                         catch (OperationCanceledException) when (StopRequested)
38                         {
39                             break;
40                         }
41                         catch (Exception ex)
42                         {
43                             HandleException(executionId, ex, out nextDelay);
44                         }
45                     }
46                     HandleStop(executionId);
47                 }
48 #if !NETSTANDARD1_3
49                 catch (ThreadAbortException ex)
50                 {
51                     HandleThreadAbort(executionId, ex);
52                 }
53 #endif
54             }
55         }
56 }

 

hangfire里面所有的独立线程都是通过run方法执行,然后回调到自己的实现类Execute方法,自此每个独立的功能线程就循环干着自己独立的工作(这个后面会单独分析RecurringJobScheduler)。继续我们的主线,server启动,我们以run的同步方法为例,第一个线程(我们就叫它主线程吧)启动了一个while循环,在循环里面并且callback调用了我们的runserver方法。
    
1 private void RunServer(Guid executionId, object state)
2         {
3             _process.Execute(executionId, (BackgroundExecution)state,  _stoppingCts.Token, _stoppedCts.Token, _shutdownCts.Token);
4         }

 

在runserver方法里面的实现很简单,直接调用了_process的execute方法,我们简单看下_process类型IBackgroundServerProcess的定义。
1 internal interface IBackgroundServerProcess
2     {
3         void Execute(
4             Guid executionId,
5             BackgroundExecution execution,
6             CancellationToken stoppingToken,
7             CancellationToken stoppedToken,
8             CancellationToken shutdownToken);
9     }

 

IBackgroundServerProcess的定义就一个execute方法,这个接口的工作其实就是初始化server服务端,我们看看它的唯一实现类BackgroundServerProcess。
  1 internal sealed class BackgroundServerProcess : IBackgroundServerProcess
  2     {
  3         
  4         // 其他成员
  5         public BackgroundServerProcess(
  6             [NotNull] JobStorage storage,
  7             [NotNull] IEnumerable<IBackgroundProcessDispatcherBuilder> dispatcherBuilders,
  8             [NotNull] BackgroundProcessingServerOptions options,
  9             [NotNull] IDictionary<string, object> properties)
 10         {
 11             if (dispatcherBuilders == null) throw new ArgumentNullException(nameof(dispatcherBuilders));
 12  
 13  
 14             _storage = storage ?? throw new ArgumentNullException(nameof(storage));
 15             _options = options ?? throw new ArgumentNullException(nameof(options));
 16             _properties = properties ?? throw new ArgumentNullException(nameof(properties));
 17  
 18  
 19             var builders = new List<IBackgroundProcessDispatcherBuilder>();
 20             builders.AddRange(GetRequiredProcesses()); // 添加默认的工作dispatcher也就是独立线程
 21             builders.AddRange(GetStorageComponents());
 22             builders.AddRange(dispatcherBuilders);
 23  
 24  
 25             _dispatcherBuilders = builders.ToArray();
 26         }
 27  
 28  
 29         public void Execute(Guid executionId, BackgroundExecution execution, CancellationToken stoppingToken,
 30             CancellationToken stoppedToken, CancellationToken shutdownToken)  // server初始化
 31         {
 32             var serverId = GetServerId();
 33             Stopwatch stoppedAt = null;
 34  
 35  
 36             void HandleRestartSignal()
 37             {
 38                 if (!stoppingToken.IsCancellationRequested)
 39                 {
 40                     _logger.Info($"{GetServerTemplate(serverId)} caught restart signal...");
 41                 }
 42             }
 43             using (var restartCts = new CancellationTokenSource())
 44             using (var restartStoppingCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, restartCts.Token))
 45             using (var restartStoppedCts = CancellationTokenSource.CreateLinkedTokenSource(stoppedToken, restartCts.Token))
 46             using (var restartShutdownCts = CancellationTokenSource.CreateLinkedTokenSource(shutdownToken, restartCts.Token))
 47             using (restartStoppingCts.Token.Register(HandleStopRestartSignal))
 48             using (stoppingToken.Register(HandleStoppingSignal))
 49             using (stoppedToken.Register(HandleStoppedSignal))
 50             using (shutdownToken.Register(HandleShutdownSignal))
 51             using (restartCts.Token.Register(HandleRestartSignal))
 52             {
 53                 var context = new BackgroundServerContext(
 54                     serverId,
 55                     _storage,
 56                     _properties,
 57                     restartStoppingCts.Token,
 58                     restartStoppedCts.Token,
 59                     restartShutdownCts.Token);
 60                 var dispatchers = new List<IBackgroundDispatcher>();
 61                 CreateServer(context);
 62                 try
 63                 {
 64                     // ReSharper disable once AccessToDisposedClosure
 65                     using (var heartbeat = CreateHeartbeatProcess(context, () => restartCts.Cancel())) // 创建守护线程
 66                     {
 67                         StartDispatchers(context, dispatchers); // 启动hangfire默认初始化的所有独立任务线程
 68                         execution.NotifySucceeded();
 69                         WaitForDispatchers(context, dispatchers);
 70  
 71  
 72                         restartCts.Cancel();
 73  
 74                         heartbeat.WaitAsync(Timeout.InfiniteTimeSpan, shutdownToken).GetAwaiter().GetResult();
 75                     }
 76                 }
 77                 finally
 78                 {
 79                     DisposeDispatchers(dispatchers);
 80                     ServerDelete(context, stoppedAt);
 81                 }
 82             }
 83         }
 84  
 85  
 86         private IBackgroundDispatcher CreateHeartbeatProcess(BackgroundServerContext context, Action requestRestart) // 创建守护线程
 87         {
 88             return new ServerHeartbeatProcess(_options.HeartbeatInterval, _options.ServerTimeout, requestRestart)
 89                 .UseBackgroundPool(threadCount: 1)
 90                 .Create(context, _options);
 91         }
 92  
 93  
 94         private IEnumerable<IBackgroundProcessDispatcherBuilder> GetRequiredProcesses() // 初始化日志和任务监控线程
 95         {
 96             yield return new ServerWatchdog(_options.ServerCheckInterval, _options.ServerTimeout).UseBackgroundPool(threadCount: 1);
 97             yield return new ServerJobCancellationWatcher(_options.CancellationCheckInterval).UseBackgroundPool(threadCount: 1);
 98         }
 99         private string GetServerId() // 获取serverid
100         {
101             var serverName = _options.ServerName
102                  ?? Environment.GetEnvironmentVariable("COMPUTERNAME")
103                  ?? Environment.GetEnvironmentVariable("HOSTNAME");
104             var guid = Guid.NewGuid().ToString();
105  
106             return !String.IsNullOrWhiteSpace(serverName) ? $"{serverName.ToLowerInvariant()}:{guid}" : guid;
107         }
108  
109         
110         private void CreateServer(BackgroundServerContext context) // 创建server,写入Server数据表
111         {
112             var stopwatch = Stopwatch.StartNew();
113             using (var connection = _storage.GetConnection())
114             {
115                 connection.AnnounceServer(context.ServerId, GetServerContext(_properties));
116             }
117             stopwatch.Stop();
118  
119  
120             ServerJobCancellationToken.AddServer(context.ServerId);
121             _logger.Info($"{GetServerTemplate(context.ServerId)} successfully announced in {stopwatch.Elapsed.TotalMilliseconds} ms");
122         }
123  
124  
125         private void StartDispatchers(BackgroundServerContext context, ICollection<IBackgroundDispatcher> dispatchers) // 启动所有独立的任务线程,包括我们的队列计划、循环计划、日志、守护等等线程
126         {
127  
128             foreach (var dispatcherBuilder in _dispatcherBuilders)
129             {
130                 dispatchers.Add(dispatcherBuilder.Create(context, _options));
131             }
132         }
133  
134     }

 

以上代码我有做精简处理,不要纠结里面的实现,代码注释也比较详细。下面我做一个简单的总结吧,第一个线程(暂时叫主线程吧)从startup里面调用usehangfireserver扩展方法-》启动一个新的worker线程用于初始化&启动server-》主程返回-》启动hangfire所有任务线程-》创建的第一个worker线程挂起(用于处理所有任务线程的资源释放)。server的初始化工作大概就是这些,下面详细看看hangfire的任务线程的执行原理,这里我们以RecurringJobScheduler循环任务为例。
 
RecurringJobScheduler实现机制
还记得上面提到的7个dispatcher任务线程的创建吗?这7个默认的任务线程初始化就发生在上面加粗的代码里面StartDispatchers方法,我们看代码。
1 private void StartDispatchers(BackgroundServerContext context,  ICollection<IBackgroundDispatcher> dispatchers)
2         {
3                // 其他代码...
4             foreach (var dispatcherBuilder in _dispatcherBuilders)
5             {
6                 dispatchers.Add(dispatcherBuilder.Create(context, _options)); // 初始化独立任务线程
7             }
8         }

 

遍历_dispatcherBuilders数组,7种任务类型,分别调用它们的Create方法。继续看create方法。
   
 1  public IBackgroundDispatcher Create(BackgroundServerContext context,  BackgroundProcessingServerOptions options) // 第一步
 2         {
 3             // 其他代码
 4             var execution = new BackgroundExecution(
 5                 context.StoppingToken,
 6                 new BackgroundExecutionOptions
 7                 {
 8                     Name = _process.GetType().Name,
 9                     RetryDelay = options.RetryDelay
10                 }); // 定义自己的execution
11             return new BackgroundDispatcher( // 创建BackgroundDispatcher 
12                 execution,
13                 ExecuteProcess, // 指定回调
14                 Tuple.Create(_process, context, execution), // 创建三元组上下文,注意一下1元组这个对象
15                 _threadFactory);
16         }
17  
18 public BackgroundDispatcher(  // 第二步
19             [NotNull] IBackgroundExecution execution,
20             [NotNull] Action<Guid, object> action,
21             [CanBeNull] object state,
22             [NotNull] Func<ThreadStart, IEnumerable<Thread>> threadFactory)
23         {
24    
25             _state = state;
26  
27             var threads = threadFactory(DispatchLoop)?.ToArray();
28            
29             foreach (var thread in threads)
30             {
31                 thread.Start(); // 执行线程
32             }
33         }
34  
35 private void DispatchLoop() // 第三步
36         {
37             try
38             {
39                 _execution.Run(_action, _state);  // 在run里面回调_action
40             }
41             catch (Exception ex)
42             {
43             }
44             finally
45             {
46                 try
47                 {
48                     _stopped.Signal();
49                 }
50                 catch (ObjectDisposedException)
51                 {
52                 }
53             }
54         }
55  
56 private static void ExecuteProcess(Guid executionId, object state) // 第四步 回调方法,对应上面的指定回调
57         {
58             var tuple = (Tuple<IBackgroundProcess, BackgroundServerContext,  BackgroundExecution>)state;
59             var serverContext = tuple.Item2;
60             var context = new BackgroundProcessContext( // 创建公共上下文
61                 serverContext.ServerId,
62                 serverContext.Storage,
63                 serverContext.Properties.ToDictionary(x => x.Key, x => x.Value),
64                 executionId,
65                 serverContext.StoppingToken,
66                 serverContext.StoppedToken,
67                 serverContext.ShutdownToken);
68             while (!context.IsStopping)
69             {
70                 tuple.Item1.Execute(context); // 执行自己元组对应的实例
71                 tuple.Item3.NotifySucceeded();
72             }
73         }

 

上面有点乱啊,我大概简单串起来说一下。第一步在create方法里面创建了BackgroundDispatcher并指定了元组参数-》第二步绑定线程的执行函数Loop并且执行-》第三步执行Loop并且回调_action委托-》第四步_action参数对应的函数地址就是ExecuteProcess,最后在ExecuteProcess里面通过元组参数调用对应的任务类型,自此7种任务类型启动并开始工作。以上代码还有个细节需要说明一下,Tuple.Create(_process, context, execution)。元组的第一个参数,其类型为IBackgroundProcess,看下定义。
1 public interface IBackgroundProcess : IServerProcess
2     {
3         void Execute([NotNull] BackgroundProcessContext context);
4     }

 

接口就定义了一个方法,没什么特别的,但是它的几个实现类就是我们单独的任务类,我们下面要说的RecurringJobScheduler循环任务类也实现了这个接口。到此我们的RecurringJobScheduler循环定时任务线程就算开始执行了。
RecurringJobScheduler循环定时任务机制
照旧看下这个类型的定义
 1 public class RecurringJobScheduler : IBackgroundProcess
 2     {
 3         // 其他代码
 4         public RecurringJobScheduler(
 5             [NotNull] IBackgroundJobFactory factory,
 6             TimeSpan pollingDelay,
 7             [NotNull] ITimeZoneResolver timeZoneResolver,
 8             [NotNull] Func<DateTime> nowFactory)
 9         {
10             if (factory == null) throw new ArgumentNullException(nameof(factory));
11             if (nowFactory == null) throw new ArgumentNullException(nameof(nowFactory));
12             if (timeZoneResolver == null) throw new ArgumentNullException(nameof(timeZoneResolver));
13  
14  
15             _factory = factory;
16             _nowFactory = nowFactory;
17             _timeZoneResolver = timeZoneResolver;
18             _pollingDelay = pollingDelay;
19             _profiler = new SlowLogProfiler(_logger);
20         }
21  
22  
23         /// <inheritdoc />
24         public void Execute(BackgroundProcessContext context) // 实现方法
25         {
26             if (context == null) throw new ArgumentNullException(nameof(context));
27  
28  
29             var jobsEnqueued = 0;
30  
31  
32             while (EnqueueNextRecurringJobs(context)) // 从数据库获取定时任务
33             {
34                 jobsEnqueued++;
35  
36  
37                 if (context.IsStopping)
38                 {
39                     break;
40                 }
41             }
42  
43  
44             if (jobsEnqueued != 0)
45             {
46                 _logger.Debug($"{jobsEnqueued} recurring job(s) enqueued.");
47             }
48  
49  
50             if (_pollingDelay > TimeSpan.Zero)
51             {
52                 context.Wait(_pollingDelay);
53             }
54             else
55             {
56                 var now = _nowFactory();
57                 context.Wait(now.AddMilliseconds(-now.Millisecond).AddSeconds(-now.Second).AddMinutes(1) - now);
58             }
59         }
60     }

 

承上,调用元组的第一个参数的execute方法,RecurringJobScheduler的execute方法得以执行,该方法就干一件事,每隔15秒从数据库获取待执行的计划,每次1000条数据。通过EnqueueNextRecurringJobs方法获取任务。
 1 private bool EnqueueNextRecurringJobs(BackgroundProcessContext context)
 2         {
 3             return UseConnectionDistributedLock(context.Storage, connection => 
 4             {
 5                 var result = false;
 6                 if (IsBatchingAvailable(connection))
 7                 {
 8                     var now = _nowFactory();
 9                     var timestamp = JobHelper.ToTimestamp(now);
10                     var recurringJobIds =  ((JobStorageConnection)connection).GetFirstByLowestScoreFromSet("recurring-jobs", 0,  timestamp, BatchSize); // 从数据库里面查询
11                     if (recurringJobIds == null || recurringJobIds.Count == 0) return  false;
12                     foreach (var recurringJobId in recurringJobIds)
13                     {
14                         if (context.IsStopping) return false;
15                         if (TryEnqueueBackgroundJob(context, connection, recurringJobId,  now))// 排队执行
16                         {
17                             result = true;
18                         }
19                     }
20                 }
21                 else
22                 {
23                     for (var i = 0; i < BatchSize; i++)
24                     {
25                         if (context.IsStopping) return false;
26                         var now = _nowFactory();
27                         var timestamp = JobHelper.ToTimestamp(now);
28                         var recurringJobId =  connection.GetFirstByLowestScoreFromSet("recurring-jobs", 0, timestamp);
29                         if (recurringJobId == null) return false;
30                         if (!TryEnqueueBackgroundJob(context, connection, recurringJobId,  now))
31                         {
32                             return false;
33                         }
34                     }
35                 }
36                 return result;
37             });
38         }

 

GetFirstByLowestScoreFromSet方法从数据库Set表里面查询top1000数据,条件是key为recurring-jobs字符串(表示定时任务)并且 时间范围是0到当前时间。随后遍历这些jobids,排队执行,往下看TryEnqueueBackgroundJob方法的实现。
 1 private bool EnqueueBackgroundJob(
 2             BackgroundProcessContext context,
 3             IStorageConnection connection,
 4             string recurringJobId,
 5             DateTime now)
 6         {
 7             // 其他代码
 8             using (connection.AcquireDistributedRecurringJobLock(recurringJobId,  LockTimeout))
 9             {
10                 try
11                 {
12                     var recurringJob = connection.GetRecurringJob(recurringJobId,  _timeZoneResolver, now);
13                     if (recurringJob == null)
14                     {
15                         using (var transaction = connection.CreateWriteTransaction())
16                         {
17                             transaction.RemoveFromSet("recurring-jobs", recurringJobId);
18                             transaction.Commit();
19                         }
20                         return false;
21                     }
22           
23                     BackgroundJob backgroundJob = null;
24                     IReadOnlyDictionary<string, string> changedFields;
25                     if (recurringJob.TrySchedule(out var nextExecution, out var error))
26                     {
27                         if (nextExecution.HasValue && nextExecution <= now)
28                         {
29                             backgroundJob = _factory.TriggerRecurringJob(context.Storage,  connection, _profiler, recurringJob, now);
30                             if (String.IsNullOrEmpty(backgroundJob?.Id))
31                             {
32                                 _logger.Debug($"Recurring job '{recurringJobId}' execution  at '{nextExecution}' has been canceled.");
33                             }
34                         }
35                         recurringJob.IsChanged(out changedFields, out nextExecution);
36                     }
37                     else if (recurringJob.RetryAttempt < MaxRetryAttemptCount)
38                     {
39                         var delay = _pollingDelay > TimeSpan.Zero ? _pollingDelay :  TimeSpan.FromMinutes(1);
40                         
41                         _logger.WarnException($"Recurring job '{recurringJobId}' can't be  scheduled due to an error and will be retried in {delay}.", error);
42                         recurringJob.ScheduleRetry(delay, out changedFields, out  nextExecution);
43                     }
44                     else
45                     {
46                         _logger.ErrorException($"Recurring job '{recurringJobId}' can't be  scheduled due to an error and will be disabled.", error);
47                         recurringJob.Disable(error, out changedFields, out nextExecution);
48                     }
49               
50                     using (var transaction = connection.CreateWriteTransaction())
51                     {
52                         if (backgroundJob != null)
53                         {
54                             _factory.StateMachine.EnqueueBackgroundJob(
55                                 context.Storage,
56                                 connection,
57                                 transaction,
58                                 recurringJob,
59                                 backgroundJob,
60                                 "Triggered by recurring job scheduler",
61                                 _profiler);
62                         }
63                         transaction.UpdateRecurringJob(recurringJob, changedFields,  nextExecution, _logger);
64                         transaction.Commit();
65                         return true;
66                     }
67                 }
68                 catch (TimeZoneNotFoundException ex)
69                 {
70                 catch (Exception ex)
71                 {
72    
73                 }
74                 return false;
75             }
76         }

 

需要注意的地方我都有加粗,该方法大概流程是:1.GetRecurringJob根据jobid从Hash表里面查询一条完整的定时任务,2.TrySchedule获取该任务的下次执行时间,如果下次执行时间小于当前,执行这条任务(并非真正执行定时任务,只是往job表里面写数据,真正执行任务由worker完成),3.获取下次执行时间&所有任务字段,4.状态机修改任务状态。定时任务就这样周而复始的重复执行以上流程。这里简单说下worker的执行机制,其实际就是轮询检索job表里面的数据执行任务表达式树,worker在hangfire里面默认开启了20个线程。第三部分就到这吧。
 
避坑
简单说下个人在改bug期间遇到的一些问题啊。
1.时区问题,在添加定时任务时如果不指定时区信息,默认使用的是utc时间,我们中国是东8区,也就是说解析出来的执行时间会晚8个小时执行。解决办法有几种可以通过全局指定options的ITimeZoneResolver属性指定,也可以通过AddorUpdate方法指定,如果是指定时区信息,需要注意看板上面的异常信息,如果有异常会导致任务不执行,时区信息它是从系统里面检索出来的,没有就抛异常。就这样吧。
 
 
 
 
 
 
posted @ 2020-03-13 23:57  小菜 。  阅读(2375)  评论(2编辑  收藏  举报