Skip to content

Commit e917bba

Browse files
committed
traversal: budget tests, well-typed errors, more error info, and fix off-by-one.
1 parent 3ac6a8f commit e917bba

File tree

4 files changed

+97
-17
lines changed

4 files changed

+97
-17
lines changed

traversal/fns.go

+15
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package traversal
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/ipld/go-ipld-prime/datamodel"
78
"github.com/ipld/go-ipld-prime/linking"
@@ -80,3 +81,17 @@ type SkipMe struct{}
8081
func (SkipMe) Error() string {
8182
return "skip"
8283
}
84+
85+
type ErrBudgetExceeded struct {
86+
BudgetKind string // "node"|"link"
87+
Path datamodel.Path
88+
Link datamodel.Link // only present if BudgetKind=="link"
89+
}
90+
91+
func (e *ErrBudgetExceeded) Error() string {
92+
msg := fmt.Sprintf("traversal budget exceeded: budget for %ss reached zero as we reached path %q", e.BudgetKind, e.Path)
93+
if e.Link != nil {
94+
msg += fmt.Sprintf(" (link: %q)", e.Link)
95+
}
96+
return msg
97+
}

traversal/focus.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func (prog *Progress) get(n datamodel.Node, p datamodel.Path, trackProgress bool
9191
if prog.Budget != nil {
9292
prog.Budget.NodeBudget--
9393
if prog.Budget.NodeBudget <= 0 {
94-
return nil, fmt.Errorf("traversal budget for nodes visited exceeded")
94+
return nil, &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path}
9595
}
9696
}
9797
// Traverse the segment.
@@ -119,15 +119,15 @@ func (prog *Progress) get(n datamodel.Node, p datamodel.Path, trackProgress bool
119119
}
120120
// Dereference any links.
121121
for n.Kind() == datamodel.Kind_Link {
122+
lnk, _ := n.AsLink()
122123
// Check the budget!
123124
if prog.Budget != nil {
124-
prog.Budget.LinkBudget--
125125
if prog.Budget.LinkBudget <= 0 {
126-
return nil, fmt.Errorf("traversal budget for links exceeded")
126+
return nil, &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk}
127127
}
128+
prog.Budget.LinkBudget--
128129
}
129130
// Put together the context info we'll offer to the loader and prototypeChooser.
130-
lnk, _ := n.AsLink()
131131
lnkCtx := linking.LinkContext{
132132
Ctx: prog.Cfg.Ctx,
133133
LinkPath: p.Truncate(i),
@@ -218,10 +218,10 @@ func (prog Progress) focusedTransform(n datamodel.Node, na datamodel.NodeAssembl
218218
seg, p2 := p.Shift()
219219
// Check the budget!
220220
if prog.Budget != nil {
221-
prog.Budget.NodeBudget--
222221
if prog.Budget.NodeBudget <= 0 {
223-
return fmt.Errorf("traversal budget for nodes visited exceeded")
222+
return &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path}
224223
}
224+
prog.Budget.NodeBudget--
225225
}
226226
// Special branch for if we've entered createParent mode in an earlier step.
227227
// This needs slightly different logic because there's no prior node to reference
@@ -341,12 +341,13 @@ func (prog Progress) focusedTransform(n datamodel.Node, na datamodel.NodeAssembl
341341
}
342342
return la.Finish()
343343
case datamodel.Kind_Link:
344+
lnk, _ := n.AsLink()
344345
// Check the budget!
345346
if prog.Budget != nil {
346-
prog.Budget.LinkBudget--
347347
if prog.Budget.LinkBudget <= 0 {
348-
return fmt.Errorf("traversal budget for links exceeded")
348+
return &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk}
349349
}
350+
prog.Budget.LinkBudget--
350351
}
351352
// Put together the context info we'll offer to the loader and prototypeChooser.
352353
lnkCtx := linking.LinkContext{
@@ -355,7 +356,6 @@ func (prog Progress) focusedTransform(n datamodel.Node, na datamodel.NodeAssembl
355356
LinkNode: n,
356357
ParentNode: nil, // TODO inconvenient that we don't have this. maybe this whole case should be a helper function.
357358
}
358-
lnk, _ := n.AsLink()
359359
// Pick what in-memory format we will build.
360360
np, err := prog.Cfg.LinkTargetNodePrototypeChooser(lnk, lnkCtx)
361361
if err != nil {

traversal/walk.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,10 @@ func (prog Progress) WalkAdv(n datamodel.Node, s selector.Selector, fn AdvVisitF
8888
func (prog Progress) walkAdv(n datamodel.Node, s selector.Selector, fn AdvVisitFn) error {
8989
// Check the budget!
9090
if prog.Budget != nil {
91-
prog.Budget.NodeBudget--
9291
if prog.Budget.NodeBudget <= 0 {
93-
return fmt.Errorf("traversal budget for nodes visited exceeded")
92+
return &ErrBudgetExceeded{BudgetKind: "node", Path: prog.Path}
9493
}
94+
prog.Budget.NodeBudget--
9595
}
9696
// Decide if this node is matched -- do callbacks as appropriate.
9797
if s.Decide(n) {
@@ -190,18 +190,18 @@ func (prog Progress) walkAdv_iterateSelective(n datamodel.Node, attn []datamodel
190190
}
191191

192192
func (prog Progress) loadLink(v datamodel.Node, parent datamodel.Node) (datamodel.Node, error) {
193+
lnk, err := v.AsLink()
194+
if err != nil {
195+
return nil, err
196+
}
193197
// Check the budget!
194198
if prog.Budget != nil {
195-
prog.Budget.LinkBudget--
196199
if prog.Budget.LinkBudget <= 0 {
197-
return nil, fmt.Errorf("traversal budget for links exceeded")
200+
return nil, &ErrBudgetExceeded{BudgetKind: "link", Path: prog.Path, Link: lnk}
198201
}
202+
prog.Budget.LinkBudget--
199203
}
200204
// Put together the context info we'll offer to the loader and prototypeChooser.
201-
lnk, err := v.AsLink()
202-
if err != nil {
203-
return nil, err
204-
}
205205
lnkCtx := linking.LinkContext{
206206
Ctx: prog.Cfg.Ctx,
207207
LinkPath: prog.Path,

traversal/walk_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package traversal_test
33
import (
44
"testing"
55

6+
qt "github.com/frankban/quicktest"
67
. "github.com/warpfork/go-wish"
78

89
_ "github.com/ipld/go-ipld-prime/codec/dagjson"
@@ -257,3 +258,67 @@ func TestWalkMatching(t *testing.T) {
257258
Wish(t, order, ShouldEqual, 7)
258259
})
259260
}
261+
262+
func TestWalkBudgets(t *testing.T) {
263+
ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any)
264+
t.Run("node-budget-halts", func(t *testing.T) {
265+
ss := ssb.ExploreFields(func(efsb builder.ExploreFieldsSpecBuilder) {
266+
efsb.Insert("foo", ssb.Matcher())
267+
efsb.Insert("bar", ssb.Matcher())
268+
})
269+
s, err := ss.Selector()
270+
qt.Assert(t, err, qt.Equals, nil)
271+
var order int
272+
prog := traversal.Progress{}
273+
prog.Budget = &traversal.Budget{
274+
NodeBudget: 2, // should reach root, then "foo", then stop.
275+
}
276+
err = prog.WalkMatching(middleMapNode, s, func(prog traversal.Progress, n datamodel.Node) error {
277+
switch order {
278+
case 0:
279+
qt.Assert(t, n, qt.CmpEquals(), basicnode.NewBool(true))
280+
qt.Assert(t, prog.Path.String(), qt.Equals, "foo")
281+
}
282+
order++
283+
return nil
284+
})
285+
qt.Check(t, order, qt.Equals, 1) // because it should've stopped early
286+
qt.Assert(t, err, qt.Not(qt.Equals), nil)
287+
qt.Check(t, err.Error(), qt.Equals, `traversal budget exceeded: budget for nodes reached zero as we reached path "bar"`)
288+
})
289+
t.Run("link-budget-halts", func(t *testing.T) {
290+
ss := ssb.ExploreAll(ssb.Matcher())
291+
s, err := ss.Selector()
292+
qt.Assert(t, err, qt.Equals, nil)
293+
var order int
294+
lsys := cidlink.DefaultLinkSystem()
295+
lsys.StorageReadOpener = (&store).OpenRead
296+
err = traversal.Progress{
297+
Cfg: &traversal.Config{
298+
LinkSystem: lsys,
299+
LinkTargetNodePrototypeChooser: basicnode.Chooser,
300+
},
301+
Budget: &traversal.Budget{
302+
NodeBudget: 9000,
303+
LinkBudget: 3,
304+
},
305+
}.WalkMatching(middleListNode, s, func(prog traversal.Progress, n datamodel.Node) error {
306+
switch order {
307+
case 0:
308+
qt.Assert(t, n, qt.CmpEquals(), basicnode.NewString("alpha"))
309+
qt.Assert(t, prog.Path.String(), qt.Equals, "0")
310+
case 1:
311+
qt.Assert(t, n, qt.CmpEquals(), basicnode.NewString("alpha"))
312+
qt.Assert(t, prog.Path.String(), qt.Equals, "1")
313+
case 2:
314+
qt.Assert(t, n, qt.CmpEquals(), basicnode.NewString("beta"))
315+
qt.Assert(t, prog.Path.String(), qt.Equals, "2")
316+
}
317+
order++
318+
return nil
319+
})
320+
qt.Check(t, order, qt.Equals, 3)
321+
qt.Assert(t, err, qt.Not(qt.Equals), nil)
322+
qt.Check(t, err.Error(), qt.Equals, `traversal budget exceeded: budget for links reached zero as we reached path "3" (link: "baguqeeyexkjwnfy")`)
323+
})
324+
}

0 commit comments

Comments
 (0)