refactor: introduce Start/Wait pattern and lifecycle hooks

This commit is contained in:
AI Engineer 2026-06-08 00:19:53 +08:00
parent 5002b1d9fa
commit 55c3f60d1c
5 changed files with 89 additions and 23 deletions

View File

@ -1,5 +1,11 @@
# Changelog: @go/starter # 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) ## v1.5.2 (2026-06-05)
- **可观测性升级: 中心化生命周期审计与 TraceID 链路追踪**: - **可观测性升级: 中心化生命周期审计与 TraceID 链路追踪**:
- **TraceID 分层**: 为 `starter` 编排层引入了独立的长短 ID 体系。 - **TraceID 分层**: 为 `starter` 编排层引入了独立的长短 ID 体系。

View File

@ -45,21 +45,31 @@ func (s *MyService) Reload() error {
func main() { func main() {
starter.SetAppInfo("myapp", "1.0.0") starter.SetAppInfo("myapp", "1.0.0")
// Register with priority 1 // Register with priority 1 and optional hooks
starter.Register("myservice", &MyService{}, 1, 0, 0) 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 ## 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`). - **`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). - **`UserSignalHandler`**: Optional interface for services that handle custom user signals (`SIGUSR1`, `SIGUSR2` or via `kill` command).
## Features ## 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. - **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. - **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. - **Secure IPC**: Token-based Unix Domain Socket for `status` and `kill` commands.

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.25.0
require ( require (
apigo.cc/go/crypto v1.5.0 apigo.cc/go/crypto v1.5.0
apigo.cc/go/id 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 require apigo.cc/go/jsmod v1.5.0 // indirect

3
go.sum
View File

@ -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/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 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= 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.5 h1:AFU7d7AQxkpgDHl7SnlEwd6yzGSFAlnrrjbrNDQnQHI=
apigo.cc/go/log v1.5.4/go.mod h1:Djy+I5aLhGB/EjwRz4KHqkVEz584IAD55FAFiIfInuo=
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E= 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/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk= apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=

View File

@ -41,6 +41,10 @@ var (
// Starter Logger // Starter Logger
starterLogger *log.Logger starterLogger *log.Logger
// Signal handling
sigs chan os.Signal
cancelCtx context.CancelFunc
) )
type managedService struct { type managedService struct {
@ -49,6 +53,30 @@ type managedService struct {
priority int priority int
startTimeout time.Duration startTimeout time.Duration
stopTimeout 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 { type command struct {
@ -74,14 +102,16 @@ func getStarterLogger() *log.Logger {
} }
// Register adds a service to be managed by the starter. // Register adds a service to be managed by the starter.
func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration) { func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration) *managedService {
services[priority] = append(services[priority], &managedService{ ms := &managedService{
Name: name, Name: name,
svc: svc, svc: svc,
priority: priority, priority: priority,
startTimeout: startTimeout, startTimeout: startTimeout,
stopTimeout: stopTimeout, stopTimeout: stopTimeout,
}) }
services[priority] = append(services[priority], ms)
return ms
} }
// SetAppInfo sets the application name and version. // 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} commands[name] = &command{name: name, desc: desc, fn: fn}
} }
// Run parses arguments and executes the service. // TODO 使用 Start / Wait 代替 Run方便在启动后做初始化操作或者支持注册某服务的 OnStarting / OnStarted / OnStopping / OnStopped 事件
func Run() {
// Start parses arguments and starts the services.
func Start() error {
flagSet.Usage = showHelp flagSet.Usage = showHelp
if len(os.Args) > 1 { if len(os.Args) > 1 {
arg := os.Args[1] arg := os.Args[1]
@ -109,36 +141,41 @@ func Run() {
// Subcommand detected, parse flags after the command // Subcommand detected, parse flags after the command
_ = flagSet.Parse(os.Args[2:]) _ = flagSet.Parse(os.Args[2:])
cmd.fn() cmd.fn()
return os.Exit(0)
} }
// Check for help/version // Check for help/version
switch arg { switch arg {
case "-h", "--help", "help": case "-h", "--help", "help":
showHelp() showHelp()
return os.Exit(0)
case "-v", "--version", "version": case "-v", "--version", "version":
fmt.Printf("%s version %s\n", appName, appVersion) fmt.Printf("%s version %s\n", appName, appVersion)
return os.Exit(0)
} }
} }
// No starter command, treat all as app flags // No starter command, treat all as app flags
_ = flagSet.Parse(os.Args[1:]) _ = flagSet.Parse(os.Args[1:])
runForeground()
}
func runForeground() {
// Setup signal handling for graceful shutdown // 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) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2)
ctx, cancel := context.WithCancel(context.Background()) var ctx context.Context
defer cancel() ctx, cancelCtx = context.WithCancel(context.Background())
if err := startServices(ctx); err != nil { if err := startServices(ctx); err != nil {
getStarterLogger().Error(fmt.Sprintf("[starter] start services failed: %v", err)) 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 { for {
@ -160,7 +197,9 @@ func runForeground() {
break break
} }
cancel() // Trigger context cancellation if cancelCtx != nil {
cancelCtx() // Trigger context cancellation
}
stopServices() stopServices()
getStarterLogger().Info("[starter] shutdown complete") 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 // 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) 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 { if err := ms.svc.Start(sctx, serviceLogger); err != nil {
errChan <- fmt.Errorf("service [%s] start error: %w", ms.Name, err) errChan <- fmt.Errorf("service [%s] start error: %w", ms.Name, err)
} else { } else {
getStarterLogger().Info(fmt.Sprintf("service [%s] started", ms.Name), "trace", serviceTraceId) getStarterLogger().Info(fmt.Sprintf("service [%s] started", ms.Name), "trace", serviceTraceId)
for _, fn := range ms.onStarted {
fn()
}
} }
}(ms) }(ms)
} }
@ -232,10 +277,16 @@ func stopServices() {
defer cancel() defer cancel()
} }
getStarterLogger().Info(fmt.Sprintf("service [%s] stopping", ms.Name)) getStarterLogger().Info(fmt.Sprintf("service [%s] stopping", ms.Name))
for _, fn := range ms.onStopping {
fn()
}
if err := ms.svc.Stop(sctx); err != nil { if err := ms.svc.Stop(sctx); err != nil {
getStarterLogger().Error(fmt.Sprintf("service [%s] stop error: %v", ms.Name, err)) getStarterLogger().Error(fmt.Sprintf("service [%s] stop error: %v", ms.Name, err))
} else { } else {
getStarterLogger().Info(fmt.Sprintf("service [%s] stopped", ms.Name)) getStarterLogger().Info(fmt.Sprintf("service [%s] stopped", ms.Name))
for _, fn := range ms.onStopped {
fn()
}
} }
}(ms) }(ms)
} }