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

92 lines
3.4 KiB
Go

package ports
import (
"context"
"fmt"
"strings"
)
// JobResultPublisher emits one entry on the `runtime:job_results` Redis
// Stream per finalised start or stop runtime job. Adapters serialise
// every JobResult field verbatim so consumers (Game Lobby's
// runtime-job-result worker today, future services tomorrow) see the
// AsyncAPI shape frozen in `rtmanager/api/runtime-jobs-asyncapi.yaml`.
//
// The start-jobs and stop-jobs consumers publish through this port.
// The synchronous REST handlers do not — REST callers receive the same
// `Result` shape directly from the service layer.
type JobResultPublisher interface {
// Publish records result on the configured `runtime:job_results`
// stream. A non-nil error reports a transport or serialisation
// failure; the caller treats the failure as a degraded emission
// (the operation_log already records the durable outcome).
Publish(ctx context.Context, result JobResult) error
}
// JobResult outcome values frozen by the
// `RuntimeJobResultPayload.outcome` enum.
const (
// JobOutcomeSuccess marks a successful start or stop, including the
// idempotent replay variant (`error_code=replay_no_op`).
JobOutcomeSuccess = "success"
// JobOutcomeFailure marks a stable failure for which the payload
// carries a non-empty `error_code`.
JobOutcomeFailure = "failure"
)
// JobResult carries the wire payload published on
// `runtime:job_results`. The fields mirror the AsyncAPI schema frozen
// in `rtmanager/api/runtime-jobs-asyncapi.yaml`; adapters serialise
// every field verbatim so consumers see the contracted shape. Fields
// that are required by the contract (every field on this struct) are
// always present in the wire entry — even when their string value is
// empty (allowed for `container_id` / `engine_endpoint` / `error_code`
// / `error_message` on appropriate variants).
type JobResult struct {
// GameID identifies the platform game the job acted on. Required.
GameID string
// Outcome reports the high-level outcome. Must be `success` or
// `failure` (use the JobOutcome* constants).
Outcome string
// ContainerID stores the Docker container id. Populated on
// `success` for fresh starts and replays; empty on `failure` and
// on `success/replay_no_op` for stop jobs that observed a removed
// record.
ContainerID string
// EngineEndpoint stores the stable engine URL
// `http://galaxy-game-{game_id}:8080`. Populated alongside
// ContainerID, empty in the same cases.
EngineEndpoint string
// ErrorCode stores the stable error code from
// `rtmanager/README.md §Error Model`. Empty for fresh successes,
// `replay_no_op` for idempotent replays, one of the failure
// codes otherwise.
ErrorCode string
// ErrorMessage stores the operator-readable detail. Empty for
// successes; populated alongside ErrorCode on failure.
ErrorMessage string
}
// Validate reports whether result satisfies the structural invariants
// implied by the AsyncAPI schema: a non-empty game id and one of the
// two known outcome values. The remaining fields are required to be
// present on the wire but may be empty strings, so Validate does not
// constrain them.
func (result JobResult) Validate() error {
if strings.TrimSpace(result.GameID) == "" {
return fmt.Errorf("job result: game id must not be empty")
}
switch result.Outcome {
case JobOutcomeSuccess, JobOutcomeFailure:
return nil
default:
return fmt.Errorf("job result: outcome %q is unsupported", result.Outcome)
}
}