前言

以前写过 Hangfire 的学习笔记, 但写的很乱. 这篇做个整理.

介绍

Hangfire 是用来做 server task 的, 比如: background job, delay job, schedule job 等等. 它可以做到分钟级别的 schedule, 任务会通过 SQL Server 来管理 (也可以支持其它 database 但不推荐)

参考:

C#-初识Hangfire

官网 docs

安装 & Startup

参考: 官网教程

安装 nuget

dotnet add package Hangfire.Core
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer

startup

builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage("Server=192.168.1.152;Database=TestHangfire;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True", new SqlServerStorageOptions
{
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
}));
builder.Services.AddHangfireServer();

配置不重要, 这里是按照官网 example 默认配置, 链接上 database 就可以了 (注: Hangfire 会负责创建 tables, 但我们需要先创建好 database, 不然会报错)

启动

app.MapHangfireDashboard();

Background Job

一般网站都有一个 send enquiry 的功能, 当用户提交 enquiry 表单后, 系统需要发 email 给管理人.

发 SMTP 是很慢的, 如果让用户等待会影响用户体验. 所以这种情况就很适合跑一个 background job.

request 设定好 background job 后就可以直接 response user. 然后系统才背地里去发 SMTP.

这种场景就可以用 Hangfire 来实现了.

public void OnPost()
{
var enquiryId = 1; // create enquiry to database
BackgroundJob.Enqueue<EmailService>(e => e.SendEmail(enquiryId));
}

调用 BackgroundJob.Enqueue 就可以了. 它会在 response 之后立刻被执行.

有几个点需要注意

1. BackgroundJob.Enqueue 的参数是 Expression 而不是 Func, 所以它只能简单地表达式, 如果要写负责逻辑就需要开一个方法, 让表达式去调用方法.

2. 方法必须是 public 的

3. Hangfire 执行 job 时, 会动态的创建这个方法的 class / interface, 它是通过 ActivatorUtilities.CreateInstance 创建的, 支持依赖注入哦, 如果创建失败 task 就 fail 了, 会去 rety.

4. 方法执行时是完全独立的一个 scope (线程), 和之前的 request 完全没有关系. 如果注入 HttpContext 会发现它是 null.

public class IndexModel : PageModel
{
public string Value { get; set; } = "default"; public void OnPost()
{
Value = "Not Default";
BackgroundJob.Enqueue(() => DoSomething());
} public void DoSomething()
{
var value = Value; // default
}
}

上面, DoSomething 执行是是全新的一个 scope, IndexModel 会被创建, 所以 value 是 default.

5. 尽量不要让方法依赖原本环境的东西, 做一个中间人负责.

比如, 与其把把所有信息以 parameters 形式传入方法, 更好的做法是让方法自己去获取所有信息, 通过一个 Id 作为中间人.

Delay Background Job

上面提到的例子是是属于马上执行的 background job, 还有一种是 delay job. 比如, 希望 user submit 之后 10 second 才发 email.

public void OnPost()
{
var enquiryId = 1; // create enquiry in database
BackgroundJob.Schedule<IEmailService>(s => s.SendEmail(enquiryId), TimeSpan.FromSeconds(10));
}

调用的方法是 .Schedule, 传入 delay 的 timespan 或者一个绝对时间 DateTimeOffset.

Hangfire 是通过一个 interval 在背后检查 schedule 的, 它默认的时间是 15 second 检查一次.

可以通过 options 修改它, 估计是性能考量所以才放 15 秒吧, 不然一直要去 query database check job 也挺伤的.

builder.Services.AddHangfireServer(options => {
options.SchedulePollingInterval = TimeSpan.FromSeconds(1);
});

Recurring Job

上面说的都是一次性执行, recurring job 是用来处理那种每星期/月要执行的 job.

说到这个就得说说 cron expression 了. 它就是用来表达, 每星期, 每月, 还是每逢...什么时辰的.

参考:

CRON 表达式详解

cron表达式详解

crontab guru 小工具

Hangfire create/remove recurring job

RecurringJob.AddOrUpdate("job name", () => Console.Write("Easy!"), "cron expression");
RecurringJob.AddOrUpdate("job name", () => Console.Write("Easy!"), Cron.Daily);
RecurringJob.RemoveIfExists("job name");

Cron.Daily 是 Hangfire 的 helper 类, 帮我们创建 cron expression.

也可以封装成 Service

RecurringJob.AddOrUpdate<FacebookReviewService>("Sync Facebook Review", service => service.SyncToDatabaseAsync(), Cron.Daily(hour: 1));

Service.cs

public class FacebookReviewService
{
private readonly IWebHostEnvironment _env; public FacebookReviewService(
IWebHostEnvironment env
)
{
_env = env;
} [AutomaticRetry(Attempts = 0)]
public async Task SyncToDatabaseAsync()
{
// do anything
}
}

AutomaticRetry 是声明是否失败了要自动重试. 0 就是不要.

状况

在设计 job 的时候要记得, server schedule 并不稳定. 有可能遇到 server down, job runtime error 等等的情况.

