trustgraph/docs/tech-specs/pubsub.he.md
Alex Jenkins 8954fa3ad7 Feat: TrustGraph i18n & Documentation Translation Updates (#781)
Native CLI i18n: The TrustGraph CLI has built-in translation support
that dynamically loads language strings. You can test and use
different languages by simply passing the --lang flag (e.g., --lang
es for Spanish, --lang ru for Russian) or by configuring your
environment's LANG variable.

Automated Docs Translations: This PR introduces autonomously
translated Markdown documentation into several target languages,
including Spanish, Swahili, Portuguese, Turkish, Hindi, Hebrew,
Arabic, Simplified Chinese, and Russian.
2026-04-14 12:08:32 +01:00

965 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: default
title: "תשתית Pub/Sub"
parent: "Hebrew (Beta)"
---
# תשתית Pub/Sub
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
## סקירה כללית
מסמך זה מפרט את כל החיבורים בין בסיס הקוד של TrustGraph לתשתית ה-pub/sub. כיום, המערכת מקודדת בצורה קבועה לשימוש ב-Apache Pulsar. ניתוח זה מזהה את כל נקודות האינטגרציה כדי ליידע שינויים עתידיים לכיוון הפשטה של pub/sub הניתנת לתצורה.
## מצב נוכחי: נקודות אינטגרציה של Pulsar
### 1. שימוש ישיר בלקוח Pulsar
**מיקום:** `trustgraph-flow/trustgraph/gateway/service.py`
שער ה-API מייבא ויוצר ישירות את לקוח ה-Pulsar:
**שורה 20:** `import pulsar`
**שורות 54-61:** יצירה ישירה של `pulsar.Client()` עם `pulsar.AuthenticationToken()` אופציונלי
**שורות 33-35:** תצורת מארח ברירת מחדל של Pulsar ממשתני סביבה
**שורות 178-192:** ארגומנטים של שורת הפקודה עבור `--pulsar-host`, `--pulsar-api-key` ו-`--pulsar-listener`
**שורות 78, 124:** מעביר `pulsar_client` ל-`ConfigReceiver` ו-`DispatcherManager`
זה המיקום היחיד שבו נוצר ישירות לקוח Pulsar מחוץ לשכבת ההפשטה.
### 2. מסגרת מעבד בסיסית
**מיקום:** `trustgraph-base/trustgraph/base/async_processor.py`
המחלקה הבסיסית לכל המעבדים מספקת קישוריות ל-Pulsar:
**שורה 9:** `import _pulsar` (לטיפול בחריגים)
**שורה 18:** `from . pubsub import PulsarClient`
**שורה 38:** יוצר `pulsar_client_object = PulsarClient(**params)`
**שורות 104-108:** מאפיינים החושפים `pulsar_host` ו-`pulsar_client`
**שורה 250:** שיטה סטטית `add_args()` קוראת ל-`PulsarClient.add_args(parser)` עבור ארגומנטים של שורת הפקודה
**שורות 223-225:** טיפול בחריגים עבור `_pulsar.Interrupted`
כל המעבדים יורשים מ-`AsyncProcessor`, מה שהופך זאת לנקודת האינטגרציה המרכזית.
### 3. הפשטת צרכן
**מיקום:** `trustgraph-base/trustgraph/base/consumer.py`
צורך הודעות מתורים ומפעיל פונקציות מטפלות:
**ייבוא של Pulsar:**
**שורה 12:** `from pulsar.schema import JsonSchema`
**שורה 13:** `import pulsar`
**שורה 14:** `import _pulsar`
**שימוש ספציפי ל-Pulsar:**
**שורות 100, 102:** `pulsar.InitialPosition.Earliest` / `pulsar.InitialPosition.Latest`
**שורה 108:** עטיפה של `JsonSchema(self.schema)`
**שורה 110:** `pulsar.ConsumerType.Shared`
**שורות 104-111:** `self.client.subscribe()` עם פרמטרים ספציפיים ל-Pulsar
**שורות 143, 150, 65:** שיטות `consumer.unsubscribe()` ו-`consumer.close()`
**שורה 162:** חריגה של `_pulsar.Timeout`
**שורות 182, 205, 232:** `consumer.acknowledge()` / `consumer.negative_acknowledge()`
**קובץ מפרט:** `trustgraph-base/trustgraph/base/consumer_spec.py`
**שורה 22:** מפנה ל-`processor.pulsar_client`
### 4. הפשטת מפרסם
**מיקום:** `trustgraph-base/trustgraph/base/producer.py`
שולח הודעות לתורים:
**ייבוא של Pulsar:**
**שורה 2:** `from pulsar.schema import JsonSchema`
**שימוש ספציפי ל-Pulsar:**
**שורה 49:** עטיפה של `JsonSchema(self.schema)`
**שורות 47-51:** `self.client.create_producer()` עם פרמטרים ספציפיים ל-Pulsar (נושא, סכימה, הפעלה של חלוקה לחלקים)
**שורות 31, 76:** שיטה `producer.close()`
**שורות 64-65:** `producer.send()` עם הודעה ומאפיינים
**קובץ מפרט:** `trustgraph-base/trustgraph/base/producer_spec.py`
**שורה 18:** מפנה ל-`processor.pulsar_client`
### 5. הפשטת מפרסם
**מיקום:** `trustgraph-base/trustgraph/base/publisher.py`
פרסום הודעות אסינכרוני עם חיץ תורים:
**ייבוא של Pulsar:**
**שורה 2:** `from pulsar.schema import JsonSchema`
**שורה 6:** `import pulsar`
**שימוש ספציפי ל-Pulsar:**
**שורה 52:** עטיפה של `JsonSchema(self.schema)`
**שורות 50-54:** `self.client.create_producer()` עם פרמטרים ספציפיים ל-Pulsar
**שורות 101, 103:** `producer.send()` עם הודעה ומאפיינים אופציונליים
**שורות 106-107:** שיטות `producer.flush()` ו-`producer.close()`
### 6. הפשטת מנוי
**מיקום:** `trustgraph-base/trustgraph/base/subscriber.py`
מספק הפצת הודעות למספר נמענים ממחזורים:
**ייבוא מ-Pulsar:**
**שורה 6:** `from pulsar.schema import JsonSchema`
**שורה 8:** `import _pulsar`
**שימוש ספציפי ל-Pulsar:**
**שורה 55:** `JsonSchema(self.schema)` wrapper
**שורה 57:** `self.client.subscribe(**subscribe_args)`
**שורות 101, 136, 160, 167-172:** חריגות של Pulsar: `_pulsar.Timeout`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
**שורות 159, 166, 170:** שיטות צרכן: `negative_acknowledge()`, `unsubscribe()`, `close()`
**שורות 247, 251:** אישור הודעות: `acknowledge()`, `negative_acknowledge()`
**קובץ מפרט:** `trustgraph-base/trustgraph/base/subscriber_spec.py`
**שורה 19:** מפנה ל-`processor.pulsar_client`
### 7. מערכת סכימות (Heart of Darkness)
**מיקום:** `trustgraph-base/trustgraph/schema/`
כל סכימת הודעה במערכת מוגדרת באמצעות מסגרת הסכימות של Pulsar.
**אלמנטים בסיסיים:** `schema/core/primitives.py`
**שורה 2:** `from pulsar.schema import Record, String, Boolean, Array, Integer`
כל הסכימות יורשות מהמחלקה הבסיסית של Pulsar `Record`
כל סוגי השדות הם סוגי Pulsar: `String()`, `Integer()`, `Boolean()`, `Array()`, `Map()`, `Double()`
**דוגמאות לסכימות:**
`schema/services/llm.py` (שורה 2): `from pulsar.schema import Record, String, Array, Double, Integer, Boolean`
`schema/services/config.py` (שורה 2): `from pulsar.schema import Record, Bytes, String, Boolean, Array, Map, Integer`
**שמות נושאים:** `schema/core/topic.py`
**שורות 2-3:** פורמט נושא: `{kind}://{tenant}/{namespace}/{topic}`
מבנה ה-URI הזה ספציפי ל-Pulsar (לדוגמה, `persistent://tg/flow/config`)
**השפעה:**
כל הגדרות הודעות בקשות/תגובות בכל בסיס הקוד משתמשות בסכימות של Pulsar
זה כולל שירותים עבור: config, flow, llm, prompt, query, storage, agent, collection, diagnosis, library, lookup, nlp_query, objects_query, retrieval, structured_query
הגדרות סכימות מיובאות ומשמשות באופן נרחב בכל המעבדים והשירותים
## סיכום
### תלויות של Pulsar לפי קטגוריה
1. **יצירת מופע לקוח:**
ישיר: `gateway/service.py`
מופשט: `async_processor.py``pubsub.py` (PulsarClient)
2. **העברת הודעות:**
צרכן: `consumer.py`, `consumer_spec.py`
מפיק: `producer.py`, `producer_spec.py`
מפרסם: `publisher.py`
מנוי: `subscriber.py`, `subscriber_spec.py`
3. **מערכת סכימות:**
סוגים בסיסיים: `schema/core/primitives.py`
כל סכימות השירות: `schema/services/*.py`
שמות נושאים: `schema/core/topic.py`
4. **מושגים ספציפיים ל-Pulsar הנדרשים:**
העברת הודעות מבוססת נושאים
מערכת סכימות (Record, סוגי שדות)
מנויים משותפים
אישור הודעות (חיובי/שלילי)
מיקום צרכן (מוקדם/מאוחר)
מאפייני הודעות
מיקומים התחלתיים וסוגי צרכנים
תמיכה בפיצול
נושאים קבועים לעומת לא קבועים
### אתגרי שינוי מבנה
החדשות הטובות: שכבת ההפשטה (צרכן, מפיק, מפרסם, מנוי) מספקת אריזה נקייה של רוב האינטראקציות של Pulsar.
האתגרים:
1. **הנחת סכימות נרחבת:** כל הגדרת הודעה משתמשת ב-`pulsar.schema.Record` ובסוגי Pulsar
2. **אנונימים ספציפיים ל-Pulsar:** `InitialPosition`, `ConsumerType`
3. **חריגות של Pulsar:** `_pulsar.Timeout`, `_pulsar.Interrupted`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
4. **חתימות שיטות:** `acknowledge()`, `negative_acknowledge()`, `subscribe()`, `create_producer()`, וכו'.
5. **פורמט URI של נושא:** מבנה `kind://tenant/namespace/topic` של Pulsar
### שלבים הבאים
כדי להפוך את התשתית של pub/sub לכזו שניתן להגדיר, עלינו:
1. ליצור ממשק הפשטה עבור מערכת הלקוח/סכימות
2. להפשיט אנונימים ספציפיים ל-Pulsar וחריגות
3. ליצור עטיפות סכימות או הגדרות סכימות חלופיות
4. ליישם את הממשק הן עבור Pulsar והן עבור מערכות חלופיות (Kafka, RabbitMQ, Redis Streams, וכו')
5. לעדכן את `pubsub.py` כך שניתן יהיה להגדיר אותו ולתמוך במספר מערכות אחוריות
6. לספק נתיב מעבר לפריסות קיימות
## טיוטת גישה 1: תבנית מתאם עם שכבת תרגום סכימות
### הערה
### אסטרטגיה: הפרעה מינימלית באמצעות מתאמים
**1. שמירה על סכימות Pulsar כייצוג הפנימי**
אין לשכתב את כל הגדרות הסכימות.
הסכימות נשארות `pulsar.schema.Record` באופן פנימי.
השתמשו במתאמים כדי לתרגם בגבול בין הקוד שלנו לבין ה-backend של ה-pub/sub.
**2. יצירת שכבת הפשטה עבור pub/sub:**
```
┌─────────────────────────────────────┐
│ Existing Code (unchanged) │
│ - Uses Pulsar schemas internally │
│ - Consumer/Producer/Publisher │
└──────────────┬──────────────────────┘
┌──────────────┴──────────────────────┐
│ PubSubFactory (configurable) │
│ - Creates backend-specific client │
└──────────────┬──────────────────────┘
┌──────┴──────┐
│ │
┌───────▼─────┐ ┌────▼─────────┐
│ PulsarAdapter│ │ KafkaAdapter │ etc...
│ (passthrough)│ │ (translates) │
└──────────────┘ └──────────────┘
```
**3. הגדרת ממשקים מופשטים:**
`PubSubClient` - חיבור לקוח
`PubSubProducer` - שליחת הודעות
`PubSubConsumer` - קבלת הודעות
`SchemaAdapter` - המרת סכימות Pulsar ל-JSON או פורמטים ספציפיים ל-backend
**4. פרטי יישום:**
עבור **מתאם Pulsar**: כמעט העברה ישירה, המרה מינימלית
עבור **backends אחרים** (Kafka, RabbitMQ, וכו'):
סריאליזציה של אובייקטי Pulsar Record ל-JSON/bytes
מיפוי מושגים כמו:
`InitialPosition.Earliest/Latest` → auto.offset.reset של Kafka
`acknowledge()` → commit של Kafka
`negative_acknowledge()` → תבנית Re-queue או DLQ
URIs של נושאים → שמות נושאים ספציפיים ל-backend
### ניתוח
**יתרונות:**
✅ שינויים מינימליים בקוד של שירותים קיימים
✅ הסכימות נשארות כפי שהן (ללא כתיבה מחדש מסיבית)
✅ מסלול מעבר הדרגתי
✅ משתמשי Pulsar לא רואים הבדל
✅ backends חדשים מתווספים באמצעות מתאמים
**חסרונות:**
⚠️ עדיין כולל תלות ב-Pulsar (לצרכי הגדרות סכימה)
⚠️ חוסר התאמה מסוים בהמרת מושגים
### שיקול חלופי
ליצור **מערכת סכימות TrustGraph** שאינה תלויה ב-pub/sub ספציפי (תוך שימוש ב-dataclasses או Pydantic), ולאחר מכן ליצור סכימות Pulsar/Kafka/וכו מתוך זה. זה דורש כתיבה מחדש של כל קובץ סכימה ועלול לגרום לשינויים משמעותיים.
### המלצה עבור טיוטה 1
להתחיל עם **גישת המתאם** מכיוון ש:
1. זה פרגמטי - עובד עם קוד קיים
2. מוכיח את הקונספט עם סיכון מינימלי
3. ניתן להתפתח למערכת סכימה מקומית יותר בעתיד אם יש צורך
4. מונחה תצורה: משתנה סביבה אחד משנה את ה-backends
## גישה טיוטה 2: מערכת סכימות עצמאית מ-backend באמצעות Dataclasses
### מושג מרכזי
להשתמש ב-**dataclasses** של Python כפורמט הגדרת סכימה ניטרלי. כל backend של pub/sub מספק את הסריאליזציה/דה-סריאליזציה שלו עבור dataclasses, מה שמבטל את הצורך לשמור על סכימות Pulsar בקוד הבסיס.
### פולימורפיזם של סכימה ברמת המפעל
במקום להמיר סכימות Pulsar, **כל backend מספק את הטיפול שלו בסכימות** שעובד עם dataclasses סטנדרטיים של Python.
### זרימת פרסום
```python
# 1. Get the configured backend from factory
pubsub = get_pubsub() # Returns PulsarBackend, MQTTBackend, etc.
# 2. Get schema class from the backend
# (Can be imported directly - backend-agnostic)
from trustgraph.schema.services.llm import TextCompletionRequest
# 3. Create a producer/publisher for a specific topic
producer = pubsub.create_producer(
topic="text-completion-requests",
schema=TextCompletionRequest # Tells backend what schema to use
)
# 4. Create message instances (same API regardless of backend)
request = TextCompletionRequest(
system="You are helpful",
prompt="Hello world",
streaming=False
)
# 5. Send the message
producer.send(request) # Backend serializes appropriately
```
### זרימת לקוח
```python
# 1. Get the configured backend
pubsub = get_pubsub()
# 2. Create a consumer
consumer = pubsub.subscribe(
topic="text-completion-requests",
schema=TextCompletionRequest # Tells backend how to deserialize
)
# 3. Receive and deserialize
msg = consumer.receive()
request = msg.value() # Returns TextCompletionRequest dataclass instance
# 4. Use the data (type-safe access)
print(request.system) # "You are helpful"
print(request.prompt) # "Hello world"
print(request.streaming) # False
```
### מה קורה מאחורי הקלעים
**עבור ה-backend של Pulsar:**
`create_producer()` → יוצר יצרן Pulsar עם סכימה JSON או רשומה שנוצרה באופן דינמי
`send(request)` → ממיר את מחלקת הנתונים לפורמט JSON/Pulsar, שולח ל-Pulsar
`receive()` → מקבל הודעת Pulsar, ממיר חזרה למחלקת נתונים
**עבור ה-backend של MQTT:**
`create_producer()` → מתחבר ל-MQTT broker, אין צורך ברישום סכימה
`send(request)` → ממיר מחלקת נתונים ל-JSON, מפרסם לנושא MQTT
`receive()` → נרשם לנושא MQTT, ממיר JSON למחלקת נתונים
**עבור ה-backend של Kafka:**
`create_producer()` → יוצר יצרן Kafka, רושם סכימת Avro במידת הצורך
`send(request)` → ממיר מחלקת נתונים לפורמט Avro, שולח ל-Kafka
`receive()` → מקבל הודעת Kafka, ממיר Avro חזרה למחלקת נתונים
### נקודות עיצוב מרכזיות
1. **יצירת אובייקט סכימה**: מופע מחלקת הנתונים (`TextCompletionRequest(...)`) זהה ללא קשר ל-backend
2. **ה-backend מטפל בקידוד**: כל backend יודע כיצד לתרגם את מחלקת הנתונים לפורמט הנתונים
3. **הגדרת סכימה ביצירה**: בעת יצירת יצרן/צרכן, מציינים את סוג הסכימה
4. **שמירה על בטיחות טיפוסים**: מקבלים בחזרה אובייקט `TextCompletionRequest` תקין, ולא מילון
5. **ללא חשיפה ל-backend**: קוד האפליקציה לעולם לא מייבא ספריות ספציפיות ל-backend
### דוגמה לטרנספורמציה
**נוכחי (ספציפי ל-Pulsar):**
```python
# schema/services/llm.py
from pulsar.schema import Record, String, Boolean, Integer
class TextCompletionRequest(Record):
system = String()
prompt = String()
streaming = Boolean()
```
**חדש (בלתי תלוי בטכנולוגיית השרת):**
```python
# schema/services/llm.py
from dataclasses import dataclass
@dataclass
class TextCompletionRequest:
system: str
prompt: str
streaming: bool = False
```
### אינטגרציה עם השרת האחורי
כל שרת אחורי מטפל בסריאליזציה/דה-סריאליזציה של מחלקות נתונים:
**שרת אחורי של Pulsar:**
יצירת מחלקות `pulsar.schema.Record` באופן דינמי ממחלקות נתונים
או סריאליזציה של מחלקות נתונים ל-JSON ושימוש בסכימה של JSON של Pulsar
שומר על תאימות לפריסות Pulsar קיימות
**שרת אחורי של MQTT/Redis:**
סריאליזציה ישירה של מופעים של מחלקות נתונים ל-JSON
שימוש ב-`dataclasses.asdict()` / `from_dict()`
קל משקל, לא נדרש רישום סכימות
**שרת אחורי של Kafka:**
יצירת סכימות Avro מהגדרות של מחלקות נתונים
שימוש ברישום סכימות של Confluent
סריאליזציה בטוחה עם תמיכה באבולוציה של סכימות
### ארכיטקטורה
```
┌─────────────────────────────────────┐
│ Application Code │
│ - Uses dataclass schemas │
│ - Backend-agnostic │
└──────────────┬──────────────────────┘
┌──────────────┴──────────────────────┐
│ PubSubFactory (configurable) │
│ - get_pubsub() returns backend │
└──────────────┬──────────────────────┘
┌──────┴──────┐
│ │
┌───────▼─────────┐ ┌────▼──────────────┐
│ PulsarBackend │ │ MQTTBackend │
│ - JSON schema │ │ - JSON serialize │
│ - or dynamic │ │ - Simple queues │
│ Record gen │ │ │
└─────────────────┘ └───────────────────┘
```
### פרטי יישום
**1. הגדרות סכימה:** מחלקות נתונים פשוטות עם רמזי סוג
`str`, `int`, `bool`, `float` עבור ערכים בסיסיים
`list[T]` עבור מערכים
`dict[str, T]` עבור מפות
מחלקות נתונים מקוננות עבור סוגים מורכבים
**2. כל ממשק מספק:**
ממיר: `dataclass → bytes/wire format`
ממיר הפוך: `bytes/wire format → dataclass`
רישום סכימה (אם נדרש, כמו Pulsar/Kafka)
**3. הפשטה של צרכן/משדר:**
כבר קיים (consumer.py, producer.py)
עדכון לשימוש בשימוש בשידור של הממשק
הסרת יבוא ישיר של Pulsar
**4. התאמות סוג:**
Pulsar `String()` → Python `str`
Pulsar `Integer()` → Python `int`
Pulsar `Boolean()` → Python `bool`
Pulsar `Array(T)` → Python `list[T]`
Pulsar `Map(K, V)` → Python `dict[K, V]`
Pulsar `Double()` → Python `float`
Pulsar `Bytes()` → Python `bytes`
### נתיב מעבר
1. **צור גרסאות של מחלקות נתונים** עבור כל הסכימות ב-`trustgraph/schema/`
2. **עדכן מחלקות ממשק** (צרכן, משדר, מפרסם, מנוי) לשימוש בשידור שמסופק על ידי הממשק
3. **יישם PulsarBackend** עם סכימה ב-JSON או יצירת רשומות דינמית
4. **בדוק עם Pulsar** כדי להבטיח תאימות לאחור עם פריסות קיימות
5. **הוסף ממשקים חדשים** (MQTT, Kafka, Redis, וכו') לפי הצורך
6. **הסר יבוא של Pulsar** מקבצי סכימה
### יתרונות
**ללא תלות ב-pub/sub** בהגדרות סכימה
**Python סטנדרטי** - קל להבנה, בדיקת סוגים, תיעוד
**כלים מודרניים** - עובד עם mypy, השלמה אוטומטית של IDE, כלי ניתוח
**מותאם לממשק** - כל ממשק משתמש בשידור מקומי
**ללא תקורה של תרגום** - שידור ישיר, ללא מתאמים
**בטיחות סוג** - אובייקטים אמיתיים עם סוגים מתאימים
**אימות קל** - ניתן להשתמש ב-Pydantic אם נדרש
### אתגרים ופתרונות
**אתגר:** ל-Pulsar יש `Record` עם אימות שדה בזמן ריצה
**פתרון:** השתמש במחלקות נתונים של Pydantic לאימות אם נדרש, או בתכונות של מחלקת נתונים של Python 3.10+ עם `__post_init__`
**אתגר:** תכונות ספציפיות של Pulsar (כמו סוג `Bytes`)
**פתרון:** התאם לסוג `bytes` במחלקת נתונים, הממשק מטפל בקידוד המתאים
**אתגר:** שמות נושא (`persistent://tenant/namespace/topic`)
**פתרון:** הפשט שמות נושא בהגדרות סכימה, הממשק ממיר לפורמט המתאים
**אתגר:** אבולוציה וגרסאות של סכימה
**פתרון:** כל ממשק מטפל בכך בהתאם ליכולות שלו (גרסאות סכימה של Pulsar, רישום סכימה של Kafka, וכו')
**אתגר:** סוגים מורכבים מקוננים
**פתרון:** השתמש במחלקות נתונים מקוננות, הממשקים מבצעים שידור/פענוח רקורסיבי
### החלטות עיצוב
1. **מחלקות נתונים פשוטות או Pydantic?**
**החלטה: השתמש במחלקות נתונים של Python פשוטות**
פשוט יותר, ללא תלויות נוספות
אימות אינו נדרש בפועל
קל יותר להבנה ולתחזוקה
2. **אבולוציה של סכימה:**
**החלטה: לא נדרש מנגנון גרסאות**
הסכימות יציבות וקיימות לאורך זמן
עדכונים בדרך כלל מוסיפים שדות חדשים (תואמים לאחור)
ממשקים מטפלים באבולוציה של סכימה בהתאם ליכולות שלהם
3. **תאימות לאחור:**
**החלטה: שינוי גרסה עיקרית, תאימות לאחור אינה נדרשת**
יהיה שינוי משמעותי עם הוראות מעבר
ניתוק נקי מאפשר עיצוב טוב יותר
מדריך מעבר יסופק עבור פריסות קיימות
4. **סוגים מקוננים ומבנים מורכבים:**
**החלטה: השתמש במחלקות נתונים מקוננות באופן טבעי**
מחלקות נתונים של Python מטפלות בקינון בצורה מושלמת
`list[T]` עבור מערכים, `dict[K, V]` עבור מפות
ממשקים מבצעים שידור/פענוח רקורסיבי
דוגמה:
```python
@dataclass
class Value:
value: str
is_uri: bool
@dataclass
class Triple:
s: Value # Nested dataclass
p: Value
o: Value
@dataclass
class GraphQuery:
triples: list[Triple] # Array of nested dataclasses
metadata: dict[str, str]
```
5. **ערכים ברירת מחדל ושדות אופציונליים:**
✅ **החלטה: שילוב של שדות חובה, ערכי ברירת מחדל ושדות אופציונליים**
שדות חובה: ללא ערך ברירת מחדל
שדות עם ערכי ברירת מחדל: תמיד נוכחים, בעלי ערך ברירת מחדל הגיוני
שדות אופציונליים לחלוטין: `T | None = None`, מושמטים מסריאליזציה כאשר `None`
דוגמה:
```python
@dataclass
class TextCompletionRequest:
system: str # Required, no default
prompt: str # Required, no default
streaming: bool = False # Optional with default value
metadata: dict | None = None # Truly optional, can be absent
```
**סמנטיקה חשובה של סריאליזציה:**
כאשר `metadata = None`:
```json
{
"system": "...",
"prompt": "...",
"streaming": false
// metadata field NOT PRESENT
}
```
כאשר `metadata = {}` (ריק במפורש):
```json
{
"system": "...",
"prompt": "...",
"streaming": false,
"metadata": {} // Field PRESENT but empty
}
```
**ההבחנה העיקרית:**
`None` → שדה שאינו קיים ב-JSON (לא מיוצא)
ערך ריק (`{}`, `[]`, `""`) → שדה קיים עם ערך ריק
זה חשוב מבחינה סמנטית: "לא סופק" לעומת "ריק באופן מפורש"
מערכות הקידוד חייבות לדלג על שדות `None`, ולא לקודד אותם כ-`null`
## טיוטה 3 של הגישה: פרטי יישום
### פורמט שם תור גנרי
החליפו שמות תורים ספציפיים לכל מערכת קידוד בפורמט גנרי שמערכות הקידוד יכולות למפות בהתאם.
**פורמט:** `{qos}/{tenant}/{namespace}/{queue-name}`
כאשר:
`qos`: רמת שירות (Quality of Service)
`q0` = מאמץ מינימלי (שליחה ללא אישור)
`q1` = לפחות פעם אחת (דורש אישור)
`q2` = בדיוק פעם אחת (אישור בשני שלבים)
`tenant`: קיבוץ לוגי עבור ריבוי דיירים
`namespace`: תת-קיבוץ בתוך דייר
`queue-name`: שם התור/נושא בפועל
**דוגמאות:**
```
q1/tg/flow/text-completion-requests
q2/tg/config/config-push
q0/tg/metrics/stats
```
### מיפוי נושאים בצד השרת
כל צד שרת ממפה את הפורמט הכללי לפורמט הייחודי שלו:
**צד שרת Pulsar:**
```python
def map_topic(self, generic_topic: str) -> str:
# Parse: q1/tg/flow/text-completion-requests
qos, tenant, namespace, queue = generic_topic.split('/', 3)
# Map QoS to persistence
persistence = 'persistent' if qos in ['q1', 'q2'] else 'non-persistent'
# Return Pulsar URI: persistent://tg/flow/text-completion-requests
return f"{persistence}://{tenant}/{namespace}/{queue}"
```
**שרת MQTT:**
```python
def map_topic(self, generic_topic: str) -> tuple[str, int]:
# Parse: q1/tg/flow/text-completion-requests
qos, tenant, namespace, queue = generic_topic.split('/', 3)
# Map QoS level
qos_level = {'q0': 0, 'q1': 1, 'q2': 2}[qos]
# Build MQTT topic including tenant/namespace for proper namespacing
mqtt_topic = f"{tenant}/{namespace}/{queue}"
return mqtt_topic, qos_level
```
### פונקציית עזר מעודכנת לנושא
```python
# schema/core/topic.py
def topic(queue_name, qos='q1', tenant='tg', namespace='flow'):
"""
Create a generic topic identifier that can be mapped by backends.
Args:
queue_name: The queue/topic name
qos: Quality of service
- 'q0' = best-effort (no ack)
- 'q1' = at-least-once (ack required)
- 'q2' = exactly-once (two-phase ack)
tenant: Tenant identifier for multi-tenancy
namespace: Namespace within tenant
Returns:
Generic topic string: qos/tenant/namespace/queue_name
Examples:
topic('my-queue') # q1/tg/flow/my-queue
topic('config', qos='q2', namespace='config') # q2/tg/config/config
"""
return f"{qos}/{tenant}/{namespace}/{queue_name}"
```
### הגדרות ואתחול
**ארגומנטים של שורת הפקודה + משתני סביבה:**
```python
# In base/async_processor.py - add_args() method
@staticmethod
def add_args(parser):
# Pub/sub backend selection
parser.add_argument(
'--pubsub-backend',
default=os.getenv('PUBSUB_BACKEND', 'pulsar'),
choices=['pulsar', 'mqtt'],
help='Pub/sub backend (default: pulsar, env: PUBSUB_BACKEND)'
)
# Pulsar-specific configuration
parser.add_argument(
'--pulsar-host',
default=os.getenv('PULSAR_HOST', 'pulsar://localhost:6650'),
help='Pulsar host (default: pulsar://localhost:6650, env: PULSAR_HOST)'
)
parser.add_argument(
'--pulsar-api-key',
default=os.getenv('PULSAR_API_KEY', None),
help='Pulsar API key (env: PULSAR_API_KEY)'
)
parser.add_argument(
'--pulsar-listener',
default=os.getenv('PULSAR_LISTENER', None),
help='Pulsar listener name (env: PULSAR_LISTENER)'
)
# MQTT-specific configuration
parser.add_argument(
'--mqtt-host',
default=os.getenv('MQTT_HOST', 'localhost'),
help='MQTT broker host (default: localhost, env: MQTT_HOST)'
)
parser.add_argument(
'--mqtt-port',
type=int,
default=int(os.getenv('MQTT_PORT', '1883')),
help='MQTT broker port (default: 1883, env: MQTT_PORT)'
)
parser.add_argument(
'--mqtt-username',
default=os.getenv('MQTT_USERNAME', None),
help='MQTT username (env: MQTT_USERNAME)'
)
parser.add_argument(
'--mqtt-password',
default=os.getenv('MQTT_PASSWORD', None),
help='MQTT password (env: MQTT_PASSWORD)'
)
```
**פונקציית יצירה:**
```python
# In base/pubsub.py or base/pubsub_factory.py
def get_pubsub(**config) -> PubSubBackend:
"""
Create and return a pub/sub backend based on configuration.
Args:
config: Configuration dict from command-line args
Must include 'pubsub_backend' key
Returns:
Backend instance (PulsarBackend, MQTTBackend, etc.)
"""
backend_type = config.get('pubsub_backend', 'pulsar')
if backend_type == 'pulsar':
return PulsarBackend(
host=config.get('pulsar_host'),
api_key=config.get('pulsar_api_key'),
listener=config.get('pulsar_listener'),
)
elif backend_type == 'mqtt':
return MQTTBackend(
host=config.get('mqtt_host'),
port=config.get('mqtt_port'),
username=config.get('mqtt_username'),
password=config.get('mqtt_password'),
)
else:
raise ValueError(f"Unknown pub/sub backend: {backend_type}")
```
**שימוש ב-AsyncProcessor:**
```python
# In async_processor.py
class AsyncProcessor:
def __init__(self, **params):
self.id = params.get("id")
# Create backend from config (replaces PulsarClient)
self.pubsub = get_pubsub(**params)
# Rest of initialization...
```
### ממשק צד שרת
```python
class PubSubBackend(Protocol):
"""Protocol defining the interface all pub/sub backends must implement."""
def create_producer(self, topic: str, schema: type, **options) -> BackendProducer:
"""
Create a producer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
schema: Dataclass type for messages
options: Backend-specific options (e.g., chunking_enabled)
Returns:
Backend-specific producer instance
"""
...
def create_consumer(
self,
topic: str,
subscription: str,
schema: type,
initial_position: str = 'latest',
consumer_type: str = 'shared',
**options
) -> BackendConsumer:
"""
Create a consumer for a topic.
Args:
topic: Generic topic format (qos/tenant/namespace/queue)
subscription: Subscription/consumer group name
schema: Dataclass type for messages
initial_position: 'earliest' or 'latest' (MQTT may ignore)
consumer_type: 'shared', 'exclusive', 'failover' (MQTT may ignore)
options: Backend-specific options
Returns:
Backend-specific consumer instance
"""
...
def close(self) -> None:
"""Close the backend connection."""
...
```
```python
class BackendProducer(Protocol):
"""Protocol for backend-specific producer."""
def send(self, message: Any, properties: dict = {}) -> None:
"""Send a message (dataclass instance) with optional properties."""
...
def flush(self) -> None:
"""Flush any buffered messages."""
...
def close(self) -> None:
"""Close the producer."""
...
```
```python
class BackendConsumer(Protocol):
"""Protocol for backend-specific consumer."""
def receive(self, timeout_millis: int = 2000) -> Message:
"""
Receive a message from the topic.
Raises:
TimeoutError: If no message received within timeout
"""
...
def acknowledge(self, message: Message) -> None:
"""Acknowledge successful processing of a message."""
...
def negative_acknowledge(self, message: Message) -> None:
"""Negative acknowledge - triggers redelivery."""
...
def unsubscribe(self) -> None:
"""Unsubscribe from the topic."""
...
def close(self) -> None:
"""Close the consumer."""
...
```
```python
class Message(Protocol):
"""Protocol for a received message."""
def value(self) -> Any:
"""Get the deserialized message (dataclass instance)."""
...
def properties(self) -> dict:
"""Get message properties/metadata."""
...
```
### שינוי מבנה מחלקות קיימות
המחלקות הקיימות `Consumer`, `Producer`, `Publisher`, `Subscriber` נשארות ברובן ללא שינוי:
**אחריות נוכחית (לשמור):**
מודל תהליכים אסינכרוניים וקבוצות משימות
לוגיקת חיבור מחדש וטיפול בכישלונות
איסוף מדדים
הגבלת קצב
ניהול תחרות
**שינויים נדרשים:**
הסרת יבוא ישיר של Pulsar (`pulsar.schema`, `pulsar.InitialPosition`, וכו')
קבלת `BackendProducer`/`BackendConsumer` במקום לקוח Pulsar
העברת פעולות פרסום/מנוי בפועל לאינסטנסים אחוריים
התאמת מושגים כלליים לפעולות אחוריות
**דוגמה לשינוי מבנה:**
```python
# OLD - consumer.py
class Consumer:
def __init__(self, client, topic, subscriber, schema, ...):
self.client = client # Direct Pulsar client
# ...
async def consumer_run(self):
# Uses pulsar.InitialPosition, pulsar.ConsumerType
self.consumer = self.client.subscribe(
topic=self.topic,
schema=JsonSchema(self.schema),
initial_position=pulsar.InitialPosition.Earliest,
consumer_type=pulsar.ConsumerType.Shared,
)
# NEW - consumer.py
class Consumer:
def __init__(self, backend_consumer, schema, ...):
self.backend_consumer = backend_consumer # Backend-specific consumer
self.schema = schema
# ...
async def consumer_run(self):
# Backend consumer already created with right settings
# Just use it directly
while self.running:
msg = await asyncio.to_thread(
self.backend_consumer.receive,
timeout_millis=2000
)
await self.handle_message(msg)
```
### התנהגויות ספציפיות לצד האחורי (Backend)
**צד אחורי Pulsar:**
ממפה `q0` → `non-persistent://`, `q1`/`q2` → `persistent://`
תומך בכל סוגי הצרכנים (משותף, בלעדי, גיבוי)
תומך בעמדה התחלתית (הכי מוקדם/הכי מאוחר)
אישור הודעות מקורי
תמיכה ברישום סכימות
**צד אחורי MQTT:**
ממפה `q0`/`q1`/`q2` → רמות QoS של MQTT 0/1/2
כולל שוכר/מרחב שם בנתיב הנושא לצורך הפרדה
מייצר באופן אוטומטי מזהי לקוח משמות מנויים
מתעלם מעמדה התחלתית (אין היסטוריית הודעות ב-MQTT בסיסי)
מתעלם מסוג צרכן (MQTT משתמש במזהי לקוח, לא בקבוצות צרכנים)
מודל פרסום/מנוי פשוט
### סיכום החלטות עיצוב
1. ✅ **שמות תורים גנריים**: פורמט `qos/tenant/namespace/queue-name`
2. ✅ **רמת QoS במזהה תור**: נקבעת על ידי הגדרת התור, ולא על ידי תצורה
3. ✅ **חיבור מחדש**: מטופל על ידי מחלקות צרכן/מפיק, ולא על ידי הצד האחורי
4. ✅ **נושאים של MQTT**: כוללים שוכר/מרחב שם לצורך הפרדה תקינה
5. ✅ **היסטוריית הודעות**: MQTT מתעלם מהפרמטר `initial_position` (שיפור עתידי)
6. ✅ **מזהי לקוח**: צד אחורי MQTT מייצר באופן אוטומטי משם מנוי
### שיפורים עתידיים
**היסטוריית הודעות של MQTT:**
ניתן להוסיף שכבת שמירה אופציונלית (לדוגמה, הודעות שמורות, אחסון חיצוני)
יאפשר תמיכה ב-`initial_position='earliest'`
לא נדרש ליישום ראשוני