مقاله

Scheduled Tasks در Laravel — Task Scheduling و Cron

Scheduled Tasks در Laravel — Task Scheduling و Cron

تعریف schedule در routes/console.php، cron سرور، withoutOverlapping و taskهای روزانه production.

Cron سرور هر شب backup می‌گیرد — اما پنج اسکریپت پراکنده با مسیر اشتباه. Laravel Scheduler یک entry cron دارد: schedule:run هر دقیقه — بقیه در کد PHP در routes/console.php تعریف می‌شود. تمیز، version control و testable.

خلاصه: دو چیز جدا با هم کار می‌کنند. cron سیستم‌عامل فقط یک کار دارد: هر دقیقه دستور php artisan schedule:run را اجرا کند. Scheduler لاراول در هر بار اجرا، فهرست taskهای تعریف‌شده را می‌خواند، بررسی می‌کند کدام‌ها در همین دقیقه موعدشان رسیده (due هستند) و فقط همان‌ها را اجرا می‌کند. اگر cron اجرا نشود، هیچ task‌ای اجرا نمی‌شود — حتی اگر کد schedule کاملاً درست باشد. روی سیستم توسعه به‌جای cron می‌توانید php artisan schedule:work را در یک ترمینال باز نگه دارید.

تعریف schedule

use Illuminate\Support\Facades\Schedule;

Schedule::command('inspire')->hourly();

Schedule::call(function () {
    DB::table('sessions')->where('last_activity', '<', now()->subDays(7)->timestamp)->delete();
})->daily();

Schedule::job(new PruneOldExports)->weekly();

از Laravel 11 به بعد (از جمله Laravel 12) پروژهٔ تازه فایل app/Console/Kernel.php ندارد و محل پیش‌فرض تعریف taskها routes/console.php است. راه دیگر، متد withSchedule() در bootstrap/app.php است. اگر پروژه را از نسخهٔ قدیمی‌تر ارتقا داده‌اید و هنوز Kernel قدیمی دارید، taskها را فقط در یک جا نگه دارید تا معلوم باشد کدام فهرست اجرا می‌شود.

یک نمونهٔ واقعی از همین سایت: feedها هر روز ساعت ۳ صبح بازسازی می‌شوند و هر task با یک کلید config قابل خاموش کردن است:

Schedule::job(new RegeneratePublicFeedsJob)
    ->dailyAt('03:00')
    ->timezone(config('app.timezone', 'Asia/Tehran'))
    ->when(fn (): bool => (bool) config('portfolio.deployment.schedule_daily_feeds', true));

Schedule::command('queue:prune-failed', ['--hours' => 168])
    ->weekly();

فرکانس‌های رایج

everyMinute، hourly، dailyAt('02:00')، weeklyOn(1, '8:00')، monthly، cron('0 */6 * * *').

جلوگیری از هم‌پوشانی

Schedule::command('reports:generate')
    ->dailyAt('01:00')
    ->withoutOverlapping(60);

اگر اجرای قبلی هنوز تمام نشده، اجرای جدید skip می‌شود — برای task طولانی حیاتی. عدد داخل پرانتز مدت انقضای lock به دقیقه است (پیش‌فرض ۱۴۴۰ دقیقه، یعنی ۲۴ ساعت). lock در cache نگه داشته می‌شود؛ اگر process وسط کار kill شود، lock تا زمان انقضا باقی می‌ماند و task در این مدت اجرا نمی‌شود. برای چنین حالتی دستور php artisan schedule:clear-cache وجود دارد. پس مدت انقضا را کمی بیشتر از طولانی‌ترین زمان اجرای واقعی task بگذارید، نه خیلی بیشتر.

یک سرور در cluster

Schedule::command('backup:run')->daily()->onOneServer();

این فقط وقتی معنا دارد که چند سرور همان cron را اجرا کنند. پیش‌نیازش این است که cache پیش‌فرض برنامه روی یک store مشترک بین همهٔ سرورها باشد — طبق مستندات Laravel یکی از driverهای database، memcached، dynamodb یا redis — تا lock بین سرورها دیده شود. cache از نوع file یا array روی هر سرور جداست و این تضمین را نمی‌دهد. Redis یک گزینه است، نه شرط؛ مدیریت Redis و Horizon را در راهنمای Redis Queue و Horizon ببینید. روی یک سرور تنها، onOneServer() لازم نیست.

