init
This commit is contained in:
103
internal/jobdef/handler.go
Normal file
103
internal/jobdef/handler.go
Normal 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
62
internal/jobdef/loader.go
Normal 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
|
||||
}
|
||||
28
internal/jobdef/registry.go
Normal file
28
internal/jobdef/registry.go
Normal 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
|
||||
}
|
||||
16
internal/jobdef/registry_test.go
Normal file
16
internal/jobdef/registry_test.go
Normal 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())
|
||||
}
|
||||
}
|
||||
44
internal/jobdef/s3_migrate_handler.go
Normal file
44
internal/jobdef/s3_migrate_handler.go
Normal 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, ¶ms); 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
|
||||
}
|
||||
31
internal/jobdef/sample_handler.go
Normal file
31
internal/jobdef/sample_handler.go
Normal 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
|
||||
}
|
||||
49
internal/jobdef/url_rewrite_handler.go
Normal file
49
internal/jobdef/url_rewrite_handler.go
Normal 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, ¶ms); 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
|
||||
}
|
||||
Reference in New Issue
Block a user