package link import ( "context" "encoding/json" "errors" "io" "log" "testing" "time" amqp "github.com/rabbitmq/amqp091-go" ) type saidTo struct{ acked, nacked, requeued bool } func (a *saidTo) Ack(uint64, bool) error { a.acked = true; return nil } func (a *saidTo) Nack(_ uint64, _ bool, requeue bool) error { a.nacked, a.requeued = true, requeue return nil } func (a *saidTo) Reject(uint64, bool) error { return nil } type heardWith struct{ err error } func (h heardWith) Heard(context.Context, Report) error { return h.err } func aReport(t *testing.T, to *saidTo) amqp.Delivery { t.Helper() body, err := json.Marshal(Report{Node: "anchor", Applied: []string{"store"}}) if err != nil { t.Fatal(err) } return amqp.Delivery{Acknowledger: to, RoutingKey: KeyReport, Body: body} } // A report the store could not take right now goes back to the broker to be asked again; one the // store answered no to is acknowledged, or it would come back for ever (novox/hq issue 082). func TestAReportTheStoreCouldNotTakeIsKeptAndOneItRefusedIsNot(t *testing.T) { quiet := log.New(io.Discard, "", 0) notNow := &saidTo{} s := &Server{listener: heardWith{err: errors.Join(ErrTryAgain, errors.New("starting up"))}, log: quiet, again: 1} s.handleReport(context.Background(), aReport(t, notNow)) if notNow.acked || !notNow.nacked || !notNow.requeued { t.Fatalf("a report the store could not take yet was not handed back to be asked again: %+v", notNow) } refused := &saidTo{} s = &Server{listener: heardWith{err: errors.New("a report named no node")}, log: quiet, again: 1} s.handleReport(context.Background(), aReport(t, refused)) if !refused.acked || refused.nacked { t.Fatalf("a report the store answered no to was not acknowledged, so it would spin: %+v", refused) } recorded := &saidTo{} s = &Server{listener: heardWith{}, log: quiet} s.handleReport(context.Background(), aReport(t, recorded)) if !recorded.acked || recorded.nacked { t.Fatalf("a recorded report was not acknowledged: %+v", recorded) } } // A store that has not come back within the bound is not restarting: the report is let go, loudly, // rather than holding every enrolment and report behind it for ever (novox/hq issue 082, review). func TestAReportIsLetGoOnceTheStoreHasBeenGoneTooLong(t *testing.T) { quiet := log.New(io.Discard, "", 0) s := &Server{listener: heardWith{err: errors.Join(ErrTryAgain, errors.New("connection refused"))}, log: quiet, again: 1, giveUp: time.Millisecond} first := &saidTo{} s.handleReport(context.Background(), aReport(t, first)) if !first.requeued { t.Fatalf("the first failure was not handed back: %+v", first) } time.Sleep(5 * time.Millisecond) later := &saidTo{} s.handleReport(context.Background(), aReport(t, later)) if !later.acked || later.nacked { t.Fatalf("a report past the bound was not let go, so it would hold the queue for ever: %+v", later) } // Let go, and forgotten: the same report arriving fresh starts a new clock. again := &saidTo{} s.handleReport(context.Background(), aReport(t, again)) if !again.requeued { t.Fatalf("a report let go was not forgotten, so its next arrival is given no chance: %+v", again) } } // Shutting down does not wait out the pause. func TestAReportBeingTriedAgainDoesNotHoldUpShutdown(t *testing.T) { quiet := log.New(io.Discard, "", 0) s := &Server{listener: heardWith{err: errors.Join(ErrTryAgain, errors.New("starting up"))}, log: quiet, again: time.Hour} ctx, cancel := context.WithCancel(context.Background()) cancel() done := make(chan struct{}) go func() { s.handleReport(ctx, aReport(t, &saidTo{})); close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("a cancelled context still waited out the pause") } }