Solid Queue عبارة عن واجهة خلفية لقائمة الانتظار قائمة على قاعدة البيانات لـ Active Job، وقد تم تصميمها مع مراعاة البساطة والأداء.
إلى جانب قائمة المهام المنتظمة ومعالجتها، تدعم Solid Queue المهام المؤجلة، وضوابط التزامن، والوظائف المتكررة، وقوائم الانتظار المؤقتة، والأولويات الرقمية لكل مهمة، والأولويات حسب ترتيب قائمة الانتظار، والقائمة المجمعة ( enqueue_all
for Active Job's perform_all_later
).
يمكن استخدام قائمة الانتظار الصلبة مع قواعد بيانات SQL مثل MySQL أو PostgreSQL أو SQLite، وهي تستفيد من عبارة FOR UPDATE SKIP LOCKED
، إذا كانت متوفرة، لتجنب الحظر وانتظار الأقفال عند استقصاء المهام. وهو يعتمد على Active Job لإعادة المحاولة، أو التجاهل، أو معالجة الأخطاء، أو التسلسل، أو التأخير، وهو متوافق مع خيوط Ruby on Rails المتعددة.
يتم تكوين قائمة الانتظار الصلبة افتراضيًا في تطبيقات Rails 8 الجديدة. ولكن إذا كنت تستخدم إصدارًا سابقًا، فيمكنك إضافته يدويًا باتباع الخطوات التالية:
bundle add solid_queue
bin/rails solid_queue:install
سيؤدي هذا إلى تكوين Solid Queue كواجهة خلفية لـ Active Job للإنتاج، وإنشاء ملفات التكوين config/queue.yml
و config/recurring.yml
، وإنشاء db/queue_schema.rb
. سيؤدي أيضًا إلى إنشاء غلاف قابل للتنفيذ bin/jobs
والذي يمكنك استخدامه لبدء Solid Queue.
بمجرد الانتهاء من ذلك، سيتعين عليك بعد ذلك إضافة التكوين لقاعدة بيانات قائمة الانتظار في config/database.yml
. إذا كنت تستخدم SQLite، فسيبدو الأمر كما يلي:
production :
primary :
<< : *default
database : storage/production.sqlite3
queue :
<< : *default
database : storage/production_queue.sqlite3
migrations_paths : db/queue_migrate
...أو إذا كنت تستخدم MySQL/PostgreSQL/Trilogy:
production :
primary : &primary_production
<< : *default
database : app_production
username : app
password : <%= ENV["APP_DATABASE_PASSWORD"] %>
queue :
<< : *primary_production
database : app_production_queue
migrations_paths : db/queue_migrate
ملاحظة: سيؤدي استدعاء bin/rails solid_queue:install
تلقائيًا إلى إضافة config.solid_queue.connects_to = { database: { writing: :queue } }
إلى config/environments/production.rb
، لذلك لا يلزم أي تكوين إضافي هناك (على الرغم من أنه يجب عليك التأكد أن تستخدم اسم queue
في database.yml
لكي يتطابق هذا!). ولكن إذا كنت تريد استخدام Solid Queue في بيئة مختلفة (مثل التدريج أو حتى التطوير)، فسيتعين عليك إضافة سطر config.solid_queue.connects_to
يدويًا إلى ملف البيئة المعني. وكما هو الحال دائمًا، تأكد من أن الاسم الذي تستخدمه لقاعدة البيانات في config/database.yml
يطابق الاسم الذي تستخدمه في config.solid_queue.connects_to
.
ثم قم بتشغيل db:prepare
في الإنتاج للتأكد من إنشاء قاعدة البيانات وتحميل المخطط.
أنت الآن جاهز لبدء معالجة المهام عن طريق تشغيل bin/jobs
على الخادم الذي يقوم بالعمل. سيؤدي هذا إلى بدء معالجة المهام في كافة قوائم الانتظار باستخدام التكوين الافتراضي. انظر أدناه لمعرفة المزيد حول تكوين قائمة الانتظار الصلبة.
بالنسبة للمشاريع الصغيرة، يمكنك تشغيل Solid Queue على نفس الجهاز مثل خادم الويب الخاص بك. عندما تكون جاهزًا للتوسع، تدعم Solid Queue التوسع الأفقي خارج الصندوق. يمكنك تشغيل Solid Queue على خادم منفصل عن خادم الويب الخاص بك، أو حتى تشغيل bin/jobs
على أجهزة متعددة في نفس الوقت. اعتمادًا على التكوين، يمكنك تعيين بعض الأجهزة لتشغيل المرسلين فقط أو العمال فقط. راجع قسم التكوين لمزيد من التفاصيل حول هذا.
ملاحظة : التغييرات المستقبلية في المخطط ستأتي في شكل عمليات ترحيل منتظمة.
يوصى بتشغيل Solid Queue في قاعدة بيانات منفصلة، ولكن من الممكن أيضًا استخدام قاعدة بيانات واحدة لكل من التطبيق وقائمة الانتظار. فقط اتبع الخطوات التالية:
db/queue_schema.rb
إلى الترحيل العادي واحذف db/queue_schema.rb
config.solid_queue.connects_to
من production.rb
bin/jobs
لن يكون لديك قواعد بيانات متعددة، لذلك لا يحتاج database.yml
إلى قاعدة بيانات أساسية وقاعدة بيانات قائمة الانتظار.
إذا كنت تخطط لاعتماد Solid Queue بشكل متزايد عن طريق تبديل وظيفة واحدة في كل مرة، فيمكنك القيام بذلك عن طريق ترك config.active_job.queue_adapter
مضبوطًا على الواجهة الخلفية القديمة لديك، ثم قم بتعيين queue_adapter
مباشرةً في الوظائف التي تنقلها:
# app/jobs/my_job.rb
class MyJob < ApplicationJob
self . queue_adapter = :solid_queue
# ...
end
تم تصميم Solid Queue للحصول على أعلى إنتاجية عند استخدامها مع MySQL 8+ أو PostgreSQL 9.5+، لأنها تدعم FOR UPDATE SKIP LOCKED
. يمكنك استخدامه مع الإصدارات الأقدم، ولكن في هذه الحالة، قد تواجه حالة انتظار القفل إذا قمت بتشغيل عدة عاملين لنفس قائمة الانتظار. يمكنك أيضًا استخدامه مع SQLite على التطبيقات الأصغر.
لدينا عدة أنواع من الممثلين في Solid Queue:
solid_queue_ready_executions
.solid_queue_scheduled_executions
إلى جدول solid_queue_ready_executions
حتى يتمكن العمال من التقاطها. علاوة على ذلك، يقومون ببعض أعمال الصيانة المتعلقة بضوابط التزامن.سيقوم مشرف Solid Queue بصياغة عملية منفصلة لكل عامل/مرسل/مجدول تحت الإشراف.
افتراضيًا، ستحاول Solid Queue العثور على التكوين الخاص بك ضمن config/queue.yml
، ولكن يمكنك تعيين مسار مختلف باستخدام متغير البيئة SOLID_QUEUE_CONFIG
أو باستخدام الخيار -c/--config_file
مع bin/jobs
، كما يلي:
bin/jobs -c config/calendar.yml
هذا ما يبدو عليه هذا التكوين:
production :
dispatchers :
- polling_interval : 1
batch_size : 500
concurrency_maintenance_interval : 300
workers :
- queues : " * "
threads : 3
polling_interval : 2
- queues : [ real_time, background ]
threads : 5
polling_interval : 0.1
processes : 3
كل شيء اختياري. إذا لم يتم توفير أي تكوين على الإطلاق، فسيتم تشغيل Solid Queue مع مرسل واحد وعامل واحد بالإعدادات الافتراضية. إذا كنت تريد تشغيل المرسلين أو العمال فقط، فأنت بحاجة فقط إلى تضمين هذا القسم وحده في التكوين. على سبيل المثال، مع التكوين التالي:
production :
dispatchers :
- polling_interval : 1
batch_size : 500
concurrency_maintenance_interval : 300
سيقوم المشرف بتشغيل مرسل واحد وليس هناك عمال.
فيما يلي نظرة عامة على الخيارات المختلفة:
polling_interval
: الفاصل الزمني بالثواني الذي سينتظره العمال والمرسلون قبل التحقق من وجود المزيد من المهام. هذه المرة الافتراضية هي ثانية 1
للمرسلين و 0.1
ثانية للعمال.
batch_size
: سيقوم المرسل بإرسال المهام على دفعات بهذا الحجم. الافتراضي هو 500.
concurrency_maintenance_interval
: الفاصل الزمني بالثواني الذي سينتظره المرسل قبل التحقق من المهام المحظورة التي يمكن إلغاء حظرها. اقرأ المزيد حول عناصر التحكم في التزامن لمعرفة المزيد حول هذا الإعداد. الافتراضي هو 600
ثانية.
queues
: قائمة قوائم الانتظار التي سيختار العمال الوظائف منها. يمكنك استخدام *
للإشارة إلى جميع قوائم الانتظار (وهو أيضًا الإعداد الافتراضي والسلوك الذي ستحصل عليه إذا حذفت هذا). يمكنك توفير قائمة انتظار واحدة، أو قائمة قوائم الانتظار كمصفوفة. سيتم استقصاء الوظائف من قوائم الانتظار هذه بالترتيب، لذلك، على سبيل المثال، باستخدام [ real_time, background ]
، لن يتم أخذ أي وظائف من background
ما لم تكن هناك أية وظائف أخرى تنتظر في real_time
. يمكنك أيضًا توفير بادئة مع حرف بدل لمطابقة قوائم الانتظار التي تبدأ ببادئة. على سبيل المثال:
staging :
workers :
- queues : staging*
threads : 3
polling_interval : 5
سيؤدي هذا إلى إنشاء عامل يقوم بإحضار المهام من جميع قوائم الانتظار بدءًا من staging
. يُسمح بحرف البدل *
فقط بمفرده أو في نهاية اسم قائمة الانتظار؛ لا يمكنك تحديد أسماء قوائم الانتظار مثل *_some_queue
. سيتم تجاهل هذه.
أخيرًا، يمكنك دمج البادئات مع الأسماء الدقيقة، مثل [ staging*, background ]
، وسيكون السلوك فيما يتعلق بالترتيب هو نفسه كما هو الحال مع الأسماء الدقيقة فقط.
تحقق من الأقسام أدناه حول كيفية تصرف ترتيب قائمة الانتظار مع الأولويات، وكيف يمكن أن تؤثر الطريقة التي تحدد بها قوائم الانتظار لكل عامل على الأداء.
threads
: هذا هو الحد الأقصى لحجم تجمع مؤشرات الترابط الذي سيتعين على كل عامل تشغيل المهام. سيقوم كل عامل بجلب هذا العدد من المهام من قائمة الانتظار (قوائم الانتظار) الخاصة به، على الأكثر، وسيقوم بنشرها في تجمع مؤشرات الترابط ليتم تشغيله. بشكل افتراضي، هذا هو 3
. العمال فقط لديهم هذا الإعداد.
processes
: هذا هو عدد العمليات المنفذة التي سيتم تشعبها بواسطة المشرف بالإعدادات المحددة. بشكل افتراضي، هذا هو 1
، مجرد عملية واحدة. يعد هذا الإعداد مفيدًا إذا كنت تريد تخصيص أكثر من وحدة معالجة مركزية واحدة لقائمة انتظار أو قوائم انتظار بنفس التكوين. العمال فقط لديهم هذا الإعداد.
concurrency_maintenance
: ما إذا كان المرسل سيقوم بأعمال صيانة التزامن. يكون هذا true
بشكل افتراضي، وهو مفيد إذا كنت لا تستخدم أي عناصر تحكم في التزامن وتريد تعطيله أو إذا قمت بتشغيل عدة مرسلين وتريد أن يقوم بعضهم بإرسال المهام فقط دون القيام بأي شيء آخر.
كما هو مذكور أعلاه، إذا قمت بتحديد قائمة قوائم الانتظار للعامل، فسيتم استقصاؤها بالترتيب المحدد، مثل القائمة real_time,background
، ولن يتم أخذ أي وظائف من background
ما لم تكن هناك أي وظائف أخرى تنتظر real_time
.
يدعم Active Job أيضًا أولويات الأعداد الصحيحة الموجبة عند وضع المهام في قائمة الانتظار. في قائمة الانتظار الصلبة، كلما كانت القيمة أصغر، زادت الأولوية. الافتراضي هو 0
.
يكون هذا مفيدًا عند تشغيل مهام ذات أهمية أو أهمية مختلفة في نفس قائمة الانتظار. ضمن نفس قائمة الانتظار، سيتم انتقاء الوظائف حسب الأولوية، ولكن في قائمة قوائم الانتظار، يكون لترتيب قائمة الانتظار الأسبقية، لذلك في المثال السابق مع real_time,background
، سيتم انتقاء الوظائف في قائمة انتظار real_time
قبل الوظائف في background
قائمة الانتظار، حتى لو كانت تلك الموجودة في قائمة انتظار background
لها أولوية أعلى (قيمة أصغر).
نوصي بعدم الخلط بين ترتيب قائمة الانتظار والأولويات ولكن إما اختيار واحدة أو أخرى، لأن ذلك سيجعل ترتيب تنفيذ المهمة أكثر وضوحًا بالنسبة لك.
للحفاظ على أداء الاستقصاء والتأكد من استخدام فهرس التغطية دائمًا، تقوم Solid Queue بإجراء نوعين فقط من استعلامات الاستقصاء:
-- No filtering by queue
SELECT job_id
FROM solid_queue_ready_executions
ORDER BY priority ASC , job_id ASC
LIMIT ?
FOR UPDATE SKIP LOCKED;
-- Filtering by a single queue
SELECT job_id
FROM solid_queue_ready_executions
WHERE queue_name = ?
ORDER BY priority ASC , job_id ASC
LIMIT ?
FOR UPDATE SKIP LOCKED;
يتم استخدام الخيار الأول (بدون تصفية حسب قائمة الانتظار) عند التحديد
queues : *
ولا توجد أي قوائم انتظار متوقفة مؤقتًا، لأننا نريد استهداف كافة قوائم الانتظار.
في حالات أخرى، نحتاج إلى قائمة من قوائم الانتظار للتصفية حسب الترتيب، لأننا لا نستطيع التصفية إلا حسب قائمة انتظار واحدة في المرة الواحدة للتأكد من أننا نستخدم فهرسًا للفرز. هذا يعني أنه إذا قمت بتحديد قوائم الانتظار الخاصة بك على النحو التالي:
queues : beta*
سنحتاج إلى الحصول على قائمة بجميع قوائم الانتظار الموجودة التي تطابق تلك البادئة أولاً، مع استعلام قد يبدو كما يلي:
SELECT DISTINCT (queue_name)
FROM solid_queue_ready_executions
WHERE queue_name LIKE ' beta% ' ;
يمكن تنفيذ هذا النوع من الاستعلامات DISTINCT
على العمود الموجود في أقصى اليسار في الفهرس بسرعة كبيرة في MySQL بفضل تقنية تسمى فحص الفهرس فضفاض. ومع ذلك، فإن PostgreSQL وSQLite لا يطبقان هذه التقنية، مما يعني أنه إذا كان جدول solid_queue_ready_executions
الخاص بك كبيرًا جدًا لأن قوائم الانتظار الخاصة بك أصبحت عميقة جدًا، فسيصبح هذا الاستعلام بطيئًا. عادةً ما يكون جدول solid_queue_ready_executions
الخاص بك صغيرًا، ولكن يمكن أن يحدث ذلك.
كما هو الحال مع استخدام البادئات، سيحدث الشيء نفسه إذا قمت بإيقاف قوائم الانتظار مؤقتًا، لأننا نحتاج إلى الحصول على قائمة بجميع قوائم الانتظار مع استعلام مثل
SELECT DISTINCT (queue_name)
FROM solid_queue_ready_executions
ثم قم بإزالة تلك المتوقفة مؤقتًا. والتوقف بشكل عام يجب أن يكون أمراً نادراً، ويستخدم في ظروف خاصة، ولفترة زمنية قصيرة. إذا كنت لا ترغب في معالجة المهام من قائمة الانتظار بعد الآن، فإن أفضل طريقة للقيام بذلك هي إزالتها من قائمة قوائم الانتظار الخاصة بك.
باختصار، إذا كنت تريد ضمان الأداء الأمثل في عملية الاقتراع ، فإن أفضل طريقة للقيام بذلك هي تحديد الأسماء الدقيقة لها دائمًا، وعدم إيقاف أي قوائم انتظار مؤقتًا.
افعل هذا:
queues : background, backend
بدلا من هذا:
queues : back*
يستخدم العاملون في قائمة الانتظار الصلبة تجمع مؤشرات الترابط لتشغيل العمل في سلاسل رسائل متعددة، ويمكن تكوينه عبر معلمة threads
أعلاه. بالإضافة إلى ذلك، يمكن تحقيق التوازي عبر عمليات متعددة على جهاز واحد (يمكن تكوينه عبر عمال مختلفين أو معلمة processes
أعلاه) أو عن طريق القياس الأفقي.
ويتولى المشرف إدارة هذه العمليات ويستجيب للإشارات التالية:
TERM
، INT
: يبدأ الإنهاء الرشيق. سيرسل المشرف إشارة TERM
إلى العمليات الخاضعة للإشراف، وسينتظر حتى وقت SolidQueue.shutdown_timeout
حتى تنتهي. إذا كانت هناك أية عمليات خاضعة للإشراف لا تزال موجودة بحلول ذلك الوقت، فسيتم إرسال إشارة QUIT
إليهم للإشارة إلى أنه يجب عليهم الخروج.QUIT
: يبدأ الإنهاء الفوري. سيقوم المشرف بإرسال إشارة QUIT
إلى العمليات الخاضعة للإشراف، مما يؤدي إلى خروجهم على الفور. عند تلقي إشارة QUIT
، إذا كان العمال لا يزال لديهم وظائف على متن الطائرة، فسيتم إرجاعها إلى قائمة الانتظار عندما يتم إلغاء تسجيل العمليات.
إذا لم يكن لدى العمليات فرصة للتنظيف قبل الخروج (على سبيل المثال، إذا قام شخص ما بسحب كابل في مكان ما)، فقد تظل المهام أثناء الرحلة مطالبة بها من خلال العمليات التي تنفذها. ترسل العمليات نبضات القلب، ويقوم المشرف بفحص العمليات التي تحتوي على نبضات منتهية الصلاحية، مما سيؤدي إلى تحرير أي وظائف تمت المطالبة بها مرة أخرى إلى قوائم الانتظار الخاصة بهم. يمكنك تكوين كل من وتيرة نبضات القلب والعتبة لاعتبار العملية ميتة. انظر القسم أدناه لهذا الغرض.
يمكنك تكوين قاعدة البيانات التي تستخدمها Solid Queue عبر الخيار config.solid_queue.connects_to
في ملفات التكوين config/application.rb
أو config/environments/production.rb
. افتراضيًا، يتم استخدام قاعدة بيانات واحدة لكل من الكتابة والقراءة تسمى queue
لمطابقة تكوين قاعدة البيانات التي قمت بإعدادها أثناء التثبيت.
يمكن هنا استخدام جميع الخيارات المتاحة لـ Active Record لقواعد بيانات متعددة.
في قائمة الانتظار الصلبة، يمكنك ربط نقطتين مختلفتين في حياة المشرف:
start
: بعد أن ينتهي المشرف من التشغيل وقبل أن يقوم بتقسيم العمال والمرسلين مباشرة.stop
: بعد تلقي إشارة ( TERM
، INT
، أو QUIT
) وقبل بدء إيقاف التشغيل الفوري أو الآمن.وإلى نقطتين مختلفتين في حياة العامل:
worker_start
: بعد انتهاء العامل من التشغيل وقبل أن يبدأ حلقة الاستقصاء مباشرة.worker_stop
: بعد تلقي إشارة ( TERM
أو INT
أو QUIT
) وقبل بدء إيقاف التشغيل السلس أو الفوري (وهو مجرد exit!
).يمكنك استخدام الطرق التالية مع الكتلة للقيام بذلك:
SolidQueue . on_start
SolidQueue . on_stop
SolidQueue . on_worker_start
SolidQueue . on_worker_stop
على سبيل المثال:
SolidQueue . on_start { start_metrics_server }
SolidQueue . on_stop { stop_metrics_server }
يمكن استدعاء هذه عدة مرات لإضافة خطافات متعددة، ولكن يجب أن يحدث ذلك قبل بدء تشغيل Solid Queue. سيكون المُهيئ مكانًا جيدًا للقيام بذلك.
ملاحظة : يجب تعيين الإعدادات الموجودة في هذا القسم في config/application.rb
أو تكوين البيئة لديك مثل هذا: config.solid_queue.silence_polling = true
هناك العديد من الإعدادات التي تتحكم في كيفية عمل Solid Queue والتي يمكنك ضبطها أيضًا:
logger
: المسجل الذي تريد أن تستخدمه Solid Queue. الإعدادات الافتراضية لمسجل التطبيق.
app_executor
: منفذ تنفيذ Rails المستخدم لتغليف العمليات غير المتزامنة، ويكون افتراضيًا هو منفذ التطبيق
on_thread_error
: lambda/Proc مخصص للاتصال به عند وجود خطأ داخل مؤشر ترابط قائمة الانتظار الصلبة الذي يأخذ الاستثناء المرفوع كوسيطة. الافتراضات ل
-> ( exception ) { Rails . error . report ( exception , handled : false ) }
لا يتم استخدام هذا للأخطاء التي تظهر أثناء تنفيذ المهمة . تتم معالجة الأخطاء التي تحدث في المهام عن طريق retry_on
أو discard_on
الخاصين بـ Active Job، وستؤدي في النهاية إلى فشل المهام. هذا مخصص للأخطاء التي تحدث داخل Solid Queue نفسها.
use_skip_locked
: ما إذا كان سيتم استخدام FOR UPDATE SKIP LOCKED
عند إجراء قراءات القفل. سيتم اكتشاف هذا تلقائيًا في المستقبل، وفي الوقت الحالي، لن تحتاج إلى تعيين هذا على false
إلا إذا كانت قاعدة بياناتك لا تدعمه. بالنسبة لـ MySQL، ستكون هذه الإصدارات أقل من 8، وبالنسبة لـ PostgreSQL، ستكون الإصدارات أقل من 9.5. إذا كنت تستخدم SQLite، فلن يكون لذلك أي تأثير، لأن عمليات الكتابة تكون متسلسلة.
process_heartbeat_interval
: الفاصل الزمني لنبضات القلب الذي ستتبعه جميع العمليات — الافتراضي هو 60 ثانية.
process_alive_threshold
: مدة الانتظار حتى تعتبر العملية ميتة بعد آخر نبضة لها - القيمة الافتراضية هي 5 دقائق.
shutdown_timeout
: الوقت الذي سينتظره المشرف منذ أن أرسل إشارة TERM
إلى عملياته الخاضعة للإشراف قبل إرسال نسخة QUIT
إليهم لطلب الإنهاء الفوري - القيمة الافتراضية هي 5 ثوانٍ.
silence_polling
: ما إذا كان سيتم إسكات سجلات Active Record المنبعثة عند الاستقصاء لكل من العمال والمرسلين - القيمة الافتراضية هي true
.
supervisor_pidfile
: المسار إلى ملف pidfile الذي سيقوم المشرف بإنشائه عند التشغيل لمنع تشغيل أكثر من مشرف في نفس المضيف، أو في حالة رغبتك في استخدامه لفحص السلامة. إنه nil
افتراضيًا.
preserve_finished_jobs
: ما إذا كان سيتم الاحتفاظ بالمهام النهائية في جدول solid_queue_jobs
أم لا - القيمة الافتراضية هي true
.
clear_finished_jobs_after
: الفترة اللازمة للاحتفاظ بالمهام المنتهية، في حالة صحة preserve_finished_jobs
، تكون القيمة الافتراضية هي يوم واحد. ملاحظة: في الوقت الحالي، لا يوجد تنظيف تلقائي للمهام المنتهية. ستحتاج إلى القيام بذلك عن طريق استدعاء SolidQueue::Job.clear_finished_in_batches
بشكل دوري، ولكن هذا سيحدث تلقائيًا في المستقبل القريب.
default_concurrency_control_period
: القيمة التي سيتم استخدامها كقيمة افتراضية لمعلمة duration
في عناصر التحكم في التزامن. الافتراضي هو 3 دقائق.
ستؤدي قائمة الانتظار الصلبة إلى ظهور SolidQueue::Job::EnqueueError
لأي أخطاء في Active Record تحدث عند وضع مهمة في قائمة الانتظار. سبب عدم رفع ActiveJob::EnqueueError
هو أن هذا الخطأ تتم معالجته بواسطة Active Job، مما يتسبب في إرجاع perform_later
false
وتعيين job.enqueue_error
، مما يؤدي إلى تسليم المهمة إلى كتلة تحتاج إلى تمريرها إلى perform_later
. يعمل هذا بشكل جيد جدًا مع المهام الخاصة بك، ولكنه يجعل من الصعب جدًا التعامل مع الفشل في المهام التي تم وضعها في قائمة الانتظار بواسطة Rails أو غيرها من العناصر الجوهرية، مثل Turbo::Streams::BroadcastJob
أو ActiveStorage::AnalyzeJob
، لأنك لا تتحكم في الاتصال perform_later
في تلك الحالات.
في حالة المهام المتكررة، إذا ظهر مثل هذا الخطأ عند وضع الوظيفة المقابلة للمهمة في قائمة الانتظار، فسيتم التعامل معها وتسجيلها ولكنها لن تظهر.
تقوم Solid Queue بتوسيع Active Job مع عناصر التحكم في التزامن، مما يسمح لك بتحديد عدد المهام من نوع معين أو باستخدام وسائط معينة يمكن تشغيلها في نفس الوقت. عند تقييدها بهذه الطريقة، سيتم حظر المهام من التشغيل، وستظل محظورة حتى تنتهي مهمة أخرى وتزيل حظرها، أو بعد انقضاء وقت انتهاء الصلاحية المحدد ( مدة حد التزامن). لا يتم تجاهل الوظائف أو فقدانها أبدًا، بل يتم حظرها فقط.
class MyJob < ApplicationJob
limits_concurrency to : max_concurrent_executions , key : -> ( arg1 , arg2 , ** ) { ... } , duration : max_interval_to_guarantee_concurrency_limit , group : concurrency_group
# ...
key
هو المعلمة الوحيدة المطلوبة، ويمكن أن يكون رمزًا أو سلسلة أو عملية تتلقى وسيطات الوظيفة كمعلمات وسيتم استخدامها لتحديد المهام التي يجب أن تكون محدودة معًا. إذا قام proc بإرجاع سجل Active Record، فسيتم إنشاء المفتاح من اسم فئته id
.to
هو 1
بشكل افتراضي.duration
على SolidQueue.default_concurrency_control_period
افتراضيًا، والتي تكون في حد ذاتها 3 minutes
افتراضيًا، ولكن يمكنك تهيئتها أيضًا.group
للتحكم في تزامن فئات الوظائف المختلفة معًا. يتم تعيينه افتراضيًا على اسم فئة الوظيفة. عندما تتضمن مهمة ما عناصر التحكم هذه، سنضمن، على الأكثر، أن عدد المهام (المشار إليها to
) التي تنتج نفس key
سيتم تنفيذها بشكل متزامن، وسيستمر هذا الضمان طوال duration
كل مهمة مدرجة في قائمة الانتظار. لاحظ أنه لا يوجد ضمان بشأن ترتيب التنفيذ ، فقط فيما يتعلق بالمهام التي يتم تنفيذها في نفس الوقت (التداخل).
تستخدم حدود التزامن مفهوم الإشارات عند وضع قائمة الانتظار، وتعمل على النحو التالي: عندما يتم وضع مهمة في قائمة الانتظار، نتحقق مما إذا كانت تحدد ضوابط التزامن. إذا كان الأمر كذلك، فإننا نتحقق من الإشارة لمفتاح التزامن المحسوب. إذا كانت الإشارة مفتوحة نطالب بها ونجعل المهمة جاهزة . جاهز يعني أنه يمكن للعمال التقاطه للتنفيذ. عندما تنتهي المهمة من التنفيذ (سواء كانت ناجحة أو غير ناجحة، مما أدى إلى فشل التنفيذ)، فإننا نشير إلى الإشارة ونحاول إلغاء حظر المهمة التالية بنفس المفتاح، إن وجد. لا يعني إلغاء حظر المهمة التالية تشغيل هذه المهمة على الفور، ولكن يعني نقلها من المحظورة إلى الجاهزة . نظرًا لأنه قد يحدث شيء ما يمنع المهمة الأولى من تحرير الإشارة وإلغاء حظر المهمة التالية (على سبيل المثال، قيام شخص ما بسحب قابس في الجهاز الذي يعمل فيه العامل)، لدينا duration
باعتبارها آمنة من الفشل. الوظائف التي تم حظرها لأكثر من مدة هي وظائف مرشحة للتحرير، ولكن فقط بالقدر الذي تسمح به قواعد التزامن، حيث سيتعين على كل واحدة منها المرور عبر فحص رقص الإشارة. وهذا يعني أن duration
لا تتعلق حقًا بالمهمة التي تم وضعها في قائمة الانتظار أو التي يتم تشغيلها، ولكنها تتعلق بالوظائف المحظورة في الانتظار.
على سبيل المثال:
class DeliverAnnouncementToContactJob < ApplicationJob
limits_concurrency to : 2 , key : -> ( contact ) { contact . account } , duration : 5 . minutes
def perform ( contact )
# ...
حيث تكون contact
account
عبارة عن سجلات ActiveRecord
. في هذه الحالة، سنتأكد من تشغيل وظيفتين على الأكثر من نوع DeliverAnnouncementToContact
لنفس الحساب بشكل متزامن. إذا استغرقت إحدى هذه المهام، لأي سبب من الأسباب، أكثر من 5 دقائق أو لم تحرر قفل التزامن الخاص بها (إشارة إلى الإشارة) خلال 5 دقائق من الحصول عليها، فقد تحصل مهمة جديدة بنفس المفتاح على القفل.
دعونا نرى مثالاً آخر باستخدام group
:
class Box :: MovePostingsByContactToDesignatedBoxJob < ApplicationJob
limits_concurrency key : -> ( contact ) { contact } , duration : 15 . minutes , group : "ContactActions"
def perform ( contact )
# ...
class Bundle :: RebundlePostingsJob < ApplicationJob
limits_concurrency key : -> ( bundle ) { bundle . contact } , duration : 15 . minutes , group : "ContactActions"
def perform ( bundle )
# ...
في هذه الحالة، إذا كانت لدينا مهمة Box::MovePostingsByContactToDesignatedBoxJob
في قائمة الانتظار لسجل جهة اتصال بالمعرف 123
ومهمة Bundle::RebundlePostingsJob
أخرى في قائمة الانتظار في وقت واحد لسجل الحزمة الذي يشير إلى جهة الاتصال 123
، فسيتم السماح لواحدة منها فقط بالمتابعة. سيبقى الآخر محجوبًا حتى انتهاء الأول (أو مرور 15 دقيقة، أيًا كان ما يحدث أولاً).
لاحظ أن إعداد duration
يعتمد بشكل غير مباشر على قيمة concurrency_maintenance_interval
التي قمت بتعيينها للمرسل (المرسلين)، حيث سيكون هذا هو التكرار الذي يتم من خلاله فحص المهام المحظورة وإلغاء حظرها. بشكل عام، يجب عليك تعيين duration
بطريقة تجعل جميع مهامك تنتهي بشكل جيد خلال تلك المدة، والتفكير في مهمة صيانة التزامن باعتبارها عملية آمنة من الفشل في حالة حدوث خطأ ما.
يتم إلغاء حظر الوظائف حسب الأولوية ولكن لا يتم أخذ ترتيب قائمة الانتظار في الاعتبار عند إلغاء حظر الوظائف. وهذا يعني أنه إذا كان لديك مجموعة من الوظائف التي تشترك في مجموعة متزامنة ولكنها موجودة في قوائم انتظار مختلفة، أو وظائف من نفس الفئة التي قمت بإدراجها في قوائم انتظار مختلفة، فلن يتم أخذ ترتيب قائمة الانتظار الذي قمت بتعيينه لعامل في الاعتبار عند إلغاء الحظر المحظور تلك. والسبب هو أن الوظيفة التي يتم تشغيلها تقوم بإلغاء حظر المهمة التالية، والوظيفة نفسها لا تعرف شيئًا عن ترتيب قائمة الانتظار لعامل معين (يمكن أن يكون لديك عمال مختلفون بأوامر قائمة انتظار مختلفة)، يمكنها فقط معرفة الأولوية. بمجرد إلغاء حظر الوظائف المحظورة وإتاحتها للاستقصاء، سيتم التقاطها بواسطة عامل بعد ترتيب قائمة الانتظار الخاصة به.
أخيرًا، تعمل المهام الفاشلة التي تتم إعادة محاولتها تلقائيًا أو يدويًا بنفس الطريقة التي تعمل بها المهام الجديدة التي يتم وضعها في قائمة الانتظار: فهي تدخل في قائمة الانتظار للحصول على إشارة مفتوحة، وعندما تحصل عليها، سيتم تشغيلها. لا يهم إذا كانوا قد حصلوا بالفعل على إشارة مفتوحة في الماضي.
لا تتضمن قائمة الانتظار الصلبة أي آلية لإعادة المحاولة التلقائية، فهي تعتمد على الوظيفة النشطة لهذا الغرض. سيتم الاحتفاظ بالمهام التي تفشل في النظام، وسيتم إنشاء تنفيذ فاشل (سجل في جدول solid_queue_failed_executions
) لهذه المهام. ستبقى المهمة هناك حتى يتم التخلص منها يدويًا أو إعادة وضعها في قائمة الانتظار. يمكنك القيام بذلك في وحدة التحكم على النحو التالي:
failed_execution = SolidQueue :: FailedExecution . find ( ... ) # Find the failed execution related to your job
failed_execution . error # inspect the error
failed_execution . retry # This will re-enqueue the job as if it was enqueued for the first time
failed_execution . discard # This will delete the job from the system
ومع ذلك، نوصي بإلقاء نظرة على Mission_control-jobs، وهي لوحة معلومات يمكنك من خلالها، من بين أشياء أخرى، فحص المهام الفاشلة وإعادة المحاولة/تجاهلها.
ترتبط بعض خدمات تتبع الأخطاء التي تتكامل مع Rails، مثل Sentry أو Rollbar، بالوظيفة النشطة وتبلغ تلقائيًا عن الأخطاء التي لم تتم معالجتها والتي تحدث أثناء تنفيذ المهمة. ومع ذلك، إذا لم يكن نظام تتبع الأخطاء الخاص بك كذلك، أو إذا كنت بحاجة إلى بعض التقارير المخصصة، فيمكنك ربط Active Job بنفسك. الطريقة الممكنة للقيام بذلك ستكون:
# application_job.rb
class ApplicationJob < ActiveJob :: Base
rescue_from ( Exception ) do | exception |
Rails . error . report ( exception )
raise exception
end
end
لاحظ أنه سيتعين عليك تكرار المنطق أعلاه في ActionMailer::MailDeliveryJob
أيضًا. وذلك لأن ActionMailer
لا يرث من ApplicationJob
ولكنه يستخدم بدلاً من ذلك ActionMailer::MailDeliveryJob
الذي يرث من ActiveJob::Base
.
# application_mailer.rb
class ApplicationMailer < ActionMailer :: Base
ActionMailer :: MailDeliveryJob . rescue_from ( Exception ) do | exception |
Rails . error . report ( exception )
raise exception
end
end
نحن نوفر مكونًا إضافيًا لـ Puma إذا كنت تريد تشغيل مشرف Solid Queue مع Puma والحصول على مراقبة Puma وإدارته. تحتاج فقط إلى إضافة
plugin :solid_queue
إلى تكوين puma.rb
الخاص بك.
نظرًا لأن هذا قد يكون أمرًا صعبًا للغاية ولا ينبغي للعديد من الأشخاص القلق بشأنه، يتم تكوين Solid Queue افتراضيًا في قاعدة بيانات مختلفة كالتطبيق الرئيسي، ويتم تأجيل قائمة انتظار المهام حتى يتم الالتزام بأي معاملة جارية بفضل وظيفة Active Job المدمجة القدرة على القيام بذلك. وهذا يعني أنه حتى إذا قمت بتشغيل Solid Queue في نفس قاعدة البيانات مثل تطبيقك، فلن تستفيد من تكامل المعاملات هذا.
إذا كنت تفضل تغيير هذا، فيمكنك تعيين config.active_job.enqueue_after_transaction_commit
على never
. يمكنك أيضًا تعيين هذا على أساس كل وظيفة.
إذا قمت بتعيين ذلك على " never
" ولكنك لا تزال ترغب في التأكد من أنك لا تعمل عن غير قصد على تكامل المعاملات، فيمكنك التأكد مما يلي:
يتم دائمًا وضع وظائفك التي تعتمد على بيانات محددة في قائمة الانتظار في عمليات رد الاتصال after_commit
أو بخلاف ذلك من مكان تكون فيه متأكدًا من أن أي بيانات ستستخدمها الوظيفة قد تم الالتزام بها في قاعدة البيانات قبل وضع المهمة في قائمة الانتظار.
أو، يمكنك تكوين قاعدة بيانات مختلفة لـ Solid Queue، حتى لو كانت مماثلة لتطبيقك، مما يضمن استخدام اتصال مختلف في طلبات معالجة سلسلة الرسائل أو المهام الجارية لتطبيقك لإدراج المهام. على سبيل المثال:
class ApplicationRecord < ActiveRecord :: Base
self . abstract_class = true
connects_to database : { writing : :primary , reading : :replica }
config . solid_queue . connects_to = { database : { writing : :primary , reading : :replica } }
تدعم Solid Queue تحديد المهام المتكررة التي يتم تشغيلها في أوقات محددة في المستقبل، بشكل منتظم مثل وظائف cron. تتم إدارتها من خلال عملية الجدولة ويتم تعريفها في ملف التكوين الخاص بها. بشكل افتراضي، يوجد الملف في config/recurring.yml
، ولكن يمكنك تعيين مسار مختلف باستخدام متغير البيئة SOLID_QUEUE_RECURRING_SCHEDULE
أو باستخدام خيار --recurring_schedule_file
مع bin/jobs
، كما يلي:
bin/jobs --recurring_schedule_file=config/schedule.yml
يبدو التكوين نفسه كما يلي:
production :
a_periodic_job :
class : MyJob
args : [ 42, { status: "custom_status" } ]
schedule : every second
a_cleanup_task :
command : " DeletedStuff.clear_all "
schedule : every day at 9am
يتم تحديد المهام كتجزئة/قاموس، حيث سيكون المفتاح هو مفتاح المهمة داخليًا. يجب أن تحتوي كل مهمة إما على class
، والتي ستكون فئة الوظيفة التي سيتم إدراجها في قائمة الانتظار، أو command
، والذي سيتم تقييمه في سياق الوظيفة ( SolidQueue::RecurringJob
) التي سيتم وضعها في قائمة الانتظار وفقًا لجدولها الزمني، في قائمة الانتظار solid_queue_recurring
.
يجب أن يكون لكل مهمة أيضًا جدول زمني، والذي يتم تحليله باستخدام Fugit، بحيث يقبل أي شيء يقبله Fugit ككرون. يمكنك اختياريًا توفير ما يلي لكل مهمة:
args
: الوسائط التي سيتم تمريرها إلى الوظيفة، كوسيطة واحدة، أو تجزئة، أو مجموعة من الوسائط التي يمكن أن تتضمن أيضًا kwargs كعنصر أخير في المصفوفة.سيتم وضع الوظيفة في مثال التكوين أعلاه في قائمة الانتظار كل ثانية على النحو التالي:
MyJob . perform_later ( 42 , status : "custom_status" )
queue
: قائمة انتظار مختلفة لاستخدامها عند وضع المهمة في قائمة الانتظار. إذا لم يكن هناك شيء، فسيتم إعداد قائمة الانتظار لفئة الوظيفة.
priority
: قيمة أولوية رقمية يتم استخدامها عند وضع المهمة في قائمة الانتظار.
يتم ترتيب المهام في أوقاتها المقابلة بواسطة المجدول، وتقوم كل مهمة بجدولة المهمة التالية. وهذا مستوحى إلى حد كبير مما يفعله GoodJob.
من الممكن تشغيل العديد من برامج الجدولة بنفس تكوين recurring_tasks
، على سبيل المثال، إذا كان لديك عدة خوادم للتكرار، وقمت بتشغيل scheduler
في أكثر من واحد منهم. لتجنب وضع المهام المكررة في قائمة الانتظار في نفس الوقت، يتم إنشاء إدخال في جدول solid_queue_recurring_executions
الجديد في نفس المعاملة التي يتم فيها وضع المهمة في قائمة الانتظار. يحتوي هذا الجدول على فهرس فريد على task_key
و run_at
، مما يضمن إنشاء إدخال واحد فقط لكل مهمة في كل مرة. لا يعمل هذا إلا إذا قمت بتعيين preserve_finished_jobs
على true
(الافتراضي)، وينطبق الضمان طالما أنك تحتفظ بالوظائف.
ملاحظة : يتم دعم جدول زمني متكرر واحد، بحيث يمكن أن يكون لديك عدة جدولة باستخدام نفس الجدول، ولكن ليس عدة جدولة تستخدم تكوينات مختلفة.
أخيرًا، من الممكن تكوين المهام التي لا يتم التعامل معها بواسطة Solid Queue. أي أنه يمكنك الحصول على وظيفة مثل هذه في تطبيقك:
class MyResqueJob < ApplicationJob
self . queue_adapter = :resque
def perform ( arg )
# ..
end
end
لا يزال بإمكانك تكوين هذا في قائمة الانتظار الصلبة:
my_periodic_resque_job :
class : MyResqueJob
args : 22
schedule : " */5 * * * * "
وسيتم إدراج المهمة في قائمة الانتظار من خلال perform_later
بحيث يتم تشغيلها في Resque. ومع ذلك، في هذه الحالة، لن نقوم بتتبع أي سجل solid_queue_recurring_execution
لها ولن تكون هناك أي ضمانات بأن المهمة يتم إدراجها في قائمة الانتظار مرة واحدة فقط في كل مرة.
لقد تم استلهام Solid Queue من Resque وGoodJob. نوصي بالاطلاع على هذه المشاريع لأنها أمثلة رائعة تعلمنا منها الكثير.
الجوهرة متاحة كمصدر مفتوح بموجب شروط ترخيص MIT.