feat: runtime manager
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user