summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2023-04-15 09:00:33 -0600
committerLuke Shumaker <lukeshu@lukeshu.com>2023-04-17 01:49:21 -0600
commitf262c7bbc72796ae36b7e40dc38ab7167d138d5b (patch)
tree1dbcbd104be9e1034037a2d43b5927b1b61c08d6
parent9814752ef68aa9ef377a4a939bc83d2409d4fcef (diff)
btrfsutil: RebuiltForrest: Add a lax-ancestor mode
-rw-r--r--cmd/btrfs-rec/inspect/rebuildtrees/rebuild.go2
-rw-r--r--cmd/btrfs-rec/main.go2
-rw-r--r--lib/btrfs/btrfstree/path.go8
-rw-r--r--lib/btrfsutil/rebuilt_forrest.go69
-rw-r--r--lib/btrfsutil/rebuilt_forrest_test.go94
-rw-r--r--lib/btrfsutil/rebuilt_tree.go31
6 files changed, 165 insertions, 41 deletions
diff --git a/cmd/btrfs-rec/inspect/rebuildtrees/rebuild.go b/cmd/btrfs-rec/inspect/rebuildtrees/rebuild.go
index 77d56ae..aeb6751 100644
--- a/cmd/btrfs-rec/inspect/rebuildtrees/rebuild.go
+++ b/cmd/btrfs-rec/inspect/rebuildtrees/rebuild.go
@@ -85,7 +85,7 @@ func NewRebuilder(ctx context.Context, fs *btrfs.FS, nodeList []btrfsvol.Logical
o := &rebuilder{
scan: scanData,
}
- o.rebuilt = btrfsutil.NewRebuiltForrest(fs, scanData.Graph, forrestCallbacks{o})
+ o.rebuilt = btrfsutil.NewRebuiltForrest(fs, scanData.Graph, forrestCallbacks{o}, false)
return o, nil
}
diff --git a/cmd/btrfs-rec/main.go b/cmd/btrfs-rec/main.go
index 26857f0..ad768de 100644
--- a/cmd/btrfs-rec/main.go
+++ b/cmd/btrfs-rec/main.go
@@ -224,7 +224,7 @@ func _runWithReadableFS(wantNodeList bool, runE func(btrfs.ReadableFS, []btrfsvo
return err
}
- rfs = btrfsutil.NewRebuiltForrest(fs, graph, nil)
+ rfs = btrfsutil.NewRebuiltForrest(fs, graph, nil, true)
}
return runE(rfs, nodeList, cmd, args)
diff --git a/lib/btrfs/btrfstree/path.go b/lib/btrfs/btrfstree/path.go
index acc0e73..327a39b 100644
--- a/lib/btrfs/btrfstree/path.go
+++ b/lib/btrfs/btrfstree/path.go
@@ -12,6 +12,7 @@ import (
"git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsprim"
"git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol"
"git.lukeshu.com/btrfs-progs-ng/lib/containers"
+ "git.lukeshu.com/btrfs-progs-ng/lib/slices"
)
// Path is a path from the superblock or a ROOT_ITEM to a node or
@@ -137,6 +138,7 @@ func checkOwner(
ctx context.Context, forrest Forrest, treeID btrfsprim.ObjID,
ownerToCheck btrfsprim.ObjID, genToCheck btrfsprim.Generation,
) error {
+ var stack []btrfsprim.ObjID
for {
if ownerToCheck == treeID {
return nil
@@ -154,6 +156,12 @@ func checkOwner(
ownerToCheck, genToCheck, err)
}
+ stack = append(stack, treeID)
+ if slices.Contains(parentID, stack) {
+ // Don't get stuck in an infinite loop if there's a cycle.
+ parentID = 0
+ }
+
if parentID == 0 && parentGen == 0 {
return fmt.Errorf("owner=%v is not acceptable in this tree",
ownerToCheck)
diff --git a/lib/btrfsutil/rebuilt_forrest.go b/lib/btrfsutil/rebuilt_forrest.go
index 019d824..900e725 100644
--- a/lib/btrfsutil/rebuilt_forrest.go
+++ b/lib/btrfsutil/rebuilt_forrest.go
@@ -7,6 +7,7 @@ package btrfsutil
import (
"context"
"fmt"
+ "sync"
"github.com/datawire/dlib/dlog"
@@ -64,14 +65,18 @@ import (
// NewRebuiltForrest().
type RebuiltForrest struct {
// static
- inner btrfs.ReadableFS
- graph Graph
- cb RebuiltForrestCallbacks
+ inner btrfs.ReadableFS
+ graph Graph
+ cb RebuiltForrestCallbacks
+ laxAncestors bool
// mutable
- treesMu nestedMutex
- trees map[btrfsprim.ObjID]*RebuiltTree // must hold .treesMu to access
+ treesMu nestedMutex
+ trees map[btrfsprim.ObjID]*RebuiltTree // must hold .treesMu to access
+ commitTreesOnce sync.Once
+ treesCommitted bool // must hold .treesMu to access
+ treesCommitter btrfsprim.ObjID
rebuiltSharedCache
}
@@ -81,11 +86,23 @@ type RebuiltForrest struct {
// The `cb` RebuiltForrestCallbacks may be nil. If `cb` also
// implements RebuiltForrestExtendedCallbacks, then a series of
// .AddedItem() calls will be made before each call to .AddedRoot().
-func NewRebuiltForrest(fs btrfs.ReadableFS, graph Graph, cb RebuiltForrestCallbacks) *RebuiltForrest {
+//
+// `laxAncestors` is whether or not an error instantiating an ancestor
+// tree should prevent instantiating an descendant tree (lax=false
+// prevents it, lax=true allows it).
+//
+// - `laxAncestors` inhibits calls to
+// RebuiltForrestExtendedCallbacks.AddedItem().
+//
+// - `laxAncestors` causes a call to RebuiltTree.RebuiltAddRoot on
+// the ROOT_TREE or the UUID_TREE to panic if a tree other than the
+// ROOT_TREE or the UUID_TREE has been read from.
+func NewRebuiltForrest(fs btrfs.ReadableFS, graph Graph, cb RebuiltForrestCallbacks, laxAncestors bool) *RebuiltForrest {
ret := &RebuiltForrest{
- inner: fs,
- graph: graph,
- cb: cb,
+ inner: fs,
+ graph: graph,
+ cb: cb,
+ laxAncestors: laxAncestors,
trees: make(map[btrfsprim.ObjID]*RebuiltTree),
}
@@ -100,6 +117,26 @@ func NewRebuiltForrest(fs btrfs.ReadableFS, graph Graph, cb RebuiltForrestCallba
return ret
}
+func (ts *RebuiltForrest) commitTrees(ctx context.Context, treeID btrfsprim.ObjID) {
+ if treeID == btrfsprim.ROOT_TREE_OBJECTID || treeID == btrfsprim.UUID_TREE_OBJECTID {
+ return
+ }
+ ts.commitTreesOnce.Do(func() {
+ if !ts.laxAncestors {
+ return
+ }
+ ctx = ts.treesMu.Lock(ctx)
+ if !ts.treesCommitted {
+ // Make sure ROOT_TREE and UUID_TREE are ready for reading.
+ _, _ = ts.RebuiltTree(ctx, btrfsprim.ROOT_TREE_OBJECTID)
+ _, _ = ts.RebuiltTree(ctx, btrfsprim.UUID_TREE_OBJECTID)
+ ts.treesCommitted = true
+ ts.treesCommitter = treeID
+ }
+ ts.treesMu.Unlock()
+ })
+}
+
// RebuiltTree returns a given tree, initializing it if nescessary.
//
// The tree is initialized with the normal root node of the tree.
@@ -111,7 +148,7 @@ func (ts *RebuiltForrest) RebuiltTree(ctx context.Context, treeID btrfsprim.ObjI
defer ts.treesMu.Unlock()
ts.rebuildTree(ctx, treeID, nil)
tree := ts.trees[treeID]
- if tree.ancestorLoop && tree.rootErr == nil {
+ if tree.ancestorLoop && tree.rootErr == nil && tree.ancestorRoot == 0 {
var loop []btrfsprim.ObjID
for ancestor := tree; true; ancestor = ancestor.Parent {
loop = append(loop, ancestor.ID)
@@ -119,7 +156,11 @@ func (ts *RebuiltForrest) RebuiltTree(ctx context.Context, treeID btrfsprim.ObjI
break
}
}
- tree.rootErr = fmt.Errorf("loop detected: %v", loop)
+ if ts.laxAncestors {
+ tree.ancestorRoot = loop[len(loop)-2]
+ } else {
+ tree.rootErr = fmt.Errorf("loop detected: %v", loop)
+ }
}
if tree.rootErr != nil {
return nil, tree.rootErr
@@ -182,7 +223,9 @@ func (ts *RebuiltForrest) rebuildTree(ctx context.Context, treeID btrfsprim.ObjI
ts.trees[treeID].ParentGen = rootOff
parentID, ok := ts.cb.LookupUUID(ctx, rootItem.ParentUUID)
if !ok {
- ts.trees[treeID].rootErr = fmt.Errorf("failed to look up UUID: %v", rootItem.ParentUUID)
+ if !ts.laxAncestors {
+ ts.trees[treeID].rootErr = fmt.Errorf("failed to look up UUID: %v", rootItem.ParentUUID)
+ }
return
}
ts.rebuildTree(ctx, parentID, stack)
@@ -191,7 +234,7 @@ func (ts *RebuiltForrest) rebuildTree(ctx context.Context, treeID btrfsprim.ObjI
case ts.trees[treeID].Parent.ancestorLoop:
ts.trees[treeID].ancestorLoop = true
return
- case ts.trees[treeID].Parent.rootErr != nil:
+ case !ts.laxAncestors && ts.trees[treeID].Parent.rootErr != nil:
ts.trees[treeID].rootErr = fmt.Errorf("failed to rebuild parent tree: %v: %w", parentID, ts.trees[treeID].Parent.rootErr)
return
}
diff --git a/lib/btrfsutil/rebuilt_forrest_test.go b/lib/btrfsutil/rebuilt_forrest_test.go
index 96749c4..8bbb50a 100644
--- a/lib/btrfsutil/rebuilt_forrest_test.go
+++ b/lib/btrfsutil/rebuilt_forrest_test.go
@@ -106,26 +106,60 @@ func TestRebuiltTreeCycles(t *testing.T) {
},
}
- rfs := NewRebuiltForrest(nil, Graph{}, cbs)
-
- tree, err := rfs.RebuiltTree(ctx, 306)
- assert.EqualError(t, err, `loop detected: [306 305 304 303 305]`)
- assert.Nil(t, tree)
-
- assert.NotNil(t, rfs.trees[305])
- tree, err = rfs.RebuiltTree(ctx, 305)
- assert.EqualError(t, err, `loop detected: [305 304 303 305]`)
- assert.Nil(t, tree)
-
- assert.NotNil(t, rfs.trees[304])
- tree, err = rfs.RebuiltTree(ctx, 304)
- assert.EqualError(t, err, `loop detected: [304 303 305 304]`)
- assert.Nil(t, tree)
-
- assert.NotNil(t, rfs.trees[303])
- tree, err = rfs.RebuiltTree(ctx, 303)
- assert.EqualError(t, err, `loop detected: [303 305 304 303]`)
- assert.Nil(t, tree)
+ t.Run("strict", func(t *testing.T) {
+ t.Parallel()
+ rfs := NewRebuiltForrest(nil, Graph{}, cbs, false)
+
+ tree, err := rfs.RebuiltTree(ctx, 306)
+ assert.EqualError(t, err, `loop detected: [306 305 304 303 305]`)
+ assert.Nil(t, tree)
+
+ assert.NotNil(t, rfs.trees[305])
+ tree, err = rfs.RebuiltTree(ctx, 305)
+ assert.EqualError(t, err, `loop detected: [305 304 303 305]`)
+ assert.Nil(t, tree)
+
+ assert.NotNil(t, rfs.trees[304])
+ tree, err = rfs.RebuiltTree(ctx, 304)
+ assert.EqualError(t, err, `loop detected: [304 303 305 304]`)
+ assert.Nil(t, tree)
+
+ assert.NotNil(t, rfs.trees[303])
+ tree, err = rfs.RebuiltTree(ctx, 303)
+ assert.EqualError(t, err, `loop detected: [303 305 304 303]`)
+ assert.Nil(t, tree)
+ })
+ t.Run("lax", func(t *testing.T) {
+ t.Parallel()
+ rfs := NewRebuiltForrest(nil, Graph{}, cbs, true)
+
+ tree, err := rfs.RebuiltTree(ctx, 306)
+ assert.NoError(t, err)
+ assert.NotNil(t, tree)
+ assert.True(t, tree.ancestorLoop)
+ assert.Equal(t, btrfsprim.ObjID(303), tree.ancestorRoot)
+
+ assert.NotNil(t, rfs.trees[305])
+ tree, err = rfs.RebuiltTree(ctx, 305)
+ assert.NoError(t, err)
+ assert.NotNil(t, tree)
+ assert.True(t, tree.ancestorLoop)
+ assert.Equal(t, btrfsprim.ObjID(303), tree.ancestorRoot)
+
+ assert.NotNil(t, rfs.trees[304])
+ tree, err = rfs.RebuiltTree(ctx, 304)
+ assert.NoError(t, err)
+ assert.NotNil(t, tree)
+ assert.True(t, tree.ancestorLoop)
+ assert.Equal(t, btrfsprim.ObjID(305), tree.ancestorRoot)
+
+ assert.NotNil(t, rfs.trees[303])
+ tree, err = rfs.RebuiltTree(ctx, 303)
+ assert.NoError(t, err)
+ assert.NotNil(t, tree)
+ assert.True(t, tree.ancestorLoop)
+ assert.Equal(t, btrfsprim.ObjID(304), tree.ancestorRoot)
+ })
}
func TestRebuiltTreeParentErr(t *testing.T) {
@@ -185,9 +219,21 @@ func TestRebuiltTreeParentErr(t *testing.T) {
},
}
- rfs := NewRebuiltForrest(nil, Graph{}, cbs)
+ t.Run("strict", func(t *testing.T) {
+ t.Parallel()
+ rfs := NewRebuiltForrest(nil, Graph{}, cbs, false)
- tree, err := rfs.RebuiltTree(ctx, 305)
- assert.EqualError(t, err, `failed to rebuild parent tree: 304: tree does not exist`)
- assert.Nil(t, tree)
+ tree, err := rfs.RebuiltTree(ctx, 305)
+ assert.EqualError(t, err, `failed to rebuild parent tree: 304: tree does not exist`)
+ assert.Nil(t, tree)
+ })
+
+ t.Run("lax", func(t *testing.T) {
+ t.Parallel()
+ rfs := NewRebuiltForrest(nil, Graph{}, cbs, true)
+
+ tree, err := rfs.RebuiltTree(ctx, 305)
+ assert.NoError(t, err)
+ assert.NotNil(t, tree)
+ })
}
diff --git a/lib/btrfsutil/rebuilt_tree.go b/lib/btrfsutil/rebuilt_tree.go
index e963f0a..3036938 100644
--- a/lib/btrfsutil/rebuilt_tree.go
+++ b/lib/btrfsutil/rebuilt_tree.go
@@ -27,6 +27,7 @@ type RebuiltTree struct {
rootErr error
ancestorLoop bool
+ ancestorRoot btrfsprim.ObjID
ID btrfsprim.ObjID
UUID btrfsprim.UUID
@@ -127,6 +128,9 @@ func (tree *RebuiltTree) uncachedNodeIndex(ctx context.Context) rebuiltNodeIndex
}
for ancestor := tree; ancestor != nil; ancestor = ancestor.Parent {
indexer.idToTree[ancestor.ID] = ancestor
+ if ancestor.ID == tree.ancestorRoot {
+ break
+ }
}
ret := rebuiltNodeIndex{
@@ -261,11 +265,12 @@ func (tree *RebuiltTree) isOwnerOK(owner btrfsprim.ObjID, gen btrfsprim.Generati
// the "false"/failure case. It will be called lots of times
// in a tight loop for both values that pass and values that
// fail.
+ root := tree.ancestorRoot
for {
if owner == tree.ID {
return true
}
- if tree.Parent == nil || gen > tree.ParentGen {
+ if tree.Parent == nil || gen > tree.ParentGen || tree.ID == root {
return false
}
tree = tree.Parent
@@ -282,6 +287,7 @@ func (tree *RebuiltTree) isOwnerOK(owner btrfsprim.ObjID, gen btrfsprim.Generati
//
// When done with the map, call .RebuiltReleaseItems().
func (tree *RebuiltTree) RebuiltAcquireItems(ctx context.Context) *containers.SortedMap[btrfsprim.Key, ItemPtr] {
+ tree.forrest.commitTrees(ctx, tree.ID)
tree.initRoots(ctx)
tree.mu.RLock()
defer tree.mu.RUnlock()
@@ -303,6 +309,7 @@ func (tree *RebuiltTree) RebuiltReleaseItems() {
//
// When done with the map, call .RebuiltReleasePotentialItems().
func (tree *RebuiltTree) RebuiltAcquirePotentialItems(ctx context.Context) *containers.SortedMap[btrfsprim.Key, ItemPtr] {
+ tree.forrest.commitTrees(ctx, tree.ID)
tree.initRoots(ctx)
tree.mu.RLock()
defer tree.mu.RUnlock()
@@ -427,6 +434,15 @@ func (s rebuiltRootStats) String() string {
// RebuiltAddRoot adds an additional root node to the tree. It is
// useful to call .RebuiltAddRoot() to re-attach part of the tree that
// has been broken off.
+//
+// If the RebuiltForrest has laxAncestors=false, then:
+//
+// - calls to RebuiltForrestExtendedCallbacks.AddedItem() are
+// inhibited.
+//
+// - calling RebuiltAddRoot on the ROOT_TREE or the UUID_TREE will
+// panic if a tree other than the ROOT_TREE or UUID_TREE has been
+// read from.
func (tree *RebuiltTree) RebuiltAddRoot(ctx context.Context, rootNode btrfsvol.LogicalAddr) {
tree.mu.Lock()
defer tree.mu.Unlock()
@@ -436,7 +452,16 @@ func (tree *RebuiltTree) RebuiltAddRoot(ctx context.Context, rootNode btrfsvol.L
shouldFlush := tree.ID == btrfsprim.ROOT_TREE_OBJECTID || tree.ID == btrfsprim.UUID_TREE_OBJECTID
- if extCB, ok := tree.forrest.cb.(RebuiltForrestExtendedCallbacks); ok {
+ if tree.forrest.laxAncestors && shouldFlush {
+ _ = tree.forrest.treesMu.Lock(ctx)
+ if tree.forrest.treesCommitted {
+ panic(fmt.Errorf("RebuiltTree(%v).RebuiltAddRoot called after a non-ROOT, non-UUID tree (%v) has been read from",
+ tree.ID, tree.forrest.treesCommitter))
+ }
+ tree.forrest.treesMu.Unlock()
+ }
+
+ if extCB, ok := tree.forrest.cb.(RebuiltForrestExtendedCallbacks); ok && !tree.forrest.laxAncestors {
var stats rebuiltRootStats
nodeToRoots := tree.acquireNodeIndex(ctx).nodeToRoots
stats.Nodes.D = len(nodeToRoots)
@@ -501,6 +526,7 @@ func (tree *RebuiltTree) RebuiltLeafToRoots(ctx context.Context, leaf btrfsvol.L
tree.ID, leaf))
}
+ tree.forrest.commitTrees(ctx, tree.ID)
tree.initRoots(ctx)
tree.mu.RLock()
defer tree.mu.RUnlock()
@@ -601,6 +627,7 @@ func (tree *RebuiltTree) TreeSubrange(ctx context.Context,
// TreeWalk implements btrfstree.Tree.
func (tree *RebuiltTree) TreeWalk(ctx context.Context, cbs btrfstree.TreeWalkHandler) {
+ tree.forrest.commitTrees(ctx, tree.ID)
tree.initRoots(ctx)
tree.mu.RLock()
defer tree.mu.RUnlock()