آموزش زمانبندی کارها با HangFire در Asp.Net Core

در این مقاله برای نمایش کد ها از GitHub Gist استفاده شده و ممکن است Load شدن کد ها کمی طول بکشد یا به نرم افزار های رفع تحریم نیاز داشته باشید.

تسک های پس زمینه (Background Job) چیست؟

بطور کلی تسک های پس زمینه کارهایی هستند که برنامه باید بصورت خودکار در زمان های مشخص آن هارا انجام دهد برای مثال :

شرایطی را در نظر بگیرید که متدی با حجم زیادی از محاسبات پیچیده دارید که وقتی کاربر درخواست خود را ارسال میکند شروع به محاسبه میشود و کاربر جاری چاره ای جز انتظار نخواهد داشت اما اگر اینکار در زمانی دیگر قبل از درخواست کاربر محاسبه میشد و صرفا نتیجه به کاربر نمایش داده میشد قطعا تصمیم بهتری نسبت به محاسبه آنی آن متد در زمان درخواست کاربر بوده.

در سناریو دیگر تصور کنید میخواهید هر شب در ساعتی مشخص خلاصه ای از مطالب وبسایتتان را برای کاربران وبسایت ایمیل کنید در این حالت برنامه باید هر شب در فلان ساعت اینکار را برای ما انجام دهد و تماما باید این اتفاق بدون دخالت هیچ اراده انسانی و بصورت خودکار توسط برنامه انجام گیرد.

همچنین شرایطی از این قبیل ، ارسال ایمیل تایید هویت یک ساعت بعد از ثبت نام ، گرفتن بک آپ از اطلاعات برنامه بصورت هفتگی و دیگر این موارد همه در دسته تسک های پس زمینه(Background Job) از یک برنامه قرار دارند.

سوال : HangFire چیست؟

همانطور که دانستید تسک های پس زمینه نیاز به یک سیستم مدیریت زمان دارد که کارها را در زمان های مشخص شده به انجام برساند. HangFire یک پکیج متن باز برای ایجاد سیستم زمانبندی شده کارها است و اینکار را به ساده ترین روش انجام خواهد داد.

همچنین HangFire در کنار Quartz که یک سیستم دیگر جهت پیاده سازی زمانبندی است از معروف ترین پکیج ها برای زمانبندی تسک های پس زمینه بشمار میرود که در ادامه بیشتر به مزایا و معایب این دو میپردازیم.

مقایسه HangFire و Quartz :

میتوان گفت این دو پکیج تا حد زیادی شبیه به هم هستند و تفاوت اصلی آن ها در لایه های زیرین و نوع محاسبات زمانی که هریک نهفته است که الگوریتم مختص به خود را برای این محاسبه دارند اما در نهایت یک کار را انجام میدهند.

دیتابیس :

تفاوتی که میتوان از آن نام برد وجود قابلیت Redis Store در HangFire است که Quratz چنین قابلیتی را از سمت خودش اراعه نداده و برای استفاده از Redis در Quartz باید شخصا این دو را باهم کانفیگ کنید. دیتابیس Redis بخاطر ساختار دیتابیسی که دارد سرعت و پرفرمنس بالاتری را اراعه میدهد که استفاده از این قابلیت در پروژه هایی با تعداد تسک ها و رکورد های زیاد کاملا مشهود است. البته این ویژگی در HangFire رایگان نیست و برای داشتن آن از سمت HangFire لازم است هزینه آن را نیز پرداخت کنید اما اگر هم نمیخواهید پولی بابتش بپردازید و همچنان از آن استفاده کنید یک پکیج اوپن سورس برای آن نیز طراحی شده که از لینک پایین میتوانید مشاهده کنید.

https://github.com/marcoCasamento/Hangfire.Redis.StackExchange


ساختار :

پکیج HangFire از ابتدا با دات نت و معماری های دات نتی توسعه داده شده اما Quartz ابتدا برای زبان جاوا نوشته شده بود و به نوعی از این زبان ریلیزی برای دات نت تهیه شد و این موضوع طبعا تاثیرات خودش را داشته و برخی از معماری ها و تفکرات جاوایی در آن مشهود است که البته مشکلی ایجاد نمیکند و محدودیتی نسبت به HangFire از لحاظ کارکرد دارا نیست شاید تنها چیزی که میتوان در این باب گفت DotNet Friendly تر بودن HangFire است که کار با متد های آن آسان تر و به اصطلاح خوش دست تر است.

