چرا به Command Schedule تو Laravel نمیشه اعتماد کرد؟

لاراول یکی از معروف ترین و محبوب ترین فریم ورک های PHP و PHP یکی از پر استفاده ترین زبان های برنامه نویسی تو ایران و خیلی از کشور هاست.

همونطور که میدونید هر فریم ورک، تسهیلاتی در اختیار توسعه دهندش قرار میده که سرعت کار رو بیشتر کنه و یکی از کاربردی ترینِ اونها تو لاراول، امکان Command Schedule یا زمان بندی دستوراته. خیلی وقت ها نیاز داریم تا در زمان های مشخصی، دستور یا کد خاصی اجرا بشه. قبل تر این کار با کمک crontab انجام می شد. اما در جهت شفاف سازی بیشتر و احتمالا به خاطر تبعیت از قاعده Everything as code (EaC)، این امکان تو چنین فریم ورک هایی به وجود اومد تا صرفن با اجرا کردن دستور ساده ای مثل php artisan schedule:run خیلی از کارهایی که نیاز به زمان بندی دارند رو بشه انجام داد.

اتفاقن این کار خیلی راحت تر هم قابل انجامه و نیاز به حفظ کردن فرمت crontab هم نیست و با متد های ساده ای مثل daily، everyMinute، hourly و ... میشه این زمان بندی رو انجام داد. هر چند که فرمت crontab رو هم ساپورت می کنه.

اما کجا ممکنه که مشکل به وجود بیاد؟

قبل از اینکه به مشکل احتمالی این فیچر بپردازیم، لازمه قبلش بدونیم این زمان بندی چطور کار می کنه. شاید تو ذهن بعضی ها اینطور جا افتاده باشه که وقتی ما دستوری رو everyThirtyMinutes قراره اجرا بکنیم، احتمالا یک زمانی رو خود سیستم به عنوان مبدا در نظر می گیره و جایی ذخیرش می کنه و شمارش های بعدی رو نسبت به اون میسنجه. خب این دیدگاه غلطه. چون هم پیچیدگی رو زیاد تر می کنه و هم قابل track کردن نیست و در عمل هم اینطور ساخته نشده.

زمان بندی command scheduler لاراول دقیقن مثل crontab عمل می کنه. یعنی تمام متد ها در نهایت تفسیر میشن به یه عبارت با فرمت crontab. برای مثل everyThirtyMinutes تفسیر میشه به 0,30 * * * * و این اتفاق برای تمام متدهاش میفته. خود دستور php artisan schedule:run هم باید به کمک ابزاری مثل supervisor یا crontab هر دقیقه یک بار اجرا بشه و با اجرای هر بار این دستور، دقیقه ای که سیستم توش قرار داره مقایسه میشه با cron expression و در صورت تطبیق، دستور مورد نظر اجرا میشه.

نکته ای که ممکنه از ذهن ها پنهان بمونه و سرمنشأ مشکل بشه، synchronous بودن کل فرایند schedule:run هست و این ابزار به خودی خود امکانی برای اجرای multi process یا multi thread فراهم نمی کنه. فرض کنید چنین عبارتی رو تو فایل Console/Kernel.php دارید:

$schedule->command('job1')->everyFiveMinutes();
$schedule->command('job2')->everyTenMinutes();

زمانی که هر کدوم از این جاب ها زمان کوتاهی برای اجرا نیاز داشته باشن، همه چیز به خوبی کار می کنه و مشکلی به وجود نمیاد. اما وقتی که از حد معمول بیشتر طول بکشن چه اتفاقی میفته؟

فرض کنید الان ساعت ۱۰:۰۵ هست و schedule:run اجرا میشه. طبق کد job1 که قراره هر ۵ دقیقه یک بار اجرا بشه، شروع می کنه به اجرا شدن و مثلا تو ۴ دقیقه (یعنی ۱۰:۰۹ دقیقه) تموم میشه. در این اجرا شرط زمانی job2 صدق نمی کنه و اجرا نمیشه. ساعت ۱۰:۰۶ تا ۱۰:۰۹ هم با اجرا شدن دستور schedule:run اتفاقی نمیفته چون شرط زمانی هیچ کدوم از دو تا جاب meet نمیشه. ولی ساعت ۱۰:۱۰ هر دو جاب باید اجرا بشن. اما اگه job1 بیشتر از ۱ دقیقه طول بکشه، زمانی که interpreter پی اچ پی به خط بعد می رسه، ساعت ۱۰:۱۱ شده و دیگه شرط everyTenMinutes براش صدق نمی کنه (چون با زمان حال مقایسه میشه) و اون دور اجرای این دستور کنسل میشه و اگه جاب مهمی باشه، احتمالا ساید افکت های اجرا نشدنش توی پروژه خودش رو نشون میده. اینجا دقیقن جاییه که درک سطحی از این ابزار ممکنه کار دست دولوپر بده و اون رو وارد یه پروسه debugging طولانی و گاهن بی نتیجه بکنه.

چیکار میشه کرد که این مشکل به وجود نیاد؟

اولین و احتمالن ساده ترین راه حل که الزامن راه درستی هم نیست، منتقل کردن این جاب ها به crontab واقعی سیستم هست که خب با هدف ساختن این فیچر و فلسفه Everything as code (EaC) در تناقضه و Debugging و Maintenance رو هم میتونه سخت تر کنه. ولی به خاطر ماهیت Multi Process بودنش دیگه این دغدغه ها رو به همراه نخواهد داشت.

دومین و احتمالا راه حل سخت تر (و پیشنهاد شده) برای حل این مسئله، تبدیل این کامند ها به Job های async هست که میتونه با کمک Queue و یا روش های دیگه انجام بشه که روند اجرای این دستورات رو از حالت خطی و synchronous در میاره و میتونه به ازای هر دستور یک process یا thread جداگونه ای داشته باشه و به صورت parallel تمامی این دستورات رو سر وقت انجام بده.

سومین راه و راه حل خیلی خیلی سخت تر برای حل این مشکل، احتمالا modify کردن سورس کد لاراول و اضافه کردن sub process به scheduler و یا اصلاح زمان مورد مقایسه برای اجرای هر کامند به زمان آغاز schedule:run به جای زمان حاله که خب کسی حالشو نداره! ولی اگه کسی انجامش داد لطفن pull request بزنه به لاراول بلکه تایید شه و به درد بقیه هم بخوره.

توصیه من به کسانی که از این قابلیت استفاده می کنند اینه که مشکلات مربوط به همزمانی و یا miss شدن جاب ها رو همیشه مدنظر داشته باشن و گول راحتی استفاده از این ابزار رو نخورن. با توجه به اینکه امکان وارد کردن لاجیک به این scheduler وجود داره، میتونه ابزار خیلی قدرتمند تری از crontab سیستم عامل باشه ولی به همون میزان به دلیل عدم پشتیبانی از multi threading میتونه خطرناک باشه.

اگه شما هم راه حل دیگه ای برای برنخوردن به این مشکل دارید، حتما با ما به اشتراک بذارید.