أساسيات Active Job في ريلز
يوفّر لك هذا الدليل كل ما تحتاج إليه للبدء في إنشاء وظائف خلفية (background jobs) وإدراجها بطوابير الانتظار وتنفيذها. بعد قراءة هذا الدليل، ستتعلم:
- كيفيّة إنشاء وظائف.
- كيفيّة إدراج الوظائف بالطوابير.
- كيفيّة تشغيل الوظائف في الخلفية.
- كيفيّة إرسال رسائل البريد الإلكتروني من التطبيق الخاص بك بشكل غير متزامن.
مقدّمة
Active Job هي إطار عمل للتصريح عن الوظائف وجعلها تعمل على مجموعة متنوعة من نظم الطوابير الخلفية (queuing backends). يمكن أن تكون هذه الوظائف أي شيء بدءًا من التنظيف المنتظم إلى رسوم الفواتير والمراسلات البريدية أو أي شيء يمكن تقسيمه لأجزاء صغيرة وتشغيله بالتوازي فعليًّا.
الغرض من Active Job
النقطة الأساسية هي التأكّد من أن جميع تطبيقات ريلز تحتوي بنية تحتيّة وظيفيّة جاهزة. يمكننا بعدها بناء ميزات إطار العمل والجواهر (gems) الأخرى بناءً على ذلك دون القلق بشأن اختلافات الواجهة البرمجية بين مُشغّلي الوظائف المختلفين مثل Delayed Job و Resque. هكذا يصبح اختيار خلفية طابور الانتظار مجرّد شأن عمليّاتي. ويصبح بإمكانك التبديل بينهم دون إعادة كتابة وظائفك.
ملاحظة: يأتي ريلز افتراضيًّا مع تنفيذ عملية الاصطفاف في الطابور غير المتزامنة (asynchronous queuing implementation) التي تُشغّل الوظائف عبر مجمِّع خيط داخل العمليّة (in-process thread pool). ستُشغَّل الوظائف بشكل غير متزامن ولكن ستُزال الوظائف من الطابور عند إعادة التشغيل.
إنشاء وظيفة
سيُوفّر هذا القسم دليلًا تفصيليًا لإنشاء وظيفة وإضافتها إلى طابور.
أنشئ الوظيفة
يُوفّر Active Job مُولّد ريلز لإنشاء وظائف. ستنشئ الشيفرة التالية وظيفة في app/jobs (مع حالة اختبار مرفقة ضمن test/jobs):
$ bin/rails generate job guests_cleanup
invoke test_unit
create test/jobs/guests_cleanup_job_test.rb
create app/jobs/guests_cleanup_job.rb
يمكنك أيضًا إنشاء وظيفة تعمل في طابور محدّد:
$ bin/rails generate job guests_cleanup --queue urgent
إن لم ترغب في استخدام مولّد، فيمكنك إنشاء ملفك الخاص داخل app/jobs ولكن تأكد فقط من أنه يرث من ApplicationJob
.
إليك ما تبدو عليه الوظيفة:
class GuestsCleanupJob < ApplicationJob
queue_as :default
def perform(*guests)
# Do something later
end
end
لاحظ أن بإمكانك تعريف perform
مع أي عدد تريده من الوسائط.
وضع الوظيفة في طابور
إدراج وظيفة في طابور يكون على النحو التالي:
# ضع وظيفة في الطابور لتُنفّذ فور فراغ نظام الطوابير
GuestsCleanupJob.perform_later guest
# ضع وظيفة في الطابور لتُنفّذ غدًا بمنتصف النهار
GuestsCleanupJob.set(wait_until: Date.tomorrow.noon).perform_later(guest)
# ضع وظيفة في الطابور لتُنفّذ بعد أسبوع من الآن
GuestsCleanupJob.set(wait: 1.week).perform_later(guest)
# وراء الكواليس كي `perform` التابع `perform_now` و `perform_later` سيستدعي
# تستطيع تمرير عدد الوسائط الذي عرّفته في الأخير
GuestsCleanupJob.perform_later(guest1, guest2, filter: 'some_filter')
هذا ما نحتاج إلى فعله فقط!
تنفيذ الوظيفة
تحتاج لإعداد نظام طابور خلفي (queuing backend) وضع الوظائف بطوابير وتنفيذها عند الإنتاج، وهذا يعني أنك بحاجة لاختيار مكتبة الطابور من جهة خارجية كي يستخدمها ريلز. يُوفّر ريلز نفسه نظام طوابير ضمن العملية (in-process queuing system) فقط والذي يحتفظ بالوظائف فقط في الذاكرة RAM. ستفقد جميع الوظائف المُعلّقة إن تعطلت العمليّة أو أُعيد ضبط الجهاز مع الخلفية الافتراضية غير المتزامنة (default async backend). قد يكون هذا مناسبًا للتطبيقات الأصغر أو الوظائف غير المهمّة ولكن معظم تطبيقات الإنتاج ستحتاج لاختيار نظم خلفيّة دائمة ومستقرة.
النظم الخلفية
تحتوي Active Job على محولات (adapters) مُدمجة لعدّة نظم طوابير خلفية (مثل Sidekiq و Resque و Delayed Job وغيرها). راجع توثيق الواجهة البرمجية من أجل ActiveJob::QueueAdapters
للحصول على قائمة مُحدّثة من المحولات.
ضبط النظام الخلفي
يُمكنك ضبط النظام الخلفي لطابورك بسهولة:
# config/application.rb
module YourApp
class Application < Rails::Application
# على جوهرة المحول Gemfile تأكد من احتواء
# واتبع تعليمات التثبيت والنشر
config.active_job.queue_adapter = :sidekiq
end
end
يمكنك أيضا إعداد النظام الخلفي حسب الوظيفة:
class GuestsCleanupJob < ApplicationJob
self.queue_adapter = :resque
#....
end
# كمحول طابور للنظام الخلفي `resque` الان ستستخدم وظيفتك
# `config.active_job.queue_adapter` ممّا يستبدل ما ضبط في
إطلاق النظام الخلفي
نظرًا لعمل الوظائف بالتوازي مع تطبيق ريلز الخاص بك، تتطلّب معظم مكتبات الطوابير أن تُشغّل خدمة طابور خاصة بمكتبة ما (بالإضافة إلى تشغيل تطبيق ريلز) حتى تعمل معالجة الوظائف بشكل صحيح. راجع توثيق المكتبات الآتية للحصول على إرشادات حول إطلاق النظام الخلفي لطابورك.
فيما يلي قائمة غير شاملة من التوثيقات:
الطوابير
تدعم معظم المحولات (adapters) عدّة طوابير. يمكنك باستخدام Active Job جدولة الوظيفة لتُنفّذ في طابور محدّد:
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
يمكنك أن تلحق (prefix) اسم الطابور لجميع وظائفك باستخدام config.active_job.queue_name_prefix في application.rb:
# config/application.rb
module YourApp
class Application < Rails::Application
config.active_job.queue_name_prefix = Rails.env
end
end
# app/jobs/guests_cleanup_job.rb
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
# بالبيئة production.low_priority ستُنفّذ وظيفتك الآن في الطابور
# staging.low_priority الإنتاجية وعلى
# (staging environment) ببيئة التطوير المحلية
محدِّد لاحقة اسم الطابور الافتراضي هو '_'. يمكن تغييره عبر ضبط config.active_job.queue_name_delimiter في application.rb:
# config/application.rb
module YourApp
class Application < Rails::Application
config.active_job.queue_name_prefix = Rails.env
config.active_job.queue_name_delimiter = '.'
end
end
# app/jobs/guests_cleanup_job.rb
class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
# بالبيئة production.low_priority ستُنفّذ وظيفتك الآن في الطابور
# staging.low_priority الإنتاجية وعلى
# (staging environment) ببيئة التطوير المحلية
إن أردت تحكّمًا أكبر في الطابور الذي سيُنفّذ الوظيفة، تستطيع تمرير الخيار :queue
إلى التابع .set
:
MyJob.set(queue: :another_queue).perform_later(record)
للتحكم في الطابور من مستوى الوظيفة، تستطيع تمرير كتلة إلى التابع .queue_as
. ستُنفّذ الكتلة في سياق الوظيفة (وبذلك تستطيع الوصول إلى self.arguments
) ويجب إعادة اسم الطابور:
class ProcessVideoJob < ApplicationJob
queue_as do
video = self.arguments.first
if video.owner.premium?
:premium_videojobs
else
:videojobs
end
end
def perform(video)
# video نفذ العملية
end
end
ProcessVideoJob.perform_later(Video.last)
ملاحظة: تاكّد من "إنصات" النظام الخلفي لطابورك (queuing backend) لاسم طابورك. ستحتاج ببعض النظم الخلفية إلى تحديد الطوابير التي يجب الإنصات لها.
ردود النداء
توفّر Active Job خطافات لإطلاق (trigger) المنطق خلال دورة حياة الوظيفة. تستطيع تنفيذ ردود النداء كتوابع بسيطة مثل ردود النداء الأخرى في ريلز واستخدام تابع صنف شبيه بالماكرو لتسجيلها كردود نداء:
class GuestsCleanupJob < ApplicationJob
queue_as :default
around_perform :around_cleanup
def perform
# افعل شيئًا لاحقًا
end
private
def around_cleanup(job)
# perform افعل شيئًا قبل
yield
# perform افعل شيئًا بعد
end
end
تستطيع توابع الأصناف ذات نمط الماكرو (macro-style class methods) أيضًا تلقّي كتلة (block). فكّر في استخدام هذا الأسلوب إن كانت التعليمات البرمجيّة داخل الكتلة قصيرة كفاية ويتسع في سطر واحد. تستطيع مثلًا إرسال مقاييس (metrics) مع كل وظيفة موضوعة بطابور:
class ApplicationJob
before_enqueue { |job| $statsd.increment "#{job.name.underscore}.enqueue" }
end
ردود النداء المتوافرة
before_enqueue
around_enqueue
after_enqueue
before_perform
around_perform
after_perform
إرسال رسائل البريد
إحدى الوظائف الأكثر شيوعًا في تطبيقات الويب الحديثة هي إرسال رسائل البريد الإلكتروني خارج دورة الطلب-الاستجابة، بحيث لا يضطر المستخدم إلى انتظار انتهائها. إنَّ Active Job مُدمجةٌ مع Action Mailer بحيث تستطيع إرسال البريد الإلكتروني بشكل غير متزامن بسهولة:
# إن أردت ارسال البريد الالكتروني الان .deliver_now استخدم
UserMailer.welcome(@user).deliver_now
# .deliver_later إن أردت إرسال البريد الإلكتروني عبر وظيفة فعالة، فاستخدم
UserMailer.welcome(@user).deliver_later
ملاحظة: يفشل عمومًا استخدام الطابور غير المتزامن من مهمة Rake (لإرسال بريد إلكتروني باستخدام deliver_later.
مثلًا) لأنه من المرجح أن ينتهي Rake مما يؤدي لحذف مجمِّع خيط المهام في العملية، قبل مُعالجة أي أو كافّة رسائل البريد الإلكتروني deliver_later.
. استخدم deliver_now.
أو شغّل طابورًا مستمرًّا (persistent queue) عند التطوير لتجنب هذه المشكلة.
التدويل (Internationalization)
تستخدم كل وظيفة المجموعة I18n.locale
عند إنشاء الوظيفة. وهي مفيدة إذا أرسلت رسائل البريد الإلكتروني بشكل غير متزامن:
I18n.locale = :eo
UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto.
GlobalID
Active Job تدعم GlobalID للمعاملات ممّا يجعل تمرير كائنات Active Record مباشرةً إلى وظيفتك ممكنًا بدلًا من الأزواج صنف/معرِّف (class/id) التي تضطرّ بعدها لإلغاء التسلسل يدويًا. سابقًا بدت الوظائف كما يلي:
class TrashableCleanupJob < ApplicationJob
def perform(trashable_class, trashable_id, depth)
trashable = trashable_class.constantize.find(trashable_id)
trashable.cleanup(depth)
end
end
تستطيع الآن ببساطة أن تقوم بالتالي:
class TrashableCleanupJob < ApplicationJob
def perform(trashable, depth)
trashable.cleanup(depth)
end
end
الاستثناءات
توفر Active Job طريقةً لالتقاط الاستثناءات التي رُميَت أثناء تنفيذ الوظيفة:
class GuestsCleanupJob < ApplicationJob
queue_as :default
rescue_from(ActiveRecord::RecordNotFound) do |exception|
# افعل شيئًا للاستثناء
end
def perform
# افعل شيئًا ما لاحقًا
end
end
إعادة محاولة أو تجاهل الوظائف الفاشلة
من الممكن أيضًا إعادة محاولة تنفيذ أو تجاهل وظيفة إن رُفع استثناء أثناء التنفيذ. على سبيل المثال:
class RemoteServiceJob < ApplicationJob
retry_on CustomAppException # defaults to 3s wait, 5 attempts
discard_on ActiveJob::DeserializationError
def perform(*args)
# CustomAppException أو ActiveJob::DeserializationError قد يرمي
end
end
للمزيد من التفاصيل، راجع توثيق الواجهة ActiveJob::Exceptions
البرمجية.
إلغاء السَلسَلة (Deserialization)
يسمح GlobalID بسَلسَلة (serializing) كائنات Active Record الكاملة المُمرّرة إلى التابع .perform
.
إن حُذف سجل مُمرّر بعد وضع الوظيفة في طابور وقبل استدعاء التابع .perform
، فسترمي Active Job الاستثناء ActiveJob::DeserializationError
.
اختبار الوظيفة
تستطيع إيجاد إرشادات مفصلة حول كيفيّة اختبار وظائفك في دليل اختبار تطبيقات ريلز.