داشبورد :

هردو پکیج از داشبورد پشتیبانی میکنند که میتوانید در این داشبورد و ui اختصاصی که برای نمایش تسک ها طراحی شده تسک های ایجاد شده را مدیریت کنید. داشبورد HangFire بصورت پیشفرض همراه با آن قرار دارد که بعد از نصب HangFire میتوانید براحتی داشبورد سوار بر آن را نیز مشاهده کنید اما در Quartz ، داشبورد باید بصورت Extension در پکیجی جدا به آن اضافه شود و مورد استفاده قرار گیرد. در لینک پایین دوتا از بهترین داشبورد ها برای Quartz را مشاهده میکنید که در صورت نیاز میتوانید از آن استفاده کنید.

https://github.com/jlucansky/Quartzmin


https://github.com/guryanovev/CrystalQuartz




استفاده از HangFire :

1. نصب :

  • برای نصب HangFire در پروژه Asp.Net Core لازم است ابتدا پکیج های مورد نیاز آن را نصب کنید که شامل :
Install-Package Hangfire.Core 
Install-Package Hangfire.SqlServer
Install-Package Hangfire.AspNetCore
  • پس از نصب پکیج ها باید تنظیمات مورد نیاز برای پیاده سازی HangFire در برنامه را اعمال کنیم. این تنظیمات شامل افزودن سرویس ها و اینترفیس های HangFire به برنامه است که اینکار را با افزودن HangFire به متد ConfigureService کلاس Startup انجام خواهیم داد :
https://gist.github.com/sajadkardel/b2fe809e07e406eea12e6beeb8ba6d6e
  • پکیج HangFire برای مدیریت کار و زمان ، Table هایی دارد که پس از نصب روی دیتابیس برنامه شما قرار میگیرد فقط باید دقت داشته باشید ConnectionString دیتابیس خود را در متد AddHangFire مقدار دهی کنید تا از این طریق دیتابیس برنامه را شناخته و Table های مورد نظر را در Schema جدیدی با نام HangFire به آن اضافه کند.

پ ن : HangFire بصورت پیشفرض با دیتابیس SqlServer ارتباط برقرار میکند.


  • این پکیج یک داشبورد اختصاصی دارد که در آن لیستی از انواع تسک های در صف انجام و گزارشی از انجام شده ها را در اختیار ما قرار میدهد. برای تنظیم این داشبورد باید Middleware مربوط به آن و endpoint جدیدی برای شناسایی مسیر داشبورد HangFire در برنامه را در متد Configure کلاس Startup اضافه کنید :
https://gist.github.com/sajadkardel/fb9088bf0aab1f506b297577a922c0f6

برای اینکه به داشبورد HangFire دسترسی داشته باشید کافیست پس از نصب و انجام تنظیمات مذکور ، برنامه را اجرا کنید و در انتهای Url برنامه ، کلمه "hangfire" را وارد کنید سپس وارد پنل داشبورد آن خواهید شد.

http://localhost:50255/hangfire

البته میتوانید آدرس داشبورد HangFire در برنامه را از کلمه "hangfire" به هر چیزی که میخواهید شخصی سازی کنید. برای اینکار کافیست درون Middleware تعریف شده بصورت ورودی string آدرس جدیدی برای HangFire تعریف کنید.

app.UseHangfireDashboard(&quot/mydashboard&quot);

و به طبع در Url :

http://localhost:50255/mydashboard

2. داشبورد :

داشبورد HangFire شامل چندین بخش و تب مختلف است که به اختصار هر یک را بررسی خواهیم کرد.

تب Job :

همه تسک های تعریف شده شامل Enqueued, Succeeded, Processing, Failed و... در این تب نشان داده میشود.

تب Retries :

