چرا زمان‌بندی کارها در لاراول کار نمی‌کند؟!

بعضی کارها باید به صورت خودکار سر زمان‌های مشخص اجرا بشن. ابزار cron توی لینوکس این امکان رو به ما میده که بگیم یه کاری در زمان‌های از پیش تعریف شده انجام بشه. امّا مسئله اینجاست که هر بار کاری از این جنس داشته باشیم،‌ باید یه مورد به لیست وظایف cron توی سیستم‌عامل اضافه کنیم؛ کاری که به زودی deployمون رو سخت می‌کنه. به همین خاطر Laravel به کمک اومده و «کاری برای تمام کارها» در اختیارمون گذاشته؛‌ چیزی به نام Task Scheduler که فقط کافیه یه cron job برای اجرای اون در سیستم‌عامل اجرا بشه و ما در کد برای اون تعریف کنیم که چه کارهایی داریم. زیباست نه؟ «معلومه که نه!». چرا نه؟ چون جزئیاتی مخفی شده که باعث میشه به جن معتقد بشیم!چطور در لاراول کارها رو زمان‌بندی کنیم؟قبل از اینکه به مشکل بپردازیم، اول خیلی سریع ببینیم چطور باید کارها رو در Laravel زمان‌بندی کرد.اول باید یک عملیات cron تعریف بشه که هر دقیقه لاراول رو صدا بزنه که کارهای زمان‌بندی‌شده رو -اگر وقت اجراشون فرا رسیده- اجرا کنه:* * *‌ * * php /path/to/project/artisan schedule:runتعریف زمان‌بندی کارها در تابع schedule از App/Console/Kernel انجام میشه. در اونجا، می‌تونیم بگیم کارهای مختلفی رو (اجرای یه دستور، فراخوانی یه تابع، قرار دادن کاری در صف، و....)‌ در زمان مورد نظر اجرا کنه.$schedule->call(new DeleteRecentUsers)->hourly(); $schedule->command(EmailsCommand::class, ['Taylor', '--force'])->everyMinute(); $schedule->job(new Heartbeat)->everyFiveMinutes(); $schedule->exec('node /home/forge/script.js')->daily();در ظاهر همه چیز خوبه: یه کاری رو می‌گم هر فلان‌قدر یک بار اجرا بشه، cron لینوکس هر دقیقه به لاراول خبره می‌ده،‌ و لابد سروقتش (یا اگر سرش شلوغ باشه یکم دیرتر) دستورات مورد نظر اجرا می‌شه.کار نمی‌کنه!ما کارهایی رو زمان‌بندی کرده بودیم که هر ۵ یا ۱۰ دقیقه یک بار اجرا بشن. امّا چیزی که تجربه کردیم توضیحش ساده نبود و به نظر میومد زمان‌بندی لاراول جن‌زده شده باشه! مشاهده ما این بود: کارها یک یا چند نوبت در میون اجرا میشه یا برای مدتی طولانی اصلاً اجرا نمیشه!بعضی وقت‌ها با ریست کردن container مشکل حل میشد و اجرا دوباره به جریان میفتاد (که بدیهیه راه‌حل مناسبی نیست!).مشاهده دیگه‌مون این بود که اگر اجرا رو برای «هر دقیقه» زمان‌بندی می‌کردیم، خیلی کم‌تر دچار این مسئله میشدیم.پس بسم‌الله گفتیم و شروع کردیم به بررسی دقیق‌تر نحوه‌ی اجرای کارهای زمان‌بندی شده در لاراول.شرّ در جزئیات نهفته استزمان‌بندی کارهای در لاراول چطور کار می‌کند؟برای شروع چند تا سؤال داشتیم:«هر ده دقیقه یک بار» یعنی چی؟ مثلاً اگر اولین بار ۱۶:۰۲ این دستور اجرا بشه، اجرای بعدی‌ش ۱۶:۱۲ است؟اگر به هر دلیلی نرسید در زمان از پیش تعیین شده کاری رو اجرا کنه، چه اتفاقی میفته؟ بی‌خیال اجرای اون نوبت می‌شه یا با تأخیر انجامش می‌ده؟اصلاً یادش می‌مونه چه کارهایی رو و در چه زمانی انجام داده؟جواب کوتاه به این سؤالات اینه:یعنی دقایقی از ساعت که به ۱۰ بخش‌پذیر هستن. پس اصلا در ۱۶:۰۲ اجرا نمیشه. یا ۱۶:۰۰ یا ۱۶:۱۰بعضی مواقع بی‌خیالش میشه. (در ادامه می‌فهمیم چرا)نه. کاملاً بی‌حافظه عمل می‌کنه.وقتی از توابع زمان‌بندی (everyMinute, everyTenMinutes, daily و...) استفاده می‌کنید، در پشت صحنه این تبدیل به یک عبارت cron می‌شن. پس در قدم اول «هر ده دقیقه» تبدیل به 10/* میشه که یعنی «دقایقی از ساعت که به ۱۰ بخش‌پذیرند». با هر بار فراخوانی دستور schedule:run،‌ طی فرآیند bootstrap محتویات Console Kernel نیز تفسیر شده و تمامی تعاریف کارها (که در تابع schedule همین کلاس انجام شده) در حافظه قرار می‌گیرن. در ادامه، از تمام اعضای لیست کارها پرسیده می‌شه:‌ «با توجه به زمانی که درش هستیم، آیا وقت اجرای شما فرارسیده؟». در انتها تمام کارهایی که جواب مثبت به این سؤال داده باشن، در لیستی قرار می‌گیرن که به ترتیب اجرا میشه.برای نمونه، اگر کاری برای هر ۱۰ دقیقه زمان‌بندی شده باشه و ساعت ۱۶:۰۲ باشه، جوابش منفیه و باید تا ۱۶:۱۰ صبر کنه. امّا اگر سر ساعت ۱۶:۱۰ (بدون در نظر گرفتن ثانیه‌ها) این سؤال ازش پرسیده نشه چی؟! هیچی! نه کار انجام نمیشه، و نه برای اجرای با تأخیر جایی ثبت میشه. پس کافی بود به دلیلی (مثل سر زمان اجرا نشدن دستور cron، کند بودن اجرا و...) دیر به شرط بررسی زمان اجرا برسیم؛ کار انجام نمی‌شد.پند امروز؟!چرا به اشتباه افتاده بودیم؟‌ چون انتظار داشتیم «هر ده دقیقه» به معنای «دست کم در هر ده دقیقه یک بار» باشه؛ یا مثل setTimeout در جاوااسکریپت، «حداقل زمان انتظار ده دقیقه» باشه. امّا شرّ در جزئیات نهفته بود. جزئیاتی که ازش غافل بودیم.پانوشت: متن مزین به ارجاعاتی به متن کد یا توضیحات بیشتر است. چشم خود را از زینت‌ها محروم نکنید D:پانوشت دوم: یکی از دوستان به مشکلی که به لاراول گزارش شده بود اشاره کرد. مشکل طرح شده در اونجا، هم‌زمانی دو کار بوده که به دلیل زمان‌بر بودن اولی، دومی در زمانی متفاوت از زمان مورد انتظار اجرا می‌شده. راه حل؟ اجرای کار اول در پس‌زمینه (()runInBackground<-)