2026-05-09 21:47:33 +08:00

144 lines
4.3 KiB
Go

// 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]
}