دعم قائمة انتظار Hangfire

Hangfire هي مكتبة ل. net (الأساسية) ، والتي تسمح بتنفيذ غير متزامن لبعض التعليمات البرمجية على مبدأ "النار ونسيان". مثال على هذا الرمز يمكن أن يكون إرسال البريد الإلكتروني ، ومعالجة الفيديو ، والمزامنة مع نظام آخر ، الخ بالإضافة إلى "إطلاق النار ونسيان" ، هناك دعم للمهام المؤجلة ، وكذلك المهام المجدولة بتنسيق Cron.


حاليا ، هناك العديد من هذه المكتبات. بعض فوائد Hangfire هي:


  • التكوين بسيط ، API مريحة
  • الموثوقية يضمن Hangfire أن المهمة التي تم إنشاؤها سيتم تنفيذها مرة واحدة على الأقل
  • القدرة على أداء المهام في أداء متوازي وممتاز
  • القابلية للتوسعة (سنستخدمها أدناه)
  • وثائق كاملة إلى حد ما ومفهومة
  • لوحة القيادة التي يمكنك أن ترى جميع الإحصاءات حول المهام

لن أخوض في الكثير من التفاصيل ، نظرًا لوجود العديد من المقالات الجيدة حول Hangfire وكيفية استخدامها. سأناقش في هذه المقالة كيفية استخدام دعم العديد من قوائم الانتظار (أو تجمعات المهام) ، وكيفية إصلاح وظيفة إعادة المحاولة القياسية وجعل كل قائمة انتظار لها تكوين فردي.


الدعم الحالي لقوائم الانتظار (الزائفة)


ملاحظة مهمة: في العنوان ، استخدمت عبارة "قائمة الانتظار الزائفة" لأن Hangfire لا يضمن تنفيذ المهام بترتيب معين. أي لا ينطبق مبدأ "First In First Out" ولن نعتمد عليه. علاوة على ذلك ، يوصي مؤلف المكتبة بجعل المهام غير مناسبة ، أي ثابت ضد تنفيذ متعددة غير متوقعة. كذلك سوف تستخدم كلمة "قائمة الانتظار" فقط ، لأنه يستخدم Hangfire مصطلح "قائمة الانتظار".


Hangfire لديه دعم قائمة انتظار بسيطة. على الرغم من أنه لا يوفر مرونة لأنظمة قائمة انتظار الرسائل مثل rabbitMQ أو Azure Service Bus ، إلا أنه غالبًا ما يكفي لحل مجموعة واسعة من المهام.


تحتوي كل مهمة على خاصية "قائمة الانتظار" ، أي اسم قائمة الانتظار التي يجب تنفيذها فيها. بشكل افتراضي ، يتم إرسال المهمة إلى قائمة الانتظار باسم "الافتراضي" ، ما لم ينص على خلاف ذلك. هناك حاجة إلى دعم قوائم الانتظار المتعددة من أجل إدارة منفصلة لتنفيذ المهام من أنواع مختلفة. على سبيل المثال ، قد نود أن تقع مهام معالجة الفيديو في قائمة انتظار "video_queue" ، وترسل رسائل البريد الإلكتروني إلى قائمة انتظار "email_queue". وبالتالي ، نحن قادرون على أداء هذين النوعين من المهام بشكل مستقل. إذا كنا نريد نقل معالجة الفيديو إلى خادم مخصص ، فيمكننا القيام بذلك بسهولة عن طريق تشغيل خادم Hangfire منفصل كتطبيق وحدة تحكم سيقوم بمعالجة قائمة انتظار "video_queue".


دعنا ننتقل إلى الممارسة


إعداد خادم Hangfire في asp.net core هو كما يلي:


public void Configure(IApplicationBuilder app) { app.UseHangfireServer(new BackgroundJobServerOptions { WorkerCount = 2, Queues = new[] { "email_queue", "video_queue" } }); } 

المشكلة 1 - تقع مهام إعادة التشغيل في قائمة الانتظار الافتراضية


