الأساسيات · فصل ٤

الـ Encoding والتطوّر

Encoding and Evolution
🎯 الهدف: تغيّر الـ schema وتعمل rolling deploy من غير ما القديم والجديد يتخانقوا.

المشكلة: الكود القديم والجديد عايشين مع بعض

التطبيقات بتتغير، والـ schema بتتغير. بس التغيير مش بيحصل في لحظة واحدة:

  • في السيرفر: rolling upgrade — بتنزّل النسخة الجديدة على كام نود في المرة (من غير downtime).
  • في الكلاينت (موبايل/ديسكتوب): المستخدم ممكن مايحدثش أصلًا لشهور!
⬅️ Backward compatibility

الكود الجديد يقرا داتا كتبها كود قديم. (سهلة نسبيًا — انت عارف الفورمات القديم.)

➡️ Forward compatibility

الكود القديم يقرا داتا كتبها كود جديد. (أصعب — لازم القديم يتجاهل الحاجات اللي مايعرفهاش.)

أشكال الـ Encoding

في الرام الداتا objects و pointers، وعلى الديسك أو الشبكة لازم تبقى بايتات متسلسلة. التحويل = encoding / serialization / marshalling والعكس decoding.

1) فورمات خاصة بلغة معينة

زي Java Serializable، Python pickle، Ruby Marshal. مريحة بس وحشة:

  • مربوطة بلغة واحدة.
  • ثغرات أمنية: الـ decoding بيقدر يعمل instantiate لـ classes عشوائية = تنفيذ كود.
  • الـ versioning متهمل.
  • الأداء ضعيف (Java serialization مشهور ببطئه وحجمه).

2) JSON و XML و CSV

نصية، مقروءة، ومنتشرة. بس فيها مشاكل:

فيه binary variants لـ JSON (MessagePack, BSON, BJSON…) بس بتوفر حاجة بسيطة لأنها لسه بتكتب أسماء الـ fields في كل record.

3) Thrift و Protocol Buffers

Binary + schema إجباري (IDL). أداة code generation بتطلعلك classes بلغتك. الفكرة الذكية: بدل ما تكتب اسم الـ field ("userName") بتكتب field tag (رقم زي 1، 2، 3) — حجم أصغر بكتير.

Thrift عنده BinaryProtocol و CompactProtocol (بيدمج الـ tag والنوع في بايت، وأرقام variable-length). Protobuf شبه CompactProtocol.

التغييرينفع؟ليه
تغيير اسم field✅الاسم مش في الداتا المتشفرة أصلًا — الـ tag بس.
تغيير رقم الـ tag❌كل الداتا القديمة هتبوظ.
إضافة field جديد✅ بشرطtag جديد. الكود القديم يتجاهله (forward ✓). والجديد يقرا القديم بشرط الـ field يبقى optional أو له default — لو required الكود الجديد هيفشل يقرا القديم.
حذف field✅ بشرطلازم يكون optional، وماتستخدمش رقم الـ tag ده تاني أبدًا.
تغيير النوع⚠️ممكن (int32 → int64) بس الكود القديم ممكن يقطع القيمة. Protobuf: optional → repeated مسموح.

4) Avro

اتعمل عشان Hadoop. أصغر encoding فيهم: مفيش tags ولا أنواع في الداتا خالص — مجرد قيم ورا بعض. طب إزاي تفهم؟ لازم تعرف الـ schema اللي اتكتبت بيها بالظبط.

  • المكتبة بتقارن الاتنين بأسماء الـ fields (فالترتيب مش مهم): field في الـ writer مش في الـ reader → يتجاهل. في الـ reader مش في الـ writer → ياخد الـ default.
  • القاعدة: تضيف أو تشيل fields ليها default بس. (و null لازم يتعمل بـ union type صريح — Avro مش عنده optional/required.)
  • طب الـ reader يعرف الـ writer’s schema منين؟ فايل كبير: مرة واحدة في أوله. داتابيز: رقم version مع كل record وجدول schemas. شبكة: يتفقوا وقت الـ connection (Avro RPC).

مميزات الـ schemas عمومًا

حجم أصغر بكتيرالـ schema = documentation دايمًا محدّثتقدر تتأكد من compatibility قبل الـ deployCode generation + type checking

