package hostbox import ( "context" "errors" "github.com/carlosframework/platform/internal/hostclaim" "testing" "time" ) // m-9: a replay keeps its uncertain state. Whatever typed refusal the transport // reports, the exact request may already have committed. func TestPlan9Fix2ReplayRefusalStaysUncertain(t *testing.T) { for _, code := range []string{"refused", "changed", "missing", "unsupported", "boom"} { t.Run(code, func(t *testing.T) { client, _ := RemoteJournals(func(_ context.Context, q JournalRequest) (JournalResponse, error) { return JournalResponse{V: 1, ID: q.ID, Error: code}, nil }) q, err := journalPrepare(JournalRequest{Op: "producer"}, time.Now()) if err != nil { t.Fatal(err) } _, err = client.ReplayUncertain(t.Context(), &JournalUncertainError{Request: q}) var uncertain *JournalUncertainError if !errors.As(err, &uncertain) || uncertain.Request.ID != q.ID { t.Fatalf("%s refusal on replay read as definite: %v", code, err) } if errors.Is(err, ErrChanged) || errors.Is(err, ErrTypedInventory) { t.Fatalf("%s refusal on replay leaked a typed refusal: %v", code, err) } }) } } // m-9: success is only a positive reply for this exact request. func TestPlan9Fix2ReplaySuccessNeedsExactReceipt(t *testing.T) { q, err := journalPrepare(JournalRequest{Op: "producer"}, time.Now()) if err != nil { t.Fatal(err) } client, _ := RemoteJournals(func(context.Context, JournalRequest) (JournalResponse, error) { return JournalResponse{V: 1, ID: "ffffffffffffffffffffffffffffffff"}, nil }) var uncertain *JournalUncertainError if _, err = client.ReplayUncertain(t.Context(), &JournalUncertainError{Request: q}); !errors.As(err, &uncertain) { t.Fatalf("a reply for another request confirmed the replay: %v", err) } client, _ = RemoteJournals(func(_ context.Context, r JournalRequest) (JournalResponse, error) { return JournalResponse{V: 1, ID: r.ID}, nil }) if _, err = client.ReplayUncertain(t.Context(), &JournalUncertainError{Request: q}); err != nil { t.Fatalf("exact confirmation refused: %v", err) } } // m-11: a forward jump of the host clock that is later corrected must not // stall new mutations until real time catches up, and must not let a pruned // request execute again. func TestPlan9Fix2ForwardClockJumpThenCorrectionKeepsLiveness(t *testing.T) { s, _, in := journalFixture(t) origin := time.Now() now := origin s.Now = func() time.Time { return now } issue := func(q JournalRequest) JournalRequest { t.Helper() q, err := journalPrepare(q, now) if err != nil { t.Fatal(err) } return q } if _, err := s.Execute(t.Context(), "peer", issue(JournalRequest{Op: "create", Launch: in})); err != nil { t.Fatal(err) } old := issue(JournalRequest{Op: "producer", Target: in.Target, Producer: "old"}) if _, err := s.Execute(t.Context(), "peer", old); err != nil { t.Fatal(err) } now = origin.Add(5 * 24 * time.Hour) if _, err := s.Execute(t.Context(), "peer", issue(JournalRequest{Op: "producer", Target: in.Target, Producer: "jumped"})); err != nil { t.Fatal(err) } var n int if err := s.Launches.db.QueryRow("SELECT count(*) FROM journal_requests WHERE id=?", old.ID).Scan(&n); err != nil || n != 0 { t.Fatalf("jump did not prune the old receipt: %d %v", n, err) } now = origin.Add(2 * time.Minute) if _, err := s.Execute(t.Context(), "peer", issue(JournalRequest{Op: "producer", Target: in.Target, Producer: "corrected"})); err != nil { t.Fatalf("mutation stalled after the clock was corrected: %v", err) } if _, err := s.Execute(t.Context(), "peer", old); !errors.Is(err, ErrJournalUncertain) { t.Fatalf("a pruned request executed again: %v", err) } } // m-2: an admitted writer that never checked probed may still observe the // committed phase when op, kind, session, stamp and generation are unchanged. // The transition itself still cannot skip a step. func TestPlan9Fix2SelectedWorkObservesCommittedAfterMissingProbed(t *testing.T) { work, _, update := plan9D1Probe(t, hostclaim.SessionObserves) if err := plan9D1Write(work); err != nil { t.Fatal(err) } update(hostclaim.SetMovePhase("m1", hostclaim.KindLanding, hostclaim.PhaseLandingProbing, hostclaim.PhaseLandingProbed)) update(hostclaim.KeepWriterSession("m1", hostclaim.SetMovePhase("m1", hostclaim.KindLanding, hostclaim.PhaseLandingProbed, hostclaim.PhaseLandingCommitted))) if err := plan9D1Write(work); err != nil { t.Fatalf("the admitted writer lost its session across an unobserved probed: %v", err) } } func TestPlan9Fix2SelectedWorkStillRefusesIdentityChangeAndBackwardSteps(t *testing.T) { for name, change := range map[string]func(*hostclaim.Claim) error{ "session": func(c *hostclaim.Claim) error { c.Op.Phase = hostclaim.PhaseLandingCommitted c.Holder.Session++ return nil }, "op": func(c *hostclaim.Claim) error { c.Op.Phase = hostclaim.PhaseLandingCommitted c.Op.ID = "m2" return nil }, "gen": func(c *hostclaim.Claim) error { c.Op.Phase = hostclaim.PhaseLandingCommitted c.Holder.Generation = "g2" return nil }, "rollback": func(c *hostclaim.Claim) error { c.Op.Kind, c.Op.Phase = hostclaim.KindRestoring, "releasing" return nil }, } { t.Run(name, func(t *testing.T) { work, _, update := plan9D1Probe(t, hostclaim.SessionObserves) update(change) if err := plan9D1Write(work); err == nil { t.Fatal("observation catch-up kept a changed session") } }) } work, _, update := plan9D1Probe(t, hostclaim.SessionObserves) update(hostclaim.SetMovePhase("m1", hostclaim.KindLanding, hostclaim.PhaseLandingProbing, hostclaim.PhaseLandingProbed)) if err := plan9D1Write(work); err != nil { t.Fatal(err) } update(func(c *hostclaim.Claim) error { c.Op.Phase = hostclaim.PhaseLandingProbing; return nil }) if err := plan9D1Write(work); err == nil { t.Fatal("observation went backward") } } // m-2: the keep guard is transition authority and still refuses a skip. func TestPlan9Fix2KeepWriterSessionStillRefusesASkippedTransition(t *testing.T) { _, store, update := plan9D1Probe(t, hostclaim.SessionObserves) current, err := hostclaim.Read(t.Context(), store, "shop.example") if err != nil { t.Fatal(err) } _, err = hostclaim.Update(t.Context(), store, current, hostclaim.KeepWriterSession("m1", hostclaim.SetMovePhase("m1", hostclaim.KindLanding, hostclaim.PhaseLandingProbing, hostclaim.PhaseLandingCommitted))) if err == nil { t.Fatal("a session-keeping step skipped probed") } _ = update }