92 lines
3.4 KiB
Go
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)
|
|
}
|
|
}
|