Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 524aaac

Browse files
wilfred-asomaniiStorj Robot
authored andcommitted
satellite/console: add pending delete project deletion chore
This chore, after a certain buffer time, deletes projects that were marked for deletion. Issue: storj/storj-private#1474 Change-Id: I0eba17c84643b433d8a927561a605b7fcdbf4b07
1 parent c1a70c5 commit 524aaac

File tree

5 files changed

+491
-1
lines changed

5 files changed

+491
-1
lines changed
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
// Copyright (C) 2025 Storj Labs, Inc.
2+
// See LICENSE for copying information.
3+
4+
package pendingdelete
5+
6+
import (
7+
"context"
8+
"sync"
9+
"sync/atomic"
10+
"time"
11+
12+
"github.com/spacemonkeygo/monkit/v3"
13+
"github.com/zeebo/errs"
14+
"go.uber.org/zap"
15+
16+
"storj.io/common/macaroon"
17+
"storj.io/common/sync2"
18+
"storj.io/common/uuid"
19+
"storj.io/storj/satellite/buckets"
20+
"storj.io/storj/satellite/console"
21+
"storj.io/storj/satellite/metabase"
22+
)
23+
24+
var (
25+
// Error defines the pendingdelete chore errors class.
26+
Error = errs.Class("pendingdelete")
27+
mon = monkit.Package()
28+
)
29+
30+
// Config contains configuration for pending deletion project cleanup.
31+
type Config struct {
32+
Enabled bool `help:"whether (pending deletion) user/project data should be deleted or not" default:"false"`
33+
Interval time.Duration `help:"how often to run this chore" default:"24h"`
34+
ListLimit int `help:"how many events to query in a batch" default:"100"`
35+
DeleteConcurrency int `help:"how many delete workers to run at a time" default:"1"`
36+
BufferTime time.Duration `help:"how long after the project was marked for deletion should we wait before deleting data" default:"720h"`
37+
}
38+
39+
// Chore completes deletion of data for projects
40+
// that have been pending deletion for a while.
41+
type Chore struct {
42+
log *zap.Logger
43+
config Config
44+
bucketsDB buckets.DB
45+
metabase *metabase.DB
46+
store console.DB
47+
48+
nowFn func() time.Time
49+
50+
Loop *sync2.Cycle
51+
}
52+
53+
// NewChore creates a new instance of this chore.
54+
func NewChore(log *zap.Logger, config Config,
55+
bucketsDB buckets.DB, consoleDB console.DB, metabase *metabase.DB,
56+
) *Chore {
57+
return &Chore{
58+
log: log,
59+
config: config,
60+
metabase: metabase,
61+
bucketsDB: bucketsDB,
62+
store: consoleDB,
63+
nowFn: time.Now,
64+
65+
Loop: sync2.NewCycle(config.Interval),
66+
}
67+
}
68+
69+
// Run starts this chore's loop.
70+
func (chore *Chore) Run(ctx context.Context) (err error) {
71+
defer mon.Task()(&ctx)(&err)
72+
73+
if !chore.config.Enabled {
74+
return nil
75+
}
76+
77+
return chore.Loop.Run(ctx, chore.runDeleteProjects)
78+
}
79+
80+
func (chore *Chore) runDeleteProjects(ctx context.Context) (err error) {
81+
defer mon.Task()(&ctx)(&err)
82+
chore.log.Debug("deleting pending deletion projects")
83+
84+
mu := new(sync.Mutex)
85+
var errGrp errs.Group
86+
87+
addErr := func(err error) {
88+
mu.Lock()
89+
errGrp.Add(err)
90+
mu.Unlock()
91+
}
92+
93+
var processedProjects atomic.Int64
94+
hasNext := true
95+
for hasNext {
96+
idsPage, err := chore.store.Projects().ListPendingDeletionBefore(
97+
ctx,
98+
0, // always on offset 0 because updating project status removes it from the list
99+
chore.config.ListLimit,
100+
chore.nowFn().Add(-chore.config.BufferTime),
101+
)
102+
if err != nil {
103+
chore.log.Error("failed to get projects for deletion", zap.Error(err))
104+
return err
105+
}
106+
hasNext = idsPage.Next
107+
108+
if !hasNext && len(idsPage.Ids) == 0 {
109+
break
110+
}
111+
112+
limiter := sync2.NewLimiter(chore.config.DeleteConcurrency)
113+
114+
for _, p := range idsPage.Ids {
115+
limiter.Go(ctx, func() {
116+
err := chore.deleteData(ctx, p.ProjectID, p.OwnerID)
117+
if err != nil {
118+
addErr(err)
119+
return
120+
}
121+
122+
err = chore.disableProject(ctx, p.ProjectID, p.OwnerID)
123+
if err != nil {
124+
addErr(err)
125+
return
126+
}
127+
processedProjects.Add(1)
128+
})
129+
}
130+
131+
limiter.Wait()
132+
}
133+
134+
chore.log.Info("finished deleting projects",
135+
zap.Int64("deleted_projects", processedProjects.Load()),
136+
)
137+
138+
return Error.Wrap(errGrp.Err())
139+
}
140+
141+
func (chore *Chore) deleteData(ctx context.Context, projectID, ownerID uuid.UUID) (err error) {
142+
mon.Task()(&ctx)(&err)
143+
144+
// first list buckets and delete data contained within them.
145+
listOptions := buckets.ListOptions{
146+
Direction: buckets.DirectionForward,
147+
}
148+
149+
allowedBuckets := macaroon.AllowedBuckets{
150+
All: true,
151+
}
152+
153+
bucketList := buckets.List{More: true}
154+
for bucketList.More {
155+
bucketList, err = chore.bucketsDB.ListBuckets(ctx, projectID, listOptions, allowedBuckets)
156+
if err != nil {
157+
chore.log.Error("failed to list buckets",
158+
zap.String("userID", ownerID.String()),
159+
zap.String("projectID", projectID.String()),
160+
zap.Error(err),
161+
)
162+
return err
163+
}
164+
165+
maxCommitDelay := 25 * time.Millisecond
166+
for _, bucket := range bucketList.Items {
167+
objectCount, err := chore.metabase.UncoordinatedDeleteAllBucketObjects(ctx, metabase.DeleteAllBucketObjects{
168+
Bucket: metabase.BucketLocation{
169+
ProjectID: projectID,
170+
BucketName: metabase.BucketName(bucket.Name),
171+
},
172+
BatchSize: 100,
173+
MaxStaleness: 10 * time.Second,
174+
MaxCommitDelay: &maxCommitDelay,
175+
})
176+
if err != nil {
177+
chore.log.Error(
178+
"failed to delete all bucket objects",
179+
zap.String("userID", ownerID.String()),
180+
zap.String("projectID", projectID.String()),
181+
zap.String("bucket", bucket.Name), zap.Error(err),
182+
)
183+
return err
184+
}
185+
chore.log.Info(
186+
"deleted data for bucket",
187+
zap.Int64("objectCount", objectCount),
188+
zap.String("userID", ownerID.String()),
189+
zap.String("projectID", projectID.String()),
190+
zap.String("bucket", bucket.Name),
191+
)
192+
}
193+
}
194+
195+
return nil
196+
}
197+
198+
func (chore *Chore) disableProject(ctx context.Context, projectID, ownerID uuid.UUID) (err error) {
199+
return chore.store.WithTx(ctx, func(ctx context.Context, tx console.DBTx) error {
200+
// delete project API keys.
201+
err = tx.APIKeys().DeleteAllByProjectID(ctx, projectID)
202+
if err != nil {
203+
chore.log.Error("failed to delete all API Keys for project",
204+
zap.String("projectID", projectID.String()),
205+
zap.String("userID", ownerID.String()),
206+
zap.Error(err),
207+
)
208+
return err
209+
}
210+
211+
// delete project domains.
212+
err = tx.Domains().DeleteAllByProjectID(ctx, projectID)
213+
if err != nil {
214+
chore.log.Error("failed to delete all domains for project",
215+
zap.String("projectID", projectID.String()),
216+
zap.String("userID", ownerID.String()),
217+
zap.Error(err),
218+
)
219+
}
220+
221+
// disable the project.
222+
err = tx.Projects().UpdateStatus(ctx, projectID, console.ProjectDisabled)
223+
if err != nil {
224+
chore.log.Error("failed to mark project as disabled",
225+
zap.String("projectID", projectID.String()),
226+
zap.String("userID", ownerID.String()),
227+
zap.Error(err),
228+
)
229+
return err
230+
}
231+
232+
chore.log.Info("marked project as disabled",
233+
zap.String("projectID", projectID.String()),
234+
zap.String("userID", ownerID.String()),
235+
)
236+
return nil
237+
})
238+
}
239+
240+
// Close stops chore.
241+
func (chore *Chore) Close() error {
242+
chore.Loop.Close()
243+
return nil
244+
}
245+
246+
// TestSetNowFn sets the function used to get the current time.
247+
// This is only to be used in tests.
248+
func (chore *Chore) TestSetNowFn(fn func() time.Time) {
249+
chore.nowFn = fn
250+
}
251+
252+
// TestSetDeleteConcurrency sets the delete concurrency for the chore.
253+
// This is only to be used in tests.
254+
func (chore *Chore) TestSetDeleteConcurrency(concurrency int) {
255+
chore.config.DeleteConcurrency = concurrency
256+
}

0 commit comments

Comments
 (0)