أساسيات 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.

اختبار الوظيفة

تستطيع إيجاد إرشادات مفصلة حول كيفيّة اختبار وظائفك في دليل اختبار تطبيقات ريلز.

مصادر