أخبار التقنية التقنية بلغة عربية واضحة
تحديث تقني Laravel وPHP

Laravel 13.35 يضيف تشغيل المهام المجدولة على خادم واحد وإيقاف المهام الطويلة أثناء النشر

يقدّم Laravel 13.35 خيار Schedule::alwaysOnOneServer() لتشغيل كل مهمة مجدولة مرة واحدة فقط عند استخدام عدة خوادم، إلى جانب Schedule::hasBeenInterruptedSince() كي تتمكن الأوامر الطويلة من التوقف بأمان عند بدء عملية نشر جديدة.

المصدر: Laravel News
Laravel 13.35 يضيف تشغيل المهام المجدولة على خادم واحد وإيقاف المهام الطويلة أثناء النشر
Laravel

أبرز النقاط

  • تضيف Laravel 13.35 الدالة Schedule::alwaysOnOneServer() لتطبيق onOneServer() على جميع المهام المجدولة دفعة واحدة.
  • تحتاج هذه الآلية إلى مخزن تخزين مؤقت مشترك بين الخوادم؛ Redis أو قاعدة البيانات مثالان مناسبان، بينما file وarray لا يحققان الغرض.
  • مهام Closure غير المسمّاة لا يشملها alwaysOnOneServer() وستعمل على كل خادم، لذا تحتاج إلى name() إذا كان المطلوب تنفيذها مرة واحدة.
  • تتيح Schedule::hasBeenInterruptedSince() للمهام الطويلة اكتشاف بدء نشر جديد والتوقف بين دفعات العمل بصورة آمنة.
  • إيقاف المهمة يعتمد على تصميم الأمر نفسه: عليه فحص المقاطعة، ويجب أن يستطيع التشغيل اللاحق متابعة العمل أو إعادة تنفيذه من دون خسارة.

عندما يعمل تطبيق Laravel على أكثر من خادم ويُشغّل الأمر schedule:run على كل واحد منها، فإن المهمة المجدولة تُنفّذ عادةً على كل خادم. قد يكون ذلك مناسباً لمهام محلية، مثل تنظيف ملفات مؤقتة على قرص الخادم نفسه، لكنه يسبب مشكلة لمهام مركزية مثل إرسال تقرير يومي عبر البريد: قد يصل التقرير مكرراً بعدد الخوادم.

في Laravel 13.35، أضيفت أداتان لمعالجة هذا النوع من السيناريوهات: تشغيل المهام المجدولة على خادم واحد افتراضياً، وإتاحة إيقاف الأوامر الطويلة بصورة منظمة عند تنفيذ نشر جديد للتطبيق.

تشغيل الجدولة كلها على خادم واحد

كان Laravel يوفر سابقاً onOneServer()، وتُضاف إلى كل مهمة يجب ألا تعمل إلا مرة واحدة:

Schedule::command('reports:send')->daily()->onOneServer();

تعتمد الآلية على قفل يُحفظ في التخزين المؤقت؛ الخادم الذي يحصل على القفل أولاً يشغّل المهمة، بينما تتجاوزها الخوادم الأخرى في تلك الدورة.

لكن تطبيق ذلك على كل مهمة يترك مجالاً لنسيانها عند إضافة مهمة جديدة. لذلك يضيف Laravel 13.35 الدالة Schedule::alwaysOnOneServer()، التي تطبّق السلوك على الجدولة كاملة من داخل AppServiceProvider:

use Illuminate\Support\Facades\Schedule;

public function boot(): void
{
    Schedule::alwaysOnOneServer();
}

تُطبّق هذه الإعدادات عندما يقرأ Laravel أحداث الجدولة، لذا تشمل المهام المعرّفة لاحقاً، مثل المهام الموجودة في routes/console.php. والخيار معطّل افتراضياً، ويمكن تعطيله صراحة عبر Schedule::alwaysOnOneServer(false).

لا يوجد استثناء لكل مهمة عند تفعيل هذا الخيار. فإذا كانت بعض المهام يجب أن تعمل على كل خادم، مثل مهمة تنظف ملفات محلية على كل قرص، فمن الأنسب إبقاء الخيار العام معطلاً واستخدام onOneServer() للمهام المركزية فقط.

مهام Closure تحتاج إلى اسم

ينشئ Laravel مفتاح القفل من اسم المهمة. الأوامر لديها اسم واضح مشتق من سطر الأمر، أما المهمة المكتوبة كـ Closure فتحتاج إلى تحديد اسمها باستخدام name().

