Skip to content

Commit 52b094b

Browse files
committed
Add support for db vacate command
1 parent 8ad0eed commit 52b094b

File tree

13 files changed

+406
-8
lines changed

13 files changed

+406
-8
lines changed

api/api.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ type QueryWarning struct {
122122
Warning string `json:"warning" super:"warning"`
123123
}
124124

125+
type VacateResponse struct {
126+
CommitIDs []ksuid.KSUID `super:"commit_ids"`
127+
}
128+
125129
type VacuumResponse struct {
126130
ObjectIDs []ksuid.KSUID `super:"object_ids"`
127131
}

api/client/connection.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,17 @@ func (c *Connection) delete(ctx context.Context, poolID ksuid.KSUID, branchName
373373
return commit, err
374374
}
375375

376+
func (c *Connection) Vacate(ctx context.Context, pool, revision string, dryrun bool) (api.VacateResponse, error) {
377+
path := urlPath("pool", pool, "revision", revision, "vacate")
378+
if dryrun {
379+
path += "?dryrun=true"
380+
}
381+
req := c.NewRequest(ctx, http.MethodPost, path, nil)
382+
var res api.VacateResponse
383+
err := c.doAndUnmarshal(req, &res)
384+
return res, err
385+
}
386+
376387
func (c *Connection) Vacuum(ctx context.Context, pool, revision string, dryrun bool) (api.VacuumResponse, error) {
377388
path := urlPath("pool", pool, "revision", revision, "vacuum")
378389
if dryrun {

cmd/super/db/vacate/command.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ package vacate
33
import (
44
"errors"
55
"flag"
6+
"fmt"
7+
"strings"
68

9+
"github.com/brimdata/super/cli/poolflags"
710
"github.com/brimdata/super/cmd/super/db"
811
"github.com/brimdata/super/pkg/charm"
12+
"github.com/brimdata/super/pkg/plural"
913
)
1014

1115
var spec = &charm.Spec{
1216
Name: "vacate",
1317
Usage: "vacate [options] commit",
14-
Short: "compact a pool's commit history by squashing old commit objects",
18+
Short: "compact a pool's commit history by removing old commit objects",
1519
Long: `
1620
See https://superdb.org/command/db.html#super-db-vacate
1721
`,
@@ -24,12 +28,62 @@ func init() {
2428

2529
type Command struct {
2630
*db.Command
31+
poolFlags poolflags.Flags
32+
dryrun bool
33+
force bool
2734
}
2835

2936
func New(parent charm.Command, f *flag.FlagSet) (charm.Command, error) {
30-
return &Command{Command: parent.(*db.Command)}, nil
37+
c := &Command{Command: parent.(*db.Command)}
38+
c.poolFlags.SetFlags(f)
39+
f.BoolVar(&c.dryrun, "dryrun", false, "view the number of commits to be deleted")
40+
f.BoolVar(&c.force, "f", false, "do not prompt for confirmation")
41+
return c, nil
3142
}
3243

3344
func (c *Command) Run(args []string) error {
34-
return errors.New("issue #2545")
45+
ctx, cleanup, err := c.Init()
46+
if err != nil {
47+
return err
48+
}
49+
defer cleanup()
50+
at, err := c.poolFlags.HEAD()
51+
if err != nil {
52+
return err
53+
}
54+
db, err := c.DBFlags.Open(ctx)
55+
if err != nil {
56+
return err
57+
}
58+
verb := "would vacate"
59+
if !c.dryrun {
60+
verb = "vacated"
61+
if err := c.confirm(at.String()); err != nil {
62+
return err
63+
}
64+
}
65+
cids, err := db.Vacate(ctx, at.Pool, at.Branch, c.dryrun)
66+
if err != nil {
67+
return err
68+
}
69+
if !c.DBFlags.Quiet {
70+
fmt.Printf("%s %d commit%s\n", verb, len(cids), plural.Slice(cids, "s"))
71+
}
72+
return nil
73+
}
74+
75+
func (c *Command) confirm(name string) error {
76+
if c.force {
77+
return nil
78+
}
79+
fmt.Printf("Are you sure you want to vacate previous commits from %q? There is no going back... [y|n]\n", name)
80+
var input string
81+
if _, err := fmt.Scanln(&input); err != nil {
82+
return err
83+
}
84+
input = strings.ToLower(input)
85+
if input == "y" || input == "yes" {
86+
return nil
87+
}
88+
return errors.New("operation canceled")
3589
}

db/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Interface interface {
3838
Revert(ctx context.Context, poolID ksuid.KSUID, branch string, commitID ksuid.KSUID, commit api.CommitMessage) (ksuid.KSUID, error)
3939
AddVectors(ctx context.Context, pool, revision string, objects []ksuid.KSUID, message api.CommitMessage) (ksuid.KSUID, error)
4040
DeleteVectors(ctx context.Context, pool, revision string, objects []ksuid.KSUID, message api.CommitMessage) (ksuid.KSUID, error)
41+
Vacate(ctx context.Context, pool, revision string, dryrun bool) ([]ksuid.KSUID, error)
4142
Vacuum(ctx context.Context, pool, revision string, dryrun bool) ([]ksuid.KSUID, error)
4243
}
4344

db/api/local.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,22 @@ func (l *local) DeleteVectors(ctx context.Context, pool, revision string, ids []
206206
return branch.DeleteVectors(ctx, ids, message.Author, message.Body)
207207
}
208208

209+
func (l *local) Vacate(ctx context.Context, pool, revision string, dryrun bool) ([]ksuid.KSUID, error) {
210+
poolID, err := l.PoolID(ctx, pool)
211+
if err != nil {
212+
return nil, err
213+
}
214+
p, err := l.db.OpenPool(ctx, poolID)
215+
if err != nil {
216+
return nil, err
217+
}
218+
commit, err := p.ResolveRevision(ctx, revision)
219+
if err != nil {
220+
return nil, err
221+
}
222+
return p.Vacate(ctx, commit, dryrun)
223+
}
224+
209225
func (l *local) Vacuum(ctx context.Context, pool, revision string, dryrun bool) ([]ksuid.KSUID, error) {
210226
poolID, err := l.PoolID(ctx, pool)
211227
if err != nil {

db/api/remote.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ func (r *remote) DeleteVectors(ctx context.Context, pool, revision string, ids [
148148
return res.Commit, err
149149
}
150150

151+
func (r *remote) Vacate(ctx context.Context, pool, revision string, dryrun bool) ([]ksuid.KSUID, error) {
152+
res, err := r.conn.Vacate(ctx, pool, revision, dryrun)
153+
return res.CommitIDs, err
154+
}
155+
151156
func (r *remote) Vacuum(ctx context.Context, pool, revision string, dryrun bool) ([]ksuid.KSUID, error) {
152157
res, err := r.conn.Vacuum(ctx, pool, revision, dryrun)
153158
return res.ObjectIDs, err

db/commits/reader.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package commits
22

33
import (
44
"context"
5+
"errors"
6+
"io/fs"
57

68
"github.com/brimdata/super"
79
"github.com/brimdata/super/sio"
@@ -37,6 +39,10 @@ func (r *LogReader) Read() (*super.Value, error) {
3739
}
3840
_, commitObject, err := r.store.GetBytes(r.ctx, r.cursor)
3941
if err != nil {
42+
if errors.Is(err, fs.ErrNotExist) {
43+
r.cursor = ksuid.Nil
44+
err = nil
45+
}
4046
return nil, err
4147
}
4248
next := commitObject.Parent

db/commits/store.go

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"io/fs"
10+
"runtime"
1011
"sync"
1112

1213
"github.com/brimdata/super"
@@ -18,6 +19,7 @@ import (
1819
arc "github.com/hashicorp/golang-lru/arc/v2"
1920
"github.com/segmentio/ksuid"
2021
"go.uber.org/zap"
22+
"golang.org/x/sync/errgroup"
2123
)
2224

2325
var (
@@ -98,6 +100,35 @@ func (s *Store) Remove(ctx context.Context, o *Object) error {
98100
return s.engine.Delete(ctx, s.pathOf(o.Commit))
99101
}
100102

103+
// DANGER ZONE - commits should only be removed once a new base has been
104+
// established.
105+
func (s *Store) DeleteCommits(ctx context.Context, commits []ksuid.KSUID) error {
106+
deleteIfExists := func(path *storage.URI) error {
107+
err := s.engine.Delete(ctx, path)
108+
if errors.Is(err, fs.ErrNotExist) {
109+
err = nil
110+
}
111+
return err
112+
}
113+
group, ctx := errgroup.WithContext(ctx)
114+
group.SetLimit(runtime.GOMAXPROCS(0))
115+
for i, c := range commits {
116+
group.Go(func() error {
117+
return deleteIfExists(s.pathOf(c))
118+
})
119+
group.Go(func() error {
120+
return deleteIfExists(s.snapshotPathOf(c))
121+
})
122+
if i > 0 {
123+
// Do not delete base if on first commit.
124+
group.Go(func() error {
125+
return deleteIfExists(s.basePathOf(c))
126+
})
127+
}
128+
}
129+
return group.Wait()
130+
}
131+
101132
func (s *Store) Snapshot(ctx context.Context, leaf ksuid.KSUID) (*Snapshot, error) {
102133
if snap, ok := s.snapshots.Get(leaf); ok {
103134
return snap, nil
@@ -108,6 +139,18 @@ func (s *Store) Snapshot(ctx context.Context, leaf ksuid.KSUID) (*Snapshot, erro
108139
s.snapshots.Add(leaf, snap)
109140
return snap, nil
110141
}
142+
snap, err := s.buildSnapshot(ctx, leaf)
143+
if err != nil {
144+
return nil, err
145+
}
146+
if err := s.putSnapshot(ctx, leaf, snap); err != nil {
147+
s.logger.Error("Storing snapshot", zap.Error(err))
148+
}
149+
s.snapshots.Add(leaf, snap)
150+
return snap, nil
151+
}
152+
153+
func (s *Store) buildSnapshot(ctx context.Context, leaf ksuid.KSUID) (*Snapshot, error) {
111154
var objects []*Object
112155
var base *Snapshot
113156
for at := leaf; at != ksuid.Nil; {
@@ -135,6 +178,17 @@ func (s *Store) Snapshot(ctx context.Context, leaf ksuid.KSUID) (*Snapshot, erro
135178
// No snapshot found, so wait for data object.
136179
wg.Wait()
137180
if oErr != nil {
181+
if errors.Is(oErr, fs.ErrNotExist) {
182+
// If object get error is not exists then perhaps commits have
183+
// been vacated at this point, check if previous is a base
184+
// commit.
185+
snap, err := s.getBase(ctx, at)
186+
if err != nil {
187+
return nil, fmt.Errorf("system error: error fetching base: %w", err)
188+
}
189+
base = snap
190+
break
191+
}
138192
return nil, oErr
139193
}
140194
objects = append(objects, o)
@@ -153,10 +207,6 @@ func (s *Store) Snapshot(ctx context.Context, leaf ksuid.KSUID) (*Snapshot, erro
153207
}
154208
}
155209
}
156-
if err := s.putSnapshot(ctx, leaf, snap); err != nil {
157-
s.logger.Error("Storing snapshot", zap.Error(err))
158-
}
159-
s.snapshots.Add(leaf, snap)
160210
return snap, nil
161211
}
162212

@@ -181,6 +231,27 @@ func (s *Store) snapshotPathOf(commit ksuid.KSUID) *storage.URI {
181231
return s.path.JoinPath(commit.String() + ".snap.bsup")
182232
}
183233

234+
func (s *Store) getBase(ctx context.Context, commit ksuid.KSUID) (*Snapshot, error) {
235+
r, err := s.engine.Get(ctx, s.basePathOf(commit))
236+
if err != nil {
237+
return nil, err
238+
}
239+
defer r.Close()
240+
return decodeSnapshot(r)
241+
}
242+
243+
func (s *Store) putBase(ctx context.Context, snap *Snapshot, commit ksuid.KSUID) error {
244+
b, err := snap.serialize()
245+
if err != nil {
246+
return err
247+
}
248+
return storage.Put(ctx, s.engine, s.basePathOf(commit), bytes.NewReader(b))
249+
}
250+
251+
func (s *Store) basePathOf(commit ksuid.KSUID) *storage.URI {
252+
return s.path.JoinPath(commit.String() + ".base.bsup")
253+
}
254+
184255
// Path return the entire path from the commit object to the root
185256
// in leaf to root order.
186257
func (s *Store) Path(ctx context.Context, leaf ksuid.KSUID) ([]ksuid.KSUID, error) {
@@ -210,11 +281,16 @@ func (s *Store) PathRange(ctx context.Context, from, to ksuid.KSUID) ([]ksuid.KS
210281
}
211282
break
212283
}
213-
path = append(path, at)
214284
o, err := s.Get(ctx, at)
215285
if err != nil {
286+
// If we get fs.ErrNotExist it means we have vacated and so we can
287+
// just return the path at this point.
288+
if errors.Is(err, fs.ErrNotExist) && to.IsNil() {
289+
break
290+
}
216291
return nil, err
217292
}
293+
path = append(path, at)
218294
if at == to {
219295
break
220296
}
@@ -247,6 +323,9 @@ func (s *Store) ReadAll(ctx context.Context, commit, stop ksuid.KSUID) ([]byte,
247323
for commit != ksuid.Nil && commit != stop {
248324
b, commitObject, err := s.GetBytes(ctx, commit)
249325
if err != nil {
326+
if errors.Is(err, fs.ErrNotExist) {
327+
break
328+
}
250329
return nil, err
251330
}
252331
size += len(b)
@@ -341,6 +420,31 @@ func (s *Store) PatchOfPath(ctx context.Context, base *Snapshot, baseID, commit
341420
return patch, nil
342421
}
343422

423+
// SetBase establishes a new base (snapshot) at the provided commit and resets
424+
// the attached caches. The caller is responsible for deleting prior commits.
425+
func (s *Store) SetBase(ctx context.Context, commit ksuid.KSUID) (*Snapshot, error) {
426+
path, err := s.Path(ctx, commit)
427+
if err != nil {
428+
return nil, err
429+
}
430+
if len(path) <= 1 {
431+
return nil, errors.New("cannot set base on earliest commit")
432+
}
433+
// Create snapshot of previous commit.
434+
snap, err := s.buildSnapshot(ctx, path[1])
435+
if err != nil {
436+
return nil, err
437+
}
438+
if err := s.putBase(ctx, snap, path[1]); err != nil {
439+
return nil, err
440+
}
441+
s.cache.Purge()
442+
s.paths.Purge()
443+
s.snapshots.Purge()
444+
s.snapshots.Add(path[1], snap)
445+
return snap, nil
446+
}
447+
344448
// Vacuumable returns the set of data.Objects in the path of leaf that are not referenced
345449
// by the leaf's snapshot.
346450
func (s *Store) Vacuumable(ctx context.Context, leaf ksuid.KSUID, out chan<- *data.Object) error {

0 commit comments

Comments
 (0)