Files
orbit/internal/retention/scheduler.go

102 lines
7.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package retention
import (
"context"
"errors"
"strings"
"time"
"unicode/utf8"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
)
// ArchiveKinds 是周期归档支持的固定类别,不接受任意表名或 SQL。
var ArchiveKinds = [...]string{"tasks", "workflows", "events", "audit", "deliveries", "inbox", "idempotency"}
// ArchivePlan 是命名空间内单一类别的周期计划;失败作业阻止本类别再生成新作业。
type ArchivePlan struct {
NamespaceID core.ID `json:"namespace_id"` // NamespaceID 由登录会话确定。
Kind string `json:"kind"` // Kind 同一命名空间内唯一的归档类别。
Revision uint64 `json:"revision,string"` // Revision 配置版本;尚未保存时为0,后台触发不改变配置版本。
Enabled bool `json:"enabled"` // Enabled 仅控制新作业触发,关闭不会中断已有作业。
PolicyRevision uint64 `json:"policy_revision,string"` // PolicyRevision 管理员确认的保留策略版本,变更策略后须重新保存计划。
StorageID core.ID `json:"storage_id"` // StorageID 固定归档卷身份,由部署配置提供,不接受浏览器任意路径。
IntervalSeconds uint32 `json:"interval_seconds"` // IntervalSeconds 周期秒数,3600至2592000;错过多次只合并触发一次。
BatchSize int `json:"batch_size"` // BatchSize 单批根记录数,1至100。
MaxBytes int64 `json:"max_bytes"` // MaxBytes 每个作业的文件字节上限,1 MiB至1 GiB。
NextRunAt *time.Time `json:"next_run_at"` // NextRunAt 下次触发UTC时间,未保存时为空;首次启用立即到期。
ActiveJobID *core.ID `json:"active_job_id"` // ActiveJobID 未完成或待处理失败作业;成功或人工关闭后清空。
UpdatedAt *time.Time `json:"updated_at"` // UpdatedAt 最后配置修改UTC时间,未保存为空。
}
// PlanInput 完整替换计划配置;保存与管理审计、幂等回执同事务。
type PlanInput struct {
ExpectedRevision uint64 `json:"expected_revision,string" required:"true"` // ExpectedRevision 已读取的计划版本。
PolicyRevision uint64 `json:"policy_revision,string" required:"true"` // PolicyRevision 已确认的保留策略版本。
Enabled bool `json:"enabled" required:"true"` // Enabled 是否开始周期触发。
IntervalSeconds uint32 `json:"interval_seconds" required:"true"` // IntervalSeconds 周期秒数。
BatchSize int `json:"batch_size" required:"true"` // BatchSize 每批根记录数。
MaxBytes int64 `json:"max_bytes" required:"true"` // MaxBytes 单作业总文件预算,字节。
Reason string `json:"reason" required:"true"` // Reason 单行操作原因,最多1024个UTF-8字节。
}
// ManagedJob 保存周期作业权威状态;文件内容和游标不通过管理列表返回。
type ManagedJob struct {
ID core.ID `json:"id"` // ID 与目录名及文件清单作业ID一致。
NamespaceID core.ID `json:"namespace_id"` // NamespaceID 所属命名空间。
Kind string `json:"kind"` // Kind 固定归档类别。
PlanRevision uint64 `json:"plan_revision,string"` // PlanRevision 创建时固定的计划配置版本。
PolicyRevision uint64 `json:"policy_revision,string"` // PolicyRevision 作业期间不可替换的保留策略版本。
StorageID core.ID `json:"storage_id"` // StorageID 固定归档卷身份,接管节点必须挂载同一物理卷。
BatchSize int `json:"batch_size"` // BatchSize 创建时固定的单批根记录上限。
MaxBytes int64 `json:"max_bytes"` // MaxBytes 累计字节预算,仅失败重试时允许提高。
State string `json:"state"` // State 为PENDING/RUNNING/SUCCEEDED/FAILED/ABANDONED。
Revision uint64 `json:"revision,string"` // Revision 每次领取、确认、人工操作递增,用于防止迟到覆盖。
Attempts uint32 `json:"attempts"` // Attempts 累计批次领取次数,包含崩溃接管,不等于业务任务重试次数。
Batches int `json:"batches"` // Batches 已验证并入库的连续批次数。
Roots int64 `json:"roots"` // Roots 已验证根记录总数。
Bytes int64 `json:"bytes"` // Bytes 已验证清单和批次总字节数。
LastSHA256 string `json:"last_sha256"` // LastSHA256 数据库确认的文件链尾摘要;恢复必须验证此前缀。
ErrorCode string `json:"error_code"` // ErrorCode 固定错误分类,不保存原始路径、业务正文或数据库错误。
LeaseID core.ID `json:"-"` // LeaseID 当前领取随机代次,不向浏览器返回。
LeaseUntil *time.Time `json:"lease_until"` // LeaseUntil UTC租约截止,非执行中为空;过期可接管。
CreatedAt time.Time `json:"created_at"` // CreatedAt 作业受理UTC时间。
UpdatedAt time.Time `json:"updated_at"` // UpdatedAt 最近状态变更UTC时间。
}
// JobActionInput 是失败作业的人工恢复或关闭输入,必须留有原因。
type JobActionInput struct {
ExpectedRevision uint64 `json:"expected_revision,string" required:"true"` // ExpectedRevision 已读取的作业版本。
Action string `json:"action" required:"true"` // Action 仅retry或abandon;后者保留已有文件与审计。
MaxBytes int64 `json:"max_bytes" required:"true"` // MaxBytes 重试可提高预算,关闭时保持原值。
Reason string `json:"reason" required:"true"` // Reason 人工操作原因。
}
// JobPage 采用已返回作业ID作为翻页位置;位置必须属于当前命名空间。
type JobPage struct {
Items []ManagedJob `json:"items"` // Items 本页最多20个作业。
NextBefore string `json:"next_before"` // NextBefore 下一页位置;空表示当前已到末页。
}
// SchedulerStore 将控制台管理与后台执行的持久事务统一到存储层。
type SchedulerStore interface {
ListArchivePlans(context.Context, auth.Token) ([]ArchivePlan, error)
SaveArchivePlan(context.Context, auth.Token, string, string, core.ID, PlanInput) (ArchivePlan, error)
ListArchiveJobs(context.Context, auth.Token, string) (JobPage, error)
ActArchiveJob(context.Context, auth.Token, core.ID, string, JobActionInput) (ManagedJob, error)
}
func ValidReason(s string) bool {
return strings.TrimSpace(s) != "" && utf8.ValidString(s) && len(s) <= 1024 && !strings.ContainsAny(s, "\r\n")
}
func (in PlanInput) Validate(namespace core.ID, kind string) error {
if err := (JobSpec{namespace, kind, in.PolicyRevision, in.BatchSize}).Validate(); err != nil {
return err
}
if in.IntervalSeconds < 3600 || in.IntervalSeconds > 2592000 || in.MaxBytes < 1<<20 || in.MaxBytes > 1<<30 || !ValidReason(in.Reason) {
return errors.New("invalid archive interval, byte budget or reason")
}
return nil
}