feat: verify bounded simultaneous schedule execution

This commit is contained in:
2026-09-20 13:56:44 +08:00
parent 71c33eae18
commit 29734596a3
21 changed files with 2371 additions and 2 deletions

View File

@@ -0,0 +1,206 @@
package main
import (
"context"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
"reflect"
"time"
)
// slotObservation 只缓存一个固定时间槽的不可变触发和核验证据。
type slotObservation struct {
occurrence *orbit.ScheduleOccurrence // occurrence 首次读到的快照;不得改写。
verified bool // verified 是否已完成全部持久化核验。
attemptID string // attemptID 唯一成功尝试,用于与本地调用关联。
trigger, queue, execution, completion time.Duration // 各阶段只使用数据库时间。
}
// planObservation 预分配固定槽位,内存上限由配置限制。
type planObservation struct {
index int // index 计划在本次配置中的序号。
start time.Time // start 固定触发基准。
namespace string // namespace 预检认证队列命名空间。
slots []slotObservation // slots 预期触发槽位,不随服务端输入增长。
}
// observations 合并轮询快照,跨计划检查唯一触发和任务身份。
type observations struct {
cfg config // cfg 固定样本配置。
runID string // runID 固定测量身份。
plans map[string]*planObservation // plans 创建时登记的有限计划集合。
occurrences map[string]string // occurrences 触发ID到计划ID,避免跨计划重复。
tasks map[string]*slotObservation // tasks 已观察任务到唯一槽位。
}
func newObservations(cfg config, runID string) *observations {
return &observations{cfg: cfg, runID: runID, plans: map[string]*planObservation{}, occurrences: map[string]string{}, tasks: map[string]*slotObservation{}}
}
func (o *observations) addPlan(id string, index int, start time.Time, namespace string) {
o.plans[id] = &planObservation{index: index, start: start, namespace: namespace, slots: make([]slotObservation, o.cfg.Requests)}
}
func (o *observations) ingest(id string, page orbit.ScheduleOccurrencePage) string {
p := o.plans[id]
if p == nil || page.Items == nil || page.NextCursor != "" || len(page.Items) > len(p.slots) {
return "OCCURRENCE_PAGE_MISMATCH"
}
seen := map[string]bool{}
for _, item := range page.Items {
delta := item.ScheduledAt.Sub(p.start)
if !validID(item.ID) || item.ScheduleID != id || item.ScheduleVersion != 1 || delta < 0 || delta%o.cfg.Interval != 0 || delta/o.cfg.Interval >= time.Duration(len(p.slots)) || item.CreatedAt.Before(item.ScheduledAt) {
return "OCCURRENCE_MISMATCH"
}
if seen[item.ID] {
return "DUPLICATE_OCCURRENCE"
}
seen[item.ID] = true
slot := &p.slots[int(delta/o.cfg.Interval)]
if slot.occurrence != nil {
if !reflect.DeepEqual(*slot.occurrence, item) {
return "OCCURRENCE_CHANGED"
}
continue
}
if _, exists := o.occurrences[item.ID]; exists {
return "DUPLICATE_OCCURRENCE"
}
// 保存已观察证据再判定策略异常;未验证的触发不会充当成功分母。
snapshot := item
slot.occurrence = &snapshot
o.occurrences[item.ID] = id
if item.Decision != "FIRED" || item.CoveredUntil != nil || item.Reason != "SCHEDULED" {
return "OCCURRENCE_DECISION_MISMATCH"
}
if item.TaskID == nil || !validID(*item.TaskID) {
return "OCCURRENCE_TASK_MISMATCH"
}
if _, exists := o.tasks[*item.TaskID]; exists {
return "DUPLICATE_TASK"
}
o.tasks[*item.TaskID] = slot
}
// 此样本总量不超过单页上限,历史不可消失;分页异常不降级继续。
known := 0
for _, s := range p.slots {
if s.occurrence != nil {
known++
}
}
if len(page.Items) != known {
return "OCCURRENCE_DISAPPEARED"
}
return ""
}
func (o *observations) verify(ctx context.Context, api scheduleAPI, id string) (bool, string) {
p := o.plans[id]
complete := true
for i := range p.slots {
slot := &p.slots[i]
if slot.occurrence == nil {
complete = false
continue
}
if slot.verified {
continue
}
if ctx.Err() != nil {
return false, "VERIFY_" + classify(ctx.Err())
}
item := slot.occurrence
if item.TaskID == nil || o.tasks[*item.TaskID] != slot {
return false, "OCCURRENCE_TASK_MISMATCH"
}
call, stop := context.WithTimeout(ctx, o.cfg.Timeout)
task, err := api.GetTask(call, *item.TaskID)
stop()
if err != nil {
return false, "TASK_" + classify(err)
}
if task.ID != *item.TaskID || task.NamespaceID != p.namespace || !matchesTask(task, o.cfg, o.runID, p.index) || !task.CreatedAt.Equal(item.CreatedAt) || !task.AvailableAt.Equal(task.CreatedAt) || !task.DeadlineAt.Equal(task.CreatedAt.Add(time.Duration(o.cfg.RunTimeout.Milliseconds())*time.Millisecond)) {
return false, "TASK_MISMATCH"
}
switch task.State {
case orbit.TaskWaiting, orbit.TaskLeased, orbit.TaskRunning:
complete = false
continue
case orbit.TaskFailed, orbit.TaskCancelled, orbit.TaskNeedsReview:
return false, "TASK_" + string(task.State)
case orbit.TaskSucceeded:
default:
return false, "UNKNOWN_TASK_STATE"
}
output, valid := decodePayload(task.Result)
if !valid || output != (schedulePayload{RunID: o.runID, Schedule: p.index}) || task.CompletedAt == nil || task.EffectUncertain || task.AttemptCount != 1 || task.CancelRequestedAt != nil {
return false, "COMPLETION_MISMATCH"
}
call, stop = context.WithTimeout(ctx, o.cfg.Timeout)
history, err := api.ListTaskAttempts(call, task.ID, orbit.HistoryPageRequest{Limit: 2})
stop()
if err != nil {
return false, "HISTORY_" + classify(err)
}
if len(history.Items) != 1 || history.NextCursor != "" {
return false, "ATTEMPT_COUNT_MISMATCH"
}
a := history.Items[0]
if !validID(a.ID) || a.TaskID != task.ID || a.Epoch != 1 || a.State != orbit.AttemptSucceeded || a.AuthorizedAt == nil || a.EndedAt == nil || a.ErrorCode != "" || a.Retryable || a.ConcurrencyToken != 0 {
return false, "ATTEMPT_MISMATCH"
}
if a.CreatedAt.Before(task.AvailableAt) || a.AuthorizedAt.Before(a.CreatedAt) || a.EndedAt.Before(*a.AuthorizedAt) || !a.EndedAt.Equal(*task.CompletedAt) {
return false, "TIMESTAMP_MISMATCH"
}
call, stop = context.WithTimeout(ctx, o.cfg.Timeout)
source, err := api.GetTaskSource(call, task.ID)
stop()
if err != nil {
return false, "SOURCE_" + classify(err)
}
s := source.Schedule
if source.TaskID != task.ID || source.Kind != "SCHEDULE" || source.Workflow != nil || source.RetryOf != nil || s == nil || s.ScheduleID != id || s.ScheduleVersion != 1 || s.OccurrenceID != item.ID || !s.ScheduledAt.Equal(item.ScheduledAt) || s.Decision != "FIRED" || s.CoveredUntil != nil {
return false, "SOURCE_MISMATCH"
}
slot.verified = true
slot.attemptID = a.ID
slot.trigger = task.CreatedAt.Sub(item.ScheduledAt)
slot.queue = a.AuthorizedAt.Sub(task.CreatedAt)
slot.execution = a.EndedAt.Sub(*a.AuthorizedAt)
slot.completion = task.CompletedAt.Sub(item.ScheduledAt)
}
return complete, ""
}
func matchesTask(t orbit.Task, c config, runID string, index int) bool {
payload, ok := decodePayload(t.Payload)
return ok && payload == (schedulePayload{RunID: runID, Schedule: index}) && validID(t.ID) && t.QueueID == c.QueueID && t.Handler == handlerName(runID) && t.HandlerVersion == "v1" && t.PayloadVersion == "v1" && t.BusinessKey == businessKey(runID, index) && t.ConcurrencyKey == "" && t.Priority == 0 && t.RetryOf == nil && t.StartDeadlineAt == nil && t.Retry == (orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: uint64(c.RunTimeout.Milliseconds())}) && t.RecoverySafety == orbit.RecoveryReconcileOnly
}
func (o *observations) summarize(r *scheduleReport) {
var trigger, queue, execution, completion []time.Duration
for i := range r.Plans {
p := &r.Plans[i]
data := o.plans[p.ID]
if data == nil {
continue
}
for j := range data.slots {
s := &data.slots[j]
if s.occurrence != nil {
p.Occurrences++
}
if s.occurrence != nil && s.occurrence.TaskID != nil && o.tasks[*s.occurrence.TaskID] == s {
p.Fired++
}
if s.verified {
p.VerifiedSuccess++
trigger = append(trigger, s.trigger)
queue = append(queue, s.queue)
execution = append(execution, s.execution)
completion = append(completion, s.completion)
}
}
r.Occurrences += p.Occurrences
r.Fired += p.Fired
r.VerifiedSuccess += p.VerifiedSuccess
}
r.TriggerLag = distributionOf(trigger)
r.QueueWait = distributionOf(queue)
r.Execution = distributionOf(execution)
r.Completion = distributionOf(completion)
}