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 }