feat: runtime manager
This commit is contained in:
@@ -118,6 +118,24 @@ const (
|
||||
// Game Lobby when capability evaluation at game finish releases a
|
||||
// reservation because the member did not meet the capability rule.
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied NotificationType = "lobby.race_name.registration_denied"
|
||||
|
||||
// NotificationTypeRuntimeImagePullFailed identifies the
|
||||
// `runtime.image_pull_failed` administrator notification published by
|
||||
// Runtime Manager when the engine image cannot be pulled during a
|
||||
// start operation.
|
||||
NotificationTypeRuntimeImagePullFailed NotificationType = "runtime.image_pull_failed"
|
||||
|
||||
// NotificationTypeRuntimeContainerStartFailed identifies the
|
||||
// `runtime.container_start_failed` administrator notification published
|
||||
// by Runtime Manager when `docker create` or `docker start` returns an
|
||||
// error during a start operation.
|
||||
NotificationTypeRuntimeContainerStartFailed NotificationType = "runtime.container_start_failed"
|
||||
|
||||
// NotificationTypeRuntimeStartConfigInvalid identifies the
|
||||
// `runtime.start_config_invalid` administrator notification published by
|
||||
// Runtime Manager when start configuration validation fails (invalid
|
||||
// `image_ref`, missing Docker network, unwritable state directory).
|
||||
NotificationTypeRuntimeStartConfigInvalid NotificationType = "runtime.start_config_invalid"
|
||||
)
|
||||
|
||||
// String returns the wire value for notificationType.
|
||||
@@ -142,7 +160,10 @@ func (notificationType NotificationType) IsKnown() bool {
|
||||
NotificationTypeLobbyInviteExpired,
|
||||
NotificationTypeLobbyRaceNameRegistrationEligible,
|
||||
NotificationTypeLobbyRaceNameRegistered,
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied,
|
||||
NotificationTypeRuntimeImagePullFailed,
|
||||
NotificationTypeRuntimeContainerStartFailed,
|
||||
NotificationTypeRuntimeStartConfigInvalid:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -170,6 +191,10 @@ func (notificationType NotificationType) ExpectedProducer() Producer {
|
||||
NotificationTypeLobbyRaceNameRegistered,
|
||||
NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
return ProducerGameLobby
|
||||
case NotificationTypeRuntimeImagePullFailed,
|
||||
NotificationTypeRuntimeContainerStartFailed,
|
||||
NotificationTypeRuntimeStartConfigInvalid:
|
||||
return ProducerRuntimeManager
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -180,7 +205,10 @@ func (notificationType NotificationType) SupportsAudience(audienceKind AudienceK
|
||||
switch notificationType {
|
||||
case NotificationTypeGeoReviewRecommended,
|
||||
NotificationTypeGameGenerationFailed,
|
||||
NotificationTypeLobbyRuntimePausedAfterStart:
|
||||
NotificationTypeLobbyRuntimePausedAfterStart,
|
||||
NotificationTypeRuntimeImagePullFailed,
|
||||
NotificationTypeRuntimeContainerStartFailed,
|
||||
NotificationTypeRuntimeStartConfigInvalid:
|
||||
return audienceKind == AudienceKindAdminEmail
|
||||
case NotificationTypeLobbyApplicationSubmitted:
|
||||
return audienceKind == AudienceKindUser || audienceKind == AudienceKindAdminEmail
|
||||
@@ -195,7 +223,10 @@ func (notificationType NotificationType) SupportsChannel(audienceKind AudienceKi
|
||||
switch notificationType {
|
||||
case NotificationTypeGeoReviewRecommended,
|
||||
NotificationTypeGameGenerationFailed,
|
||||
NotificationTypeLobbyRuntimePausedAfterStart:
|
||||
NotificationTypeLobbyRuntimePausedAfterStart,
|
||||
NotificationTypeRuntimeImagePullFailed,
|
||||
NotificationTypeRuntimeContainerStartFailed,
|
||||
NotificationTypeRuntimeStartConfigInvalid:
|
||||
return audienceKind == AudienceKindAdminEmail && channel == ChannelEmail
|
||||
case NotificationTypeLobbyApplicationSubmitted:
|
||||
if audienceKind == AudienceKindAdminEmail {
|
||||
@@ -222,6 +253,9 @@ const (
|
||||
|
||||
// ProducerGameLobby identifies Game Lobby.
|
||||
ProducerGameLobby Producer = "game_lobby"
|
||||
|
||||
// ProducerRuntimeManager identifies Runtime Manager.
|
||||
ProducerRuntimeManager Producer = "runtime_manager"
|
||||
)
|
||||
|
||||
// String returns the wire value for producer.
|
||||
@@ -232,7 +266,7 @@ func (producer Producer) String() string {
|
||||
// IsKnown reports whether producer belongs to the frozen producer set.
|
||||
func (producer Producer) IsKnown() bool {
|
||||
switch producer {
|
||||
case ProducerGeoProfile, ProducerGameMaster, ProducerGameLobby:
|
||||
case ProducerGeoProfile, ProducerGameMaster, ProducerGameLobby, ProducerRuntimeManager:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -801,6 +835,13 @@ func validatePayloadObject(notificationType NotificationType, payload map[string
|
||||
return validateStringFields(payload, "race_name")
|
||||
case NotificationTypeLobbyRaceNameRegistrationDenied:
|
||||
return validateStringFields(payload, "game_id", "game_name", "race_name", "reason")
|
||||
case NotificationTypeRuntimeImagePullFailed,
|
||||
NotificationTypeRuntimeContainerStartFailed,
|
||||
NotificationTypeRuntimeStartConfigInvalid:
|
||||
if err := validateStringFields(payload, "game_id", "image_ref", "error_code", "error_message"); err != nil {
|
||||
return err
|
||||
}
|
||||
return validatePositiveIntFields(payload, "attempted_at_ms")
|
||||
default:
|
||||
return fmt.Errorf("payload_json notification type %q is unsupported", notificationType)
|
||||
}
|
||||
|
||||
@@ -269,6 +269,54 @@ func TestConstructorsBuildExpectedIntentValues(t *testing.T) {
|
||||
recipientUserIDs: []string{"user-9"},
|
||||
payloadJSON: `{"game_id":"game-1","game_name":"Nebula Clash","race_name":"Skylancer","reason":"capability_not_met"}`,
|
||||
},
|
||||
{
|
||||
name: "runtime image pull failed",
|
||||
build: func() (Intent, error) {
|
||||
return NewRuntimeImagePullFailedIntent(metadata, RuntimeImagePullFailedPayload{
|
||||
GameID: "game-1",
|
||||
ImageRef: "galaxy/game:1.4.7",
|
||||
ErrorCode: "image_pull_failed",
|
||||
ErrorMessage: "manifest unknown",
|
||||
AttemptedAtMs: 1775121700000,
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeRuntimeImagePullFailed,
|
||||
producer: ProducerRuntimeManager,
|
||||
audienceKind: AudienceKindAdminEmail,
|
||||
payloadJSON: `{"game_id":"game-1","image_ref":"galaxy/game:1.4.7","error_code":"image_pull_failed","error_message":"manifest unknown","attempted_at_ms":1775121700000}`,
|
||||
},
|
||||
{
|
||||
name: "runtime container start failed",
|
||||
build: func() (Intent, error) {
|
||||
return NewRuntimeContainerStartFailedIntent(metadata, RuntimeContainerStartFailedPayload{
|
||||
GameID: "game-1",
|
||||
ImageRef: "galaxy/game:1.4.7",
|
||||
ErrorCode: "container_start_failed",
|
||||
ErrorMessage: "OCI runtime create failed",
|
||||
AttemptedAtMs: 1775121700001,
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeRuntimeContainerStartFailed,
|
||||
producer: ProducerRuntimeManager,
|
||||
audienceKind: AudienceKindAdminEmail,
|
||||
payloadJSON: `{"game_id":"game-1","image_ref":"galaxy/game:1.4.7","error_code":"container_start_failed","error_message":"OCI runtime create failed","attempted_at_ms":1775121700001}`,
|
||||
},
|
||||
{
|
||||
name: "runtime start config invalid",
|
||||
build: func() (Intent, error) {
|
||||
return NewRuntimeStartConfigInvalidIntent(metadata, RuntimeStartConfigInvalidPayload{
|
||||
GameID: "game-1",
|
||||
ImageRef: "galaxy/game:1.4.7",
|
||||
ErrorCode: "start_config_invalid",
|
||||
ErrorMessage: "docker network galaxy-net not found",
|
||||
AttemptedAtMs: 1775121700002,
|
||||
})
|
||||
},
|
||||
notificationType: NotificationTypeRuntimeStartConfigInvalid,
|
||||
producer: ProducerRuntimeManager,
|
||||
audienceKind: AudienceKindAdminEmail,
|
||||
payloadJSON: `{"game_id":"game-1","image_ref":"galaxy/game:1.4.7","error_code":"start_config_invalid","error_message":"docker network galaxy-net not found","attempted_at_ms":1775121700002}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -335,6 +383,26 @@ func TestConstructorsRejectInvalidPayloads(t *testing.T) {
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "payload_json.turn_number must be at least 1")
|
||||
|
||||
_, err = NewRuntimeImagePullFailedIntent(defaultMetadata(), RuntimeImagePullFailedPayload{
|
||||
GameID: "game-1",
|
||||
ImageRef: "galaxy/game:1.4.7",
|
||||
ErrorCode: "",
|
||||
ErrorMessage: "manifest unknown",
|
||||
AttemptedAtMs: 1775121700000,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "payload_json.error_code must not be empty")
|
||||
|
||||
_, err = NewRuntimeContainerStartFailedIntent(defaultMetadata(), RuntimeContainerStartFailedPayload{
|
||||
GameID: "game-1",
|
||||
ImageRef: "galaxy/game:1.4.7",
|
||||
ErrorCode: "container_start_failed",
|
||||
ErrorMessage: "OCI runtime create failed",
|
||||
AttemptedAtMs: 0,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "payload_json.attempted_at_ms must be at least 1")
|
||||
}
|
||||
|
||||
func TestDecodeIntentRejectsMissingRequiredTopLevelField(t *testing.T) {
|
||||
|
||||
@@ -127,6 +127,39 @@ type LobbyRaceNameRegistrationDeniedPayload struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// RuntimeImagePullFailedPayload stores the normalized payload for
|
||||
// `runtime.image_pull_failed`. AttemptedAtMs carries Unix milliseconds in
|
||||
// UTC of the failed pull attempt.
|
||||
type RuntimeImagePullFailedPayload struct {
|
||||
GameID string `json:"game_id"`
|
||||
ImageRef string `json:"image_ref"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
AttemptedAtMs int64 `json:"attempted_at_ms"`
|
||||
}
|
||||
|
||||
// RuntimeContainerStartFailedPayload stores the normalized payload for
|
||||
// `runtime.container_start_failed`. AttemptedAtMs carries Unix milliseconds
|
||||
// in UTC of the failed start attempt.
|
||||
type RuntimeContainerStartFailedPayload struct {
|
||||
GameID string `json:"game_id"`
|
||||
ImageRef string `json:"image_ref"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
AttemptedAtMs int64 `json:"attempted_at_ms"`
|
||||
}
|
||||
|
||||
// RuntimeStartConfigInvalidPayload stores the normalized payload for
|
||||
// `runtime.start_config_invalid`. AttemptedAtMs carries Unix milliseconds
|
||||
// in UTC of the rejected start attempt.
|
||||
type RuntimeStartConfigInvalidPayload struct {
|
||||
GameID string `json:"game_id"`
|
||||
ImageRef string `json:"image_ref"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
AttemptedAtMs int64 `json:"attempted_at_ms"`
|
||||
}
|
||||
|
||||
// NewGeoReviewRecommendedIntent builds the admin-email intent published by Geo
|
||||
// Profile Service when a user becomes review-worthy.
|
||||
func NewGeoReviewRecommendedIntent(metadata Metadata, payload GeoReviewRecommendedPayload) (Intent, error) {
|
||||
@@ -226,3 +259,25 @@ func NewLobbyRaceNameRegisteredIntent(metadata Metadata, recipientUserID string,
|
||||
func NewLobbyRaceNameRegistrationDeniedIntent(metadata Metadata, recipientUserID string, payload LobbyRaceNameRegistrationDeniedPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeLobbyRaceNameRegistrationDenied, ProducerGameLobby, AudienceKindUser, []string{recipientUserID}, metadata, payload)
|
||||
}
|
||||
|
||||
// NewRuntimeImagePullFailedIntent builds the administrator-email intent
|
||||
// published by Runtime Manager when a start operation fails because the
|
||||
// engine image cannot be pulled.
|
||||
func NewRuntimeImagePullFailedIntent(metadata Metadata, payload RuntimeImagePullFailedPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeRuntimeImagePullFailed, ProducerRuntimeManager, AudienceKindAdminEmail, nil, metadata, payload)
|
||||
}
|
||||
|
||||
// NewRuntimeContainerStartFailedIntent builds the administrator-email
|
||||
// intent published by Runtime Manager when a start operation fails because
|
||||
// `docker create` or `docker start` returns an error.
|
||||
func NewRuntimeContainerStartFailedIntent(metadata Metadata, payload RuntimeContainerStartFailedPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeRuntimeContainerStartFailed, ProducerRuntimeManager, AudienceKindAdminEmail, nil, metadata, payload)
|
||||
}
|
||||
|
||||
// NewRuntimeStartConfigInvalidIntent builds the administrator-email intent
|
||||
// published by Runtime Manager when start configuration validation rejects
|
||||
// the request (invalid image reference, missing Docker network, unwritable
|
||||
// state directory).
|
||||
func NewRuntimeStartConfigInvalidIntent(metadata Metadata, payload RuntimeStartConfigInvalidPayload) (Intent, error) {
|
||||
return newIntent(NotificationTypeRuntimeStartConfigInvalid, ProducerRuntimeManager, AudienceKindAdminEmail, nil, metadata, payload)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user