fix: align business reports and record task migration progress

This commit is contained in:
Yvan
2026-11-06 00:47:06 +08:00
parent 1bc2082718
commit b1f14bd525
32 changed files with 501 additions and 53 deletions
+18 -1
View File
@@ -5,7 +5,9 @@ package migration
import (
"fmt"
"log/slog"
"strings"
"time"
"gorm.io/gorm"
)
@@ -23,10 +25,20 @@ type record struct {
func (record) TableName() string { return TableName }
func Run(db *gorm.DB, steps []Step) error {
func Run(db *gorm.DB, steps []Step) (runErr error) {
if db == nil {
return fmt.Errorf("database is nil")
}
started := time.Now()
completed := 0
slog.Info("database migrations started", "steps", len(steps))
defer func() {
status := "completed"
if runErr != nil {
status = "failed"
}
slog.Info("database migrations finished", "status", status, "applied", completed, "elapsed_ms", time.Since(started).Milliseconds())
}()
db = db.Session(&gorm.Session{NewDB: true})
if err := repairMigrationTable(db); err != nil {
return err
@@ -66,12 +78,17 @@ func Run(db *gorm.DB, steps []Step) error {
if step.Migrate == nil {
return fmt.Errorf("migration %q has no function", step.ID)
}
stepStarted := time.Now()
slog.Info("database migration started", "migration", step.ID)
if err := step.Migrate(db.Session(&gorm.Session{NewDB: true})); err != nil {
slog.Error("database migration failed", "migration", step.ID, "elapsed_ms", time.Since(stepStarted).Milliseconds(), "error", err)
return fmt.Errorf("apply migration %q: %w", step.ID, err)
}
if err := db.Session(&gorm.Session{NewDB: true}).Create(&record{ID: step.ID}).Error; err != nil {
return fmt.Errorf("record migration %q: %w", step.ID, err)
}
completed++
slog.Info("database migration completed", "migration", step.ID, "elapsed_ms", time.Since(stepStarted).Milliseconds())
}
return nil
}
+63
View File
@@ -0,0 +1,63 @@
package task
import (
"context"
"fmt"
"strings"
"sync"
"unicode/utf8"
)
type outputKey struct{}
// Output collects bounded execution details without changing the method contract.
// A timed-out method may still write, so snapshots and writes must be synchronized.
type Output struct {
mu sync.Mutex
text string
truncated bool
}
func WithOutput(ctx context.Context) (context.Context, *Output) {
output := &Output{}
return context.WithValue(ctx, outputKey{}, output), output
}
// Logf records operational counts and progress. Do not include credentials or
// whole request/response bodies. Outside a task execution it is a no-op.
func Logf(ctx context.Context, format string, args ...any) {
if ctx == nil {
return
}
output, _ := ctx.Value(outputKey{}).(*Output)
if output == nil {
return
}
line := fmt.Sprintf(format, args...)
output.mu.Lock()
defer output.mu.Unlock()
const limit = 3000 // Reserve space for the executor's final status.
if output.truncated {
return
}
remaining := limit - len(output.text)
if len(line)+1 > remaining {
cut := max(0, remaining-1)
for cut > 0 && !utf8.RuneStart(line[cut]) {
cut--
}
line = line[:cut]
output.truncated = true
}
output.text += line + "\n"
}
func (output *Output) String() string {
output.mu.Lock()
defer output.mu.Unlock()
text := strings.TrimSpace(output.text)
if output.truncated {
text += "\n...(过程详情已截断)"
}
return text
}
+35
View File
@@ -0,0 +1,35 @@
package task
import (
"context"
"strings"
"sync"
"testing"
"unicode/utf8"
)
func TestOutputIsBoundedConcurrentAndExecutionLocal(t *testing.T) {
ctx, output := WithOutput(context.Background())
other, separate := WithOutput(context.Background())
Logf(other, "独立任务")
var workers sync.WaitGroup
for i := 0; i < 20; i++ {
workers.Add(1)
go func() {
defer workers.Done()
for j := 0; j < 100; j++ {
Logf(ctx, "%s", strings.Repeat("渠道", 20))
_ = output.String()
}
}()
}
workers.Wait()
text := output.String()
if !utf8.ValidString(text) || len(text) > 3100 || !strings.Contains(text, "已截断") {
t.Fatalf("invalid bounded output: bytes=%d", len(text))
}
if separate.String() != "独立任务" {
t.Fatal("execution details leaked between tasks")
}
Logf(context.Background(), "outside an execution")
}