أساسيات الوظيفة الفعالة في ريلز
يوفّر لك هذا الدليل كل ما تحتاج إليه للبدء في إنشاء وظائف خلفية (background jobs) وإدراجها بطوابير الانتظار وتنفيذها. بعد قراءة هذا الدليل، ستتعلم:
- كيفيّة إنشاء وظائف.
- كيفيّة إدراج الوظائف بالطوابير.
- كيفيّة تشغيل الوظائف في الخلفية.
- كيفيّة إرسال رسائل البريد الإلكتروني من التطبيق الخاص بك بشكل غير متزامن.
مقدّمة
الوظيفة الفعَّالة (Active Job) هي إطار عمل للتصريح عن الوظائف وجعلها تعمل على مجموعة متنوعة من نظم الطوابير الخلفية (queuing backends). يمكن أن تكون هذه الوظائف أي شيء بدءًا من التنظيف المنتظم إلى رسوم الفواتير والمراسلات البريدية أو أي شيء يمكن تقسيمه لأجزاء صغيرة وتشغيله بالتوازي فعليًّا.
الغرض من الوظيفة الفعالة
النقطة الأساسية هي التأكّد من أن جميع تطبيقات ريلز تحتوي بنية تحتيّة وظيفيّة جاهزة. يمكننا بعدها بناء ميزات إطار العمل والجواهر (gems) الأخرى بناءً على ذلك دون القلق بشأن اختلافات الواجهة البرمجية بين مُشغّلي الوظائف المختلفين مثل Delayed Job و Resque. هكذا يصبح اختيار خلفية طابور الانتظار مجرّد شأن عمليّاتي. ويصبح بإمكانك التبديل بينهم دون إعادة كتابة وظائفك.
ملاحظة: يأتي ريلز افتراضيًّا مع تنفيذ عملية الاصطفاف في الطابور غير المتزامنة (asynchronous queuing implementation) التي تُشغّل الوظائف عبر مجمِّع خيط داخل العمليّة (in-process thread pool). ستُشغَّل الوظائف بشكل غير متزامن ولكن ستُزال الوظائف من الطابور عند إعادة التشغيل.
إنشاء وظيفة
سيُوفّر هذا القسم دليلًا تفصيليًا لإنشاء وظيفة وإضافتها إلى طابور.
أنشئ الوظيفة
يُوفّر الإجراء 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) لاسم طابورك. ستحتاج ببعض النظم الخلفية إلى تحديد الطوابير التي يجب الإنصات لها.
ردود النداء
توفّر الوظيفة الفعالة خطافات لإطلاق (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
الإجراء Mailer
إحدى الوظائف الأكثر شيوعًا في تطبيقات الويب الحديثة هي إرسال رسائل البريد الإلكتروني خارج دورة الطلب-الاستجابة، بحيث لا يضطر المستخدم إلى انتظار انتهائها. الوظيفة الفعالة مُدمجةٌ مع الإجراء 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
الوظيفة الفعالة تدعم GlobalID للمعاملات ممّا يجعل تمرير كائنات السجل الفعال مباشرةً إلى وظيفتك ممكنًا بدلًا من الأزواج صنف/معرِّف (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
الاستثناءات
توفر الوظيفة الفعالة طريقةً لالتقاط الاستثناءات التي رُميَت أثناء تنفيذ الوظيفة:
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) كائنات السجل الفعال الكاملة المُمرّرة إلى التابع .perform
.
إن حُذف سجل مُمرّر بعد وضع الوظيفة في طابور وقبل استدعاء التابع .perform
، فسترمي الوظيفة الفعالة الاستثناء ActiveJob::DeserializationError
.
اختبار الوظيفة
تستطيع إيجاد إرشادات مفصلة حول كيفيّة اختبار وظائفك في دليل اختبار تطبيقات ريلز.