package link import ( "context" "errors" "strings" "sync" "testing" "time" ) // A machine that just woke does not wait to be told its link is dead. // // After a resume the socket looks perfectly healthy from inside the process — no error, no close, // because nothing has tried to send anything. Heartbeats discover it twenty or thirty seconds // later, and for that time the node believes it is in a mesh it has left, which is the one state // this design says must never be indistinguishable from being connected. func TestBeingRousedEndsTheCurrentAttemptRatherThanWaitingForATimeout(t *testing.T) { // A link that never returns on its own, which is exactly what a suspended connection is. held := make(chan context.Context, 4) running := func(ctx context.Context) error { held <- ctx <-ctx.Done() return errors.New("the link ended") } rouse := make(chan struct{}, 1) said := &saidSoFar{} ctx, stop := context.WithCancel(context.Background()) defer stop() finished := make(chan error, 1) go func() { finished <- holdWith(ctx, running, said.say, rouse) }() first := <-held select { case <-first.Done(): t.Fatal("the link ended before anything roused it") case <-time.After(50 * time.Millisecond): } rouse <- struct{}{} select { case <-first.Done(): case <-time.After(2 * time.Second): t.Fatal("the machine woke and the link was left running against a socket that is gone") } // And it opens another one rather than stopping. select { case <-held: case <-time.After(5 * time.Second): t.Fatal("the link was dropped and never opened again") } if !strings.Contains(said.all(), "woken or moved") { t.Fatalf("nothing was said about why the link was dropped:\n%s", said.all()) } stop() select { case <-finished: case <-time.After(2 * time.Second): t.Fatal("it did not stop when asked") } } // A machine nothing ever rouses is every machine that does not suspend, and must behave as before. func TestAMachineNothingRousesIsUnaffected(t *testing.T) { attempts := make(chan struct{}, 4) running := func(context.Context) error { attempts <- struct{}{} return errors.New("the link ended") } ctx, stop := context.WithCancel(context.Background()) defer stop() go func() { _ = holdWith(ctx, running, func(string) {}, nil) }() // It keeps trying, which is the behaviour a node with no rouse has always had. for i := 0; i < 2; i++ { select { case <-attempts: case <-time.After(10 * time.Second): t.Fatal("a node with nothing to rouse it stopped reconnecting") } } } type saidSoFar struct { mu sync.Mutex said []string } func (s *saidSoFar) say(line string) { s.mu.Lock() defer s.mu.Unlock() s.said = append(s.said, line) } func (s *saidSoFar) all() string { s.mu.Lock() defer s.mu.Unlock() return strings.Join(s.said, "\n") }