fix(felis): 启动失败的服可在面板重试或停止,玩家入服时直接告知启动失败
This commit is contained in:
28 files changed
+775
-75
No files matched your search
@@ -0,0 +1,131 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"felis.lolicon.best/internal/apis/felis/v1alpha1"
|
||||
"felis.lolicon.best/internal/maintenance"
|
||||
)
|
||||
|
||||
// A start that Failed holds desiredState Running already, so the wake that
|
||||
// re-writes Running changes nothing. These pin the two ways out: a person's
|
||||
// wake in the panel starts it over (RetryStart), and a player's join onto one
|
||||
// whose retries are spent is told so (409 start_failed) and never resets them.
|
||||
|
||||
func failedServer(gaveUp bool) *ServerInfo {
|
||||
return &ServerInfo{Name: "survival", AutostartPolicy: "public",
|
||||
DesiredState: string(v1alpha1.DesiredRunning), Phase: string(v1alpha1.PhaseFailed),
|
||||
AutoRestarts: v1alpha1.MaxAutoRestarts, StartGaveUp: gaveUp}
|
||||
}
|
||||
|
||||
func TestWakeOfFailedServerRetriesStart(t *testing.T) {
|
||||
mk := func(info *ServerInfo) (*API, *fakeCluster, *fakeRepo) {
|
||||
repo := newFakeRepo()
|
||||
cl := newFakeCluster()
|
||||
cl.byName["survival"] = info
|
||||
api := newTestAPI(repo, cl)
|
||||
api.External = staticExternal{p: &Principal{UserID: "u1", Role: "user"}}
|
||||
return api, cl, repo
|
||||
}
|
||||
wake := func(api *API) int {
|
||||
return do(api.ExternalHandler(), "POST", "/api/v1/servers/survival/wake", "", nil).Code
|
||||
}
|
||||
|
||||
t.Run("retries spent: the wake starts it over", func(t *testing.T) {
|
||||
api, cl, repo := mk(failedServer(true))
|
||||
if code := wake(api); code != http.StatusAccepted {
|
||||
t.Fatalf("code = %d, want 202", code)
|
||||
}
|
||||
if len(cl.retried) != 1 || cl.retried[0] != "survival" {
|
||||
t.Fatalf("retried = %v, want [survival]", cl.retried)
|
||||
}
|
||||
if len(repo.audits) != 1 || repo.audits[0].Action != "retry_start" {
|
||||
t.Fatalf("audits = %+v, want one retry_start", repo.audits)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inside the backoff: a person asking skips the wait", func(t *testing.T) {
|
||||
api, cl, _ := mk(failedServer(false))
|
||||
if code := wake(api); code != http.StatusAccepted || len(cl.retried) != 1 {
|
||||
t.Fatalf("code = %d retried = %v, want 202 and one retry", code, cl.retried)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stopped: a plain wake", func(t *testing.T) {
|
||||
api, cl, repo := mk(&ServerInfo{Name: "survival", AutostartPolicy: "public",
|
||||
DesiredState: string(v1alpha1.DesiredStopped), Phase: string(v1alpha1.PhaseStopped)})
|
||||
if code := wake(api); code != http.StatusAccepted {
|
||||
t.Fatalf("code = %d, want 202", code)
|
||||
}
|
||||
if len(cl.retried) != 0 || cl.desired["survival"] != v1alpha1.DesiredRunning {
|
||||
t.Fatalf("retried = %v desired = %q, want a plain start", cl.retried, cl.desired["survival"])
|
||||
}
|
||||
if len(repo.audits) != 1 || repo.audits[0].Action != "wake" {
|
||||
t.Fatalf("audits = %+v, want one wake", repo.audits)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("maintenance holds the world: refused, nothing retried", func(t *testing.T) {
|
||||
api, cl, _ := mk(failedServer(true))
|
||||
cl.wakeErr["survival"] = &MaintenanceBusyError{Kind: maintenance.KindRestore}
|
||||
w := do(api.ExternalHandler(), "POST", "/api/v1/servers/survival/wake", "", nil)
|
||||
if w.Code != http.StatusConflict || decodeErr(t, w) != "maintenance_in_progress" {
|
||||
t.Fatalf("code = %d body %s, want 409 maintenance_in_progress", w.Code, w.Body.String())
|
||||
}
|
||||
if len(cl.retried) != 0 {
|
||||
t.Fatalf("retried = %v, want none", cl.retried)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternalWakeOfFailedServer(t *testing.T) {
|
||||
body := `{"mc_uuid":"` + wakeUUID + `"}`
|
||||
|
||||
t.Run("retries spent: 409 start_failed, budget and cooldown untouched", func(t *testing.T) {
|
||||
api, cl := newInternalWakeAPI("public")
|
||||
cl.byName["survival"] = failedServer(true)
|
||||
api.WakeCooldown = time.Minute
|
||||
repo := api.Repo.(*fakeRepo)
|
||||
|
||||
w := internalWake(api, body)
|
||||
if w.Code != http.StatusConflict || decodeErr(t, w) != "start_failed" {
|
||||
t.Fatalf("code = %d body %s, want 409 start_failed", w.Code, w.Body.String())
|
||||
}
|
||||
if len(cl.retried) != 0 {
|
||||
t.Fatalf("a join must never reset the restart budget: retried = %v", cl.retried)
|
||||
}
|
||||
if len(repo.audits) != 0 {
|
||||
t.Fatalf("nothing was woken, so nothing is audited: %+v", repo.audits)
|
||||
}
|
||||
// The owner fixes it and starts it from the panel; the next join is not
|
||||
// held back by a cooldown the refused one never spent.
|
||||
cl.byName["survival"] = &ServerInfo{Name: "survival", AutostartPolicy: "public",
|
||||
DesiredState: string(v1alpha1.DesiredRunning), Phase: string(v1alpha1.PhaseStarting)}
|
||||
if w := internalWake(api, body); w.Code != http.StatusAccepted {
|
||||
t.Fatalf("join after the fix: code = %d body %s, want 202", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inside the backoff: 202, the player waits for the next attempt", func(t *testing.T) {
|
||||
api, cl := newInternalWakeAPI("public")
|
||||
cl.byName["survival"] = failedServer(false)
|
||||
if w := internalWake(api, body); w.Code != http.StatusAccepted {
|
||||
t.Fatalf("code = %d body %s, want 202", w.Code, w.Body.String())
|
||||
}
|
||||
if len(cl.retried) != 0 {
|
||||
t.Fatalf("a join must never reset the restart budget: retried = %v", cl.retried)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("forbidden player: 403 comes first", func(t *testing.T) {
|
||||
api, cl := newInternalWakeAPI("ownerOnly")
|
||||
info := failedServer(true)
|
||||
info.AutostartPolicy = "ownerOnly"
|
||||
cl.byName["survival"] = info
|
||||
if w := internalWake(api, body); w.Code != http.StatusForbidden {
|
||||
t.Fatalf("code = %d, want 403", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user