mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-23 18:58:09 +00:00
A module can now register cleanup and have it happen. Until this commit the
process stopped serving and returned; anything a module had set up went down
with the process rather than being taken down.
BeginShutdown is said first, before anything is dismantled. Without it a
configuration reload arriving in this window re-runs AfterResource - rebuilding
the pool and the queue adapter and re-registering consumers - on top of cleanup
that has already run.
The cleanup runs whether or not Shutdown reported an error, which is the whole
reason that error stopped being fatal in the first place: Shutdown fails exactly
when connections were still in flight, and that is when there is most left to
take down.
The two budgets are spent one after the other, so what has to fit inside the
orchestrator's grace period is their sum. `docker stop` allows 10s by default
before SIGKILL; 5+3 leaves room to finish returning. Raising one without
lowering the other buys nothing.
Both halves are tested through the existing subprocess child, which now
registers a BeforeExit callback of its own:
- after a Shutdown that timed out, the callback still runs. Moving the call
into the success branch reports "the BeforeExit callback did not run after
a failed Shutdown".
- a callback that outlasts its budget is abandoned, not awaited. It sleeps
two seconds against a 300ms budget; RunShutdown reports the deadline, the
process exits cleanly inside one second, and the callback's own marker
never appears. Widening the budget to five seconds makes the test time out
waiting for the exit, which is what "awaited" looks like.
Both counter-proofs compile and fail.
329 lines
11 KiB
Go
329 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-admin-team/go-admin-core/v2/config/source/file"
|
|
log "github.com/go-admin-team/go-admin-core/v2/logger"
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk"
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/api"
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/config"
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/pkg"
|
|
"github.com/go-admin-team/go-admin-core/v2/sdk/runtime"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
|
|
"go-admin/app/admin/models"
|
|
"go-admin/app/admin/router"
|
|
"go-admin/app/jobs"
|
|
"go-admin/common/database"
|
|
"go-admin/common/global"
|
|
common "go-admin/common/middleware"
|
|
"go-admin/common/middleware/handler"
|
|
"go-admin/common/storage"
|
|
ext "go-admin/config"
|
|
)
|
|
|
|
var (
|
|
configYml string
|
|
apiCheck bool
|
|
StartCmd = &cobra.Command{
|
|
Use: "server",
|
|
Short: "Start API server",
|
|
Example: "go-admin server -c config/settings.yml",
|
|
SilenceUsage: true,
|
|
PreRun: func(cmd *cobra.Command, args []string) {
|
|
setup()
|
|
},
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return run()
|
|
},
|
|
}
|
|
)
|
|
|
|
var AppRouters = make([]func(), 0)
|
|
|
|
func init() {
|
|
StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
|
|
StartCmd.PersistentFlags().BoolVarP(&apiCheck, "api", "a", false, "Start server with check api data")
|
|
|
|
//注册路由 fixme 其他应用的路由,在本目录新建文件放在init方法
|
|
AppRouters = append(AppRouters, router.InitRouter)
|
|
}
|
|
|
|
func setup() {
|
|
// 注入配置扩展项
|
|
config.ExtendConfig = &ext.ExtConfig
|
|
//1. 读取配置
|
|
config.Setup(
|
|
file.NewSource(file.WithPath(configYml)),
|
|
database.Setup,
|
|
storage.Setup,
|
|
)
|
|
//注册监听函数
|
|
queue := sdk.Runtime.GetQueuePrefix("")
|
|
queue.Register(global.LoginLog, models.SaveLoginLog)
|
|
queue.Register(global.OperateLog, models.SaveOperaLog)
|
|
queue.Register(global.ApiCheck, models.SaveSysApi)
|
|
go queue.Run()
|
|
|
|
usageStr := `starting api server...`
|
|
log.Info(usageStr)
|
|
}
|
|
|
|
func run() error {
|
|
if config.ApplicationConfig.Mode == pkg.ModeProd.String() {
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
// The last point at which a module can still affect how routes are built.
|
|
// It is not the same moment as the before registry, which runStartupHooks
|
|
// drains below - those callbacks run after initRouter has built the engine,
|
|
// not before it.
|
|
sdk.Runtime.RunPhase(runtime.BeforeRouter)
|
|
|
|
initRouter()
|
|
|
|
runStartupHooks()
|
|
|
|
srv := &http.Server{
|
|
Addr: fmt.Sprintf("%s:%d", config.ApplicationConfig.Host, config.ApplicationConfig.Port),
|
|
Handler: sdk.Runtime.GetEngine(),
|
|
ReadTimeout: time.Duration(config.ApplicationConfig.ReadTimeout) * time.Second,
|
|
WriteTimeout: time.Duration(config.ApplicationConfig.WriterTimeout) * time.Second,
|
|
}
|
|
|
|
go func() {
|
|
jobs.InitJob()
|
|
jobs.Setup(sdk.Runtime.GetAllDb())
|
|
|
|
}()
|
|
|
|
if apiCheck {
|
|
var routers = sdk.Runtime.GetRouter()
|
|
q := sdk.Runtime.GetQueuePrefix("")
|
|
mp := make(map[string]interface{})
|
|
mp["List"] = routers
|
|
message, err := sdk.Runtime.GetStreamMessage("", global.ApiCheck, mp)
|
|
if err != nil {
|
|
log.Infof("GetStreamMessage error, %s \n", err.Error())
|
|
//日志报错错误,不中断请求
|
|
} else {
|
|
err = q.Append(message)
|
|
if err != nil {
|
|
log.Infof("Append message error, %s \n", err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Armed before the server starts serving, and well before the readiness
|
|
// banner: a signal arriving between "the process is up" and "the process
|
|
// is listening for signals" reaches the default handler and kills it
|
|
// without any of the shutdown below. That window is the whole reason
|
|
// arming is separate from waiting.
|
|
quit, disarmStopSignals := armStopSignals()
|
|
|
|
if err := startServing(srv, config.SslConfig.Enable, config.SslConfig.Pem, config.SslConfig.KeyStr); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Println(pkg.Red(string(global.LogoContent)))
|
|
tip()
|
|
fmt.Println(pkg.Green("Server run at:"))
|
|
fmt.Printf("- Local: %s://localhost:%d/ \r\n", "http", config.ApplicationConfig.Port)
|
|
fmt.Printf("- Network: %s://%s:%d/ \r\n", "http", pkg.GetLocalHost(), config.ApplicationConfig.Port)
|
|
fmt.Println(pkg.Green("Swagger run at:"))
|
|
fmt.Printf("- Local: http://localhost:%d/swagger/admin/index.html \r\n", config.ApplicationConfig.Port)
|
|
fmt.Printf("- Network: %s://%s:%d/swagger/admin/index.html \r\n", "http", pkg.GetLocalHost(), config.ApplicationConfig.Port)
|
|
fmt.Printf("%s Enter Control + C Shutdown Server \r\n", pkg.GetCurrentTimeStr())
|
|
|
|
<-quit
|
|
// Restored here, not deferred: from this point a second signal must reach
|
|
// the default handler, so a shutdown that hangs can still be interrupted.
|
|
disarmStopSignals()
|
|
|
|
// Said before anything is taken apart. A configuration reload arriving in
|
|
// this window would otherwise re-run AfterResource - rebuilding the pool
|
|
// and the queue adapter, and re-registering consumers - on top of cleanup
|
|
// that has already run.
|
|
sdk.Runtime.BeginShutdown()
|
|
|
|
log.Info("Shutdown Server ... ")
|
|
if err := shutdownServer(srv, shutdownTimeout); err != nil {
|
|
// Not log.Fatal: that is an unconditional os.Exit(1), and Shutdown
|
|
// reports an error exactly when connections were still in flight -
|
|
// which is when the cleanup that follows matters most.
|
|
log.Error("Server Shutdown: ", err)
|
|
}
|
|
|
|
// Runs whether or not the line above reported an error, for that reason.
|
|
if err := runShutdownHooks(cleanupTimeout); err != nil {
|
|
log.Error("Cleanup: ", err)
|
|
}
|
|
log.Info("Server exiting")
|
|
|
|
return nil
|
|
}
|
|
|
|
// shutdownTimeout is how long Shutdown waits for in-flight requests, and
|
|
// cleanupTimeout how long the BeforeExit callbacks get after it.
|
|
//
|
|
// They are consumed one after the other, so the two together are what has to
|
|
// stay inside the orchestrator's grace period: `docker stop` allows 10s by
|
|
// default before it sends SIGKILL, and 5+3 leaves room for the process to
|
|
// finish returning. Raising either without lowering the other buys nothing -
|
|
// the budget that runs out is the orchestrator's.
|
|
const (
|
|
shutdownTimeout = 5 * time.Second
|
|
cleanupTimeout = 3 * time.Second
|
|
)
|
|
|
|
// armStopSignals registers for the stop signals and returns the channel they
|
|
// arrive on together with the function that restores the default disposition.
|
|
//
|
|
// SIGTERM is what actually arrives in production: `docker stop`, a Kubernetes
|
|
// pod deletion and `systemctl stop` all send it, and Go terminates the process
|
|
// immediately for a signal nobody listens for. Registering only os.Interrupt
|
|
// meant every graceful shutdown below the wait was dead code outside a
|
|
// terminal.
|
|
//
|
|
// Registering is separate from waiting so a caller can arm before it announces
|
|
// that it is ready: a signal that arrives between the two is delivered to the
|
|
// default handler, which for both of these means the process dies without
|
|
// running any of this.
|
|
func armStopSignals() (<-chan os.Signal, func()) {
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
|
return quit, func() { signal.Stop(quit) }
|
|
}
|
|
|
|
// startServing binds srv.Addr, hands the listener to srv on its own goroutine,
|
|
// and announces AfterListen.
|
|
//
|
|
// The bind is done here rather than left to ListenAndServe, which binds on the
|
|
// goroutine that serves. That put the failure every deployment actually hits -
|
|
// "address already in use" - on a goroutine nobody was reading, so the banner
|
|
// went on to claim the server was up, and there would be no way to keep
|
|
// AfterListen from announcing a socket that does not exist. A hook there is
|
|
// promised a reachable port; the only way to keep that promise is for the bind
|
|
// to have already happened on this goroutine.
|
|
//
|
|
// AfterListen is announced synchronously. Running it in a goroutine to save the
|
|
// few milliseconds would let it overlap the shutdown: on a fast SIGTERM the
|
|
// cleanup callbacks could finish before the startup ones had.
|
|
func startServing(srv *http.Server, useTLS bool, pem, key string) error {
|
|
ln, err := net.Listen("tcp", srv.Addr)
|
|
if err != nil {
|
|
return errors.Wrap(err, "listen")
|
|
}
|
|
|
|
go func() {
|
|
// 服务连接
|
|
var err error
|
|
if useTLS {
|
|
err = srv.ServeTLS(ln, pem, key)
|
|
} else {
|
|
err = srv.Serve(ln)
|
|
}
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
// Still fatal, as it was. The bind is no longer among the errors
|
|
// that reach here; what is left is a serve that failed after the
|
|
// port was taken, and carrying on would park the process on
|
|
// <-quit with nothing serving. TLS is the one case that can still
|
|
// fail immediately - ServeTLS reads the certificate files - so
|
|
// with ssl enabled AfterListen can run against a server that is
|
|
// already on its way down.
|
|
log.Fatal("listen: ", err)
|
|
}
|
|
}()
|
|
|
|
sdk.Runtime.RunPhase(runtime.AfterListen)
|
|
return nil
|
|
}
|
|
|
|
// shutdownServer stops srv, giving in-flight requests up to timeout to finish.
|
|
//
|
|
// It returns the error instead of exiting on it. A caller that exits here skips
|
|
// its own cleanup, and Shutdown fails precisely when there was something left
|
|
// to clean up after.
|
|
func shutdownServer(srv *http.Server, timeout time.Duration) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
return srv.Shutdown(ctx)
|
|
}
|
|
|
|
// runShutdownHooks runs the BeforeExit callbacks with timeout to share.
|
|
//
|
|
// What the budget bounds is the wait, not the work. When it is gone RunShutdown
|
|
// stops waiting and returns; a callback that never looks at its context carries
|
|
// on until the process exits, and may leave a partial write behind. Go cannot
|
|
// cancel a function that does not check for cancellation, which is why the
|
|
// callbacks are handed a context at all.
|
|
func runShutdownHooks(timeout time.Duration) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
return sdk.Runtime.RunShutdown(ctx)
|
|
}
|
|
|
|
// runStartupHooks runs the router registries and then the before callbacks.
|
|
//
|
|
// The package-level slice runs first and in its existing order, so a fork that
|
|
// only ever appended to AppRouters sees no change at all. The core registry
|
|
// runs second, through RunAppRouters: a module can register through
|
|
// sdk.Runtime.SetAppRouters and no longer has to import this command package -
|
|
// which is a main package's plumbing - just to be routed.
|
|
//
|
|
// The loop over the core registry now lives in core, which is what brings the
|
|
// panic guard and the registration seal with it. RunBefore closes a gap rather
|
|
// than moving one: the open-source edition never executed the before callbacks
|
|
// at all, so SetBefore was accepted and silently ignored. It has to stay ahead
|
|
// of ListenAndServe, because a callback registered WithFatal exits the process
|
|
// and that must not happen to one that is already serving.
|
|
func runStartupHooks() {
|
|
for _, f := range AppRouters {
|
|
f()
|
|
}
|
|
sdk.Runtime.RunAppRouters()
|
|
sdk.Runtime.RunBefore()
|
|
}
|
|
|
|
//var Router runtime.Router
|
|
|
|
func tip() {
|
|
usageStr := `欢迎使用 ` + pkg.Green(`go-admin `+global.Version) + ` 可以使用 ` + pkg.Red(`-h`) + ` 查看命令`
|
|
fmt.Printf("%s \n\n", usageStr)
|
|
}
|
|
|
|
func initRouter() {
|
|
var r *gin.Engine
|
|
h := sdk.Runtime.GetEngine()
|
|
if h == nil {
|
|
h = gin.New()
|
|
sdk.Runtime.SetEngine(h)
|
|
}
|
|
switch h.(type) {
|
|
case *gin.Engine:
|
|
r = h.(*gin.Engine)
|
|
default:
|
|
log.Fatal("not support other engine")
|
|
//os.Exit(-1)
|
|
}
|
|
if config.SslConfig.Enable {
|
|
r.Use(handler.TlsHandler())
|
|
}
|
|
//r.Use(middleware.Metrics())
|
|
r.Use(common.Sentinel()).
|
|
Use(common.RequestId(pkg.TrafficKey)).
|
|
Use(api.SetRequestLogger)
|
|
|
|
common.InitMiddleware(r)
|
|
|
|
}
|