// Package mysql stores cron-service scheduler leases and run history. package mysql import ( "context" "database/sql" "strings" "time" _ "github.com/go-sql-driver/mysql" "hyapp/pkg/xerr" ) // Repository owns cron-service metadata only; business data stays in owner services. type Repository struct { db *sql.DB } // TaskRun is the durable audit row for one scheduled batch attempt. type TaskRun struct { RunID string TaskName string AppCode string OwnerNodeID string Status string ClaimedCount int ProcessedCount int SuccessCount int FailureCount int StartedAtMs int64 FinishedAtMs int64 DurationMs int64 ErrorMessage string } // Open creates the cron metadata connection pool and verifies connectivity. func Open(ctx context.Context, dsn string) (*Repository, error) { if strings.TrimSpace(dsn) == "" { return nil, xerr.New(xerr.InvalidArgument, "mysql_dsn is required") } db, err := sql.Open("mysql", dsn) if err != nil { return nil, err } if err := db.PingContext(ctx); err != nil { _ = db.Close() return nil, err } return &Repository{db: db}, nil } // Close releases the cron metadata connection pool. func (r *Repository) Close() error { if r == nil || r.db == nil { return nil } return r.db.Close() } // Ping verifies that cron metadata storage is currently reachable. func (r *Repository) Ping(ctx context.Context) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "cron repository is not configured") } return r.db.PingContext(ctx) } // TryAcquireTaskLease serializes schedule triggers per task and app_code. func (r *Repository) TryAcquireTaskLease(ctx context.Context, taskName string, appCode string, ownerNodeID string, nowMs int64, leaseTTL time.Duration) (bool, error) { if r == nil || r.db == nil { return false, xerr.New(xerr.Unavailable, "cron repository is not configured") } if leaseTTL <= 0 { leaseTTL = 30 * time.Second } leaseUntilMs := nowMs + leaseTTL.Milliseconds() result, err := r.db.ExecContext(ctx, ` INSERT IGNORE INTO cron_task_leases (task_name, app_code, owner_node_id, lease_until_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?) `, taskName, appCode, ownerNodeID, leaseUntilMs, nowMs) if err != nil { return false, err } inserted, err := result.RowsAffected() if err != nil { return false, err } if inserted > 0 { return true, nil } result, err = r.db.ExecContext(ctx, ` UPDATE cron_task_leases SET owner_node_id = ?, lease_until_ms = ?, updated_at_ms = ? WHERE task_name = ? AND app_code = ? AND (lease_until_ms <= ? OR owner_node_id = ?) `, ownerNodeID, leaseUntilMs, nowMs, taskName, appCode, nowMs, ownerNodeID) if err != nil { return false, err } updated, err := result.RowsAffected() if err != nil { return false, err } return updated > 0, nil } // StartTaskRun inserts a running audit row before calling the owner service. func (r *Repository) StartTaskRun(ctx context.Context, run TaskRun) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "cron repository is not configured") } _, err := r.db.ExecContext(ctx, ` INSERT INTO cron_task_runs ( run_id, task_name, app_code, owner_node_id, status, claimed_count, processed_count, success_count, failure_count, started_at_ms, finished_at_ms, duration_ms, error_message ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, '') `, run.RunID, run.TaskName, run.AppCode, run.OwnerNodeID, run.Status, run.ClaimedCount, run.ProcessedCount, run.SuccessCount, run.FailureCount, run.StartedAtMs) return err } // FinishTaskRun records the final outcome for one scheduled batch attempt. func (r *Repository) FinishTaskRun(ctx context.Context, run TaskRun) error { if r == nil || r.db == nil { return xerr.New(xerr.Unavailable, "cron repository is not configured") } _, err := r.db.ExecContext(ctx, ` UPDATE cron_task_runs SET status = ?, claimed_count = ?, processed_count = ?, success_count = ?, failure_count = ?, finished_at_ms = ?, duration_ms = ?, error_message = ? WHERE run_id = ? `, run.Status, run.ClaimedCount, run.ProcessedCount, run.SuccessCount, run.FailureCount, run.FinishedAtMs, run.DurationMs, truncateError(run.ErrorMessage), run.RunID) return err } func truncateError(value string) string { value = strings.TrimSpace(value) if len(value) <= 512 { return value } return value[:512] }