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) } }