| index c9031fb8a..07da6011c 100644 |
| --- a/run_test.go |
| +++ b/run_test.go |
| @@ -812,27 +812,63 @@ func TestFollowRunInterrupts(t *testing.T) { |
| }) |
| } |
| |
| -// gatedRun holds one page call open, as a request in flight: entered is |
| -// closed when the call arrives, and the call returns when release is closed. |
| +// gatedRun holds one request open, as a request in flight: entered is closed |
| +// when the held call arrives, and the call returns when release is closed or |
| +// its context is cancelled, as a real request does. |
| type gatedRun struct { |
| *scriptedRun |
| - holdCall int // the page call (1-based) to hold |
| - entered chan struct{} |
| - release chan struct{} |
| - ctxErr error // the held call's context error when it returned |
| + holdPage int // the page call (1-based) to hold, 0 for none |
| + holdCancel bool // hold the first cancel call |
| + holdAck bool // hold the first ack call |
| + entered chan struct{} |
| + release chan struct{} |
| + ctxErr error // the held call's context error when it returned |
| + held int // calls made to a held route, counted when one is held |
| +} |
| + |
| +func (g *gatedRun) hold(ctx context.Context) error { |
| + g.held++ |
| + close(g.entered) |
| + select { |
| + case <-g.release: |
| + case <-ctx.Done(): |
| + } |
| + g.ctxErr = ctx.Err() |
| + return g.ctxErr |
| } |
| |
| func (g *gatedRun) page(ctx context.Context, id string, after int64) (runs.Page, error) { |
| - if g.pagesRead()+1 == g.holdCall { |
| - close(g.entered) |
| - <-g.release |
| - g.ctxErr = ctx.Err() |
| + if g.pagesRead()+1 == g.holdPage { |
| + if err := g.hold(ctx); err != nil { |
| + g.scriptedRun.mu.Lock() |
| + g.scriptedRun.pageCalls++ |
| + g.scriptedRun.mu.Unlock() |
| + return runs.Page{}, err |
| + } |
| } |
| return g.scriptedRun.page(ctx, id, after) |
| } |
| |
| -func newGatedRun(s *scriptedRun, holdCall int) *gatedRun { |
| - return &gatedRun{scriptedRun: s, holdCall: holdCall, entered: make(chan struct{}), release: make(chan struct{})} |
| +func (g *gatedRun) cancel(ctx context.Context, id string) (string, error) { |
| + if g.holdCancel && g.held == 0 { |
| + g.scriptedRun.mu.Lock() |
| + g.scriptedRun.cancels++ |
| + g.scriptedRun.mu.Unlock() |
| + return "", g.hold(ctx) |
| + } |
| + return g.scriptedRun.cancel(ctx, id) |
| +} |
| + |
| +func (g *gatedRun) ack(ctx context.Context, id string, last int64) error { |
| + if g.holdAck && g.held == 0 { |
| + g.scriptedRun.acked = append(g.scriptedRun.acked, last) |
| + return g.hold(ctx) |
| + } |
| + return g.scriptedRun.ack(ctx, id, last) |
| +} |
| + |
| +func newGatedRun(s *scriptedRun) *gatedRun { |
| + return &gatedRun{scriptedRun: s, entered: make(chan struct{}), release: make(chan struct{})} |
| } |
| |
| func awaitFollowRun(t *testing.T, done <-chan int, what string) int { |
| @@ -896,16 +932,34 @@ func TestFollowRunCtrlCOnceCancelsTwiceExits(t *testing.T) { |
| } |
| } |
| |
| -func TestFollowRunCtrlCWhileAPageIsInFlight(t *testing.T) { |
| +func TestFollowRunCtrlCWhileARequestIsInFlight(t *testing.T) { |
| picked := runs.Page{State: "picked"} |
| done := runs.Page{State: "done", Result: &runs.Result{Cancelled: true, Started: time.Unix(1_700_000_000, 0)}} |
| - t.Run("the first is taken when the request returns, before the next poll", func(t *testing.T) { |
| - f := newTermFx() |
| - g := newGatedRun(&scriptedRun{pages: []runs.Page{picked, done}}, 1) |
| + start := func(g *gatedRun, f *termFx) <-chan int { |
| codes := make(chan int, 1) |
| go func() { |
| codes <- followRun(context.Background(), g, f.term(), "shop.example.com", "run_a", time.Time{}) |
| }() |
| + return codes |
| + } |
| + // promptly is how long a second Ctrl-C may take to end a request that |
| + // never answers; the held request would otherwise wait for ever. |
| + const promptly = 500 * time.Millisecond |
| + quick := func(t *testing.T, codes <-chan int) int { |
| + t.Helper() |
| + select { |
| + case code := <-codes: |
| + return code |
| + case <-time.After(promptly): |
| + t.Fatal("the second Ctrl-C did not end the request in flight") |
| + return 0 |
| + } |
| + } |
| + t.Run("the first is taken when the request returns, before the next poll", func(t *testing.T) { |
| + f := newTermFx() |
| + g := newGatedRun(&scriptedRun{pages: []runs.Page{picked, done}}) |
| + g.holdPage = 1 |
| + codes := start(g, f) |
| awaitSignal(t, g.entered, "the first page request was never made") |
| f.sig <- os.Interrupt |
| // The request is not abandoned, and nothing is cancelled until it is back. |
| @@ -929,32 +983,86 @@ func TestFollowRunCtrlCWhileAPageIsInFlight(t *testing.T) { |
| t.Fatalf("stderr %q", got) |
| } |
| }) |
| - t.Run("the second is taken when the request returns, with no further poll", func(t *testing.T) { |
| + t.Run("the second ends a page request in flight at once", func(t *testing.T) { |
| f := newTermFx() |
| f.sig <- os.Interrupt // the first, handled after the first page |
| - g := newGatedRun(&scriptedRun{pages: []runs.Page{picked}}, 2) |
| - codes := make(chan int, 1) |
| - go func() { |
| - codes <- followRun(context.Background(), g, f.term(), "shop.example.com", "run_a", time.Time{}) |
| - }() |
| + g := newGatedRun(&scriptedRun{pages: []runs.Page{picked}}) |
| + g.holdPage = 2 |
| + codes := start(g, f) |
| awaitSignal(t, g.entered, "the second page request was never made") |
| if got := g.cancelsMade(); got != 1 { |
| t.Fatalf("%d cancels before the second page request, want 1", got) |
| } |
| - f.sig <- os.Interrupt |
| - close(g.release) |
| - if code := awaitFollowRun(t, codes, "the second Ctrl-C did not stop waiting"); code != 130 { |
| + f.sig <- os.Interrupt // the request is never released |
| + if code := quick(t, codes); code != 130 { |
| t.Fatalf("exit %d, want 130", code) |
| } |
| + if !errors.Is(g.ctxErr, context.Canceled) { |
| + t.Fatalf("the request in flight ended with %v, want it cut off", g.ctxErr) |
| + } |
| if got := g.pagesRead(); got != 2 { |
| t.Fatalf("%d page requests, want 2: no poll after the second Ctrl-C", got) |
| } |
| if got := g.cancelsMade(); got != 1 { |
| - t.Fatalf("%d cancels, want 1", got) |
| + t.Fatalf("%d cancels, want 1: the one already sent stays sent and is not repeated", got) |
| } |
| if len(g.acked) != 0 { |
| t.Fatalf("acked %v", g.acked) |
| } |
| + if got := f.err.String(); got != "cancelling run run_a. press Ctrl-C again to stop waiting\n" { |
| + t.Fatalf("stderr %q", got) |
| + } |
| + }) |
| + t.Run("both arriving during one page request still cancel, then stop", func(t *testing.T) { |
| + f := newTermFx() |
| + g := newGatedRun(&scriptedRun{pages: []runs.Page{picked}}) |
| + g.holdPage = 1 |
| + codes := start(g, f) |
| + awaitSignal(t, g.entered, "the first page request was never made") |
| + f.sig <- os.Interrupt |
| + f.sig <- os.Interrupt |
| + if code := quick(t, codes); code != 130 { |
| + t.Fatalf("exit %d, want 130", code) |
| + } |
| + if got := g.cancelsMade(); got != 1 { |
| + t.Fatalf("%d cancels, want 1: the first Ctrl-C still asks for the cancel", got) |
| + } |
| + if got := f.err.String(); got != "cancelling run run_a. press Ctrl-C again to stop waiting\n" { |
| + t.Fatalf("stderr %q", got) |
| + } |
| + }) |
| + t.Run("the second ends a cancel request in flight and does not send it again", func(t *testing.T) { |
| + f := newTermFx() |
| + f.sig <- os.Interrupt |
| + g := newGatedRun(&scriptedRun{pages: []runs.Page{picked}}) |
| + g.holdCancel = true |
| + codes := start(g, f) |
| + awaitSignal(t, g.entered, "the cancel request was never made") |
| + f.sig <- os.Interrupt |
| + if code := quick(t, codes); code != 130 { |
| + t.Fatalf("exit %d, want 130", code) |
| + } |
| + if got := g.cancelsMade(); got != 1 { |
| + t.Fatalf("%d cancels, want 1", got) |
| + } |
| + if got := f.err.String(); got != "cancelling run run_a. press Ctrl-C again to stop waiting\n" { |
| + t.Fatalf("stderr %q: a cut-off cancel is not an error to report", got) |
| + } |
| + }) |
| + t.Run("the second ends an ack in flight", func(t *testing.T) { |
| + f := newTermFx() |
| + g := newGatedRun(&scriptedRun{pages: []runs.Page{{State: "done", Result: &runs.Result{LastSeq: 3}}}}) |
| + g.holdAck = true |
| + codes := start(g, f) |
| + awaitSignal(t, g.entered, "the ack request was never made") |
| + f.sig <- os.Interrupt |
| + f.sig <- os.Interrupt |
| + if code := quick(t, codes); code != 130 { |
| + t.Fatalf("exit %d, want 130", code) |
| + } |
| + if got := g.cancelsMade(); got != 1 { |
| + t.Fatalf("%d cancels, want 1", got) |
| + } |
| }) |
| } |
| |