This commit is contained in:
vet
2026-05-28 00:16:19 +08:00
commit 52446ccf3f
54 changed files with 4617 additions and 0 deletions

103
internal/jobdef/handler.go Normal file
View File

@@ -0,0 +1,103 @@
package jobdef
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"go.mongodb.org/mongo-driver/mongo"
"scheduler-backend/pkg/config"
"scheduler-backend/pkg/log"
)
const logTailLines = 2000
type LogCollector struct {
mu sync.Mutex
lines []string
}
func NewLogCollector() *LogCollector {
return &LogCollector{}
}
func (c *LogCollector) Appendf(format string, args ...any) {
if c == nil {
return
}
c.mu.Lock()
c.lines = append(c.lines, fmt.Sprintf(format, args...))
c.mu.Unlock()
}
func (c *LogCollector) String() string {
if c == nil {
return ""
}
c.mu.Lock()
defer c.mu.Unlock()
return strings.Join(c.lines, "\n")
}
type FlushResult struct {
LogText string
LogFile string
}
func (c *LogCollector) Flush(executionID string) FlushResult {
if c == nil {
return FlushResult{}
}
c.mu.Lock()
lines := c.lines
c.mu.Unlock()
if len(lines) == 0 {
return FlushResult{}
}
if len(lines) <= logTailLines {
return FlushResult{LogText: strings.Join(lines, "\n")}
}
filename := executionID + ".log"
dir := filepath.Join("logs", "joblog")
_ = os.MkdirAll(dir, 0o755)
fullPath := filepath.Join(dir, filename)
_ = os.WriteFile(fullPath, []byte(strings.Join(lines, "\n")+"\n"), 0o644)
tail := lines[len(lines)-logTailLines:]
header := fmt.Sprintf("[完整日志: %s (%d行)]", filename, len(lines))
return FlushResult{
LogText: header + "\n" + strings.Join(tail, "\n"),
LogFile: filename,
}
}
type ExecuteRequest struct {
ExecutionID string `json:"executionID"`
JobID string `json:"jobID"`
TriggerType string `json:"triggerType"`
Params json.RawMessage `json:"params"`
LogCollector *LogCollector `json:"-"`
}
type Runtime struct {
Config config.Config
Logger log.Logger
MetaDB *mongo.Database
BusinessDB *mongo.Database
}
type Handler interface {
Key() string
Name() string
Description() string
Run(ctx context.Context, runtime Runtime, req ExecuteRequest) error
}

62
internal/jobdef/loader.go Normal file
View File

@@ -0,0 +1,62 @@
package jobdef
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"scheduler-backend/internal/store/model"
"scheduler-backend/pkg/log"
)
type JobConfigUpserter interface {
UpsertByHandlerKey(ctx context.Context, item *model.JobConfig) error
}
type configFile struct {
Name string `json:"name"`
HandlerKey string `json:"handlerKey"`
ScheduleType string `json:"scheduleType"`
ScheduleValue string `json:"scheduleValue"`
DefaultParams string `json:"defaultParams"`
}
func SyncJobConfigs(ctx context.Context, dir string, store JobConfigUpserter, logger log.Logger) error {
matches, err := filepath.Glob(filepath.Join(dir, "*.json"))
if err != nil {
return fmt.Errorf("glob job config files: %w", err)
}
if len(matches) == 0 {
logger.Info("no job config files found", "dir", dir)
return nil
}
for _, path := range matches {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
var cfg configFile
if err := json.Unmarshal(data, &cfg); err != nil {
return fmt.Errorf("parse %s: %w", path, err)
}
if cfg.HandlerKey == "" {
return fmt.Errorf("%s: handlerKey is required", path)
}
item := &model.JobConfig{
Name: cfg.Name,
HandlerKey: cfg.HandlerKey,
ScheduleType: cfg.ScheduleType,
ScheduleValue: cfg.ScheduleValue,
DefaultParams: cfg.DefaultParams,
}
if err := store.UpsertByHandlerKey(ctx, item); err != nil {
return fmt.Errorf("upsert %s: %w", cfg.HandlerKey, err)
}
logger.Info("synced job config", "handlerKey", cfg.HandlerKey, "name", cfg.Name, "file", filepath.Base(path))
}
return nil
}

View File

