praca_magisterska/pytania/generate_pubsub_diagrams.py

452 lines
18 KiB
Python
Raw Permalink Normal View History

2026-02-17 21:08:36 +01:00
#!/usr/bin/env python3
"""
Generate Pub/Sub diagrams for PYTANIE 19:
Subscription types (4 separate images):
1. Topic-based
2. Content-based
3. Type-based
4. Hierarchical (wildcards)
Delivery guarantees (3 separate images):
5. At-most-once
6. At-least-once
7. Exactly-once
All: A4-width, B&W, 300 DPI, laser-printer-friendly.
One diagram per image no cramming.
"""
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch
import os
DPI = 300
BG = 'white'
LN = 'black'
FS = 9
FS_TITLE = 13
FIG_W = 8.27 # A4 width in inches
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'img')
os.makedirs(OUTPUT_DIR, exist_ok=True)
GRAY1 = '#E8E8E8'
GRAY2 = '#D0D0D0'
GRAY3 = '#B8B8B8'
GRAY4 = '#F5F5F5'
GRAY5 = '#C0C0C0'
def draw_box(ax, x, y, w, h, text, fill='white', lw=1.2, fontsize=FS,
fontweight='normal', ha='center', va='center', rounded=True):
if rounded:
rect = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.05",
lw=lw, edgecolor=LN, facecolor=fill)
else:
rect = mpatches.Rectangle((x, y), w, h, lw=lw, edgecolor=LN, facecolor=fill)
ax.add_patch(rect)
ax.text(x + w/2, y + h/2, text, ha=ha, va=va, fontsize=fontsize,
fontweight=fontweight, wrap=True)
def draw_arrow(ax, x1, y1, x2, y2, lw=1.2, style='->', color=LN, label='',
label_offset=0.15, label_fs=8):
ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
arrowprops=dict(arrowstyle=style, color=color, lw=lw))
if label:
mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
ax.text(mx, my, label, ha='center', va='bottom', fontsize=label_fs, color=color)
def draw_dashed_arrow(ax, x1, y1, x2, y2, lw=1.0, color=LN, label='',
label_offset=0.15, label_fs=8):
ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
arrowprops=dict(arrowstyle='->', color=color, lw=lw,
linestyle='dashed'))
if label:
mx, my = (x1 + x2) / 2, (y1 + y2) / 2 + label_offset
ax.text(mx, my, label, ha='center', va='bottom', fontsize=label_fs, color=color)
def draw_cross(ax, x, y, size=0.15, lw=2.5, color='black'):
ax.plot([x - size, x + size], [y - size, y + size], color=color, lw=lw)
ax.plot([x - size, x + size], [y + size, y - size], color=color, lw=lw)
def draw_check(ax, x, y, size=0.15, lw=2.5, color='black'):
ax.plot([x - size, x - size*0.2], [y, y - size*0.7], color=color, lw=lw)
ax.plot([x - size*0.2, x + size], [y - size*0.7, y + size*0.5], color=color, lw=lw)
def save(fig, name):
plt.tight_layout()
fig.savefig(os.path.join(OUTPUT_DIR, name), dpi=DPI,
bbox_inches='tight', facecolor=BG)
plt.close(fig)
print(f"{name}")
# ============================================================
# 1. Topic-based subscription
# ============================================================
def draw_sub_topic():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.0))
ax.set_xlim(0, 12)
ax.set_ylim(0, 5.5)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('Subskrypcja topic-based — routing po nazwie tematu',
fontsize=FS_TITLE, fontweight='bold', pad=12)
# Publisher + messages
draw_box(ax, 0.2, 3.2, 2.4, 1.1, 'Publisher', fill=GRAY1, fontsize=10, fontweight='bold')
draw_box(ax, 0.3, 1.8, 2.2, 0.8, 'topic: "orders"', fill=GRAY4, fontsize=8)
draw_box(ax, 0.3, 0.7, 2.2, 0.8, 'topic: "payments"', fill=GRAY4, fontsize=8)
# Broker
draw_box(ax, 4.2, 1.5, 2.8, 2.2, 'BROKER\n\ntopic routing', fill=GRAY2, fontsize=10, fontweight='bold')
# Subscribers
draw_box(ax, 8.5, 3.8, 3.0, 1.0, 'Subscriber A\nsubskrybuje: "orders"', fill=GRAY1, fontsize=8.5)
draw_box(ax, 8.5, 2.2, 3.0, 1.0, 'Subscriber B\nsubskrybuje: "payments"', fill=GRAY1, fontsize=8.5)
draw_box(ax, 8.5, 0.6, 3.0, 1.0, 'Subscriber C\nsubskrybuje: "orders"', fill=GRAY1, fontsize=8.5)
# Arrows: publisher → broker
draw_arrow(ax, 2.6, 2.2, 4.2, 2.8, label_fs=8)
draw_arrow(ax, 2.6, 1.1, 4.2, 2.2, label_fs=8)
# Arrows: broker → subscribers
draw_arrow(ax, 7.0, 3.4, 8.5, 4.2, label='"orders"', label_fs=8)
draw_arrow(ax, 7.0, 2.6, 8.5, 2.7, label='"payments"', label_fs=8)
draw_arrow(ax, 7.0, 2.2, 8.5, 1.2, label='"orders"', label_fs=8)
# Explanation
ax.text(6.0, 0.1, 'Subscriber deklaruje nazwę tematu. Broker kieruje wiadomości\n'
'do WSZYSTKICH subscriberów danego tematu. Najprostszy model.',
ha='center', va='bottom', fontsize=8.5, style='italic',
bbox=dict(boxstyle='round,pad=0.3', facecolor=GRAY4, edgecolor=GRAY3))
save(fig, 'pubsub_sub_topic.png')
# ============================================================
# 2. Content-based subscription
# ============================================================
def draw_sub_content():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('Subskrypcja content-based — filtrowanie po treści wiadomości',
fontsize=FS_TITLE, fontweight='bold', pad=12)
# Publisher + message
draw_box(ax, 0.2, 3.5, 2.4, 1.1, 'Publisher', fill=GRAY1, fontsize=10, fontweight='bold')
draw_box(ax, 0.2, 1.8, 2.4, 1.2, 'price: 150\ntype: "book"\ncategory: "IT"', fill=GRAY4, fontsize=8.5)
# Broker
draw_box(ax, 4.0, 2.0, 3.0, 2.5, 'BROKER\n\newaluuje filtry\nkażdego subscribera', fill=GRAY2, fontsize=9, fontweight='bold')
# Subscribers with filters
draw_box(ax, 8.5, 4.2, 3.2, 1.0, 'Sub A\nfiltr: price > 100', fill=GRAY1, fontsize=9)
draw_box(ax, 8.5, 2.6, 3.2, 1.0, 'Sub B\nfiltr: type = "food"', fill=GRAY1, fontsize=9)
draw_box(ax, 8.5, 1.0, 3.2, 1.0, 'Sub C\nfiltr: price < 50', fill=GRAY1, fontsize=9)
# Arrows
draw_arrow(ax, 2.6, 2.4, 4.0, 3.0)
draw_arrow(ax, 7.0, 4.0, 8.5, 4.6, label='150 > 100 ✓ dostarczono', label_fs=8)
draw_dashed_arrow(ax, 7.0, 3.2, 8.5, 3.1, label='"book""food" ✗ odrzucono', label_fs=8)
draw_dashed_arrow(ax, 7.0, 2.5, 8.5, 1.6, label='150 < 50 ✗ odrzucono', label_fs=8)
ax.text(6.0, 0.2, 'Broker analizuje TREŚĆ wiadomości i ewaluuje predykaty.\n'
'Bardziej elastyczny niż topic-based, ale wolniejszy (koszt ewaluacji).',
ha='center', va='bottom', fontsize=8.5, style='italic',
bbox=dict(boxstyle='round,pad=0.3', facecolor=GRAY4, edgecolor=GRAY3))
save(fig, 'pubsub_sub_content.png')
# ============================================================
# 3. Type-based subscription
# ============================================================
def draw_sub_type():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.0))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6.5)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('Subskrypcja type-based — routing po typie (klasie) obiektu',
fontsize=FS_TITLE, fontweight='bold', pad=12)
# Publisher
draw_box(ax, 0.2, 4.2, 2.4, 1.1, 'Publisher', fill=GRAY1, fontsize=10, fontweight='bold')
# Messages
draw_box(ax, 0.1, 2.8, 2.6, 0.9, 'new OrderEvent()', fill=GRAY4, fontsize=9)
draw_box(ax, 0.1, 1.5, 2.6, 0.9, 'new PaymentEvent()', fill=GRAY4, fontsize=9)
# Broker
draw_box(ax, 4.0, 2.3, 3.0, 2.4, 'BROKER\n\nrouting po\ntypie klasy', fill=GRAY2, fontsize=10, fontweight='bold')
# Subscribers
draw_box(ax, 8.5, 4.8, 3.2, 1.0, 'Sub A\n→ OrderEvent', fill=GRAY1, fontsize=9)
draw_box(ax, 8.5, 3.2, 3.2, 1.0, 'Sub B\n→ PaymentEvent', fill=GRAY1, fontsize=9)
draw_box(ax, 8.5, 1.6, 3.2, 1.0, 'Sub C\n→ Event (base)', fill=GRAY1, fontsize=9)
# Arrows
draw_arrow(ax, 2.7, 3.2, 4.0, 3.8)
draw_arrow(ax, 2.7, 2.0, 4.0, 3.0)
draw_arrow(ax, 7.0, 4.3, 8.5, 5.2, label='OrderEvent', label_fs=8)
draw_arrow(ax, 7.0, 3.5, 8.5, 3.7, label='PaymentEvent', label_fs=8)
draw_arrow(ax, 7.0, 3.0, 8.5, 2.2, label='oba (dziedziczenie!)', label_fs=8)
# Class hierarchy inset
hx, hy = 0.5, 0.0
draw_box(ax, hx + 2.0, hy + 0.2, 1.8, 0.6, 'Event', fill=GRAY3, fontsize=8, fontweight='bold')
draw_box(ax, hx + 0.0, hy + 0.2, 1.8, 0.6, 'OrderEvent', fill=GRAY4, fontsize=7.5)
draw_box(ax, hx + 4.0, hy + 0.2, 2.0, 0.6, 'PaymentEvent', fill=GRAY4, fontsize=7.5)
draw_arrow(ax, hx + 2.9, hy + 0.2, hx + 0.9, hy + 0.2, lw=1.0, style='->', label='extends', label_offset=-0.3, label_fs=7)
draw_arrow(ax, hx + 2.9, hy + 0.2, hx + 5.0, hy + 0.2, lw=1.0, style='->', label='extends', label_offset=-0.3, label_fs=7)
ax.text(9.5, 0.5, 'Sub C subskrybuje bazowy Event\n→ otrzymuje WSZYSTKIE podtypy',
ha='center', va='center', fontsize=8.5, style='italic',
bbox=dict(boxstyle='round,pad=0.3', facecolor=GRAY4, edgecolor=GRAY3))
save(fig, 'pubsub_sub_type.png')
# ============================================================
# 4. Hierarchical / Wildcards subscription
# ============================================================
def draw_sub_hierarchical():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('Subskrypcja hierarchiczna (wildcards) — wzorce tematów',
fontsize=FS_TITLE, fontweight='bold', pad=12)
# Topic tree
draw_box(ax, 4.5, 5.8, 2.4, 0.8, 'sensors/', fill=GRAY2, fontsize=10, fontweight='bold')
draw_box(ax, 1.5, 4.2, 2.4, 0.8, 'temperature/', fill=GRAY3, fontsize=9)
draw_box(ax, 7.5, 4.2, 2.4, 0.8, 'humidity/', fill=GRAY3, fontsize=9)
draw_box(ax, 0.2, 2.8, 1.8, 0.7, 'room1', fill=GRAY4, fontsize=8.5)
draw_box(ax, 2.4, 2.8, 1.8, 0.7, 'room2', fill=GRAY4, fontsize=8.5)
draw_box(ax, 6.8, 2.8, 1.8, 0.7, 'room1', fill=GRAY4, fontsize=8.5)
draw_box(ax, 9.0, 2.8, 1.8, 0.7, 'room2', fill=GRAY4, fontsize=8.5)
# Tree edges
draw_arrow(ax, 5.7, 5.8, 2.7, 5.0, lw=1.0)
draw_arrow(ax, 5.7, 5.8, 8.7, 5.0, lw=1.0)
draw_arrow(ax, 2.2, 4.2, 1.1, 3.5, lw=1.0)
draw_arrow(ax, 3.2, 4.2, 3.3, 3.5, lw=1.0)
draw_arrow(ax, 8.2, 4.2, 7.7, 3.5, lw=1.0)
draw_arrow(ax, 9.2, 4.2, 9.9, 3.5, lw=1.0)
# Full paths
ax.text(1.1, 2.4, 'sensors/temperature/room1', fontsize=7, ha='center',
fontfamily='monospace', style='italic')
ax.text(3.3, 2.4, 'sensors/temperature/room2', fontsize=7, ha='center',
fontfamily='monospace', style='italic')
# Wildcard examples
ax.text(0.3, 1.5, 'Wzorce subskrypcji (MQTT-style):', fontsize=10, fontweight='bold')
patterns = [
('"sensors/temperature/room1"', '→ TYLKO room1', '(dokładne dopasowanie)'),
('"sensors/temperature/*"', '→ room1, room2', '( * = jeden poziom)'),
('"sensors/#"', '→ WSZYSTKO', '( # = dowolna głębokość)'),
]
for i, (pat, result, note) in enumerate(patterns):
yy = 0.9 - i * 0.55
ax.text(0.5, yy, pat, fontsize=9, fontweight='bold', fontfamily='monospace')
ax.text(7.0, yy, result, fontsize=9, fontweight='bold')
ax.text(9.5, yy, note, fontsize=8, style='italic')
save(fig, 'pubsub_sub_hierarchical.png')
# ============================================================
# 5. At-most-once (QoS 0)
# ============================================================
def draw_qos_at_most_once():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 4.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('QoS: At-most-once — „wyślij i zapomnij" (0 lub 1 dostarczenie)',
fontsize=FS_TITLE, fontweight='bold', pad=12)
# Actors
px, bx, sx = 1.0, 4.8, 8.5
pw, bw, sw = 2.0, 2.2, 2.0
bh = 0.8
draw_box(ax, px, 5.0, pw, bh, 'Publisher', fill=GRAY1, fontsize=10, fontweight='bold')
draw_box(ax, bx, 5.0, bw, bh, 'Broker', fill=GRAY2, fontsize=10, fontweight='bold')
draw_box(ax, sx, 5.0, sw, bh, 'Subscriber', fill=GRAY1, fontsize=10, fontweight='bold')
# Timelines
for xc in [px + pw/2, bx + bw/2, sx + sw/2]:
ax.plot([xc, xc], [5.0, 1.2], color=GRAY3, lw=1, linestyle=':')
# Scenario A: success
y = 4.3
ax.text(0.2, y + 0.15, 'Scenariusz A:', fontsize=8.5, fontweight='bold')
draw_arrow(ax, px + pw/2, y, bx + bw/2, y, label='MSG', label_fs=9)
draw_arrow(ax, bx + bw/2, y - 0.6, sx + sw/2, y - 0.6, label='MSG', label_fs=9)
draw_check(ax, sx + sw/2 + 0.4, y - 0.6, size=0.18)
ax.text(sx + sw/2 + 0.7, y - 0.6, 'OK', fontsize=9, fontweight='bold')
# Scenario B: lost
y = 2.6
ax.text(0.2, y + 0.15, 'Scenariusz B:', fontsize=8.5, fontweight='bold')
draw_arrow(ax, px + pw/2, y, bx + bw/2, y, label='MSG', label_fs=9)
draw_dashed_arrow(ax, bx + bw/2, y - 0.6, 7.5, y - 0.6)
draw_cross(ax, 7.8, y - 0.6, size=0.2)
ax.text(8.2, y - 0.55, 'UTRACONA', fontsize=9, fontweight='bold')
ax.text(8.2, y - 1.0, '(brak retransmisji)', fontsize=8, style='italic')
# Summary
ax.text(6.0, 0.5, 'Brak ACK, brak retransmisji. Najszybszy. Use case: logi, metryki, telemetria.',
ha='center', va='center', fontsize=9,
bbox=dict(boxstyle='round,pad=0.4', facecolor=GRAY4, edgecolor=GRAY3))
save(fig, 'pubsub_qos_at_most_once.png')
# ============================================================
# 6. At-least-once (QoS 1)
# ============================================================
def draw_qos_at_least_once():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.0))
ax.set_xlim(0, 12)
ax.set_ylim(0, 6.5)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('QoS: At-least-once — „powtarzaj aż potwierdzą" (≥1 dostarczenie)',
fontsize=FS_TITLE, fontweight='bold', pad=12)
bx, bw = 3.5, 2.2
sx, sw = 8.0, 2.2
bh = 0.8
draw_box(ax, bx, 5.5, bw, bh, 'Broker', fill=GRAY2, fontsize=10, fontweight='bold')
draw_box(ax, sx, 5.5, sw, bh, 'Subscriber', fill=GRAY1, fontsize=10, fontweight='bold')
# Timelines
for xc in [bx + bw/2, sx + sw/2]:
ax.plot([xc, xc], [5.5, 0.8], color=GRAY3, lw=1, linestyle=':')
# Step 1: send MSG
y1 = 4.8
draw_arrow(ax, bx + bw/2, y1, sx + sw/2, y1, label='MSG #1', label_fs=9)
draw_check(ax, sx + sw + 0.2, y1, size=0.15)
ax.text(sx + sw + 0.5, y1, 'odebrano', fontsize=8)
# Step 2: ACK lost
y2 = 3.9
draw_dashed_arrow(ax, sx + sw/2, y2, bx + bw + 1.2, y2)
ax.text((bx + bw/2 + sx + sw/2)/2, y2 + 0.18, 'ACK', fontsize=9)
draw_cross(ax, bx + bw + 0.8, y2, size=0.18)
ax.text(bx + 0.3, y2 - 0.35, 'ACK utracony!', fontsize=8.5, style='italic')
# Step 3: timeout → retry
y3 = 2.9
ax.text(bx + bw/2, y3 + 0.45, 'timeout...', fontsize=8.5, style='italic', ha='center')
draw_arrow(ax, bx + bw/2, y3, sx + sw/2, y3, label='MSG #1 (retry)', label_fs=9)
draw_check(ax, sx + sw + 0.2, y3, size=0.15)
ax.text(sx + sw + 0.5, y3, 'odebrano\n(ponownie!)', fontsize=8)
# Step 4: ACK ok
y4 = 2.0
draw_arrow(ax, sx + sw/2, y4, bx + bw/2, y4, label='ACK', label_fs=9)
draw_check(ax, bx + bw/2 - 0.5, y4, size=0.18)
# Duplicate bracket
ax.annotate('', xy=(sx + sw + 1.3, y1), xytext=(sx + sw + 1.3, y3),
arrowprops=dict(arrowstyle='<->', color='black', lw=1.2))
ax.text(sx + sw + 1.6, (y1 + y3)/2, 'DUPLIKAT!\nSubscriber\notrzymał 2×',
fontsize=9, ha='left', va='center', fontweight='bold',
bbox=dict(boxstyle='round,pad=0.25', facecolor=GRAY4, edgecolor=GRAY3))
# Summary
ax.text(6.0, 0.5, 'Broker czeka na ACK, retransmituje po timeout. Mogą być duplikaty!\n'
'Use case: zamówienia, płatności (subscriber musi być idempotentny).',
ha='center', va='center', fontsize=9,
bbox=dict(boxstyle='round,pad=0.4', facecolor=GRAY4, edgecolor=GRAY3))
save(fig, 'pubsub_qos_at_least_once.png')
# ============================================================
# 7. Exactly-once (QoS 2)
# ============================================================
def draw_qos_exactly_once():
fig, ax = plt.subplots(1, 1, figsize=(FIG_W, 5.5))
ax.set_xlim(0, 12)
ax.set_ylim(0, 7)
ax.set_aspect('equal')
ax.axis('off')
ax.set_title('QoS: Exactly-once — 4-krokowy handshake (dokładnie 1 dostarczenie)',
fontsize=FS_TITLE, fontweight='bold', pad=12)
bx, bw = 2.5, 2.2
sx, sw = 7.5, 2.2
bh = 0.8
draw_box(ax, bx, 6.0, bw, bh, 'Broker', fill=GRAY2, fontsize=10, fontweight='bold')
draw_box(ax, sx, 6.0, sw, bh, 'Subscriber', fill=GRAY1, fontsize=10, fontweight='bold')
# Timelines
for xc in [bx + bw/2, sx + sw/2]:
ax.plot([xc, xc], [6.0, 1.0], color=GRAY3, lw=1, linestyle=':')
# 4-step handshake
steps = [
(5.2, 'right', 'PUBLISH (msg_id=42)', 'Broker wysyła wiadomość'),
(4.2, 'left', 'PUBREC (otrzymałem id=42)', 'Sub potwierdza odbiór, zapisuje id'),
(3.2, 'right', 'PUBREL (możesz przetworzyć)', 'Broker zwalnia wiadomość'),
(2.2, 'left', 'PUBCOMP (zakończone)', 'Sub potwierdza przetworzenie'),
]
for i, (y, direction, label, desc) in enumerate(steps):
# Step number
ax.text(bx + bw/2 - 0.7, y, f'{i+1}', fontsize=9,
fontweight='bold', ha='center', va='center',
bbox=dict(boxstyle='circle,pad=0.18', facecolor=GRAY3, edgecolor=LN))
if direction == 'right':
draw_arrow(ax, bx + bw/2, y, sx + sw/2, y, label=label, label_fs=9)
else:
draw_arrow(ax, sx + sw/2, y, bx + bw/2, y, label=label, label_fs=9)
# Side description
ax.text(sx + sw + 0.3, y, desc, fontsize=8, ha='left', va='center', style='italic')
# Summary
ax.text(6.0, 0.6, 'Deduplikacja po msg_id. Sub nie przetwarza przed PUBREL.\n'
'Najkosztowniejszy (4 pakiety). Use case: transakcje finansowe, krytyczne zdarzenia.',
ha='center', va='center', fontsize=9,
bbox=dict(boxstyle='round,pad=0.4', facecolor=GRAY4, edgecolor=GRAY3))
save(fig, 'pubsub_qos_exactly_once.png')
# ============================================================
# Main
# ============================================================
if __name__ == '__main__':
print("Generating Pub/Sub diagrams (7 separate images)...")
draw_sub_topic()
draw_sub_content()
draw_sub_type()
draw_sub_hierarchical()
draw_qos_at_most_once()
draw_qos_at_least_once()
draw_qos_exactly_once()
print("Done!")