كما ذكرت أعلاه ، هناك قائمة انتظار افتراضية في Hangfire تسمى "الافتراضي". إذا فشلت المهمة الموضوعة في قائمة الانتظار ، على سبيل المثال ، "video_queue" وتحتاج إلى إعادة المحاولة ، فسيتم إرسالها إلى قائمة الانتظار "الافتراضية" مرة أخرى وليس "video_queue" ، ونتيجة لذلك ، لن يتم تنفيذ المهمة على الإطلاق مثيل خادم Hangfire الذي نود ، على كل حال. تم إنشاء هذا السلوك بواسطتي بشكل تجريبي وربما يكون خطأ في Hangfire نفسها.


مرشحات الوظيفة


يوفر لنا Hangfire إمكانية توسيع الوظيفة بمساعدة المرشحات المسماة (مرشحات الوظيفة ) ، والتي تشبه من حيث المبدأ عوامل تصفية الإجراءات في ASP.NET MVC. الحقيقة هي أن المنطق الداخلي لـ Hangfire يتم تنفيذه كآلة حكومية. هذا هو المحرك الذي ينقل المهام في التجمع بشكل متتابع من حالة إلى أخرى (على سبيل المثال ، تم إنشاؤه -> enqueued -> معالجة -> نجح) ، وتتيح لنا عوامل التصفية "اعتراض" المهمة التي يتم تنفيذها في كل مرة تتغير حالتها والتعامل معها. يتم تطبيق عامل التصفية كسمة يمكن تطبيقها على طريقة واحدة أو فئة أو على مستوى عالمي.


معلمات الوظيفة


يتم تمرير الكائن ElectStateContext كوسيطة إلى أسلوب عامل التصفية. يحتوي هذا الكائن على معلومات كاملة حول المهمة الحالية. من بين أشياء أخرى ، يحتوي على أساليب GetJobParameter <> (...) و SettJobParameter <> (...). تتيح لك معلمات المهمة حفظ المعلومات المتعلقة بالمهمة في قاعدة بيانات. في معلمات الوظيفة ، يتم تخزين اسم قائمة الانتظار التي تم إرسال المهمة إليها في الأصل ، وذلك فقط لسبب من الأسباب تم تجاهل هذه المعلومات أثناء إعادة المحاولة التالية.


الحل


لذلك ، لدينا مهمة انتهت بالخطأ ويجب إرسالها لإعادة تنفيذها في قائمة الانتظار الصحيحة (في نفس المهمة التي تم تعيينها لها في وقت الإنشاء الأولي). تكرار المهمة التي أكملت بخطأ ما هو الانتقال من الحالة "الفاشلة" إلى الحالة "المحصنة". لحل المشكلة ، قم بإنشاء عامل تصفية ، عندما تدخل المهمة في الحالة "enqueued" ، والتي تحقق في قائمة الانتظار التي تم إرسال المهمة في البداية ووضع المعلمة "QueueName" في القيمة المطلوبة:


 public class HangfireUseCorrectQueueFilter : JobFilterAttribute, IElectStateFilter { public void OnStateElection(ElectStateContext context) { if (context.CandidateState is EnqueuedState enqueuedState) { var queueName = context.GetJobParameter<string>("QueueName"); if (string.IsNullOrWhiteSpace(queueName)) { context.SetJobParameter("QueueName", enqueuedState.Queue); } else { enqueuedState.Queue = queueName; } } } } 

لتطبيق الفلتر الافتراضي على جميع المهام (أي على الصعيد العالمي) ، أضف الكود التالي إلى التكوين الخاص بنا:


 GlobalJobFilters.Filters.Add(new HangfireUseCorrectQueueFilter { Order = 1 }); 