این تب مربوط به تسک هایی است که در روال زمانبندی و اجرا به دلایل مختلف مثل Stop شدن برنامه توسط iis یا Down شدن سرور و یا هر عامل خارجی دیگر شکست خوردند و در زمانبندی مشخص شده اجرا نشدند. همچنین قابلیت دوباره به جریان انداختن job مورد نظر را در اختیار ما قرار میدهد که از این طریق میتوان تسک های از دست رفته را مدیریت کرد و دوباره انجام داد.

تب Recurring Jobs :

وقتی شما یک تسک مانند گرفتن بکاپ از دیتابیس بصورت ماهانه تعریف میکنید و قرار است در هر ماه این اتفاق رخ دهد این یک تسک تکراری تلقی شده و این تب مسئول نشان دادن اینگونه از تسک ها میباشد.

تب Servers :

این بخش سرویس هایی که HangFire برای محاسبه زمانبندی از آن استفاده میکند را نشان میدهد. وقتی متد services.AddHangfireServer را به متد ConfigureService کلاس Startup اضافه میکنید سرویس های HangFire جهت محاسبه زمانبندی ها فعال میشود.


3. امنیت داشبورد :

همانطور که دانستید داشبورد ، اطلاعات کاملی از نوع کار و زمان اجرای آن و نام متدها را در اختیار ما قرار میدهد و همچنین اجازه تغییراتی مثل حذف یک تسک یا دوباره به اجرا در آوردن تسک ها و یا اجرای سریع تسک های به موعد نرسیده را به کاربر میدهد. گاهی ممکن است این اطلاعات شامل محتوایی امنیتی و غیر عمومی باشد که هرکسی در برنامه حق دسترسی به آنهارا ندارد. برای مدیریت کردن این امر میتوانید مراحل زیر را طی کنید :

  • مرحله اول : یک کلاس ایجاد میکنیم (مثلا با نام MyAuthorizationFilter) که این کلاس از اینترفیسی با نام IDashboardAuthorizationFilter ارث بری خواهد کرد.
https://gist.github.com/sajadkardel/5d5f2d9470e9e04d2c8a8daa732021f9

درون این کلاس متدی با نام Authorize از اینترفیس مربوطه impliment میشود که شروط احراز هویت و صدور یا عدم صدور دسترسی را کنترل میکند. این متد یک خروجی Boolian دارد که اگر هر یک از شروط احراز هویت شما تایید نشد خروجی false برمیگرداند. در این مثال ما برای دسترسی محدودیت Login بودن را اعمال کرده ایم که این را از HttpContext میگیریم.

  • مرحله دوم : در این مرحله کلاسی که بعنوان فیلتر احراز هویت برای کاربران ساخته ایم را در option های middleware پکیج HangFire اضافه میکنیم.
https://gist.github.com/sajadkardel/06fb146cfda06d14a37b33d7fd323f9b

یکی دیگر از option های این middleware که میتوان برای کنترل دسترسی در HangFire استفاده کرد آپشن Read-only view نام دارد.

https://gist.github.com/sajadkardel/f73171e49cd2e6b69fbb3f4a2b31a52a

این آپشن اجازه هرگونه تغییر در روند تسک ها از طریق صفحه داشبورد را از هر کاربری سلب میکند و داشبورد را صرفا به جهت نمایش کار ها استفاده میکند نه چیز دیگر.



انواع تسک ها در HangFire :

1. تسک های Fire-And-Forget :

تسک های Fire-And-Forget زمانبندی خاصی ندارند و بلافاصله بعد از فراخوانی اجرا میشوند. برای مثال شرایطی را در نظر بگیرید که میخواهید پس از ثبت نام هر کاربر در وبسایت ، یک ایمیل خوش آمد گویی ارسال کنید. این عمل یک تسک پس زمینه تلقی میشود اما زمانبندی خاصی نیز نمیخواهید برایش در نظر بگیرید در چنین شرایطی میتوانید از متد Enqueue استفاده کنید و یک تسک Fire-And-Forget ایجاد کنید تا این تسک صرفا در تسک های پس زمینه تان نام برده شود و قابل مشاهده باشد.

https://gist.github.com/sajadkardel/b3324e010d4ee653c96ff95848e72553

