Files
Maven/internal/ipc/cancel_test.go
T
claude cbd8077d2c a request context on the ipc server, cancelled by Close (V-638)
serveConn dispatched under context.Background(), so a dispatch in flight
during shutdown could not be told to stop and closeGrace could only
abandon it. The server now carries a context, Close cancels it, and each
conn derives its own so nothing outlives the connection.

A zero-value Server built outside Listen falls back to Background; two
wiring tests do that.

Client.Close read c.conn with no lock while roundtrip re-dialed and
dropped it, which -race caught on the new test. The conn field now has
its own mutex, held only across a read or an assignment, so Close and
the watchdog reach the connection without queueing behind the call they
are interrupting.
2026-08-06 22:59:02 +04:00

171 lines
4.4 KiB
Go

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