mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
feat(multiplayer): expose server shutdown acknowledgement
This commit is contained in:
+35
-3
@@ -40,6 +40,9 @@ type ResultSubmitter interface {
|
||||
type ServerRegistrar interface {
|
||||
RegisterServer(context.Context, domain.WorkloadBinding, int, bool, string, time.Time) error
|
||||
}
|
||||
type ServerShutdowner interface {
|
||||
ShutdownServer(context.Context, domain.WorkloadBinding, string, string, time.Time) error
|
||||
}
|
||||
|
||||
type QueueBackend interface {
|
||||
Create(context.Context, string, string, string, domain.QueueSpec, time.Time) (domain.QueueTicket, error)
|
||||
@@ -113,6 +116,7 @@ type Service struct {
|
||||
WorkloadVerify WorkloadVerifier
|
||||
ResultSubmitter ResultSubmitter
|
||||
ServerRegistrar ServerRegistrar
|
||||
ServerShutdowner ServerShutdowner
|
||||
Assignment AssignmentProvider
|
||||
Roster RosterProvider
|
||||
Now func() time.Time
|
||||
@@ -432,7 +436,8 @@ func (s *Service) contractAssignment(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Service) contractServerMutation(w http.ResponseWriter, r *http.Request) {
|
||||
// Unlike contractAssignment, the documented shape here is two segments
|
||||
// (/servers/{serverId}/result, /servers/{serverId}/register) — rejecting
|
||||
// (/servers/{serverId}/result, /servers/{serverId}/register, or
|
||||
// /servers/{serverId}/shutdown) — rejecting
|
||||
// any "/" would 404 every real call. Delegate shape validation to
|
||||
// serverMutation, which already enforces exactly {id}/{result|register}.
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/v1/servers/")
|
||||
@@ -464,7 +469,7 @@ type serverRegistrationRequest struct {
|
||||
|
||||
func (s *Service) serverMutation(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/v1/servers/"), "/")
|
||||
if len(parts) != 2 || parts[0] == "" || (parts[1] != "result" && parts[1] != "register" && parts[1] != "roster") {
|
||||
if len(parts) != 2 || parts[0] == "" || (parts[1] != "result" && parts[1] != "register" && parts[1] != "roster" && parts[1] != "shutdown") {
|
||||
writeError(w, http.StatusNotFound, "not_found")
|
||||
return
|
||||
}
|
||||
@@ -472,7 +477,7 @@ func (s *Service) serverMutation(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed")
|
||||
return
|
||||
}
|
||||
if s.WorkloadVerify == nil || (parts[1] == "result" && s.ResultSubmitter == nil) || (parts[1] == "register" && s.ServerRegistrar == nil) || (parts[1] == "roster" && s.Roster == nil) {
|
||||
if s.WorkloadVerify == nil || (parts[1] == "result" && s.ResultSubmitter == nil) || (parts[1] == "register" && s.ServerRegistrar == nil) || (parts[1] == "roster" && s.Roster == nil) || (parts[1] == "shutdown" && s.ServerShutdowner == nil) {
|
||||
writeError(w, http.StatusServiceUnavailable, "server_unavailable")
|
||||
return
|
||||
}
|
||||
@@ -538,6 +543,33 @@ func (s *Service) serverMutation(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if parts[1] == "shutdown" {
|
||||
var input struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if !decodeBody(w, r, &input) {
|
||||
return
|
||||
}
|
||||
if input.Reason == "" || len(input.Reason) > 96 || strings.ContainsAny(input.Reason, "\r\n\t") {
|
||||
s.logEvent(observability.Event{Event: "server_shutdown", MatchID: binding.MatchID, ServerID: parts[0], Stage: "rejected", OccurredAt: now})
|
||||
writeError(w, http.StatusUnprocessableEntity, "invalid_request")
|
||||
return
|
||||
}
|
||||
if err := s.ServerShutdowner.ShutdownServer(r.Context(), binding, input.Reason, key, now); err != nil {
|
||||
stage := "invalid"
|
||||
if errors.Is(err, domain.ErrConflict) {
|
||||
stage = "conflict"
|
||||
writeError(w, http.StatusConflict, "conflict")
|
||||
} else {
|
||||
writeError(w, http.StatusUnprocessableEntity, "invalid_request")
|
||||
}
|
||||
s.logEvent(observability.Event{Event: "server_shutdown", MatchID: binding.MatchID, ServerID: parts[0], Stage: stage, OccurredAt: now})
|
||||
return
|
||||
}
|
||||
s.logEvent(observability.Event{Event: "server_shutdown", MatchID: binding.MatchID, ServerID: parts[0], Stage: "acknowledged", OccurredAt: now})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
var input resultRequest
|
||||
if !decodeBody(w, r, &input) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user