package mysql import ( "context" "database/sql" "errors" "time" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" "git.yuntuo.hk/k2safe/orbit/pulse/service" ) func (s *TaskStore) GetTaskSource(ctx context.Context, t auth.Token, id core.ID) (service.TaskSourceView, error) { out := service.TaskSourceView{TaskID: id} err := s.transaction(ctx, t, "tasks:read", func(tx *sql.Tx, p auth.Permissions) error { var queue core.ID var owner sql.NullString var ownerID, gate, retry *core.ID err := tx.QueryRowContext(ctx, `SELECT queue_id,owner_kind,owner_id,gate_id,retry_of FROM pulse_tasks WHERE namespace_id=? AND id=?`, t.NamespaceID, id).Scan(&queue, &owner, &ownerID, &gate, &retry) if errors.Is(err, sql.ErrNoRows) { return fault.NotFound } if err != nil { return err } if !p.AllowsQueue(queue) { return fault.NotFound } // 来源字段由创建事务固定;当前未实现的门控/所有者不能被误判为普通受理。 if gate != nil || owner.Valid != (ownerID != nil) || owner.Valid && retry != nil { return fault.Unavailable } if owner.Valid { if owner.String != "schedule" { return fault.Unavailable } var source service.TaskScheduleSource // 每条关联均显式限定命名空间、计划、版本与任务队列,不依赖数据库外键。 err := tx.QueryRowContext(ctx, `SELECT o.schedule_id,o.schedule_version,o.id,o.scheduled_at,o.decision,o.covered_until FROM pulse_schedule_occurrences o JOIN pulse_schedules s ON s.namespace_id=o.namespace_id AND s.id=o.schedule_id AND s.queue_id=? JOIN pulse_schedule_versions v ON v.namespace_id=s.namespace_id AND v.schedule_id=s.id AND v.version=o.schedule_version AND v.version<=s.current_version WHERE o.namespace_id=? AND o.task_id=? AND o.schedule_id=? AND JSON_UNQUOTE(JSON_EXTRACT(v.task_template,'$.queue_id'))=?`, queue, t.NamespaceID, id, *ownerID, string(queue)).Scan(&source.ScheduleID, &source.ScheduleVersion, &source.OccurrenceID, &source.ScheduledAt, &source.Decision, &source.CoveredUntil) if errors.Is(err, sql.ErrNoRows) { return fault.Unavailable } if err != nil { return err } if source.ScheduleVersion == 0 || !sourceTimeValid(source.ScheduledAt) || source.CoveredUntil != nil && !sourceTimeValid(*source.CoveredUntil) || source.Decision == "FIRED" && source.CoveredUntil != nil || source.Decision == "COALESCED" && (source.CoveredUntil == nil || !source.CoveredUntil.After(source.ScheduledAt)) || source.Decision != "FIRED" && source.Decision != "COALESCED" { return fault.Unavailable } out.Kind = "SCHEDULE" out.Schedule = &source return nil } var occurrences int if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM pulse_schedule_occurrences WHERE namespace_id=? AND task_id=?`, t.NamespaceID, id).Scan(&occurrences); err != nil { return err } if occurrences != 0 { return fault.Unavailable } if retry == nil { out.Kind = "DIRECT" return nil } var parent core.ID err = tx.QueryRowContext(ctx, `SELECT id FROM pulse_tasks WHERE namespace_id=? AND id=? AND id<>? AND queue_id=? AND owner_kind IS NULL AND owner_id IS NULL AND gate_id IS NULL AND state IN ('FAILED','CANCELLED')`, t.NamespaceID, *retry, id, queue).Scan(&parent) if errors.Is(err, sql.ErrNoRows) { return fault.Unavailable } if err != nil { return err } out.Kind = "MANUAL_RETRY" out.RetryOf = &parent return nil }) if err != nil { return service.TaskSourceView{}, err } return out, nil } func sourceTimeValid(t time.Time) bool { return t.Year() >= 2000 && t.Year() <= 9998 && t.Nanosecond()%1000 == 0 }