fix(panel): bed om en planlagt genstart, ikke en der dør med afsenderen
Knappen tog produktionen ned og lod den ligge. Panelets almindelige restart udfører arbejdet mens forespørgslen står åben: den stopper containeren — hvilket slår denne proces ihjel — og panelet, der ser afsenderen forsvinde, når aldrig til den halvdel, der starter igen. Fire minutters nedetid den 18. august, og det kan ikke lappes herfra: enhver knap, der synkront beder om sin egen død, har samme form. safe-restart planlægges i stedet. Panelet svarer "Restart scheduled" og gør arbejdet bagefter for egen regning, så intet afhænger af, at denne proces stadig er i live til at høre svaret. Prøven hang bagefter, og det var prøvens skyld: dens falske panel læste aldrig forespørgslens krop, og der er en nu. En server, der ikke læser hvad den fik, opdager aldrig at afsenderen har lagt på — så httptest ventede på en handler, der aldrig vendte tilbage, og hele binæren løb tør for tid i stedet for den ene forespørgsel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b0f78d1549
commit
6ba013cb12
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,13 +73,26 @@ func (s *Server) handleRestartFromPanel(w http.ResponseWriter, r *http.Request)
|
|||||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/servers/%s/restart", s.cfg.PanelURL, s.cfg.PanelServerID)
|
// safe-restart rather than restart, and the difference is not politeness.
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
|
//
|
||||||
|
// The plain endpoint does its work while the request is open: it stops the
|
||||||
|
// container — which kills this process — and the panel, seeing the caller
|
||||||
|
// vanish, never gets to the starting half. That took production down and left
|
||||||
|
// it down for four minutes on 18 August, and it cannot be fixed from this side:
|
||||||
|
// any button that synchronously asks for its own death has the same shape.
|
||||||
|
//
|
||||||
|
// safe-restart is scheduled. The panel answers "Restart scheduled" and does the
|
||||||
|
// work on its own afterwards, so nothing depends on this process still being
|
||||||
|
// alive to hear the reply.
|
||||||
|
url := fmt.Sprintf("%s/api/servers/%s/safe-restart", s.cfg.PanelURL, s.cfg.PanelServerID)
|
||||||
|
body := strings.NewReader(`{"backup_first":false,"target_id":""}`)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.internal(w, r, "build panel request", err)
|
s.internal(w, r, "build panel request", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+s.cfg.PanelToken)
|
req.Header.Set("Authorization", "Bearer "+s.cfg.PanelToken)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package httpapi
|
package httpapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,6 +16,11 @@ import (
|
|||||||
// request has to end either way, or whatever sits in front answers for it.
|
// request has to end either way, or whatever sits in front answers for it.
|
||||||
func TestAPanelThatNeverAnswersStillEndsTheRequest(t *testing.T) {
|
func TestAPanelThatNeverAnswersStillEndsTheRequest(t *testing.T) {
|
||||||
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Drained first. A real server reads what it was sent, and one that does not
|
||||||
|
// never learns the caller has hung up — httptest.Server.Close then waits for
|
||||||
|
// a handler that will never return, and the whole binary times out instead
|
||||||
|
// of the one request.
|
||||||
|
_, _ = io.Copy(io.Discard, r.Body)
|
||||||
<-r.Context().Done()
|
<-r.Context().Done()
|
||||||
}))
|
}))
|
||||||
defer panel.Close()
|
defer panel.Close()
|
||||||
@@ -56,6 +62,7 @@ func TestAPanelThatNeverAnswersStillEndsTheRequest(t *testing.T) {
|
|||||||
// And a panel that says no must say so in this server's own words, not in a proxy's.
|
// And a panel that says no must say so in this server's own words, not in a proxy's.
|
||||||
func TestAPanelThatRefusesSaysSoAsJSON(t *testing.T) {
|
func TestAPanelThatRefusesSaysSoAsJSON(t *testing.T) {
|
||||||
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, _ = io.Copy(io.Discard, r.Body)
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
}))
|
}))
|
||||||
defer panel.Close()
|
defer panel.Close()
|
||||||
|
|||||||
Reference in New Issue
Block a user