package instancectl import ( "context" "net/http" "strings" "testing" "time" ) // #292, Astra round 2 P1: cancelling the agent's /start does not join // root's start handler, which can still submit its systemd start after a // /stop for the same host has answered, if it has not yet seen the // disconnect. The agent then proves the unit stopped, gives the lease // back, and the late job launches a writer no lease covers. Here the // handler is held before its submit, past the client's cancellation, and // does not see that cancellation (unseenCancel): no start may be submitted // once the /stop has returned. func TestALegacyStartDelayedPastItsCancellationNeverSubmitsAfterAStop(t *testing.T) { host := "late.example.com" unit := UnitName(RoleRun, host) entered, gate := make(chan struct{}), make(chan struct{}) ctl := &fakeCtl{ answers: map[string]string{"show -p InvocationID --value": "inv1\n"}, unseenCancel: true, before: map[string]func(){"set-property": func() { close(entered) <-gate }}, } _, c := newServer(t, ctl) startCtx, cancelStart := context.WithCancel(context.Background()) startDone := make(chan struct{}) go func() { defer close(startDone) req, _ := http.NewRequestWithContext(startCtx, http.MethodPost, "http://unix/start?host="+host+"&role=run&memory_max=536870912", nil) if resp, err := c.Do(req); err == nil { resp.Body.Close() } }() select { case <-entered: case <-time.After(5 * time.Second): t.Fatal("the start handler never reached its tier step") } cancelStart() // the client gives up; the handler has not noticed <-startDone stopped := make(chan int, 1) go func() { resp := post(t, c, "/stop?host="+host+"&role=run") resp.Body.Close() stopped <- resp.StatusCode }() // The held handler goes on past the cancellation it has not seen. time.Sleep(100 * time.Millisecond) close(gate) var status int select { case status = <-stopped: case <-time.After(15 * time.Second): t.Fatal("/stop never returned") } if status != http.StatusNoContent { t.Fatalf("/stop status %d, want 204", status) } stopAt := len(strings.Split(ctl.joined(), "\n")) // Give the held handler every chance to submit late. deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { calls := strings.Split(ctl.joined(), "\n") for _, line := range calls[stopAt:] { if line == "start "+unit { t.Fatalf("a start was submitted after /stop returned; calls:\n%s", ctl.joined()) } } time.Sleep(20 * time.Millisecond) } calls := ctl.joined() stopIdx := strings.Index(calls, "stop "+unit) if stopIdx < 0 { t.Fatalf("the unit was never stopped; calls:\n%s", calls) } if i := strings.LastIndex(calls, "start "+unit); i > stopIdx { t.Fatalf("a start was submitted after the stop; calls:\n%s", calls) } } // A start whose own request was accepted before a /stop for the host is an // earlier request: refused once that stop has run, even if its handler only // gets going afterwards. func TestALegacyStartFromBeforeAStopIsRefused(t *testing.T) { host := "early.example.com" ctl := &fakeCtl{answers: map[string]string{"show -p InvocationID --value": "inv1\n"}} s, _ := newServer(t, ctl) // Stamp two connections in accept order: the start's first. startSeq := s.nextConnSeq() _ = s.nextConnSeq() // the stop's own connection release, err := s.fenceLegacyStarts(context.Background(), host) if err != nil { t.Fatalf("fence: %v", err) } release() if _, _, err := s.beginLegacyStart(context.Background(), host, startSeq); err == nil { t.Fatal("a start accepted before the stop was admitted after it") } if _, finish, err := s.beginLegacyStart(context.Background(), host, s.nextConnSeq()); err != nil { t.Fatalf("a start accepted after the stop was refused: %v", err) } else { finish() } }