|
@@ -11,6 +11,7 @@ flowchart LR
|
|
|
WH[Webhook / HTTP]
|
|
WH[Webhook / HTTP]
|
|
|
WS[WebSocket clients]
|
|
WS[WebSocket clients]
|
|
|
MQ[MQTT publishers]
|
|
MQ[MQTT publishers]
|
|
|
|
|
+ GR[gRPC bidi-stream]
|
|
|
end
|
|
end
|
|
|
subgraph BA[Broad-Announce]
|
|
subgraph BA[Broad-Announce]
|
|
|
IG[ingestd]
|
|
IG[ingestd]
|
|
@@ -36,6 +37,7 @@ flowchart LR
|
|
|
WH --> IG
|
|
WH --> IG
|
|
|
WS --> IG
|
|
WS --> IG
|
|
|
MQ --> IG
|
|
MQ --> IG
|
|
|
|
|
+ GR --> IG
|
|
|
IG --> RD
|
|
IG --> RD
|
|
|
IG --> BR
|
|
IG --> BR
|
|
|
BR --> RT
|
|
BR --> RT
|
|
@@ -102,7 +104,47 @@ sequenceDiagram
|
|
|
end
|
|
end
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 3. Recipient resolution algorithm
|
|
|
|
|
|
|
+## 3. gRPC ingest (M11)
|
|
|
|
|
+
|
|
|
|
|
+```mermaid
|
|
|
|
|
+sequenceDiagram
|
|
|
|
|
+ autonumber
|
|
|
|
|
+ participant Src as Internal service
|
|
|
|
|
+ participant GS as ingestd (gRPC :9090)
|
|
|
|
|
+ participant RD as Redis
|
|
|
|
|
+ participant NATS as NATS JetStream
|
|
|
|
|
+
|
|
|
|
|
+ Src->>GS: open bidi stream (metadata: authorization=Bearer <api_key>)
|
|
|
|
|
+ GS->>GS: verify API key, attach source_id
|
|
|
|
|
+ loop
|
|
|
|
|
+ Src->>GS: Alert{company_id, source_id, severity, ...}
|
|
|
|
|
+ GS->>GS: schema validate
|
|
|
|
|
+ GS->>RD: SET dedupe:{key} 1 NX EX 60
|
|
|
|
|
+ alt first
|
|
|
|
|
+ RD-->>GS: OK
|
|
|
|
|
+ GS->>NATS: publish alerts.<company_id>
|
|
|
|
|
+ GS-->>Src: Ack{alert_id, dedupe_count=1}
|
|
|
|
|
+ else dup
|
|
|
|
|
+ RD-->>GS: nil
|
|
|
|
|
+ RD->>RD: INCR → n
|
|
|
|
|
+ GS->>NATS: publish alerts.<company_id> {dedupe_count=n}
|
|
|
|
|
+ GS-->>Src: Ack{alert_id, dedupe_count=n}
|
|
|
|
|
+ end
|
|
|
|
|
+ opt rate-limited
|
|
|
|
|
+ GS-->>Src: Ack{error: RATE_LIMITED, retry_after_ms=1000}
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+ Src-->>GS: half-close (writes done)
|
|
|
|
|
+ GS-->>Src: EOF
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+Key invariants:
|
|
|
|
|
+- Stream is the unit of auth + rate-limit (one stream = one source).
|
|
|
|
|
+- Per-stream in-flight cap = 256 → natural backpressure when the
|
|
|
|
|
+ source outpaces us.
|
|
|
|
|
+- After M11, gRPC handles the same `dedupe_count` contract as HTTP.
|
|
|
|
|
+
|
|
|
|
|
+## 4. Recipient resolution algorithm
|
|
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
|
flowchart TD
|
|
flowchart TD
|
|
@@ -122,7 +164,7 @@ flowchart TD
|
|
|
J -- no --> K[emit one Delivery per channel in sub.channel_mask]
|
|
J -- no --> K[emit one Delivery per channel in sub.channel_mask]
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 4. Storage topology
|
|
|
|
|
|
|
+## 5. Storage topology
|
|
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
|
flowchart LR
|
|
flowchart LR
|
|
@@ -139,7 +181,7 @@ flowchart LR
|
|
|
CH --- AGG[materialized aggregates<br/>per-company, per-channel]
|
|
CH --- AGG[materialized aggregates<br/>per-company, per-channel]
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 5. NATS subject layout
|
|
|
|
|
|
|
+## 6. NATS subject layout
|
|
|
|
|
|
|
|
| subject | producer | consumer | retention |
|
|
| subject | producer | consumer | retention |
|
|
|
|---|---|---|---|
|
|
|---|---|---|---|
|
|
@@ -156,7 +198,7 @@ flowchart LR
|
|
|
Subject-based partitioning keeps a single company's alert stream in
|
|
Subject-based partitioning keeps a single company's alert stream in
|
|
|
order (mostly) and lets us add per-company worker affinity in K8s later.
|
|
order (mostly) and lets us add per-company worker affinity in K8s later.
|
|
|
|
|
|
|
|
-## 6. FCM delivery paths
|
|
|
|
|
|
|
+## 7. FCM delivery paths
|
|
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
|
flowchart TD
|
|
flowchart TD
|
|
@@ -200,7 +242,7 @@ flowchart TD
|
|
|
The Android app reads `data.category` and `data.severity` to pick
|
|
The Android app reads `data.category` and `data.severity` to pick
|
|
|
the right sound + channel.
|
|
the right sound + channel.
|
|
|
|
|
|
|
|
-## 7. Telegram bot surface
|
|
|
|
|
|
|
+## 8. Telegram bot surface
|
|
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
|
sequenceDiagram
|
|
sequenceDiagram
|
|
@@ -211,12 +253,14 @@ sequenceDiagram
|
|
|
participant RT as routerd
|
|
participant RT as routerd
|
|
|
participant PG as Postgres
|
|
participant PG as Postgres
|
|
|
|
|
|
|
|
- U->>TG: /start <invite_code>
|
|
|
|
|
|
|
+ U->>TG: /start INVITE_CODE
|
|
|
TG->>BOT: update (chat_id, from.id, text=invite_code)
|
|
TG->>BOT: update (chat_id, from.id, text=invite_code)
|
|
|
BOT->>PG: SELECT individual WHERE invite_code=?
|
|
BOT->>PG: SELECT individual WHERE invite_code=?
|
|
|
- Note over BOT,PG: Individual must pre-exist;<br/>admin creates row + invite code,<br/>bot matches and burns code
|
|
|
|
|
|
|
+ Note over BOT,PG: Individual must pre-exist;
|
|
|
|
|
+ Note over BOT,PG: admin creates row + invite code;
|
|
|
|
|
+ Note over BOT,PG: bot matches and burns code
|
|
|
BOT->>PG: UPDATE individual SET telegram_chat_id=...
|
|
BOT->>PG: UPDATE individual SET telegram_chat_id=...
|
|
|
- BOT->>TG: sendMessage(chat_id, "Linked to <name>")
|
|
|
|
|
|
|
+ BOT->>TG: sendMessage "Linked to NAME"
|
|
|
|
|
|
|
|
U->>TG: /mute 2h
|
|
U->>TG: /mute 2h
|
|
|
TG->>BOT: update
|
|
TG->>BOT: update
|
|
@@ -224,17 +268,17 @@ sequenceDiagram
|
|
|
|
|
|
|
|
Note over RT,PG: New alert arrives
|
|
Note over RT,PG: New alert arrives
|
|
|
RT->>PG: resolve recipients incl. telegram
|
|
RT->>PG: resolve recipients incl. telegram
|
|
|
- RT->>BR: deliveries.telegram.<company_id>
|
|
|
|
|
|
|
+ RT->>BR: deliveries.telegram.COMPANY_ID
|
|
|
BR->>BOT: job
|
|
BR->>BOT: job
|
|
|
BOT->>TG: sendMessage(chat_id, text, reply_markup)
|
|
BOT->>TG: sendMessage(chat_id, text, reply_markup)
|
|
|
|
|
|
|
|
U->>TG: taps [Acknowledge]
|
|
U->>TG: taps [Acknowledge]
|
|
|
TG->>BOT: callback_query
|
|
TG->>BOT: callback_query
|
|
|
- BOT->>BR: ack.<alert_id>.<individual_id>
|
|
|
|
|
|
|
+ BOT->>BR: ack.ALERT_ID.INDIVIDUAL_ID
|
|
|
BR->>RT: close alert (or notify source)
|
|
BR->>RT: close alert (or notify source)
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 8. Failure & retry
|
|
|
|
|
|
|
+## 9. Failure & retry
|
|
|
|
|
|
|
|
| layer | failure mode | behavior |
|
|
| layer | failure mode | behavior |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
@@ -246,7 +290,7 @@ sequenceDiagram
|
|
|
| FCM | token unregistered | mark `fcm_tokens.status='unregistered'`, skip on next send |
|
|
| FCM | token unregistered | mark `fcm_tokens.status='unregistered'`, skip on next send |
|
|
|
| Telegram | chat not found | mark `individuals.telegram_status='revoked'`, skip |
|
|
| Telegram | chat not found | mark `individuals.telegram_status='revoked'`, skip |
|
|
|
|
|
|
|
|
-## 9. SLOs
|
|
|
|
|
|
|
+## 10. SLOs
|
|
|
|
|
|
|
|
| SLI | target | measured at |
|
|
| SLI | target | measured at |
|
|
|
|---|---|---|
|
|
|---|---|---|
|
|
@@ -257,7 +301,7 @@ sequenceDiagram
|
|
|
| DLQ rate | < 0.5% of deliveries | deliverd |
|
|
| DLQ rate | < 0.5% of deliveries | deliverd |
|
|
|
| Per-company data isolation | 100% (no cross-tenant queries possible) | audit + e2e test |
|
|
| Per-company data isolation | 100% (no cross-tenant queries possible) | audit + e2e test |
|
|
|
|
|
|
|
|
-## 10. Capacity model (back-of-envelope)
|
|
|
|
|
|
|
+## 11. Capacity model (back-of-envelope)
|
|
|
|
|
|
|
|
Assumption: 50k alerts/sec peak, avg fan-out 10 recipients × 2 channels = 20 deliveries per alert.
|
|
Assumption: 50k alerts/sec peak, avg fan-out 10 recipients × 2 channels = 20 deliveries per alert.
|
|
|
|
|
|
|
@@ -278,7 +322,7 @@ this — that is exactly the SLO that forces K8s in v2.
|
|
|
For v1 we target **5k alerts/sec** sustained in the docker-compose
|
|
For v1 we target **5k alerts/sec** sustained in the docker-compose
|
|
|
profile, and gate "50k/s" on the K8s + multi-broker milestone.
|
|
profile, and gate "50k/s" on the K8s + multi-broker milestone.
|
|
|
|
|
|
|
|
-## 11. Security model
|
|
|
|
|
|
|
+## 12. Security model
|
|
|
|
|
|
|
|
```mermaid
|
|
```mermaid
|
|
|
flowchart LR
|
|
flowchart LR
|
|
@@ -304,7 +348,7 @@ flowchart LR
|
|
|
APP --> RD
|
|
APP --> RD
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-## 12. What changes when we move to K8s
|
|
|
|
|
|
|
+## 13. What changes when we move to K8s
|
|
|
|
|
|
|
|
- ingestd: HPA on CPU + custom metric `ingestd_queue_depth`
|
|
- ingestd: HPA on CPU + custom metric `ingestd_queue_depth`
|
|
|
- routerd: HPA on CPU + custom metric `routerd_pending_messages`
|
|
- routerd: HPA on CPU + custom metric `routerd_pending_messages`
|