404 Not Found

404 Not Found


nginx

طوابير Laravel والمهام غير المتزامنة

الطوابير هي "فريق العمل الخلفي" في Laravel — يتم تفويض المهام المستغرقة للوقت إلى الطابور للمعالجة غير المتزامنة، بحيث لا يضطر المستخدم للانتظار وتعود الاستجابة فورًا.

1. ما ستتعلمه


2. قصة مستخدم

(1) المشكلة: تصدير التقارير يتسبب في تجميد النظام بالكامل

نقر Bob على "تصدير التقرير الشهري" في لوحة إدارة ShopMetrics — استغرق إنشاء ملف CSV يحتوي على 100,000 سجل طلب 30 ثانية، وخلال ذلك الوقت استمرت الصفحة في الدوران، كما عانت Alice أيضًا من بطء عند الوصول إلى لوحة المعلومات في نفس الوقت. لزيادة الطين بلة، أطلق Bob خمسة تصديرات تقارير في وقت واحد، مما استنزف تجمع عمليات PHP وتسبب في إرجاع الموقع بالكامل لخطأ 502.

(2) الحل باستخدام الطوابير

يرسل الطابور المهام المستهلكة للوقت إلى الخلفية — عندما ينقر المستخدم على "تصدير"، يرى فورًا رسالة "جاري إنشاء التقرير"، بينما تعمل المهمة ببطء في الخلفية؛ وبمجرد الانتهاء، يتم إرسال بريد إلكتروني يحتوي على رابط التحميل.

PHP
// متزامن — يحجب لمدة 30 ثانية
$csv = ReportService::generateMonthlyReport($tenant);

// غير متزامن — يعود فورًا، تعمل المهمة في الخلفية
GenerateReportJob::dispatch($tenant, 'monthly');
// يرى المستخدم: "جاري إنشاء التقرير. سنرسل لك بريدًا إلكترونيًا عندما يكون جاهزًا."

(3) النتيجة

بعد أن نفّذ Bob الطابور، أصبحت طلبات تصدير التقارير تعود خلال 100 مللي ثانية، وتعمل المهام في الخلفية بسلاسة، ولوحة معلومات Alice لم تعد تتجمد.


3. تهيئة مشغلات الطوابير

(1) مقارنة المشغلات

المشغل الاستمرارية الأداء الملاءمة التكلفة
sync ❌ تنفيذ فوري الأسرع التطوير/الاختبار مجاني
database ✅ جدول قاعدة بيانات بطيء المشاريع الصغيرة مجاني
redis ✅ الذاكرة سريع بيئة الإنتاج Redis
sqs ✅ AWS عالي المشاريع الكبيرة الدفع حسب الاستخدام
beanstalkd ✅ مخصص سريع المشاريع المتوسطة مجاني

(2) التهيئة

BASH
# .env
QUEUE_CONNECTION=redis

# اتصال Redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PHP
// config/queue.php
'connections' => [
    'database' => [
        'driver' => 'database',
        'table' => 'jobs',
        'queue' => 'default',
        'retry_after' => 90,
    ],
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default',
        'queue' => '{default}',
        'retry_after' => 90,
        'block_for' => null,
    ],
],

(3) إنشاء جداول الطوابير

BASH
# لمشغل قاعدة البيانات
php artisan queue:table
php artisan queue:failed-table
php artisan migrate

# ينشئ: جدول jobs + جدول failed_jobs

(1) ▶ مثال: تهيئة طوابير ShopMetrics

BASH
# .env — بيئة التطوير
QUEUE_CONNECTION=database

# .env — بيئة الإنتاج
QUEUE_CONNECTION=redis
REDIS_HOST=redis.shopmetrics.internal
REDIS_PORT=6379

# إنشاء جداول الطوابير (مشغل قاعدة البيانات)
php artisan queue:table
php artisan queue:failed-table
php artisan queue:batches-table
php artisan migrate

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

4. إنشاء المهام وتوزيعها

(1) إنشاء مهمة

BASH
php artisan make:job GenerateReportJob
# ينشئ: app/Jobs/GenerateReportJob.php

(2) تعريف المهمة

PHP
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public bool $deleteWhenMissingModels = true;

    public function __construct(
        public Tenant $tenant,
        public string $reportType,
        public string $format = 'csv',
    ) {}

    public function handle(
        ReportService $reportService,
        Mailer $mailer,
    ): void {
        $path = $reportService->generate(
            $this->tenant,
            $this->reportType,
            $this->format,
        );

        $url = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));

        $this->tenant->users->each(function ($user) use ($url) {
            $mailer->to($user)->send(new ReportReadyNotification($url, $this->reportType));
        });
    }

    public function failed(\Throwable $exception): void
    {
        Log::error('Report generation failed', [
            'tenant_id' => $this->tenant->id,
            'report_type' => $this->reportType,
            'error' => $exception->getMessage(),
        ]);
    }
}

(3) توزيع المهمة

PHP
// توزيع أساسي
GenerateReportJob::dispatch($tenant, 'monthly');

// توزيع مؤجل
GenerateReportJob::dispatch($tenant, 'monthly')
    ->delay(now()->addMinutes(5));

// توزيع على طابور محدد
GenerateReportJob::dispatch($tenant, 'monthly')
    ->onQueue('reports');

// توزيع شرطي
GenerateReportJob::dispatchIf($tenant->subscription?->isActive(), $tenant, 'monthly');

// توزيع بعد إرسال الاستجابة للمستخدم
GenerateReportJob::dispatchAfterResponse($tenant, 'monthly');
طريقة التوزيع الوصف السيناريوهات المطبقة
dispatch() إضافة إلى الطابور المهام غير المتزامنة العامة
dispatchSync() تنفيذ متزامن يجب إكماله فورًا
dispatchAfterResponse() تنفيذ بعد الاستجابة المهام الخفيفة
delay() تنفيذ مؤجل المهام المجدولة
onQueue() طابور محدد حسب الأولوية

(1) ▶ مثال: مهمة معالجة الطلبات في ShopMetrics

PHP
// app/Jobs/ProcessOrderJob.php
class ProcessOrderJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = [30, 60, 120];

    public function __construct(public Order $order) {}

    public function handle(
        OrderService $orderService,
        PaymentGateway $payment,
    ): void {
        // خصم الدفع
        $payment->charge($this->order);

        // تحديث المخزون
        $orderService->deductInventory($this->order);

        // بث الحدث
        event(new OrderPlaced($this->order));

        // إرسال بريد تأكيد
        $this->order->user->notify(new OrderConfirmationNotification($this->order));
    }

    public function failed(\Throwable $exception): void
    {
        $this->order->update(['status' => 'failed']);
        $this->order->user->notify(new OrderFailedNotification($this->order));
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

5. آلية إعادة المحاولة عند الفشل

(1) تهيئة إعادة المحاولة

PHP
// في فئة المهمة
public int $tries = 3;           // الحد الأقصى لمحاولات إعادة المحاولة
public int $backoff = 60;        // ثوانٍ بين المحاولات
public int $timeout = 120;       // الحد الأقصى للثواني لكل محاولة

// أو تراجع أسي
public array $backoff = [30, 60, 120]; // 30ث، 60ث، 120ث

// أو تراجع ديناميكي
public function backoff(): int
{
    return 30 * $this->attempts();
}

(2) معالجة الأخطاء

BASH
# عرض المهام الفاشلة
php artisan queue:failed

# إعادة محاولة مهمة فاشلة محددة
php artisan queue:retry 5

# إعادة محاولة جميع المهام الفاشلة
php artisan queue:retry all

# حذف مهمة فاشلة
php artisan queue:forget 5

# مسح جميع المهام الفاشلة
php artisan queue:flush

(3) دورة حياة مهمة الطابور

100%
flowchart LR
    A[تم توزيع المهمة] --> B[الطابور]
    B --> C[استلمها العامل]
    C --> D{نجاح؟}
    D -->|نعم| E[اكتملت المهمة]
    D -->|لا| F{المحاولات < الحد؟}
    F -->|نعم| G[تراجع + إعادة محاولة]
    G --> B
    F -->|لا| H[جدول failed_jobs]
    H --> I[إعادة محاولة يدوية / مسح]

(1) ▶ مثال: تهيئة إعادة محاولة الفشل في ShopMetrics

PHP
// app/Jobs/SendWebhookJob.php
class SendWebhookJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public array $backoff = [10, 30, 60, 120, 300];
    public int $timeout = 30;

    public function __construct(
        public Shop $shop,
        public array $payload,
    ) {}

    public function handle(): void
    {
        $response = Http::timeout($this->timeout)
            ->post($this->shop->webhook_url, $this->payload);

        if (!$response->successful()) {
            $this->release($this->backoff[$this->attempts() - 1] ?? 60);
        }
    }

    public function failed(\Throwable $exception): void
    {
        $this->shop->tenant->users->each(function ($user) {
            $user->notify(new WebhookFailedNotification($this->shop));
        });
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

6. المهام الدفعية

(1) إنشاء دفعة

BASH
php artisan queue:batches-table
php artisan migrate
PHP
// app/Jobs/ProcessTenantAnalyticsJob.php
class ProcessTenantAnalyticsJob implements ShouldQueue
{
    use Batchable;

    public function __construct(public Tenant $tenant) {}

    public function handle(): void
    {
        if ($this->batch()->cancelled()) {
            return;
        }

        AnalyticsService::computeForTenant($this->tenant);
    }
}

(2) توزيع الدفعة

PHP
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch(
    Tenant::active()->get()->map(
        fn ($tenant) => new ProcessTenantAnalyticsJob($tenant)
    )
)->then(function (Batch $batch) {
    // اكتملت جميع المهام بنجاح
    Log::info("Batch {$batch->id} completed: {$batch->totalJobs} tenants processed.");
})->catch(function (Batch $batch, \Throwable $e) {
    // فشل أول مهمة
    Log::error("Batch {$batch->id} failed: {$e->getMessage()}");
})->finally(function (Batch $batch) {
    // يعمل دائمًا (نجاح أو فشل)
    Cache::forget('analytics:computing');
})->name('Process Monthly Analytics')
  ->onQueue('analytics')
  ->dispatch();

(3) إدارة الدفعات

PHP
// التحقق من تقدم الدفعة
$batch = Bus::findBatch($batchId);
$batch->progress();     // 0-100
$batch->processedJobs();
$batch->totalJobs();
$batch->failedJobs();
$batch->finished();

// إلغاء الدفعة
$batch->cancel();
الطريقة الوصف
then() جميع عمليات الاستدعاء ناجحة
catch() استدعاء فشل المحاولة الأولى
finally() استدعاء عند الانتهاء (بغض النظر عن النجاح أو الفشل)
progress() النسبة المئوية المكتملة
cancel() إلغاء المهام المتبقية

(1) ▶ مثال: مهمة دفعية للتحليل الشهري في ShopMetrics

PHP
// app/Console/Commands/ProcessMonthlyAnalytics.php
class ProcessMonthlyAnalytics extends Command
{
    protected $signature = 'analytics:process-monthly';

    public function handle(): int
    {
        $tenants = Tenant::active()->get();
        $this->info("Processing analytics for {$tenants->count()} tenants...");

        Bus::batch(
            $tenants->map(fn ($t) => new ProcessTenantAnalyticsJob($t))
        )->then(function (Batch $batch) {
            $this->info("All {$batch->totalJobs} tenants processed.");
        })->catch(function (Batch $batch, \Throwable $e) {
            $this->error("Batch failed: {$e->getMessage()}");
        })->name('Monthly Analytics')
          ->onQueue('analytics')
          ->allowFailures()
          ->dispatch();

        return self::SUCCESS;
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. Supervisor كعملية خفية

(1) تثبيت Supervisor

BASH
# Ubuntu/Debian
sudo apt-get install supervisor

# إنشاء تهيئة
sudo nano /etc/supervisor/conf.d/shopmetrics-worker.conf

(2) تهيئة Supervisor

INI
[program:shopmetrics-worker-default]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-default.log
stopwaitsecs=3600

[program:shopmetrics-worker-reports]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=reports --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=1
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-reports.log

(3) أوامر Supervisor

BASH
# قراءة التهيئة الجديدة
sudo supervisorctl reread
sudo supervisorctl update

# بدء/إيقاف/إعادة تشغيل العمال
sudo supervisorctl start shopmetrics-worker-default:*
sudo supervisorctl stop shopmetrics-worker-default:*
sudo supervisorctl restart shopmetrics-worker-default:*

# التحقق من الحالة
sudo supervisorctl status
عدد العمليات الطابور المعالج الذاكرة الوصف
2 default منخفض متوسط المهام العامة
1 reports عالي عالي إنشاء التقارير
1 analytics عالي عالي تحليل البيانات
1 notifications منخفض منخفض البريد/الإشعارات

(1) ▶ مثال: تشغيل عامل طوابير متعددة في ShopMetrics

BASH
# التطوير — عامل واحد، جميع الطوابير
php artisan queue:work --queue=default,reports,notifications

# الإنتاج — عمال منفصلون حسب أولوية كل طابور
# الأولوية: high > default > low
php artisan queue:work redis --queue=high,default
php artisan queue:work redis --queue=reports,low

# معالجة المهام بحد زمني (إعادة تشغيل تلقائية لتسرب الذاكرة)
php artisan queue:work --max-time=3600 --max-jobs=1000

# مراقبة الطابور
php artisan queue:monitor redis:default,redis:reports

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

8. مثال شامل: نظام التقارير غير المتزامنة في ShopMetrics

PHP
// ============================================
// شامل: نظام التقارير غير المتزامنة في ShopMetrics
// يغطي: المهام، الدفعات، إعادة المحاولة، Supervisor، الإشعارات
// ============================================

// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public array $backoff = [60, 180, 600];
    public int $timeout = 600;
    public bool $deleteWhenMissingModels = true;

    public function __construct(
        public Tenant $tenant,
        public string $reportType,
        public string $format = 'csv',
        public ?int $userId = null,
    ) {
        $this->onQueue('reports');
    }

    public function handle(ReportService $reportService): void
    {
        $path = $reportService->generate(
            $this->tenant,
            $this->reportType,
            $this->format,
        );

        $downloadUrl = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));

        $user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
        $user?->notify(new ReportReadyNotification(
            downloadUrl: $downloadUrl,
            reportType: $this->reportType,
            expiresAt: now()->addDays(7),
        ));
    }

    public function failed(\Throwable $exception): void
    {
        $user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
        $user?->notify(new ReportFailedNotification(
            reportType: $this->reportType,
            error: $exception->getMessage(),
        ));
    }
}

// الاستخدام في المتحكم
class ReportController extends Controller
{
    public function generate(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'report_type' => 'required|in:monthly,weekly,custom',
            'format' => 'sometimes|in:csv,xlsx,pdf',
            'date_from' => 'sometimes|date',
            'date_to' => 'sometimes|date|after:date_from',
        ]);

        GenerateReportJob::dispatch(
            tenant(),
            $validated['report_type'],
            $validated['format'] ?? 'csv',
            auth()->id(),
        );

        return response()->json([
            'message' => 'Report generation started. You will receive an email when ready.',
            'estimated_time' => '5-15 minutes',
        ], 202);
    }
}