الصيد الصغيرة الأخرى هي أن مجموعة GlobalJobFilters افتراضياً تحتوي على مثيل لفئة AutomaticRetryAttribute. هذا عامل تصفية قياسي مسؤول عن إعادة تنفيذ المهام الفاشلة. كما يرسل المهمة إلى قائمة الانتظار "الافتراضية" ، متجاهلاً قائمة الانتظار الأصلية. من أجل ركوب الدراجة الخاصة بنا ، تحتاج إلى إزالة هذا الفلتر من المجموعة وترك مرشحنا يتحمل مسؤولية المهام المتكررة. نتيجة لذلك ، سيبدو رمز التكوين كما يلي:


 var defaultRetryFilter = GlobalJobFilters.Filters .FirstOrDefault(f => f.Instance is AutomaticRetryAttribute); if (defaultRetryFilter != null && defaultRetryFilter.Instance != null) { GlobalJobFilters.Filters.Remove(defaultRetryFilter.Instance); } GlobalJobFilters.Filters.Add(new HangfireUseCorrectQueueFilter { Order = 1 }); 

تجدر الإشارة إلى أن AutomaticRetryAttribute تنفذ منطق زيادة الفاصل الزمني بين المحاولات تلقائيًا (يزيد الفاصل الزمني مع كل محاولة لاحقة) ، وإزالة AutomaticRetryAttribute من مجموعة GlobalJobFilters ، فإننا نتخلى عن هذه الوظيفة (انظر تطبيق أسلوب ScheduleAgainLater )


لذلك ، لقد حققنا أنه يمكن تنفيذ مهامنا في طوابير مختلفة ، وهذا يسمح لنا بإدارة تنفيذها بشكل مستقل ، بما في ذلك معالجة طوابير مختلفة على أجهزة مختلفة. الآن فقط لا نعرف عدد المرات والفترة الزمنية التي سيتم فيها تكرار مهامنا في حالة حدوث خطأ ، لأننا أزلنا AutomaticRetryAttribute من مجموعة المرشحات.


المشكلة 2 - الإعدادات الفردية لكل قائمة انتظار


نريد أن نكون قادرين على تكوين الفاصل الزمني وعدد التكرارات بشكل منفصل لكل قائمة انتظار ، وأيضًا ، إذا لم نحدد بعض القيم صراحة ، فإننا نريد تطبيق القيم الافتراضية. للقيام بذلك ، نقوم بتطبيق مرشح آخر ونطلق عليه HangfireRetryJobFilter .


من الناحية المثالية ، يجب أن يبدو رمز التكوين مثل هذا:


 GlobalJobFilters.Filters.Add(new HangfireRetryJobFilter { Order = 2, ["email_queue"] = new HangfireQueueSettings { DelayInSeconds = 120, RetryAttempts = 3 }, ["video_queue"] = new HangfireQueueSettings { DelayInSeconds = 60, RetryAttempts = 5 } }); 

الحل


للقيام بذلك ، قم أولاً بإضافة فئة HangfireQueueSettings ، والتي ستكون بمثابة حاوية لإعداداتنا.


 public sealed class HangfireQueueSettings { public int RetryAttempts { get; set; } public int DelayInSeconds { get; set; } } 

