mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-25 16:36:21 +02:00
966 lines
38 KiB
Markdown
966 lines
38 KiB
Markdown
|
|
---
|
|||
|
|
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'`
|
|||
|
|
לא נדרש ליישום ראשוני
|