Add ordered setup, reverse rollback and teardown, concurrent services, and managed background workers using only the standard library. Coordinate context cancellation, injectable signals and joined errors within one configurable shutdown deadline. Include race tests, a two-service example, and the accepted lifecycle ADR. Closes #5
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
package lifecycle_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.auvem.com/corestack/lifecycle"
|
|
)
|
|
|
|
func TestRealFlow(t *testing.T) {
|
|
for _, failure := range []bool{false, true} {
|
|
t.Run(fmt.Sprint(failure), func(t *testing.T) {
|
|
signals := make(chan os.Signal, 2)
|
|
app, err := lifecycle.New(lifecycle.Config{Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), Signals: signals, ShutdownTimeout: time.Second})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ready := make(chan struct{}, 3)
|
|
release := make(chan struct{})
|
|
t.Cleanup(func() {
|
|
select {
|
|
case <-release:
|
|
default:
|
|
close(release)
|
|
}
|
|
})
|
|
failed := errors.New("service failed")
|
|
for _, name := range []string{"daemon", "muxer"} {
|
|
err = app.Register(lifecycle.Module{Name: name, Setup: func(ctx context.Context) error {
|
|
if name == "daemon" {
|
|
return app.Go(ctx, "queue", func(ctx context.Context) error { ready <- struct{}{}; <-ctx.Done(); <-release; return ctx.Err() })
|
|
}
|
|
return nil
|
|
}, Run: func(ctx context.Context) error {
|
|
ready <- struct{}{}
|
|
if name == "muxer" && failure {
|
|
<-release
|
|
return failed
|
|
}
|
|
<-ctx.Done()
|
|
return ctx.Err()
|
|
}, Teardown: func(context.Context) error { return nil }})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
done := make(chan error, 1)
|
|
go func() { done <- app.Run(context.Background()) }()
|
|
for range 3 {
|
|
select {
|
|
case <-ready:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("services did not start")
|
|
}
|
|
}
|
|
if !failure {
|
|
signals <- syscall.SIGTERM
|
|
}
|
|
close(release)
|
|
select {
|
|
case err = <-done:
|
|
if failure && !errors.Is(err, failed) || !failure && err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("Run blocked")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Example() {
|
|
signals := make(chan os.Signal, 2)
|
|
app, err := lifecycle.New(lifecycle.Config{Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), Signals: signals})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ready := make(chan struct{}, 2)
|
|
for _, name := range []string{"daemon", "muxer"} {
|
|
err := app.Register(lifecycle.Module{
|
|
Name: name,
|
|
Setup: func(ctx context.Context) error {
|
|
return app.Go(ctx, name+" worker", func(ctx context.Context) error { <-ctx.Done(); return ctx.Err() })
|
|
},
|
|
Run: func(ctx context.Context) error { ready <- struct{}{}; <-ctx.Done(); return ctx.Err() },
|
|
Teardown: func(context.Context) error { return nil },
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
go func() { <-ready; <-ready; signals <- syscall.SIGTERM }()
|
|
fmt.Println(app.Run(context.Background()))
|
|
// Output: <nil>
|
|
}
|