همانطور که میبینید در مثال بالا ابتدا برای استفاده از تسک های Fire-and-Forget در HangFire باید اینترفیس IBackgroundJobClient را تزریق کنیم و با استفاده از متد Enqueue در این اینترفیس یک تسک پس زمینه ایجاد میکنیم که کار آن فراخوانی متد SendWelcomeMail خواهد بود.

2. تسک های Delayed :

همانطور از اسم آن پیداست تسک های Delayed تسک هایی هستند که با یک تاخیر در زمان اجرا خواهند شد. بطور کلی زمانبندی این تسک ها به دو دسته تقسیم میشود :

  • دسته اول : اجرا پس از تاخیر در زمان مشخص.

همان شرایط ارسال ایمیل به کاربرانی که در وبسایتتان ثبت نام میکنند را در نظر بگیرید اما اینبار میخواهید نه بلافاصله بلکه 10 دقیقه بعد از ثبت نام کاربر ایمیل خوش آمد گویی را ارسال کنید. در این نوع شما یک تاخیر 10 دقیقه ای میخواهید که Delayed Job ها اینکار را برای ما انجام میدهند.

https://gist.github.com/sajadkardel/52f55f6f9bc8e2629e3ba93e8514d262

در این مثال با استفاده از متد Schedule در اینترفیس IBackgroundJobClient توانستیم متد SendWelcomeMail را صدا بزنیم و با ورودی TimeSpan یک تاخیر 10 دقیقه ای در متد HangFire اعمال کنیم.

همچنین میتوانید از ورودی های دیگر TimeSpan شامل TimeSpan.FromMilliseconds و TimeSpan.FromSecondsو TimeSpan.FromMinutes و TimeSpan.FromDays برای تنظیم تاخیر در تسک های خود استفاده کنید.

  • دسته دوم : اجرا در زمان مشخص.

نوع دیگر استفاده از متد Schedule تنظیم یک تاریخ و زمان مشخص برای اجرا شدن تسک های در آن تاریخ و زمان واحد میباشد. برای مثال سناریو ای را در نظر بگیرید که دستور اجرا و زمانبندی آن در اختیار کاربر باشد و کاربر بخواهد یک Reminder در تاریخ مشخص برایش ارسال شود که در اینصوررت میتوانید با استفاده از instance دیگری از متد Schedule که ورودی ای از جنس DateTimeOffset دریافت میکند ، تاریخ مشخص برای اجرا انتخاب کنید.

https://gist.github.com/sajadkardel/496c958231b6d57d48e4337f34cb4bab

در این مثال تاریخ مشخص برای اجرای تسک های خود را از کاربر در ورودی اکشن دریافت کرده ایم و به متد Schedule در غالب DateTimeOffset تعریف شده پاس میدهیم.

3. تسک های Recurring :

تسک های Recurring به تسک هایی گفته میشود که باید در یک بازه گردشی از زمان اجرا شوند. در یک مثال بیشتر با آن آشنا خواهیم شد. فرض کنید میخواهید هر هفته برنامه از اطلاعات دیتابیس موجود بکاپ بگیرد. در اینجا تسکی دارید که قرار است هر هفته و هربار به تکرر اجرا شود.

https://gist.github.com/sajadkardel/58739a4ec12c713cf6cd8c9504a4beb2

برای تنظیم یک Recurring Job باید اینترفیس دیگری بنام IRecurringJobManager را تزریق کرده و متد AddOrUpdate را استفاده کنید. در ورودی این متد یک جنس تعریف شده در HangFire بنام Cron دریافت میشود که بازه گردش در زمان را دریافت میکند که در اینجا بصورت هفتگی است.

انواع دیگر Cron شامل :

  • هر دقیقه (Cron.Minutely) :

این Cron هر دقیقه یکبار اجرا خواهد شد.

 _recurringJobManager.AddOrUpdate(&quottest&quot, () => job , Cron.Minutely);
  • هر ساعت (Cron.Hourly) :

این Cron هر یک ساعت یکبار و بصورت پیشفرض در دقیقه اول هر ساعت اجرا میشود.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Hourly);