Cron روی سرور

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

فقط این یک خط — Laravel بقیه را مدیریت می‌کند. چند نکتهٔ عملی:

  • cron را برای همان کاربری تعریف کنید که فایل‌های پروژه و storage متعلق به اوست (مثلاً sudo crontab -u www-data -e)؛ اگر با root اجرا شود، فایل‌های cache و log ممکن است با مالک اشتباه ساخته شوند.
  • محیط cron از shell شما ساده‌تر است و ممکن است php را در PATH پیدا نکند یا نسخهٔ دیگری را اجرا کند. با which php مسیر کامل را پیدا کنید و در cron بنویسید، مثلاً /usr/bin/php. در هاست اشتراکی هم مسیر PHP مخصوص همان نسخه را از پنل بگیرید.
  • >> /dev/null 2>&1 خروجی را دور می‌ریزد. هنگام راه‌اندازی یا عیب‌یابی، موقتاً خروجی را به یک فایل بفرستید (مثلاً >> storage/logs/schedule-cron.log 2>&1) تا خطای اجرای خود cron دیده شود.

local در برابر سرور production

محیطروش اجرا
سیستم توسعهphp artisan schedule:work در ترمینال باز؛ هر دقیقه schedule:run را صدا می‌زند و با بستن ترمینال متوقف می‌شود
سرور production یا stagingیک entry در crontab که هر دقیقه schedule:run را اجرا کند
هاست اشتراکیهمان entry از بخش Cron Jobs پنل هاست، با مسیر کامل PHP

schedule:work ابزار توسعه است؛ آن را به‌جای cron روی سرور production رها نکنید.

لیست و تست

php artisan schedule:list
php artisan schedule:test
php artisan schedule:work  # local — هر دقیقه اجرا

schedule:list همهٔ taskها، عبارت cron و زمان اجرای بعدی را نشان می‌دهد — اولین جایی که باید ببینید task اصلاً ثبت شده و زمان بعدی‌اش با انتظار شما (و timezone درست) می‌خواند. schedule:test یک task را همین حالا اجرا می‌کند بدون اینکه منتظر موعدش بمانید. schedule:run فقط taskهایی را اجرا می‌کند که در همان دقیقه due باشند؛ اگر cron در دقیقه‌ای اجرا نشود، taskهای آن دقیقه بعداً جبران نمی‌شوند.

لاگ و خروجی taskها

Schedule::command('reports:generate')
    ->dailyAt('01:00')
    ->appendOutputTo(storage_path('logs/reports.log'))
    ->onFailure(function () {
        Log::error('reports:generate failed');
    });

sendOutputTo() فایل را بازنویسی می‌کند و appendOutputTo() به آن اضافه می‌کند. emailOutputOnFailure() هم هست، به شرطی که ارسال ایمیل در برنامه درست تنظیم شده باشد. این‌ها خروجی taskهای command را ثبت می‌کنند؛ برای jobهایی که با Schedule::job() به صف می‌روند، خطا در worker و جدول failed jobs دیده می‌شود.

مثال‌های production

  • پاکسازی failed_jobs — failed jobs
  • sitemap generate — RSS این پورتفولیو
  • backup database
  • expire subscription
  • queue:prune-batches

تفاوت Schedule و Queue

Schedule زمان ثابت؛ Queue event-driven. گاهی schedule یک job dispatch می‌کند — ترکیب درست.

تفاوت عملی مهم: Scheduler تصمیم می‌گیرد کی کاری شروع شود؛ queue worker (queue:work یا Horizon) jobها را اجرا می‌کند. Schedule::job() و Schedule::command() که داخلش job dispatch می‌شود، فقط job را در صف می‌گذارند؛ اگر worker روشن نباشد (یا QUEUE_CONNECTION روی sync نباشد)، job در صف می‌ماند و از دید شما «task اجرا نشده». cron جای worker را نمی‌گیرد و worker هم جای cron را. برای نحوهٔ dispatch، راهنمای dispatch در Laravel و برای jobهای شکست‌خورده، failed jobs را ببینید.

