2018-09-04 23:46:02 +02:00
|
|
|
package snapper
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"sort"
|
|
|
|
"sync"
|
2019-03-22 19:41:12 +01:00
|
|
|
"time"
|
2018-09-04 23:46:02 +02:00
|
|
|
|
2019-03-22 19:41:12 +01:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
|
|
|
|
"github.com/zrepl/zrepl/config"
|
|
|
|
"github.com/zrepl/zrepl/daemon/filters"
|
2019-07-26 21:12:21 +02:00
|
|
|
"github.com/zrepl/zrepl/daemon/hooks"
|
2019-03-22 19:41:12 +01:00
|
|
|
"github.com/zrepl/zrepl/logger"
|
|
|
|
"github.com/zrepl/zrepl/zfs"
|
|
|
|
)
|
2018-09-04 23:46:02 +02:00
|
|
|
|
|
|
|
//go:generate stringer -type=SnapState
|
|
|
|
type SnapState uint
|
|
|
|
|
|
|
|
const (
|
|
|
|
SnapPending SnapState = 1 << iota
|
|
|
|
SnapStarted
|
|
|
|
SnapDone
|
|
|
|
SnapError
|
|
|
|
)
|
|
|
|
|
2019-09-19 00:13:55 +02:00
|
|
|
// All fields protected by Snapper.mtx
|
2018-09-04 23:46:02 +02:00
|
|
|
type snapProgress struct {
|
|
|
|
state SnapState
|
|
|
|
|
|
|
|
// SnapStarted, SnapDone, SnapError
|
2019-09-19 00:13:55 +02:00
|
|
|
name string
|
|
|
|
startAt time.Time
|
|
|
|
hookPlan *hooks.Plan
|
2018-09-04 23:46:02 +02:00
|
|
|
|
|
|
|
// SnapDone
|
|
|
|
doneAt time.Time
|
|
|
|
|
2019-07-26 21:12:21 +02:00
|
|
|
// SnapErr TODO disambiguate state
|
|
|
|
runResults hooks.PlanReport
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type args struct {
|
|
|
|
ctx context.Context
|
|
|
|
log Logger
|
|
|
|
prefix string
|
|
|
|
interval time.Duration
|
|
|
|
fsf *filters.DatasetMapFilter
|
2019-03-22 19:41:12 +01:00
|
|
|
snapshotsTaken chan<- struct{}
|
2019-07-26 21:12:21 +02:00
|
|
|
hooks *hooks.List
|
|
|
|
dryRun bool
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type Snapper struct {
|
|
|
|
args args
|
|
|
|
|
2019-03-22 19:41:12 +01:00
|
|
|
mtx sync.Mutex
|
2018-09-04 23:46:02 +02:00
|
|
|
state State
|
|
|
|
|
|
|
|
// set in state Plan, used in Waiting
|
|
|
|
lastInvocation time.Time
|
|
|
|
|
|
|
|
// valid for state Snapshotting
|
2019-09-19 00:13:55 +02:00
|
|
|
plan map[*zfs.DatasetPath]*snapProgress
|
2018-09-04 23:46:02 +02:00
|
|
|
|
|
|
|
// valid for state SyncUp and Waiting
|
|
|
|
sleepUntil time.Time
|
|
|
|
|
|
|
|
// valid for state Err
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
//go:generate stringer -type=State
|
|
|
|
type State uint
|
|
|
|
|
|
|
|
const (
|
2019-03-22 19:41:12 +01:00
|
|
|
SyncUp State = 1 << iota
|
2019-03-15 13:43:19 +01:00
|
|
|
SyncUpErrWait
|
2018-09-04 23:46:02 +02:00
|
|
|
Planning
|
|
|
|
Snapshotting
|
|
|
|
Waiting
|
2018-09-24 12:29:34 +02:00
|
|
|
ErrorWait
|
2018-09-24 14:40:47 +02:00
|
|
|
Stopped
|
2018-09-04 23:46:02 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
func (s State) sf() state {
|
|
|
|
m := map[State]state{
|
2019-03-22 19:41:12 +01:00
|
|
|
SyncUp: syncUp,
|
2019-03-15 13:43:19 +01:00
|
|
|
SyncUpErrWait: wait,
|
2019-03-22 19:41:12 +01:00
|
|
|
Planning: plan,
|
|
|
|
Snapshotting: snapshot,
|
|
|
|
Waiting: wait,
|
|
|
|
ErrorWait: wait,
|
|
|
|
Stopped: nil,
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
return m[s]
|
|
|
|
}
|
|
|
|
|
|
|
|
type updater func(u func(*Snapper)) State
|
|
|
|
type state func(a args, u updater) state
|
|
|
|
|
|
|
|
type contextKey int
|
|
|
|
|
|
|
|
const (
|
|
|
|
contextKeyLog contextKey = 0
|
|
|
|
)
|
|
|
|
|
|
|
|
type Logger = logger.Logger
|
|
|
|
|
|
|
|
func WithLogger(ctx context.Context, log Logger) context.Context {
|
|
|
|
return context.WithValue(ctx, contextKeyLog, log)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getLogger(ctx context.Context) Logger {
|
|
|
|
if log, ok := ctx.Value(contextKeyLog).(Logger); ok {
|
|
|
|
return log
|
|
|
|
}
|
|
|
|
return logger.NewNullLogger()
|
|
|
|
}
|
|
|
|
|
2018-10-11 15:22:52 +02:00
|
|
|
func PeriodicFromConfig(g *config.Global, fsf *filters.DatasetMapFilter, in *config.SnapshottingPeriodic) (*Snapper, error) {
|
|
|
|
if in.Prefix == "" {
|
2018-09-04 23:46:02 +02:00
|
|
|
return nil, errors.New("prefix must not be empty")
|
|
|
|
}
|
|
|
|
if in.Interval <= 0 {
|
|
|
|
return nil, errors.New("interval must be positive")
|
|
|
|
}
|
|
|
|
|
2019-07-26 21:12:21 +02:00
|
|
|
hookList, err := hooks.ListFromConfig(&in.Hooks)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "hook config error")
|
|
|
|
}
|
|
|
|
|
2018-09-04 23:46:02 +02:00
|
|
|
args := args{
|
2019-03-22 19:41:12 +01:00
|
|
|
prefix: in.Prefix,
|
2018-09-04 23:46:02 +02:00
|
|
|
interval: in.Interval,
|
2019-03-22 19:41:12 +01:00
|
|
|
fsf: fsf,
|
2019-07-26 21:12:21 +02:00
|
|
|
hooks: hookList,
|
2018-09-04 23:46:02 +02:00
|
|
|
// ctx and log is set in Run()
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Snapper{state: SyncUp, args: args}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Snapper) Run(ctx context.Context, snapshotsTaken chan<- struct{}) {
|
|
|
|
|
|
|
|
getLogger(ctx).Debug("start")
|
|
|
|
defer getLogger(ctx).Debug("stop")
|
|
|
|
|
|
|
|
s.args.snapshotsTaken = snapshotsTaken
|
|
|
|
s.args.ctx = ctx
|
|
|
|
s.args.log = getLogger(ctx)
|
2019-07-26 21:12:21 +02:00
|
|
|
s.args.dryRun = false // for future expansion
|
2018-09-04 23:46:02 +02:00
|
|
|
|
|
|
|
u := func(u func(*Snapper)) State {
|
|
|
|
s.mtx.Lock()
|
|
|
|
defer s.mtx.Unlock()
|
|
|
|
if u != nil {
|
|
|
|
u(s)
|
|
|
|
}
|
|
|
|
return s.state
|
|
|
|
}
|
|
|
|
|
|
|
|
var st state = syncUp
|
|
|
|
|
|
|
|
for st != nil {
|
|
|
|
pre := u(nil)
|
|
|
|
st = st(s.args, u)
|
|
|
|
post := u(nil)
|
|
|
|
getLogger(ctx).
|
|
|
|
WithField("transition", fmt.Sprintf("%s=>%s", pre, post)).
|
|
|
|
Debug("state transition")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func onErr(err error, u updater) state {
|
|
|
|
return u(func(s *Snapper) {
|
|
|
|
s.err = err
|
2019-03-15 13:43:19 +01:00
|
|
|
preState := s.state
|
|
|
|
switch s.state {
|
|
|
|
case SyncUp:
|
|
|
|
s.state = SyncUpErrWait
|
|
|
|
case Planning:
|
|
|
|
fallthrough
|
|
|
|
case Snapshotting:
|
|
|
|
s.state = ErrorWait
|
|
|
|
}
|
|
|
|
s.args.log.WithError(err).WithField("pre_state", preState).WithField("post_state", s.state).Error("snapshotting error")
|
2018-09-04 23:46:02 +02:00
|
|
|
}).sf()
|
|
|
|
}
|
|
|
|
|
2018-09-24 14:40:47 +02:00
|
|
|
func onMainCtxDone(ctx context.Context, u updater) state {
|
|
|
|
return u(func(s *Snapper) {
|
|
|
|
s.err = ctx.Err()
|
|
|
|
s.state = Stopped
|
|
|
|
}).sf()
|
|
|
|
}
|
|
|
|
|
2018-09-04 23:46:02 +02:00
|
|
|
func syncUp(a args, u updater) state {
|
2019-03-15 13:43:19 +01:00
|
|
|
u(func(snapper *Snapper) {
|
|
|
|
snapper.lastInvocation = time.Now()
|
|
|
|
})
|
2018-12-11 22:01:50 +01:00
|
|
|
fss, err := listFSes(a.ctx, a.fsf)
|
2018-09-04 23:46:02 +02:00
|
|
|
if err != nil {
|
|
|
|
return onErr(err, u)
|
|
|
|
}
|
|
|
|
syncPoint, err := findSyncPoint(a.log, fss, a.prefix, a.interval)
|
|
|
|
if err != nil {
|
|
|
|
return onErr(err, u)
|
|
|
|
}
|
2019-03-22 19:41:12 +01:00
|
|
|
u(func(s *Snapper) {
|
2018-09-04 23:46:02 +02:00
|
|
|
s.sleepUntil = syncPoint
|
|
|
|
})
|
2019-03-22 20:45:27 +01:00
|
|
|
t := time.NewTimer(time.Until(syncPoint))
|
2018-09-04 23:46:02 +02:00
|
|
|
defer t.Stop()
|
|
|
|
select {
|
|
|
|
case <-t.C:
|
|
|
|
return u(func(s *Snapper) {
|
|
|
|
s.state = Planning
|
|
|
|
}).sf()
|
|
|
|
case <-a.ctx.Done():
|
2018-09-24 14:40:47 +02:00
|
|
|
return onMainCtxDone(a.ctx, u)
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func plan(a args, u updater) state {
|
|
|
|
u(func(snapper *Snapper) {
|
|
|
|
snapper.lastInvocation = time.Now()
|
|
|
|
})
|
2018-12-11 22:01:50 +01:00
|
|
|
fss, err := listFSes(a.ctx, a.fsf)
|
2018-09-04 23:46:02 +02:00
|
|
|
if err != nil {
|
|
|
|
return onErr(err, u)
|
|
|
|
}
|
|
|
|
|
2019-09-19 00:13:55 +02:00
|
|
|
plan := make(map[*zfs.DatasetPath]*snapProgress, len(fss))
|
2018-09-04 23:46:02 +02:00
|
|
|
for _, fs := range fss {
|
2019-09-19 00:13:55 +02:00
|
|
|
plan[fs] = &snapProgress{state: SnapPending}
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
return u(func(s *Snapper) {
|
|
|
|
s.state = Snapshotting
|
|
|
|
s.plan = plan
|
2019-09-19 00:13:55 +02:00
|
|
|
s.err = nil
|
2018-09-04 23:46:02 +02:00
|
|
|
}).sf()
|
|
|
|
}
|
|
|
|
|
|
|
|
func snapshot(a args, u updater) state {
|
|
|
|
|
2019-09-19 00:13:55 +02:00
|
|
|
var plan map[*zfs.DatasetPath]*snapProgress
|
2018-09-04 23:46:02 +02:00
|
|
|
u(func(snapper *Snapper) {
|
|
|
|
plan = snapper.plan
|
|
|
|
})
|
|
|
|
|
2019-07-26 21:12:21 +02:00
|
|
|
hookMatchCount := make(map[hooks.Hook]int, len(*a.hooks))
|
|
|
|
for _, h := range *a.hooks {
|
|
|
|
hookMatchCount[h] = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
anyFsHadErr := false
|
2018-09-04 23:46:02 +02:00
|
|
|
// TODO channel programs -> allow a little jitter?
|
|
|
|
for fs, progress := range plan {
|
|
|
|
suffix := time.Now().In(time.UTC).Format("20060102_150405_000")
|
|
|
|
snapname := fmt.Sprintf("%s%s", a.prefix, suffix)
|
|
|
|
|
|
|
|
l := a.log.
|
|
|
|
WithField("fs", fs.ToString()).
|
|
|
|
WithField("snap", snapname)
|
|
|
|
|
2019-07-26 21:12:21 +02:00
|
|
|
hookEnvExtra := hooks.Env{
|
|
|
|
hooks.EnvFS: fs.ToString(),
|
|
|
|
hooks.EnvSnapshot: snapname,
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
|
2019-07-26 21:12:21 +02:00
|
|
|
jobCallback := hooks.NewCallbackHookForFilesystem("snapshot", fs, func(_ context.Context) (err error) {
|
|
|
|
l.Debug("create snapshot")
|
|
|
|
err = zfs.ZFSSnapshot(fs, snapname, false) // TODO propagagte context to ZFSSnapshot
|
|
|
|
if err != nil {
|
|
|
|
l.WithError(err).Error("cannot create snapshot")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
})
|
|
|
|
|
|
|
|
fsHadErr := false
|
|
|
|
var planReport hooks.PlanReport
|
|
|
|
var plan *hooks.Plan
|
|
|
|
{
|
|
|
|
filteredHooks, err := a.hooks.CopyFilteredForFilesystem(fs)
|
|
|
|
if err != nil {
|
|
|
|
l.WithError(err).Error("unexpected filter error")
|
|
|
|
fsHadErr = true
|
|
|
|
goto updateFSState
|
|
|
|
}
|
|
|
|
// account for running hooks
|
|
|
|
for _, h := range filteredHooks {
|
|
|
|
hookMatchCount[h] = hookMatchCount[h] + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
var planErr error
|
|
|
|
plan, planErr = hooks.NewPlan(&filteredHooks, hooks.PhaseSnapshot, jobCallback, hookEnvExtra)
|
|
|
|
if planErr != nil {
|
|
|
|
fsHadErr = true
|
|
|
|
l.WithError(planErr).Error("cannot create job hook plan")
|
|
|
|
goto updateFSState
|
|
|
|
}
|
|
|
|
}
|
2019-09-19 00:13:55 +02:00
|
|
|
u(func(snapper *Snapper) {
|
|
|
|
progress.name = snapname
|
|
|
|
progress.startAt = time.Now()
|
|
|
|
progress.hookPlan = plan
|
|
|
|
progress.state = SnapStarted
|
|
|
|
})
|
2019-07-26 21:12:21 +02:00
|
|
|
{
|
|
|
|
l := hooks.GetLogger(a.ctx).WithField("fs", fs.ToString()).WithField("snap", snapname)
|
|
|
|
l.WithField("report", plan.Report().String()).Debug("begin run job plan")
|
|
|
|
plan.Run(hooks.WithLogger(a.ctx, l), a.dryRun)
|
|
|
|
planReport = plan.Report()
|
|
|
|
fsHadErr = planReport.HadError() // not just fatal errors
|
|
|
|
if fsHadErr {
|
|
|
|
l.WithField("report", planReport.String()).Error("end run job plan with error")
|
|
|
|
} else {
|
|
|
|
l.WithField("report", planReport.String()).Info("end run job plan successful")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateFSState:
|
|
|
|
anyFsHadErr = anyFsHadErr || fsHadErr
|
2018-09-04 23:46:02 +02:00
|
|
|
u(func(snapper *Snapper) {
|
2019-09-19 00:13:55 +02:00
|
|
|
progress.doneAt = time.Now()
|
2018-09-04 23:46:02 +02:00
|
|
|
progress.state = SnapDone
|
2019-07-26 21:12:21 +02:00
|
|
|
if fsHadErr {
|
2018-09-04 23:46:02 +02:00
|
|
|
progress.state = SnapError
|
|
|
|
}
|
2019-07-26 21:12:21 +02:00
|
|
|
progress.runResults = planReport
|
2018-09-04 23:46:02 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
select {
|
|
|
|
case a.snapshotsTaken <- struct{}{}:
|
|
|
|
default:
|
2018-09-24 12:29:34 +02:00
|
|
|
if a.snapshotsTaken != nil {
|
|
|
|
a.log.Warn("callback channel is full, discarding snapshot update event")
|
|
|
|
}
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
|
2019-07-26 21:12:21 +02:00
|
|
|
for h, mc := range hookMatchCount {
|
|
|
|
if mc == 0 {
|
|
|
|
hookIdx := -1
|
|
|
|
for idx, ah := range *a.hooks {
|
|
|
|
if ah == h {
|
|
|
|
hookIdx = idx
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
a.log.WithField("hook", h.String()).WithField("hook_number", hookIdx+1).Warn("hook did not match any snapshotted filesystems")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 23:46:02 +02:00
|
|
|
return u(func(snapper *Snapper) {
|
2019-07-26 21:12:21 +02:00
|
|
|
if anyFsHadErr {
|
2018-09-24 12:29:34 +02:00
|
|
|
snapper.state = ErrorWait
|
|
|
|
snapper.err = errors.New("one or more snapshots could not be created, check logs for details")
|
2018-09-04 23:46:02 +02:00
|
|
|
} else {
|
|
|
|
snapper.state = Waiting
|
2019-09-19 00:13:55 +02:00
|
|
|
snapper.err = nil
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
}).sf()
|
|
|
|
}
|
|
|
|
|
|
|
|
func wait(a args, u updater) state {
|
|
|
|
var sleepUntil time.Time
|
|
|
|
u(func(snapper *Snapper) {
|
|
|
|
lastTick := snapper.lastInvocation
|
|
|
|
snapper.sleepUntil = lastTick.Add(a.interval)
|
|
|
|
sleepUntil = snapper.sleepUntil
|
2019-03-15 13:43:19 +01:00
|
|
|
log := a.log.WithField("sleep_until", sleepUntil).WithField("duration", a.interval)
|
|
|
|
logFunc := log.Debug
|
|
|
|
if snapper.state == ErrorWait || snapper.state == SyncUpErrWait {
|
|
|
|
logFunc = log.Error
|
|
|
|
}
|
|
|
|
logFunc("enter wait-state after error")
|
2018-09-04 23:46:02 +02:00
|
|
|
})
|
|
|
|
|
2019-03-22 20:45:27 +01:00
|
|
|
t := time.NewTimer(time.Until(sleepUntil))
|
2018-09-04 23:46:02 +02:00
|
|
|
defer t.Stop()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case <-t.C:
|
|
|
|
return u(func(snapper *Snapper) {
|
|
|
|
snapper.state = Planning
|
|
|
|
}).sf()
|
|
|
|
case <-a.ctx.Done():
|
2018-09-24 14:40:47 +02:00
|
|
|
return onMainCtxDone(a.ctx, u)
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-11 22:01:50 +01:00
|
|
|
func listFSes(ctx context.Context, mf *filters.DatasetMapFilter) (fss []*zfs.DatasetPath, err error) {
|
|
|
|
return zfs.ZFSListMapping(ctx, mf)
|
2018-09-04 23:46:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func findSyncPoint(log Logger, fss []*zfs.DatasetPath, prefix string, interval time.Duration) (syncPoint time.Time, err error) {
|
|
|
|
type snapTime struct {
|
|
|
|
ds *zfs.DatasetPath
|
|
|
|
time time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(fss) == 0 {
|
|
|
|
return time.Now(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
snaptimes := make([]snapTime, 0, len(fss))
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
log.Debug("examine filesystem state")
|
|
|
|
for _, d := range fss {
|
|
|
|
|
|
|
|
l := log.WithField("fs", d.ToString())
|
|
|
|
|
|
|
|
fsvs, err := zfs.ZFSListFilesystemVersions(d, filters.NewTypedPrefixFilter(prefix, zfs.Snapshot))
|
|
|
|
if err != nil {
|
|
|
|
l.WithError(err).Error("cannot list filesystem versions")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(fsvs) <= 0 {
|
|
|
|
l.WithField("prefix", prefix).Debug("no filesystem versions with prefix")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort versions by creation
|
|
|
|
sort.SliceStable(fsvs, func(i, j int) bool {
|
|
|
|
return fsvs[i].CreateTXG < fsvs[j].CreateTXG
|
|
|
|
})
|
|
|
|
|
|
|
|
latest := fsvs[len(fsvs)-1]
|
|
|
|
l.WithField("creation", latest.Creation).
|
|
|
|
Debug("found latest snapshot")
|
|
|
|
|
|
|
|
since := now.Sub(latest.Creation)
|
|
|
|
if since < 0 {
|
|
|
|
l.WithField("snapshot", latest.Name).
|
|
|
|
WithField("creation", latest.Creation).
|
|
|
|
Error("snapshot is from the future")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
next := now
|
|
|
|
if since < interval {
|
|
|
|
next = latest.Creation.Add(interval)
|
|
|
|
}
|
|
|
|
snaptimes = append(snaptimes, snapTime{d, next})
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(snaptimes) == 0 {
|
|
|
|
snaptimes = append(snaptimes, snapTime{nil, now})
|
|
|
|
}
|
|
|
|
|
|
|
|
sort.Slice(snaptimes, func(i, j int) bool {
|
|
|
|
return snaptimes[i].time.Before(snaptimes[j].time)
|
|
|
|
})
|
|
|
|
|
|
|
|
return snaptimes[0].time, nil
|
|
|
|
|
|
|
|
}
|