You've already forked agentic-coding-workflow
fix(server): propagate shutdown error, http timeouts, ctx-aware migrate
This commit is contained in:
+8
-4
@@ -51,7 +51,7 @@ func New(ctx context.Context, cfg *config.Config, dist embed.FS) (*App, error) {
|
|||||||
log := logger.FromEnv(cfg.Env, nil)
|
log := logger.FromEnv(cfg.Env, nil)
|
||||||
|
|
||||||
// 跑 migrations 必须在开池前;否则首次启动连不存在的表会让 ping 失败。
|
// 跑 migrations 必须在开池前;否则首次启动连不存在的表会让 ping 失败。
|
||||||
if err := db.Migrate(cfg.DB.DSN, "file://migrations"); err != nil {
|
if err := db.Migrate(ctx, cfg.DB.DSN, "file://migrations"); err != nil {
|
||||||
return nil, fmt.Errorf("migrate: %w", err)
|
return nil, fmt.Errorf("migrate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +109,9 @@ func New(ctx context.Context, cfg *config.Config, dist embed.FS) (*App, error) {
|
|||||||
Addr: cfg.HTTP.Addr,
|
Addr: cfg.HTTP.Addr,
|
||||||
Handler: r,
|
Handler: r,
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
|
ReadTimeout: 30 * time.Second,
|
||||||
|
WriteTimeout: 60 * time.Second,
|
||||||
|
IdleTimeout: 120 * time.Second,
|
||||||
}
|
}
|
||||||
return &App{cfg: cfg, log: log, pool: pool, server: srv}, nil
|
return &App{cfg: cfg, log: log, pool: pool, server: srv}, nil
|
||||||
}
|
}
|
||||||
@@ -130,11 +133,12 @@ func (a *App) Run(ctx context.Context) error {
|
|||||||
a.log.Info("shutdown signal received, draining...")
|
a.log.Info("shutdown signal received, draining...")
|
||||||
shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := a.server.Shutdown(shutCtx); err != nil {
|
shutErr := a.server.Shutdown(shutCtx)
|
||||||
a.log.Error("server shutdown", "err", err.Error())
|
if shutErr != nil {
|
||||||
|
a.log.Error("server shutdown", "err", shutErr.Error())
|
||||||
}
|
}
|
||||||
a.pool.Close()
|
a.pool.Close()
|
||||||
return nil
|
return shutErr
|
||||||
case err := <-errCh:
|
case err := <-errCh:
|
||||||
a.pool.Close()
|
a.pool.Close()
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func TestEndToEnd_LoginAndDispatchNotification(t *testing.T) {
|
|||||||
|
|
||||||
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
|
dsn, err := container.ConnectionString(ctx, "sslmode=disable")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, db.Migrate(dsn, "file://../../migrations"))
|
require.NoError(t, db.Migrate(ctx, dsn, "file://../../migrations"))
|
||||||
|
|
||||||
pool, err := db.NewPool(ctx, dsn)
|
pool, err := db.NewPool(ctx, dsn)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
@@ -12,17 +13,33 @@ import (
|
|||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Migrate 跑指定路径下的迁移到最新版本。
|
// Migrate 跑指定路径下的迁移到最新版本。ctx 取消可让本函数提前返回 ctx.Err(),
|
||||||
|
// 但底层 golang-migrate 不接 ctx,已经 in-flight 的 Up() 会继续在后台 goroutine
|
||||||
|
// 中执行直到自然完成(或外部 statement_timeout 触发)。
|
||||||
// migrationsPath 形如 "file://migrations",dsn 为 postgres 连接串。
|
// migrationsPath 形如 "file://migrations",dsn 为 postgres 连接串。
|
||||||
// 当无新迁移可应用时(migrate.ErrNoChange)视为成功,返回 nil。
|
// 当无新迁移可应用时(migrate.ErrNoChange)视为成功,返回 nil。
|
||||||
func Migrate(dsn, migrationsPath string) error {
|
func Migrate(ctx context.Context, dsn, migrationsPath string) error {
|
||||||
m, err := migrate.New(migrationsPath, dsn)
|
m, err := migrate.New(migrationsPath, dsn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("init migrate: %w", err)
|
return fmt.Errorf("init migrate: %w", err)
|
||||||
}
|
}
|
||||||
defer func() { _, _ = m.Close() }()
|
errCh := make(chan error, 1)
|
||||||
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
go func() {
|
||||||
return fmt.Errorf("migrate up: %w", err)
|
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errCh <- nil
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
_, _ = m.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("migrate up: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
// 注意:m.Close 不能在这里调,会与正在跑的 Up() 竞态。让 goroutine 自然结束。
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user