mirror of
https://github.com/garethgeorge/backrest.git
synced 2025-12-11 16:25:39 +00:00
feat: support restic check operation (#303)
This commit is contained in:
185
internal/orchestrator/tasks/taskcheck.go
Normal file
185
internal/orchestrator/tasks/taskcheck.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package tasks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
"github.com/garethgeorge/backrest/internal/hook"
|
||||
"github.com/garethgeorge/backrest/internal/ioutil"
|
||||
"github.com/garethgeorge/backrest/internal/oplog"
|
||||
"github.com/garethgeorge/backrest/internal/oplog/indexutil"
|
||||
"github.com/garethgeorge/backrest/internal/protoutil"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CheckTask struct {
|
||||
BaseTask
|
||||
force bool
|
||||
didRun bool
|
||||
}
|
||||
|
||||
func NewCheckTask(repoID, planID string, force bool) Task {
|
||||
return &CheckTask{
|
||||
BaseTask: BaseTask{
|
||||
TaskName: fmt.Sprintf("prune repo %q", repoID),
|
||||
TaskRepoID: repoID,
|
||||
TaskPlanID: planID,
|
||||
},
|
||||
force: force,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CheckTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error) {
|
||||
if t.force {
|
||||
if t.didRun {
|
||||
return NeverScheduledTask, nil
|
||||
}
|
||||
t.didRun = true
|
||||
return ScheduledTask{
|
||||
Task: t,
|
||||
RunAt: now,
|
||||
Op: &v1.Operation{
|
||||
Op: &v1.Operation_OperationCheck{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
repo, err := runner.GetRepo(t.RepoID())
|
||||
if err != nil {
|
||||
return ScheduledTask{}, fmt.Errorf("get repo %v: %w", t.RepoID(), err)
|
||||
}
|
||||
|
||||
if repo.CheckPolicy.GetSchedule() == nil {
|
||||
return NeverScheduledTask, nil
|
||||
}
|
||||
|
||||
var lastRan time.Time
|
||||
var foundBackup bool
|
||||
if err := runner.OpLog().ForEach(oplog.Query{RepoId: t.RepoID()}, indexutil.Reversed(indexutil.CollectAll()), func(op *v1.Operation) error {
|
||||
if _, ok := op.Op.(*v1.Operation_OperationCheck); ok {
|
||||
lastRan = time.Unix(0, op.UnixTimeEndMs*int64(time.Millisecond))
|
||||
return oplog.ErrStopIteration
|
||||
}
|
||||
if _, ok := op.Op.(*v1.Operation_OperationBackup); ok {
|
||||
foundBackup = true
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return NeverScheduledTask, fmt.Errorf("finding last check run time: %w", err)
|
||||
} else if !foundBackup {
|
||||
return NeverScheduledTask, nil
|
||||
}
|
||||
|
||||
zap.L().Debug("last prune time", zap.Time("time", lastRan), zap.String("repo", t.RepoID()))
|
||||
|
||||
runAt, err := protoutil.ResolveSchedule(repo.CheckPolicy.GetSchedule(), lastRan)
|
||||
if errors.Is(err, protoutil.ErrScheduleDisabled) {
|
||||
return NeverScheduledTask, nil
|
||||
} else if err != nil {
|
||||
return NeverScheduledTask, fmt.Errorf("resolve schedule: %w", err)
|
||||
}
|
||||
|
||||
return ScheduledTask{
|
||||
Task: t,
|
||||
RunAt: runAt,
|
||||
Op: &v1.Operation{
|
||||
Op: &v1.Operation_OperationCheck{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner) error {
|
||||
op := st.Op
|
||||
|
||||
repo, err := runner.GetRepoOrchestrator(t.RepoID())
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get repo %q: %w", t.RepoID(), err)
|
||||
}
|
||||
|
||||
if err := runner.ExecuteHooks([]v1.Hook_Condition{
|
||||
v1.Hook_CONDITION_CHECK_START,
|
||||
}, hook.HookVars{}); err != nil {
|
||||
// TODO: generalize this logic
|
||||
op.DisplayMessage = err.Error()
|
||||
var cancelErr *hook.HookErrorRequestCancel
|
||||
if errors.As(err, &cancelErr) {
|
||||
op.Status = v1.OperationStatus_STATUS_USER_CANCELLED // user visible cancelled status
|
||||
return nil
|
||||
}
|
||||
op.Status = v1.OperationStatus_STATUS_ERROR
|
||||
return fmt.Errorf("execute check start hooks: %w", err)
|
||||
}
|
||||
|
||||
err = repo.UnlockIfAutoEnabled(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auto unlock repo %q: %w", t.RepoID(), err)
|
||||
}
|
||||
|
||||
opCheck := &v1.Operation_OperationCheck{
|
||||
OperationCheck: &v1.OperationCheck{},
|
||||
}
|
||||
op.Op = opCheck
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
interval := time.NewTicker(1 * time.Second)
|
||||
defer interval.Stop()
|
||||
buf := ioutil.HeadWriter{Limit: 16 * 1024}
|
||||
bufWriter := ioutil.SynchronizedWriter{W: &buf}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-interval.C:
|
||||
bufWriter.Mu.Lock()
|
||||
output := string(buf.Bytes())
|
||||
bufWriter.Mu.Unlock()
|
||||
|
||||
if opCheck.OperationCheck.Output != string(output) {
|
||||
opCheck.OperationCheck.Output = string(output)
|
||||
|
||||
if err := runner.OpLog().Update(op); err != nil {
|
||||
zap.L().Error("update prune operation with status output", zap.Error(err))
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := repo.Check(ctx, &bufWriter); err != nil {
|
||||
cancel()
|
||||
|
||||
runner.ExecuteHooks([]v1.Hook_Condition{
|
||||
v1.Hook_CONDITION_CHECK_ERROR,
|
||||
v1.Hook_CONDITION_ANY_ERROR,
|
||||
}, hook.HookVars{
|
||||
Error: err.Error(),
|
||||
})
|
||||
|
||||
return fmt.Errorf("prune: %w", err)
|
||||
}
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
opCheck.OperationCheck.Output = string(buf.Bytes())
|
||||
|
||||
// Run a stats task after a successful prune
|
||||
if err := runner.ScheduleTask(NewStatsTask(t.RepoID(), PlanForSystemTasks, false), TaskPriorityStats); err != nil {
|
||||
zap.L().Error("schedule stats task", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := runner.ExecuteHooks([]v1.Hook_Condition{
|
||||
v1.Hook_CONDITION_CHECK_SUCCESS,
|
||||
}, hook.HookVars{}); err != nil {
|
||||
return fmt.Errorf("execute prune success hooks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user