fix(backend): secure production startup and migrations

This commit is contained in:
2026-07-22 13:11:30 +08:00
parent 0daea4def9
commit 7980943660
13 changed files with 220 additions and 43 deletions

View File

@@ -67,6 +67,12 @@ func TestAutoMigrateUpgradesLegacyProjectsAndTagsWithoutLosingAssociations(t *te
require.Error(t, database.Exec(`INSERT INTO senlin_agent_tags (project_id, name) VALUES (1, 'UI')`).Error)
}
func TestMigrationAdvisoryLockIsRequiredOnlyForPostgres(t *testing.T) {
require.True(t, migrationAdvisoryLockRequired("postgres"))
require.False(t, migrationAdvisoryLockRequired("sqlite"))
require.False(t, migrationAdvisoryLockRequired("mysql"))
}
func projectIdentifiers(projects []SenlinAgentProject) []string {
result := make([]string, 0, len(projects))
for _, project := range projects {

View File

@@ -1,12 +1,17 @@
package models
import (
"fmt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DBService *gorm.DB
// migrationAdvisoryLockKey scopes the PostgreSQL session lock to SenlinAI schema changes.
const migrationAdvisoryLockKey int64 = 0x53454e4c494e4149
func New(dsn string) error {
// 启用 Gorm 跨驱动错误翻译,业务层无需解析 Postgres 错误文本或 SQLSTATE。
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
@@ -23,6 +28,29 @@ func New(dsn string) error {
}
func AutoMigrate(database *gorm.DB) error {
if !migrationAdvisoryLockRequired(database.Dialector.Name()) {
return autoMigrateUnlocked(database)
}
// Connection pins every migration statement to one PostgreSQL session so the
// advisory lock covers both Gorm's schema work and the versioned data repairs.
return database.Connection(func(connection *gorm.DB) (resultErr error) {
if err := connection.Exec("SELECT pg_advisory_lock(?)", migrationAdvisoryLockKey).Error; err != nil {
return fmt.Errorf("acquire schema migration advisory lock: %w", err)
}
defer func() {
if err := connection.Exec("SELECT pg_advisory_unlock(?)", migrationAdvisoryLockKey).Error; resultErr == nil && err != nil {
resultErr = fmt.Errorf("release schema migration advisory lock: %w", err)
}
}()
return autoMigrateUnlocked(connection)
})
}
func migrationAdvisoryLockRequired(dialect string) bool {
return dialect == "postgres"
}
func autoMigrateUnlocked(database *gorm.DB) error {
if err := database.AutoMigrate(
&SenlinAgentSchemaMigration{},
&SenlinAgentUser{},

View File

@@ -3,20 +3,15 @@
package models
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"senlinai-agent/backend/internal/testutil"
)
func TestPostgresPing(t *testing.T) {
dsn := os.Getenv("TEST_DATABASE_URL")
require.NotEmpty(t, dsn, "TEST_DATABASE_URL is required for integration tests and must point to an isolated database")
database, err := gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true})
require.NoError(t, err)
database := testutil.OpenIsolatedPostgres(t, &gorm.Config{TranslateError: true})
sqlDB, err := database.DB()
require.NoError(t, err)
require.NoError(t, sqlDB.Ping())