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
+
+