♻️ Refactor synchronization service
This commit is contained in:
248
internal/syncer/snapshot/snapshot.go
Normal file
248
internal/syncer/snapshot/snapshot.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package snapshot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// CurrentVersion 是当前快照格式版本。
|
||||
CurrentVersion = 1
|
||||
)
|
||||
|
||||
// Snapshot 描述一次完整的数据快照。
|
||||
type Snapshot struct {
|
||||
Version int
|
||||
CreatedAt time.Time
|
||||
Resources map[string][]Record
|
||||
}
|
||||
|
||||
// Record 描述单条资源记录。
|
||||
type Record struct {
|
||||
Kind string
|
||||
ID string
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time
|
||||
Values map[string]interface{}
|
||||
Blobs map[string][]byte
|
||||
}
|
||||
|
||||
// Snapshotter 描述快照导出与应用接口。
|
||||
type Snapshotter interface {
|
||||
Export(ctx context.Context) (*Snapshot, error)
|
||||
Apply(ctx context.Context, snap *Snapshot) error
|
||||
}
|
||||
|
||||
// New 创建新的空快照。
|
||||
func New() *Snapshot {
|
||||
return &Snapshot{
|
||||
Version: CurrentVersion,
|
||||
CreatedAt: time.Now(),
|
||||
Resources: make(map[string][]Record),
|
||||
}
|
||||
}
|
||||
|
||||
// NewRecord 根据业务字段构造规范化记录。
|
||||
func NewRecord(kind string, id string, values map[string]interface{}, blobs map[string][]byte) (Record, error) {
|
||||
if strings.TrimSpace(kind) == "" {
|
||||
return Record{}, errors.New("record kind is required")
|
||||
}
|
||||
|
||||
normalizedValues := cloneValues(values)
|
||||
if id == "" {
|
||||
uuid, _ := normalizedValues["uuid"].(string)
|
||||
id = uuid
|
||||
}
|
||||
if id == "" {
|
||||
return Record{}, errors.New("record id is required")
|
||||
}
|
||||
normalizedValues["uuid"] = id
|
||||
|
||||
updatedAt, err := parseRequiredTime(normalizedValues["updated_at"])
|
||||
if err != nil {
|
||||
return Record{}, fmt.Errorf("record %s updated_at: %w", id, err)
|
||||
}
|
||||
|
||||
deletedAt, err := parseOptionalTime(normalizedValues["deleted_at"])
|
||||
if err != nil {
|
||||
return Record{}, fmt.Errorf("record %s deleted_at: %w", id, err)
|
||||
}
|
||||
|
||||
return Record{
|
||||
Kind: kind,
|
||||
ID: id,
|
||||
UpdatedAt: updatedAt,
|
||||
DeletedAt: deletedAt,
|
||||
Values: normalizedValues,
|
||||
Blobs: cloneBlobs(blobs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Clone 返回快照的深拷贝。
|
||||
func Clone(snap *Snapshot) *Snapshot {
|
||||
if snap == nil {
|
||||
return New()
|
||||
}
|
||||
|
||||
cloned := &Snapshot{
|
||||
Version: snap.Version,
|
||||
CreatedAt: snap.CreatedAt,
|
||||
Resources: make(map[string][]Record, len(snap.Resources)),
|
||||
}
|
||||
for kind, records := range snap.Resources {
|
||||
copied := make([]Record, 0, len(records))
|
||||
for _, record := range records {
|
||||
copied = append(copied, CloneRecord(record))
|
||||
}
|
||||
cloned.Resources[kind] = copied
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
|
||||
// CloneRecord 返回记录的深拷贝。
|
||||
func CloneRecord(record Record) Record {
|
||||
return Record{
|
||||
Kind: record.Kind,
|
||||
ID: record.ID,
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
DeletedAt: cloneTime(record.DeletedAt),
|
||||
Values: cloneValues(record.Values),
|
||||
Blobs: cloneBlobs(record.Blobs),
|
||||
}
|
||||
}
|
||||
|
||||
// Digest 计算快照的稳定摘要。
|
||||
func Digest(snap *Snapshot) (string, error) {
|
||||
normalized := Clone(snap)
|
||||
|
||||
type digestRecord struct {
|
||||
ID string `json:"id"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
DeletedAt *string `json:"deleted_at,omitempty"`
|
||||
Values map[string]interface{} `json:"values"`
|
||||
Blobs map[string][]byte `json:"blobs,omitempty"`
|
||||
}
|
||||
|
||||
payload := struct {
|
||||
Version int `json:"version"`
|
||||
Resources map[string][]digestRecord `json:"resources"`
|
||||
}{
|
||||
Version: normalized.Version,
|
||||
Resources: make(map[string][]digestRecord, len(normalized.Resources)),
|
||||
}
|
||||
|
||||
for _, kind := range sortedKinds(normalized.Resources) {
|
||||
records := normalized.Resources[kind]
|
||||
sort.Slice(records, func(i int, j int) bool {
|
||||
return records[i].ID < records[j].ID
|
||||
})
|
||||
|
||||
items := make([]digestRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
var deletedAt *string
|
||||
if record.DeletedAt != nil {
|
||||
value := record.DeletedAt.Format(time.RFC3339)
|
||||
deletedAt = &value
|
||||
}
|
||||
items = append(items, digestRecord{
|
||||
ID: record.ID,
|
||||
UpdatedAt: record.UpdatedAt.Format(time.RFC3339),
|
||||
DeletedAt: deletedAt,
|
||||
Values: record.Values,
|
||||
Blobs: record.Blobs,
|
||||
})
|
||||
}
|
||||
payload.Resources[kind] = items
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
// RecordDigest 计算单条记录的稳定摘要。
|
||||
func RecordDigest(record Record) string {
|
||||
sum, err := Digest(&Snapshot{
|
||||
Version: CurrentVersion,
|
||||
Resources: map[string][]Record{
|
||||
record.Kind: {CloneRecord(record)},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// cloneValues 复制字段 map。
|
||||
func cloneValues(values map[string]interface{}) map[string]interface{} {
|
||||
if values == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return maps.Clone(values)
|
||||
}
|
||||
|
||||
// cloneBlobs 复制二进制 blob 集合。
|
||||
func cloneBlobs(blobs map[string][]byte) map[string][]byte {
|
||||
if len(blobs) == 0 {
|
||||
return nil
|
||||
}
|
||||
copied := make(map[string][]byte, len(blobs))
|
||||
for name, blob := range blobs {
|
||||
copied[name] = append([]byte(nil), blob...)
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
// cloneTime 复制时间指针。
|
||||
func cloneTime(value *time.Time) *time.Time {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *value
|
||||
return &cloned
|
||||
}
|
||||
|
||||
// parseRequiredTime 解析必填时间字段。
|
||||
func parseRequiredTime(value interface{}) (time.Time, error) {
|
||||
text, _ := value.(string)
|
||||
if text == "" {
|
||||
return time.Time{}, errors.New("time value is required")
|
||||
}
|
||||
return time.Parse(time.RFC3339, text)
|
||||
}
|
||||
|
||||
// parseOptionalTime 解析可选时间字段。
|
||||
func parseOptionalTime(value interface{}) (*time.Time, error) {
|
||||
text, _ := value.(string)
|
||||
if text == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
// sortedKinds 返回稳定排序后的资源类型列表。
|
||||
func sortedKinds(resources map[string][]Record) []string {
|
||||
kinds := make([]string, 0, len(resources))
|
||||
for kind := range resources {
|
||||
kinds = append(kinds, kind)
|
||||
}
|
||||
sort.Strings(kinds)
|
||||
return kinds
|
||||
}
|
||||
Reference in New Issue
Block a user