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 }