ثم نضيف تطبيق المرشح نفسه ، والذي ، عند تكرار المهام بعد حدوث خطأ ، سيتم تطبيق الإعدادات حسب تكوين قائمة الانتظار ومراقبة عدد مرات إعادة المحاولة:


 public class HangfireRetryJobFilter : JobFilterAttribute, IElectStateFilter, IApplyStateFilter { private readonly HangfireQueueSettings _defaultQueueSettings = new HangfireQueueSettings { RetryAttempts = 3, DelayInSeconds = 10 }; private readonly IDictionary<string, HangfireQueueSettings> _settings = new Dictionary<string, HangfireQueueSettings>(); public HangfireQueueSettings this[string queueName] { get { return _settings.TryGetValue(queueName, out HangfireQueueSettings queueSettings) ? queueSettings : _defaultQueueSettings; } set { _settings[queueName] = value; } } public void OnStateElection(ElectStateContext context) { if (!(context.CandidateState is FailedState failedState)) { // This filter accepts only failed job state. return; } var retryAttempt = context.GetJobParameter<int>("RetryCount") + 1; var queueName = context.GetJobParameter<string>("QueueName"); if (retryAttempt <= this[queueName].RetryAttempts) { ScheduleAgainLater(context, retryAttempt, failedState, queueName); } else { TransitionToDeleted(context, failedState, queueName); } } public void OnStateApplied( ApplyStateContext context, IWriteOnlyTransaction transaction) { if (context.NewState is ScheduledState && context.NewState.Reason != null && context.NewState.Reason.StartsWith("Retry attempt")) { transaction.AddToSet("retries", context.BackgroundJob.Id); } } public void OnStateUnapplied( ApplyStateContext context, IWriteOnlyTransaction transaction) { if (context.OldStateName == ScheduledState.StateName) { transaction.RemoveFromSet("retries", context.BackgroundJob.Id); } } private void ScheduleAgainLater( ElectStateContext context, int retryAttempt, FailedState failedState, string queueName) { context.SetJobParameter("RetryCount", retryAttempt); var delay = TimeSpan.FromSeconds(this[queueName].DelayInSeconds); const int maxMessageLength = 50; var exceptionMessage = failedState.Exception.Message.Length > maxMessageLength ? failedState.Exception.Message.Substring(0, maxMessageLength - 1) + "…" : failedState.Exception.Message; // If attempt number is less than max attempts, we should // schedule the job to run again later. var reason = $"Retry attempt {retryAttempt} of {this[queueName].RetryAttempts}: {exceptionMessage}"; context.CandidateState = delay == TimeSpan.Zero ? (IState)new EnqueuedState { Reason = reason } : new ScheduledState(delay) { Reason = reason }; } private void TransitionToDeleted( ElectStateContext context, FailedState failedState, string queueName) { context.CandidateState = new DeletedState { Reason = this[queueName].RetryAttempts > 0 ? "Exceeded the maximum number of retry attempts." : "Retries were disabled for this job." }; } } 

ملاحظة إلى التعليمات البرمجية: عند تطبيق فئة HangfireRetryJobFilter ، تم أخذ فئة AutomaticRetryAttribute من HangfireRetryJobFilter كأساس ، وبالتالي فإن تطبيق بعض الطرق يتزامن جزئيًا مع الطرق المقابلة لهذه الفئة.

المشكلة 3 - كيفية إرسال مهمة إلى قائمة انتظار محددة؟


تمكنت من العثور على طريقتين لتعيين المهمة إلى قائمة الانتظار: موثقة و - لا.


الطريقة الأولى - تعليق السمة المقابلة على الطريقة


 [Queue("video_queue")] public void SomeMethod() { } BackgroundJob.Enqueue(() => SomeMethod()); 

http://docs.hangfire.io/en/latest/background-processing/configuring-queues.html


الطريقة الثانية (بدون وثائق) - استخدم الفئة BackgroundJobClient


 var client = new BackgroundJobClient(); client.Create(() => MyMethod(), new EnqueuedState("video_queue")); 

تتمثل ميزة الطريقة الثانية في أنها لا تنشئ تبعيات غير ضرورية على Hangfire وتسمح لك بتحديد أي عملية يجب أن تستمر المهمة خلالها. لسوء الحظ ، في الوثائق الرسمية ، لم أجد ذكرًا لفئة BackgroundJobClient وكيفية تطبيقها. لقد استخدمت الطريقة الثانية في حل بلدي ، لذلك يتم اختباره في الممارسة العملية.


الخاتمة


في هذه المقالة ، استخدمنا دعم قوائم انتظار متعددة في Hangfire لفصل معالجة أنواع مختلفة من المهام. طبقنا آليتنا لتكرار المهام المكتملة غير الناجحة مع إمكانية التكوين الفردي لكل قائمة انتظار ، وتوسيع وظيفة Hangfire باستخدام Job Filters ، وتعلمنا أيضًا كيفية إرسال المهام إلى قائمة الانتظار المطلوبة للتنفيذ.


آمل أن يكون هذا المقال مفيدًا لشخص ما. سأكون سعيدا للتعليق.


روابط مفيدة


وثائق Hangfire
مصدر شفرة Hangfire
Scott Hanselman - كيفية تشغيل مهام الخلفية في ASP.NET

Source: https://habr.com/ru/post/ar434364/


All Articles