82 lines
3.2 KiB
Go
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
|
|
}
|