101 lines
3.1 KiB
Go
101 lines
3.1 KiB
Go
// Package jobresultspublisher provides the Redis-Streams-backed
|
|
// publisher for `runtime:job_results`. The start-jobs and stop-jobs
|
|
// consumers call this adapter so every consumed envelope produces
|
|
// exactly one outcome entry on the result stream.
|
|
//
|
|
// The wire fields mirror the AsyncAPI schema frozen in
|
|
// `rtmanager/api/runtime-jobs-asyncapi.yaml`. Every field is XADDed
|
|
// even when empty so consumers can rely on the schema's required-field
|
|
// set.
|
|
package jobresultspublisher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"galaxy/rtmanager/internal/ports"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// Wire field names used by the Redis Streams payload. Frozen by
|
|
// `rtmanager/api/runtime-jobs-asyncapi.yaml`; renaming any of them
|
|
// breaks consumers.
|
|
const (
|
|
fieldGameID = "game_id"
|
|
fieldOutcome = "outcome"
|
|
fieldContainerID = "container_id"
|
|
fieldEngineEndpoint = "engine_endpoint"
|
|
fieldErrorCode = "error_code"
|
|
fieldErrorMessage = "error_message"
|
|
)
|
|
|
|
// Config groups the dependencies and stream name required to construct
|
|
// a Publisher.
|
|
type Config struct {
|
|
// Client appends entries to the Redis Stream. Must be non-nil.
|
|
Client *redis.Client
|
|
|
|
// Stream stores the Redis Stream key job results are published to
|
|
// (e.g. `runtime:job_results`). Must not be empty.
|
|
Stream string
|
|
}
|
|
|
|
// Publisher implements `ports.JobResultPublisher` on top of a shared
|
|
// Redis client.
|
|
type Publisher struct {
|
|
client *redis.Client
|
|
stream string
|
|
}
|
|
|
|
// NewPublisher constructs one Publisher from cfg. Validation errors
|
|
// surface the missing collaborator verbatim.
|
|
func NewPublisher(cfg Config) (*Publisher, error) {
|
|
if cfg.Client == nil {
|
|
return nil, errors.New("new rtmanager job results publisher: nil redis client")
|
|
}
|
|
if strings.TrimSpace(cfg.Stream) == "" {
|
|
return nil, errors.New("new rtmanager job results publisher: stream must not be empty")
|
|
}
|
|
return &Publisher{
|
|
client: cfg.Client,
|
|
stream: cfg.Stream,
|
|
}, nil
|
|
}
|
|
|
|
// Publish XADDs result to the configured Redis Stream. The wire payload
|
|
// includes every field declared as required by the AsyncAPI schema —
|
|
// empty strings are kept so consumers always see the documented keys.
|
|
func (publisher *Publisher) Publish(ctx context.Context, result ports.JobResult) error {
|
|
if publisher == nil || publisher.client == nil {
|
|
return errors.New("publish job result: nil publisher")
|
|
}
|
|
if ctx == nil {
|
|
return errors.New("publish job result: nil context")
|
|
}
|
|
if err := result.Validate(); err != nil {
|
|
return fmt.Errorf("publish job result: %w", err)
|
|
}
|
|
|
|
values := map[string]any{
|
|
fieldGameID: result.GameID,
|
|
fieldOutcome: result.Outcome,
|
|
fieldContainerID: result.ContainerID,
|
|
fieldEngineEndpoint: result.EngineEndpoint,
|
|
fieldErrorCode: result.ErrorCode,
|
|
fieldErrorMessage: result.ErrorMessage,
|
|
}
|
|
if err := publisher.client.XAdd(ctx, &redis.XAddArgs{
|
|
Stream: publisher.stream,
|
|
Values: values,
|
|
}).Err(); err != nil {
|
|
return fmt.Errorf("publish job result: xadd: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Compile-time assertion: Publisher implements ports.JobResultPublisher.
|
|
var _ ports.JobResultPublisher = (*Publisher)(nil)
|