package link import ( "context" "errors" "testing" "time" ) type heardWith struct{ err error } func (h heardWith) Heard(context.Context, Report) error { return h.err } // switchable answers with whatever it is set to — the store away, then back. type switchable struct{ err error } func (h *switchable) Heard(context.Context, Report) error { return h.err } func aReport(node, declared string) Report { return Report{Node: node, Declared: declared, Applied: []string{"store"}} } // A report the store could not take right now is held, unsettled, and recorded when the store is // back; one the store answered no to is acknowledged; one recorded is acknowledged (issue 082, 083). func TestAReportTheStoreCouldNotTakeIsHeldAndOneItRefusedIsNot(t *testing.T) { store := &switchable{err: errors.Join(ErrTryAgain, errors.New("starting up"))} s, in := serving() s.listener = store held := &settled{} s.act(context.Background(), in.sends(t, held, KindReport, aReport("anchor", "d1"))) if !held.unsettled() || len(in.held) != 1 { t.Fatalf("a report the store could not take was not held: %+v, %d held", held, len(in.held)) } store.err = nil in.retries(context.Background(), s) if !held.acked || len(in.held) != 0 { t.Fatalf("a held report was not recorded once the store was back: %+v, %d held", held, len(in.held)) } refused := &settled{} s, in = serving() s.listener = heardWith{err: errors.New("a report named no node")} s.act(context.Background(), in.sends(t, refused, KindReport, aReport("anchor", "d1"))) if !refused.acked || refused.nacked { t.Fatalf("a report the store answered no to was not acknowledged: %+v", refused) } } // A newer report from the same node supersedes one of its reports still held: recorded after the // newer, the older would overwrite what the node is doing now. func TestANewerReportSupersedesAHeldOneFromTheSameNode(t *testing.T) { s, in := serving() s.listener = heardWith{err: errors.Join(ErrTryAgain, errors.New("starting up"))} older, newer, other := &settled{}, &settled{}, &settled{} s.act(context.Background(), in.sends(t, older, KindReport, aReport("anchor", "d1"))) s.act(context.Background(), in.sends(t, other, KindReport, aReport("laptop", "d7"))) s.act(context.Background(), in.sends(t, newer, KindReport, aReport("anchor", "d2"))) if !older.acked { t.Fatalf("the older report was not set aside by the newer: %+v", older) } if !newer.unsettled() || !other.unsettled() || len(in.held) != 2 { t.Fatalf("the newer report and another node's were not both held: newer %+v other %+v, %d held", newer, other, len(in.held)) } } // A store that has not come back within the bound is not restarting: the report is let go, loudly, // rather than held for ever. func TestAReportIsLetGoOnceTheStoreHasBeenGoneTooLong(t *testing.T) { s, in := serving() s.listener = heardWith{err: errors.Join(ErrTryAgain, errors.New("connection refused"))} s.giveUp = time.Millisecond held := &settled{} s.act(context.Background(), in.sends(t, held, KindReport, aReport("anchor", "d1"))) if !held.unsettled() { t.Fatalf("the first failure was not held: %+v", held) } time.Sleep(5 * time.Millisecond) in.retries(context.Background(), s) if !held.acked || len(in.held) != 0 { t.Fatalf("a report past the bound was not let go: %+v, %d held", held, len(in.held)) } }