Skip to content

Commit

Permalink
refactor: give some love to path/resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Oct 5, 2023
1 parent 1a093bd commit dc30a58
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 217 deletions.
11 changes: 8 additions & 3 deletions gateway/blocks_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ func (bb *BlocksBackend) GetCAR(ctx context.Context, p path.ImmutablePath, param
}

// walkGatewaySimpleSelector walks the subgraph described by the path and terminal element parameters
func walkGatewaySimpleSelector(ctx context.Context, p path.Path, params CarParams, lsys *ipld.LinkSystem, pathResolver resolver.Resolver) error {
func walkGatewaySimpleSelector(ctx context.Context, p path.ImmutablePath, params CarParams, lsys *ipld.LinkSystem, pathResolver resolver.Resolver) error {
// First resolve the path since we always need to.
lastCid, remainder, err := pathResolver.ResolveToLastNode(ctx, p)
if err != nil {
Expand Down Expand Up @@ -598,7 +598,7 @@ func (bb *BlocksBackend) getPathRoots(ctx context.Context, contentPath path.Immu
// TODO: should we be more explicit here and is this part of the IPFSBackend contract?
// The issue here was that we returned datamodel.ErrWrongKind instead of this resolver error
if isErrNotFound(err) {
return nil, nil, resolver.ErrNoLink{Name: root, Node: lastPath.Cid()}
return nil, nil, &resolver.ErrNoLink{Name: root, Node: lastPath.Cid()}
}
return nil, nil, err
}
Expand Down Expand Up @@ -692,7 +692,12 @@ func (bb *BlocksBackend) resolvePath(ctx context.Context, p path.Path) (path.Imm
return nil, fmt.Errorf("unsupported path namespace: %s", p.Namespace())
}

node, rest, err := bb.resolver.ResolveToLastNode(ctx, p)
imPath, err := path.NewImmutablePath(p)
if err != nil {
return nil, err
}

node, rest, err := bb.resolver.ResolveToLastNode(ctx, imPath)
if err != nil {
return nil, err
}
Expand Down
11 changes: 5 additions & 6 deletions gateway/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,14 @@ func webError(w http.ResponseWriter, r *http.Request, c *Config, err error, defa
// isErrNotFound returns true for IPLD errors that should return 4xx errors (e.g. the path doesn't exist, the data is
// the wrong type, etc.), rather than issues with just finding and retrieving the data.
func isErrNotFound(err error) bool {
if errors.Is(err, &resolver.ErrNoLink{}) {
return true
}

// Checks if err is of a type that does not implement the .Is interface and
// cannot be directly compared to. Therefore, errors.Is cannot be used.
for {
_, ok := err.(resolver.ErrNoLink)
if ok {
return true
}

_, ok = err.(datamodel.ErrWrongKind)
_, ok := err.(datamodel.ErrWrongKind)
if ok {
return true
}
Expand Down
2 changes: 1 addition & 1 deletion gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ func TestErrorBubblingFromBackend(t *testing.T) {
}

testError("500 Not Found from IPLD", &ipld.ErrNotFound{}, http.StatusInternalServerError)
testError("404 Not Found from path resolver", resolver.ErrNoLink{}, http.StatusNotFound)
testError("404 Not Found from path resolver", &resolver.ErrNoLink{}, http.StatusNotFound)
testError("502 Bad Gateway", ErrBadGateway, http.StatusBadGateway)
testError("504 Gateway Timeout", ErrGatewayTimeout, http.StatusGatewayTimeout)

Expand Down
176 changes: 55 additions & 121 deletions path/resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package resolver

import (
"context"
"errors"
"fmt"
"time"

Expand All @@ -15,7 +14,6 @@ import (
fetcherhelpers "github.com/ipfs/boxo/fetcher/helpers"
"github.com/ipfs/boxo/path"
cid "github.com/ipfs/go-cid"
format "github.com/ipfs/go-ipld-format"
logging "github.com/ipfs/go-log/v2"
"github.com/ipld/go-ipld-prime"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
Expand All @@ -24,76 +22,73 @@ import (
"github.com/ipld/go-ipld-prime/traversal/selector/builder"
)

var log = logging.Logger("pathresolv")
var log = logging.Logger("path/resolver")

// ErrNoComponents is used when Paths after a protocol
// do not contain at least one component
var ErrNoComponents = errors.New(
"path must contain at least one component")

// ErrNoLink is returned when a link is not found in a path
// ErrNoLink is returned when a link is not found in a path.
type ErrNoLink struct {
Name string
Node cid.Cid
}

// Error implements the Error interface for ErrNoLink with a useful
// human readable message.
func (e ErrNoLink) Error() string {
// Error implements the [errors.Error] interface.
func (e *ErrNoLink) Error() string {
return fmt.Sprintf("no link named %q under %s", e.Name, e.Node.String())
}

// Is implements [errors.Is] interface.
func (e *ErrNoLink) Is(err error) bool {
switch err.(type) {
case *ErrNoLink:
return true
default:
return false
}
}

// Resolver provides path resolution to IPFS.
type Resolver interface {
// ResolveToLastNode walks the given path and returns the cid of the
// last block referenced by the path, and the path segments to
// traverse from the final block boundary to the final node within the
// block.
ResolveToLastNode(ctx context.Context, fpath path.Path) (cid.Cid, []string, error)
// ResolvePath fetches the node for given path. It returns the last
// item returned by ResolvePathComponents and the last link traversed
// which can be used to recover the block.
ResolvePath(ctx context.Context, fpath path.Path) (ipld.Node, ipld.Link, error)
// ResolvePathComponents fetches the nodes for each segment of the given path.
// It uses the first path component as a hash (key) of the first node, then
// resolves all other components walking the links via a selector traversal
ResolvePathComponents(ctx context.Context, fpath path.Path) ([]ipld.Node, error)
// ResolveToLastNode walks the given path and returns the CID of the last block
// referenced by the path, as well as the remainder of the path segments to traverse
// from the final block boundary to the final node within the block.
ResolveToLastNode(context.Context, path.ImmutablePath) (cid.Cid, []string, error)

// ResolvePath fetches the node for the given path. It returns the last item returned
// by [Resolver.ResolvePathComponents] and the last link traversed which can be used
// to recover the block.
ResolvePath(context.Context, path.ImmutablePath) (ipld.Node, ipld.Link, error)

// ResolvePathComponents fetches the nodes for each segment of the given path. It
// uses the first path component as the CID of the first node, then resolves all
// other components walking the links via a selector traversal.
ResolvePathComponents(context.Context, path.ImmutablePath) ([]ipld.Node, error)
}

// basicResolver implements the Resolver interface.
// It references a FetcherFactory, which is uses to resolve nodes.
// TODO: now that this is more modular, try to unify this code with the
//
// the resolvers in namesys.
// basicResolver implements the [Resolver] interface. It requires a [fetcher.Factory],
// which is used to resolve the nodes.
type basicResolver struct {
FetcherFactory fetcher.Factory
}

// NewBasicResolver constructs a new basic resolver.
func NewBasicResolver(fetcherFactory fetcher.Factory) Resolver {
// NewBasicResolver constructs a new basic resolver using the given [fetcher.Factory].
func NewBasicResolver(factory fetcher.Factory) Resolver {
return &basicResolver{
FetcherFactory: fetcherFactory,
FetcherFactory: factory,
}
}

// ResolveToLastNode walks the given path and returns the cid of the last
// block referenced by the path, and the path segments to traverse from the
// final block boundary to the final node within the block.
func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path) (cid.Cid, []string, error) {
// ResolveToLastNode implements [Resolver.ResolveToLastNode].
func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.ImmutablePath) (cid.Cid, []string, error) {
ctx, span := startSpan(ctx, "basicResolver.ResolveToLastNode", trace.WithAttributes(attribute.Stringer("Path", fpath)))
defer span.End()

c, p, err := splitImmutablePath(fpath)
if err != nil {
return cid.Cid{}, nil, err
}
c, remainder := fpath.Cid(), fpath.Segments()[2:]

if len(p) == 0 {
if len(remainder) == 0 {
return c, nil, nil
}

// create a selector to traverse and match all path segments
pathSelector := pathAllSelector(p[:len(p)-1])
pathSelector := pathAllSelector(remainder[:len(remainder)-1])

// create a new cancellable session
ctx, cancel := context.WithTimeout(ctx, time.Minute)
Expand All @@ -107,27 +102,27 @@ func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path)

if len(nodes) < 1 {
return cid.Cid{}, nil, fmt.Errorf("path %v did not resolve to a node", fpath)
} else if len(nodes) < len(p) {
return cid.Undef, nil, ErrNoLink{Name: p[len(nodes)-1], Node: lastCid}
} else if len(nodes) < len(remainder) {
return cid.Undef, nil, &ErrNoLink{Name: remainder[len(nodes)-1], Node: lastCid}
}

parent := nodes[len(nodes)-1]
lastSegment := p[len(p)-1]
lastSegment := remainder[len(remainder)-1]

// find final path segment within node
nd, err := parent.LookupBySegment(ipld.ParsePathSegment(lastSegment))
switch err.(type) {
case nil:
case schema.ErrNoSuchField:
return cid.Undef, nil, ErrNoLink{Name: lastSegment, Node: lastCid}
return cid.Undef, nil, &ErrNoLink{Name: lastSegment, Node: lastCid}
default:
return cid.Cid{}, nil, err
}

// if last node is not a link, just return it's cid, add path to remainder and return
if nd.Kind() != ipld.Kind_Link {
// return the cid and the remainder of the path
return lastCid, p[len(p)-depth-1:], nil
return lastCid, remainder[len(remainder)-depth-1:], nil
}

lnk, err := nd.AsLink()
Expand All @@ -143,22 +138,18 @@ func (r *basicResolver) ResolveToLastNode(ctx context.Context, fpath path.Path)
return clnk.Cid, []string{}, nil
}

// ResolvePath fetches the node for given path. It returns the last item
// returned by ResolvePathComponents and the last link traversed which can be used to recover the block.
// ResolvePath implements [Resolver.ResolvePath].
//
// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be
// possible to load certain values.
func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.Path) (ipld.Node, ipld.Link, error) {
// Note: if/when the context is cancelled or expires then if a multi-block ADL
// node is returned then it may not be possible to load certain values.
func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.ImmutablePath) (ipld.Node, ipld.Link, error) {
ctx, span := startSpan(ctx, "basicResolver.ResolvePath", trace.WithAttributes(attribute.Stringer("Path", fpath)))
defer span.End()

c, p, err := splitImmutablePath(fpath)
if err != nil {
return nil, nil, err
}
c, remainder := fpath.Cid(), fpath.Segments()[2:]

// create a selector to traverse all path segments but only match the last
pathSelector := pathLeafSelector(p)
pathSelector := pathLeafSelector(remainder)

nodes, c, _, err := r.resolveNodes(ctx, c, pathSelector)
if err != nil {
Expand All @@ -170,72 +161,25 @@ func (r *basicResolver) ResolvePath(ctx context.Context, fpath path.Path) (ipld.
return nodes[len(nodes)-1], cidlink.Link{Cid: c}, nil
}

// ResolveSingle simply resolves one hop of a path through a graph with no
// extra context (does not opaquely resolve through sharded nodes)
// Deprecated: fetch node as ipld-prime or convert it and then use a selector to traverse through it.
func ResolveSingle(ctx context.Context, ds format.NodeGetter, nd format.Node, names []string) (*format.Link, []string, error) {
_, span := startSpan(ctx, "ResolveSingle", trace.WithAttributes(attribute.Stringer("CID", nd.Cid())))
defer span.End()
return nd.ResolveLink(names)
}

// ResolvePathComponents fetches the nodes for each segment of the given path.
// It uses the first path component as a hash (key) of the first node, then
// resolves all other components walking the links via a selector traversal
// ResolvePathComponents implements [Resolver.ResolvePathComponents].
//
// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be
// possible to load certain values.
func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.Path) (nodes []ipld.Node, err error) {
// Note: if/when the context is cancelled or expires then if a multi-block ADL
// node is returned then it may not be possible to load certain values.
func (r *basicResolver) ResolvePathComponents(ctx context.Context, fpath path.ImmutablePath) (nodes []ipld.Node, err error) {
ctx, span := startSpan(ctx, "basicResolver.ResolvePathComponents", trace.WithAttributes(attribute.Stringer("Path", fpath)))
defer span.End()

defer log.Debugw("resolvePathComponents", "fpath", fpath, "error", err)

c, p, err := splitImmutablePath(fpath)
if err != nil {
return nil, err
}
c, remainder := fpath.Cid(), fpath.Segments()[2:]

// create a selector to traverse and match all path segments
pathSelector := pathAllSelector(p)
pathSelector := pathAllSelector(remainder)

nodes, _, _, err = r.resolveNodes(ctx, c, pathSelector)
return nodes, err
}

// ResolveLinks iteratively resolves names by walking the link hierarchy.
// Every node is fetched from the Fetcher, resolving the next name.
// Returns the list of nodes forming the path, starting with ndd. This list is
// guaranteed never to be empty.
//
// ResolveLinks(nd, []string{"foo", "bar", "baz"})
// would retrieve "baz" in ("bar" in ("foo" in nd.Links).Links).Links
//
// Note: if/when the context is cancelled or expires then if a multi-block ADL node is returned then it may not be
// possible to load certain values.
func (r *basicResolver) ResolveLinks(ctx context.Context, ndd ipld.Node, names []string) (nodes []ipld.Node, err error) {
ctx, span := startSpan(ctx, "basicResolver.ResolveLinks")
defer span.End()

defer log.Debugw("resolvePathComponents", "names", names, "error", err)
// create a selector to traverse and match all path segments
pathSelector := pathAllSelector(names)

session := r.FetcherFactory.NewSession(ctx)

// traverse selector
nodes = []ipld.Node{ndd}
err = session.NodeMatching(ctx, ndd, pathSelector, func(res fetcher.FetchResult) error {
nodes = append(nodes, res.Node)
return nil
})
if err != nil {
return nil, err
}

return nodes, err
}

// Finds nodes matching the selector starting with a cid. Returns the matched nodes, the cid of the block containing
// the last node, and the depth of the last node within its block (root is depth 0).
func (r *basicResolver) resolveNodes(ctx context.Context, c cid.Cid, sel ipld.Node) ([]ipld.Node, cid.Cid, int, error) {
Expand Down Expand Up @@ -302,13 +246,3 @@ func pathSelector(path []string, ssb builder.SelectorSpecBuilder, reduce func(st
func startSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return otel.Tracer("boxo/path/resolver").Start(ctx, fmt.Sprintf("Path.%s", name), opts...)
}

// splitImmutablePath cleans up and splits the given path.
func splitImmutablePath(p path.Path) (cid.Cid, []string, error) {
imPath, err := path.NewImmutablePath(p)
if err != nil {
return cid.Undef, nil, err
}

return imPath.Cid(), imPath.Segments()[2:], nil
}
Loading

0 comments on commit dc30a58

Please sign in to comment.