Files
galaxy-game/rtmanager/internal/ports/healtheventspublisher.go
T
2026-04-28 20:39:18 +02:00

82 lines
3.2 KiB
Go

package ports
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"galaxy/rtmanager/internal/domain/health"
)
// HealthEventPublisher emits one entry on the `runtime:health_events`
// Redis Stream and updates `health_snapshots` with the latest observation
// for the affected game. Adapters publish and snapshot in one call so
// every emission durably advances both surfaces; partial publishes (event
// without snapshot, or vice versa) are not allowed.
//
// The start service emits `container_started` through this port; the
// periodic Docker inspect, the active probe, and the Docker events
// listener publish the rest of the event types through the same port
// without changing its surface.
type HealthEventPublisher interface {
// Publish records envelope on the configured `runtime:health_events`
// stream and upserts the matching `health_snapshots` row. A non-nil
// error reports a transport or storage failure; the caller treats it
// as a degraded emission per `rtmanager/README.md §Notification
// Contracts` (the underlying business state is the source of truth,
// not the event stream).
Publish(ctx context.Context, envelope HealthEventEnvelope) error
}
// HealthEventEnvelope carries the payload published on
// `runtime:health_events`. The fields mirror the AsyncAPI schema frozen
// in `rtmanager/api/runtime-health-asyncapi.yaml`; adapters serialise
// every field verbatim so consumers see the contracted shape.
type HealthEventEnvelope struct {
// GameID identifies the platform game the event refers to.
GameID string
// ContainerID identifies the Docker container observed by the event
// source. May differ from the record's current container id after a
// restart race; consumers are expected to treat the value as the
// observation's container, not the record's.
ContainerID string
// EventType classifies the event per the frozen vocabulary in
// `galaxy/rtmanager/internal/domain/health.EventType`.
EventType health.EventType
// OccurredAt stores the wall-clock at which Runtime Manager observed
// the event. Adapters convert it to UTC milliseconds for the wire
// payload (`occurred_at_ms`).
OccurredAt time.Time
// Details stores the event-type-specific JSON payload. Adapters
// persist and stream it verbatim; nil and empty values are treated as
// the canonical empty-object payload.
Details json.RawMessage
}
// Validate reports whether envelope satisfies the structural invariants
// implied by the AsyncAPI schema.
func (envelope HealthEventEnvelope) Validate() error {
if strings.TrimSpace(envelope.GameID) == "" {
return fmt.Errorf("health event envelope: game id must not be empty")
}
if strings.TrimSpace(envelope.ContainerID) == "" {
return fmt.Errorf("health event envelope: container id must not be empty")
}
if !envelope.EventType.IsKnown() {
return fmt.Errorf("health event envelope: event type %q is unsupported", envelope.EventType)
}
if envelope.OccurredAt.IsZero() {
return fmt.Errorf("health event envelope: occurred at must not be zero")
}
if len(envelope.Details) > 0 && !json.Valid(envelope.Details) {
return fmt.Errorf("health event envelope: details must be valid JSON when non-empty")
}
return nil
}