Skip to content
This repository was archived by the owner on Mar 28, 2020. It is now read-only.

Commit 9145a6f

Browse files
author
Matthew Krajewski
committed
etcd-backup-operator: fixed sorting of etcd backups to ensure deletion of only the oldest backups when rotating
When rotating backups based on a maximum number allowed, newer backups were being deleted when the number of digits in an etcd store revision increased. E.g., when "v99" became "v100", the newer "v100" backup was being deleted, because the "1" in "100" was sorted to come before the first "9" in "99". This new sorting method relies on the timestamp in each backup path, rather than the revision number. The reason the timestamp is given precedence over the revision when sorting is because the revision could potentially go backwards when an older backup is restored. See my comment in pkg/backup/util/util.go's SortableBackupPaths type for more details. Fixes #2113
1 parent 1521ae4 commit 9145a6f

3 files changed

Lines changed: 424 additions & 1 deletion

File tree

pkg/backup/backup_manager.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"sort"
2222
"time"
2323

24+
"github.com/coreos/etcd-operator/pkg/backup/util"
2425
"github.com/coreos/etcd-operator/pkg/backup/writer"
2526
"github.com/coreos/etcd-operator/pkg/util/constants"
2627

@@ -73,6 +74,7 @@ func (bm *BackupManager) SaveSnap(ctx context.Context, s3Path string, isPeriodic
7374
}
7475
defer rc.Close()
7576
if isPeriodic {
77+
// NOTE: make sure this path format stays in sync with util.SortableBackupPaths
7678
s3Path = fmt.Sprintf(s3Path+"_v%d_%s", rev, now.Format("2006-01-02-15:04:05"))
7779
}
7880
_, err = bm.bw.Write(ctx, s3Path, rc)
@@ -89,7 +91,7 @@ func (bm *BackupManager) EnsureMaxBackup(ctx context.Context, basePath string, m
8991
if err != nil {
9092
return fmt.Errorf("failed to get exisiting snapshots: %v", err)
9193
}
92-
sort.Sort(sort.Reverse(sort.StringSlice(savedSnapShots)))
94+
sort.Sort(sort.Reverse(util.SortableBackupPaths(sort.StringSlice(savedSnapShots))))
9395
for i, snapshotPath := range savedSnapShots {
9496
if i < maxCount {
9597
continue

pkg/backup/util/util.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ package util
1616

1717
import (
1818
"fmt"
19+
"regexp"
20+
"sort"
21+
"strconv"
1922
"strings"
2023
)
2124

@@ -32,3 +35,74 @@ func ParseBucketAndKey(path string) (string, string, error) {
3235
}
3336
return toks[0], toks[1], nil
3437
}
38+
39+
// SortableBackupPaths implements extends sort.StringSlice to allow sorting to work
40+
// with paths used for backups, in the format "<base path>_v<etcd store revision>_YYYY-MM-DD-HH:mm:SS",
41+
// where the timestamp is what is being sorted on.
42+
type SortableBackupPaths sort.StringSlice
43+
44+
// regular expressions used in backup path order comparison
45+
var backupTimestampRegex = regexp.MustCompile(`_\d+-\d+-\d+-\d+:\d+:\d+`)
46+
var etcdStoreRevisionRegex = regexp.MustCompile(`_v\d+`)
47+
48+
// Len is the number of elements in the collection.
49+
func (s SortableBackupPaths) Len() int {
50+
return len(s)
51+
}
52+
53+
// Less reports whether the element with index i should sort before the element with index j.
54+
// Assumes that the two paths are part of a sequence of backups of the same etcd cluster,
55+
// relying on comparison of the timestamps found in the two elements' paths,
56+
// but not making assumptions about either path's base path.
57+
// The last etcd store revision number found in each path is used to break ties.
58+
// Timestamp comparison takes precedence in case an older revision of the etcd store is restored
59+
// to the cluster, resulting in newer, more relevant backups that happen to have older revision numbers.
60+
// If either base path happens to have a similarly formatted revision number,
61+
// the last one in each path is compared.
62+
// This method will work even if the format changes somewhat (e.g., the revision is placed after the timesamp).
63+
// If a comparison can't be made based on path format, the one conforming to the format is considered to be newer,
64+
// and if neither path conforms, false is returned.
65+
func (s SortableBackupPaths) Less(i, j int) bool {
66+
// compare timestamps first
67+
itmatches := backupTimestampRegex.FindAll([]byte(s[i]), -1)
68+
jtmatches := backupTimestampRegex.FindAll([]byte(s[j]), -1)
69+
70+
if len(itmatches) < 1 {
71+
return true
72+
}
73+
if len(jtmatches) < 1 {
74+
return false
75+
}
76+
77+
itstr := string(itmatches[len(itmatches)-1])
78+
jtstr := string(jtmatches[len(jtmatches)-1])
79+
80+
timestampComparison := strings.Compare(string(jtstr), string(itstr))
81+
if timestampComparison != 0 {
82+
return timestampComparison > 0
83+
}
84+
85+
// fall through to revision comparison
86+
irmatches := etcdStoreRevisionRegex.FindAll([]byte(s[i]), -1)
87+
jrmatches := etcdStoreRevisionRegex.FindAll([]byte(s[j]), -1)
88+
89+
if len(irmatches) < 1 {
90+
return true
91+
}
92+
if len(jrmatches) < 1 {
93+
return false
94+
}
95+
96+
irstr := string(irmatches[len(irmatches)-1])
97+
jrstr := string(jrmatches[len(jrmatches)-1])
98+
99+
ir, _ := strconv.ParseInt(irstr[2:len(irstr)], 10, 64)
100+
jr, _ := strconv.ParseInt(jrstr[2:len(jrstr)], 10, 64)
101+
102+
return ir < jr
103+
}
104+
105+
// Swap swaps the elements with indexes i and j.
106+
func (s SortableBackupPaths) Swap(i, j int) {
107+
s[i], s[j] = s[j], s[i]
108+
}

0 commit comments

Comments
 (0)