feat: add retention policy and stability verification
This commit is contained in:
192
internal/retention/policy.go
Normal file
192
internal/retention/policy.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user