cmdgc

package
v0.13.27 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2025 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package cmdgc implements the "ffs gc" subcommand.

Index

Constants

This section is empty.

Variables

View Source
var Command = &command.C{
	Name: "gc",
	Help: `Garbage-collect objects not reachable from known roots.

If no roots are defined, an error is reported without making any changes
unless --force is set. This avoids accidentally deleting everything in a
store without roots.
`,

	SetFlags: command.Flags(flax.MustBind, &gcFlags),

	Run: command.Adapt(func(env *command.Env) error {
		cfg := env.Config.(*config.Settings)
		return cfg.WithStore(env.Context(), func(s filetree.Store) error {
			var keys []string
			for key, err := range s.Roots().List(env.Context(), "") {
				if err != nil {
					return fmt.Errorf("listing roots: %w", err)
				}
				keys = append(keys, key)
			}

			if len(keys) == 0 && !gcFlags.Force {
				return errors.New("there are no root keys defined")
			} else if len(keys) == 0 {
				fmt.Fprint(env, `>> WARNING <<
* No root keys found!
* Proceeding with collection anyway because --force is set

`)
			}

			n, err := s.Files().Len(env.Context())
			if err != nil {
				return err
			} else if n == 0 {
				return errors.New("the store is empty")
			}
			var idxs []*index.Index
			idx := index.New(int(n), &index.Options{FalsePositiveRate: 0.01})
			fmt.Fprintf(env, "Begin GC of %d objects from %d roots\n", n, len(keys))
			dprintf(env, "Roots: %s\n", wrap(keys, 90, "  ", ", "))

			for _, key := range keys {
				rp, err := root.Open(env.Context(), s.Roots(), key)
				if err != nil {
					return fmt.Errorf("opening %q: %w", key, err)
				}
				idx.Add(key)

				if rp.IndexKey != "" {
					rpi, err := config.LoadIndex(env.Context(), s.Files(), rp.IndexKey)
					if err != nil {
						return err
					}
					idxs = append(idxs, rpi)
					idx.Add(rp.IndexKey)
					dprintf(env, "Loaded cached index for %q (%d keys, %s)\n",
						key, rpi.Stats().NumKeys, config.FormatKey(rp.IndexKey),
					)
					continue
				}

				if gcFlags.RequireIndex {
					return fmt.Errorf("missing required index for %q", key)
				}

				rf, err := rp.File(env.Context(), s.Files())
				if err != nil {
					return fmt.Errorf("opening %q: %w", rp.FileKey, err)
				}
				idx.Add(rp.FileKey)

				dprintf(env, "Scanning data reachable from %q (%s)...\n",
					config.PrintableKey(key), config.FormatKey(rp.FileKey))
				scanned := mapset.New[string]()
				start := time.Now()
				if err := rf.Scan(env.Context(), func(si file.ScanItem) bool {
					key := si.Key()
					if scanned.Has(key) {
						return false
					}
					scanned.Add(key)
					idx.Add(key)
					for _, dkey := range si.Data().Keys() {
						idx.Add(dkey)
					}
					return true
				}); err != nil {
					return fmt.Errorf("scanning %q: %w", key, err)
				}
				dprintf(env, "Finished scanning %d objects [%v elapsed]\n",
					idx.Len(), time.Since(start).Truncate(10*time.Millisecond))
			}
			idxs = append(idxs, idx)

			hasKey := func(key string) bool {
				for _, idx := range idxs {
					if idx.Has(key) {
						return true
					}
				}
				return false
			}

			ctx, cancel := context.WithCancelCause(env.Context())
			defer cancel(nil)
			if gcFlags.Limit > 0 {
				t := time.AfterFunc(gcFlags.Limit, func() { cancel(errSweepLimit) })
				defer t.Stop()
				fmt.Fprintf(env, "Begin sweep over %d objects (limit %v)\n", n, gcFlags.Limit)
			} else {
				fmt.Fprintf(env, "Begin sweep over %d objects\n", n)
			}

			g, run := taskgroup.New(cancel).Limit(gcFlags.Tasks)

			start := time.Now()
			var numKeep, numDrop atomic.Int64

			// Sweep phase 1: Collect all the keys eligible for deletion.
			var toDrop mapset.Set[string]
			for key, err := range s.Files().List(ctx, "") {
				if err != nil {
					return err
				}

				if hasKey(key) {
					numKeep.Add(1)
					continue
				}
				toDrop.Add(key)
			}

			if !toDrop.IsEmpty() {
				fmt.Fprintf(env, "Found %d objects to delete\n", toDrop.Len())

				pb := pbar.New(env, int64(toDrop.Len())).Start()
				for key := range toDrop {
					if ctx.Err() != nil {
						break
					}
					run.Go(func() error {
						pb.Add(1)
						err := s.Files().Delete(ctx, key)
						if err == nil || blob.IsKeyNotFound(err) {
							pb.SetMeta(numDrop.Add(1))
							return nil
						} else if !errors.Is(err, context.Canceled) {
							log.Printf("WARNING: delete key %s: %v", config.FormatKey(key), err)
						}
						return err
					})
				}

				serr := g.Wait()
				pb.Stop()
				fmt.Fprintln(env, " *")
				if serr != nil {
					if errors.Is(context.Cause(ctx), errSweepLimit) {
						fmt.Fprintln(env, "(sweep limit reached)")
					} else {
						return fmt.Errorf("sweeping failed: %w", serr)
					}
				}
			}
			fmt.Fprintf(env, "GC complete: keep %d, drop %d [%v elapsed]\n",
				numKeep.Load(), numDrop.Load(), time.Since(start).Truncate(10*time.Millisecond))
			return nil
		})
	}),
}

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL