79 lines
2.4 KiB
Go
79 lines
2.4 KiB
Go
// Package stream implementiert den langlebigen Kanal vom Sultan-Server zum
|
|
// Agenten als Server-Sent Events (SSE) über eine ausgehende HTTP-Verbindung
|
|
// des Agenten.
|
|
//
|
|
// Der Server (Hub) hält pro Knoten einen offenen Stream, schickt Events sofort
|
|
// und sendet in Ruhephasen alle 45s die SSE-Kommentarzeile ":\n\n" als
|
|
// Keepalive, damit NAT-/Conntrack-Einträge nicht verfallen. Der Agent (Client)
|
|
// liest den Stream, erkennt einen toten Server, wenn zu lange gar nichts
|
|
// ankommt (auch kein Keepalive), und verbindet sich mit exponentiellem
|
|
// Backoff neu.
|
|
//
|
|
// Die Gegenrichtung (Agent -> Server, z. B. service_state) ist nicht Teil
|
|
// dieses Streams; sie läuft über eigene Requests.
|
|
package stream
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// EventType ist der Typ eines Events gemäß Spec Abschnitt 4.2.
|
|
type EventType string
|
|
|
|
const (
|
|
EventServiceState EventType = "service_state"
|
|
EventLeaseRenew EventType = "lease_renew"
|
|
EventNodeCommand EventType = "node_command"
|
|
EventError EventType = "error"
|
|
)
|
|
|
|
// ErrInvalidEvent meldet ein Event, das sich nicht senden oder lesen lässt.
|
|
var ErrInvalidEvent = errors.New("stream: ungültiges Event")
|
|
|
|
// Event ist die Nachricht auf dem Stream.
|
|
type Event struct {
|
|
ID string `json:"event_id"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
Type EventType `json:"type"`
|
|
NodeID string `json:"node_id"`
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
// NewEvent erzeugt ein Event mit neuer ID, aktuellem Zeitstempel und der als
|
|
// JSON serialisierten payload (nil ergibt keine Payload). NodeID setzt der Hub
|
|
// beim Senden.
|
|
func NewEvent(t EventType, payload any) (Event, error) {
|
|
if t == "" {
|
|
return Event{}, fmt.Errorf("%w: Typ fehlt", ErrInvalidEvent)
|
|
}
|
|
ev := Event{ID: newEventID(), Timestamp: time.Now().Unix(), Type: t}
|
|
if payload != nil {
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return Event{}, fmt.Errorf("%w: %v", ErrInvalidEvent, err)
|
|
}
|
|
ev.Payload = b
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func newEventID() string {
|
|
var b [16]byte
|
|
rand.Read(b[:])
|
|
return "evt_" + hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// encode erzeugt den SSE-Rahmen für ev.
|
|
func encode(ev Event) ([]byte, error) {
|
|
data, err := json.Marshal(ev)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidEvent, err)
|
|
}
|
|
return fmt.Appendf(nil, "id: %s\nevent: %s\ndata: %s\n\n", ev.ID, ev.Type, data), nil
|
|
}
|