الداتا بتمشي إزاي؟ (Modes of Dataflow)

أ) عبر الداتابيز

اللي بيكتب = encoder، واللي بيقرا = decoder (ممكن انت نفسك في المستقبل — “بتبعت رسالة لنفسك في المستقبل”). مع الـ rolling upgrade، نسخة قديمة ممكن تقرا داتا كتبتها نسخة جديدة → forward compatibility لازم.

“Data outlives code”: الكود بيتبدل في دقايق، بس الداتا عمرها سنين. عشان كده الـ migration الكامل (rewrite) نادر، والداتابيز بتسيب القديم زي ما هو وتملا الـ null وقت القراءة. وفي الـ archival/backup تقدر تعيد الـ encoding بأحدث schema (Avro object container أو Parquet).

ب) عبر الخدمات: REST و RPC

Client/Server — والسيرفر ممكن نفسه يبقى client لخدمة تانية = SOA / Microservices. الهدف: كل فريق ينشر خدمته مستقل → لازم compatibility بين النسخ.

REST

فلسفة فوق HTTP: URLs للـ resources، cache control، auth، content negotiation. بسيطة ومتسقة مع الويب. (OpenAPI/Swagger للتوثيق.)

SOAP

XML protocol معقد، WSDL، ومعتمد على الـ tooling. Interoperability بين الشركات كانت سيئة. بقى أقل انتشارًا.

Custom RPC (Thrift/gRPC) أسرع وأنسب للخدمات الداخلية جوه نفس الشركة، وREST أنسب للـ public APIs وأسهل في التجربة (curl). والـ compatibility: السيرفرات بتتحدث الأول، فلازم backward في الريكوستات و forward في الردود. ولو API عام، غالبًا هتحتاج تدعم versions قديمة للأبد (version في الـ URL أو الـ header).

ج) عبر الرسايل (Message Brokers)

زي RPC بس asynchronous، والرسالة بتروح لوسيط (RabbitMQ, ActiveMQ, Kafka, NATS…). المميزات:

Buffer لو المستقبل مش متاحإعادة إرسال لو process وقعالمرسل مش لازم يعرف IP المستقبلرسالة واحدة لكذا مستقبلفك الارتباط بين المرسل والمستقبل

الطريقة: producer يبعت لـ queue أو topic، والـ broker يوصّلها لـ consumers/subscribers. المرسل مش بيستنى رد (لو عايز رد يبقى رسالة على queue تانية). والـ broker مش بيفرض data model — أي bytes.

Actor model (Akka, Orleans, Erlang OTP): كل actor له state محلي ومش بيشاركه، وبيتواصل برسايل async. والـ distributed actors بتعمل نفس الفكرة على كذا نود — وبما إنها بتفترض أصلًا إن الرسايل ممكن تضيع، فالفرق بين المحلي والموزع أقل من RPC. بس لسه لازم تفكر في compatibility أثناء الـ rolling upgrade.

🧪 Lab

  1. اعمل ملف .proto فيه message بـ 3 fields، وولّد منه كود (أو استخدم protobufjs).
  2. اعمل encode لرسالة، وبعدين ضيف field جديد بـ tag جديد واقرا الرسالة القديمة بالكود الجديد — هتشتغل.
  3. جرّب تغيّر رقم tag موجود واقرا القديم — هتشوف الداتا بايظة.
  4. قارن حجم نفس الـ object كـ JSON و Protobuf بـ Buffer.byteLength.

راجع الـ docs بتاعة المكتبة لأي flag.

✅ الخلاصة — Checklist

🧠 اختبر نفسك
١) ضفت field جديد required في Protobuf — إيه اللي هيحصل؟
الكود الجديد هيفشل يقرا الداتا القديمة اللي مفيهاش الـ field (كسرت الـ backward compatibility).
٢) ليه Avro مش محتاج tag numbers؟
لأنه بيطابق الـ writer’s schema مع الـ reader’s schema بأسماء الـ fields.
٣) إيه مشكلة retry في RPC؟
ممكن الطلب الأول اتنفذ والرد اللي ضاع، فيتنفذ مرتين — لازم idempotency.