mirror of
https://github.com/garethgeorge/backrest.git
synced 2025-12-12 16:55:39 +00:00
204 lines
5.0 KiB
Go
204 lines
5.0 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
|
"github.com/garethgeorge/backrest/internal/hook"
|
|
"github.com/garethgeorge/backrest/internal/oplog"
|
|
"github.com/garethgeorge/backrest/internal/oplog/indexutil"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// PruneTask tracks a forget operation.
|
|
type PruneTask struct {
|
|
TaskWithOperation
|
|
plan *v1.Plan
|
|
at *time.Time
|
|
force bool
|
|
}
|
|
|
|
var _ Task = &PruneTask{}
|
|
|
|
func NewOneoffPruneTask(orchestrator *Orchestrator, plan *v1.Plan, at time.Time, force bool) *PruneTask {
|
|
return &PruneTask{
|
|
TaskWithOperation: TaskWithOperation{
|
|
orch: orchestrator,
|
|
},
|
|
plan: plan,
|
|
at: &at,
|
|
force: force, // overrides the PrunePolicy's MaxFrequencyDays
|
|
}
|
|
}
|
|
|
|
func (t *PruneTask) Name() string {
|
|
return fmt.Sprintf("prune for plan %q", t.plan.Id)
|
|
}
|
|
|
|
func (t *PruneTask) Next(now time.Time) *time.Time {
|
|
shouldRun, err := t.shouldRun(now)
|
|
if err != nil {
|
|
zap.S().Errorf("task %v failed to check if it should run: %v", t.Name(), err)
|
|
}
|
|
if !shouldRun {
|
|
return nil
|
|
}
|
|
|
|
ret := t.at
|
|
if ret != nil {
|
|
t.at = nil
|
|
if err := t.setOperation(&v1.Operation{
|
|
PlanId: t.plan.Id,
|
|
RepoId: t.plan.Repo,
|
|
UnixTimeStartMs: timeToUnixMillis(*ret),
|
|
Status: v1.OperationStatus_STATUS_PENDING,
|
|
Op: &v1.Operation_OperationPrune{},
|
|
}); err != nil {
|
|
zap.S().Errorf("task %v failed to add operation to oplog: %v", t.Name(), err)
|
|
return nil
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (t *PruneTask) shouldRun(now time.Time) (bool, error) {
|
|
if t.force {
|
|
return true, nil
|
|
}
|
|
|
|
repo, err := t.orch.GetRepo(t.plan.Repo)
|
|
if err != nil {
|
|
return false, fmt.Errorf("get repo %v: %w", t.plan.Repo, err)
|
|
}
|
|
|
|
nextPruneTime, err := t.getNextPruneTime(repo, repo.repoConfig.PrunePolicy)
|
|
if err != nil {
|
|
return false, fmt.Errorf("get next prune time: %w", err)
|
|
}
|
|
|
|
return nextPruneTime.Before(now), nil
|
|
}
|
|
|
|
func (t *PruneTask) getNextPruneTime(repo *RepoOrchestrator, policy *v1.PrunePolicy) (time.Time, error) {
|
|
var lastPruneTime time.Time
|
|
t.orch.OpLog.ForEachByRepo(t.plan.Repo, indexutil.Reversed(indexutil.CollectAll()), func(op *v1.Operation) error {
|
|
if _, ok := op.Op.(*v1.Operation_OperationPrune); ok {
|
|
lastPruneTime = time.Unix(0, op.UnixTimeStartMs*int64(time.Millisecond))
|
|
return oplog.ErrStopIteration
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if repo.repoConfig.PrunePolicy != nil {
|
|
return lastPruneTime.Add(time.Duration(repo.repoConfig.PrunePolicy.MaxFrequencyDays) * 24 * time.Hour), nil
|
|
} else {
|
|
return lastPruneTime.Add(7 * 24 * time.Hour), nil // default to 7 days.
|
|
}
|
|
}
|
|
|
|
func (t *PruneTask) Run(ctx context.Context) error {
|
|
if err := t.runWithOpAndContext(ctx, func(ctx context.Context, op *v1.Operation) error {
|
|
repo, err := t.orch.GetRepo(t.plan.Repo)
|
|
if err != nil {
|
|
return fmt.Errorf("get repo %v: %w", t.plan.Repo, err)
|
|
}
|
|
|
|
err = repo.UnlockIfAutoEnabled(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("auto unlock repo %q: %w", t.plan.Repo, err)
|
|
}
|
|
|
|
opPrune := &v1.Operation_OperationPrune{
|
|
OperationPrune: &v1.OperationPrune{},
|
|
}
|
|
op.Op = opPrune
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
interval := time.NewTicker(1 * time.Second)
|
|
defer interval.Stop()
|
|
var buf synchronizedBuffer
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-interval.C:
|
|
output := buf.String()
|
|
if len(output) > 8*1024 { // only provide live status upto the first 8K of output.
|
|
output = output[:len(output)-8*1024]
|
|
}
|
|
|
|
if opPrune.OperationPrune.Output != output {
|
|
opPrune.OperationPrune.Output = buf.String()
|
|
|
|
if err := t.orch.OpLog.Update(op); err != nil {
|
|
zap.L().Error("update prune operation with status output", zap.Error(err))
|
|
}
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
if err := repo.Prune(ctx, &buf); err != nil {
|
|
cancel()
|
|
return fmt.Errorf("prune: %w", err)
|
|
}
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
// TODO: it would be best to store the output in separate storage for large status data.
|
|
output := buf.String()
|
|
if len(output) > 8*1024 { // only save the first 4K of output.
|
|
output = output[:len(output)-8*1024]
|
|
}
|
|
op.Op = &v1.Operation_OperationPrune{
|
|
OperationPrune: &v1.OperationPrune{
|
|
Output: output,
|
|
},
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
repo, _ := t.orch.GetRepo(t.plan.Repo)
|
|
t.orch.hookExecutor.ExecuteHooks(repo.Config(), t.plan, "", []v1.Hook_Condition{
|
|
v1.Hook_CONDITION_ANY_ERROR,
|
|
}, hook.HookVars{
|
|
Task: t.Name(),
|
|
Error: err.Error(),
|
|
})
|
|
return err
|
|
}
|
|
|
|
t.orch.ScheduleTask(NewOneoffStatsTask(t.orch, t.plan.Repo, t.plan.Id, time.Now()), TaskPriorityStats)
|
|
|
|
return nil
|
|
}
|
|
|
|
// synchronizedBuffer is used for collecting prune command's output
|
|
type synchronizedBuffer struct {
|
|
mu sync.Mutex
|
|
buf bytes.Buffer
|
|
}
|
|
|
|
func (w *synchronizedBuffer) Write(p []byte) (n int, err error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
return w.buf.Write(p)
|
|
}
|
|
|
|
func (w *synchronizedBuffer) String() string {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
|
|
return w.buf.String()
|
|
}
|