❓ أسئلة شائعة

س ما الفرق بين queue:work و queue:listen؟
ج يعمل queue:work في الذاكرة دون إعادة تشغيل إطار العمل (سريع، لكن يتطلب إعادة تشغيل يدوية بعد تغييرات الكود)؛ بينما يعيد queue:listen تشغيل إطار العمل لكل مهمة (بطيء، لكن يحمل الكود الجديد تلقائيًا). استخدم queue:work + Supervisor في الإنتاج، واستخدم queue:listen أو queue:work --once أثناء التطوير.
س هل يمكنني استخدام نماذج Eloquent في المهمة؟
ج نعم، السمة SerializesModels تقوم تلقائيًا بتسلسل معرّفات النماذج واسترجاعها من قاعدة البيانات أثناء إلغاء التسلسل. الفائدة هي أن المهام لها بصمة بيانات أصغر؛ والمخاطرة هي أنه إذا تم حذف النموذج، ستفشل المهمة (يمكن تجاهل ذلك باستخدام deleteWhenMissingModels).
س كيف يمكنني منع تراكم الطابور؟
ج قم بزيادة عدد العمال (numprocs)، واستخدم مشغلًا أعلى أداءً (Redis)، وحدد مهلة للمهمة لمنع الجمود، وقم بإعداد تنبيهات لمراقبة طول الطابور.
س ماذا يجب أن أفعل إذا فشلت مهمة واحدة في دفعة؟
ج بشكل افتراضي، تلغي الدفعة المهام المتبقية عند مواجهة أول فشل. اضبط allowFailures() للسماح للدفعة بالاستمرار في التنفيذ رغم الفشل الجزئي. استخدم Bus::findBatch() لعرض تفاصيل الفشل وإعادة محاولة المهام الفاشلة يدويًا.
س كيف أقوم بتصحيح أخطاء المهمة؟
ج أثناء التطوير، استخدم QUEUE_CONNECTION=sync لتشغيل المهمة بشكل متزامن (تظهر الأخطاء فورًا)؛ في الإنتاج، استخدم php artisan queue:failed لعرض رسائل الخطأ للمهام الفاشلة؛ يمكنك أيضًا تسجيل معلومات تفصيلية في طريقة failed() للمهمة.
س ما الفرق بين Supervisor و systemd؟
ج Supervisor هو مدير عمليات Python سهل التهيئة وموصى به رسميًا من Laravel؛ بينما systemd هو مدير الخدمات الأصلي في Linux، والذي يقدم أداءً أفضل لكن تهيئته أكثر تعقيدًا. كلاهما يمكنه تشغيل عملية queue:work كعملية خفية.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ GenerateReportJob، وأرسل طلبًا غير متزامن لإنشاء تقرير CSV في المتحكم، واختبر باستخدام مشغل قاعدة البيانات، وشغّل queue:work للتحقق من تنفيذ المهمة.

  2. تمرين متقدم (⭐⭐): قم بتهيئة مشغل طابور Redis لتنفيذ 3 محاولات بإعادة محاولة أسيّة (30ث/60ث/120ث)، وأرسل إشعارات للمستخدمين عند فشل المهمة، واختبر إعادة المحاولة اليدوية (queue:retry).

  3. تحدي (⭐⭐⭐): استخدم Bus::batch() لتنفيذ مهمة دفعية للتحليل الشهري متعدد المستأجرين — مر عبر جميع المستأجرين النشطين، وأنشئ مهمة واحدة لكل مستأجر، وراقب النسبة المئوية للتقدم، وامسح ذاكرة التخزين المؤقت عند الانتهاء، وقم بتهيئة Supervisor لمراقبة العامل.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%