feat: add retention policy and stability verification

This commit is contained in:
2026-09-22 16:19:21 +08:00
parent c33c12c5e9
commit 2f95d9c45c
30 changed files with 1633 additions and 18 deletions

View File

@@ -0,0 +1,192 @@
// Package retention 定义 Orbit 历史数据保留策略的通用模型。
//
// 策略只描述候选数据的最短保留窗口,不直接删除任务、事件或审计记录。
// 归档执行器必须在独立流程中先完成核查、备份和归档证明,再按策略处理。
package retention
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"unicode/utf8"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
)
const (
// DefaultIdempotencyDays 是请求幂等记录的默认保留天数。
DefaultIdempotencyDays uint32 = 90
// DefaultTaskHistoryDays 是终态任务历史的默认保留天数。
DefaultTaskHistoryDays uint32 = 365
// DefaultWorkflowHistoryDays 是终态流程历史的默认保留天数。
DefaultWorkflowHistoryDays uint32 = 365
// DefaultEventLogDays 是状态事件和结果事件的默认保留天数。
DefaultEventLogDays uint32 = 365
// DefaultDeliveryHistoryDays 是已确认交付历史的默认保留天数。
DefaultDeliveryHistoryDays uint32 = 90
// DefaultInboxHistoryDays 是已处理流程 Inbox 历史的默认保留天数。
DefaultInboxHistoryDays uint32 = 90
// DefaultAuditLogDays 是管理审计的默认保留天数,约七年。
DefaultAuditLogDays uint32 = 2555
minRetentionDays uint32 = 1
maxRetentionDays uint32 = 3650
)
// Policy 是按命名空间保存的历史数据保留策略。
// Enabled 默认关闭;即使打开,也允许只读归档导出使用满足窗口且已核验的数据。
// 当前版本不会因为配置变更自动删除任何数据。
type Policy struct {
// NamespaceID 是策略所属命名空间,不能由控制台请求替换。
NamespaceID core.ID `json:"namespace_id"`
// Revision 是乐观并发版本;默认策略未落库时为0,首次保存后从1开始。
Revision uint64 `json:"revision,string"`
// Enabled 表示是否允许归档执行器使用该策略;默认关闭以避免误清理。
Enabled bool `json:"enabled"`
// IdempotencyDays 是请求幂等记录至少保留的天数。
IdempotencyDays uint32 `json:"idempotency_days"`
// TaskHistoryDays 是终态任务及执行证据至少保留的天数。
TaskHistoryDays uint32 `json:"task_history_days"`
// WorkflowHistoryDays 是终态流程、步骤、等待和流程事件至少保留的天数。
WorkflowHistoryDays uint32 `json:"workflow_history_days"`
// EventLogDays 是状态事件和结果事件至少保留的天数。
EventLogDays uint32 `json:"event_log_days"`
// DeliveryHistoryDays 是已确认结果交付至少保留的天数;未确认交付不得按窗口清理。
DeliveryHistoryDays uint32 `json:"delivery_history_days"`
// InboxHistoryDays 是已处理流程 Inbox 事件至少保留的天数;未处理记录不得清理。
InboxHistoryDays uint32 `json:"inbox_history_days"`
// AuditLogDays 是管理审计至少保留的天数;审计记录不参与普通清理。
AuditLogDays uint32 `json:"audit_log_days"`
// UpdatedAt 是策略最后修改时间,统一使用 UTC。
UpdatedAt time.Time `json:"updated_at"`
// Source 表示策略来自默认值还是数据库配置,便于控制台明确当前状态。
Source string `json:"source"`
}
// UpdateInput 是控制台保存策略所需的完整替换输入。
// 所有窗口必须显式提交,避免遗漏字段被解释成缩短保留期。
type UpdateInput struct {
// ExpectedRevision 是页面读取到的版本;首次保存必须为0。
ExpectedRevision uint64 `json:"expected_revision,string" required:"true"`
// Enabled 是否允许归档执行器读取本策略。
Enabled bool `json:"enabled" required:"true"`
// IdempotencyDays 请求幂等记录保留天数。
IdempotencyDays uint32 `json:"idempotency_days" required:"true"`
// TaskHistoryDays 终态任务历史保留天数。
TaskHistoryDays uint32 `json:"task_history_days" required:"true"`
// WorkflowHistoryDays 终态流程历史保留天数。
WorkflowHistoryDays uint32 `json:"workflow_history_days" required:"true"`
// EventLogDays 状态和结果事件保留天数。
EventLogDays uint32 `json:"event_log_days" required:"true"`
// DeliveryHistoryDays 已确认交付历史保留天数。
DeliveryHistoryDays uint32 `json:"delivery_history_days" required:"true"`
// InboxHistoryDays 已处理 Inbox 历史保留天数。
InboxHistoryDays uint32 `json:"inbox_history_days" required:"true"`
// AuditLogDays 管理审计保留天数。
AuditLogDays uint32 `json:"audit_log_days" required:"true"`
// Reason 是本次策略变更原因,会进入管理审计。
Reason string `json:"reason" required:"true"`
}
// Store 是控制台和归档执行器共享的最小策略存储边界。
// 实现必须保证 Update 的版本核对、幂等和审计与策略写入处于同一事务。
type Store interface {
GetRetentionPolicy(context.Context, core.ID) (Policy, error)
UpdateRetentionPolicy(context.Context, auth.Token, string, UpdateInput) (Policy, error)
}
// Defaults 返回不会触发任何归档执行的安全默认策略。
func Defaults(namespace core.ID) Policy {
return Policy{
NamespaceID: namespace,
Enabled: false,
IdempotencyDays: DefaultIdempotencyDays,
TaskHistoryDays: DefaultTaskHistoryDays,
WorkflowHistoryDays: DefaultWorkflowHistoryDays,
EventLogDays: DefaultEventLogDays,
DeliveryHistoryDays: DefaultDeliveryHistoryDays,
InboxHistoryDays: DefaultInboxHistoryDays,
AuditLogDays: DefaultAuditLogDays,
Source: "default",
}
}
// Validate 检查策略身份、窗口和审计原因。
func (p Policy) Validate() error {
if p.NamespaceID.Validate() != nil {
return errors.New("retention namespace is invalid")
}
for name, days := range map[string]uint32{
"idempotency_days": p.IdempotencyDays,
"task_history_days": p.TaskHistoryDays,
"workflow_history_days": p.WorkflowHistoryDays,
"event_log_days": p.EventLogDays,
"delivery_history_days": p.DeliveryHistoryDays,
"inbox_history_days": p.InboxHistoryDays,
"audit_log_days": p.AuditLogDays,
} {
if days < minRetentionDays || days > maxRetentionDays {
return fmt.Errorf("%s must be between %d and %d days", name, minRetentionDays, maxRetentionDays)
}
}
if p.IdempotencyDays < DefaultIdempotencyDays {
return errors.New("idempotency_days cannot be less than 90 days")
}
return nil
}
// ToPolicy 将完整输入转为待保存策略,版本由事务内分配。
func (in UpdateInput) ToPolicy(namespace core.ID) (Policy, error) {
if strings.TrimSpace(in.Reason) == "" || !utf8.ValidString(in.Reason) || len(in.Reason) > 1024 || strings.ContainsAny(in.Reason, "\r\n") {
return Policy{}, errors.New("retention reason must be 1-1024 UTF-8 bytes without newlines")
}
p := Policy{
NamespaceID: namespace,
Enabled: in.Enabled,
IdempotencyDays: in.IdempotencyDays,
TaskHistoryDays: in.TaskHistoryDays,
WorkflowHistoryDays: in.WorkflowHistoryDays,
EventLogDays: in.EventLogDays,
DeliveryHistoryDays: in.DeliveryHistoryDays,
InboxHistoryDays: in.InboxHistoryDays,
AuditLogDays: in.AuditLogDays,
}
if err := p.Validate(); err != nil {
return Policy{}, err
}
return p, nil
}
// FromEnv 读取部署默认值;数据库策略保存后优先于部署默认值。
// 所有节点应注入同一组默认值,保证尚未保存策略的展示一致。
func FromEnv(get func(string) string) (Policy, error) {
p := Defaults("")
for _, f := range []struct {
name string
dst *uint32
}{
{"ORBIT_RETENTION_IDEMPOTENCY_DAYS", &p.IdempotencyDays},
{"ORBIT_RETENTION_TASK_DAYS", &p.TaskHistoryDays},
{"ORBIT_RETENTION_WORKFLOW_DAYS", &p.WorkflowHistoryDays},
{"ORBIT_RETENTION_EVENT_DAYS", &p.EventLogDays},
{"ORBIT_RETENTION_DELIVERY_DAYS", &p.DeliveryHistoryDays},
{"ORBIT_RETENTION_INBOX_DAYS", &p.InboxHistoryDays},
{"ORBIT_RETENTION_AUDIT_DAYS", &p.AuditLogDays},
} {
if raw := get(f.name); raw != "" {
n, err := strconv.ParseUint(raw, 10, 32)
if err != nil || n < 1 || n > 3650 {
return Policy{}, fmt.Errorf("%s must be 1-3650 days", f.name)
}
*f.dst = uint32(n)
}
}
if p.IdempotencyDays < 90 {
return Policy{}, errors.New("ORBIT_RETENTION_IDEMPOTENCY_DAYS cannot be less than 90")
}
return p, nil
}