feat: configure application transports and unify graceful shutdown
This commit is contained in:
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
@@ -41,28 +42,52 @@ func registerRoutes(application *omni.App, users service.UserRepository, logger
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger, err := logx.New(logx.Config{
|
||||
Service: "reference-api",
|
||||
Environment: "local",
|
||||
Format: "json",
|
||||
Writer: os.Stdout,
|
||||
})
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer logger.Close()
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if err := runReference(ctx); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func referenceConfig() (omni.Config, error) {
|
||||
settings, err := omni.LoadConfig("")
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
// Preserve the example's loopback default and legacy address variable.
|
||||
if strings.TrimSpace(os.Getenv("GO_OMNI_HTTP_ADDR")) == "" {
|
||||
settings.Address = strings.TrimSpace(os.Getenv("GO_OMNI_ADDR"))
|
||||
if settings.Address == "" {
|
||||
settings.Address = "127.0.0.1:8080"
|
||||
}
|
||||
}
|
||||
if settings.Log.Service == "" {
|
||||
settings.Log.Service = "reference-api"
|
||||
}
|
||||
if settings.Log.Environment == "" {
|
||||
settings.Log.Environment = "local"
|
||||
}
|
||||
if strings.TrimSpace(os.Getenv("GO_OMNI_LOG_OUTPUT")) == "" {
|
||||
settings.Log.Output = "stdout"
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func runReference(ctx context.Context) error {
|
||||
settings, err := referenceConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger, err := logx.New(settings.Log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logger.Close()
|
||||
logx.SetDefault(logger)
|
||||
users, closeStorage, storage, err := openUsers(ctx, logger)
|
||||
if err != nil {
|
||||
logger.Error("storage initialization failed", "error", err)
|
||||
os.Exit(1)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := closeStorage(); err != nil {
|
||||
logger.Error("storage close failed", "error", err)
|
||||
}
|
||||
}()
|
||||
devToken := strings.TrimSpace(os.Getenv("GO_OMNI_DEV_TOKEN"))
|
||||
if devToken == "" {
|
||||
devToken = "dev-token"
|
||||
@@ -70,16 +95,11 @@ func main() {
|
||||
authenticator := auth.StaticAuthenticator{
|
||||
devToken: {UserID: "dev-user", Roles: []string{"admin"}},
|
||||
}
|
||||
application := newProtectedApp(users, logger, authenticator)
|
||||
address := os.Getenv("GO_OMNI_ADDR")
|
||||
if address == "" {
|
||||
address = "127.0.0.1:8080"
|
||||
}
|
||||
logger.Info("starting reference API", "address", address, "storage", storage)
|
||||
if err := application.RunContext(ctx, address); err != nil {
|
||||
logger.Error("server stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
application := newProtectedApp(users, logger, authenticator, omni.WithConfig(settings))
|
||||
application.OnShutdown(logger.Close)
|
||||
application.OnShutdown(closeStorage)
|
||||
logger.Info("starting reference API", "address", settings.Address, "transport", settings.Transport, "tls", settings.TLS.CertFile != "", "storage", storage)
|
||||
return application.RunContext(ctx)
|
||||
}
|
||||
|
||||
func openUsers(ctx context.Context, logger *logx.Logger) (service.UserRepository, func() error, string, error) {
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestUserCRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func checkUserCRUD(t *testing.T, app *omni.App) {
|
||||
func checkUserCRUD(t *testing.T, app http.Handler) {
|
||||
t.Helper()
|
||||
health := dataAs[map[string]string](t, request(t, app, "GET", "/health", "", 200, "OK"))
|
||||
if health["status"] != "ok" {
|
||||
|
||||
182
examples/reference/transport_test.go
Normal file
182
examples/reference/transport_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
"git.yuntuo.hk/k2safe/go-omni/auth"
|
||||
"git.yuntuo.hk/k2safe/go-omni/examples/reference/repository"
|
||||
"git.yuntuo.hk/k2safe/go-omni/internal/testutil"
|
||||
)
|
||||
|
||||
// networkHandler runs the same business assertions through a real TCP client.
|
||||
// The application handler is only called by its configured server transport.
|
||||
type networkHandler struct {
|
||||
t *testing.T
|
||||
client *http.Client
|
||||
address, scheme, token, alpn string
|
||||
proto int
|
||||
requests *int
|
||||
}
|
||||
|
||||
func (h networkHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.t.Helper()
|
||||
req := r.Clone(r.Context())
|
||||
req.RequestURI = ""
|
||||
req.URL.Scheme, req.URL.Host, req.Host = h.scheme, h.address, h.address
|
||||
if h.token != "" && req.Header.Get("Authorization") == "" {
|
||||
req.Header.Set("Authorization", "Bearer "+h.token)
|
||||
}
|
||||
response, err := h.client.Do(req)
|
||||
if err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if strings.HasPrefix(req.URL.Path, "/api/v1/") {
|
||||
*h.requests++
|
||||
}
|
||||
if response.ProtoMajor != h.proto {
|
||||
h.t.Fatalf("protocol %s, want HTTP/%d", response.Proto, h.proto)
|
||||
}
|
||||
if h.scheme == "https" && (response.TLS == nil || len(response.TLS.VerifiedChains) == 0 || response.TLS.NegotiatedProtocol != h.alpn) {
|
||||
h.t.Fatalf("TLS/ALPN verification failed: %+v", response.TLS)
|
||||
}
|
||||
for key, values := range response.Header {
|
||||
w.Header()[key] = append([]string(nil), values...)
|
||||
}
|
||||
w.WriteHeader(response.StatusCode)
|
||||
if _, err := io.Copy(w, response.Body); err != nil {
|
||||
h.t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type lockedLog struct {
|
||||
sync.Mutex
|
||||
bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *lockedLog) Write(p []byte) (int, error) {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
return b.Buffer.Write(p)
|
||||
}
|
||||
|
||||
func (b *lockedLog) snapshot() string {
|
||||
b.Lock()
|
||||
defer b.Unlock()
|
||||
return b.Buffer.String()
|
||||
}
|
||||
|
||||
func TestBusinessConfiguredTransports(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, transport string
|
||||
tls bool
|
||||
proto int
|
||||
alpn string
|
||||
}{
|
||||
{"nethttp", omni.TransportNetHTTP, false, 1, ""},
|
||||
{"native", omni.TransportNative, false, 1, ""},
|
||||
{"nethttpTLS", omni.TransportNetHTTP, true, 2, "h2"},
|
||||
{"nativeTLS", omni.TransportNative, true, 1, "http/1.1"},
|
||||
{"http2TLS", omni.TransportHTTP2, true, 2, "h2"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cert, key, tlsClient := testutil.CertificateFiles(t)
|
||||
t.Setenv("REFERENCE_TEST_TRANSPORT", test.transport)
|
||||
t.Setenv("REFERENCE_TEST_SHUTDOWN_TIMEOUT", "3s")
|
||||
t.Setenv("REFERENCE_TEST_TLS_CERT_FILE", "")
|
||||
t.Setenv("REFERENCE_TEST_TLS_KEY_FILE", "")
|
||||
if test.tls {
|
||||
t.Setenv("REFERENCE_TEST_TLS_CERT_FILE", cert)
|
||||
t.Setenv("REFERENCE_TEST_TLS_KEY_FILE", key)
|
||||
}
|
||||
settings, err := omni.LoadConfig("REFERENCE_TEST_")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var logs lockedLog
|
||||
logger := testLogger(&logs)
|
||||
application := newProtectedApp(repository.NewMemoryUsers(), logger, auth.StaticAuthenticator{
|
||||
"admin": {UserID: "admin", Roles: []string{"admin"}},
|
||||
"user": {UserID: "user", Roles: []string{"user"}},
|
||||
}, omni.WithConfig(settings))
|
||||
application.OnShutdown(logger.Close)
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- application.ServeContext(ctx, listener) }()
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Error("business server did not stop")
|
||||
listener.Close()
|
||||
}
|
||||
})
|
||||
client := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{TLSClientConfig: tlsClient, ForceAttemptHTTP2: true}}
|
||||
defer client.CloseIdleConnections()
|
||||
scheme := "http"
|
||||
if test.tls {
|
||||
scheme = "https"
|
||||
}
|
||||
requests := 0
|
||||
network := networkHandler{t: t, client: client, address: listener.Addr().String(), scheme: scheme, proto: test.proto, alpn: test.alpn, requests: &requests}
|
||||
request(t, network, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, 401, "UNAUTHENTICATED")
|
||||
requestWithAuthorization(t, network, "DELETE", "/api/v1/users/1", "", "Bearer user", 403, "FORBIDDEN")
|
||||
network.token = "admin"
|
||||
checkUserCRUD(t, network)
|
||||
request(t, network, "POST", "/api/v1/users", `{"name":`, 400, "INVALID_BODY")
|
||||
count := 0
|
||||
for _, line := range strings.Split(strings.TrimSpace(logs.snapshot()), "\n") {
|
||||
var record map[string]any
|
||||
if err := json.Unmarshal([]byte(line), &record); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if record["requestId"] == "reference-test" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
if count < requests {
|
||||
t.Fatalf("missing correlated business logs: %d", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferenceAddressCompatibility(t *testing.T) {
|
||||
t.Setenv("GO_OMNI_TRANSPORT", "native")
|
||||
t.Setenv("GO_OMNI_TLS_CERT_FILE", "")
|
||||
t.Setenv("GO_OMNI_TLS_KEY_FILE", "")
|
||||
t.Setenv("GO_OMNI_HTTP_ADDR", "")
|
||||
t.Setenv("GO_OMNI_ADDR", "")
|
||||
settings, err := referenceConfig()
|
||||
if err != nil || settings.Address != "127.0.0.1:8080" || settings.Transport != "native" {
|
||||
t.Fatalf("default: %+v %v", settings, err)
|
||||
}
|
||||
t.Setenv("GO_OMNI_ADDR", "127.0.0.1:8081")
|
||||
settings, err = referenceConfig()
|
||||
if err != nil || settings.Address != "127.0.0.1:8081" {
|
||||
t.Fatalf("legacy: %+v %v", settings, err)
|
||||
}
|
||||
t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:8082")
|
||||
settings, err = referenceConfig()
|
||||
if err != nil || settings.Address != "127.0.0.1:8082" {
|
||||
t.Fatalf("standard: %+v %v", settings, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user