189 lines
5.5 KiB
Go
189 lines
5.5 KiB
Go
package main
|
|
|
|
// The approvals pipe's accept loop (pipeAcceptLoop, approvalpipe.go), driven
|
|
// with a fake pipe so its one invariant is checked off Windows: from the first
|
|
// instance until the service stops, the pipe's name always has one of our
|
|
// instances open, so no other process can create it in between.
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakePipeInstances counts open instances and records every moment one of
|
|
// ours closed with none left open while the loop was still serving.
|
|
type fakePipeInstances struct {
|
|
mu sync.Mutex
|
|
open int
|
|
made int
|
|
gaps []string
|
|
stop <-chan struct{}
|
|
stopping bool
|
|
// script is what each connect returns, in order; after it, connect waits
|
|
// for stop.
|
|
script []error
|
|
connects int
|
|
}
|
|
|
|
func (f *fakePipeInstances) create() (int, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.made++
|
|
f.open++
|
|
return f.made, nil
|
|
}
|
|
|
|
func (f *fakePipeInstances) release(id int) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.open--
|
|
if f.open == 0 && !f.stopping {
|
|
f.gaps = append(f.gaps, fmt.Sprintf("instance %d closed with none of ours open", id))
|
|
}
|
|
}
|
|
|
|
func (f *fakePipeInstances) connect(int) error {
|
|
f.mu.Lock()
|
|
i := f.connects
|
|
f.connects++
|
|
f.mu.Unlock()
|
|
if i < len(f.script) {
|
|
return f.script[i]
|
|
}
|
|
<-f.stop
|
|
f.mu.Lock()
|
|
f.stopping = true
|
|
f.mu.Unlock()
|
|
return errPipeStopped
|
|
}
|
|
|
|
func (f *fakePipeInstances) snapshot() (connects, open int, gaps []string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.connects, f.open, append([]string(nil), f.gaps...)
|
|
}
|
|
|
|
func TestThePipeNameAlwaysHasOneOfOurInstancesOpen(t *testing.T) {
|
|
stop := make(chan struct{})
|
|
// A client that connects and closes at once fails the connect; it can do
|
|
// that on purpose, as often as it likes. Mixed with clients that are served.
|
|
closed := errors.New("the client closed before the connect")
|
|
f := &fakePipeInstances{stop: stop, script: []error{closed, closed, nil, closed, nil, closed}}
|
|
served := make(chan int, 8)
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- pipeAcceptLoop[int](f, stop, 2, time.Millisecond, func(string, ...any) {}, nil, func(id int) { served <- id })
|
|
}()
|
|
for i := 0; i < 2; i++ {
|
|
select {
|
|
case <-served:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the loop did not serve the connected clients")
|
|
}
|
|
}
|
|
for deadline := time.Now().Add(5 * time.Second); ; {
|
|
if n, _, _ := f.snapshot(); n > len(f.script) {
|
|
break // waiting for the next client
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("the loop never came back to wait for the next client")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
close(stop)
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatalf("the loop ended with %v, want nil on stop", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the loop did not end on stop")
|
|
}
|
|
if _, _, gaps := f.snapshot(); len(gaps) > 0 {
|
|
t.Fatalf("the pipe's name was free while the service served it, so another process could have created it:\n %v", gaps)
|
|
}
|
|
// And nothing is left open once the served clients are done.
|
|
for deadline := time.Now().Add(5 * time.Second); ; {
|
|
if _, open, _ := f.snapshot(); open == 0 {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
_, open, _ := f.snapshot()
|
|
t.Fatalf("%d instance(s) still open after stop", open)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestThePipeIsRetriedWithBackoffWhenItsNameIsTaken(t *testing.T) {
|
|
// What each attempt to serve does: fail before it holds the name (another
|
|
// process created it first), or hold the name (ready) and then fail or stop.
|
|
type attempt struct {
|
|
ready bool
|
|
err error
|
|
}
|
|
taken := errors.New("cannot create the pipe: the name is held by another process")
|
|
broke := errors.New("the next instance could not be created")
|
|
attempts := []attempt{{false, taken}, {false, taken}, {false, taken}, {true, broke}, {false, taken}, {true, nil}}
|
|
var n int
|
|
var waits []time.Duration
|
|
var logs []string
|
|
pipeServeRetrying(func(ready func()) error {
|
|
if n >= len(attempts) {
|
|
t.Fatal("served again after it stopped")
|
|
}
|
|
a := attempts[n]
|
|
n++
|
|
if a.ready {
|
|
ready()
|
|
}
|
|
return a.err
|
|
}, func(d time.Duration) bool {
|
|
waits = append(waits, d)
|
|
return true
|
|
}, func(f string, a ...any) { logs = append(logs, fmt.Sprintf(f, a...)) })
|
|
|
|
if n != len(attempts) {
|
|
t.Fatalf("%d attempts, want %d: a failed start must be tried again", n, len(attempts))
|
|
}
|
|
want := []time.Duration{time.Second, 2 * time.Second, 4 * time.Second, time.Second, 2 * time.Second}
|
|
if fmt.Sprint(waits) != fmt.Sprint(want) {
|
|
t.Fatalf("waits %v, want %v (doubling, and starting again once the name was held)", waits, want)
|
|
}
|
|
// Once per series of attempts, and once when it is serving again.
|
|
retrying, again := 0, 0
|
|
for _, l := range logs {
|
|
switch {
|
|
case strings.Contains(l, "retrying"):
|
|
retrying++
|
|
case strings.Contains(l, "serving again"):
|
|
again++
|
|
}
|
|
}
|
|
if retrying != 2 || again != 2 || len(logs) != 4 {
|
|
t.Fatalf("log lines %q: want one per series of failed attempts (2) and one per recovery (2)", logs)
|
|
}
|
|
}
|
|
|
|
func TestThePipeRetryWaitIsCappedAndEndsWithTheService(t *testing.T) {
|
|
var waits []time.Duration
|
|
tries := 0
|
|
pipeServeRetrying(func(func()) error {
|
|
tries++
|
|
return errors.New("the name is held by another process")
|
|
}, func(d time.Duration) bool {
|
|
waits = append(waits, d)
|
|
return len(waits) < 12 // the service stops during the twelfth wait
|
|
}, func(string, ...any) {})
|
|
if tries != 12 {
|
|
t.Fatalf("%d attempts, want 12: none after the service stopped", tries)
|
|
}
|
|
if last := waits[len(waits)-1]; last != pipeRetryMax {
|
|
t.Fatalf("the wait grew to %v, want it capped at %v", last, pipeRetryMax)
|
|
}
|
|
}
|