feat: configure application transports and unify graceful shutdown

This commit is contained in:
2026-09-23 12:49:56 +08:00
parent 59bf04a461
commit 8e18528fc2
18 changed files with 842 additions and 96 deletions

View File

@@ -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) {

View File

@@ -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" {

View 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)
}
}