bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user