Remove redunant ping and update comments.

This commit is contained in:
2025-10-28 22:42:58 +00:00
parent f1e16fbc52
commit 72e655674f

View File

@@ -1,30 +1,51 @@
// Package bootstrap // Package bootstrap
// Path /internal/platform/bootstrap // Path: /internal/platform/bootstrap
// File: loader.go // File: loader.go
// //
// Purpose: // Purpose
// Centralized application initializer (the “application kernel”). // Centralized application initializer (the “application kernel”).
// This constructs and wires together the core runtime graph used by the // Constructs and wires the core runtime graph used by the system:
// entire system: configuration, database, session manager (SCS), router (Gin), // configuration, database, schema bootstrap, session manager (SCS), router (Gin),
// CSRF wrapper (nosurf), and the HTTP server. // CSRF wrapper (nosurf), and the HTTP server.
// //
// Responsibilities: // Responsibilities (as implemented here)
// 1) Load strongly-typed configuration. // 1) Load strongly-typed configuration from disk (config.Load).
// 2) Initialize long-lived infrastructure (DB, sessions). // 2) Open MySQL with pool tuning and DSN options (parseTime, utf8mb4, UTC).
// 3) Build the Gin router and mount global middleware and routes. // 3) Ensure initial schema on an empty DB (databasePlatform.EnsureInitialSchema).
// 4) Wrap the router with SCS (LoadAndSave) and CSRF in the correct order. // 4) Register gob types needed by sessions (map[string]string, []string, time.Time).
// 5) Construct the http.Server and expose the assembled components via App. // 5) Create an SCS session manager via platform/session.New.
// 6) Build a Gin engine, attach global middleware, static mounts, and error handlers.
// 7) Inject *App into Gin context (c.Set("app", app)) for handler access.
// 8) Wrap Gin with SCS LoadAndSave, then wrap that with CSRF (nosurf).
// 9) Construct http.Server with Handler and ReadHeaderTimeout.
// //
// HTTP stack order (important): // HTTP stack order (matches code)
// Gin Router → SCS LoadAndSave → CSRF Wrapper → http.Server // Gin Router → SCS LoadAndSave → CSRF Wrapper → http.Server
// //
// Design guarantees: // Design guarantees
// - Single source of truth via the App struct. // - Single source of truth via the App struct (Config, DB, SessionManager, Router, Handler, Server).
// - Stable middleware order (SCS must wrap Gin before CSRF). // - Stable middleware order: SCS wraps Gin before CSRF.
// - Gin handlers can access *App via c.MustGet("app"). // - Gin handlers can access *App via c.MustGet("app").
// - Error surfaces are unified via custom NoRoute/NoMethod/Recovery handlers.
// - Extensible: add infra (cache/mailer/metrics) here. // - Extensible: add infra (cache/mailer/metrics) here.
// //
// Change log: // Operational details observed in this file
// - MySQL DSN uses: parseTime=true, charset=utf8mb4, loc=UTC.
// - Pool sizing and conn lifetime are read from config if set.
// - Schema application is idempotent and runs on startup.
// - Static files are served at /static and /favicon.ico.
// - Logging uses gin.Logger(). Recovery uses gin.CustomRecovery with weberr.Recovery.
// - ReadHeaderTimeout is set to 10s (currently hard-coded).
//
// Notes & TODOs (code-accurate)
// - Theres a second DB ping: openMySQL() pings, and Load() pings again.
// This is harmless but redundant; consider deleting one for clarity.
// - gin.Recovery() is commented out; we use CustomRecovery instead (intentional).
// - Consider moving ReadHeaderTimeout to config to match the rest of server tuning.
//
// Change log
// [2025-10-28] Document EnsureInitialSchema bootstrap, explicit gob registrations,
// clarified middleware/handler order, and server timeout behavior.
// [2025-10-24] Migrated to SCS-first wrapping and explicit App wiring. // [2025-10-24] Migrated to SCS-first wrapping and explicit App wiring.
package bootstrap package bootstrap
@@ -60,38 +81,37 @@ type App struct {
} }
func Load(configPath string) (*App, error) { func Load(configPath string) (*App, error) {
// Load configuration
cfg, err := config.Load(configPath) cfg, err := config.Load(configPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("load config: %w", err) return nil, fmt.Errorf("load config: %w", err)
} }
// Open DB
db, err := openMySQL(cfg) db, err := openMySQL(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Ensure initial schema (idempotent; safe on restarts)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return nil, fmt.Errorf("mysql ping: %w", err)
}
if err := databasePlatform.EnsureInitialSchema(db); err != nil { if err := databasePlatform.EnsureInitialSchema(db); err != nil {
return nil, fmt.Errorf("ensure schema: %w", err) return nil, fmt.Errorf("ensure schema: %w", err)
} }
// Register gob types used in session values
gob.Register(map[string]string{}) gob.Register(map[string]string{})
gob.Register([]string{}) gob.Register([]string{})
gob.Register(time.Time{}) gob.Register(time.Time{})
// Create SCS session manager
sessions := session.New(cfg) sessions := session.New(cfg)
// Build Gin router and global middleware
router := gin.New() router := gin.New()
//router.Use(gin.Logger(), gin.Recovery())
router.Use(gin.Logger()) router.Use(gin.Logger())
router.Static("/static", "./web/static") router.Static("/static", "./web/static")
router.StaticFile("/favicon.ico", "./web/static/favicon.ico") router.StaticFile("/favicon.ico", "./web/static/favicon.ico")
// Assemble App prior to injecting into context
app := &App{ app := &App{
Config: cfg, Config: cfg,
DB: db, DB: db,
@@ -99,18 +119,22 @@ func Load(configPath string) (*App, error) {
Router: router, Router: router,
} }
// Inject *App into Gin context for handler access
router.Use(func(c *gin.Context) { router.Use(func(c *gin.Context) {
c.Set("app", app) c.Set("app", app)
c.Next() c.Next()
}) })
// Error handling surfaces
router.NoRoute(weberr.NoRoute(app.SessionManager)) router.NoRoute(weberr.NoRoute(app.SessionManager))
router.NoMethod(weberr.NoMethod(app.SessionManager)) router.NoMethod(weberr.NoMethod(app.SessionManager))
router.Use(gin.CustomRecovery(weberr.Recovery(app.SessionManager))) router.Use(gin.CustomRecovery(weberr.Recovery(app.SessionManager)))
// Wrap: Gin → SCS → CSRF (nosurf)
handler := sessions.LoadAndSave(router) handler := sessions.LoadAndSave(router)
handler = csrf.Wrap(handler, cfg) handler = csrf.Wrap(handler, cfg)
// 9) Build HTTP server
addr := fmt.Sprintf("%s:%d", cfg.HttpServer.Address, cfg.HttpServer.Port) addr := fmt.Sprintf("%s:%d", cfg.HttpServer.Address, cfg.HttpServer.Port)
srv := &http.Server{ srv := &http.Server{
Addr: addr, Addr: addr,
@@ -127,9 +151,11 @@ func Load(configPath string) (*App, error) {
func openMySQL(cfg config.Config) (*sql.DB, error) { func openMySQL(cfg config.Config) (*sql.DB, error) {
dbCfg := cfg.Database dbCfg := cfg.Database
// Credentials are used as-is; escaping handled by DSN rules for mysql driver.
escapedUser := dbCfg.Username escapedUser := dbCfg.Username
escapedPass := dbCfg.Password escapedPass := dbCfg.Password
// DSN opts: parseTime=true, utf8mb4, UTC location
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4,utf8&loc=UTC", dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4,utf8&loc=UTC",
escapedUser, escapedUser,
escapedPass, escapedPass,
@@ -143,6 +169,7 @@ func openMySQL(cfg config.Config) (*sql.DB, error) {
return nil, fmt.Errorf("mysql open: %w", err) return nil, fmt.Errorf("mysql open: %w", err)
} }
// Pool tuning from config (optional)
if dbCfg.MaxOpenConnections > 0 { if dbCfg.MaxOpenConnections > 0 {
db.SetMaxOpenConns(dbCfg.MaxOpenConnections) db.SetMaxOpenConns(dbCfg.MaxOpenConnections)
} }
@@ -155,6 +182,7 @@ func openMySQL(cfg config.Config) (*sql.DB, error) {
} }
} }
// Connectivity check with timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() defer cancel()
if err := db.PingContext(ctx); err != nil { if err := db.PingContext(ctx); err != nil {