feat: add scoped task source tracing and SDK examples

This commit is contained in:
2026-09-17 22:40:42 +08:00
parent 7641423f9d
commit 2b8213a4f7
26 changed files with 760 additions and 9 deletions

68
sdk/go/task_source.go Normal file
View File

@@ -0,0 +1,68 @@
package orbit
import (
"context"
"errors"
"time"
)
// TaskSource 只读的直接来源,不遍历整条重试链,也不授予来源资源管理权限。
type TaskSource struct {
// TaskID 本次已授权查询的任务标识。
TaskID string `json:"task_id"`
// Kind 当前支持DIRECT、MANUAL_RETRY、SCHEDULE;分别是普通受理、人工派生、计划触发。
Kind string `json:"kind"`
// RetryOf 人工重试的直接原任务,限同一命名空间及队列;其他种类省略。
RetryOf *string `json:"retry_of,omitempty"`
// Schedule 计划触发的固定来源,仅SCHEDULE返回,不包含任务模板或计划配置。
Schedule *TaskScheduleSource `json:"schedule,omitempty"`
}
// TaskScheduleSource 关联已生成任务的触发记录及当时的不可变计划版本。
type TaskScheduleSource struct {
// ScheduleID 原计划标识,不因发布新版本而变化。
ScheduleID string `json:"schedule_id"`
// ScheduleVersion 触发时使用的固定版本,JSON字符串避免精度丢失。
ScheduleVersion uint64 `json:"schedule_version,string"`
// OccurrenceID 唯一关联该任务的触发记录标识。
OccurrenceID string `json:"occurrence_id"`
// ScheduledAt 原计划时间,UTC微秒精度,不等于实际受理/执行时间。
ScheduledAt time.Time `json:"scheduled_at"`
// Decision FIRED为单点触发,COALESCED为合并触发;跳过不会产生任务。
Decision string `json:"decision"`
// CoveredUntil 合并范围的排除边界,仅COALESCED返回。
CoveredUntil *time.Time `json:"covered_until,omitempty"`
}
// GetTaskSource 查询直接来源;不会授予来源计划的读取/管理或任务执行权限。
func (c *Client) GetTaskSource(ctx context.Context, id string) (TaskSource, error) {
var out TaskSource
if !validID(id) {
return out, errors.New("invalid task id")
}
if err := c.call(ctx, "GET", "/v1/tasks/"+id+"/source", "", nil, &out); err != nil {
return TaskSource{}, err
}
if out.TaskID != id {
return TaskSource{}, errors.New("source does not match requested task")
}
valid := false
switch out.Kind {
case "DIRECT":
valid = out.RetryOf == nil && out.Schedule == nil
case "MANUAL_RETRY":
valid = out.RetryOf != nil && validID(*out.RetryOf) && *out.RetryOf != id && out.Schedule == nil
case "SCHEDULE":
s := out.Schedule
if s != nil && out.RetryOf == nil && validID(s.ScheduleID) && validID(s.OccurrenceID) && s.ScheduleVersion > 0 && validSourceTime(s.ScheduledAt) {
valid = s.Decision == "FIRED" && s.CoveredUntil == nil || s.Decision == "COALESCED" && s.CoveredUntil != nil && validSourceTime(*s.CoveredUntil) && s.CoveredUntil.After(s.ScheduledAt)
}
}
if !valid {
return TaskSource{}, errors.New("invalid or unsupported task source response")
}
return out, nil
}
func validSourceTime(t time.Time) bool {
return t.Year() >= 2000 && t.Year() <= 9998 && t.Nanosecond()%1000 == 0
}