1. Runtime error and retry

当 job 出现 runtime error 时, Hangfire 默认会 retry 10 次, 每次 retry 都会有一个间隔时间.

它的 delay 算法是

如果想修 retry count 和 delay, 可以放 AutomaticRetryAttribute 到 job 方法上, 0 表示不要 retry, AttemptsExceededAction.Delete | Fail 意思是 error 以后是否要把这个 job 洗掉后者留一个 status fail 做计入 (这个不影响它 retry).

public class EmailService : IEmailService
{
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public void SendEmail(int enquiryId)
{ }
}

由于它有 retry 的机制, 所以在设计 job 时, 需要做 transaction 确保原子性, 或者把方法做成幂等,

2. Miss execute time

Server down, retry 都有可能导致 job 运行的时间和预想的不一致. 比如预设每星期一凌晨 12 点跑.

结果那个时段 server down 了, Hangfire 会在 server up 的时候立刻补上错过的 job.

Retry 的 delay 间隔, 也会造成运行时间和预期不符.

所以在设计 job 时也需要考虑到时间.

3. 超时任务

job 运行太耗时, 与至于下一次的运行也启动了. 这时就容易出现混乱. 这个视乎是风水的问题. 应该避免大任务执行, 把它切分成小任务.

部分部分去 complete.

数据结构

Job 是查看所有运行过的 job, 不管是成功失败.

State 是所有 job 每一次 state change 的记入, 包括了 Enqueued, Processing, Succeeded 等

Set 是 recurring job 的 definition, crod expression 这些

其它就比较少会去看.

Dashboard

26-01-2022 Issue: Dashboard page blank after upgrade to .net 6.0

hot reload 和 Hangfire dashboard 撞. 目前没有看到有 github issue. wordaround 是关掉 hot reload.

访问 /Hangfire 就可以看见 build-in 的 dashboard 了

这里还可以 manual trigger job 或者移除 job 哦. 这也是 Hangfire 的一大卖点.

想自定义 url 可以这样做

app.MapHangfireDashboard("/jobs");

Read-only

app.MapHangfireDashboard("/Hangfire", new DashboardOptions
{
IsReadOnlyFunc = (DashboardContext context) => true
});

Authorize

默认只有 dev 情况下才能无授权访问 dashboard, 通过自定义 IDashboardAuthorizationFilter 就可以设定权限访问.

public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext(); // Allow all authenticated users to see the Dashboard (potentially dangerous).
return httpContext.User.Identity.IsAuthenticated;
}
}

注: UseHangfireDashboard 要在 authentication, authorize middleware 之后.

app.MapHangfireDashboard("/Hangfire", new DashboardOptions
{
Authorization = new [] { new MyAuthorizationFilter() }
});

上面这个方法比较过时了, 如果是有搭配 login 界面的话推荐使用 MapHangfireDashboardWithAuthorizationPolicy 来做

builder.Services.AddAuthorization(options =>
options.AddPolicy("HangfireDashboard", policy => policy.RequireAuthenticatedUser())
);
app.MapHangfireDashboardWithAuthorizationPolicy("HangfireDashboard");

还有一招是用 basic authentication, 参考: Stack Overflow – ASP.NET Core MVC Hangfire custom authentication

MapHangfireDashboard vs UseHangfireDashboard

Map 是 endpoint routing 的版本, 尽量用 Map 呗

Error : JobStorage.Current property value has not been initialized

如果没有调用 app.MapHangfireDashboard 而直接使用 RecurringJob.AddOrUpdate 是会报错的.

相关提问: Stack Overflow – JobStorage.Current property value has not been initialized. You must set it before using Hangfire Client or Server API

解决方式有 2 种

1. 调用 MapHangfireDashboard

2. RequiredService<JobStorage> 激活它一下

app.Services.GetRequiredService<JobStorage>();
RecurringJob.AddOrUpdate("job name", () => Console.Write("Easy!"), Cron.Daily(hour: 6));

Multiple Server

今天突然发现 job duplicated 了. 然后发现既然有 2 个 server instance.

后来发现, 原来是我忘了把 staging server 关掉. 低级错误. 哈哈

下面这个代码可以查看当前的 server instance 有哪些

var monitoringApi = JobStorage.Current.GetMonitoringApi();
var removingServers = monitoringApi.Servers().Where(e => e.Name.Contains("myserver")).ToList();
removingServers.ForEach(removingServer => JobStorage.Current.GetConnection().RemoveServer(removingServer.Name));

