Files
go-omni/app/fatalShutdown_test.go

120 lines
3.1 KiB
Go

package app
import (
"context"
"errors"
"io"
"net"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
"git.yuntuo.hk/k2safe/go-omni/config"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
)
func TestFatalShutdownDrainsAndPreservesCause(t *testing.T) {
for _, transport := range []string{config.TransportNetHTTP, config.TransportNative} {
t.Run(transport, func(t *testing.T) {
app := New(config.WithTransport(transport), config.WithShutdownTimeout(2*time.Second))
control := lifecycle.New(context.Background())
defer control.RequestShutdown(nil)
entered, release := make(chan struct{}), make(chan struct{})
var releaseOnce sync.Once
unblock := func() { releaseOnce.Do(func() { close(release) }) }
defer unblock()
var closed atomic.Int32
closeErr := errors.New("cleanup diagnostic")
app.OnShutdown(func() error { closed.Add(1); return closeErr })
app.GET("/work", func(c *httpx.Context) error {
close(entered)
<-release
if closed.Load() != 0 {
return errors.New("closed during request")
}
return c.OK("done")
})
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() { done <- app.ServeContext(control.Context(), listener) }()
requestDone := make(chan error, 1)
client := http.Client{Timeout: 3 * time.Second}
go func() {
res, err := client.Get("http://" + listener.Addr().String() + "/work")
if err == nil {
_, err = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
if res.StatusCode != 200 {
err = errors.New("request failed")
}
}
requestDone <- err
}()
select {
case <-entered:
case <-time.After(3 * time.Second):
t.Fatal("request never started")
}
fatal := errors.New("lease lost")
control.RequestShutdown(fatal)
select {
case err := <-done:
t.Fatal("did not drain", err)
case <-time.After(30 * time.Millisecond):
}
if closed.Load() != 0 {
t.Fatal("closed dependency before drain")
}
unblock()
select {
case err := <-requestDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(3 * time.Second):
t.Fatal("request stuck")
}
select {
case err := <-done:
if !errors.Is(err, fatal) || !errors.Is(err, closeErr) {
t.Fatal("lost cause", err)
}
case <-time.After(3 * time.Second):
t.Fatal("shutdown stuck")
}
_ = app.Close()
if closed.Load() != 1 {
t.Fatal("cleanup repeated")
}
})
}
}
func TestCanceledStartupPreservesFault(t *testing.T) {
fatal := errors.New("dependency failed during startup")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(fatal)
app := New()
if err := app.RunContext(ctx); !errors.Is(err, fatal) {
t.Fatal(err)
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
if err = New().ServeContext(ctx, listener); !errors.Is(err, fatal) {
t.Fatal(err)
}
normal, stop := context.WithCancel(context.Background())
stop()
if err = New().RunContext(normal); err != nil {
t.Fatal("normal stop failed", err)
}
}