github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/state_checker.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libkbfs 6 7 import ( 8 "errors" 9 "fmt" 10 "reflect" 11 "time" 12 13 "github.com/keybase/client/go/kbfs/data" 14 "github.com/keybase/client/go/kbfs/kbfsblock" 15 "github.com/keybase/client/go/kbfs/kbfsmd" 16 "github.com/keybase/client/go/kbfs/kbfssync" 17 "github.com/keybase/client/go/kbfs/libkey" 18 "github.com/keybase/client/go/kbfs/tlf" 19 "github.com/keybase/client/go/logger" 20 "github.com/keybase/client/go/protocol/keybase1" 21 "golang.org/x/net/context" 22 ) 23 24 // StateChecker verifies that the server-side state for KBFS is 25 // consistent. Useful mostly for testing because it isn't scalable 26 // and loads all the state in memory. 27 type StateChecker struct { 28 config Config 29 log logger.Logger 30 } 31 32 // NewStateChecker returns a new StateChecker instance. 33 func NewStateChecker(config Config) *StateChecker { 34 return &StateChecker{config, config.MakeLogger("")} 35 } 36 37 // findAllFileBlocks adds all file blocks found under this block to 38 // the blockSizes map, if the given path represents an indirect block. 39 func (sc *StateChecker) findAllFileBlocks(ctx context.Context, 40 lState *kbfssync.LockState, ops *folderBranchOps, kmd libkey.KeyMetadata, 41 file data.Path, blockSizes map[data.BlockPointer]uint32) error { 42 infos, err := ops.blocks.GetIndirectFileBlockInfos(ctx, lState, kmd, file) 43 if err != nil { 44 return err 45 } 46 47 for _, info := range infos { 48 blockSizes[info.BlockPointer] = info.EncodedSize 49 } 50 return nil 51 } 52 53 // findAllDirBlocks adds all dir blocks found under this block to the 54 // blockSizes map, if the given path represents an indirect block. 55 func (sc *StateChecker) findAllDirBlocks(ctx context.Context, 56 lState *kbfssync.LockState, ops *folderBranchOps, kmd libkey.KeyMetadata, 57 dir data.Path, blockSizes map[data.BlockPointer]uint32) error { 58 infos, err := ops.blocks.GetIndirectDirBlockInfos(ctx, lState, kmd, dir) 59 if err != nil { 60 return err 61 } 62 63 for _, info := range infos { 64 blockSizes[info.BlockPointer] = info.EncodedSize 65 } 66 return nil 67 } 68 69 // findAllBlocksInPath adds all blocks found within this directory to 70 // the blockSizes map, and then recursively checks all 71 // subdirectories. 72 func (sc *StateChecker) findAllBlocksInPath(ctx context.Context, 73 lState *kbfssync.LockState, ops *folderBranchOps, kmd libkey.KeyMetadata, 74 dir data.Path, blockSizes map[data.BlockPointer]uint32) error { 75 children, err := ops.blocks.GetEntries(ctx, lState, kmd, dir) 76 if err != nil { 77 return err 78 } 79 80 err = sc.findAllDirBlocks(ctx, lState, ops, kmd, dir, blockSizes) 81 if err != nil { 82 return err 83 } 84 85 for name, de := range children { 86 if de.Type == data.Sym { 87 continue 88 } 89 90 blockSizes[de.BlockPointer] = de.EncodedSize 91 p := dir.ChildPath(name, de.BlockPointer, ops.makeObfuscator()) 92 93 if de.Type == data.Dir { 94 err := sc.findAllBlocksInPath(ctx, lState, ops, kmd, p, blockSizes) 95 if err != nil { 96 return err 97 } 98 } else { 99 // If it's a file, check to see if it's indirect. 100 err := sc.findAllFileBlocks(ctx, lState, ops, kmd, p, blockSizes) 101 if err != nil { 102 return err 103 } 104 } 105 } 106 return nil 107 } 108 109 func (sc *StateChecker) getLastGCData(ctx context.Context, 110 tlfID tlf.ID) (time.Time, kbfsmd.Revision) { 111 config, ok := sc.config.(*ConfigLocal) 112 if !ok { 113 return time.Time{}, kbfsmd.RevisionUninitialized 114 } 115 116 var latestTime time.Time 117 var latestRev kbfsmd.Revision 118 for _, c := range *config.allKnownConfigsForTesting { 119 ops := c.KBFSOps().(*KBFSOpsStandard).getOpsIfExists( 120 context.Background(), 121 data.FolderBranch{Tlf: tlfID, Branch: data.MasterBranch}) 122 if ops == nil { 123 continue 124 } 125 rt, rev := ops.fbm.getLastQRData() 126 if rt.After(latestTime) && rev > latestRev { 127 latestTime = rt 128 latestRev = rev 129 } 130 } 131 if latestTime.IsZero() { 132 return latestTime, latestRev 133 } 134 135 sc.log.CDebugf(ctx, "Last qr data for TLF %s: revTime=%s, rev=%d", 136 tlfID, latestTime, latestRev) 137 return latestTime.Add( 138 -sc.config.Mode().QuotaReclamationMinUnrefAge()), latestRev 139 } 140 141 // CheckMergedState verifies that the state for the given tlf is 142 // consistent. 143 func (sc *StateChecker) CheckMergedState(ctx context.Context, tlfID tlf.ID) error { 144 // Blow away MD cache so we don't have any lingering re-embedded 145 // block changes (otherwise we won't be able to learn their sizes). 146 sc.config.SetMDCache(NewMDCacheStandard(defaultMDCacheCapacity)) 147 148 // Fetch all the MD updates for this folder, and use the block 149 // change lists to build up the set of currently referenced blocks. 150 rmds, err := getMergedMDUpdates(ctx, sc.config, tlfID, 151 kbfsmd.RevisionInitial, nil) 152 if err != nil { 153 return err 154 } 155 if len(rmds) == 0 { 156 sc.log.CDebugf(ctx, "No state to check for folder %s", tlfID) 157 return nil 158 } 159 160 lState := makeFBOLockState() 161 162 // Re-embed block changes. 163 kbfsOps, ok := sc.config.KBFSOps().(*KBFSOpsStandard) 164 if !ok { 165 return errors.New("Unexpected KBFSOps type") 166 } 167 168 fb := data.FolderBranch{Tlf: tlfID, Branch: data.MasterBranch} 169 ops := kbfsOps.getOps(context.Background(), fb, FavoritesOpNoChange) 170 lastGCRevisionTime, lastGCRev := sc.getLastGCData(ctx, tlfID) 171 172 // Build the expected block list. 173 expectedLiveBlocks := make(map[data.BlockPointer]bool) 174 expectedRef := uint64(0) 175 expectedMDRef := uint64(0) 176 archivedBlocks := make(map[data.BlockPointer]bool) 177 actualLiveBlocks := make(map[data.BlockPointer]uint32) 178 179 // See what the last GC op revision is. All unref'd pointers from 180 // that revision or earlier should be deleted from the block 181 // server. 182 gcRevision := kbfsmd.RevisionUninitialized 183 for _, rmd := range rmds { 184 // Don't process copies. 185 if rmd.IsWriterMetadataCopiedSet() { 186 continue 187 } 188 189 for _, op := range rmd.data.Changes.Ops { 190 GCOp, ok := op.(*GCOp) 191 if !ok { 192 continue 193 } 194 gcRevision = GCOp.LatestRev 195 } 196 } 197 198 for _, rmd := range rmds { 199 // Don't process copies. 200 if rmd.IsWriterMetadataCopiedSet() { 201 continue 202 } 203 // Unembedded block changes count towards the MD size. 204 if info := rmd.data.cachedChanges.Info; info.BlockPointer != data.ZeroPtr { 205 sc.log.CDebugf(ctx, "Unembedded block change: %v, %d", 206 info.BlockPointer, info.EncodedSize) 207 actualLiveBlocks[info.BlockPointer] = info.EncodedSize 208 209 // Any child block change pointers? 210 file := data.Path{ 211 FolderBranch: data.FolderBranch{ 212 Tlf: tlfID, Branch: data.MasterBranch}, 213 Path: []data.PathNode{{ 214 BlockPointer: info.BlockPointer, 215 Name: data.NewPathPartString(fmt.Sprintf( 216 "<MD with revision %d>", rmd.Revision()), nil), 217 }}} 218 err := sc.findAllFileBlocks(ctx, lState, ops, rmd.ReadOnly(), 219 file, actualLiveBlocks) 220 if err != nil { 221 return err 222 } 223 } 224 225 var hasGCOp bool 226 updated := make(map[data.BlockPointer]bool) 227 for _, op := range rmd.data.Changes.Ops { 228 _, isGCOp := op.(*GCOp) 229 hasGCOp = hasGCOp || isGCOp 230 231 opRefs := make(map[data.BlockPointer]bool) 232 for _, ptr := range op.Refs() { 233 if ptr != data.ZeroPtr { 234 expectedLiveBlocks[ptr] = true 235 opRefs[ptr] = true 236 } 237 } 238 if !isGCOp { 239 for _, ptr := range op.Unrefs() { 240 if updated[ptr] { 241 return fmt.Errorf( 242 "%s already updated in this revision %d", 243 ptr, rmd.Revision()) 244 } 245 delete(expectedLiveBlocks, ptr) 246 if ptr != data.ZeroPtr { 247 // If the revision has been garbage-collected, 248 // or if the pointer has been referenced and 249 // unreferenced within the same op (which 250 // indicates a failed and retried sync), the 251 // corresponding block should already be 252 // cleaned up. 253 if rmd.Revision() <= gcRevision || opRefs[ptr] { 254 delete(archivedBlocks, ptr) 255 } else { 256 archivedBlocks[ptr] = true 257 } 258 } 259 } 260 } 261 for _, update := range op.allUpdates() { 262 if update.Ref != update.Unref { 263 updated[update.Unref] = true 264 delete(expectedLiveBlocks, update.Unref) 265 } 266 if update.Unref != data.ZeroPtr && update.Ref != update.Unref { 267 if rmd.Revision() <= gcRevision { 268 delete(archivedBlocks, update.Unref) 269 } else { 270 archivedBlocks[update.Unref] = true 271 } 272 } 273 if update.Ref != data.ZeroPtr && update.Ref != update.Unref { 274 expectedLiveBlocks[update.Ref] = true 275 } 276 } 277 } 278 expectedRef += rmd.RefBytes() 279 expectedRef -= rmd.UnrefBytes() 280 expectedMDRef += rmd.MDRefBytes() 281 282 if len(rmd.data.Changes.Ops) == 1 && hasGCOp { 283 // Don't check GC status for GC revisions 284 continue 285 } 286 287 // Make sure that if this revision should be covered by a GC 288 // op, it is. Note that this assumes that if QR is ever run, 289 // it will be run completely and not left partially done due 290 // to there being too many pointers to collect in one sweep. 291 mtime := time.Unix(0, rmd.data.Dir.Mtime) 292 if !lastGCRevisionTime.Before(mtime) && rmd.Revision() <= lastGCRev && 293 rmd.Revision() > gcRevision { 294 return fmt.Errorf("Revision %d happened on or before the last "+ 295 "gc time %s rev %d, but was not included in the latest "+ 296 "gc op revision %d", rmd.Revision(), lastGCRevisionTime, 297 lastGCRev, gcRevision) 298 } 299 } 300 sc.log.CDebugf(ctx, "Folder %v has %d expected live blocks, "+ 301 "total %d bytes (%d MD bytes)", tlfID, len(expectedLiveBlocks), 302 expectedRef, expectedMDRef) 303 304 currMD := rmds[len(rmds)-1] 305 expectedUsage := currMD.DiskUsage() 306 if expectedUsage != expectedRef { 307 return fmt.Errorf("Expected ref bytes %d doesn't match latest disk "+ 308 "usage %d", expectedRef, expectedUsage) 309 } 310 expectedMDUsage := currMD.MDDiskUsage() 311 if expectedMDUsage != expectedMDRef { 312 return fmt.Errorf("Expected MD ref bytes %d doesn't match latest disk "+ 313 "MD usage %d", expectedMDRef, expectedMDUsage) 314 } 315 316 // Then, using the current MD head, start at the root of the FS 317 // and recursively walk the directory tree to find all the blocks 318 // that are currently accessible. 319 rootNode, _, _, err := ops.getRootNode(ctx) 320 if err != nil { 321 return err 322 } 323 rootPath := ops.nodeCache.PathFromNode(rootNode) 324 if g, e := rootPath.TailPointer(), currMD.data.Dir.BlockPointer; g != e { 325 return fmt.Errorf("Current MD root pointer %v doesn't match root "+ 326 "node pointer %v", e, g) 327 } 328 actualLiveBlocks[rootPath.TailPointer()] = currMD.data.Dir.EncodedSize 329 if err := sc.findAllBlocksInPath(ctx, lState, ops, currMD.ReadOnly(), 330 rootPath, actualLiveBlocks); err != nil { 331 return err 332 } 333 sc.log.CDebugf(ctx, "Folder %v has %d actual live blocks", 334 tlfID, len(actualLiveBlocks)) 335 336 // Compare the two and see if there are any differences. Don't use 337 // reflect.DeepEqual so we can print out exactly what's wrong. 338 var extraBlocks []data.BlockPointer 339 actualSize := uint64(0) 340 actualMDSize := uint64(0) 341 for ptr, size := range actualLiveBlocks { 342 if ptr.GetBlockType() == keybase1.BlockType_MD { 343 actualMDSize += uint64(size) 344 } else { 345 actualSize += uint64(size) 346 } 347 if !expectedLiveBlocks[ptr] { 348 extraBlocks = append(extraBlocks, ptr) 349 } 350 } 351 if len(extraBlocks) != 0 { 352 sc.log.CWarningf(ctx, "%v: Extra live blocks found: %v", 353 tlfID, extraBlocks) 354 return fmt.Errorf("Folder %v has inconsistent state", tlfID) 355 } 356 var missingBlocks []data.BlockPointer 357 for ptr := range expectedLiveBlocks { 358 if _, ok := actualLiveBlocks[ptr]; !ok { 359 missingBlocks = append(missingBlocks, ptr) 360 } 361 } 362 if len(missingBlocks) != 0 { 363 sc.log.CWarningf(ctx, "%v: Expected live blocks not found: %v", 364 tlfID, missingBlocks) 365 return fmt.Errorf("Folder %v has inconsistent state", tlfID) 366 } 367 368 if actualSize != expectedRef { 369 return fmt.Errorf("Actual size %d doesn't match expected size %d", 370 actualSize, expectedRef) 371 } 372 if actualMDSize != expectedMDRef { 373 return fmt.Errorf("Actual MD size %d doesn't match expected MD size %d", 374 actualMDSize, expectedMDRef) 375 } 376 377 // Check that the set of referenced blocks matches exactly what 378 // the block server knows about. 379 bserverLocal, ok := sc.config.BlockServer().(blockServerLocal) 380 if !ok { 381 if jbs, jok := sc.config.BlockServer().(journalBlockServer); jok { 382 bserverLocal, ok = jbs.BlockServer.(blockServerLocal) 383 if !ok { 384 sc.log.CDebugf(ctx, "Bad block server: %T", jbs.BlockServer) 385 } 386 } 387 } 388 if !ok { 389 return errors.New("StateChecker only works against BlockServerLocal") 390 } 391 bserverKnownBlocks, err := bserverLocal.getAllRefsForTest(ctx, tlfID) 392 if err != nil { 393 return err 394 } 395 396 blockRefsByID := make(map[kbfsblock.ID]blockRefMap) 397 for ptr := range expectedLiveBlocks { 398 if _, ok := blockRefsByID[ptr.ID]; !ok { 399 blockRefsByID[ptr.ID] = make(blockRefMap) 400 } 401 err := blockRefsByID[ptr.ID].put(ptr.Context, liveBlockRef, "") 402 if err != nil { 403 return err 404 } 405 } 406 for ptr := range archivedBlocks { 407 if _, ok := blockRefsByID[ptr.ID]; !ok { 408 blockRefsByID[ptr.ID] = make(blockRefMap) 409 } 410 err := blockRefsByID[ptr.ID].put(ptr.Context, archivedBlockRef, "") 411 if err != nil { 412 return err 413 } 414 } 415 416 if g, e := bserverKnownBlocks, blockRefsByID; !reflect.DeepEqual(g, e) { 417 for id, eRefs := range e { 418 if gRefs := g[id]; !reflect.DeepEqual(gRefs, eRefs) { 419 sc.log.CDebugf(ctx, "Refs for ID %v don't match. "+ 420 "Got %v, expected %v", id, gRefs, eRefs) 421 } 422 } 423 for id, gRefs := range g { 424 if _, ok := e[id]; !ok { 425 sc.log.CDebugf(ctx, "Did not find matching expected "+ 426 "ID for found block %v (with refs %v)", id, gRefs) 427 } 428 } 429 430 return fmt.Errorf("Folder %v has inconsistent state", tlfID) 431 } 432 433 // TODO: Check the archived and deleted blocks as well. 434 return nil 435 }