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.
38 KiB
| layout | title | parent |
|---|---|---|
| default | תשתית Pub/Sub | 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 לפי קטגוריה
-
יצירת מופע לקוח: ישיר:
gateway/service.pyמופשט:async_processor.py→pubsub.py(PulsarClient) -
העברת הודעות: צרכן:
consumer.py,consumer_spec.pyמפיק:producer.py,producer_spec.pyמפרסם:publisher.pyמנוי:subscriber.py,subscriber_spec.py -
מערכת סכימות: סוגים בסיסיים:
schema/core/primitives.pyכל סכימות השירות:schema/services/*.pyשמות נושאים:schema/core/topic.py -
מושגים ספציפיים ל-Pulsar הנדרשים: העברת הודעות מבוססת נושאים מערכת סכימות (Record, סוגי שדות) מנויים משותפים אישור הודעות (חיובי/שלילי) מיקום צרכן (מוקדם/מאוחר) מאפייני הודעות מיקומים התחלתיים וסוגי צרכנים תמיכה בפיצול נושאים קבועים לעומת לא קבועים
אתגרי שינוי מבנה
החדשות הטובות: שכבת ההפשטה (צרכן, מפיק, מפרסם, מנוי) מספקת אריזה נקייה של רוב האינטראקציות של Pulsar.
האתגרים:
- הנחת סכימות נרחבת: כל הגדרת הודעה משתמשת ב-
pulsar.schema.Recordובסוגי Pulsar - אנונימים ספציפיים ל-Pulsar:
InitialPosition,ConsumerType - חריגות של Pulsar:
_pulsar.Timeout,_pulsar.Interrupted,_pulsar.InvalidConfiguration,_pulsar.AlreadyClosed - חתימות שיטות:
acknowledge(),negative_acknowledge(),subscribe(),create_producer(), וכו'. - פורמט URI של נושא: מבנה
kind://tenant/namespace/topicשל Pulsar
שלבים הבאים
כדי להפוך את התשתית של pub/sub לכזו שניתן להגדיר, עלינו:
- ליצור ממשק הפשטה עבור מערכת הלקוח/סכימות
- להפשיט אנונימים ספציפיים ל-Pulsar וחריגות
- ליצור עטיפות סכימות או הגדרות סכימות חלופיות
- ליישם את הממשק הן עבור Pulsar והן עבור מערכות חלופיות (Kafka, RabbitMQ, Redis Streams, וכו')
- לעדכן את
pubsub.pyכך שניתן יהיה להגדיר אותו ולתמוך במספר מערכות אחוריות - לספק נתיב מעבר לפריסות קיימות
טיוטת גישה 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
להתחיל עם גישת המתאם מכיוון ש:
- זה פרגמטי - עובד עם קוד קיים
- מוכיח את הקונספט עם סיכון מינימלי
- ניתן להתפתח למערכת סכימה מקומית יותר בעתיד אם יש צורך
- מונחה תצורה: משתנה סביבה אחד משנה את ה-backends
גישה טיוטה 2: מערכת סכימות עצמאית מ-backend באמצעות Dataclasses
מושג מרכזי
להשתמש ב-dataclasses של Python כפורמט הגדרת סכימה ניטרלי. כל backend של pub/sub מספק את הסריאליזציה/דה-סריאליזציה שלו עבור dataclasses, מה שמבטל את הצורך לשמור על סכימות Pulsar בקוד הבסיס.
פולימורפיזם של סכימה ברמת המפעל
במקום להמיר סכימות Pulsar, כל backend מספק את הטיפול שלו בסכימות שעובד עם dataclasses סטנדרטיים של 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
זרימת לקוח
# 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 חזרה למחלקת נתונים
נקודות עיצוב מרכזיות
- יצירת אובייקט סכימה: מופע מחלקת הנתונים (
TextCompletionRequest(...)) זהה ללא קשר ל-backend - ה-backend מטפל בקידוד: כל backend יודע כיצד לתרגם את מחלקת הנתונים לפורמט הנתונים
- הגדרת סכימה ביצירה: בעת יצירת יצרן/צרכן, מציינים את סוג הסכימה
- שמירה על בטיחות טיפוסים: מקבלים בחזרה אובייקט
TextCompletionRequestתקין, ולא מילון - ללא חשיפה ל-backend: קוד האפליקציה לעולם לא מייבא ספריות ספציפיות ל-backend
דוגמה לטרנספורמציה
נוכחי (ספציפי ל-Pulsar):
# schema/services/llm.py
from pulsar.schema import Record, String, Boolean, Integer
class TextCompletionRequest(Record):
system = String()
prompt = String()
streaming = Boolean()
חדש (בלתי תלוי בטכנולוגיית השרת):
# 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
נתיב מעבר
- צור גרסאות של מחלקות נתונים עבור כל הסכימות ב-
trustgraph/schema/ - עדכן מחלקות ממשק (צרכן, משדר, מפרסם, מנוי) לשימוש בשידור שמסופק על ידי הממשק
- יישם PulsarBackend עם סכימה ב-JSON או יצירת רשומות דינמית
- בדוק עם Pulsar כדי להבטיח תאימות לאחור עם פריסות קיימות
- הוסף ממשקים חדשים (MQTT, Kafka, Redis, וכו') לפי הצורך
- הסר יבוא של Pulsar מקבצי סכימה
יתרונות
✅ ללא תלות ב-pub/sub בהגדרות סכימה ✅ Python סטנדרטי - קל להבנה, בדיקת סוגים, תיעוד ✅ כלים מודרניים - עובד עם mypy, השלמה אוטומטית של IDE, כלי ניתוח ✅ מותאם לממשק - כל ממשק משתמש בשידור מקומי ✅ ללא תקורה של תרגום - שידור ישיר, ללא מתאמים ✅ בטיחות סוג - אובייקטים אמיתיים עם סוגים מתאימים ✅ אימות קל - ניתן להשתמש ב-Pydantic אם נדרש
אתגרים ופתרונות
אתגר: ל-Pulsar יש Record עם אימות שדה בזמן ריצה
פתרון: השתמש במחלקות נתונים של Pydantic לאימות אם נדרש, או בתכונות של מחלקת נתונים של Python 3.10+ עם __post_init__
אתגר: תכונות ספציפיות של Pulsar (כמו סוג Bytes)
פתרון: התאם לסוג bytes במחלקת נתונים, הממשק מטפל בקידוד המתאים
אתגר: שמות נושא (persistent://tenant/namespace/topic)
פתרון: הפשט שמות נושא בהגדרות סכימה, הממשק ממיר לפורמט המתאים
אתגר: אבולוציה וגרסאות של סכימה פתרון: כל ממשק מטפל בכך בהתאם ליכולות שלו (גרסאות סכימה של Pulsar, רישום סכימה של Kafka, וכו')
אתגר: סוגים מורכבים מקוננים פתרון: השתמש במחלקות נתונים מקוננות, הממשקים מבצעים שידור/פענוח רקורסיבי
החלטות עיצוב
-
מחלקות נתונים פשוטות או Pydantic? ✅ החלטה: השתמש במחלקות נתונים של Python פשוטות פשוט יותר, ללא תלויות נוספות אימות אינו נדרש בפועל קל יותר להבנה ולתחזוקה
-
אבולוציה של סכימה: ✅ החלטה: לא נדרש מנגנון גרסאות הסכימות יציבות וקיימות לאורך זמן עדכונים בדרך כלל מוסיפים שדות חדשים (תואמים לאחור) ממשקים מטפלים באבולוציה של סכימה בהתאם ליכולות שלהם
-
תאימות לאחור: ✅ החלטה: שינוי גרסה עיקרית, תאימות לאחור אינה נדרשת יהיה שינוי משמעותי עם הוראות מעבר ניתוק נקי מאפשר עיצוב טוב יותר מדריך מעבר יסופק עבור פריסות קיימות
-
סוגים מקוננים ומבנים מורכבים: ✅ החלטה: השתמש במחלקות נתונים מקוננות באופן טבעי מחלקות נתונים של Python מטפלות בקינון בצורה מושלמת
list[T]עבור מערכים,dict[K, V]עבור מפות ממשקים מבצעים שידור/פענוח רקורסיבי דוגמה:@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] -
ערכים ברירת מחדל ושדות אופציונליים: ✅ החלטה: שילוב של שדות חובה, ערכי ברירת מחדל ושדות אופציונליים שדות חובה: ללא ערך ברירת מחדל שדות עם ערכי ברירת מחדל: תמיד נוכחים, בעלי ערך ברירת מחדל הגיוני שדות אופציונליים לחלוטין:
T | None = None, מושמטים מסריאליזציה כאשרNoneדוגמה:@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:{ "system": "...", "prompt": "...", "streaming": false // metadata field NOT PRESENT }כאשר
metadata = {}(ריק במפורש):{ "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:
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:
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
פונקציית עזר מעודכנת לנושא
# 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}"
הגדרות ואתחול
ארגומנטים של שורת הפקודה + משתני סביבה:
# 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)'
)
פונקציית יצירה:
# 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:
# 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...
ממשק צד שרת
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."""
...
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."""
...
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."""
...
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
העברת פעולות פרסום/מנוי בפועל לאינסטנסים אחוריים
התאמת מושגים כלליים לפעולות אחוריות
דוגמה לשינוי מבנה:
# 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 משתמש במזהי לקוח, לא בקבוצות צרכנים)
מודל פרסום/מנוי פשוט
סיכום החלטות עיצוב
- ✅ שמות תורים גנריים: פורמט
qos/tenant/namespace/queue-name - ✅ רמת QoS במזהה תור: נקבעת על ידי הגדרת התור, ולא על ידי תצורה
- ✅ חיבור מחדש: מטופל על ידי מחלקות צרכן/מפיק, ולא על ידי הצד האחורי
- ✅ נושאים של MQTT: כוללים שוכר/מרחב שם לצורך הפרדה תקינה
- ✅ היסטוריית הודעות: MQTT מתעלם מהפרמטר
initial_position(שיפור עתידי) - ✅ מזהי לקוח: צד אחורי MQTT מייצר באופן אוטומטי משם מנוי
שיפורים עתידיים
היסטוריית הודעות של MQTT:
ניתן להוסיף שכבת שמירה אופציונלית (לדוגמה, הודעות שמורות, אחסון חיצוני)
יאפשר תמיכה ב-initial_position='earliest'
לא נדרש ליישום ראשוני