package ipc import ( "context" "errors" "net" "path/filepath" "testing" "time" ) // A cancelled context has to abort a call that is already in flight. It did not // until V-638: call checked ctx once before sending and then blocked in // roundtrip with no connection deadline, so a daemon that read the frame and // never answered parked the caller for as long as the socket stayed open. // // The server here is that daemon: it accepts, reads nothing, replies nothing. func deafServer(t *testing.T) string { t.Helper() sock := filepath.Join(t.TempDir(), "deaf.sock") ln, err := net.Listen("unix", sock) if err != nil { t.Fatalf("listen: %v", err) } t.Cleanup(func() { _ = ln.Close() }) go func() { for { conn, err := ln.Accept() if err != nil { return } // Hold it open and say nothing. Closed by the listener cleanup. t.Cleanup(func() { _ = conn.Close() }) } }() return sock } func TestClientCancelAbortsAReadInFlight(t *testing.T) { c, err := Dial(deafServer(t)) if err != nil { t.Fatalf("dial: %v", err) } defer c.Close() ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(50 * time.Millisecond) cancel() }() done := make(chan error, 1) go func() { _, err := c.Ping(ctx) done <- err }() select { case err := <-done: // Ping is read-only, so the cancellation is reported as itself rather // than as an ambiguous mutation. if !errors.Is(err, context.Canceled) { t.Errorf("got %v, want context.Canceled", err) } case <-time.After(5 * time.Second): t.Fatal("a cancelled Ping did not return") } } // A mutation cancelled while awaiting the reply may already have committed, so // it is ErrAmbiguousOutcome and never a retry. That split is the invariant // internal/ipc/maperr_test.go's neighbours rest on. func TestClientCancelLeavesAMutationAmbiguous(t *testing.T) { c, err := Dial(deafServer(t)) if err != nil { t.Fatalf("dial: %v", err) } defer c.Close() ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() done := make(chan error, 1) go func() { _, err := c.WriteFact(ctx, WriteFactReq{Key: "water", Value: "drank"}) done <- err }() select { case err := <-done: if !errors.Is(err, ErrAmbiguousOutcome) { t.Errorf("got %v, want ErrAmbiguousOutcome", err) } case <-time.After(5 * time.Second): t.Fatal("a cancelled WriteFact did not return") } } // The deadline itself, with no cancellation: a call on a context with no // deadline used to have no bound at all. This one has one and must respect it. func TestClientDeadlineBoundsACall(t *testing.T) { c, err := Dial(deafServer(t)) if err != nil { t.Fatalf("dial: %v", err) } defer c.Close() ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() start := time.Now() if _, err := c.Ping(ctx); err == nil { t.Fatal("a deaf server answered a Ping") } if elapsed := time.Since(start); elapsed > 3*time.Second { t.Errorf("Ping took %v, want the context deadline to bound it", elapsed) } } // blockingAPI parks Presence until its context is cancelled and records what // cancelled it. Every other method is the unimplemented floor. type blockingAPI struct { UnimplementedCoreAPI entered chan struct{} err chan error } func (b *blockingAPI) Presence(ctx context.Context) (Presence, error) { close(b.entered) <-ctx.Done() b.err <- ctx.Err() return Presence{}, ctx.Err() } // serveConn dispatched under context.Background() until V-638, so Close could // only abandon a dispatch in flight and never tell it to stop. func TestServerCloseCancelsADispatchInFlight(t *testing.T) { api := &blockingAPI{entered: make(chan struct{}), err: make(chan error, 1)} srv, err := Listen(filepath.Join(t.TempDir(), "core.sock"), api) if err != nil { t.Fatalf("listen: %v", err) } served := make(chan struct{}) go func() { _ = srv.Serve(); close(served) }() cli, err := Dial(srv.Path()) if err != nil { t.Fatalf("dial: %v", err) } defer cli.Close() go func() { _, _ = cli.Presence(context.Background()) }() select { case <-api.entered: case <-time.After(5 * time.Second): t.Fatal("the handler was never dispatched") } _ = srv.Close() <-served select { case got := <-api.err: if !errors.Is(got, context.Canceled) { t.Errorf("handler saw %v, want context.Canceled", got) } case <-time.After(5 * time.Second): t.Fatal("Close did not cancel the dispatch") } } // The watchdog closes the conn, and it races the end of the call: a // cancellation landing as the reply arrives can close a conn the call was // already done with. That is survivable either way, because a write to a closed // socket is errWriteLost and errWriteLost re-dials and retries, so this test // passes with or without the drop in roundtrip's defer. What it pins is that // the recovery is real and costs one round trip at most, never an error the // caller sees. func TestClientSurvivesACancelledCall(t *testing.T) { _, _, cli, _ := newServerWithStore(t) for i := 0; i < 20; i++ { ctx, cancel := context.WithCancel(context.Background()) go cancel() // races the reply on purpose _, _ = cli.Ping(ctx) cancel() if _, err := cli.Ping(context.Background()); err != nil { t.Fatalf("call %d after a cancelled one: %v", i, err) } } }