@@ -0,0 +1,28 @@
package jobdef
type Registry struct {
items map[string]Handler
}
func NewRegistry(handlers ...Handler) *Registry {
items := make(map[string]Handler, len(handlers))
for _, handler := range handlers {
items[handler.Key()] = handler
}
return &Registry{items: items}
}
func (r *Registry) Get(key string) (Handler, bool) {
handler, ok := r.items[key]
return handler, ok
}
func (r *Registry) List() []Handler {
result := make([]Handler, 0, len(r.items))
for _, handler := range r.items {
result = append(result, handler)
}
return result
}

View File

@@ -0,0 +1,16 @@
package jobdef
import "testing"
func TestRegistryGetReturnsRegisteredHandler(t *testing.T) {
registry := NewRegistry(SampleHandler{})
handler, ok := registry.Get("sample-handler")
if !ok {
t.Fatal("expected registered handler")
}
if handler.Key() != "sample-handler" {
t.Fatalf("handler key = %q, want sample-handler", handler.Key())
}
}

View File

@@ -0,0 +1,44 @@
package jobdef
import (
"context"
"encoding/json"
"fmt"
"scheduler-backend/internal/s3migrate"
)
type S3MigrateHandler struct{}
func (S3MigrateHandler) Key() string {
return "s3-migrate"
}
func (S3MigrateHandler) Name() string {
return "S3 Migrate"
}
func (S3MigrateHandler) Description() string {
return "Migrate objects between S3-compatible storage engines (MinIO, AWS S3, etc.). Streams files via presigned URLs and updates MongoDB engine records."
}
func (S3MigrateHandler) Run(ctx context.Context, runtime Runtime, req ExecuteRequest) error {
var params s3migrate.Params
if err := json.Unmarshal(req.Params, &params); err != nil {
return fmt.Errorf("parse params: %w", err)
}
logf := func(msg string, args ...any) {
line := fmt.Sprintf(msg, args...)
req.LogCollector.Appendf("%s", line)
runtime.Logger.Info(line, "jobID", req.JobID, "executionID", req.ExecutionID)
}
if err := s3migrate.Run(ctx, runtime.BusinessDB, params, logf); err != nil {
logf("s3-migrate failed: %v", err)
return fmt.Errorf("s3-migrate failed: %w", err)
}
logf("s3-migrate finished")
return nil
}

View File

@@ -0,0 +1,31 @@
package jobdef
import "context"
type SampleHandler struct{}
func (SampleHandler) Key() string {
return "sample-handler"
}
func (SampleHandler) Name() string {
return "Sample Handler"
}
func (SampleHandler) Description() string {
return "Writes a sample startup log for scheduler plumbing."
}
func (SampleHandler) Run(ctx context.Context, runtime Runtime, req ExecuteRequest) error {
req.LogCollector.Appendf("sample handler executed: jobID=%s executionID=%s triggerType=%s", req.JobID, req.ExecutionID, req.TriggerType)
runtime.Logger.Info(
"sample handler executed",
"jobID",
req.JobID,
"executionID",
req.ExecutionID,
"triggerType",
req.TriggerType,
)
return nil
}

View File

@@ -0,0 +1,49 @@
package jobdef
import (
"context"
"encoding/json"
"fmt"
"scheduler-backend/internal/urlrewrite"
)
type URLRewriteHandler struct{}
func (URLRewriteHandler) Key() string {
return "url-rewrite"
}
func (URLRewriteHandler) Name() string {
return "URL Rewrite"
}
func (URLRewriteHandler) Description() string {
return "Batch-rewrite URL prefixes in MongoDB (user avatars, message media, favorites). Supports dry-run, apply, rollback, and cache invalidation."
}
func (URLRewriteHandler) Run(ctx context.Context, runtime Runtime, req ExecuteRequest) error {
var params urlrewrite.Params
if err := json.Unmarshal(req.Params, &params); err != nil {
return fmt.Errorf("parse params: %w", err)
}
logf := func(msg string, args ...any) {
line := fmt.Sprintf(msg, args...)
req.LogCollector.Appendf("%s", line)
runtime.Logger.Info(line, "jobID", req.JobID, "executionID", req.ExecutionID)
}
redisCfg := urlrewrite.RedisConfig{
Addr: runtime.Config.RedisAddr,
Password: runtime.Config.RedisPassword,
}
batchID, err := urlrewrite.Run(ctx, runtime.BusinessDB, params, redisCfg, logf)
if err != nil {
logf("url-rewrite failed: %v", err)
return fmt.Errorf("url-rewrite %s failed (batch_id=%s): %w", params.Mode, batchID, err)
}
logf("url-rewrite finished, mode=%s batch_id=%s", params.Mode, batchID)
return nil
}