containerd/metadata/gc_test.go
Derek McGowan 17471d5592
Metadata garbage collection
Marks and sweeps unreferenced objects.
Add snapshot cleanup to metadata.
Add content garbage collection

Add dirty flags for snapshotters and content store which
are set on deletion and used during the next garbage collection.
Cleanup content store backend when content metadata is removed.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2017-10-11 10:42:47 -07:00

407 lines
11 KiB
Go

package metadata
import (
"context"
"io"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"sort"
"testing"
"github.com/boltdb/bolt"
"github.com/containerd/containerd/gc"
"github.com/containerd/containerd/metadata/boltutil"
digest "github.com/opencontainers/go-digest"
)
func TestGCRoots(t *testing.T) {
db, cleanup, err := newDatabase()
if err != nil {
t.Fatal(err)
}
defer cleanup()
alters := []alterFunc{
addImage("ns1", "image1", dgst(1), nil),
addImage("ns1", "image2", dgst(2), labelmap(string(labelGCSnapRef)+"overlay", "sn2")),
addContent("ns1", dgst(1), nil),
addContent("ns1", dgst(2), nil),
addContent("ns1", dgst(3), nil),
addContent("ns2", dgst(1), nil),
addContent("ns2", dgst(2), labelmap(string(labelGCRoot), "always")),
addSnapshot("ns1", "overlay", "sn1", "", nil),
addSnapshot("ns1", "overlay", "sn2", "", nil),
addSnapshot("ns1", "overlay", "sn3", "", labelmap(string(labelGCRoot), "always")),
}
expected := []gc.Node{
gcnode(ResourceContent, "ns1", dgst(1).String()),
gcnode(ResourceContent, "ns1", dgst(2).String()),
gcnode(ResourceContent, "ns2", dgst(2).String()),
gcnode(ResourceSnapshot, "ns1", "overlay/sn2"),
gcnode(ResourceSnapshot, "ns1", "overlay/sn3"),
}
if err := db.Update(func(tx *bolt.Tx) error {
v1bkt, err := tx.CreateBucketIfNotExists(bucketKeyVersion)
if err != nil {
return err
}
for _, alter := range alters {
if err := alter(v1bkt); err != nil {
return err
}
}
return nil
}); err != nil {
t.Fatalf("Update failed: %+v", err)
}
ctx := context.Background()
checkNodes(ctx, t, db, expected, func(ctx context.Context, tx *bolt.Tx, nc chan<- gc.Node) error {
return scanRoots(ctx, tx, nc)
})
}
func TestGCRemove(t *testing.T) {
db, cleanup, err := newDatabase()
if err != nil {
t.Fatal(err)
}
defer cleanup()
alters := []alterFunc{
addImage("ns1", "image1", dgst(1), nil),
addImage("ns1", "image2", dgst(2), labelmap(string(labelGCSnapRef)+"overlay", "sn2")),
addContent("ns1", dgst(1), nil),
addContent("ns1", dgst(2), nil),
addContent("ns1", dgst(3), nil),
addContent("ns2", dgst(1), nil),
addContent("ns2", dgst(2), labelmap(string(labelGCRoot), "always")),
addSnapshot("ns1", "overlay", "sn1", "", nil),
addSnapshot("ns1", "overlay", "sn2", "", nil),
addSnapshot("ns1", "overlay", "sn3", "", labelmap(string(labelGCRoot), "always")),
addSnapshot("ns2", "overlay", "sn1", "", nil),
}
all := []gc.Node{
gcnode(ResourceContent, "ns1", dgst(1).String()),
gcnode(ResourceContent, "ns1", dgst(2).String()),
gcnode(ResourceContent, "ns1", dgst(3).String()),
gcnode(ResourceContent, "ns2", dgst(1).String()),
gcnode(ResourceContent, "ns2", dgst(2).String()),
gcnode(ResourceSnapshot, "ns1", "overlay/sn1"),
gcnode(ResourceSnapshot, "ns1", "overlay/sn2"),
gcnode(ResourceSnapshot, "ns1", "overlay/sn3"),
gcnode(ResourceSnapshot, "ns2", "overlay/sn1"),
}
var deleted, remaining []gc.Node
for i, n := range all {
if i%2 == 0 {
deleted = append(deleted, n)
} else {
remaining = append(remaining, n)
}
}
if err := db.Update(func(tx *bolt.Tx) error {
v1bkt, err := tx.CreateBucketIfNotExists(bucketKeyVersion)
if err != nil {
return err
}
for _, alter := range alters {
if err := alter(v1bkt); err != nil {
return err
}
}
return nil
}); err != nil {
t.Fatalf("Update failed: %+v", err)
}
ctx := context.Background()
checkNodes(ctx, t, db, all, func(ctx context.Context, tx *bolt.Tx, nc chan<- gc.Node) error {
return scanAll(ctx, tx, nc)
})
if t.Failed() {
t.Fatal("Scan all failed")
}
if err := db.Update(func(tx *bolt.Tx) error {
for _, n := range deleted {
if err := remove(ctx, tx, n); err != nil {
return err
}
}
return nil
}); err != nil {
t.Fatalf("Update failed: %+v", err)
}
checkNodes(ctx, t, db, remaining, func(ctx context.Context, tx *bolt.Tx, nc chan<- gc.Node) error {
return scanAll(ctx, tx, nc)
})
}
func TestGCRefs(t *testing.T) {
db, cleanup, err := newDatabase()
if err != nil {
t.Fatal(err)
}
defer cleanup()
alters := []alterFunc{
addContent("ns1", dgst(1), nil),
addContent("ns1", dgst(2), nil),
addContent("ns1", dgst(3), nil),
addContent("ns1", dgst(4), labelmap(string(labelGCContentRef), dgst(1).String())),
addContent("ns1", dgst(5), labelmap(string(labelGCContentRef)+".anything-1", dgst(2).String(), string(labelGCContentRef)+".anything-2", dgst(3).String())),
addContent("ns1", dgst(6), labelmap(string(labelGCContentRef)+"bad", dgst(1).String())),
addContent("ns2", dgst(1), nil),
addContent("ns2", dgst(2), nil),
addSnapshot("ns1", "overlay", "sn1", "", nil),
addSnapshot("ns1", "overlay", "sn2", "sn1", nil),
addSnapshot("ns1", "overlay", "sn3", "sn2", nil),
addSnapshot("ns1", "overlay", "sn4", "", labelmap(string(labelGCSnapRef)+"btrfs", "sn1", string(labelGCSnapRef)+"overlay", "sn1")),
addSnapshot("ns1", "btrfs", "sn1", "", nil),
addSnapshot("ns2", "overlay", "sn1", "", nil),
addSnapshot("ns2", "overlay", "sn2", "sn1", nil),
}
refs := map[gc.Node][]gc.Node{
gcnode(ResourceContent, "ns1", dgst(1).String()): nil,
gcnode(ResourceContent, "ns1", dgst(2).String()): nil,
gcnode(ResourceContent, "ns1", dgst(3).String()): nil,
gcnode(ResourceContent, "ns1", dgst(4).String()): {
gcnode(ResourceContent, "ns1", dgst(1).String()),
},
gcnode(ResourceContent, "ns1", dgst(5).String()): {
gcnode(ResourceContent, "ns1", dgst(2).String()),
gcnode(ResourceContent, "ns1", dgst(3).String()),
},
gcnode(ResourceContent, "ns1", dgst(6).String()): nil,
gcnode(ResourceContent, "ns2", dgst(1).String()): nil,
gcnode(ResourceContent, "ns2", dgst(2).String()): nil,
gcnode(ResourceSnapshot, "ns1", "overlay/sn1"): nil,
gcnode(ResourceSnapshot, "ns1", "overlay/sn2"): {
gcnode(ResourceSnapshot, "ns1", "overlay/sn1"),
},
gcnode(ResourceSnapshot, "ns1", "overlay/sn3"): {
gcnode(ResourceSnapshot, "ns1", "overlay/sn2"),
},
gcnode(ResourceSnapshot, "ns1", "overlay/sn4"): {
gcnode(ResourceSnapshot, "ns1", "btrfs/sn1"),
gcnode(ResourceSnapshot, "ns1", "overlay/sn1"),
},
gcnode(ResourceSnapshot, "ns1", "btrfs/sn1"): nil,
gcnode(ResourceSnapshot, "ns2", "overlay/sn1"): nil,
gcnode(ResourceSnapshot, "ns2", "overlay/sn2"): {
gcnode(ResourceSnapshot, "ns2", "overlay/sn1"),
},
}
if err := db.Update(func(tx *bolt.Tx) error {
v1bkt, err := tx.CreateBucketIfNotExists(bucketKeyVersion)
if err != nil {
return err
}
for _, alter := range alters {
if err := alter(v1bkt); err != nil {
return err
}
}
return nil
}); err != nil {
t.Fatalf("Update failed: %+v", err)
}
ctx := context.Background()
for n, nodes := range refs {
checkNodes(ctx, t, db, nodes, func(ctx context.Context, tx *bolt.Tx, nc chan<- gc.Node) error {
return references(ctx, tx, n, func(n gc.Node) {
select {
case nc <- n:
case <-ctx.Done():
}
})
})
if t.Failed() {
t.Fatalf("Failure scanning %v", n)
}
}
}
func newDatabase() (*bolt.DB, func(), error) {
td, err := ioutil.TempDir("", "gc-roots-")
if err != nil {
return nil, nil, err
}
db, err := bolt.Open(filepath.Join(td, "test.db"), 0777, nil)
if err != nil {
os.RemoveAll(td)
return nil, nil, err
}
return db, func() {
db.Close()
os.RemoveAll(td)
}, nil
}
func checkNodes(ctx context.Context, t *testing.T, db *bolt.DB, expected []gc.Node, fn func(context.Context, *bolt.Tx, chan<- gc.Node) error) {
var actual []gc.Node
nc := make(chan gc.Node)
done := make(chan struct{})
go func() {
defer close(done)
for n := range nc {
actual = append(actual, n)
}
}()
if err := db.View(func(tx *bolt.Tx) error {
defer close(nc)
return fn(ctx, tx, nc)
}); err != nil {
t.Fatal(err)
}
<-done
checkNodesEqual(t, actual, expected)
}
func checkNodesEqual(t *testing.T, n1, n2 []gc.Node) {
sort.Sort(nodeList(n1))
sort.Sort(nodeList(n2))
if len(n1) != len(n2) {
t.Errorf("Nodes do not match\n\tExpected:\n\t%v\n\tActual:\n\t%v", n2, n1)
return
}
for i := range n1 {
if n1[i] != n2[i] {
t.Errorf("[%d] root does not match expected: expected %v, got %v", i, n2[i], n1[i])
}
}
}
type nodeList []gc.Node
func (nodes nodeList) Len() int {
return len(nodes)
}
func (nodes nodeList) Less(i, j int) bool {
if nodes[i].Type != nodes[j].Type {
return nodes[i].Type < nodes[j].Type
}
if nodes[i].Namespace != nodes[j].Namespace {
return nodes[i].Namespace < nodes[j].Namespace
}
return nodes[i].Key < nodes[j].Key
}
func (nodes nodeList) Swap(i, j int) {
nodes[i], nodes[j] = nodes[j], nodes[i]
}
type alterFunc func(bkt *bolt.Bucket) error
func addImage(ns, name string, dgst digest.Digest, labels map[string]string) alterFunc {
return func(bkt *bolt.Bucket) error {
ibkt, err := createBuckets(bkt, ns, string(bucketKeyObjectImages), name)
if err != nil {
return err
}
tbkt, err := ibkt.CreateBucket(bucketKeyTarget)
if err != nil {
return err
}
if err := tbkt.Put(bucketKeyDigest, []byte(dgst.String())); err != nil {
return err
}
return boltutil.WriteLabels(ibkt, labels)
}
}
func addSnapshot(ns, snapshotter, name, parent string, labels map[string]string) alterFunc {
return func(bkt *bolt.Bucket) error {
sbkt, err := createBuckets(bkt, ns, string(bucketKeyObjectSnapshots), snapshotter, name)
if err != nil {
return err
}
if parent != "" {
if err := sbkt.Put(bucketKeyParent, []byte(parent)); err != nil {
return err
}
}
return boltutil.WriteLabels(sbkt, labels)
}
}
func addContent(ns string, dgst digest.Digest, labels map[string]string) alterFunc {
return func(bkt *bolt.Bucket) error {
cbkt, err := createBuckets(bkt, ns, string(bucketKeyObjectContent), string(bucketKeyObjectBlob), dgst.String())
if err != nil {
return err
}
return boltutil.WriteLabels(cbkt, labels)
}
}
func addContainer(ns, name, snapshotter, snapshot string, labels map[string]string) alterFunc {
return func(bkt *bolt.Bucket) error {
cbkt, err := createBuckets(bkt, ns, string(bucketKeyObjectContainers), name)
if err != nil {
return err
}
if err := cbkt.Put(bucketKeySnapshotter, []byte(snapshotter)); err != nil {
return err
}
if err := cbkt.Put(bucketKeySnapshotKey, []byte(snapshot)); err != nil {
return err
}
return boltutil.WriteLabels(cbkt, labels)
}
}
func createBuckets(bkt *bolt.Bucket, names ...string) (*bolt.Bucket, error) {
for _, name := range names {
nbkt, err := bkt.CreateBucketIfNotExists([]byte(name))
if err != nil {
return nil, err
}
bkt = nbkt
}
return bkt, nil
}
func labelmap(kv ...string) map[string]string {
if len(kv)%2 != 0 {
panic("bad labels argument")
}
l := map[string]string{}
for i := 0; i < len(kv); i = i + 2 {
l[kv[i]] = kv[i+1]
}
return l
}
func dgst(i int64) digest.Digest {
r := rand.New(rand.NewSource(i))
dgstr := digest.SHA256.Digester()
if _, err := io.CopyN(dgstr.Hash(), r, 256); err != nil {
panic(err)
}
return dgstr.Digest()
}