cbd8077d2c
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.
171 lines
4.4 KiB
Go
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")
|
|
}
|
|
}
|