Skip to content

Commit 32f5f15

Browse files
committed
backup: implement manifest aggregation
Add backup manifest data types, fragment decoding, aggregation, validation, warnings, and unit tests with testdata fixtures. Closes TNTP-8208
1 parent cfaaefc commit 32f5f15

13 files changed

Lines changed: 1297 additions & 0 deletions

lib/backup/aggregation.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package backup
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"time"
7+
)
8+
9+
const artifactCompression = "zstd"
10+
11+
// RecoveryPoint describes one engine recovery point returned by Tarantool.
12+
type RecoveryPoint struct {
13+
UUID string `json:"uuid"`
14+
ReplicaID uint32 `json:"replica_id"`
15+
LSN uint64 `json:"lsn"`
16+
Timestamp int64 `json:"timestamp"` // unix-time
17+
}
18+
19+
// Fragment is a per-replicaset backup description stored in an instance archive.
20+
type Fragment struct {
21+
ReplicasetUUID string `json:"replicaset_uuid"`
22+
InstanceUUID string `json:"instance_uuid"`
23+
InstanceName string `json:"instance_name"`
24+
Hostname string `json:"hostname"`
25+
Type BackupType `json:"type"`
26+
VclockBegin Vclock `json:"vclock_begin"`
27+
VclockEnd Vclock `json:"vclock_end"`
28+
Files []string `json:"files"`
29+
ChecksumSHA256 string `json:"checksum_sha256"`
30+
RecoveryPoints []*RecoveryPoint `json:"recovery_points,omitempty"`
31+
}
32+
33+
// AggregateInput contains all external data needed to build a manifest.
34+
type AggregateInput struct {
35+
BackupID BackupID
36+
BaseFullBackupID BackupID
37+
PreviousBackupID BackupID
38+
CreationTime time.Time
39+
CreationDuration time.Duration
40+
Topology Topology
41+
Shards []*ShardInput
42+
}
43+
44+
// ShardInput describes one expected replicaset backup result.
45+
type ShardInput struct {
46+
ReplicasetUUID string
47+
Fragment *Fragment
48+
Location *ArtifactLocation
49+
Err error
50+
}
51+
52+
// ArtifactLocation identifies an already uploaded shard archive.
53+
type ArtifactLocation struct {
54+
Path string
55+
SizeBytes int64
56+
}
57+
58+
// DecodeFragment decodes and validates one instance_backup.json payload.
59+
func DecodeFragment(data []byte) (*Fragment, error) {
60+
var fragment Fragment
61+
62+
if err := json.Unmarshal(data, &fragment); err != nil {
63+
return nil, fmt.Errorf("decode fragment: %w", err)
64+
}
65+
66+
if err := fragment.Validate(); err != nil {
67+
return nil, fmt.Errorf("validate fragment: %w", err)
68+
}
69+
70+
return &fragment, nil
71+
}
72+
73+
// NewAggregateInput collects backup metadata, topology and shard inputs.
74+
func NewAggregateInput(
75+
backupID BackupID,
76+
previousBackupID BackupID,
77+
baseFullBackupID BackupID,
78+
creationTime time.Time,
79+
creationDuration time.Duration,
80+
topology Topology,
81+
shards []*ShardInput,
82+
) AggregateInput {
83+
return AggregateInput{
84+
BackupID: backupID,
85+
PreviousBackupID: previousBackupID,
86+
BaseFullBackupID: baseFullBackupID,
87+
CreationTime: creationTime,
88+
CreationDuration: creationDuration,
89+
Topology: topology,
90+
Shards: shards,
91+
}
92+
}
93+
94+
// Aggregate builds and validates a cluster manifest from shard fragments.
95+
func Aggregate(in AggregateInput) (*ClusterManifest, error) {
96+
manifest := newClusterManifest(in)
97+
98+
for _, shardInput := range in.Shards {
99+
if err := aggregateShard(manifest, shardInput); err != nil {
100+
return nil, fmt.Errorf("aggregate shard: %w", err)
101+
}
102+
}
103+
104+
manifest.Status = calculateStatus(manifest)
105+
if err := manifest.Validate(); err != nil {
106+
return nil, fmt.Errorf("validate cluster manifest: %w", err)
107+
}
108+
109+
return manifest, nil
110+
}
111+
112+
// newClusterManifest initializes a manifest with immutable aggregate metadata.
113+
func newClusterManifest(in AggregateInput) *ClusterManifest {
114+
return &ClusterManifest{
115+
SchemaVersion: SchemaVersion,
116+
BackupID: in.BackupID,
117+
PreviousBackupID: in.PreviousBackupID,
118+
BaseFullBackupID: in.BaseFullBackupID,
119+
Status: StatusFailed,
120+
CreationTime: in.CreationTime,
121+
CreationDuration: in.CreationDuration,
122+
Shards: make(map[string]Shard, len(in.Shards)),
123+
Topology: in.Topology,
124+
Warnings: make([]Warning, 0),
125+
}
126+
}
127+
128+
// aggregateShard adds one shard input to the manifest.
129+
func aggregateShard(manifest *ClusterManifest, shardInput *ShardInput) error {
130+
replicasetUUID := shardInput.ReplicasetUUID
131+
132+
if shardInput.Fragment == nil {
133+
aggregateFailedShard(manifest, replicasetUUID, shardInput.Err)
134+
return nil
135+
}
136+
137+
if shardInput.Err != nil {
138+
manifest.Shards[replicasetUUID] = Shard{Error: shardInput.Err.Error()}
139+
manifest.Warnings = append(
140+
manifest.Warnings,
141+
NewShardPartialWarning(
142+
replicasetUUID,
143+
shardInput.Fragment.InstanceUUID,
144+
shardInput.Err.Error(),
145+
),
146+
)
147+
return nil
148+
}
149+
150+
if shardInput.Fragment.ReplicasetUUID != replicasetUUID {
151+
return fmt.Errorf(
152+
"fragment replicaset_uuid %q does not match shard input replicaset_uuid %q",
153+
shardInput.Fragment.ReplicasetUUID,
154+
replicasetUUID,
155+
)
156+
}
157+
158+
aggregateSuccessfulShard(manifest, replicasetUUID, shardInput)
159+
return nil
160+
}
161+
162+
// aggregateFailedShard adds an error result for a shard.
163+
func aggregateFailedShard(manifest *ClusterManifest, replicasetUUID string, err error) {
164+
if err == nil {
165+
manifest.Shards[replicasetUUID] = Shard{Error: "shard unreachable"}
166+
manifest.Warnings = append(manifest.Warnings,
167+
NewShardUnreachableWarning(replicasetUUID))
168+
return
169+
}
170+
171+
manifest.Shards[replicasetUUID] = Shard{Error: err.Error()}
172+
}
173+
174+
// aggregateSuccessfulShard adds an instance result for a shard.
175+
func aggregateSuccessfulShard(
176+
manifest *ClusterManifest,
177+
replicasetUUID string,
178+
shardInput *ShardInput,
179+
) {
180+
fragment := shardInput.Fragment
181+
location := ArtifactLocation{}
182+
if shardInput.Location != nil {
183+
location = *shardInput.Location
184+
}
185+
186+
manifest.Shards[replicasetUUID] = Shard{
187+
Instance: &ShardInstance{
188+
InstanceUUID: fragment.InstanceUUID,
189+
InstanceName: fragment.InstanceName,
190+
Hostname: fragment.Hostname,
191+
VclockBegin: fragment.VclockBegin,
192+
VclockEnd: fragment.VclockEnd,
193+
Artifact: Artifact{
194+
Path: location.Path,
195+
SizeBytes: location.SizeBytes,
196+
ChecksumSHA256: fragment.ChecksumSHA256,
197+
Compression: artifactCompression,
198+
Files: append([]string(nil), fragment.Files...),
199+
RecoveryPoints: recoveryPointsFromFragment(manifest, replicasetUUID, fragment),
200+
Type: fragment.Type,
201+
},
202+
},
203+
}
204+
}
205+
206+
// recoveryPointsFromFragment converts optional fragment recovery points.
207+
func recoveryPointsFromFragment(
208+
manifest *ClusterManifest,
209+
replicasetUUID string,
210+
fragment *Fragment,
211+
) []RecoveryPoint {
212+
recoveryPoints := make([]RecoveryPoint, 0)
213+
if fragment.RecoveryPoints == nil {
214+
manifest.Warnings = append(manifest.Warnings,
215+
NewRecoveryPointsUnavailableWarning(replicasetUUID, "recovery points unavailable"))
216+
return recoveryPoints
217+
}
218+
219+
for _, point := range fragment.RecoveryPoints {
220+
if point != nil {
221+
recoveryPoints = append(recoveryPoints, *point)
222+
}
223+
}
224+
return recoveryPoints
225+
}
226+
227+
// calculateStatus derives cluster backup health from shard results and warnings.
228+
func calculateStatus(manifest *ClusterManifest) Status {
229+
successful := 0
230+
failed := 0
231+
232+
for _, shard := range manifest.Shards {
233+
if shard.Instance != nil {
234+
successful++
235+
}
236+
if shard.Error != "" {
237+
failed++
238+
}
239+
}
240+
241+
if successful == 0 {
242+
return StatusFailed
243+
}
244+
if isDegraded(manifest, successful, failed) {
245+
return StatusDegraded
246+
}
247+
return StatusOK
248+
}
249+
250+
// isDegraded reports whether a partially useful manifest has issues.
251+
func isDegraded(manifest *ClusterManifest, successful, failed int) bool {
252+
return failed > 0 ||
253+
len(manifest.Warnings) > 0 ||
254+
successful < len(manifest.Topology.Replicasets)
255+
}

0 commit comments

Comments
 (0)