trustgraph/docs/tech-specs/pubsub.he.md

966 lines
38 KiB
Markdown
Raw Normal View History

---
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'`
לא נדרש ליישום ראשוני