اما میتوانید یک ورودی دقیقه به آن بدهید که در اینصورت در N اُمین دقیقه از هر ساعت اجرا شود.

 _recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Hourly(10));
  • هر روز (Cron.Daily) :

این Cron بصورت روزانه و در حالت پیشفرض در اولین ساعت و اولین دقیقه هر روز اجرا خواهد شد.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Daily);

در حالتی دیگر میتوانید ورودی ساعت و دقیقه را به آن بدهید تا در ساعت و دقیقه ای مشخص در هر روز اجرا شود.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Daily(3,10));
  • هر هفته (Cron.Weekly) :

این Cron هفتگی است. بصورت پیشفرض هر هفته شنبه در اولین ساعت و در اولین دقیقه اجرا میشود.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Weekly);

در حالتی دیگر چندمین روز هفته و ساعت و دقیقه مشخص را در ورودی میگیرد و حول آن میچرخد.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job,Cron.Weekly(DayOfWeek.Monday,3,10));
  • هر ماه (Cron.Monthly) :

این Cron بصورت ماهانه اولین روز ماه در اولین ساعت روز و در اولین دقیقه ساعت زمانبندی خود را اعمال میکند.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Monthly);

و در صورت دادن ورودی میتوانید زمانبندی آن در چندمین روز ماه در چه ساعت و دقیقه ای را نیز تنظیم کنید.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Monthly(10,3,10));
  • هر سال (Cron.Yearly) :
    و در نهایت این Cron بصورت سالانه و در اولین ماه ، روز ، ساعت و دقیقه هر سال وظیفه خود را انجام خواهد داد.
_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Yearly);

که اینهم مانند بقیه ، ورودی هایی دریافت میکند که به ترتیب شامل ماه ، روز ، ساعت و دقیقه است.

_recurringJobManager.AddOrUpdate(&quottest&quot, () => Job, Cron.Yearly(2,4,3,10));

در نهایت با استفاده از این Cron ها میتوانید انواع مختلفی از Recurring Job هارا بسازید.

4. تسک های Continuations :

این نوع از تسک های ، وابسته به تسک های دیگر هستند و بطور کلی وقتی استفاده میشود که ما میخواهیم تسکی را پس از تسک دیگری با یک زمانبندی به نسبت زمان اجرای تسک اول ، اجرا کنیم. برای مثال میخواهیم 10 دقیقه بعد از ثبت نام کاربر برای او ایمیل احراز هویت ارسال شود که شبیه اینکار را در تسک های Delayed انجام داده بودیم. اما همچنین قصد داریم 5 دقیقه بعد از ارسال ایمیل احراز هویت لینک فرستاده شده را منسوخ کنیم. در این سناریو ما دو زمانبندی داریم اول 10 دقیقه بعد از ثبت نام کاربر و دوم 5 دقیقه بعد از اجرای متد اول.

https://gist.github.com/sajadkardel/6f007357fb1138d16f77761236d96ed7

برای ایجاد یک Continuations Job باید از متد ContinueJobWith در اینترفیس IBackgroundJobClient استفاده کنیم و در ورودی اول این متد آیدی تسک ایجاد شده قبلی را پاس دهیم.


برخی از نکات و ترفند های HangFire :

1. استفاده از Cron Expression در Recurring Job ها :

بطور کلی Cron ها ساختاری تعریف شده برای تعیین بازه های زمانی است. Cron اختصار یافته کلمات Command Run On میباشد که به اجرا شدن یک دستور در زمان مشخص اشاره دارد. برای استفاده از آن ابتدا به تعریف این ساختار میپردازیم :

https://gist.github.com/sajadkardel/83d9afe714c732868defdd6c4d886319

این ساختار را از پایین به بالا در زیر برایتان تشریح میکنیم :