عند استخدام alwaysOnOneServer()، تتجاوز Laravel مهام Closure غير المسمّاة؛ أي إنها ستستمر في العمل على كل خادم من دون تحذير. لتشغيلها على خادم واحد، ينبغي تسميتها:

Schedule::call(fn () => Report::sendDaily())
    ->name('send-daily-report')
    ->daily();

لذلك، عند تفعيل الإعداد الجديد، من المهم مراجعة مهام Closure والتأكد من إضافة name() للمهام التي لا ينبغي تكرارها.

التخزين المؤقت المشترك شرط أساسي

توجد الأقفال التي تمنع التكرار داخل مخزن التخزين المؤقت. لهذا يجب أن تستخدم جميع الخوادم مخزناً مشتركاً، مثل Redis أو Memcached أو DynamoDB أو قاعدة البيانات.

أما استخدام مخزن file أو array فيجعل كل خادم يرى قفله الخاص، وبالتالي ستعمل المهمة على جميع الخوادم رغم تفعيل التشغيل على خادم واحد.

إذا كان مخزن التطبيق الافتراضي محلياً، يمكن تحديد مخزن مشترك لأقفال المجدول فقط:

public function boot(): void
{
    Schedule::alwaysOnOneServer();
    Schedule::useCache('redis');
}

لا يغيّر useCache() مخزن التخزين المؤقت لبقية التطبيق؛ بل يخص أقفال المجدول، بما فيها onOneServer() وwithoutOverlapping().

إيقاف الأوامر الطويلة عند النشر

يتعامل التحديث الثاني مع المهام الطويلة التي تعمل في الخلفية عبر runInBackground(). في السابق، كان php artisan schedule:interrupt يوقف حلقة المجدول للمهام المتكررة خلال الدقيقة، لكنه لا يوقف الأمر الذي بدأ بالفعل في عملية مستقلة. وقد يعني ذلك استمرار مهمة طويلة على نسخة التطبيق القديمة أثناء النشر، أو إيقافها قسراً لاحقاً.

الآن يسجل schedule:interrupt وقت المقاطعة، ويمكن للأمر الطويل مقارنة هذا الوقت بوقت بدايته عبر Schedule::hasBeenInterruptedSince()، ثم التوقف بين دفعات العمل:

public function handle(Schedule $schedule): int
{
    $startedAt = now();

    foreach (Product::lazyById(500) as $product) {
        if ($schedule->hasBeenInterruptedSince($startedAt)) {
            $this->info('Interrupted by a deploy, stopping.');

            return self::SUCCESS;
        }

        $this->syncProduct($product);
    }

    return self::SUCCESS;
}

هذا لا يوقف المهمة تلقائياً في أي نقطة؛ على الأمر نفسه أن يفحص حالة المقاطعة بين دفعات يمكن التوقف عندها بأمان. كما يجب أن تكون المهمة قابلة للاستئناف في التشغيل التالي من دون فقدان العمل المتبقي.

يعيد hasBeenInterruptedSince() القيمة true فقط إذا حدثت المقاطعة عند وقت بداية المهمة أو بعده، لذلك لا تؤثر إشارة نشر قديمة في مهمة بدأت لاحقاً. ويجب الانتباه أيضاً إلى أن وقت المقاطعة يُخزن في مخزن التخزين المؤقت الافتراضي، وليس المخزن المحدد عبر useCache()؛ لذا يحتاج هذا المخزن الافتراضي إلى أن يكون مشتركاً بين الخوادم في بيئات التشغيل المتعدد.

في مسار نشر يستخدم المجدول وعمّال الطوابير، يمكن إرسال الإشارتين التاليتين قبل الانتقال إلى الإصدار الجديد:

php artisan schedule:interrupt
php artisan queue:restart

كما أن تعطيل استطلاع المقاطعة عبر Schedule::withoutInterruptionPolling() يجعل hasBeenInterruptedSince() يعيد false دائماً.

لماذا يهم؟

تقلل الإضافة الجديدة احتمال تكرار رسائل البريد والتقارير والعمليات المركزية عند توسعة تطبيق Laravel إلى عدة خوادم، وتمنح المهام الطويلة طريقة أوضح للخروج أثناء النشر بدلاً من الاستمرار على كود قديم. لكن نجاح الطريقتين يعتمد على إعداد cache مشترك وتصميم المهام بحيث يمكن إيقافها واستئنافها بأمان.

#Laravel#PHP#جدولة_المهام#أدوات_المطورين#Redis