Skip to content

Commit c26f8bf

Browse files
committed
fix: avoid forced snapshot cleanup (#231):
Take into account related clones while clean up snapshots. check all snapshots that we are going to delete and skip those that have active dependent clones.
1 parent fc1817c commit c26f8bf

File tree

2 files changed

+103
-3
lines changed

2 files changed

+103
-3
lines changed

pkg/services/provision/thinclones/zfs/zfs.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strconv"
1111
"strings"
1212
"time"
13+
"unicode"
1314

1415
"github.com/pkg/errors"
1516

@@ -305,12 +306,21 @@ func (m *Manager) DestroySnapshot(snapshotName string) error {
305306
return nil
306307
}
307308

308-
// CleanupSnapshots destroys old snapshots considering retention limit.
309+
// CleanupSnapshots destroys old snapshots considering retention limit and related clones.
309310
func (m *Manager) CleanupSnapshots(retentionLimit int) ([]string, error) {
311+
clonesCmd := fmt.Sprintf("zfs list -S clones -o name,origin -H -r %s", m.config.Pool.Name)
312+
313+
clonesOutput, err := m.runner.Run(clonesCmd)
314+
if err != nil {
315+
return nil, errors.Wrap(err, "failed to list snapshots")
316+
}
317+
318+
busySnapshots := m.getBusySnapshotList(clonesOutput)
319+
310320
cleanupCmd := fmt.Sprintf(
311-
"zfs list -t snapshot -H -o name -s %s -s creation -r %s | grep -v clone | head -n -%d "+
321+
"zfs list -t snapshot -H -o name -s %s -s creation -r %s | grep -v clone | head -n -%d %s"+
312322
"| xargs -n1 --no-run-if-empty zfs destroy -R ",
313-
dataStateAtLabel, m.config.Pool.Name, retentionLimit)
323+
dataStateAtLabel, m.config.Pool.Name, retentionLimit, excludeBusySnapshots(busySnapshots))
314324

315325
out, err := m.runner.Run(cleanupCmd)
316326
if err != nil {
@@ -322,6 +332,52 @@ func (m *Manager) CleanupSnapshots(retentionLimit int) ([]string, error) {
322332
return lines, nil
323333
}
324334

335+
func (m *Manager) getBusySnapshotList(clonesOutput string) []string {
336+
systemClones, userClones := make(map[string]string), make(map[string]struct{})
337+
338+
userClonePrefix := m.config.Pool.Name + "/" + util.ClonePrefix
339+
340+
for _, line := range strings.Split(clonesOutput, "\n") {
341+
cloneLine := strings.FieldsFunc(line, unicode.IsSpace)
342+
343+
if len(cloneLine) != 2 || cloneLine[1] == "-" {
344+
continue
345+
}
346+
347+
if strings.HasPrefix(cloneLine[0], userClonePrefix) {
348+
origin := cloneLine[1]
349+
350+
if idx := strings.Index(origin, "@"); idx != -1 {
351+
origin = origin[:idx]
352+
}
353+
354+
userClones[origin] = struct{}{}
355+
356+
continue
357+
}
358+
359+
systemClones[cloneLine[0]] = cloneLine[1]
360+
}
361+
362+
busySnapshots := make([]string, 0, len(userClones))
363+
364+
for userClone := range userClones {
365+
busySnapshots = append(busySnapshots, systemClones[userClone])
366+
}
367+
368+
return busySnapshots
369+
}
370+
371+
// excludeBusySnapshots excludes snapshots that match a pattern by name.
372+
// The exclusion logic relies on the fact that snapshots have unique substrings (timestamps).
373+
func excludeBusySnapshots(busySnapshots []string) string {
374+
if len(busySnapshots) == 0 {
375+
return ""
376+
}
377+
378+
return fmt.Sprintf("| grep -Ev '%s' ", strings.Join(busySnapshots, "|"))
379+
}
380+
325381
// GetSessionState returns a state of a session.
326382
func (m *Manager) GetSessionState(name string) (*resources.SessionState, error) {
327383
entries, err := m.listFilesystems(m.config.Pool.Name)

pkg/services/provision/thinclones/zfs/zfs_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,47 @@ func TestFailedListClones(t *testing.T) {
113113
assert.Nil(t, cloneNames)
114114
assert.EqualError(t, err, "failed to list clones: runner error")
115115
}
116+
117+
func TestBusySnapshotList(t *testing.T) {
118+
m := Manager{config: Config{Pool: &resources.Pool{Name: "dblab_pool"}}}
119+
120+
out := `dblab_pool -
121+
dblab_pool/clone_pre_20210127105215 dblab_pool@snapshot_20210127105215_pre
122+
dblab_pool/clone_pre_20210127113000 dblab_pool@snapshot_20210127113000_pre
123+
dblab_pool/clone_pre_20210127120000 dblab_pool@snapshot_20210127120000_pre
124+
dblab_pool/clone_pre_20210127123000 dblab_pool@snapshot_20210127123000_pre
125+
dblab_pool/clone_pre_20210127130000 dblab_pool@snapshot_20210127130000_pre
126+
dblab_pool/clone_pre_20210127133000 dblab_pool@snapshot_20210127133000_pre
127+
dblab_pool/clone_pre_20210127140000 dblab_pool@snapshot_20210127140000_pre
128+
dblab_pool/dblab_clone_6000 dblab_pool/clone_pre_20210127133000@snapshot_20210127133008
129+
dblab_pool/dblab_clone_6001 dblab_pool/clone_pre_20210127123000@snapshot_20210127133008
130+
`
131+
expected := []string{"dblab_pool@snapshot_20210127133000_pre", "dblab_pool@snapshot_20210127123000_pre"}
132+
133+
list := m.getBusySnapshotList(out)
134+
assert.Equal(t, expected, list)
135+
}
136+
137+
func TestExcludingBusySnapshots(t *testing.T) {
138+
testCases := []struct {
139+
snapshotList []string
140+
result string
141+
}{
142+
{
143+
snapshotList: []string{},
144+
result: "",
145+
},
146+
{
147+
snapshotList: []string{"dblab_pool@snapshot_20210127133000_pre"},
148+
result: "| grep -Ev 'dblab_pool@snapshot_20210127133000_pre' ",
149+
},
150+
{
151+
snapshotList: []string{"dblab_pool@snapshot_20210127133000_pre", "dblab_pool@snapshot_20210127123000_pre"},
152+
result: "| grep -Ev 'dblab_pool@snapshot_20210127133000_pre|dblab_pool@snapshot_20210127123000_pre' ",
153+
},
154+
}
155+
156+
for _, tc := range testCases {
157+
assert.Equal(t, tc.result, excludeBusySnapshots(tc.snapshotList))
158+
}
159+
}

0 commit comments

Comments
 (0)