* * * * *
  • فیلد اول (Minute) : در این فیلد بایستی دقیقه ای مشخص از یک ساعت را وارد کنید.مانند دقیقه 10 (میتوانید محدوده هم تعیین کنید)
  • فیلد دوم (Hour) : در این فیلد بایستی زمان معلوم را با فرمت ساعت وارد کنید.مانند ساعت 7 (میتوانید محدوده هم تعیین کنید) ، مانند ساعات 12-7
  • فیلد سوم (Day of Month) : در این فیلد بایستی یک روز از ماه را وارد کنید مانند روز 15 ام از ماه (میتوانید محدوده هم تعیین کنید)
  • فیلد چهارم (Month) : در این فیلد بایستی یک ماه از سال را وارد کنید مثلا ماه 4 ام(آوریل) (میتوانید محدوده هم تعیین کنید)
  • فیلد پنجم (Day of Week) : در این فیلد بایستی روزی از روز های هفته یا محدوده ای از آن روز ها را تعیین کنید.مانند صفرم هفته که در کشور های اروپایی و آمریکایی معادل روز یکشنبه است.

همانطور که میبینید Cron ها دسترسی بهتری از تعیین بازه های زمانی مختلف اراعه میدهند که میتوانید از آن در Recurring متد ها بجای ورودی های Yearly - Monthly - Weekly - Daily - Hourly - Minutly استفاده کنید. در واقع خود این ورودی ها نیز متدی تعریف شده در کلاس Cron هستند که با فراخوانی آن خروجی Cron Expression میسازند و در درون ورودی متد Recurring قرار میگیرند.

در ادامه مثالی خواهیم زد تا نیازمندی به Cron Expression ها را بیشتر درک کنید. فرض کنید میخواهید یک زمانبندی داشته باشید که "هر ماه بین بازه 10 ام تا 15 ام ، بطور روزانه در ساعت 4:00" اجرا شود. اعمال این زمانبندی با متد های معمول در کلاس Cron امکان پذیر نیست اما میتوانید با Cron Expression این را اعمال کنید که به این شکل خواهد بود :

0 4 10-15 * *

برای ساخت Cron Expression ها وبسایت هایی وجود دارند که کمک میکنند انواع Cron Expression های پیچیده را طراحی کنیم و با استفاده از آن زمانبندی های دقیق تر و جزئی تر بسازیم. یکی از بهترین وبسایت ها برای اینکار crontab.guru است.

پ ن : برای استفاده از Cron Expression در متد های Recurring کافی است بجای ورودی های Yearly - Monthly - Weekly - Daily - Hourly - Minutly ، خود Cron Expression را درون ورودی متد تعریف کنیم :

 _recurringJobManager.AddOrUpdate(&quottest&quot, () => job , &quot0 4 10-15 * *&quot );

2. متد Trigger :

متد Trigger یک متد برای اجرای آنی تسک ها Recurring است که به کمک آن میتوانید این نوع از تسک های را بدون در نظر گرفتن زمانبندی آن در لحظه اجرا کنید و البته تاثیری در دفعات بعدی تکرار نداشته باشد.

RecurringJob.Trigger(&quotsome-id&quot);

3. تعیین تاریخ انقضا برای Recurring Job ها :

گاهی ممکن است در تسک های Recurring شرایطی پیش آید که برفرض میخواهید کاری را هر ماه انجام دهید اما این تکرار در پایان همان سال تمام میشود. در اینصورت باید یک Expire Time برای متد Recurring خود تنظیم کنیم تا بعد از 12 ماه تکرار در تاریخ 140X/12/30 به پایان برسد. HangFire برای متد های Recurring ورودی با عنوان ExpireTime تعریف نکرده اما میتوان از طریق ایجاد یک زمانبندی Schedule تاریخ مشخصی برای حذف کردن متد Recurring تعریف کرد همانند یک ExpireTime عمل میکند.

https://gist.github.com/sajadkardel/0301ec39846292b1429bebe165d38560

با اجرای این متد اول کاری برای تکرار در زمانبندی ماهیانه ایجاد میشود و در متد دوم زمانی برای حذف متد اول مشخص میکند.


در آخر امیدوارم این مقاله برایتان مفید واقع شده باشد. میتوانید فیدبکتان را در قالب کامنت یا یک قهوه برایم ارسال کنید.

https://coffeebede.ir/buycoffee/sajadkardel


مقالات بیشتر در دات نت زوم

https://t.me/DotNetZoom