diff --git a/go.mod b/go.mod index 90c6059b..f6e4e64b 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,5 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 // indirect ) diff --git a/go.sum b/go.sum index 92b97795..b6519a3c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -connectrpc.com/connect v1.14.0 h1:PDS+J7uoz5Oui2VEOMcfz6Qft7opQM9hPiKvtGC01pA= -connectrpc.com/connect v1.14.0/go.mod h1:uoAq5bmhhn43TwhaKdGKN/bZcGtzPW1v+ngDTn5u+8s= connectrpc.com/connect v1.15.0 h1:lFdeCbZrVVDydAqwr4xGV2y+ULn+0Z73s5JBj2LikWo= connectrpc.com/connect v1.15.0/go.mod h1:bQmjpDY8xItMnttnurVgOkHUBMRT9cpsNi2O4AjKhmA= github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= @@ -34,51 +32,31 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo= -google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= -google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641 h1:GihpvzHjeZHw+/mzsWpdxwr1LaG6E3ff/gyeZlVHbyc= -google.golang.org/genproto v0.0.0-20240228224816-df926f6c8641/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8= -google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641 h1:SO1wX9btGFrwj9EzH3ocqfwiPVOxfv4ggAJajzlHA5s= google.golang.org/genproto/googleapis/api v0.0.0-20240228224816-df926f6c8641/go.mod h1:wLupoVsUfYPgOMwjzhYFbaVklw/INms+dqTp0tc1fv8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641 h1:DKU1r6Tj5s1vlU/moGhuGz7E3xRfwjdAfDzbsaQJtEY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240228224816-df926f6c8641/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= -google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= -google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/internal/api/backresthandler.go b/internal/api/backresthandler.go index ceb8044e..40015bff 100644 --- a/internal/api/backresthandler.go +++ b/internal/api/backresthandler.go @@ -385,6 +385,20 @@ func (s *BackrestHandler) Unlock(ctx context.Context, req *connect.Request[types return connect.NewResponse(&emptypb.Empty{}), nil } +func (s *BackrestHandler) Stats(ctx context.Context, req *connect.Request[types.StringValue]) (*connect.Response[emptypb.Empty], error) { + at := time.Now() + var err error + var wg sync.WaitGroup + wg.Add(1) + s.orchestrator.ScheduleTask(orchestrator.NewOneoffStatsTask(s.orchestrator, req.Msg.Value, orchestrator.PlanForUnassociatedOperations, at), orchestrator.TaskPriorityInteractive+orchestrator.TaskPriorityStats, func(e error) { + err = e + wg.Done() + }) + wg.Wait() + return connect.NewResponse(&emptypb.Empty{}), err + +} + func (s *BackrestHandler) Cancel(ctx context.Context, req *connect.Request[types.Int64Value]) (*connect.Response[emptypb.Empty], error) { if err := s.orchestrator.CancelOperation(req.Msg.Value, v1.OperationStatus_STATUS_USER_CANCELLED); err != nil { return nil, err diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 9659183c..fd17167b 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -22,6 +22,8 @@ var ErrRepoNotFound = errors.New("repo not found") var ErrRepoInitializationFailed = errors.New("repo initialization failed") var ErrPlanNotFound = errors.New("plan not found") +const PlanForUnassociatedOperations = "_unassociated_" + const ( TaskPriorityDefault = 0 TaskPriorityInteractive = 10 diff --git a/internal/orchestrator/taskindexsnapshots.go b/internal/orchestrator/taskindexsnapshots.go index a0e23348..e5fcc2e4 100644 --- a/internal/orchestrator/taskindexsnapshots.go +++ b/internal/orchestrator/taskindexsnapshots.go @@ -14,8 +14,6 @@ import ( "go.uber.org/zap" ) -const planForUntrackedSnapshots = "_unassociated_" - // IndexSnapshotsTask tracks a forget operation. type IndexSnapshotsTask struct { orchestrator *Orchestrator // owning orchestrator @@ -186,5 +184,5 @@ func planForSnapshot(snapshot *v1.ResticSnapshot) string { return tag[5:] } } - return planForUntrackedSnapshots + return PlanForUnassociatedOperations } diff --git a/internal/orchestrator/taskprune.go b/internal/orchestrator/taskprune.go index 5bfc1d3a..23e95fa0 100644 --- a/internal/orchestrator/taskprune.go +++ b/internal/orchestrator/taskprune.go @@ -177,7 +177,7 @@ func (t *PruneTask) Run(ctx context.Context) error { return err } - t.orch.ScheduleTask(NewOneoffStatsTask(t.orch, t.plan, time.Now()), TaskPriorityStats) + t.orch.ScheduleTask(NewOneoffStatsTask(t.orch, t.plan.Repo, t.plan.Id, time.Now()), TaskPriorityStats) return nil } diff --git a/internal/orchestrator/taskstats.go b/internal/orchestrator/taskstats.go index 75ccaca7..070cdb92 100644 --- a/internal/orchestrator/taskstats.go +++ b/internal/orchestrator/taskstats.go @@ -2,7 +2,6 @@ package orchestrator import ( "context" - "errors" "fmt" "time" @@ -14,24 +13,26 @@ import ( // StatsTask tracks a restic stats operation. type StatsTask struct { TaskWithOperation - plan *v1.Plan - at *time.Time + planId string + repoId string + at *time.Time } var _ Task = &StatsTask{} -func NewOneoffStatsTask(orchestrator *Orchestrator, plan *v1.Plan, at time.Time) *StatsTask { +func NewOneoffStatsTask(orchestrator *Orchestrator, repoId, planId string, at time.Time) *StatsTask { return &StatsTask{ TaskWithOperation: TaskWithOperation{ orch: orchestrator, }, - plan: plan, - at: &at, + planId: planId, + repoId: repoId, + at: &at, } } func (t *StatsTask) Name() string { - return fmt.Sprintf("stats for plan %q", t.plan.Id) + return fmt.Sprintf("stats for repo %q", t.repoId) } func (t *StatsTask) Next(now time.Time) *time.Time { @@ -40,8 +41,8 @@ func (t *StatsTask) Next(now time.Time) *time.Time { t.at = nil if err := t.setOperation(&v1.Operation{ - PlanId: t.plan.Id, - RepoId: t.plan.Repo, + PlanId: t.planId, + RepoId: t.repoId, UnixTimeStartMs: timeToUnixMillis(*ret), Status: v1.OperationStatus_STATUS_PENDING, Op: &v1.Operation_OperationStats{}, @@ -54,14 +55,10 @@ func (t *StatsTask) Next(now time.Time) *time.Time { } func (t *StatsTask) Run(ctx context.Context) error { - if t.plan.Retention == nil { - return errors.New("plan does not have a retention policy") - } - if err := t.runWithOpAndContext(ctx, func(ctx context.Context, op *v1.Operation) error { - repo, err := t.orch.GetRepo(t.plan.Repo) + repo, err := t.orch.GetRepo(t.repoId) if err != nil { - return fmt.Errorf("get repo %q: %w", t.plan.Repo, err) + return fmt.Errorf("get repo %q: %w", t.repoId, err) } stats, err := repo.Stats(ctx) @@ -77,8 +74,9 @@ func (t *StatsTask) Run(ctx context.Context) error { return err }); err != nil { - repo, _ := t.orch.GetRepo(t.plan.Repo) - t.orch.hookExecutor.ExecuteHooks(repo.Config(), t.plan, "", []v1.Hook_Condition{ + repo, _ := t.orch.GetRepo(t.repoId) + plan, _ := t.orch.GetPlan(t.planId) + t.orch.hookExecutor.ExecuteHooks(repo.Config(), plan, "", []v1.Hook_Condition{ v1.Hook_CONDITION_ANY_ERROR, }, hook.HookVars{ Task: t.Name(), diff --git a/webui/src/views/RepoView.tsx b/webui/src/views/RepoView.tsx index 7f75847e..d7b791d7 100644 --- a/webui/src/views/RepoView.tsx +++ b/webui/src/views/RepoView.tsx @@ -26,6 +26,10 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { await backrestService.indexSnapshots(new StringValue({ value: repo.id! })); } + const handleStatsNow = async () => { + await backrestService.stats(new StringValue({ value: repo.id! })); + } + // Gracefully handle deletions by checking if the plan is still in the config. let repoInConfig = config?.repos?.find((r) => r.id === repo.id); if (!repoInConfig) { @@ -91,6 +95,12 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { Index Snapshots + + + + Compute Stats + +