• ASP.NET Core Library – Hangfire


    前言

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

    介绍

    Hangfire 是用来做 server task 的, 定时任务, delay 执行之类的. 它可以做到分钟级别的 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", 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 的功能, 当用户提交后, 系统需要发 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. 方法的 class/interface 在执行时会被创建通过 ActivatorUtilities.CreateInstance 创建, 它是基于依赖注入的哦, 如果创建失败 task 就 fail 了, 会去 rety.

    4. 方法执行的时候是完全独立的一个 scope (线程), 和之前的 request 是没有关系的了. 如果注入 http context 会发现它是 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 的. 比如希望 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.

    状况

    在设计 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.UseHangfireDashboard("/jobs");

    Read-only

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

    默认只有 localhost 情况下才能无授权访问 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.UseHangfireDashboard("/hangfire", new DashboardOptions
    {
        Authorization = new [] { new MyAuthorizationFilter() }
    });
  • 相关阅读:
    洛谷P3157 [CQOI2011]动态逆序对
    CDQ分治
    快速数论变换(NTT)
    洛谷P3338 [ZJOI2014]力
    洛谷 P1919 A*B Problem升级版
    0-1分数规划
    洛谷P4593 [TJOI2018]教科书般的亵渎
    拉格朗日插值
    20180912-3 词频统计
    20190912-1 每周例行报告
  • 原文地址:https://www.cnblogs.com/keatkeat/p/15771877.html
Copyright © 2020-2023  润新知