لأسابيع، كانت مهمة cron في Elevare Digital تستيقظ في موعدها المحدد، وتفحص طابورها، وتسجل نجاحاً تاماً. لكنها لم توافق على أي مسودة على الإطلاق. تسع عشرة قطعة من المحتوى ظلت تنتظر. لم يكتشف الفريق الأمر إلا لاحقاً، بعد أن تحول الفراغ الصامت من مجرد أمر غريب إلى تراكم ملحوظ في العمل. لم يتعطل أي شيء، ولم تنطلق أي تنبيهات استدعاء. كان النظام سليماً من الناحية التقنية، ولكنه ميت من الناحية الوظيفية.
هذا هو الرعب الصامت لخطوط الأنابيب المستقلة (autonomous pipelines). فعندما تزيل العنصر البشري من الحلقة، فإنك تزيل أيضاً الشخص الذي يلاحظ أن لا شيء يحدث.
خط الأنابيب الذي يعمل تلقائياً
تدير Elevare Digital سير عمل للمحتوى مؤتمتاً بالكامل. تقوم وكلاء البرمجيات (Software agents) بإنشاء المسودات، وتعمل مهمة cron مجدولة للموافقة كحارس للبوابة، حيث تراجع تلك المسودات وتدفع العناصر المعتمدة مباشرة إلى النشر. لا يفتح أي بشري لوحة التحكم لمباركة كل دفعة. الهدف الأساسي هو أن تتولى الآلة الأعمال الروتينية المملة بينما ينتقل الفريق إلى مشكلات أخرى.
في ظل هذا النموذج، تصبح الثقة هي واجهتك الأساسية. أنت تثق في أن المجدول سيعمل، وتثق في أن المهمة ستنفذ، وتثق في رمز الخروج (exit code). عندما تظهر السجلات نبضات مستقرة من استجابات 200 OK، تفترض أن العمل يسير. ولأسابيع، كان ذلك النبض مثالياً؛ كانت مهمة cron تعمل في وقتها، في كل مرة، لكنها ببساطة لم تقم بالعمل الفعلي أبداً.
تسع عشرة مسودة دون أي إنذار
كان الاكتشاف عرضياً. لاحظ شخص ما في النهاية أن طابور النشر قد سكن، أو ربما تحقق من مقياس لاحق (downstream metric) ورأى خطاً مستقيماً ثابتاً. ما وجدوه كان مخزناً من تسع عشرة مسودة لم تلمسها يد. كان المعتمد يعمل بجد، ويسجل النجاح كل يوم، لكنه لم يعالج أي واحدة منها.
في سير العمل اليدوي، كان المراجع البشري سيلاحظ صندوق وارد فارغاً أو تكدساً للعناصر المعلقة منذ اليوم الأول. أما في النسخة المؤتمتة، فقد بدا غياب النشاط تماماً مثل غياب العمل. لم يكن لدى مهمة cron مدير لتخيب أمله؛ كانت تكتفي بتسجيل الحضور والعودة إلى المنزل مبكراً.
خطآن برمجيان، ونتيجة واحدة فارغة
كان للفشل سببان. لم يكن أي منهما خطأ في الصياغة (syntax error)، أو انتهاء مهلة (timeout)، أو انقطاعاً في التبعيات. كلاهما كان خطأً دلالياً (semantic mistake) أدى إلى تحويل تسعة عشر صفاً صالحاً إلى لا شيء في نظر محرك الاستعلام.
أولاً، عدم تطابق الأنواع (type mismatch). قام الوكيل الذي ينشئ المسودات بكتابة سجلات مصنفة كـ article. بينما قامت مهمة cron الخاصة بالاعتماد بالاستعلام تحديداً عن أنواع thread. هذا هو نوع الانحراف الذي يحدث عندما يتطور المنتجون والمستهلكون في مسارات متوازية. قرر فريق واحد — أو وكيل واحد — أن المخرج هو مقال (article)، بينما كتب آخر المستهلك بافتراض أنه سيعالج خيوطاً (threads). لم يطلق أي نظام أنواع خطأ وقت التجميع (compile-time error) لأن هذه كانت على الأرجح وسوم نصوص مرنة، ربما حقول JSON أو قيم varchar غير مقيدة. ببساطة، لم يجد محرك قاعدة البيانات أي تطابقات وأعاد مجموعة فارغة. وهذا لا يعتبر حالة خطأ بالنسبة للمحرك، بل هو إجابة صحيحة على سؤال خاطئ.
ثانياً، قام عملية ربط داخلي (inner join) في استعلام المعتمد بابتلاع الصفوف بالكامل وبصمت. إذا قام الاستعلام بربط جدول المسودات بجدول آخر — ربما للبحث عن بيانات وصفية (metadata)، أو أعلام الحالة (status flags)، أو قواعد التوجيه (routing rules) — وفشل شرط الربط، فإن الربط الداخلي سيتصرف تماماً كما هو مصمم له: استبعاد الصفوف غير المتطابقة. لم تظهر أي صفوف يتيمة في مجموعة النتائج، ولم تشير أي قيم null إلى وجود مشكلة. مرت المسودات التسع عشرة عبر الاستعلام مثل الماء عبر المنخل، وتلقت طبقة التطبيق قائمة فارغة ونقية.
ولأن الاستعلام لم يرجع أي صفوف، خرجت الدالة بسلاسة. لم تتصاعد أي استثناءات (exceptions)، وكانت استجابة HTTP هي 200 OK. سجلت مهمة cron النجاح وعادت إلى النوم.
فخ "المعالجة صفر"
هنا يكمن جوهر المشكلة. في النظام القائم على الطوابير، غالباً ما يجد المستهلك صفراً من الصفوف لمعالجتها؛ فيفرغ الطابور، وينتهي العامل بسرعة. يقرأ السجل processed: 0 ويقرأ الفريق ذلك كخبر جيد: نحن نواكب الطلب. هذه حالة سليمة.
لكن processed: 0 تشفر واقعين مختلفين تماماً:
- حالة سليمة: صفر تمت معالجته لأن الصفر معلق. الطابور فارغ. النظام في حالة خمول عن تصميم.
- حالة معطلة: صفر تمت معالجته لأن المستهلك لا يستطيع رؤية العمل. الطابور يحتوي على تسعة عشر صفاً. النظام أعمى، وليس خاملاً.
بدون فحص مستقل لعمق الطابور (queue depth)، ترسل هاتان الحالتان بيانات تتبع (telemetry) متطابقة. تبدوان متشابهتين في لوحات التحكم، وتظهران بنفس الطريقة في مجمعات السجلات، وتسببان نفس الصمت في PagerDuty. لقد بنيت استراتيجية مراقبة تكتشف متى يصرخ العامل، وليس متى يمر بصمت بجانب كومة من العمل الحقيقي.
سد الفجوة
Elevare Digital fixed the problem by changing what they monitor. They stopped relying solely on error rates and success statuses. Instead, they started alerting on the gap between available work and completed work.
After every batch, they now run a simple invariant check:
- If processed is 0 and pending rows are greater than 0, trigger a high severity alert.
This rule is deliberately agnostic about cause. It does not care if the miss was a bad filter, a broken join, or a mistyped enum string. It cares only that work exists and no work got done. This shifts monitoring from “Did the process complain?” to “Did the work move?”
To support this, they treat queue depth as a first-class metric, tracked over time, not just as a spot-check. If the producer keeps adding rows while the consumer continuously reports success, the depth trend turns into a smoking gun. A static snapshot might lie, but a creeping backlog never does.
Lessons for Autonomous Systems
The Elevare incident contains a handful of practical rules for anyone running hands-off pipelines.
Log scanned rows separately from processed rows. The consumer might execute a query that touches forty rows, filters them all out through bad criteria, and reports processed: 0. If you only log the final count, you miss the ghost interaction. A scanned-rows metric reveals that the worker showed up, looked at the work, and walked away confused. That gap between scanned and processed is often your earliest signal.
Track queue depth as a time-series. A queue that is temporarily empty is fine. A queue that grows monotonically while workers stay green is not. Plot depth against consumer throughput. When the two diverge, investigate immediately, even if every health check is passing.
Test consumers against real producer output, not just mocks. Unit tests with mocked data carry the assumptions of the tester. If the mock factory produces thread types and the consumer expects thread types, your tests pass while production fails. Run integration tests that pull actual records from the producer’s output. Make sure the consumer can truly see what the producer writes.
Treat data types and enum values as contracts. Loose string tags in JSON blobs are convenient until they become invisible failure points. Define schemas explicitly. Share constants. Validate payloads at the seam between producer and consumer. If the contract breaks, the system should fail loudly at the boundary, not silently inside a WHERE clause.
The Real Takeaway
Autonomous systems do not fail like humans. They do not call in sick, throw exceptions every time, or leave obvious crash dumps. They return 200 OK and let the inventory rot. If your alerts only listen for screams, you will miss the most expensive failures—the ones where everything looks fine and nothing gets done.
Design your observability to watch the gap. Measure the work that enters against the work that exits. When the two no longer match, assume the machine is lying to you. Because sometimes, a perfect success log is the only symptom of a system that has gone completely blind.