ASP.NET Core Library – Hangfire的更多相关文章

  1. ASP.NET Core 使用 Hangfire 定时任务

    定时任务组件,除了 Hangfire 外,还有一个 Quarz.NET,不过 Hangfire .NET Core 支持的会更好些. ASP.NET Core 使用 Hangfire 很简单,首先,N ...

  2. Asp.Net Core 集成 Hangfire 配置使用 Redis 存储

    Hangfire 官方支持 MSSQL 与 Redis(Hangfire.Pro.Redis) 两种 ,由于我的数据库是 MYSQL ,粗略查询了一下文档,现在对 .NET Core 支持的并不够好, ...

  3. asp.net core 中hangfire面板的配置及使用

    1.定义校验授权类DyDashboardAuthorizationFilter /// <summary> /// Hangfire仪表盘配置授权 /// </summary> ...

  4. ASP.NET Core开发-后台任务利器Hangfire使用

    ASP.NET Core开发系列之后台任务利器Hangfire 使用. Hangfire 是一款强大的.NET开源后台任务利器,无需Windows服务/任务计划程序. 可以使用于ASP.NET 应用也 ...

  5. 解决 ASP.NET Core Hangfire 未授权(401 Unauthorized)

    相关文章:ASP.NET Core 使用 Hangfire 定时任务 ASP.NET Core Hangfire 在正式环境发布之后,如果访问 http://10.1.2.31:5000/hangfi ...

  6. 《ASP.NET Core 高性能系列》致敬伟大的.NET斗士甲骨文!

    写在开始 三年前,曾写过一篇文章:从.NET和Java之争谈IT这个行业,当时遭到某些自认为懂得java就了不起的Javaer抨击, 现在可以致敬伟大的.NET斗士甲骨文了 (JDK8以上都需要收费, ...

  7. ASP.NET Core and .NET Core Library Support

    ASP.NET Core and .NET Core Library Support 详情参见:https://github.com/linezero/NETCoreLibrary/blob/mast ...

  8. Hangfire在ASP.NET CORE中的简单实现

    hangfire是执行后台任务的利器,具体请看官网介绍:https://www.hangfire.io/ 新建一个asp.net core mvc 项目 引入nuget包 Hangfire.AspNe ...

  9. ASP.NET CORE MVC 2.0 项目中引用第三方DLL报错的解决办法 - InvalidOperationException: Cannot find compilation library location for package

    目前在学习ASP.NET CORE MVC中,今天看到微软在ASP.NET CORE MVC 2.0中又恢复了允许开发人员引用第三方DLL程序集的功能,感到甚是高兴!于是我急忙写了个Demo想试试,我 ...

  10. 在Asp.Net Core中使用DI的方式使用Hangfire构建后台执行脚本

    最近项目中需要用到后台Job,原有在Windows中我们会使用命令行程序结合计划任务或者直接生成Windows Service,现在.Net Core跨平台了,虽然Linux下也有计划任务,但跟原有方 ...

随机推荐

  1. 解决方案 | 外接键盘win+d失效,绿联键盘win+d,win+e失效

    按下fn + 右边的win键 即可解决.如下图所示.

  2. django 计算两个TimeField的时差

    在 Django 中,你可以使用 datetime 模块来计算两个 TimeField 字段的时间差.以下是一个示例: from datetime import datetime, timedelta ...

  3. [oeasy]python0099_雅达利大崩溃_IBM的开放架构_兼容机_oem

    雅达利大崩溃 回忆上次内容 个人计算机浪潮已经来临 苹果公司迅速发展 微软公司脱离mits准备做纯软件公司 IBM用大型机思路制作的5100惨败 Commodore 64 既做计算机 又做游戏机 计算 ...

  4. AT_abc218_d 题解

    洛谷链接&Atcoder 本篇题解为此题较简单做法及较少码量,并且码风优良,请放心阅读. 题目简述 给定一个平面内的 \(N\) 个点的坐标,求这 \(N\) 个点中选 \(4\) 个点可构成 ...

  5. C#:进程之间传递数据

    一.思路 在Windows程序中,各个进程之间常常需要交换数据,进行数据通讯.常用的方法有 使用内存映射文件 通过共享内存DLL共享内存 使用SendMessage向另一进程发送WM_COPYDATA ...

  6. C# Winform与JS交互

    一.C#调用JS函数 1.JS代码 < script language = "javascript" > function Hello(msg) { alert('我是 ...

  7. Elasticjob执行job幂等

    ElasticJob的幂等机制,是指作业分片执行的幂等,他需要做到以下两点: 同一个分片在当前作业实例上不会被重复执行 一个作业分片不能同时在多个作业实例上执行 如何实现幂等 场景模拟:存在任务A执行 ...

  8. scratch源码下载 | 几何冲刺

    程序说明: <几何冲刺>是一款基于Scratch平台开发的跑酷类游戏程序.在这个游戏中,玩家控制一个黄色的小方块,在快速向前冲刺的过程中躲避各种障碍物.通过按下键盘上的上方向键,玩家可以操 ...

  9. 【Git】GithubDesktop 忽略文件无法忽略BUG

    问题描述: 从仓库拉取的[.gitignore]忽略配置文件,在项目跑起来之后会生成诸多.idea文件,target打包文件 一开始没有忽略,但是发现使用GD配置之后忽略无效: 解决办法: 做一次随便 ...

  10. 【Mybatis】06 Session获取 & 配置参数总结

    会话获取 SqlSessionFactory 最佳的获取方式就是使用Mybatis提供的资源类加载配置文件 调用会话工厂建造者实例的建造方法注入读取流 要注意的是建造者生成了了实例就可以不需要了 这里 ...