github.com/letsencrypt/trillian@v1.1.2-0.20180615153820-ae375a99d36a/server/admin/tree_gc_test.go (about)

     1  // Copyright 2017 Google Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package admin
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/golang/mock/gomock"
    25  	"github.com/golang/protobuf/proto"
    26  	"github.com/golang/protobuf/ptypes"
    27  	"github.com/google/trillian"
    28  	"github.com/google/trillian/storage"
    29  	"github.com/google/trillian/storage/testonly"
    30  )
    31  
    32  func TestDeletedTreeGC_Run(t *testing.T) {
    33  	ctrl := gomock.NewController(t)
    34  	defer ctrl.Finish()
    35  
    36  	// Test the following scenario:
    37  	// * 1st iteration: tree1 is listed and hard-deleted
    38  	// * Sleep
    39  	// * 2nd iteration: ListTrees returns an empty slice, nothing gets deleted
    40  	// * Sleep (ctx cancelled)
    41  	//
    42  	// DeletedTreeGC.Run() until ctx in cancelled. Since it always sleeps between iterations, we
    43  	// make "timeSleep" cancel ctx the second time around to break the loop.
    44  
    45  	tree1 := proto.Clone(testonly.LogTree).(*trillian.Tree)
    46  	tree1.TreeId = 1
    47  	tree1.Deleted = true
    48  	tree1.DeleteTime, _ = ptypes.TimestampProto(time.Date(2017, 9, 21, 10, 0, 0, 0, time.UTC))
    49  
    50  	listTX1 := storage.NewMockReadOnlyAdminTX(ctrl)
    51  	listTX2 := storage.NewMockReadOnlyAdminTX(ctrl)
    52  	deleteTX1 := storage.NewMockAdminTX(ctrl)
    53  	as := &testonly.FakeAdminStorage{
    54  		TX:         []storage.AdminTX{deleteTX1},
    55  		ReadOnlyTX: []storage.ReadOnlyAdminTX{listTX1, listTX2},
    56  	}
    57  
    58  	ctx, cancel := context.WithCancel(context.Background())
    59  
    60  	// Sequence Snapshot()/ReadWriteTransaction() calls.
    61  	// * 1st loop: Snapshot()/ListTrees() followed by ReadWriteTransaction()/HardDeleteTree()
    62  	// * 2nd loop: Snapshot()/ListTrees() only.
    63  
    64  	// 1st loop
    65  	listTX1.EXPECT().ListTrees(gomock.Any(), true /* includeDeleted */).Return([]*trillian.Tree{tree1}, nil)
    66  	listTX1.EXPECT().Close().Return(nil)
    67  	listTX1.EXPECT().Commit().Return(nil)
    68  	deleteTX1.EXPECT().HardDeleteTree(gomock.Any(), tree1.TreeId).Return(nil)
    69  	deleteTX1.EXPECT().Close().Return(nil)
    70  	deleteTX1.EXPECT().Commit().Return(nil)
    71  
    72  	// 2nd loop
    73  	listTX2.EXPECT().ListTrees(gomock.Any(), true /* includeDeleted */).Return(nil, nil)
    74  	listTX2.EXPECT().Close().Return(nil)
    75  	listTX2.EXPECT().Commit().Return(nil)
    76  
    77  	defer func(now func() time.Time, sleep func(time.Duration)) {
    78  		timeNow = now
    79  		timeSleep = sleep
    80  	}(timeNow, timeSleep)
    81  
    82  	const deleteThreshold = 1 * time.Hour
    83  	const runInterval = 3 * time.Second
    84  
    85  	// now > tree1.DeleteTime + deleteThreshold, so tree1 gets deleted on first round
    86  	now, _ := ptypes.Timestamp(tree1.DeleteTime)
    87  	now = now.Add(deleteThreshold).Add(1 * time.Second)
    88  	timeNow = func() time.Time { return now }
    89  
    90  	calls := 0
    91  	timeSleep = func(d time.Duration) {
    92  		calls++
    93  		if d < runInterval || d >= 2*runInterval {
    94  			t.Errorf("Called time.Sleep(%v), want %v", d, runInterval)
    95  		}
    96  		if calls >= 2 {
    97  			cancel()
    98  		}
    99  	}
   100  
   101  	NewDeletedTreeGC(as, deleteThreshold, runInterval, nil /* mf */).Run(ctx)
   102  }
   103  
   104  func TestDeletedTreeGC_RunOnce(t *testing.T) {
   105  	ctrl := gomock.NewController(t)
   106  	defer ctrl.Finish()
   107  
   108  	tree1 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   109  	tree1.TreeId = 1
   110  	tree2 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   111  	tree2.TreeId = 2
   112  	tree2.Deleted = true
   113  	tree2.DeleteTime, _ = ptypes.TimestampProto(time.Date(2017, 9, 21, 10, 0, 0, 0, time.UTC))
   114  	tree3 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   115  	tree3.TreeId = 3
   116  	tree3.Deleted = true
   117  	tree3.DeleteTime, _ = ptypes.TimestampProto(time.Date(2017, 9, 22, 11, 0, 0, 0, time.UTC))
   118  	tree4 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   119  	tree4.TreeId = 4
   120  	tree4.Deleted = true
   121  	tree4.DeleteTime, _ = ptypes.TimestampProto(time.Date(2017, 9, 23, 12, 0, 0, 0, time.UTC))
   122  	tree5 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   123  	tree5.TreeId = 5
   124  	allTrees := []*trillian.Tree{tree1, tree2, tree3, tree4, tree5}
   125  
   126  	tests := []struct {
   127  		desc            string
   128  		now             time.Time
   129  		deleteThreshold time.Duration
   130  		wantDeleted     []int64
   131  	}{
   132  		{
   133  			desc:            "noDeletions",
   134  			now:             time.Date(2017, 9, 28, 10, 0, 0, 0, time.UTC),
   135  			deleteThreshold: 7 * 24 * time.Hour,
   136  		},
   137  		{
   138  			desc:            "oneDeletion",
   139  			now:             time.Date(2017, 9, 28, 11, 0, 0, 0, time.UTC),
   140  			deleteThreshold: 7 * 24 * time.Hour,
   141  			wantDeleted:     []int64{tree2.TreeId},
   142  		},
   143  		{
   144  			desc:            "twoDeletions",
   145  			now:             time.Date(2017, 9, 22, 12, 1, 0, 0, time.UTC),
   146  			deleteThreshold: 1 * time.Hour,
   147  			wantDeleted:     []int64{tree2.TreeId, tree3.TreeId},
   148  		},
   149  		{
   150  			desc:            "threeDeletions",
   151  			now:             time.Date(2017, 9, 23, 13, 30, 0, 0, time.UTC),
   152  			deleteThreshold: 1 * time.Hour,
   153  			wantDeleted:     []int64{tree2.TreeId, tree3.TreeId, tree4.TreeId},
   154  		},
   155  	}
   156  
   157  	defer func(f func() time.Time) { timeNow = f }(timeNow)
   158  	ctx := context.Background()
   159  	for _, test := range tests {
   160  		timeNow = func() time.Time { return test.now }
   161  
   162  		listTX := storage.NewMockReadOnlyAdminTX(ctrl)
   163  		as := &testonly.FakeAdminStorage{ReadOnlyTX: []storage.ReadOnlyAdminTX{listTX}}
   164  
   165  		listTX.EXPECT().ListTrees(gomock.Any(), true /* includeDeleted */).Return(allTrees, nil)
   166  		listTX.EXPECT().Close().Return(nil)
   167  		listTX.EXPECT().Commit().Return(nil)
   168  
   169  		for _, id := range test.wantDeleted {
   170  			deleteTX := storage.NewMockAdminTX(ctrl)
   171  			deleteTX.EXPECT().HardDeleteTree(gomock.Any(), id).Return(nil)
   172  			deleteTX.EXPECT().Close().Return(nil)
   173  			deleteTX.EXPECT().Commit().Return(nil)
   174  			as.TX = append(as.TX, deleteTX)
   175  		}
   176  
   177  		gc := NewDeletedTreeGC(as, test.deleteThreshold, 1*time.Second /* minRunInterval */, nil /* mf */)
   178  		switch count, err := gc.RunOnce(ctx); {
   179  		case err != nil:
   180  			t.Errorf("%v: RunOnce() returned err = %v", test.desc, err)
   181  		case count != len(test.wantDeleted):
   182  			t.Errorf("%v: RunOnce() = %v, want = %v", test.desc, count, len(test.wantDeleted))
   183  		}
   184  	}
   185  }
   186  
   187  // listTreesSpec specifies all parameters required to mock a ListTrees TX call.
   188  type listTreesSpec struct {
   189  	snapshotErr, listErr, commitErr error
   190  	trees                           []*trillian.Tree
   191  }
   192  
   193  // hardDeleteTreeSpec specifies all parameters required to mock a HardDeleteTree TX call.
   194  type hardDeleteTreeSpec struct {
   195  	beginErr, deleteErr, commitErr error
   196  	treeID                         int64
   197  }
   198  
   199  func TestDeletedTreeGC_RunOnceErrors(t *testing.T) {
   200  	ctrl := gomock.NewController(t)
   201  	defer ctrl.Finish()
   202  
   203  	deleteTime := time.Date(2017, 10, 25, 16, 0, 0, 0, time.UTC)
   204  	deleteTimePB, err := ptypes.TimestampProto(deleteTime)
   205  	if err != nil {
   206  		t.Fatalf("TimestampProto(%v) returned err = %v", deleteTime, err)
   207  	}
   208  	logTree1 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   209  	logTree1.TreeId = 10
   210  	logTree1.Deleted = true
   211  	logTree1.DeleteTime = deleteTimePB
   212  	logTree2 := proto.Clone(testonly.LogTree).(*trillian.Tree)
   213  	logTree2.TreeId = 20
   214  	logTree2.Deleted = true
   215  	logTree2.DeleteTime = deleteTimePB
   216  	mapTree := proto.Clone(testonly.MapTree).(*trillian.Tree)
   217  	mapTree.TreeId = 30
   218  	mapTree.Deleted = true
   219  	mapTree.DeleteTime = deleteTimePB
   220  	badTS := proto.Clone(testonly.LogTree).(*trillian.Tree)
   221  	badTS.TreeId = 40
   222  	badTS.Deleted = true
   223  	// badTS.DeleteTime is nil
   224  
   225  	// To simplify the test all trees are deleted and passed the deletion threshold.
   226  	// Other aspects of RunOnce() are covered by TestDeletedTreeGC_RunOnce.
   227  	deleteThreshold := 1 * time.Hour
   228  	now := deleteTime.Add(2 * time.Hour)
   229  	defer func(f func() time.Time) { timeNow = f }(timeNow)
   230  	timeNow = func() time.Time { return now }
   231  
   232  	tests := []struct {
   233  		desc string
   234  
   235  		listTrees      listTreesSpec
   236  		hardDeleteTree []hardDeleteTreeSpec
   237  
   238  		// wantCount is the count of successfully deleted trees.
   239  		wantCount int
   240  		// wantErrs defines which strings must be present in the resulting error.
   241  		wantErrs []string
   242  	}{
   243  		{
   244  			desc: "snapshotErr",
   245  			listTrees: listTreesSpec{
   246  				snapshotErr: errors.New("snapshot err"),
   247  			},
   248  			wantErrs: []string{"snapshot err"},
   249  		},
   250  		{
   251  			desc: "listErr",
   252  			listTrees: listTreesSpec{
   253  				listErr: errors.New("list err"),
   254  			},
   255  			wantErrs: []string{"list err"},
   256  		},
   257  		{
   258  			desc: "snapshotCommitErr",
   259  			listTrees: listTreesSpec{
   260  				commitErr: errors.New("commit err"),
   261  				trees:     []*trillian.Tree{logTree1, logTree2, mapTree},
   262  			},
   263  			wantErrs: []string{"commit err"},
   264  		},
   265  		{
   266  			desc: "beginErr",
   267  			listTrees: listTreesSpec{
   268  				trees: []*trillian.Tree{logTree1, logTree2},
   269  			},
   270  			hardDeleteTree: []hardDeleteTreeSpec{
   271  				{beginErr: errors.New("begin err")},
   272  				{treeID: logTree2.TreeId},
   273  			},
   274  			wantCount: 1,
   275  			wantErrs:  []string{"begin err"},
   276  		},
   277  		{
   278  			desc: "deleteErr",
   279  			listTrees: listTreesSpec{
   280  				trees: []*trillian.Tree{logTree1, logTree2},
   281  			},
   282  			hardDeleteTree: []hardDeleteTreeSpec{
   283  				{deleteErr: errors.New("cannot delete logTree1"), treeID: logTree1.TreeId},
   284  				{treeID: logTree2.TreeId},
   285  			},
   286  			wantCount: 1,
   287  			wantErrs:  []string{"cannot delete logTree1"},
   288  		},
   289  		{
   290  			desc: "commitErr",
   291  			listTrees: listTreesSpec{
   292  				trees: []*trillian.Tree{logTree1, logTree2},
   293  			},
   294  			hardDeleteTree: []hardDeleteTreeSpec{
   295  				{commitErr: errors.New("commit err"), treeID: logTree1.TreeId},
   296  				{treeID: logTree2.TreeId},
   297  			},
   298  			wantCount: 1,
   299  			wantErrs:  []string{"commit err"},
   300  		},
   301  		{
   302  			// logTree1 = delete successful
   303  			// logTree2 = delete error
   304  			// mapTree  = commit error
   305  			// badTS    = timestamp parse error (no HardDeleteTree() call)
   306  			desc: "multipleErrors",
   307  			listTrees: listTreesSpec{
   308  				trees: []*trillian.Tree{logTree1, logTree2, mapTree, badTS},
   309  			},
   310  			hardDeleteTree: []hardDeleteTreeSpec{
   311  				{treeID: logTree1.TreeId},
   312  				{deleteErr: errors.New("delete err"), treeID: logTree2.TreeId},
   313  				{commitErr: errors.New("commit err"), treeID: mapTree.TreeId},
   314  			},
   315  			wantCount: 1,
   316  			wantErrs:  []string{"delete err", "commit err", "error parsing delete_time"},
   317  		},
   318  	}
   319  
   320  	ctx := context.Background()
   321  	for _, test := range tests {
   322  		t.Run(test.desc, func(t *testing.T) {
   323  			listTX := storage.NewMockReadOnlyAdminTX(ctrl)
   324  			listTX.EXPECT().ListTrees(gomock.Any(), true /* includeDeleted */).AnyTimes().Return(test.listTrees.trees, test.listTrees.listErr)
   325  			listTX.EXPECT().Close().AnyTimes().Return(nil)
   326  			listTX.EXPECT().Commit().AnyTimes().Return(test.listTrees.commitErr)
   327  
   328  			as := &testonly.FakeAdminStorage{
   329  				ReadOnlyTX: []storage.ReadOnlyAdminTX{listTX},
   330  			}
   331  			if test.listTrees.snapshotErr != nil {
   332  				as.SnapshotErr = append(as.SnapshotErr, test.listTrees.snapshotErr)
   333  			}
   334  
   335  			for _, hardDeleteTree := range test.hardDeleteTree {
   336  				deleteTX := storage.NewMockAdminTX(ctrl)
   337  
   338  				if hardDeleteTree.beginErr != nil {
   339  					as.TXErr = append(as.TXErr, hardDeleteTree.beginErr)
   340  				} else {
   341  					as.TX = append(as.TX, deleteTX)
   342  				}
   343  
   344  				if hardDeleteTree.treeID != 0 {
   345  					deleteTX.EXPECT().HardDeleteTree(gomock.Any(), hardDeleteTree.treeID).AnyTimes().Return(hardDeleteTree.deleteErr)
   346  				}
   347  				deleteTX.EXPECT().Close().AnyTimes().Return(nil)
   348  				deleteTX.EXPECT().Commit().AnyTimes().Return(hardDeleteTree.commitErr)
   349  			}
   350  
   351  			gc := NewDeletedTreeGC(as, deleteThreshold, 1*time.Second /* minRunInterval */, nil /* mf */)
   352  			count, err := gc.RunOnce(ctx)
   353  			if err == nil {
   354  				t.Fatalf("%v: RunOnce() returned err = nil, want non-nil", test.desc)
   355  			}
   356  			if count != test.wantCount {
   357  				t.Errorf("%v: RunOnce() = %v, want = %v", test.desc, count, test.wantCount)
   358  			}
   359  			for _, want := range test.wantErrs {
   360  				if !strings.Contains(err.Error(), want) {
   361  					t.Errorf("%v: RunOnce() returned err = %q, want substring %q", test.desc, err, want)
   362  				}
   363  			}
   364  		})
   365  	}
   366  }