استثنای رایج هاست اشتراکی: وقتی Supervisor در دسترس نیست، بعضی پروژه‌ها (از جمله همین سایت، با یک کلید config) هر دقیقه از طریق scheduler یک worker کوتاه‌عمر اجرا می‌کنند: queue:work --stop-when-empty --max-time=50 همراه با withoutOverlapping(). این راه‌حل برای حجم کم است، نه جایگزین worker دائمی روی سرور.

timezone

Scheduler در Laravel 12 اول مقدار app.schedule_timezone را نگاه می‌کند و اگر تعریف نشده باشد از app.timezone استفاده می‌کند؛ با ->timezone('Asia/Tehran') هم می‌شود برای یک task جدا تعیین کرد. ساعت سیستم‌عامل سرور در این تصمیم نقشی ندارد، ولی schedule:list را بعد از هر تغییر timezone نگاه کنید. اگر برنامه در منطقه‌ای با ساعت تابستانی اجرا می‌شود، taskهایی که در ساعت جابه‌جایی تنظیم شده‌اند ممکن است دو بار اجرا شوند یا اصلاً اجرا نشوند؛ مستندات Laravel توصیه می‌کند در چنین حالتی تا جای ممکن از timezone-specific scheduling پرهیز کنید.

وقتی task اجرا نمی‌شود — کجا را بررسی کنیم؟

  1. cron اجرا می‌شود؟ crontab -l را برای کاربر درست بگیرید و لاگ cron سیستم را نگاه کنید. خروجی cron را موقتاً به فایل بفرستید.
  2. task ثبت شده؟ php artisan schedule:list — اگر در فهرست نیست، فایل تعریف یا شرط when()/environments() را بررسی کنید.
  3. موعدش رسیده؟ زمان «Next Due» در schedule:list را با timezone مورد انتظار مقایسه کنید.
  4. دستی اجرا می‌شود؟ php artisan schedule:test — اگر اینجا هم خطا می‌دهد، مشکل در خود command یا job است، نه scheduler.
  5. lock گیر کرده؟ اگر withoutOverlapping() دارد و قبلاً process قطع شده، php artisan schedule:clear-cache.
  6. به صف رفته ولی اجرا نشده؟ worker روشن است؟ jobهای شکست‌خورده را با php artisan queue:failed ببینید.
  7. maintenance mode؟ taskها در حالت maintenance اجرا نمی‌شوند مگر با evenInMaintenanceMode().
  8. config کش شده؟ بعد از تغییر .env، php artisan config:cache را دوباره اجرا کنید؛ وگرنه scheduler با مقدار قبلی کار می‌کند.

چک‌لیست deploy و بررسی

  • entry cron یک بار و فقط یک بار برای هر سرور تعریف شده (entry تکراری یعنی اجرای تکراری)
  • مسیر پروژه در cron به release فعلی یا symlink current اشاره می‌کند، نه به پوشهٔ release قدیمی
  • بعد از deploy، php artisan schedule:list روی سرور همان taskهای مورد انتظار را نشان می‌دهد
  • اگر taskها job dispatch می‌کنند، worker بعد از deploy ری‌استارت شده (queue:restart یا horizon:terminate) تا کد جدید را بخواند
  • برای taskهای طولانی withoutOverlapping() با زمان انقضای منطقی، و در چندسروری onOneServer() با cache مشترک
  • بعد از اولین موعد، لاگ یا نتیجهٔ task (فایل ساخته‌شده، رکورد پاک‌شده) را واقعاً بررسی کنید
  • در deploy با رویکرد بدون قطعی، قبل از جابه‌جایی release می‌توانید php artisan schedule:interrupt را اجرا کنید تا اجرای جاری scheduler (مثلاً taskهای زیر یک دقیقه) متوقف شود

جمع‌بندی

یک cron + routes/console.php = مرکز زمان‌بندی. cron فقط ماشه است؛ تصمیم «کدام task و کی» با Scheduler است، و اجرای jobهای صف با worker. Queue: مقدمه. برای راه‌اندازی کامل سرور، deploy روی Ubuntu و Nginx را ببینید.

اتوماسیون سرور

اشتراک‌گذاری