RabbitMQعلى تدفّق موجود.
إزاي تاخد عملية شغّالة بالفعل وتحوّلها لمعالجة في الخلفية — القرار الأول (إيه يستاهل)، وبعدين نقطة الربط، والتطبيق الكامل على الـ checkout مع التعامل مع الفشل والتكرار.
عشان تنقل عملية شغّالة في Magento 2 لـ RabbitMQ: اختار العمليات اللي العميل مش مستني نتيجتها وبتعتمد على نظام خارجي، زي إرسال الأوردر لـ ERP. انشر من observer على sales_order_save_commit_after بعد تأكيد الحفظ، وابعت الـ id مش الكائن كامل، واعمل consumer بيشيّك جدول تتبّع بقيد unique قبل المعالجة، لأن الـ queue بيضمن التوصيل مرة على الأقل مش مرة بالظبط.
إيه اللي يستاهل يروح queue
أهم قرار — ولو غلط، الـ queue بيضيف تعقيد من غير فايدة.
- إرسال أوردر لـ ERP — نظام خارجي، بطيء، العميل مش محتاج ينتظره.
- إشعار push لتطبيق الموبايل — خدمة خارجية، والتأخير ثانيتين مقبول.
- تحديث مخزون في نظام POS — تكامل، مش فوري بالضرورة.
- توليد PDF للفاتورة — تقيل، ومش مطلوب لحظة الشراء.
- مزامنة العميل مع CRM — خارجي وغير حرج.
- حجز المخزون — لازم يحصل فوراً وإلا تبيع حاجة مش موجودة.
- تفويض الدفع — العميل لازم يعرف نجح ولا لأ قبل ما يمشي.
- حساب الإجماليات — النتيجة مطلوبة على الشاشة.
- التحقق من الكوبون — العميل مستني الرد.
فين تحط نقطة الإطلاق
ثلاث اختيارات، وكل واحد له موقفه.
استخدم الأحداث اللي بتتطلق بعد تأكيد الحفظ — اللي أسماؤها بتنتهي بـ _commit_after، أو checkout_submit_all_after اللي بيتطلق بعد اكتمال الـ checkout كله.
خطر
النشر في _save_after — لسه جوّه الـ transaction، وممكن تفشل بعدها.
آمن
النشر في _save_commit_after — الحفظ اتأكّد فعلاً.
تصميم الرسالة
قرار صغير بيفرق كتير في الصيانة.
متبعتش الكائن كامل
الأوردر كله في الرسالة — حجم كبير، وبيانات قديمة لما الـ consumer يشتغل.
ابعت المعرّف
الـ id بس، والـ consumer يحمّل النسخة الحالية من قاعدة البيانات.
- البيانات دايماً حديثة — الـ consumer ممكن يشتغل بعد دقايق، والأوردر ممكن يكون اتغيّر.
- الرسالة خفيفة — أسرع في النقل وأقل ذاكرة.
- مفيش مشاكل serialization — الكائنات المعقّدة بتسبب مشاكل في التحويل.
- التغيير أسهل — لو ضفت حقل للأوردر، مش محتاج تغيّر شكل الرسالة.
$this->publisher->publish( 'vendor.order.sync', json_encode($order->getData()) // ثقيل وقديم );
$message = json_encode([ 'order_id' => (int) $order->getId(), 'increment' => $order->getIncrementId(), // للتتبّع 'store_id' => (int) $order->getStoreId(), 'event' => 'order_placed', // للتمييز 'queued_at' => gmdate('c'), // للتشخيص ]); $this->publisher->publish('vendor.order.sync', $message);
تحليل الـ checkout
نطبّق الأربع أسئلة على عمليات الـ checkout الحقيقية.
العميل بيشوف صفحة الشكر بعد إنشاء الأوردر مباشرة. الـ ERP والإيميل والإشعار بيحصلوا في الخلفية بعد كده.
جدول التتبّع
قبل الكود — ده اللي هيخلّي الـ idempotency ممكنة.
الـ queue ممكن يوصّل نفس الرسالة أكتر من مرة (لو الـ consumer وقع قبل التأكيد). محتاج تعرف "أنا عالجت الأوردر ده قبل كده ولا لأ" — والجدول ده هو الذاكرة دي.
<?xml version="1.0"?> <schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <table name="vendor_order_sync" resource="default"> <column xsi:type="int" name="entity_id" unsigned="true" identity="true"/> <column xsi:type="int" name="order_id" unsigned="true"/> <column xsi:type="varchar" name="status" length="20"/> <column xsi:type="int" name="attempts" default="0"/> <column xsi:type="text" name="last_error" nullable="true"/> <column xsi:type="timestamp" name="updated_at" default="CURRENT_TIMESTAMP" on_update="true"/> <constraint xsi:type="primary" referenceId="PRIMARY"> <column name="entity_id"/> </constraint> <!-- المفتاح: أوردر واحد = صف واحد --> <constraint xsi:type="unique" referenceId="VENDOR_ORDER_SYNC_ORDER_ID"> <column name="order_id"/> </constraint> </table> </schema>
ملفات الإعداد
الأربعة المعتادين — مختصرين هنا.
<config> <topic name="vendor.order.sync" request="string" handler="Vendor\OrderSync\Model\Consumer::process"/> </config>
<config> <exchange name="magento" type="topic" connection="amqp"> <binding id="orderSyncBinding" topic="vendor.order.sync" destinationType="queue" destination="vendor.order.sync"/> </exchange> </config>
<config> <publisher topic="vendor.order.sync"> <connection name="amqp" exchange="magento"/> </publisher> </config>
<config> <consumer name="vendorOrderSync" queue="vendor.order.sync" connection="amqp" handler="Vendor\OrderSync\Model\Consumer::process"/> </config>
نقطة الربط — الـ Observer
هنا بنربط بالتدفّق الموجود من غير ما نلمسه.
<config> <!-- بعد تأكيد الحفظ، مش قبله --> <event name="sales_order_save_commit_after"> <observer name="vendor_queue_order_sync" instance="Vendor\OrderSync\Observer\QueueOrderSync"/> </event> </config>
namespace Vendor\OrderSync\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; use Magento\Framework\MessageQueue\PublisherInterface; use Psr\Log\LoggerInterface; class QueueOrderSync implements ObserverInterface { private const TOPIC = 'vendor.order.sync'; public function __construct( private readonly PublisherInterface $publisher, private readonly LoggerInterface $logger ) {} public function execute(Observer $observer): void { $order = $observer->getEvent()->getOrder(); if (!$order || !$order->getId()) { return; } // الحدث بيتطلق مع أي حفظ — ننشر عند الإنشاء بس if (!$order->isObjectNew()) { return; } try { $this->publisher->publish(self::TOPIC, json_encode([ 'order_id' => (int) $order->getId(), 'increment' => $order->getIncrementId(), 'store_id' => (int) $order->getStoreId(), 'queued_at' => gmdate('c'), ])); } catch (\Exception $e) { // الأوردر اتعمل بنجاح — منوقّعوش بسبب الطابور $this->logger->critical( 'Failed to queue order sync: ' . $e->getMessage(), ['order_id' => $order->getId()] ); } } }
- _commit_after مش _save_after — ضمان إن الأوردر موجود فعلاً.
- شيك isObjectNew() — الحدث بيتطلق مع كل حفظ للأوردر، مش عند الإنشاء بس. من غيره هتبعت رسالة كل مرة الأوردر يتغيّر.
- try/catch حوالين النشر — لو RabbitMQ واقع، الأوردر المفروض يكمّل عادي. الفشل بيتسجّل كـ critical عشان يتراجع.
الـ Consumer مع idempotency
الجزء اللي بيحدد جودة التكامل كله.
namespace Vendor\OrderSync\Model; class Consumer { public function __construct( private readonly OrderRepositoryInterface $orderRepository, private readonly SyncLog $syncLog, private readonly ErpClient $erp, private readonly LoggerInterface $logger ) {} public function process(string $message): void { $data = json_decode($message, true); $orderId = (int) ($data['order_id'] ?? 0); if (!$orderId) { // رسالة تالفة — سجّل واخرج، متحاولش تاني $this->logger->error('Malformed message', ['raw' => $message]); return; } // 1. الحماية من التكرار if ($this->syncLog->isDone($orderId)) { return; // اتعالج قبل كده } try { // 2. حمّل النسخة الحالية $order = $this->orderRepository->get($orderId); // 3. العملية الخارجية $this->erp->sendOrder($order); // 4. سجّل النجاح $this->syncLog->markDone($orderId); } catch (NoSuchEntityException $e) { // الأوردر مش موجود — إعادة المحاولة مش هتنفع $this->syncLog->markFailed($orderId, 'order not found'); } catch (\Exception $e) { // فشل مؤقت — سجّله عشان الـ cron يعيد $this->syncLog->markRetry($orderId, $e->getMessage()); $this->logger->error('ERP sync failed', [ 'order_id' => $orderId, 'error' => $e->getMessage(), ]); } } }
ده أهم تفصيل في الكود. فشل دائم (أوردر مش موجود، رسالة تالفة) — إعادة المحاولة هتفشل بنفس الطريقة للأبد، فنسجّله ونخرج. فشل مؤقت (الـ ERP واقع، انقطاع شبكة) — يستاهل إعادة.
لأن الـ queue بيضمن التوصيل مرة على الأقل، مش مرة بالظبط. لو الـ consumer عالج الأوردر وبعت للـ ERP ووقع قبل ما يأكّد استلام الرسالة، الرسالة هترجع — والشيك ده هو اللي بيمنع إرسالها للـ ERP مرتين.
الفشل وإعادة المحاولة
الجزء اللي بيتنسي لحد ما يحصل مشكلة.
إعادة المحاولة المدمجة في الـ queue محدودة وصعب التحكم فيها. الأسهل والأوضح: سجّل الفشل في جدولك، واعمل cron بيدوّر على اللي فشل ويعيد نشره بتباعد متزايد.
public function execute(): void { // اللي فشل ولسه محاولاته أقل من الحد $rows = $this->syncLog->getRetryable(maxAttempts: 5); foreach ($rows as $row) { // تباعد متزايد: 2^n دقيقة $waitMinutes = 2 ** (int) $row['attempts']; if (!$this->waitedEnough($row, $waitMinutes)) { continue; } $this->publisher->publish('vendor.order.sync', json_encode([ 'order_id' => (int) $row['order_id'], 'retry' => true, ])); } }
لو الـ ERP واقع، إعادة المحاولة كل دقيقة بتزوّد الضغط عليه وبتأخّر تعافيه. التباعد المتزايد (دقيقتين، أربعة، تمانية...) بيدّي الخدمة فرصة ترجع.
بعد الحد الأقصى، علّم الصف كـ failed نهائياً ونبّه حد — إيميل أو Slack. أوردر مش متزامن مع الـ ERP مشكلة تشغيلية محتاجة تدخّل بشري، مش حاجة تتسجّل في log ومحدش يقراها.
تشغيل الـ Consumers
الـ consumer مايشتغلش لوحده — لازم حاجة تشغّله وتحافظ عليه.
# شغّال باستمرار bin/magento queue:consumers:start vendorOrderSync # عدد محدود — مفيد للاختبار bin/magento queue:consumers:start vendorOrderSync \ --max-messages=10 # شوف كل الـ consumers المتاحة bin/magento queue:consumers:list
'cron_consumers_runner' => [ 'cron_run' => true, 'max_messages' => 1000, 'consumers' => ['vendorOrderSync'], ],
المراقبة والتشخيص
الطوابير بتفشل بصمت — ده أخطر ما فيها.
- طول الطابور — لو بيزيد باستمرار، الـ consumer واقف أو بطيء.
- عمر أقدم رسالة — بيقولك التأخير الفعلي.
- عدد الـ consumers الشغّالة — صفر = مفيش معالجة خالص.
- معدّل الفشل — ارتفاع مفاجئ يعني النظام الخارجي عنده مشكلة.
# طول الطوابير (من سيرفر RabbitMQ) rabbitmqctl list_queues name messages consumers # هل الـ consumer شغّال؟ ps aux | grep queue:consumers:start # أخطاء المزامنة في الـ log grep "ERP sync failed" var/log/*.log | tail -20
-- توزيع الحالات SELECT status, COUNT(*) FROM vendor_order_sync GROUP BY status; -- الأوردرات اللي فشلت نهائياً SELECT order_id, attempts, last_error, updated_at FROM vendor_order_sync WHERE status = 'failed' ORDER BY updated_at DESC; -- أوردرات مالهاش صف أصلاً (رسالة ضاعت) SELECT o.entity_id FROM sales_order o LEFT JOIN vendor_order_sync s ON s.order_id = o.entity_id WHERE s.order_id IS NULL AND o.created_at < NOW() - INTERVAL 1 HOUR;
# 1. اعمل أوردر من الواجهة أو برمجياً # 2. شوف الرسالة وصلت الطابور rabbitmqctl list_queues name messages # 3. شغّل رسالة واحدة وراقب bin/magento queue:consumers:start vendorOrderSync \ --max-messages=1 # 4. اتأكد من التسجيل # SELECT * FROM vendor_order_sync ORDER BY entity_id DESC LIMIT 1; # 5. اختبر الحماية من التكرار: أعد نشر نفس الرسالة # المفروض الـ consumer يتجاهلها
الفخاخ الشائعة
اقرا دي حتى لو مش هتقرا حاجة تانية.
- النشر جوّه transaction — لو فشلت، بعتّ رسالة عن حاجة مش موجودة. انشر في _commit_after.
- مفيش idempotency — التكرار وارد دايماً. بدونها هتبعت نفس الأوردر للـ ERP مرتين.
- إرسال الكائن كامل — بيانات قديمة ورسالة تقيلة. ابعت المعرّف.
- عدم التمييز بين الفشل المؤقت والدائم — رسالة تالفة بتتعاد للأبد.
- نسيان isObjectNew() — رسالة بتتبعت مع كل حفظ للأوردر مش عند الإنشاء بس.
- مفيش max_messages — الـ consumer بيستهلك ذاكرة لحد ما يقع.
- مفيش مراقبة — الطابور بيتراكم لأسبوع ومحدش واخد باله.
- استثناء غير محسوب في الـ publisher — سقوط RabbitMQ بيوقّع الـ checkout. لفّه في try/catch.
- نقل عملية متزامنة بطبيعتها — الدفع والمخزون لازم يفضلوا فوريين.
RabbitMQ على تدفّق موجود ✓
دلوقتي عندك الطريقة كاملة: تقرّر إيه يستاهل، تختار نقطة الربط الصح، تصمّم الرسالة، تكتب consumer آمن ضد التكرار، وتشغّله وتراقبه في الإنتاج.
الخلاصة: انشر بعد تأكيد الحفظ، ابعت المعرّف مش الكائن، واعتبر التكرار حاصل لا محالة. التلاتة دول بيفرّقوا بين تكامل بيشتغل وتكامل بيسبّب مشاكل صامتة.
خلّصت الدرس؟علّمه عشان تتابع تقدّمك في الكورس.
أسئلة شائعة
إيه العمليات اللي تستاهل تروح message queue في Magento 2؟
اسأل أربع أسئلة: العميل محتاج النتيجة فورًا؟ بتعتمد على نظام خارجي؟ بتاخد وقت ملحوظ؟ ممكن تتنفّذ مرتين بأمان؟ إرسال الأوردر لـ ERP وإيميل التأكيد والـ push notification ونقاط الولاء والتحليلات تستاهل. تفويض الدفع وحجز المخزون وإنشاء الأوردر والتحقق من الكوبون لازم يفضلوا متزامنين لأن العميل مستني نتيجتهم على الشاشة.
أنشر الرسالة في أنهي event في Magento 2؟
بعد ما الـ transaction تتأكّد، يعني sales_order_save_commit_after أو checkout_submit_all_after، مش _save_after اللي لسه جوّه الـ transaction وممكن تفشل بعده فتبعت رسالة عن أوردر مش موجود. في الـ observer اتأكد بـ isObjectNew إنك بتنشر عند الإنشاء بس، ولفّ النشر في try/catch عشان لو RabbitMQ واقع الأوردر يكمّل عادي والفشل يتسجّل كـ critical.
أبعت الأوردر كامل في الرسالة ولا الـ id بس؟
الـ id بس مع سياق بسيط زي increment_id و store_id و queued_at، والـ consumer يحمّل النسخة الحالية من قاعدة البيانات. الكائن كامل بيبقى تقيل، وبياناته بتبقى قديمة لأن الـ consumer ممكن يشتغل بعد دقايق والأوردر اتغيّر، وبيسبب مشاكل serialization، وأي حقل جديد بيغيّر شكل الرسالة. الـ queued_at والـ increment بيوفّروا وقت كبير في التشخيص.
إزاي أعمل idempotency في consumer بتاع Magento 2؟
جدول تتبّع فيه order_id بقيد unique و status و attempts و last_error. الـ consumer بيشيّك isDone الأول ويخرج لو اتعالج، وبعدين يحمّل الأوردر وينفّذ العملية الخارجية ويعلّم النجاح. القيد الفريد في قاعدة البيانات بيحمي حتى لو رسالتين اتعالجوا في نفس اللحظة. الـ queue بيضمن التوصيل مرة على الأقل مش مرة بالظبط، فالتكرار حاصل لا محالة.
إزاي أفرّق بين الفشل المؤقت والدائم في الـ consumer؟
الفشل الدائم زي أوردر مش موجود أو رسالة تالفة إعادة المحاولة هتفشل فيه بنفس الطريقة للأبد، فسجّله كـ failed واخرج وإلا هتبقى عندك poison message بتملا الـ log. الفشل المؤقت زي الـ ERP واقع أو انقطاع شبكة يستاهل إعادة: علّمه retry في جدول التتبّع، وخلّي cron يعيد نشره بتباعد متزايد لحد أقصى من المحاولات، وبعدها نبّه حد.
إزاي أشغّل الـ consumers في production في Magento 2؟
أبسط طريقة cron_consumers_runner في env.php بـ cron_run true و max_messages وقائمة الـ consumers، والـ cron بيشغّلهم دوريًا ومناسب لو التأخير بدقايق مقبول. لو محتاج معالجة شبه فورية استخدم Supervisor بيشغّل queue:consumers:start باستمرار ويرجّعه لو وقع. الـ max_messages مهم لأنه بيعيد تدوير عملية PHP طويلة العمر قبل ما الذاكرة تتراكم.
إزاي أعرف إن رسالة ضاعت من الطابور؟
راقب طول الطابور وعمر أقدم رسالة وعدد الـ consumers الشغّالة بـ rabbitmqctl list_queues name messages consumers. والأهم استعلام LEFT JOIN بين sales_order وجدول التتبّع بيطلّع الأوردرات اللي مالهاش صف أصلًا بعد ساعة من إنشائها، لأن دي الحالة اللي مفيش log بيمسكها. اعمل cron احتياطي يعيد نشر الأوردرات دي، واختبر الـ idempotency صراحةً بإعادة نشر نفس الرسالة.