From 55c3f60d1c6e9db9bd69cc273e38527bd8a58a1e Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Mon, 8 Jun 2026 00:19:53 +0800 Subject: [PATCH] refactor: introduce Start/Wait pattern and lifecycle hooks --- CHANGELOG.md | 6 ++++ README.md | 18 +++++++++--- go.mod | 2 +- go.sum | 3 +- starter.go | 83 ++++++++++++++++++++++++++++++++++++++++++---------- 5 files changed, 89 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e9437a..739c85f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog: @go/starter +## v1.5.3 (2026-06-07) +- **重大架构更迭: 引入 Start/Wait 编排模式与生命周期钩子**: + - **废弃 `Run()`**: 彻底移除黑盒式的 `Run()` 方法,改为显式的 `Start()` (解析与非阻塞启动) 与 `Wait()` (阻塞与信号处理) 模式。 + - **生命周期钩子**: `Register` 现返回 `*managedService` 指针,支持链式注册 `OnStarting`, `OnStarted`, `OnStopping`, `OnStopped` 回调。 + - **中间状态编排**: 允许开发者在 `Start()` 之后、`Wait()` 之前执行自定义初始化逻辑(如跨服务的业务对齐)。 + ## v1.5.2 (2026-06-05) - **可观测性升级: 中心化生命周期审计与 TraceID 链路追踪**: - **TraceID 分层**: 为 `starter` 编排层引入了独立的长短 ID 体系。 diff --git a/README.md b/README.md index 6bd7486..eb070da 100644 --- a/README.md +++ b/README.md @@ -45,21 +45,31 @@ func (s *MyService) Reload() error { func main() { starter.SetAppInfo("myapp", "1.0.0") - // Register with priority 1 - starter.Register("myservice", &MyService{}, 1, 0, 0) + // Register with priority 1 and optional hooks + starter.Register("myservice", &MyService{}, 1, 0, 0).OnStarted(func() { + fmt.Println("MyService is ready!") + }) - starter.Run() + if err := starter.Start(); err != nil { + panic(err) + } + + // Custom initialization logic here + + starter.Wait() } ``` ## Interfaces -- **`Service`**: Core lifecycle interface (`Start`, `Stop`, `Health`). +- **`Service`**: Core lifecycle interface (`Start`, `Stop`, `Status`). - **`Reloader`**: Optional interface for services that support `Reload()` (triggered by `SIGHUP`). - **`UserSignalHandler`**: Optional interface for services that handle custom user signals (`SIGUSR1`, `SIGUSR2` or via `kill` command). ## Features +- **Lifecycle Hooks**: `OnStarting`, `OnStarted`, `OnStopping`, `OnStopped` hooks for precise coordination. +- **Start/Wait Pattern**: Replaces `Run()` to allow custom logic between service startup and blocking. - **Tiered Startup/Shutdown**: Concurrent execution within priority levels, serial execution across them. - **Trace ID Propagation**: Automatically generates a shared Trace ID for all services during startup, ensuring log correlation. - **Secure IPC**: Token-based Unix Domain Socket for `status` and `kill` commands. diff --git a/go.mod b/go.mod index 1adf2a9..bade513 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( apigo.cc/go/crypto v1.5.0 apigo.cc/go/id v1.5.0 - apigo.cc/go/log v1.5.4 + apigo.cc/go/log v1.5.5 ) require apigo.cc/go/jsmod v1.5.0 // indirect diff --git a/go.sum b/go.sum index 883f4b1..01b26a5 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,7 @@ apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8= apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA= apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec= apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= -apigo.cc/go/log v1.5.4 h1:LNyU4v09gfcnZOY53ctnXoKzo45FHoEcPR33lk6PBaY= -apigo.cc/go/log v1.5.4/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo= +apigo.cc/go/log v1.5.5 h1:AFU7d7AQxkpgDHl7SnlEwd6yzGSFAlnrrjbrNDQnQHI= apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E= apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk= apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk= diff --git a/starter.go b/starter.go index b102467..d567055 100644 --- a/starter.go +++ b/starter.go @@ -41,6 +41,10 @@ var ( // Starter Logger starterLogger *log.Logger + + // Signal handling + sigs chan os.Signal + cancelCtx context.CancelFunc ) type managedService struct { @@ -49,6 +53,30 @@ type managedService struct { priority int startTimeout time.Duration stopTimeout time.Duration + onStarting []func() + onStarted []func() + onStopping []func() + onStopped []func() +} + +func (ms *managedService) OnStarting(fn func()) *managedService { + ms.onStarting = append(ms.onStarting, fn) + return ms +} + +func (ms *managedService) OnStarted(fn func()) *managedService { + ms.onStarted = append(ms.onStarted, fn) + return ms +} + +func (ms *managedService) OnStopping(fn func()) *managedService { + ms.onStopping = append(ms.onStopping, fn) + return ms +} + +func (ms *managedService) OnStopped(fn func()) *managedService { + ms.onStopped = append(ms.onStopped, fn) + return ms } type command struct { @@ -74,14 +102,16 @@ func getStarterLogger() *log.Logger { } // Register adds a service to be managed by the starter. -func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration) { - services[priority] = append(services[priority], &managedService{ +func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration) *managedService { + ms := &managedService{ Name: name, svc: svc, priority: priority, startTimeout: startTimeout, stopTimeout: stopTimeout, - }) + } + services[priority] = append(services[priority], ms) + return ms } // SetAppInfo sets the application name and version. @@ -100,8 +130,10 @@ func AddCommand(name, desc string, fn func()) { commands[name] = &command{name: name, desc: desc, fn: fn} } -// Run parses arguments and executes the service. -func Run() { +// TODO 使用 Start / Wait 代替 Run,方便在启动后做初始化操作,或者支持注册某服务的 OnStarting / OnStarted / OnStopping / OnStopped 事件 + +// Start parses arguments and starts the services. +func Start() error { flagSet.Usage = showHelp if len(os.Args) > 1 { arg := os.Args[1] @@ -109,36 +141,41 @@ func Run() { // Subcommand detected, parse flags after the command _ = flagSet.Parse(os.Args[2:]) cmd.fn() - return + os.Exit(0) } // Check for help/version switch arg { case "-h", "--help", "help": showHelp() - return + os.Exit(0) case "-v", "--version", "version": fmt.Printf("%s version %s\n", appName, appVersion) - return + os.Exit(0) } } // No starter command, treat all as app flags _ = flagSet.Parse(os.Args[1:]) - runForeground() -} -func runForeground() { // Setup signal handling for graceful shutdown - sigs := make(chan os.Signal, 1) + sigs = make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + var ctx context.Context + ctx, cancelCtx = context.WithCancel(context.Background()) if err := startServices(ctx); err != nil { getStarterLogger().Error(fmt.Sprintf("[starter] start services failed: %v", err)) - os.Exit(1) + return err + } + return nil +} + +// Wait blocks until a termination signal is received. +func Wait() { + if sigs == nil { + return } for { @@ -160,7 +197,9 @@ func runForeground() { break } - cancel() // Trigger context cancellation + if cancelCtx != nil { + cancelCtx() // Trigger context cancellation + } stopServices() getStarterLogger().Info("[starter] shutdown complete") } @@ -194,10 +233,16 @@ func startServices(ctx context.Context) error { // Log using starter's logger (8-byte ID) but include service's trace ID in extra fields getStarterLogger().Info(fmt.Sprintf("service [%s] starting", ms.Name), "trace", serviceTraceId) + for _, fn := range ms.onStarting { + fn() + } if err := ms.svc.Start(sctx, serviceLogger); err != nil { errChan <- fmt.Errorf("service [%s] start error: %w", ms.Name, err) } else { getStarterLogger().Info(fmt.Sprintf("service [%s] started", ms.Name), "trace", serviceTraceId) + for _, fn := range ms.onStarted { + fn() + } } }(ms) } @@ -232,10 +277,16 @@ func stopServices() { defer cancel() } getStarterLogger().Info(fmt.Sprintf("service [%s] stopping", ms.Name)) + for _, fn := range ms.onStopping { + fn() + } if err := ms.svc.Stop(sctx); err != nil { getStarterLogger().Error(fmt.Sprintf("service [%s] stop error: %v", ms.Name, err)) } else { getStarterLogger().Info(fmt.Sprintf("service [%s] stopped", ms.Name)) + for _, fn := range ms.onStopped { + fn() + } } }(ms) }