diff --git a/benchmarks/testnet/virtual.go b/benchmarks/testnet/virtual.go index 024f4dc9..4e8dce95 100644 --- a/benchmarks/testnet/virtual.go +++ b/benchmarks/testnet/virtual.go @@ -16,6 +16,7 @@ import ( "google.golang.org/protobuf/proto" gsmsg "github.com/ipfs/go-graphsync/message" + gsmsgv1 "github.com/ipfs/go-graphsync/message/v1" gsnet "github.com/ipfs/go-graphsync/network" ) @@ -137,7 +138,7 @@ func (n *network) SendMessage( rateLimiters[to] = rateLimiter } - pbMsg, err := mes.ToProto() + pbMsg, err := gsmsgv1.NewMessageHandler().ToProto(peer.ID("foo"), mes) if err != nil { return err } diff --git a/cidset/cidset.go b/cidset/cidset.go index 449a1606..c9f1f49c 100644 --- a/cidset/cidset.go +++ b/cidset/cidset.go @@ -4,32 +4,30 @@ import ( "errors" "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/fluent" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" - - "github.com/ipfs/go-graphsync/ipldutil" ) // EncodeCidSet encodes a cid set into bytes for the do-no-send-cids extension -func EncodeCidSet(cids *cid.Set) ([]byte, error) { +func EncodeCidSet(cids *cid.Set) datamodel.Node { list := fluent.MustBuildList(basicnode.Prototype.List, int64(cids.Len()), func(la fluent.ListAssembler) { _ = cids.ForEach(func(c cid.Cid) error { la.AssembleValue().AssignLink(cidlink.Link{Cid: c}) return nil }) }) - return ipldutil.EncodeNode(list) + return list } // DecodeCidSet decode a cid set from data for the do-no-send-cids extension -func DecodeCidSet(data []byte) (*cid.Set, error) { - list, err := ipldutil.DecodeNode(data) - if err != nil { - return nil, err +func DecodeCidSet(data datamodel.Node) (*cid.Set, error) { + if data.Kind() != datamodel.Kind_List { + return nil, errors.New("did not receive a list of CIDs") } set := cid.NewSet() - iter := list.ListIterator() + iter := data.ListIterator() for !iter.Done() { _, next, err := iter.Next() if err != nil { diff --git a/cidset/cidset_test.go b/cidset/cidset_test.go index abb1f0d4..741a0b7c 100644 --- a/cidset/cidset_test.go +++ b/cidset/cidset_test.go @@ -15,8 +15,7 @@ func TestDecodeEncodeCidSet(t *testing.T) { for _, c := range cids { set.Add(c) } - encoded, err := EncodeCidSet(set) - require.NoError(t, err, "encode errored") + encoded := EncodeCidSet(set) decodedCidSet, err := DecodeCidSet(encoded) require.NoError(t, err, "decode errored") require.Equal(t, decodedCidSet.Len(), set.Len()) diff --git a/dedupkey/dedupkey.go b/dedupkey/dedupkey.go index 0fd2b4fe..5e7cccfd 100644 --- a/dedupkey/dedupkey.go +++ b/dedupkey/dedupkey.go @@ -1,27 +1,21 @@ package dedupkey import ( + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" - - "github.com/ipfs/go-graphsync/ipldutil" ) // EncodeDedupKey returns encoded cbor data for string key -func EncodeDedupKey(key string) ([]byte, error) { +func EncodeDedupKey(key string) (datamodel.Node, error) { nb := basicnode.Prototype.String.NewBuilder() err := nb.AssignString(key) if err != nil { return nil, err } - nd := nb.Build() - return ipldutil.EncodeNode(nd) + return nb.Build(), nil } // DecodeDedupKey returns a string key decoded from cbor data -func DecodeDedupKey(data []byte) (string, error) { - nd, err := ipldutil.DecodeNode(data) - if err != nil { - return "", err - } - return nd.AsString() +func DecodeDedupKey(data datamodel.Node) (string, error) { + return data.AsString() } diff --git a/docs/async-loading.png b/docs/async-loading.png deleted file mode 100644 index 4b856ce2..00000000 Binary files a/docs/async-loading.png and /dev/null differ diff --git a/docs/async-loading.puml b/docs/async-loading.puml deleted file mode 100644 index 5517eaf8..00000000 --- a/docs/async-loading.puml +++ /dev/null @@ -1,75 +0,0 @@ -@startuml async loading -participant IPLD -participant "Intercepted Loader" as ILoader -participant RequestManager -participant AsyncLoader -participant LoadAttemptQueue -participant ResponseCache -participant Loader -participant Storer -IPLD -> ILoader: Load Link -activate ILoader -ILoader -> AsyncLoader: Load Link Asynchronously -activate AsyncLoader -ILoader <-- AsyncLoader: Channel For future response -... transfer to internal process ... -AsyncLoader -> LoadAttemptQueue: Try Loading This Link -deactivate AsyncLoader - -LoadAttemptQueue -> ResponseCache: Try Loading This Link -alt response cache has block -ResponseCache -> Storer: Store the block for later -ResponseCache -> ResponseCache: Remove the block from cache -LoadAttemptQueue <-- ResponseCache: "Here's the block" -note over LoadAttemptQueue: Response = Block -else response cache told block is missing by remote peer -LoadAttemptQueue <-- ResponseCache: "We're missing this link" -note over LoadAttemptQueue: Response = Error Missing Link -else local store has block -LoadAttemptQueue <-- ResponseCache: "I Don't Have it!" -LoadAttemptQueue -> Loader: Try Loading This Link From Local Cache -LoadAttemptQueue <-- Loader: "Here's the block" -note over LoadAttemptQueue: Response = Block -else no block or known missing link yet -LoadAttemptQueue <-- ResponseCache: "I Don't Have it!" -LoadAttemptQueue -> Loader: Try Loading This Link From Local Cache -LoadAttemptQueue <-- Loader: "I Don't Have it!" -LoadAttemptQueue -> LoadAttemptQueue: Store load request to try later -end -loop 0 or more times till I have a response for the link -... -opt new responses comes in -RequestManager -> AsyncLoader: New Responses Present -activate AsyncLoader -AsyncLoader -> ResponseCache: New Responses Present -... transfer to internal process ... -AsyncLoader -> LoadAttemptQueue: Try Loading Again -deactivate AsyncLoader -LoadAttemptQueue -> ResponseCache: Try Loading Stored Links -alt response cache now has block -ResponseCache -> Storer: Store the block for later -ResponseCache -> ResponseCache: Remove the block from cache -LoadAttemptQueue <-- ResponseCache: "Here's the block" -note over LoadAttemptQueue: Response = Block -else response cache now knows link is missing by remote peer -LoadAttemptQueue <-- ResponseCache: "We're missing this link" -note over LoadAttemptQueue: Response = Error Missing Link -else still no response -LoadAttemptQueue <-- ResponseCache: "I don't have it" -LoadAttemptQueue -> LoadAttemptQueue: Store load request to try later -end -end -opt no more responses -RequestManager -> AsyncLoader: No more responses -activate AsyncLoader -... transfer to internal process ... -AsyncLoader -> LoadAttemptQueue: Cancel attempts to load -note over LoadAttemptQueue: Response = Error Request Finished -deactivate AsyncLoader -end -end -ILoader <-- LoadAttemptQueue: Response Sent Over Channel -IPLD <-- ILoader : "Here's the stream of block\n data or an error" -deactivate ILoader - -@enduml \ No newline at end of file diff --git a/docs/processes.png b/docs/processes.png index 964dffd3..ddd3289c 100644 Binary files a/docs/processes.png and b/docs/processes.png differ diff --git a/docs/processes.puml b/docs/processes.puml index 953a80cd..c44775c7 100644 --- a/docs/processes.puml +++ b/docs/processes.puml @@ -12,16 +12,17 @@ partition "Top Level Interface" { if (operation type) then (outgoing request or incoming response) partition "Graphsync Requestor Implementation" { :RequestManager; -if (operation type) then (incoming response) -partition "Verifying Queries" { +partition "Executing Requests" { +:TaskQueue; fork -:ipld.Traverse; +:Executor; fork again -:ipld.Traverse; +:Executor; fork again -:ipld.Traverse; +:Executor; end fork } +if (operation type) then (verified responses) partition "Collecting Responses" { fork :Response Collector; @@ -33,7 +34,7 @@ end fork } :Responses returned to client; stop -else (outgoing request) +else (request messages) :Send Request To Network; endif } @@ -41,13 +42,13 @@ else (incoming request) partition "Graphsync Responder Implementation" { :ResponseManager; partition "Performing Queries" { -:PeerTaskQueue; +:TaskQueue; fork -:ipld.Traverse; +:QueryExecutor; fork again -:ipld.Traverse; +:QueryExecutor; fork again -:ipld.Traverse; +:QueryExecutor; end fork } } diff --git a/docs/request-execution.png b/docs/request-execution.png new file mode 100644 index 00000000..8487c510 Binary files /dev/null and b/docs/request-execution.png differ diff --git a/docs/request-execution.puml b/docs/request-execution.puml new file mode 100644 index 00000000..b715436a --- /dev/null +++ b/docs/request-execution.puml @@ -0,0 +1,102 @@ +@startuml Request Execution +participant "GraphSync\nTop Level\nInterface" as TLI +participant RequestManager +participant TaskQueue +participant RequestExecutor as RE +participant ReconciledLoader +participant TraversalRecord +participant Verifier +participant LocalStorage +participant Traverser +participant Network + +== Initialization == + +TLI -> RequestManager ** : Setup +TLI -> RE ** : Setup +TLI -> TaskQueue ** : Setup + +== Executing A Request == + +par +note over TLI : Request Initiation +TLI -> RequestManager : New Request +RequestManager -> RequestManager : Create Request Context +RequestManager -> TaskQueue : Push Request +else +note over RE: Request Execution +TaskQueue -> RE : Next Request\nTo Process +RE -> RequestManager : Initiate request execution +RequestManager -> Traverser ** : Create to manage selector traversal +RequestManager -> ReconciledLoader ** : create to manage +RequestManager -> RE : Traverser + ReconciledLoader +note over RE: Local loading phase +loop until traversal complete, request context cancelled, or missing block locally +Traverser -> RE : Request to load blocks\nto perform traversal +RE -> ReconciledLoader : Load next block +ReconciledLoader -> LocalStorage : Load Block +LocalStorage --> ReconciledLoader : Block or missing +ReconciledLoader -> TraversalRecord : Record link traversal +TraversalRecord --> ReconciledLoader +ReconciledLoader --> RE : Block or missing +opt block is present +RE --> Traverser : Next block to load +end +end +RE -> Network : Send Graphsync Request +RE -> ReconciledLoader : remote online +ReconciledLoader -> Verifier ** : Create new from traversal record +ReconciledLoader -> RE +note over RE: Remote loading phase +loop until traversal complete, request context cancelled, or missing block locally +Traverser -> RE : Request to load blocks\nto perform traversal +RE -> ReconciledLoader : Load next block +alt on missing path for remote +ReconciledLoader -> LocalStorage : Load Block +LocalStorage --> ReconciledLoader : Block or missing +else +loop until block loaded, missing, or error +opt new remote responses + alt verification not done + ReconciledLoader -> Verifier : verify next response + alt success + Verifier --> ReconciledLoader : verified + ReconciledLoader -> ReconciledLoader : wait for more responses + else failure + Verifier --> ReconciledLoader : error + end + else verification done + alt next response matches current block load + + alt next response contains a block + ReconciledLoader -> LocalStorage : store remote block + LocalStorage --> ReconciledLoader + ReconciledLoader -> ReconciledLoader : block laoded from remote + else next response does not contain block + opt next response is missing + ReconciledLoader -> ReconciledLoader : record missing path + end + ReconciledLoader -> LocalStorage : load block + LocalStorage --> ReconciledLoader : block or missing + end + else next response doesn not match + ReconciledLoader -> ReconciledLoader : error + end + end +end +opt remote goes offline +ReconciledLoader -> LocalStorage : load block +LocalStorage --> ReconciledLoader : block or missing +end +end +ReconciledLoader -> TraversalRecord : Record link traversal +TraversalRecord --> ReconciledLoader +ReconciledLoader --> RE : Block, missing or error +RE -> Traverser : Next block to load +end +end +else +Network -> RequestManager : New Responses +RequestManager -> ReconciledLoader : Ingest Responses +end +@enduml \ No newline at end of file diff --git a/docs/responder-sequence.puml b/docs/responder-sequence.puml index e9885d4d..65ded652 100644 --- a/docs/responder-sequence.puml +++ b/docs/responder-sequence.puml @@ -1,24 +1,20 @@ @startuml Responding To A Request participant "GraphSync\nTop Level\nInterface" as TLI participant ResponseManager -participant "Query Executor" as QW -participant PeerTaskQueue +participant "QueryExecutor" as QW +participant TaskQueue participant PeerTracker participant Traverser participant ResponseAssembler participant LinkTracker -participant ResponseBuilder -participant "Intercepted Loader" as ILoader participant Loader participant "Message Sending\nLayer" as Message == Initialization == TLI -> ResponseManager ** : Setup -ResponseManager -> QW ** : Create -activate QW -TLI -> PeerTaskQueue ** : Setup -TLI -> PeerResponseManager ** : Setup +TLI -> QW ** : Setup +TLI -> TaskQueue ** : Setup == Responding To Request == @@ -27,10 +23,8 @@ loop until shutdown note over TLI : Request Queueing Loop TLI -> ResponseManager : Process requests alt new request -ResponseManager -> PeerTaskQueue : Push Request -PeerTaskQueue -> PeerTracker ** : Create for peer\n as neccesary -PeerTaskQueue -> PeerTracker : Push Request ResponseManager -> ResponseManager : Create Request Context +ResponseManager -> TaskQueue : Push Request else cancel request ResponseManager -> ResponseManager : Cancel Request Context end @@ -38,27 +32,23 @@ end else loop until shutdown note over QW: Request Processing Loop -QW -> PeerTaskQueue : Pop Request -PeerTaskQueue -> PeerTracker : Pop Request -PeerTracker -> PeerTaskQueue : Next Request\nTo Process -PeerTaskQueue -> QW : Next Request\nTo Process +TaskQueue -> QW : Next Request\nTo Process +activate QW QW -> QW : Process incoming request hooks -QW -> ILoader ** : Create w/ Request, Peer, and Loader QW -> Traverser ** : Create to manage selector traversal loop until traversal complete or request context cancelled note over Traverser: Selector Traversal Loop -Traverser -> ILoader : Request to load blocks\nto perform traversal -ILoader -> Loader : Load blocks\nfrom local storage -Loader -> ILoader : Blocks From\nlocal storage or error -ILoader -> Traverser : Blocks to continue\n traversal or error -ILoader -> QW : Block or error to Send Back +Traverser -> QW : Request to load blocks\nto perform traversal +QW -> Loader : Load blocks\nfrom local storage +Loader -> QW : Blocks From\nlocal storage or error +QW -> Traverser : Blocks to continue\n traversal or error QW -> QW: Processing outgoing block hooks QW -> ResponseAssembler: Add outgoing responses activate ResponseAssembler ResponseAssembler -> LinkTracker ** : Create for peer if not already present ResponseAssembler -> LinkTracker : Notify block or\n error, ask whether\n block is duplicate LinkTracker -> ResponseAssembler : Whether to\n send block -ResponseAssembler -> ResponseBuilder : Aggregate Response Metadata & Block +ResponseAssembler -> ResponseAssembler : Aggregate Response Metadata & Blocks ResponseAssembler -> Message : Send aggregate response deactivate ResponseAssembler end @@ -67,7 +57,7 @@ QW -> ResponseAssembler : Request Finished activate ResponseAssembler ResponseAssembler -> LinkTracker : Query If Errors\n Were Present LinkTracker -> ResponseAssembler : True/False\n if errors present -ResponseAssembler -> ResponseBuilder : Aggregate request finishing +ResponseAssembler -> ResponseAssembler : Aggregate request finishing ResponseAssembler -> Message : Send aggregate response deactivate ResponseAssembler end diff --git a/donotsendfirstblocks/donotsendfirstblocks.go b/donotsendfirstblocks/donotsendfirstblocks.go index af6e125f..83cd8a7f 100644 --- a/donotsendfirstblocks/donotsendfirstblocks.go +++ b/donotsendfirstblocks/donotsendfirstblocks.go @@ -1,23 +1,17 @@ package donotsendfirstblocks import ( + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" - - "github.com/ipfs/go-graphsync/ipldutil" ) // EncodeDoNotSendFirstBlocks returns encoded cbor data for the given number // of blocks to skip -func EncodeDoNotSendFirstBlocks(skipBlockCount int64) ([]byte, error) { - nd := basicnode.NewInt(skipBlockCount) - return ipldutil.EncodeNode(nd) +func EncodeDoNotSendFirstBlocks(skipBlockCount int64) datamodel.Node { + return basicnode.NewInt(skipBlockCount) } // DecodeDoNotSendFirstBlocks returns the number of blocks to skip -func DecodeDoNotSendFirstBlocks(data []byte) (int64, error) { - nd, err := ipldutil.DecodeNode(data) - if err != nil { - return 0, err - } - return nd.AsInt() +func DecodeDoNotSendFirstBlocks(data datamodel.Node) (int64, error) { + return data.AsInt() } diff --git a/go.mod b/go.mod index 8b7e7830..6ef70cec 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/ipfs/go-graphsync go 1.16 require ( - github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect + github.com/google/go-cmp v0.5.7 + github.com/google/uuid v1.3.0 github.com/hannahhoward/cbor-gen-for v0.0.0-20200817222906-ea96cece81f1 github.com/hannahhoward/go-pubsub v0.0.0-20200423002714-8d62886cc36e github.com/ipfs/go-block-format v0.0.3 @@ -27,7 +28,7 @@ require ( github.com/ipfs/go-unixfs v0.3.1 github.com/ipfs/go-unixfsnode v1.2.0 github.com/ipld/go-codec-dagpb v1.3.0 - github.com/ipld/go-ipld-prime v0.14.4 + github.com/ipld/go-ipld-prime v0.14.5-0.20220204050122-679d74376a0d github.com/jbenet/go-random v0.0.0-20190219211222-123a90aedc0c github.com/libp2p/go-buffer-pool v0.0.2 github.com/libp2p/go-libp2p v0.16.0 @@ -37,13 +38,10 @@ require ( github.com/libp2p/go-msgio v0.1.0 github.com/multiformats/go-multiaddr v0.4.0 github.com/multiformats/go-multihash v0.1.0 - github.com/smartystreets/assertions v1.0.0 // indirect github.com/stretchr/testify v1.7.0 - github.com/whyrusleeping/cbor-gen v0.0.0-20210219115102-f37d292932f2 go.opentelemetry.io/otel v1.2.0 go.opentelemetry.io/otel/sdk v1.2.0 go.opentelemetry.io/otel/trace v1.2.0 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/protobuf v1.27.1 ) diff --git a/go.sum b/go.sum index 7a2c37f9..b923b801 100644 --- a/go.sum +++ b/go.sum @@ -184,8 +184,9 @@ github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiD github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= -github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/frankban/quicktest v1.14.1 h1:7j+0Tuzrdj5XLLmXnI8qipQ31hf5nlUW3LPgT+O9aT8= +github.com/frankban/quicktest v1.14.1/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -262,8 +263,9 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -290,9 +292,8 @@ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= -github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -454,8 +455,9 @@ github.com/ipld/go-codec-dagpb v1.3.0/go.mod h1:ga4JTU3abYApDC3pZ00BC2RSvC3qfBb9 github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8= github.com/ipld/go-ipld-prime v0.11.0/go.mod h1:+WIAkokurHmZ/KwzDOMUuoeJgaRQktHtEaLglS3ZeV8= github.com/ipld/go-ipld-prime v0.14.0/go.mod h1:9ASQLwUFLptCov6lIYc70GRB4V7UTyLD0IJtrDJe6ZM= -github.com/ipld/go-ipld-prime v0.14.4 h1:bqhmume8+nbNsX4/+J6eohktfZHAI8GKrF3rQ0xgOyc= github.com/ipld/go-ipld-prime v0.14.4/go.mod h1:QcE4Y9n/ZZr8Ijg5bGPT0GqYWgZ1704nH0RDcQtgTP0= +github.com/ipld/go-ipld-prime v0.14.5-0.20220204050122-679d74376a0d h1:HMvFmQbipEXniV3cRdqnkrsvAlKYMjEPbvvKN3mWsDE= +github.com/ipld/go-ipld-prime v0.14.5-0.20220204050122-679d74376a0d/go.mod h1:f5ls+uUY8Slf1NN6YUOeEyYe3TA/J02Rn7zw1NQTeSk= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20211210234204-ce2a1c70cd73/go.mod h1:2PJ0JgxyB08t0b2WKrcuqI3di0V+5n6RS/LTUJhkoxY= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= @@ -1023,9 +1025,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= -github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa/go.mod h1:2RVY1rIf+2J2o/IM9+vPq9RzmHDSseB7FoXiSNIUsoU= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -1080,9 +1081,8 @@ github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvS github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= github.com/whyrusleeping/cbor-gen v0.0.0-20200123233031-1cdf64d27158/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI= +github.com/whyrusleeping/cbor-gen v0.0.0-20200710004633-5379fc63235d h1:wSxKhvbN7kUoP0sfRS+w2tWr45qlU8409i94hHLOT8w= github.com/whyrusleeping/cbor-gen v0.0.0-20200710004633-5379fc63235d/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= -github.com/whyrusleeping/cbor-gen v0.0.0-20210219115102-f37d292932f2 h1:bsUlNhdmbtlfdLVXAVfuvKQ01RnWAM09TVrJkI7NZs4= -github.com/whyrusleeping/cbor-gen v0.0.0-20210219115102-f37d292932f2/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= diff --git a/graphsync.go b/graphsync.go index 75756c88..0e0536d6 100644 --- a/graphsync.go +++ b/graphsync.go @@ -5,18 +5,45 @@ import ( "errors" "fmt" + "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/traversal" "github.com/libp2p/go-libp2p-core/peer" ) // RequestID is a unique identifier for a GraphSync request. -type RequestID int32 +type RequestID struct{ string } // Tag returns an easy way to identify this request id as a graphsync request (for libp2p connections) func (r RequestID) Tag() string { - return fmt.Sprintf("graphsync-request-%d", r) + return r.String() +} + +// String form of a RequestID (should be a well-formed UUIDv4 string) +func (r RequestID) String() string { + return uuid.Must(uuid.FromBytes([]byte(r.string))).String() +} + +// Byte form of a RequestID +func (r RequestID) Bytes() []byte { + return []byte(r.string) +} + +// Create a new, random RequestID (should be a UUIDv4) +func NewRequestID() RequestID { + u := uuid.New() + return RequestID{string(u[:])} +} + +// Create a RequestID from a byte slice +func ParseRequestID(b []byte) (RequestID, error) { + _, err := uuid.FromBytes(b) + if err != nil { + return RequestID{}, err + } + return RequestID{string(b)}, nil } // Priority a priority for a GraphSync request. @@ -28,18 +55,13 @@ type ExtensionName string // ExtensionData is a name/data pair for a graphsync extension type ExtensionData struct { Name ExtensionName - Data []byte + Data datamodel.Node } const ( // Known Graphsync Extensions - // ExtensionMetadata provides response metadata for a Graphsync request and is - // documented at - // https://github.com/ipld/specs/blob/master/block-layer/graphsync/known_extensions.md - ExtensionMetadata = ExtensionName("graphsync/response-metadata") - // ExtensionDoNotSendCIDs tells the responding peer not to send certain blocks if they // are encountered in a traversal and is documented at // https://github.com/ipld/specs/blob/master/block-layer/graphsync/known_extensions.md @@ -105,14 +127,29 @@ func (e RequestNotFoundErr) Error() string { } // RemoteMissingBlockErr indicates that the remote peer was missing a block -// in the selector requested. It is a non-terminal error in the error stream +// in the selector requested, and we also don't have it locally. +// It is a -terminal error in the error stream // for a request and does NOT cause a request to fail completely type RemoteMissingBlockErr struct { Link ipld.Link + Path ipld.Path } func (e RemoteMissingBlockErr) Error() string { - return fmt.Sprintf("remote peer is missing block: %s", e.Link.String()) + return fmt.Sprintf("remote peer is missing block (%s) at path %s", e.Link.String(), e.Path) +} + +// RemoteIncorrectResponseError indicates that the remote peer sent a response +// to a traversal that did not correspond with the expected next link +// in the selector traversal based on verification of data up to this point +type RemoteIncorrectResponseError struct { + LocalLink ipld.Link + RemoteLink ipld.Link + Path ipld.Path +} + +func (e RemoteIncorrectResponseError) Error() string { + return fmt.Sprintf("expected link (%s) at path %s does not match link sent by remote (%s), possible malicious responder", e.LocalLink, e.Path, e.RemoteLink) } var ( @@ -146,12 +183,25 @@ type RequestData interface { // Extension returns the content for an extension on a response, or errors // if extension is not present - Extension(name ExtensionName) ([]byte, bool) + Extension(name ExtensionName) (datamodel.Node, bool) // IsCancel returns true if this particular request is being cancelled - IsCancel() bool + Type() RequestType } +type RequestType string + +const ( + // RequestTypeNew means a new request + RequestTypeNew = RequestType("New") + + // RequestTypeCancel means cancel the request referenced by request ID + RequestTypeCancel = RequestType("Cancel") + + // RequestTypeUpdate means the extensions contain an update about this request + RequestTypeUpdate = RequestType("Update") +) + // ResponseData describes a received Graphsync response type ResponseData interface { // RequestID returns the request ID for this response @@ -162,7 +212,37 @@ type ResponseData interface { // Extension returns the content for an extension on a response, or errors // if extension is not present - Extension(name ExtensionName) ([]byte, bool) + Extension(name ExtensionName) (datamodel.Node, bool) + + // Metadata returns a copy of the link metadata contained in this response + Metadata() LinkMetadata +} + +// LinkAction is a code that is used by message metadata to communicate the +// state and reason for blocks being included or not in a transfer +type LinkAction string + +const ( + // LinkActionPresent means the linked block was present on this machine, and + // is included a this message + LinkActionPresent = LinkAction("Present") + + // LinkActionMissing means I did not have the linked block, so I skipped over + // this part of the traversal + LinkActionMissing = LinkAction("Missing") +) + +// LinkMetadataIterator is used to access individual link metadata through a +// LinkMetadata object +type LinkMetadataIterator func(cid.Cid, LinkAction) + +// LinkMetadata is used to access link metadata through an Iterator +type LinkMetadata interface { + // Length returns the number of metadata entries + Length() int64 + // Iterate steps over individual metadata one by one using the provided + // callback + Iterate(LinkMetadataIterator) } // BlockData gives information about a block included in a graphsync response @@ -423,25 +503,18 @@ type GraphExchange interface { // RegisterReceiverNetworkErrorListener adds a listener for when errors occur receiving data over the wire RegisterReceiverNetworkErrorListener(listener OnReceiverNetworkErrorListener) UnregisterHookFunc - // UnpauseRequest unpauses a request that was paused in a block hook based request ID - // Can also send extensions with unpause - UnpauseRequest(RequestID, ...ExtensionData) error - - // PauseRequest pauses an in progress request (may take 1 or more blocks to process) - PauseRequest(RequestID) error + // Pause pauses an in progress request or response (may take 1 or more blocks to process) + Pause(context.Context, RequestID) error - // UnpauseResponse unpauses a response that was paused in a block hook based on peer ID and request ID + // Unpause unpauses a request or response that was paused // Can also send extensions with unpause - UnpauseResponse(peer.ID, RequestID, ...ExtensionData) error - - // PauseResponse pauses an in progress response (may take 1 or more blocks to process) - PauseResponse(peer.ID, RequestID) error + Unpause(context.Context, RequestID, ...ExtensionData) error - // CancelResponse cancels an in progress response - CancelResponse(peer.ID, RequestID) error + // Cancel cancels an in progress request or response + Cancel(context.Context, RequestID) error - // CancelRequest cancels an in progress request - CancelRequest(context.Context, RequestID) error + // SendUpdate sends an update for an in progress request or response + SendUpdate(context.Context, RequestID, ...ExtensionData) error // Stats produces insight on the current state of a graphsync exchange Stats() Stats diff --git a/impl/graphsync.go b/impl/graphsync.go index 0f546b24..36ecd9ba 100644 --- a/impl/graphsync.go +++ b/impl/graphsync.go @@ -2,6 +2,7 @@ package graphsync import ( "context" + "errors" "time" logging "github.com/ipfs/go-log/v2" @@ -20,13 +21,12 @@ import ( gsnet "github.com/ipfs/go-graphsync/network" "github.com/ipfs/go-graphsync/peermanager" "github.com/ipfs/go-graphsync/peerstate" + "github.com/ipfs/go-graphsync/persistenceoptions" "github.com/ipfs/go-graphsync/requestmanager" - "github.com/ipfs/go-graphsync/requestmanager/asyncloader" "github.com/ipfs/go-graphsync/requestmanager/executor" requestorhooks "github.com/ipfs/go-graphsync/requestmanager/hooks" "github.com/ipfs/go-graphsync/responsemanager" responderhooks "github.com/ipfs/go-graphsync/responsemanager/hooks" - "github.com/ipfs/go-graphsync/responsemanager/persistenceoptions" "github.com/ipfs/go-graphsync/responsemanager/queryexecutor" "github.com/ipfs/go-graphsync/responsemanager/responseassembler" "github.com/ipfs/go-graphsync/selectorvalidator" @@ -50,7 +50,6 @@ type GraphSync struct { requestManager *requestmanager.RequestManager responseManager *responsemanager.ResponseManager queryExecutor *queryexecutor.QueryExecutor - asyncLoader *asyncloader.AsyncLoader responseQueue taskqueue.TaskQueue requestQueue taskqueue.TaskQueue requestExecutor *executor.Executor @@ -230,10 +229,9 @@ func New(parent context.Context, network gsnet.GraphSyncNetwork, } peerManager := peermanager.NewMessageManager(ctx, createMessageQueue) - asyncLoader := asyncloader.New(ctx, linkSystem) requestQueue := taskqueue.NewTaskQueue(ctx) - requestManager := requestmanager.New(ctx, asyncLoader, linkSystem, outgoingRequestHooks, incomingResponseHooks, networkErrorListeners, outgoingRequestProcessingListeners, requestQueue, network.ConnectionManager(), gsConfig.maxLinksPerOutgoingRequest) - requestExecutor := executor.NewExecutor(requestManager, incomingBlockHooks, asyncLoader.AsyncLoad) + requestManager := requestmanager.New(ctx, persistenceOptions, linkSystem, outgoingRequestHooks, incomingResponseHooks, networkErrorListeners, outgoingRequestProcessingListeners, requestQueue, network.ConnectionManager(), gsConfig.maxLinksPerOutgoingRequest) + requestExecutor := executor.NewExecutor(requestManager, incomingBlockHooks) responseAssembler := responseassembler.New(ctx, peerManager) var ptqopts []peertaskqueue.Option if gsConfig.maxInProgressIncomingRequestsPerPeer > 0 { @@ -266,7 +264,6 @@ func New(parent context.Context, network gsnet.GraphSyncNetwork, requestManager: requestManager, responseManager: responseManager, queryExecutor: queryExecutor, - asyncLoader: asyncLoader, responseQueue: responseQueue, requestQueue: requestQueue, requestExecutor: requestExecutor, @@ -296,6 +293,7 @@ func New(parent context.Context, network gsnet.GraphSyncNetwork, responseManager.Startup() responseQueue.Startup(gsConfig.maxInProgressIncomingRequests, queryExecutor) network.SetDelegate((*graphSyncReceiver)(graphSync)) + return graphSync } @@ -339,19 +337,11 @@ func (gs *GraphSync) RegisterOutgoingRequestHook(hook graphsync.OnOutgoingReques // RegisterPersistenceOption registers an alternate loader/storer combo that can be substituted for the default func (gs *GraphSync) RegisterPersistenceOption(name string, lsys ipld.LinkSystem) error { - err := gs.asyncLoader.RegisterPersistenceOption(name, lsys) - if err != nil { - return err - } return gs.persistenceOptions.Register(name, lsys) } // UnregisterPersistenceOption unregisters an alternate loader/storer combo func (gs *GraphSync) UnregisterPersistenceOption(name string) error { - err := gs.asyncLoader.UnregisterPersistenceOption(name) - if err != nil { - return err - } return gs.persistenceOptions.Unregister(name) } @@ -402,35 +392,42 @@ func (gs *GraphSync) RegisterReceiverNetworkErrorListener(listener graphsync.OnR return gs.receiverErrorListeners.Register(listener) } -// UnpauseRequest unpauses a request that was paused in a block hook based request ID -// Can also send extensions with unpause -func (gs *GraphSync) UnpauseRequest(requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { - return gs.requestManager.UnpauseRequest(requestID, extensions...) -} - -// PauseRequest pauses an in progress request (may take 1 or more blocks to process) -func (gs *GraphSync) PauseRequest(requestID graphsync.RequestID) error { - return gs.requestManager.PauseRequest(requestID) -} - -// UnpauseResponse unpauses a response that was paused in a block hook based on peer ID and request ID -func (gs *GraphSync) UnpauseResponse(p peer.ID, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { - return gs.responseManager.UnpauseResponse(p, requestID, extensions...) +// Pause pauses an in progress request or response +func (gs *GraphSync) Pause(ctx context.Context, requestID graphsync.RequestID) error { + var reqNotFound graphsync.RequestNotFoundErr + if err := gs.requestManager.PauseRequest(ctx, requestID); !errors.As(err, &reqNotFound) { + return err + } + return gs.responseManager.PauseResponse(ctx, requestID) } -// PauseResponse pauses an in progress response (may take 1 or more blocks to process) -func (gs *GraphSync) PauseResponse(p peer.ID, requestID graphsync.RequestID) error { - return gs.responseManager.PauseResponse(p, requestID) +// Unpause unpauses a request or response that was paused +// Can also send extensions with unpause +func (gs *GraphSync) Unpause(ctx context.Context, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { + var reqNotFound graphsync.RequestNotFoundErr + if err := gs.requestManager.UnpauseRequest(ctx, requestID, extensions...); !errors.As(err, &reqNotFound) { + return err + } + return gs.responseManager.UnpauseResponse(ctx, requestID, extensions...) } -// CancelResponse cancels an in progress response -func (gs *GraphSync) CancelResponse(p peer.ID, requestID graphsync.RequestID) error { - return gs.responseManager.CancelResponse(p, requestID) +// Cancel cancels an in progress request or response +func (gs *GraphSync) Cancel(ctx context.Context, requestID graphsync.RequestID) error { + var reqNotFound graphsync.RequestNotFoundErr + if err := gs.requestManager.CancelRequest(ctx, requestID); !errors.As(err, &reqNotFound) { + return err + } + return gs.responseManager.CancelResponse(ctx, requestID) } -// CancelRequest cancels an in progress request -func (gs *GraphSync) CancelRequest(ctx context.Context, requestID graphsync.RequestID) error { - return gs.requestManager.CancelRequest(ctx, requestID) +// SendUpdate sends an update for an in progress request or response +func (gs *GraphSync) SendUpdate(ctx context.Context, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { + // TODO: error if len(extensions)==0? + var reqNotFound graphsync.RequestNotFoundErr + if err := gs.requestManager.UpdateRequest(ctx, requestID, extensions...); !errors.As(err, &reqNotFound) { + return err + } + return gs.responseManager.UpdateResponse(ctx, requestID, extensions...) } // Stats produces insight on the current state of a graphsync exchange diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 83b11140..66d0cad7 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -7,14 +7,11 @@ import ( "fmt" "io" "io/ioutil" - "math" - "math/rand" "os" "path/filepath" "testing" "time" - blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-blockservice" "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" @@ -31,12 +28,14 @@ import ( "github.com/ipfs/go-unixfsnode" unixfsbuilder "github.com/ipfs/go-unixfsnode/data/builder" ipld "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/traversal/selector" "github.com/ipld/go-ipld-prime/traversal/selector/builder" "github.com/libp2p/go-libp2p-core/host" "github.com/libp2p/go-libp2p-core/peer" + "github.com/libp2p/go-libp2p-core/protocol" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" @@ -44,7 +43,6 @@ import ( "github.com/ipfs/go-graphsync/cidset" "github.com/ipfs/go-graphsync/donotsendfirstblocks" "github.com/ipfs/go-graphsync/ipldutil" - gsmsg "github.com/ipfs/go-graphsync/message" gsnet "github.com/ipfs/go-graphsync/network" "github.com/ipfs/go-graphsync/requestmanager/hooks" "github.com/ipfs/go-graphsync/storeutil" @@ -52,128 +50,20 @@ import ( "github.com/ipfs/go-graphsync/testutil" ) -func TestMakeRequestToNetwork(t *testing.T) { - - // create network - ctx := context.Background() - ctx, collectTracing := testutil.SetupTracing(ctx) - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - td := newGsTestData(ctx, t) - r := &receiver{ - messageReceived: make(chan receivedMessage), - } - td.gsnet2.SetDelegate(r) - graphSync := td.GraphSyncHost1() - - blockChainLength := 100 - blockChain := testutil.SetupBlockChain(ctx, t, td.persistence1, 100, blockChainLength) - - requestCtx, requestCancel := context.WithCancel(ctx) - defer requestCancel() - graphSync.Request(requestCtx, td.host2.ID(), blockChain.TipLink, blockChain.Selector(), td.extension) - - var message receivedMessage - testutil.AssertReceive(ctx, t, r.messageReceived, &message, "did not receive message sent") - - sender := message.sender - require.Equal(t, td.host1.ID(), sender, "received message from wrong node") - - received := message.message - receivedRequests := received.Requests() - require.Len(t, receivedRequests, 1, "Did not add request to received message") - receivedRequest := receivedRequests[0] - receivedSpec := receivedRequest.Selector() - require.Equal(t, blockChain.Selector(), receivedSpec, "did not transmit selector spec correctly") - _, err := selector.ParseSelector(receivedSpec) - require.NoError(t, err, "did not receive parsible selector on other side") - - returnedData, found := receivedRequest.Extension(td.extensionName) - require.True(t, found) - require.Equal(t, td.extensionData, returnedData, "Failed to encode extension") - - drain(graphSync) - - tracing := collectTracing(t) - require.ElementsMatch(t, []string{ - "request(0)->newRequest(0)", - "request(0)->executeTask(0)", - "request(0)->terminateRequest(0)", - "message(0)->sendMessage(0)", - }, tracing.TracesToStrings()) - - // make sure the attributes are what we expect - requestSpans := tracing.FindSpans("request") - require.Equal(t, td.host2.ID().Pretty(), testutil.AttributeValueInTraceSpan(t, requestSpans[0], "peerID").AsString()) - require.Equal(t, blockChain.TipLink.String(), testutil.AttributeValueInTraceSpan(t, requestSpans[0], "root").AsString()) - require.Equal(t, []string{string(td.extensionName)}, testutil.AttributeValueInTraceSpan(t, requestSpans[0], "extensions").AsStringSlice()) - require.Equal(t, int64(0), testutil.AttributeValueInTraceSpan(t, requestSpans[0], "requestID").AsInt64()) -} - -func TestSendResponseToIncomingRequest(t *testing.T) { - // create network - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - td := newGsTestData(ctx, t) - r := &receiver{ - messageReceived: make(chan receivedMessage), - } - td.gsnet1.SetDelegate(r) - - var receivedRequestData []byte - // initialize graphsync on second node to response to requests - gsnet := td.GraphSyncHost2() - gsnet.RegisterIncomingRequestHook( - func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) { - var has bool - receivedRequestData, has = requestData.Extension(td.extensionName) - require.True(t, has, "did not have expected extension") - hookActions.SendExtensionData(td.extensionResponse) - }, - ) - - blockChainLength := 100 - blockChain := testutil.SetupBlockChain(ctx, t, td.persistence2, 100, blockChainLength) - - requestID := graphsync.RequestID(rand.Int31()) - - builder := gsmsg.NewBuilder() - builder.AddRequest(gsmsg.NewRequest(requestID, blockChain.TipLink.(cidlink.Link).Cid, blockChain.Selector(), graphsync.Priority(math.MaxInt32), td.extension)) - message, err := builder.Build() - require.NoError(t, err) - // send request across network - err = td.gsnet1.SendMessage(ctx, td.host2.ID(), message) - require.NoError(t, err) - // read the values sent back to requestor - var received gsmsg.GraphSyncMessage - var receivedBlocks []blocks.Block - var receivedExtensions [][]byte - for { - var message receivedMessage - testutil.AssertReceive(ctx, t, r.messageReceived, &message, "did not receive complete response") - - sender := message.sender - require.Equal(t, td.host2.ID(), sender, "received message from wrong node") - - received = message.message - receivedBlocks = append(receivedBlocks, received.Blocks()...) - receivedResponses := received.Responses() - receivedExtension, found := receivedResponses[0].Extension(td.extensionName) - if found { - receivedExtensions = append(receivedExtensions, receivedExtension) - } - require.Len(t, receivedResponses, 1, "Did not receive response") - require.Equal(t, requestID, receivedResponses[0].RequestID(), "Sent response for incorrect request id") - if receivedResponses[0].Status() != graphsync.PartialResponse { - break - } - } - - require.Len(t, receivedBlocks, blockChainLength, "Send incorrect number of blocks or there were duplicate blocks") - require.Equal(t, td.extensionData, receivedRequestData, "did not receive correct request extension data") - require.Len(t, receivedExtensions, 1, "should have sent extension responses but didn't") - require.Equal(t, td.extensionResponseData, receivedExtensions[0], "did not return correct extension data") +// nil means use the default protocols +// tests data transfer for the following protocol combinations: +// default protocol -> default protocols +// old protocol -> default protocols +// default protocols -> old protocol +// old protocol -> old protocol +var protocolsForTest = map[string]struct { + host1Protocols []protocol.ID + host2Protocols []protocol.ID +}{ + "(v2.0 -> v2.0)": {nil, nil}, + "(v1.0 -> v2.0)": {[]protocol.ID{gsnet.ProtocolGraphsync_1_0_0}, nil}, + "(v2.0 -> v1.0)": {nil, []protocol.ID{gsnet.ProtocolGraphsync_1_0_0}}, + "(v1.0 -> v1.0)": {[]protocol.ID{gsnet.ProtocolGraphsync_1_0_0}, []protocol.ID{gsnet.ProtocolGraphsync_1_0_0}}, } func TestRejectRequestsByDefault(t *testing.T) { @@ -207,7 +97,7 @@ func TestRejectRequestsByDefault(t *testing.T) { "request(0)->newRequest(0)", "request(0)->executeTask(0)", "request(0)->terminateRequest(0)", - "processResponses(0)->loaderProcess(0)->cacheProcess(0)", + "processResponses(0)", "processRequests(0)->transaction(0)->execute(0)->buildMessage(0)", "message(0)->sendMessage(0)", "message(1)->sendMessage(0)", @@ -261,8 +151,8 @@ func TestGraphsyncRoundTripRequestBudgetRequestor(t *testing.T) { require.Contains(t, traceStrings, "request(0)->newRequest(0)") require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block // has ErrBudgetExceeded exception recorded in the right place tracing.SingleExceptionEvent(t, "request(0)->executeTask(0)", "ErrBudgetExceeded", "traversal budget exceeded", true) @@ -272,7 +162,6 @@ func TestGraphsyncRoundTripRequestBudgetRequestor(t *testing.T) { } func TestGraphsyncRoundTripRequestBudgetResponder(t *testing.T) { - // create network ctx := context.Background() ctx, collectTracing := testutil.SetupTracing(ctx) @@ -311,8 +200,8 @@ func TestGraphsyncRoundTripRequestBudgetResponder(t *testing.T) { require.Contains(t, traceStrings, "request(0)->newRequest(0)") require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block // has ContextCancelError exception recorded in the right place // the requester gets a cancel, the responder gets a ErrBudgetExceeded @@ -320,112 +209,115 @@ func TestGraphsyncRoundTripRequestBudgetResponder(t *testing.T) { } func TestGraphsyncRoundTrip(t *testing.T) { - - // create network - ctx := context.Background() - ctx, collectTracing := testutil.SetupTracing(ctx) - ctx, cancel := context.WithTimeout(ctx, 3*time.Second) - defer cancel() - td := newGsTestData(ctx, t) - - // initialize graphsync on first node to make requests - requestor := td.GraphSyncHost1() - - // setup receiving peer to just record message coming in - blockChainLength := 100 - blockChain := testutil.SetupBlockChain(ctx, t, td.persistence2, 100, blockChainLength) - - // initialize graphsync on second node to response to requests - responder := td.GraphSyncHost2() - assertComplete := assertCompletionFunction(responder, 1) - - var receivedResponseData []byte - var receivedRequestData []byte - - requestor.RegisterIncomingResponseHook( - func(p peer.ID, responseData graphsync.ResponseData, hookActions graphsync.IncomingResponseHookActions) { - data, has := responseData.Extension(td.extensionName) - if has { - receivedResponseData = data + for pname, ps := range protocolsForTest { + t.Run(pname, func(t *testing.T) { + // create network + ctx := context.Background() + ctx, collectTracing := testutil.SetupTracing(ctx) + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + td := newOptionalGsTestData(ctx, t, ps.host1Protocols, ps.host2Protocols) + + // initialize graphsync on first node to make requests + requestor := td.GraphSyncHost1() + + // setup receiving peer to just record message coming in + blockChainLength := 100 + blockChain := testutil.SetupBlockChain(ctx, t, td.persistence2, 100, blockChainLength) + + // initialize graphsync on second node to response to requests + responder := td.GraphSyncHost2() + assertComplete := assertCompletionFunction(responder, 1) + + var receivedResponseData datamodel.Node + var receivedRequestData datamodel.Node + + requestor.RegisterIncomingResponseHook( + func(p peer.ID, responseData graphsync.ResponseData, hookActions graphsync.IncomingResponseHookActions) { + data, has := responseData.Extension(td.extensionName) + if has { + receivedResponseData = data + } + }) + + responder.RegisterIncomingRequestHook(func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) { + var has bool + receivedRequestData, has = requestData.Extension(td.extensionName) + if !has { + hookActions.TerminateWithError(errors.New("Missing extension")) + } else { + hookActions.SendExtensionData(td.extensionResponse) + } + }) + + finalResponseStatusChan := make(chan graphsync.ResponseStatusCode, 1) + responder.RegisterCompletedResponseListener(func(p peer.ID, request graphsync.RequestData, status graphsync.ResponseStatusCode) { + select { + case finalResponseStatusChan <- status: + default: + } + }) + progressChan, errChan := requestor.Request(ctx, td.host2.ID(), blockChain.TipLink, blockChain.Selector(), td.extension) + + blockChain.VerifyWholeChain(ctx, progressChan) + testutil.VerifyEmptyErrors(ctx, t, errChan) + require.Len(t, td.blockStore1, blockChainLength, "did not store all blocks") + + // verify extension roundtrip + require.Equal(t, td.extensionData, receivedRequestData, "did not receive correct extension request data") + require.Equal(t, td.extensionResponseData, receivedResponseData, "did not receive correct extension response data") + + // verify listener + var finalResponseStatus graphsync.ResponseStatusCode + testutil.AssertReceive(ctx, t, finalResponseStatusChan, &finalResponseStatus, "should receive status") + require.Equal(t, graphsync.RequestCompletedFull, finalResponseStatus) + + drain(requestor) + drain(responder) + assertComplete(ctx, t) + + tracing := collectTracing(t) + + traceStrings := tracing.TracesToStrings() + require.Contains(t, traceStrings, "response(0)->executeTask(0)->processBlock(0)->loadBlock(0)") + require.Contains(t, traceStrings, "response(0)->executeTask(0)->processBlock(0)->sendBlock(0)->processBlockHooks(0)") + require.Contains(t, traceStrings, "request(0)->newRequest(0)") + require.Contains(t, traceStrings, "request(0)->executeTask(0)") + require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + + processUpdateSpan := tracing.FindSpanByTraceString("response(0)") + require.Equal(t, int64(0), testutil.AttributeValueInTraceSpan(t, *processUpdateSpan, "priority").AsInt64()) + require.Equal(t, []string{string(td.extensionName)}, testutil.AttributeValueInTraceSpan(t, *processUpdateSpan, "extensions").AsStringSlice()) + + // each verifyBlock span should link to a cacheProcess span that stored it + + processResponsesSpans := tracing.FindSpans("processResponses") + processResponsesLinks := make(map[string]int64) + verifyBlockSpans := tracing.FindSpans("verifyBlock") + + for _, verifyBlockSpan := range verifyBlockSpans { + require.Len(t, verifyBlockSpan.Links, 1, "verifyBlock span should have one link") + found := false + for _, prcessResponseSpan := range processResponsesSpans { + sid := prcessResponseSpan.SpanContext.SpanID().String() + if verifyBlockSpan.Links[0].SpanContext.SpanID().String() == sid { + found = true + processResponsesLinks[sid] = processResponsesLinks[sid] + 1 + break + } + } + require.True(t, found, "verifyBlock should link to a known cacheProcess span") } - }) - - responder.RegisterIncomingRequestHook(func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) { - var has bool - receivedRequestData, has = requestData.Extension(td.extensionName) - if !has { - hookActions.TerminateWithError(errors.New("Missing extension")) - } else { - hookActions.SendExtensionData(td.extensionResponse) - } - }) - - finalResponseStatusChan := make(chan graphsync.ResponseStatusCode, 1) - responder.RegisterCompletedResponseListener(func(p peer.ID, request graphsync.RequestData, status graphsync.ResponseStatusCode) { - select { - case finalResponseStatusChan <- status: - default: - } - }) - progressChan, errChan := requestor.Request(ctx, td.host2.ID(), blockChain.TipLink, blockChain.Selector(), td.extension) - - blockChain.VerifyWholeChain(ctx, progressChan) - testutil.VerifyEmptyErrors(ctx, t, errChan) - require.Len(t, td.blockStore1, blockChainLength, "did not store all blocks") - - // verify extension roundtrip - require.Equal(t, td.extensionData, receivedRequestData, "did not receive correct extension request data") - require.Equal(t, td.extensionResponseData, receivedResponseData, "did not receive correct extension response data") - - // verify listener - var finalResponseStatus graphsync.ResponseStatusCode - testutil.AssertReceive(ctx, t, finalResponseStatusChan, &finalResponseStatus, "should receive status") - require.Equal(t, graphsync.RequestCompletedFull, finalResponseStatus) - - drain(requestor) - drain(responder) - assertComplete(ctx, t) - tracing := collectTracing(t) - - traceStrings := tracing.TracesToStrings() - require.Contains(t, traceStrings, "response(0)->executeTask(0)->processBlock(0)->loadBlock(0)") - require.Contains(t, traceStrings, "response(0)->executeTask(0)->processBlock(0)->sendBlock(0)->processBlockHooks(0)") - require.Contains(t, traceStrings, "request(0)->newRequest(0)") - require.Contains(t, traceStrings, "request(0)->executeTask(0)") - require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + // each cacheProcess span should be linked to one verifyBlock span per block it stored - processUpdateSpan := tracing.FindSpanByTraceString("response(0)") - require.Equal(t, int64(0), testutil.AttributeValueInTraceSpan(t, *processUpdateSpan, "priority").AsInt64()) - require.Equal(t, []string{string(td.extensionName)}, testutil.AttributeValueInTraceSpan(t, *processUpdateSpan, "extensions").AsStringSlice()) - - // each verifyBlock span should link to a cacheProcess span that stored it - - cacheProcessSpans := tracing.FindSpans("cacheProcess") - cacheProcessLinks := make(map[string]int64) - verifyBlockSpans := tracing.FindSpans("verifyBlock") - - for _, verifyBlockSpan := range verifyBlockSpans { - require.Len(t, verifyBlockSpan.Links, 1, "verifyBlock span should have one link") - found := false - for _, cacheProcessSpan := range cacheProcessSpans { - sid := cacheProcessSpan.SpanContext.SpanID().String() - if verifyBlockSpan.Links[0].SpanContext.SpanID().String() == sid { - found = true - cacheProcessLinks[sid] = cacheProcessLinks[sid] + 1 - break + for _, processResponseSpan := range processResponsesSpans { + blockCount := testutil.AttributeValueInTraceSpan(t, processResponseSpan, "blockCount").AsInt64() + require.Equal(t, processResponsesLinks[processResponseSpan.SpanContext.SpanID().String()], blockCount, "cacheProcess span should be linked to one verifyBlock span per block it processed") } - } - require.True(t, found, "verifyBlock should link to a known cacheProcess span") - } - - // each cacheProcess span should be linked to one verifyBlock span per block it stored - - for _, cacheProcessSpan := range cacheProcessSpans { - blockCount := testutil.AttributeValueInTraceSpan(t, cacheProcessSpan, "blockCount").AsInt64() - require.Equal(t, cacheProcessLinks[cacheProcessSpan.SpanContext.SpanID().String()], blockCount, "cacheProcess span should be linked to one verifyBlock span per block it processed") + }) } } @@ -468,7 +360,7 @@ func TestGraphsyncRoundTripPartial(t *testing.T) { for err := range errChan { // verify the error is received for leaf beta node being missing - require.EqualError(t, err, fmt.Sprintf("remote peer is missing block: %s", tree.LeafBetaLnk.String())) + require.EqualError(t, err, fmt.Sprintf("remote peer is missing block (%s) at path linkedList/2", tree.LeafBetaLnk.String())) } require.Equal(t, tree.LeafAlphaBlock.RawData(), td.blockStore1[tree.LeafAlphaLnk]) require.Equal(t, tree.MiddleListBlock.RawData(), td.blockStore1[tree.MiddleListNodeLnk]) @@ -491,8 +383,8 @@ func TestGraphsyncRoundTripPartial(t *testing.T) { require.Contains(t, traceStrings, "request(0)->newRequest(0)") require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block } func TestGraphsyncRoundTripIgnoreCids(t *testing.T) { @@ -519,8 +411,7 @@ func TestGraphsyncRoundTripIgnoreCids(t *testing.T) { td.blockStore1[cidlink.Link{Cid: blk.Cid()}] = blk.RawData() set.Add(blk.Cid()) } - encodedCidSet, err := cidset.EncodeCidSet(set) - require.NoError(t, err) + encodedCidSet := cidset.EncodeCidSet(set) extension := graphsync.ExtensionData{ Name: graphsync.ExtensionDoNotSendCIDs, Data: encodedCidSet, @@ -591,8 +482,7 @@ func TestGraphsyncRoundTripIgnoreNBlocks(t *testing.T) { td.blockStore1[cidlink.Link{Cid: blk.Cid()}] = blk.RawData() } - doNotSendFirstBlocksData, err := donotsendfirstblocks.EncodeDoNotSendFirstBlocks(50) - require.NoError(t, err) + doNotSendFirstBlocksData := donotsendfirstblocks.EncodeDoNotSendFirstBlocks(50) extension := graphsync.ExtensionData{ Name: graphsync.ExtensionsDoNotSendFirstBlocks, Data: doNotSendFirstBlocksData, @@ -706,7 +596,7 @@ func TestPauseResume(t *testing.T) { require.Len(t, responderPeerState.IncomingState.Diagnostics(), 0) requestID := <-requestIDChan - err := responder.UnpauseResponse(td.host1.ID(), requestID) + err := responder.Unpause(ctx, requestID) require.NoError(t, err) blockChain.VerifyRemainder(ctx, progressChan, stopPoint) @@ -727,8 +617,8 @@ func TestPauseResume(t *testing.T) { require.Contains(t, traceStrings, "request(0)->newRequest(0)") require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block // pause recorded tracing.SingleExceptionEvent(t, "response(0)->executeTask(0)", "github.com/ipfs/go-graphsync/responsemanager/hooks.ErrPaused", hooks.ErrPaused{}.Error(), false) @@ -776,7 +666,7 @@ func TestPauseResumeRequest(t *testing.T) { testutil.AssertDoesReceiveFirst(t, timer.C, "should pause request", progressChan) requestID := <-requestIDChan - err := requestor.UnpauseRequest(requestID, td.extensionUpdate) + err := requestor.Unpause(ctx, requestID, td.extensionUpdate) require.NoError(t, err) blockChain.VerifyRemainder(ctx, progressChan, stopPoint) @@ -810,8 +700,8 @@ func TestPauseResumeRequest(t *testing.T) { require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->executeTask(1)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block // has ErrPaused exception recorded in the right place tracing.SingleExceptionEvent(t, "request(0)->executeTask(0)", "ErrPaused", hooks.ErrPaused{}.Error(), false) @@ -826,8 +716,8 @@ func TestPauseResumeViaUpdate(t *testing.T) { defer cancel() td := newGsTestData(ctx, t) - var receivedReponseData []byte - var receivedUpdateData []byte + var receivedReponseData datamodel.Node + var receivedUpdateData datamodel.Node // initialize graphsync on first node to make requests requestor := td.GraphSyncHost1() assertAllResponsesReceived := assertAllResponsesReceivedFunction(requestor) @@ -927,8 +817,8 @@ func TestPauseResumeViaUpdateOnBlockHook(t *testing.T) { defer cancel() td := newGsTestData(ctx, t) - var receivedReponseData []byte - var receivedUpdateData []byte + var receivedReponseData datamodel.Node + var receivedUpdateData datamodel.Node // initialize graphsync on first node to make requests requestor := td.GraphSyncHost1() @@ -1075,7 +965,7 @@ func TestNetworkDisconnect(t *testing.T) { require.NoError(t, td.mn.DisconnectPeers(td.host1.ID(), td.host2.ID())) require.NoError(t, td.mn.UnlinkPeers(td.host1.ID(), td.host2.ID())) requestID := <-requestIDChan - err := responder.UnpauseResponse(td.host1.ID(), requestID) + err := responder.Unpause(ctx, requestID) require.NoError(t, err) testutil.AssertReceive(ctx, t, networkError, &err, "should receive network error") @@ -1098,8 +988,8 @@ func TestNetworkDisconnect(t *testing.T) { require.Contains(t, traceStrings, "request(0)->newRequest(0)") require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block // has ContextCancelError exception recorded in the right place tracing.SingleExceptionEvent(t, "request(0)->executeTask(0)", "ContextCancelError", ipldutil.ContextCancelError{}.Error(), false) @@ -1239,8 +1129,8 @@ func TestGraphsyncRoundTripAlternatePersistenceAndNodes(t *testing.T) { require.Contains(t, traceStrings, "request(1)->newRequest(0)") require.Contains(t, traceStrings, "request(1)->executeTask(0)") require.Contains(t, traceStrings, "request(1)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(1)->verifyBlock(0)") // should have one of these per block (TODO: why request(1) and not (0)?) + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(1)->verifyBlock(0)") // should have one of these per block (TODO: why request(1) and not (0)?) // TODO(rvagg): this is randomly either a SkipMe or a ipldutil.ContextCancelError; confirm this is sane // tracing.SingleExceptionEvent(t, "request(0)->newRequest(0)","request(0)->executeTask(0)", "SkipMe", traversal.SkipMe{}.Error(), true) @@ -1328,8 +1218,8 @@ func TestGraphsyncRoundTripMultipleAlternatePersistence(t *testing.T) { require.Contains(t, traceStrings, "request(1)->newRequest(0)") require.Contains(t, traceStrings, "request(1)->executeTask(0)") require.Contains(t, traceStrings, "request(1)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block } // TestRoundTripLargeBlocksSlowNetwork test verifies graphsync continues to work @@ -1583,8 +1473,8 @@ func TestUnixFSFetch(t *testing.T) { require.Contains(t, traceStrings, "request(0)->newRequest(0)") require.Contains(t, traceStrings, "request(0)->executeTask(0)") require.Contains(t, traceStrings, "request(0)->terminateRequest(0)") - require.Contains(t, traceStrings, "processResponses(0)->loaderProcess(0)->cacheProcess(0)") // should have one of these per response - require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block + require.Contains(t, traceStrings, "processResponses(0)") // should have one of these per response + require.Contains(t, traceStrings, "request(0)->verifyBlock(0)") // should have one of these per block } func TestGraphsyncBlockListeners(t *testing.T) { @@ -1621,8 +1511,8 @@ func TestGraphsyncBlockListeners(t *testing.T) { blocksOutgoing++ }) - var receivedResponseData []byte - var receivedRequestData []byte + var receivedResponseData datamodel.Node + var receivedRequestData datamodel.Node requestor.RegisterIncomingResponseHook( func(p peer.ID, responseData graphsync.ResponseData, hookActions graphsync.IncomingResponseHookActions) { @@ -1691,6 +1581,130 @@ func TestGraphsyncBlockListeners(t *testing.T) { ), tracing.TracesToStrings()) } +func TestSendUpdates(t *testing.T) { + // create network + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + td := newGsTestData(ctx, t) + + // initialize graphsync on first node to make requests + requestor := td.GraphSyncHost1() + + // setup receiving peer to just record message coming in + blockChainLength := 100 + blockChain := testutil.SetupBlockChain(ctx, t, td.persistence2, 100, blockChainLength) + + // initialize graphsync on second node to response to requests + responder := td.GraphSyncHost2() + + // set up pause point + stopPoint := 50 + blocksSent := 0 + requestIDChan := make(chan graphsync.RequestID, 1) + responder.RegisterOutgoingBlockHook(func(p peer.ID, requestData graphsync.RequestData, blockData graphsync.BlockData, hookActions graphsync.OutgoingBlockHookActions) { + _, has := requestData.Extension(td.extensionName) + if has { + select { + case requestIDChan <- requestData.ID(): + default: + } + blocksSent++ + if blocksSent == stopPoint { + hookActions.PauseResponse() + } + } else { + hookActions.TerminateWithError(errors.New("should have sent extension")) + } + }) + assertOneRequestCompletes := assertCompletionFunction(responder, 1) + progressChan, errChan := requestor.Request(ctx, td.host2.ID(), blockChain.TipLink, blockChain.Selector(), td.extension) + + blockChain.VerifyResponseRange(ctx, progressChan, 0, stopPoint) + timer := time.NewTimer(100 * time.Millisecond) + testutil.AssertDoesReceiveFirst(t, timer.C, "should pause request", progressChan) + + requestID := <-requestIDChan + + // set up extensions and listen for updates on the responder + responderExt1 := graphsync.ExtensionData{Name: graphsync.ExtensionName("grip grop"), Data: basicnode.NewString("flim flam, blim blam")} + responderExt2 := graphsync.ExtensionData{Name: graphsync.ExtensionName("Humpty/Dumpty"), Data: basicnode.NewInt(101)} + + var responderReceivedExt1 int + var responderReceivedExt2 int + updateRequests := make(chan struct{}, 1) + unreg := responder.RegisterRequestUpdatedHook(func(p peer.ID, request graphsync.RequestData, update graphsync.RequestData, hookActions graphsync.RequestUpdatedHookActions) { + ext, found := update.Extension(responderExt1.Name) + if found { + responderReceivedExt1++ + require.Equal(t, responderExt1.Data, ext) + } + + ext, found = update.Extension(responderExt2.Name) + if found { + responderReceivedExt2++ + require.Equal(t, responderExt2.Data, ext) + } + + updateRequests <- struct{}{} + }) + + // send updates + requestor.SendUpdate(ctx, requestID, responderExt1, responderExt2) + + // check we received what we expected + testutil.AssertDoesReceive(ctx, t, updateRequests, "request never completed") + require.Equal(t, 1, responderReceivedExt1, "got extension 1 in update") + require.Equal(t, 1, responderReceivedExt2, "got extension 2 in update") + unreg() + + // set up extensions and listen for updates on the requestor + requestorExt1 := graphsync.ExtensionData{Name: graphsync.ExtensionName("PING"), Data: basicnode.NewBytes(testutil.RandomBytes(100))} + requestorExt2 := graphsync.ExtensionData{Name: graphsync.ExtensionName("PONG"), Data: basicnode.NewBytes(testutil.RandomBytes(100))} + + updateResponses := make(chan struct{}, 1) + + var requestorReceivedExt1 int + var requestorReceivedExt2 int + + unreg = requestor.RegisterIncomingResponseHook(func(p peer.ID, responseData graphsync.ResponseData, hookActions graphsync.IncomingResponseHookActions) { + ext, found := responseData.Extension(requestorExt1.Name) + if found { + requestorReceivedExt1++ + require.Equal(t, requestorExt1.Data, ext) + } + + ext, found = responseData.Extension(requestorExt2.Name) + if found { + requestorReceivedExt2++ + require.Equal(t, requestorExt2.Data, ext) + } + + updateResponses <- struct{}{} + }) + + // send updates the other way + responder.SendUpdate(ctx, requestID, requestorExt1, requestorExt2) + + // check we received what we expected + testutil.AssertDoesReceive(ctx, t, updateResponses, "request never completed") + require.Equal(t, 1, requestorReceivedExt1, "got extension 1 in update") + require.Equal(t, 1, requestorReceivedExt2, "got extension 2 in update") + unreg() + + // finish up + err := responder.Unpause(ctx, requestID) + require.NoError(t, err) + + blockChain.VerifyRemainder(ctx, progressChan, stopPoint) + testutil.VerifyEmptyErrors(ctx, t, errChan) + require.Len(t, td.blockStore1, blockChainLength, "did not store all blocks") + + drain(requestor) + drain(responder) + assertOneRequestCompletes(ctx, t) +} + type gsTestData struct { mn mocknet.Mocknet ctx context.Context @@ -1700,12 +1714,12 @@ type gsTestData struct { gsnet2 gsnet.GraphSyncNetwork blockStore1, blockStore2 map[ipld.Link][]byte persistence1, persistence2 ipld.LinkSystem - extensionData []byte + extensionData datamodel.Node extensionName graphsync.ExtensionName extension graphsync.ExtensionData - extensionResponseData []byte + extensionResponseData datamodel.Node extensionResponse graphsync.ExtensionData - extensionUpdateData []byte + extensionUpdateData datamodel.Node extensionUpdate graphsync.ExtensionData } @@ -1765,6 +1779,10 @@ func assertCancelOrCompleteFunction(gs graphsync.GraphExchange, requestCount int } func newGsTestData(ctx context.Context, t *testing.T) *gsTestData { + return newOptionalGsTestData(ctx, t, nil, nil) +} + +func newOptionalGsTestData(ctx context.Context, t *testing.T, network1Protocols []protocol.ID, network2Protocols []protocol.ID) *gsTestData { t.Helper() td := &gsTestData{ctx: ctx} td.mn = mocknet.New(ctx) @@ -1777,25 +1795,33 @@ func newGsTestData(ctx context.Context, t *testing.T) *gsTestData { err = td.mn.LinkAll() require.NoError(t, err, "error linking hosts") - td.gsnet1 = gsnet.NewFromLibp2pHost(td.host1) - td.gsnet2 = gsnet.NewFromLibp2pHost(td.host2) + opts := make([]gsnet.Option, 0) + if network1Protocols != nil { + opts = append(opts, gsnet.GraphsyncProtocols(network1Protocols)) + } + td.gsnet1 = gsnet.NewFromLibp2pHost(td.host1, opts...) + opts = make([]gsnet.Option, 0) + if network2Protocols != nil { + opts = append(opts, gsnet.GraphsyncProtocols(network2Protocols)) + } + td.gsnet2 = gsnet.NewFromLibp2pHost(td.host2, opts...) td.blockStore1 = make(map[ipld.Link][]byte) td.persistence1 = testutil.NewTestStore(td.blockStore1) td.blockStore2 = make(map[ipld.Link][]byte) td.persistence2 = testutil.NewTestStore(td.blockStore2) // setup extension handlers - td.extensionData = testutil.RandomBytes(100) + td.extensionData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionName = graphsync.ExtensionName("AppleSauce/McGee") td.extension = graphsync.ExtensionData{ Name: td.extensionName, Data: td.extensionData, } - td.extensionResponseData = testutil.RandomBytes(100) + td.extensionResponseData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionResponse = graphsync.ExtensionData{ Name: td.extensionName, Data: td.extensionResponseData, } - td.extensionUpdateData = testutil.RandomBytes(100) + td.extensionUpdateData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionUpdate = graphsync.ExtensionData{ Name: td.extensionName, Data: td.extensionUpdateData, @@ -1812,43 +1838,6 @@ func (td *gsTestData) GraphSyncHost2(options ...Option) graphsync.GraphExchange return New(td.ctx, td.gsnet2, td.persistence2, options...) } -type receivedMessage struct { - message gsmsg.GraphSyncMessage - sender peer.ID -} - -// Receiver is an interface for receiving messages from the GraphSyncNetwork. -type receiver struct { - messageReceived chan receivedMessage -} - -func (r *receiver) ReceiveMessage( - ctx context.Context, - sender peer.ID, - incoming gsmsg.GraphSyncMessage) { - - select { - case <-ctx.Done(): - case r.messageReceived <- receivedMessage{incoming, sender}: - } -} - -func (r *receiver) ReceiveError(_ peer.ID, err error) { - fmt.Println("got receive err") -} - -func (r *receiver) Connected(p peer.ID) { -} - -func (r *receiver) Disconnected(p peer.ID) { -} - func processResponsesTraces(t *testing.T, tracing *testutil.Collector, responseCount int) []string { - traces := testutil.RepeatTraceStrings("processResponses({})->loaderProcess(0)->cacheProcess(0)", responseCount-1) - finalStub := tracing.FindSpanByTraceString(fmt.Sprintf("processResponses(%d)->loaderProcess(0)", responseCount-1)) - require.NotNil(t, finalStub) - if len(testutil.AttributeValueInTraceSpan(t, *finalStub, "requestIDs").AsInt64Slice()) == 0 { - return append(traces, fmt.Sprintf("processResponses(%d)->loaderProcess(0)", responseCount-1)) - } - return append(traces, fmt.Sprintf("processResponses(%d)->loaderProcess(0)->cacheProcess(0)", responseCount-1)) + return testutil.RepeatTraceStrings("processResponses({})", responseCount) } diff --git a/linktracker/linktracker_test.go b/linktracker/linktracker_test.go index af9745c9..4ba4c336 100644 --- a/linktracker/linktracker_test.go +++ b/linktracker/linktracker_test.go @@ -1,7 +1,6 @@ package linktracker import ( - "math/rand" "testing" "github.com/ipld/go-ipld-prime" @@ -74,7 +73,7 @@ func TestBlockRefCount(t *testing.T) { linkTracker := New() link := testutil.NewTestLink() for _, rq := range data.requests { - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() for _, present := range rq.traversals { linkTracker.RecordLinkTraversal(requestID, link, present) } @@ -116,7 +115,7 @@ func TestFinishRequest(t *testing.T) { for testCase, data := range testCases { t.Run(testCase, func(t *testing.T) { linkTracker := New() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() for _, lt := range data.linksTraversed { linkTracker.RecordLinkTraversal(requestID, lt.link, lt.blockPresent) } @@ -151,7 +150,7 @@ func TestIsKnownMissingLink(t *testing.T) { t.Run(testCase, func(t *testing.T) { linkTracker := New() link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() for _, present := range data.traversals { linkTracker.RecordLinkTraversal(requestID, link, present) } diff --git a/message/bench/bench_test.go b/message/bench/bench_test.go new file mode 100644 index 00000000..d7e003b0 --- /dev/null +++ b/message/bench/bench_test.go @@ -0,0 +1,103 @@ +package bench + +import ( + "bytes" + "math/rand" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + blocks "github.com/ipfs/go-block-format" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/traversal/selector/builder" + "github.com/libp2p/go-libp2p-core/peer" + "github.com/stretchr/testify/require" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + v1 "github.com/ipfs/go-graphsync/message/v1" + v2 "github.com/ipfs/go-graphsync/message/v2" + "github.com/ipfs/go-graphsync/testutil" +) + +func BenchmarkMessageEncodingRoundtrip(b *testing.B) { + root := testutil.GenerateCids(1)[0] + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + selector := ssb.Matcher().Node() + bb := basicnode.Prototype.Bytes.NewBuilder() + bb.AssignBytes(testutil.RandomBytes(100)) + extensionName := graphsync.ExtensionName("graphsync/awesome") + extension := graphsync.ExtensionData{ + Name: extensionName, + Data: bb.Build(), + } + id := graphsync.NewRequestID() + priority := graphsync.Priority(rand.Int31()) + status := graphsync.RequestAcknowledged + + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority, extension)) + builder.AddRequest(message.NewRequest(id, root, selector, priority)) + builder.AddResponseCode(id, status) + builder.AddExtensionData(id, extension) + builder.AddBlock(blocks.NewBlock([]byte("W"))) + builder.AddBlock(blocks.NewBlock([]byte("E"))) + builder.AddBlock(blocks.NewBlock([]byte("F"))) + builder.AddBlock(blocks.NewBlock([]byte("M"))) + + p := peer.ID("test peer") + gsm, err := builder.Build() + require.NoError(b, err) + + b.Run("Protobuf", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + buf := new(bytes.Buffer) + mh := v1.NewMessageHandler() + for pb.Next() { + buf.Reset() + + err := mh.ToNet(p, gsm, buf) + require.NoError(b, err) + + gsm2, err := mh.FromNet(p, buf) + require.NoError(b, err) + + // Note that require.Equal doesn't seem to handle maps well. + // It says they are non-equal simply because their order isn't deterministic. + if diff := cmp.Diff(gsm, gsm2, + cmp.Exporter(func(reflect.Type) bool { return true }), + cmp.Comparer(ipld.DeepEqual), + ); diff != "" { + b.Fatal(diff) + } + } + }) + }) + + b.Run("DagCbor", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + buf := new(bytes.Buffer) + mh := v2.NewMessageHandler() + for pb.Next() { + buf.Reset() + + err := mh.ToNet(p, gsm, buf) + require.NoError(b, err) + + gsm2, err := mh.FromNet(p, buf) + require.NoError(b, err) + + // same as above. + if diff := cmp.Diff(gsm, gsm2, + cmp.Exporter(func(reflect.Type) bool { return true }), + cmp.Comparer(ipld.DeepEqual), + ); diff != "" { + b.Fatal(diff) + } + } + }) + }) +} diff --git a/message/bench/empty.go b/message/bench/empty.go new file mode 100644 index 00000000..354df9d0 --- /dev/null +++ b/message/bench/empty.go @@ -0,0 +1 @@ +package bench diff --git a/message/builder.go b/message/builder.go index 15017198..a4bec912 100644 --- a/message/builder.go +++ b/message/builder.go @@ -7,7 +7,6 @@ import ( cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" ) // Builder captures components of a message across multiple @@ -17,7 +16,7 @@ type Builder struct { outgoingBlocks map[cid.Cid]blocks.Block blkSize uint64 completedResponses map[graphsync.RequestID]graphsync.ResponseStatusCode - outgoingResponses map[graphsync.RequestID]metadata.Metadata + outgoingResponses map[graphsync.RequestID][]GraphSyncLinkMetadatum extensions map[graphsync.RequestID][]graphsync.ExtensionData requests map[graphsync.RequestID]GraphSyncRequest } @@ -28,7 +27,7 @@ func NewBuilder() *Builder { requests: make(map[graphsync.RequestID]GraphSyncRequest), outgoingBlocks: make(map[cid.Cid]blocks.Block), completedResponses: make(map[graphsync.RequestID]graphsync.ResponseStatusCode), - outgoingResponses: make(map[graphsync.RequestID]metadata.Metadata), + outgoingResponses: make(map[graphsync.RequestID][]GraphSyncLinkMetadatum), extensions: make(map[graphsync.RequestID][]graphsync.ExtensionData), } } @@ -61,8 +60,8 @@ func (b *Builder) BlockSize() uint64 { // AddLink adds the given link and whether its block is present // to the message for the given request ID. -func (b *Builder) AddLink(requestID graphsync.RequestID, link ipld.Link, blockPresent bool) { - b.outgoingResponses[requestID] = append(b.outgoingResponses[requestID], metadata.Item{Link: link.(cidlink.Link).Cid, BlockPresent: blockPresent}) +func (b *Builder) AddLink(requestID graphsync.RequestID, link ipld.Link, linkAction graphsync.LinkAction) { + b.outgoingResponses[requestID] = append(b.outgoingResponses[requestID], GraphSyncLinkMetadatum{Link: link.(cidlink.Link).Cid, Action: linkAction}) } // AddResponseCode marks the given request as completed in the message, @@ -96,7 +95,7 @@ func (b *Builder) ScrubResponses(requestIDs []graphsync.RequestID) uint64 { for _, item := range metadata { block, willSendBlock := b.outgoingBlocks[item.Link] _, alreadySavedBlock := savedBlocks[item.Link] - if item.BlockPresent && willSendBlock && !alreadySavedBlock { + if item.Action == graphsync.LinkActionPresent && willSendBlock && !alreadySavedBlock { savedBlocks[item.Link] = block newBlkSize += uint64(len(block.RawData())) } @@ -111,16 +110,8 @@ func (b *Builder) ScrubResponses(requestIDs []graphsync.RequestID) uint64 { func (b *Builder) Build() (GraphSyncMessage, error) { responses := make(map[graphsync.RequestID]GraphSyncResponse, len(b.outgoingResponses)) for requestID, linkMap := range b.outgoingResponses { - mdRaw, err := metadata.EncodeMetadata(linkMap) - if err != nil { - return GraphSyncMessage{}, err - } - b.extensions[requestID] = append(b.extensions[requestID], graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: mdRaw, - }) status, isComplete := b.completedResponses[requestID] - responses[requestID] = NewResponse(requestID, responseCode(status, isComplete), b.extensions[requestID]...) + responses[requestID] = NewResponse(requestID, responseCode(status, isComplete), linkMap, b.extensions[requestID]...) } return GraphSyncMessage{ b.requests, responses, b.outgoingBlocks, diff --git a/message/builder_test.go b/message/builder_test.go index b9722c84..7cb325cf 100644 --- a/message/builder_test.go +++ b/message/builder_test.go @@ -2,15 +2,14 @@ package message import ( "io" - "math/rand" "testing" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/stretchr/testify/require" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" "github.com/ipfs/go-graphsync/testutil" ) @@ -20,22 +19,24 @@ func TestMessageBuilding(t *testing.T) { for _, block := range blocks { links = append(links, cidlink.Link{Cid: block.Cid()}) } - extensionData1 := testutil.RandomBytes(100) + bb := basicnode.Prototype.Bytes.NewBuilder() + bb.AssignBytes(testutil.RandomBytes(100)) extensionName1 := graphsync.ExtensionName("AppleSauce/McGee") extension1 := graphsync.ExtensionData{ Name: extensionName1, - Data: extensionData1, + Data: bb.Build(), } - extensionData2 := testutil.RandomBytes(100) + bb = basicnode.Prototype.Bytes.NewBuilder() + bb.AssignBytes(testutil.RandomBytes(100)) extensionName2 := graphsync.ExtensionName("HappyLand/Happenstance") extension2 := graphsync.ExtensionData{ Name: extensionName2, - Data: extensionData2, + Data: bb.Build(), } - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) - requestID3 := graphsync.RequestID(rand.Int31()) - requestID4 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() + requestID3 := graphsync.NewRequestID() + requestID4 := graphsync.NewRequestID() closer := io.NopCloser(nil) testCases := map[string]struct { build func(*Builder) @@ -46,20 +47,20 @@ func TestMessageBuilding(t *testing.T) { "normal building": { build: func(rb *Builder) { - rb.AddLink(requestID1, links[0], true) - rb.AddLink(requestID1, links[1], false) - rb.AddLink(requestID1, links[2], true) + rb.AddLink(requestID1, links[0], graphsync.LinkActionPresent) + rb.AddLink(requestID1, links[1], graphsync.LinkActionMissing) + rb.AddLink(requestID1, links[2], graphsync.LinkActionPresent) rb.AddResponseCode(requestID1, graphsync.RequestCompletedPartial) - rb.AddLink(requestID2, links[1], true) - rb.AddLink(requestID2, links[2], true) - rb.AddLink(requestID2, links[1], true) + rb.AddLink(requestID2, links[1], graphsync.LinkActionPresent) + rb.AddLink(requestID2, links[2], graphsync.LinkActionPresent) + rb.AddLink(requestID2, links[1], graphsync.LinkActionPresent) rb.AddResponseCode(requestID2, graphsync.RequestCompletedFull) - rb.AddLink(requestID3, links[0], true) - rb.AddLink(requestID3, links[1], true) + rb.AddLink(requestID3, links[0], graphsync.LinkActionPresent) + rb.AddLink(requestID3, links[1], graphsync.LinkActionPresent) rb.AddResponseCode(requestID4, graphsync.RequestCompletedFull) rb.AddExtensionData(requestID1, extension1) @@ -81,26 +82,26 @@ func TestMessageBuilding(t *testing.T) { response1 := findResponseForRequestID(t, responses, requestID1) require.Equal(t, graphsync.RequestCompletedPartial, response1.Status(), "did not generate completed partial response") - assertMetadata(t, response1, metadata.Metadata{ - metadata.Item{Link: links[0].(cidlink.Link).Cid, BlockPresent: true}, - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: false}, - metadata.Item{Link: links[2].(cidlink.Link).Cid, BlockPresent: true}, + assertMetadata(t, response1, []GraphSyncLinkMetadatum{ + {Link: links[0].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionMissing}, + {Link: links[2].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, }) assertExtension(t, response1, extension1) response2 := findResponseForRequestID(t, responses, requestID2) require.Equal(t, graphsync.RequestCompletedFull, response2.Status(), "did not generate completed full response") - assertMetadata(t, response2, metadata.Metadata{ - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, - metadata.Item{Link: links[2].(cidlink.Link).Cid, BlockPresent: true}, - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, + assertMetadata(t, response2, []GraphSyncLinkMetadatum{ + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, + {Link: links[2].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, }) response3 := findResponseForRequestID(t, responses, requestID3) require.Equal(t, graphsync.PartialResponse, response3.Status(), "did not generate partial response") - assertMetadata(t, response3, metadata.Metadata{ - metadata.Item{Link: links[0].(cidlink.Link).Cid, BlockPresent: true}, - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, + assertMetadata(t, response3, []GraphSyncLinkMetadatum{ + {Link: links[0].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, }) assertExtension(t, response3, extension2) @@ -137,19 +138,19 @@ func TestMessageBuilding(t *testing.T) { "scrub response": { build: func(rb *Builder) { - rb.AddLink(requestID1, links[0], true) - rb.AddLink(requestID1, links[1], false) - rb.AddLink(requestID1, links[2], true) + rb.AddLink(requestID1, links[0], graphsync.LinkActionPresent) + rb.AddLink(requestID1, links[1], graphsync.LinkActionMissing) + rb.AddLink(requestID1, links[2], graphsync.LinkActionPresent) rb.AddResponseCode(requestID1, graphsync.RequestCompletedPartial) - rb.AddLink(requestID2, links[1], true) - rb.AddLink(requestID2, links[2], true) - rb.AddLink(requestID2, links[1], true) + rb.AddLink(requestID2, links[1], graphsync.LinkActionPresent) + rb.AddLink(requestID2, links[2], graphsync.LinkActionPresent) + rb.AddLink(requestID2, links[1], graphsync.LinkActionPresent) rb.AddResponseCode(requestID2, graphsync.RequestCompletedFull) - rb.AddLink(requestID3, links[1], true) + rb.AddLink(requestID3, links[1], graphsync.LinkActionPresent) rb.AddResponseCode(requestID4, graphsync.RequestCompletedFull) rb.AddExtensionData(requestID1, extension1) @@ -168,16 +169,16 @@ func TestMessageBuilding(t *testing.T) { response2 := findResponseForRequestID(t, responses, requestID2) require.Equal(t, graphsync.RequestCompletedFull, response2.Status(), "did not generate completed full response") - assertMetadata(t, response2, metadata.Metadata{ - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, - metadata.Item{Link: links[2].(cidlink.Link).Cid, BlockPresent: true}, - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, + assertMetadata(t, response2, []GraphSyncLinkMetadatum{ + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, + {Link: links[2].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, }) response3 := findResponseForRequestID(t, responses, requestID3) require.Equal(t, graphsync.PartialResponse, response3.Status(), "did not generate partial response") - assertMetadata(t, response3, metadata.Metadata{ - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, + assertMetadata(t, response3, []GraphSyncLinkMetadatum{ + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, }) assertExtension(t, response3, extension2) @@ -195,19 +196,19 @@ func TestMessageBuilding(t *testing.T) { "scrub multiple responses": { build: func(rb *Builder) { - rb.AddLink(requestID1, links[0], true) - rb.AddLink(requestID1, links[1], false) - rb.AddLink(requestID1, links[2], true) + rb.AddLink(requestID1, links[0], graphsync.LinkActionPresent) + rb.AddLink(requestID1, links[1], graphsync.LinkActionMissing) + rb.AddLink(requestID1, links[2], graphsync.LinkActionPresent) rb.AddResponseCode(requestID1, graphsync.RequestCompletedPartial) - rb.AddLink(requestID2, links[1], true) - rb.AddLink(requestID2, links[2], true) - rb.AddLink(requestID2, links[1], true) + rb.AddLink(requestID2, links[1], graphsync.LinkActionPresent) + rb.AddLink(requestID2, links[2], graphsync.LinkActionPresent) + rb.AddLink(requestID2, links[1], graphsync.LinkActionPresent) rb.AddResponseCode(requestID2, graphsync.RequestCompletedFull) - rb.AddLink(requestID3, links[1], true) + rb.AddLink(requestID3, links[1], graphsync.LinkActionPresent) rb.AddResponseCode(requestID4, graphsync.RequestCompletedFull) rb.AddExtensionData(requestID1, extension1) @@ -226,8 +227,8 @@ func TestMessageBuilding(t *testing.T) { response3 := findResponseForRequestID(t, responses, requestID3) require.Equal(t, graphsync.PartialResponse, response3.Status(), "did not generate partial response") - assertMetadata(t, response3, metadata.Metadata{ - metadata.Item{Link: links[1].(cidlink.Link).Cid, BlockPresent: true}, + assertMetadata(t, response3, []GraphSyncLinkMetadatum{ + {Link: links[1].(cidlink.Link).Cid, Action: graphsync.LinkActionPresent}, }) assertExtension(t, response3, extension2) @@ -265,10 +266,6 @@ func assertExtension(t *testing.T, response GraphSyncResponse, extension graphsy require.Equal(t, extension.Data, returnedExtensionData, "did not encode extension") } -func assertMetadata(t *testing.T, response GraphSyncResponse, expectedMetadata metadata.Metadata) { - responseMetadataRaw, found := response.Extension(graphsync.ExtensionMetadata) - require.True(t, found, "Metadata should be included in response") - responseMetadata, err := metadata.DecodeMetadata(responseMetadataRaw) - require.NoError(t, err) - require.Equal(t, expectedMetadata, responseMetadata, "incorrect metadata included in response") +func assertMetadata(t *testing.T, response GraphSyncResponse, expectedMetadata []GraphSyncLinkMetadatum) { + require.Equal(t, expectedMetadata, response.metadata, "incorrect metadata included in response") } diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go new file mode 100644 index 00000000..e6e920ce --- /dev/null +++ b/message/ipldbind/message.go @@ -0,0 +1,89 @@ +package ipldbind + +import ( + cid "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync/message" + "github.com/ipld/go-ipld-prime/datamodel" + + "github.com/ipfs/go-graphsync" +) + +// GraphSyncExtensions is a container for representing extension data for +// bindnode, it's converted to a graphsync.ExtensionData list by +// ToExtensionsList() +type GraphSyncExtensions struct { + Keys []string + Values map[string]datamodel.Node +} + +// NewGraphSyncExtensions creates GraphSyncExtensions from either a request or +// response object +func NewGraphSyncExtensions(part message.MessagePartWithExtensions) *GraphSyncExtensions { + names := part.ExtensionNames() + if len(names) == 0 { + return nil + } + keys := make([]string, 0, len(names)) + values := make(map[string]datamodel.Node, len(names)) + for _, name := range names { + keys = append(keys, string(name)) + data, _ := part.Extension(graphsync.ExtensionName(name)) + values[string(name)] = data + } + return &GraphSyncExtensions{keys, values} +} + +// ToExtensionsList creates a list of graphsync.ExtensionData objects from a +// GraphSyncExtensions +func (gse GraphSyncExtensions) ToExtensionsList() []graphsync.ExtensionData { + exts := make([]graphsync.ExtensionData, 0, len(gse.Values)) + for name, data := range gse.Values { + exts = append(exts, graphsync.ExtensionData{Name: graphsync.ExtensionName(name), Data: data}) + } + return exts +} + +// GraphSyncRequest is a struct to capture data on a request contained in a +// GraphSyncMessage. +type GraphSyncRequest struct { + Id []byte + RequestType graphsync.RequestType + Priority *graphsync.Priority + Root *cid.Cid + Selector *datamodel.Node + Extensions *GraphSyncExtensions +} + +// GraphSyncResponse is an struct to capture data on a response sent back +// in a GraphSyncMessage. +type GraphSyncResponse struct { + Id []byte + Status graphsync.ResponseStatusCode + Metadata *[]message.GraphSyncLinkMetadatum + Extensions *GraphSyncExtensions +} + +// GraphSyncBlock is a container for representing extension data for bindnode, +// it's converted to a block.Block by the message translation layer +type GraphSyncBlock struct { + Prefix []byte + Data []byte +} + +// GraphSyncMessage is a container for representing extension data for bindnode, +// it's converted to a message.GraphSyncMessage by the message translation layer +type GraphSyncMessage struct { + Requests *[]GraphSyncRequest + Responses *[]GraphSyncResponse + Blocks *[]GraphSyncBlock +} + +type GraphSyncMessageRoot struct { + Gs2 *GraphSyncMessage +} + +// NamedExtension exists just for the purpose of the constructors +type NamedExtension struct { + Name graphsync.ExtensionName + Data datamodel.Node +} diff --git a/message/ipldbind/schema.go b/message/ipldbind/schema.go new file mode 100644 index 00000000..8e570fda --- /dev/null +++ b/message/ipldbind/schema.go @@ -0,0 +1,25 @@ +package ipldbind + +import ( + _ "embed" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" +) + +//go:embed schema.ipldsch +var embedSchema []byte + +var Prototype struct { + Message schema.TypedPrototype +} + +func init() { + ts, err := ipld.LoadSchemaBytes(embedSchema) + if err != nil { + panic(err) + } + + Prototype.Message = bindnode.Prototype((*GraphSyncMessageRoot)(nil), ts.TypeByName("GraphSyncMessageRoot")) +} diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch new file mode 100644 index 00000000..5f20390b --- /dev/null +++ b/message/ipldbind/schema.ipldsch @@ -0,0 +1,114 @@ +################################################################################ +### GraphSync messaging protocol version 2 ### +################################################################################ + +# UUID bytes +type GraphSyncRequestID bytes + +type GraphSyncPriority int + +# Extensions as a name:data map where the data is any arbitrary, valid Node +type GraphSyncExtensions { String : nullable Any } + +type GraphSyncLinkAction enum { + # Present means the linked block was present on this machine, and is included + # in this message + | Present ("p") + # DuplicateNotSent means the linked block was present on this machine, but I + # am not sending it (most likely duplicate) + # TODO: | DuplicateNotSent ("d") + # Missing means I did not have the linked block, so I skipped over this part + # of the traversal + | Missing ("m") + # DuplicateDAGSkipped means the DAG with this link points toward has already + # been traversed entirely in the course of this request + # so I am skipping over it entirely + # TODO: | DuplicateDAGSkipped ("s") +} representation string + +# Metadata for each "link" in the DAG being communicated, each block gets one of +# these and missing blocks also get one, with an explanation as per +# GraphSyncLinkAction +type GraphSyncMetadatum struct { + link Link + action GraphSyncLinkAction +} representation tuple + +type GraphSyncMetadata [GraphSyncMetadatum] + +type GraphSyncResponseStatusCode enum { + # Informational Codes (request in progress) + + | RequestAcknowledged ("10") + | AdditionalPeers ("11") + | NotEnoughGas ("12") + | OtherProtocol ("13") + | PartialResponse ("14") + | RequestPaused ("15") + + # Success Response Codes (request terminated) + + | RequestCompletedFull ("20") + | RequestCompletedPartial ("21") + + # Error Response Codes (request terminated) + + | RequestRejected ("30") + | RequestFailedBusy ("31") + | RequestFailedUnknown ("32") + | RequestFailedLegal ("33") + | RequestFailedContentNotFound ("34") + | RequestCancelled ("35") +} representation int + +type GraphSyncRequestType enum { + # New means a new request + | New ("n") + # Cancel means cancel the request referenced by request ID + | Cancel ("c") + # Update means the extensions contain an update about this request + | Update ("u") + # Restart means restart this request from the begging, respecting the any + # DoNotSendCids/DoNotSendBlocks contained in the extensions--essentially a + # cancel followed by a new + # TODO: | Restart ("r") +} representation string + +type GraphSyncRequest struct { + id GraphSyncRequestID (rename "id") # unique id set on the requester side + requestType GraphSyncRequestType (rename "type") # the request type + priority optional GraphSyncPriority (rename "pri") # the priority (normalized). default to 1 + root optional Link (rename "root") # a CID for the root node in the query + selector optional Any (rename "sel") # see https://github.com/ipld/specs/blob/master/selectors/selectors.md + extensions optional GraphSyncExtensions (rename "ext") # side channel information +} representation map + +type GraphSyncResponse struct { + id GraphSyncRequestID (rename "reqid") # the request id we are responding to + status GraphSyncResponseStatusCode (rename "stat") # a status code. + metadata optional GraphSyncMetadata (rename "meta") # metadata about response + extensions optional GraphSyncExtensions (rename "ext") # side channel information +} representation map + +# Block data and CID prefix that can be used to reconstruct the entire CID from +# the hash of the bytes +type GraphSyncBlock struct { + prefix Bytes # CID prefix (cid version, multicodec and multihash prefix (type + length) + data Bytes +} representation tuple + +# We expect each message to contain at least one of the fields, typically either +# just requests, or responses and possibly blocks with it +type GraphSyncMessage struct { + requests optional [GraphSyncRequest] (rename "req") + responses optional [GraphSyncResponse] (rename "rsp") + blocks optional [GraphSyncBlock] (rename "blk") +} representation map + +# Parent keyed union to hold the message, the root of the structure that can be +# used to version the messaging format outside of the protocol and makes the +# data itself more self-descriptive (i.e. `{"gs2":...` will appear at the front +# of every msg) +type GraphSyncMessageRoot union { + | GraphSyncMessage "gs2" +} representation keyed diff --git a/message/message.go b/message/message.go index 6e818847..354cc236 100644 --- a/message/message.go +++ b/message/message.go @@ -1,61 +1,69 @@ package message import ( - "encoding/binary" - "errors" + "bytes" "fmt" "io" + "strings" blocks "github.com/ipfs/go-block-format" cid "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" - pool "github.com/libp2p/go-buffer-pool" - "github.com/libp2p/go-libp2p-core/network" + "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/libp2p/go-libp2p-core/peer" "github.com/libp2p/go-msgio" - "google.golang.org/protobuf/proto" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/ipldutil" - pb "github.com/ipfs/go-graphsync/message/pb" ) -// IsTerminalSuccessCode returns true if the response code indicates the -// request terminated successfully. -// DEPRECATED: use status.IsSuccess() -func IsTerminalSuccessCode(status graphsync.ResponseStatusCode) bool { - return status.IsSuccess() +// MessageHandler provides a consistent interface for maintaining per-peer state +// within the differnet protocol versions +type MessageHandler interface { + FromNet(peer.ID, io.Reader) (GraphSyncMessage, error) + FromMsgReader(peer.ID, msgio.Reader) (GraphSyncMessage, error) + ToNet(peer.ID, GraphSyncMessage, io.Writer) error } -// IsTerminalFailureCode returns true if the response code indicates the -// request terminated in failure. -// DEPRECATED: use status.IsFailure() -func IsTerminalFailureCode(status graphsync.ResponseStatusCode) bool { - return status.IsFailure() -} - -// IsTerminalResponseCode returns true if the response code signals -// the end of the request -// DEPRECATED: use status.IsTerminal() -func IsTerminalResponseCode(status graphsync.ResponseStatusCode) bool { - return status.IsTerminal() -} - -// Exportable is an interface that can serialize to a protobuf -type Exportable interface { - ToProto() (*pb.Message, error) - ToNet(w io.Writer) error +// MessagePartWithExtensions is an interface for accessing metadata on both +// requests and responses, which have a consistent extension accessor mechanism +type MessagePartWithExtensions interface { + ExtensionNames() []graphsync.ExtensionName + Extension(name graphsync.ExtensionName) (datamodel.Node, bool) } // GraphSyncRequest is a struct to capture data on a request contained in a // GraphSyncMessage. type GraphSyncRequest struct { - root cid.Cid - selector ipld.Node - priority graphsync.Priority - id graphsync.RequestID - extensions map[string][]byte - isCancel bool - isUpdate bool + root cid.Cid + selector ipld.Node + priority graphsync.Priority + id graphsync.RequestID + extensions map[string]datamodel.Node + requestType graphsync.RequestType +} + +// String returns a human-readable form of a GraphSyncRequest +func (gsr GraphSyncRequest) String() string { + sel := "nil" + if gsr.selector != nil { + var buf bytes.Buffer + dagjson.Encode(gsr.selector, &buf) + sel = buf.String() + } + extStr := strings.Builder{} + for _, name := range gsr.ExtensionNames() { + extStr.WriteString(string(name)) + extStr.WriteString("|") + } + return fmt.Sprintf("GraphSyncRequest", + gsr.root.String(), + sel, + gsr.priority, + gsr.id.String(), + gsr.requestType, + extStr.String(), + ) } // GraphSyncResponse is an struct to capture data on a response sent back @@ -63,38 +71,101 @@ type GraphSyncRequest struct { type GraphSyncResponse struct { requestID graphsync.RequestID status graphsync.ResponseStatusCode - extensions map[string][]byte + metadata []GraphSyncLinkMetadatum + extensions map[string]datamodel.Node +} + +// GraphSyncLinkMetadatum is used for holding individual pieces of metadata, +// this is not intended for public consumption and is used within +// GraphSyncLinkMetadata to contain the metadata +type GraphSyncLinkMetadatum struct { + Link cid.Cid + Action graphsync.LinkAction } +// GraphSyncLinkMetadata is a graphsync.LinkMetadata compatible type that is +// used for holding and accessing the metadata for a request +type GraphSyncLinkMetadata struct { + linkMetadata []GraphSyncLinkMetadatum +} + +// String returns a human-readable form of a GraphSyncResponse +func (gsr GraphSyncResponse) String() string { + extStr := strings.Builder{} + for _, name := range gsr.ExtensionNames() { + extStr.WriteString(string(name)) + extStr.WriteString("|") + } + return fmt.Sprintf("GraphSyncResponse", + gsr.requestID.String(), + gsr.status, + extStr.String(), + ) +} + +// GraphSyncMessage is the internal representation form of a message sent or +// received over the wire type GraphSyncMessage struct { requests map[graphsync.RequestID]GraphSyncRequest responses map[graphsync.RequestID]GraphSyncResponse blocks map[cid.Cid]blocks.Block } -// NewRequest builds a new Graphsync request +// NewMessage generates a new message containing the provided requests, +// responses and blocks +func NewMessage( + requests map[graphsync.RequestID]GraphSyncRequest, + responses map[graphsync.RequestID]GraphSyncResponse, + blocks map[cid.Cid]blocks.Block, +) GraphSyncMessage { + return GraphSyncMessage{requests, responses, blocks} +} + +// String returns a human-readable (multi-line) form of a GraphSyncMessage and +// its contents +func (gsm GraphSyncMessage) String() string { + cts := make([]string, 0) + for _, req := range gsm.requests { + cts = append(cts, req.String()) + } + for _, resp := range gsm.responses { + cts = append(cts, resp.String()) + } + for _, c := range gsm.blocks { + cts = append(cts, fmt.Sprintf("Block<%s>", c.String())) + } + return fmt.Sprintf("GraphSyncMessage<\n\t%s\n>", strings.Join(cts, ",\n\t")) +} + +// NewRequest builds a new GraphSyncRequest func NewRequest(id graphsync.RequestID, root cid.Cid, selector ipld.Node, priority graphsync.Priority, extensions ...graphsync.ExtensionData) GraphSyncRequest { - return newRequest(id, root, selector, priority, false, false, toExtensionsMap(extensions)) + return newRequest(id, root, selector, priority, graphsync.RequestTypeNew, toExtensionsMap(extensions)) +} + +// NewCancelRequest request generates a request to cancel an in progress request +func NewCancelRequest(id graphsync.RequestID) GraphSyncRequest { + return newRequest(id, cid.Cid{}, nil, 0, graphsync.RequestTypeCancel, nil) } -// CancelRequest request generates a request to cancel an in progress request -func CancelRequest(id graphsync.RequestID) GraphSyncRequest { - return newRequest(id, cid.Cid{}, nil, 0, true, false, nil) +// NewUpdateRequest generates a new request to update an in progress request with the given extensions +func NewUpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionData) GraphSyncRequest { + return newRequest(id, cid.Cid{}, nil, 0, graphsync.RequestTypeUpdate, toExtensionsMap(extensions)) } -// UpdateRequest generates a new request to update an in progress request with the given extensions -func UpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionData) GraphSyncRequest { - return newRequest(id, cid.Cid{}, nil, 0, false, true, toExtensionsMap(extensions)) +// NewLinkMetadata generates a new graphsync.LinkMetadata compatible object, +// used for accessing the metadata in a message +func NewLinkMetadata(md []GraphSyncLinkMetadatum) GraphSyncLinkMetadata { + return GraphSyncLinkMetadata{md} } -func toExtensionsMap(extensions []graphsync.ExtensionData) (extensionsMap map[string][]byte) { +func toExtensionsMap(extensions []graphsync.ExtensionData) (extensionsMap map[string]datamodel.Node) { if len(extensions) > 0 { - extensionsMap = make(map[string][]byte, len(extensions)) + extensionsMap = make(map[string]datamodel.Node, len(extensions)) for _, extension := range extensions { extensionsMap[string(extension.Name)] = extension.Data } @@ -106,110 +177,48 @@ func newRequest(id graphsync.RequestID, root cid.Cid, selector ipld.Node, priority graphsync.Priority, - isCancel bool, - isUpdate bool, - extensions map[string][]byte) GraphSyncRequest { + requestType graphsync.RequestType, + extensions map[string]datamodel.Node) GraphSyncRequest { + return GraphSyncRequest{ - id: id, - root: root, - selector: selector, - priority: priority, - isCancel: isCancel, - isUpdate: isUpdate, - extensions: extensions, + id: id, + root: root, + selector: selector, + priority: priority, + requestType: requestType, + extensions: extensions, } } // NewResponse builds a new Graphsync response func NewResponse(requestID graphsync.RequestID, status graphsync.ResponseStatusCode, + md []GraphSyncLinkMetadatum, extensions ...graphsync.ExtensionData) GraphSyncResponse { - return newResponse(requestID, status, toExtensionsMap(extensions)) + + return newResponse(requestID, status, md, toExtensionsMap(extensions)) } func newResponse(requestID graphsync.RequestID, - status graphsync.ResponseStatusCode, extensions map[string][]byte) GraphSyncResponse { + status graphsync.ResponseStatusCode, + responseMetadata []GraphSyncLinkMetadatum, + extensions map[string]datamodel.Node) GraphSyncResponse { + return GraphSyncResponse{ requestID: requestID, status: status, + metadata: responseMetadata, extensions: extensions, } } -func newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.GetRequests())) - for _, req := range pbm.Requests { - if req == nil { - return GraphSyncMessage{}, errors.New("request is nil") - } - var root cid.Cid - var err error - if !req.Cancel && !req.Update { - root, err = cid.Cast(req.Root) - if err != nil { - return GraphSyncMessage{}, err - } - } - - var selector ipld.Node - if !req.Cancel && !req.Update { - selector, err = ipldutil.DecodeNode(req.Selector) - if err != nil { - return GraphSyncMessage{}, err - } - } - exts := req.GetExtensions() - if exts == nil { - exts = make(map[string][]byte) - } - requests[graphsync.RequestID(req.Id)] = newRequest(graphsync.RequestID(req.Id), root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) - } - - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.GetResponses())) - for _, res := range pbm.Responses { - if res == nil { - return GraphSyncMessage{}, errors.New("response is nil") - } - exts := res.GetExtensions() - if exts == nil { - exts = make(map[string][]byte) - } - responses[graphsync.RequestID(res.Id)] = newResponse(graphsync.RequestID(res.Id), graphsync.ResponseStatusCode(res.Status), exts) - } - - blks := make(map[cid.Cid]blocks.Block, len(pbm.GetData())) - for _, b := range pbm.GetData() { - if b == nil { - return GraphSyncMessage{}, errors.New("block is nil") - } - - pref, err := cid.PrefixFromBytes(b.GetPrefix()) - if err != nil { - return GraphSyncMessage{}, err - } - - c, err := pref.Sum(b.GetData()) - if err != nil { - return GraphSyncMessage{}, err - } - - blk, err := blocks.NewBlockWithCid(b.GetData(), c) - if err != nil { - return GraphSyncMessage{}, err - } - - blks[blk.Cid()] = blk - } - - return GraphSyncMessage{ - requests, responses, blks, - }, nil -} - +// Empty returns true if this message contains no meaningful content: requests, +// responses, or blocks func (gsm GraphSyncMessage) Empty() bool { return len(gsm.blocks) == 0 && len(gsm.requests) == 0 && len(gsm.responses) == 0 } +// Requests provides a copy of the requests in this message func (gsm GraphSyncMessage) Requests() []GraphSyncRequest { requests := make([]GraphSyncRequest, 0, len(gsm.requests)) for _, request := range gsm.requests { @@ -218,6 +227,8 @@ func (gsm GraphSyncMessage) Requests() []GraphSyncRequest { return requests } +// ResponseCodes returns a list of ResponseStatusCodes contained in the +// responses in this GraphSyncMessage func (gsm GraphSyncMessage) ResponseCodes() map[graphsync.RequestID]graphsync.ResponseStatusCode { codes := make(map[graphsync.RequestID]graphsync.ResponseStatusCode, len(gsm.responses)) for id, response := range gsm.responses { @@ -226,6 +237,7 @@ func (gsm GraphSyncMessage) ResponseCodes() map[graphsync.RequestID]graphsync.Re return codes } +// Responses provides a copy of the responses in this message func (gsm GraphSyncMessage) Responses() []GraphSyncResponse { responses := make([]GraphSyncResponse, 0, len(gsm.responses)) for _, response := range gsm.responses { @@ -234,6 +246,7 @@ func (gsm GraphSyncMessage) Responses() []GraphSyncResponse { return responses } +// Blocks provides a copy of all of the blocks in this message func (gsm GraphSyncMessage) Blocks() []blocks.Block { bs := make([]blocks.Block, 0, len(gsm.blocks)) for _, block := range gsm.blocks { @@ -242,106 +255,7 @@ func (gsm GraphSyncMessage) Blocks() []blocks.Block { return bs } -// FromNet can read a network stream to deserialized a GraphSyncMessage -func FromNet(r io.Reader) (GraphSyncMessage, error) { - reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) - return FromMsgReader(reader) -} - -// FromMsgReader can deserialize a protobuf message into a GraphySyncMessage. -func FromMsgReader(r msgio.Reader) (GraphSyncMessage, error) { - msg, err := r.ReadMsg() - if err != nil { - return GraphSyncMessage{}, err - } - - var pb pb.Message - err = proto.Unmarshal(msg, &pb) - r.ReleaseMsg(msg) - if err != nil { - return GraphSyncMessage{}, err - } - - return newMessageFromProto(&pb) -} - -func (gsm GraphSyncMessage) ToProto() (*pb.Message, error) { - pbm := new(pb.Message) - pbm.Requests = make([]*pb.Message_Request, 0, len(gsm.requests)) - for _, request := range gsm.requests { - var selector []byte - var err error - if request.selector != nil { - selector, err = ipldutil.EncodeNode(request.selector) - if err != nil { - return nil, err - } - } - pbm.Requests = append(pbm.Requests, &pb.Message_Request{ - Id: int32(request.id), - Root: request.root.Bytes(), - Selector: selector, - Priority: int32(request.priority), - Cancel: request.isCancel, - Update: request.isUpdate, - Extensions: request.extensions, - }) - } - - pbm.Responses = make([]*pb.Message_Response, 0, len(gsm.responses)) - for _, response := range gsm.responses { - pbm.Responses = append(pbm.Responses, &pb.Message_Response{ - Id: int32(response.requestID), - Status: int32(response.status), - Extensions: response.extensions, - }) - } - - blocks := gsm.Blocks() - pbm.Data = make([]*pb.Message_Block, 0, len(blocks)) - for _, b := range blocks { - pbm.Data = append(pbm.Data, &pb.Message_Block{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), - }) - } - return pbm, nil -} - -func (gsm GraphSyncMessage) ToNet(w io.Writer) error { - msg, err := gsm.ToProto() - if err != nil { - return err - } - size := proto.Size(msg) - buf := pool.Get(size + binary.MaxVarintLen64) - defer pool.Put(buf) - - n := binary.PutUvarint(buf, uint64(size)) - - out, err := proto.MarshalOptions{}.MarshalAppend(buf[:n], msg) - if err != nil { - return err - } - _, err = w.Write(out) - return err -} - -func (gsm GraphSyncMessage) Loggable() map[string]interface{} { - requests := make([]string, 0, len(gsm.requests)) - for _, request := range gsm.requests { - requests = append(requests, fmt.Sprintf("%d", request.id)) - } - responses := make([]string, 0, len(gsm.responses)) - for _, response := range gsm.responses { - responses = append(responses, fmt.Sprintf("%d", response.requestID)) - } - return map[string]interface{}{ - "requests": requests, - "responses": responses, - } -} - +// Clone returns a shallow copy of this GraphSyncMessage func (gsm GraphSyncMessage) Clone() GraphSyncMessage { requests := make(map[graphsync.RequestID]GraphSyncRequest, len(gsm.requests)) for id, request := range gsm.requests { @@ -372,7 +286,7 @@ func (gsr GraphSyncRequest) Priority() graphsync.Priority { return gsr.priority // Extension returns the content for an extension on a response, or errors // if extension is not present -func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) ([]byte, bool) { +func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) (datamodel.Node, bool) { if gsr.extensions == nil { return nil, false } @@ -384,19 +298,16 @@ func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) ([]byte, boo } // ExtensionNames returns the names of the extensions included in this request -func (gsr GraphSyncRequest) ExtensionNames() []string { - var extNames []string +func (gsr GraphSyncRequest) ExtensionNames() []graphsync.ExtensionName { + var extNames []graphsync.ExtensionName for ext := range gsr.extensions { - extNames = append(extNames, ext) + extNames = append(extNames, graphsync.ExtensionName(ext)) } return extNames } -// IsCancel returns true if this particular request is being cancelled -func (gsr GraphSyncRequest) IsCancel() bool { return gsr.isCancel } - -// IsUpdate returns true if this particular request is being updated -func (gsr GraphSyncRequest) IsUpdate() bool { return gsr.isUpdate } +// RequestType returns the type of this request (new, cancel, update, etc.) +func (gsr GraphSyncRequest) Type() graphsync.RequestType { return gsr.requestType } // RequestID returns the request ID for this response func (gsr GraphSyncResponse) RequestID() graphsync.RequestID { return gsr.requestID } @@ -406,7 +317,7 @@ func (gsr GraphSyncResponse) Status() graphsync.ResponseStatusCode { return gsr. // Extension returns the content for an extension on a response, or errors // if extension is not present -func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) ([]byte, bool) { +func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) (datamodel.Node, bool) { if gsr.extensions == nil { return nil, false } @@ -418,18 +329,44 @@ func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) ([]byte, bo } // ExtensionNames returns the names of the extensions included in this request -func (gsr GraphSyncResponse) ExtensionNames() []string { - var extNames []string +func (gsr GraphSyncResponse) ExtensionNames() []graphsync.ExtensionName { + var extNames []graphsync.ExtensionName for ext := range gsr.extensions { - extNames = append(extNames, ext) + extNames = append(extNames, graphsync.ExtensionName(ext)) } return extNames } +// Metadata returns an instance of a graphsync.LinkMetadata for accessing the +// individual metadatum via an iterator +func (gsr GraphSyncResponse) Metadata() graphsync.LinkMetadata { + return GraphSyncLinkMetadata{gsr.metadata} +} + +// Iterate over the metadata one by one via a graphsync.LinkMetadataIterator +// callback function +func (gslm GraphSyncLinkMetadata) Iterate(iter graphsync.LinkMetadataIterator) { + for _, md := range gslm.linkMetadata { + iter(md.Link, md.Action) + } +} + +// Length returns the number of metadata entries +func (gslm GraphSyncLinkMetadata) Length() int64 { + return int64(len(gslm.linkMetadata)) +} + +// RawMetadata accesses the raw GraphSyncLinkMetadatum contained in this object, +// this is not exposed via the graphsync.LinkMetadata API and in general the +// Iterate() method should be used instead for accessing the individual metadata +func (gslm GraphSyncLinkMetadata) RawMetadata() []GraphSyncLinkMetadatum { + return gslm.linkMetadata +} + // ReplaceExtensions merges the extensions given extensions into the request to create a new request, // but always uses new data func (gsr GraphSyncRequest) ReplaceExtensions(extensions []graphsync.ExtensionData) GraphSyncRequest { - req, _ := gsr.MergeExtensions(extensions, func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { + req, _ := gsr.MergeExtensions(extensions, func(name graphsync.ExtensionName, oldData datamodel.Node, newData datamodel.Node) (datamodel.Node, error) { return newData, nil }) return req @@ -438,12 +375,12 @@ func (gsr GraphSyncRequest) ReplaceExtensions(extensions []graphsync.ExtensionDa // MergeExtensions merges the given list of extensions to produce a new request with the combination of the old request // plus the new extensions. When an old extension and a new extension are both present, mergeFunc is called to produce // the result -func (gsr GraphSyncRequest) MergeExtensions(extensions []graphsync.ExtensionData, mergeFunc func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error)) (GraphSyncRequest, error) { +func (gsr GraphSyncRequest) MergeExtensions(extensions []graphsync.ExtensionData, mergeFunc func(name graphsync.ExtensionName, oldData datamodel.Node, newData datamodel.Node) (datamodel.Node, error)) (GraphSyncRequest, error) { if gsr.extensions == nil { - return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.isCancel, gsr.isUpdate, toExtensionsMap(extensions)), nil + return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.requestType, toExtensionsMap(extensions)), nil } newExtensionMap := toExtensionsMap(extensions) - combinedExtensions := make(map[string][]byte) + combinedExtensions := make(map[string]datamodel.Node) for name, newData := range newExtensionMap { oldData, ok := gsr.extensions[name] if !ok { @@ -464,5 +401,5 @@ func (gsr GraphSyncRequest) MergeExtensions(extensions []graphsync.ExtensionData } combinedExtensions[name] = oldData } - return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.isCancel, gsr.isUpdate, combinedExtensions), nil + return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.requestType, combinedExtensions), nil } diff --git a/message/pb/message.pb.go b/message/pb/message.pb.go index 7110c0b1..cdab7196 100644 --- a/message/pb/message.pb.go +++ b/message/pb/message.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.1 // source: message.proto package graphsync_message_pb diff --git a/message/v1/message.go b/message/v1/message.go new file mode 100644 index 00000000..0554c355 --- /dev/null +++ b/message/v1/message.go @@ -0,0 +1,344 @@ +package v1 + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "sync" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" + pool "github.com/libp2p/go-buffer-pool" + "github.com/libp2p/go-libp2p-core/network" + "github.com/libp2p/go-libp2p-core/peer" + "github.com/libp2p/go-msgio" + "google.golang.org/protobuf/proto" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/ipldutil" + "github.com/ipfs/go-graphsync/message" + pb "github.com/ipfs/go-graphsync/message/pb" + "github.com/ipfs/go-graphsync/message/v1/metadata" +) + +const extensionMetadata = string("graphsync/response-metadata") + +type v1RequestKey struct { + p peer.ID + id int32 +} + +// MessageHandler is used to hold per-peer state for each connection. For the v1 +// protocol, we need to maintain a mapping of old style integer RequestIDs and +// the newer UUID forms. This happens on a per-peer basis and needs to work +// for both incoming and outgoing messages. +type MessageHandler struct { + mapLock sync.Mutex + // each host can have multiple peerIDs, so our integer requestID mapping for + // protocol v1.0.0 needs to be a combo of peerID and requestID + fromV1Map map[v1RequestKey]graphsync.RequestID + toV1Map map[graphsync.RequestID]int32 + nextIntId int32 +} + +// NewMessageHandler instantiates a new MessageHandler instance +func NewMessageHandler() *MessageHandler { + return &MessageHandler{ + fromV1Map: make(map[v1RequestKey]graphsync.RequestID), + toV1Map: make(map[graphsync.RequestID]int32), + } +} + +// FromNet can read a v1.0.0 network stream to deserialized a GraphSyncMessage +func (mh *MessageHandler) FromNet(p peer.ID, r io.Reader) (message.GraphSyncMessage, error) { + reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) + return mh.FromMsgReader(p, reader) +} + +// FromMsgReader can deserialize a v1.0.0 protobuf message into a GraphySyncMessage +func (mh *MessageHandler) FromMsgReader(p peer.ID, r msgio.Reader) (message.GraphSyncMessage, error) { + msg, err := r.ReadMsg() + if err != nil { + return message.GraphSyncMessage{}, err + } + + var pb pb.Message + err = proto.Unmarshal(msg, &pb) + r.ReleaseMsg(msg) + if err != nil { + return message.GraphSyncMessage{}, err + } + + return mh.fromProto(p, &pb) +} + +// ToNet writes a GraphSyncMessage in its v1.0.0 protobuf format to a writer +func (mh *MessageHandler) ToNet(p peer.ID, gsm message.GraphSyncMessage, w io.Writer) error { + msg, err := mh.ToProto(p, gsm) + if err != nil { + return err + } + size := proto.Size(msg) + buf := pool.Get(size + binary.MaxVarintLen64) + defer pool.Put(buf) + + n := binary.PutUvarint(buf, uint64(size)) + + out, err := proto.MarshalOptions{}.MarshalAppend(buf[:n], msg) + if err != nil { + return err + } + _, err = w.Write(out) + return err +} + +// toProto converts a GraphSyncMessage to its pb.Message equivalent +func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb.Message, error) { + mh.mapLock.Lock() + defer mh.mapLock.Unlock() + + pbm := new(pb.Message) + requests := gsm.Requests() + pbm.Requests = make([]*pb.Message_Request, 0, len(requests)) + for _, request := range requests { + var selector []byte + var err error + if request.Selector() != nil { + selector, err = ipldutil.EncodeNode(request.Selector()) + if err != nil { + return nil, err + } + } + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, request.ID().Bytes()) + if err != nil { + return nil, err + } + ext, err := toEncodedExtensions(request, nil) + if err != nil { + return nil, err + } + + pbm.Requests = append(pbm.Requests, &pb.Message_Request{ + Id: rid, + Root: request.Root().Bytes(), + Selector: selector, + Priority: int32(request.Priority()), + Cancel: request.Type() == graphsync.RequestTypeCancel, + Update: request.Type() == graphsync.RequestTypeUpdate, + Extensions: ext, + }) + } + + responses := gsm.Responses() + pbm.Responses = make([]*pb.Message_Response, 0, len(responses)) + for _, response := range responses { + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.RequestID().Bytes()) + if err != nil { + return nil, err + } + ext, err := toEncodedExtensions(response, response.Metadata()) + if err != nil { + return nil, err + } + pbm.Responses = append(pbm.Responses, &pb.Message_Response{ + Id: rid, + Status: int32(response.Status()), + Extensions: ext, + }) + } + + blocks := gsm.Blocks() + pbm.Data = make([]*pb.Message_Block, 0, len(blocks)) + for _, b := range blocks { + pbm.Data = append(pbm.Data, &pb.Message_Block{ + Prefix: b.Cid().Prefix().Bytes(), + Data: b.RawData(), + }) + } + return pbm, nil +} + +// Mapping from a pb.Message object to a GraphSyncMessage object, including +// RequestID (int / uuid) mapping. +func (mh *MessageHandler) fromProto(p peer.ID, pbm *pb.Message) (message.GraphSyncMessage, error) { + mh.mapLock.Lock() + defer mh.mapLock.Unlock() + + requests := make(map[graphsync.RequestID]message.GraphSyncRequest, len(pbm.GetRequests())) + for _, req := range pbm.Requests { + if req == nil { + return message.GraphSyncMessage{}, errors.New("request is nil") + } + var err error + + id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, req.Id) + if err != nil { + return message.GraphSyncMessage{}, err + } + + if req.Cancel { + requests[id] = message.NewCancelRequest(id) + continue + } + + exts, metadata, err := fromEncodedExtensions(req.GetExtensions()) + if err != nil { + return message.GraphSyncMessage{}, err + } + + if metadata != nil { + return message.GraphSyncMessage{}, fmt.Errorf("received unexpected metadata in request extensions for request id: %s", id) + } + + if req.Update { + requests[id] = message.NewUpdateRequest(id, exts...) + continue + } + + root, err := cid.Cast(req.Root) + if err != nil { + return message.GraphSyncMessage{}, err + } + + selector, err := ipldutil.DecodeNode(req.Selector) + if err != nil { + return message.GraphSyncMessage{}, err + } + + requests[id] = message.NewRequest(id, root, selector, graphsync.Priority(req.Priority), exts...) + } + + responses := make(map[graphsync.RequestID]message.GraphSyncResponse, len(pbm.GetResponses())) + for _, res := range pbm.Responses { + if res == nil { + return message.GraphSyncMessage{}, errors.New("response is nil") + } + id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, res.Id) + if err != nil { + return message.GraphSyncMessage{}, err + } + exts, metadata, err := fromEncodedExtensions(res.GetExtensions()) + if err != nil { + return message.GraphSyncMessage{}, err + } + responses[id] = message.NewResponse(id, graphsync.ResponseStatusCode(res.Status), metadata, exts...) + } + + blks := make(map[cid.Cid]blocks.Block, len(pbm.GetData())) + for _, b := range pbm.GetData() { + if b == nil { + return message.GraphSyncMessage{}, errors.New("block is nil") + } + + pref, err := cid.PrefixFromBytes(b.GetPrefix()) + if err != nil { + return message.GraphSyncMessage{}, err + } + + c, err := pref.Sum(b.GetData()) + if err != nil { + return message.GraphSyncMessage{}, err + } + + blk, err := blocks.NewBlockWithCid(b.GetData(), c) + if err != nil { + return message.GraphSyncMessage{}, err + } + + blks[blk.Cid()] = blk + } + + return message.NewMessage(requests, responses, blks), nil +} + +// Note that even for protocol v1 we now only support DAG-CBOR encoded extension data. +// Anything else will be rejected with an error. +func toEncodedExtensions(part message.MessagePartWithExtensions, linkMetadata graphsync.LinkMetadata) (map[string][]byte, error) { + names := part.ExtensionNames() + out := make(map[string][]byte, len(names)) + for _, name := range names { + data, ok := part.Extension(graphsync.ExtensionName(name)) + if !ok || data == nil { + out[string(name)] = nil + } else { + byts, err := ipldutil.EncodeNode(data) + if err != nil { + return nil, err + } + out[string(name)] = byts + } + } + if linkMetadata != nil { + md := make(metadata.Metadata, 0) + linkMetadata.Iterate(func(c cid.Cid, la graphsync.LinkAction) { + md = append(md, metadata.Item{Link: c, BlockPresent: la == graphsync.LinkActionPresent}) + }) + mdNode := metadata.EncodeMetadata(md) + mdByts, err := ipldutil.EncodeNode(mdNode) + if err != nil { + return nil, err + } + out[extensionMetadata] = mdByts + } + return out, nil +} + +func fromEncodedExtensions(in map[string][]byte) ([]graphsync.ExtensionData, []message.GraphSyncLinkMetadatum, error) { + if in == nil { + return []graphsync.ExtensionData{}, nil, nil + } + out := make([]graphsync.ExtensionData, 0, len(in)) + var md []message.GraphSyncLinkMetadatum + for name, data := range in { + var node datamodel.Node + var err error + if len(data) > 0 { + node, err = ipldutil.DecodeNode(data) + if err != nil { + return nil, nil, err + } + if name == string(extensionMetadata) { + mdd, err := metadata.DecodeMetadata(node) + if err != nil { + return nil, nil, err + } + md = mdd.ToGraphSyncMetadata() + } else { + out = append(out, graphsync.ExtensionData{Name: graphsync.ExtensionName(name), Data: node}) + } + } + } + return out, md, nil +} + +// Maps a []byte slice form of a RequestID (uuid) to an integer format as used +// by a v1 peer. Inverse of intIdToRequestId() +func bytesIdToInt(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, toV1Map map[graphsync.RequestID]int32, nextIntId *int32, id []byte) (int32, error) { + rid, err := graphsync.ParseRequestID(id) + if err != nil { + return 0, err + } + iid, ok := toV1Map[rid] + if !ok { + iid = *nextIntId + *nextIntId++ + toV1Map[rid] = iid + fromV1Map[v1RequestKey{p, iid}] = rid + } + return iid, nil +} + +// Maps an integer form of a RequestID as used by a v1 peer to a native (uuid) form. +// Inverse of bytesIdToInt(). +func intIdToRequestId(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, toV1Map map[graphsync.RequestID]int32, iid int32) (graphsync.RequestID, error) { + key := v1RequestKey{p, iid} + rid, ok := fromV1Map[key] + if !ok { + rid = graphsync.NewRequestID() + fromV1Map[key] = rid + toV1Map[rid] = iid + } + return rid, nil +} diff --git a/message/message_test.go b/message/v1/message_test.go similarity index 75% rename from message/message_test.go rename to message/v1/message_test.go index 135342d3..22399b8a 100644 --- a/message/message_test.go +++ b/message/v1/message_test.go @@ -1,4 +1,4 @@ -package message +package v1 import ( "bytes" @@ -8,12 +8,15 @@ import ( blocks "github.com/ipfs/go-block-format" cid "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/traversal/selector/builder" + "github.com/libp2p/go-libp2p-core/peer" "github.com/stretchr/testify/require" "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/ipldutil" + "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/testutil" ) @@ -21,16 +24,16 @@ func TestAppendingRequests(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) - builder := NewBuilder() - builder.AddRequest(NewRequest(id, root, selector, priority, extension)) + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority, extension)) gsm, err := builder.Build() require.NoError(t, err) requests := gsm.Requests() @@ -38,28 +41,34 @@ func TestAppendingRequests(t *testing.T) { request := requests[0] extensionData, found := request.Extension(extensionName) require.Equal(t, id, request.ID()) - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority, request.Priority()) require.Equal(t, root.String(), request.Root().String()) require.Equal(t, selector, request.Selector()) require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := gsm.ToProto() + mh := NewMessageHandler() + + pbMessage, err := mh.ToProto(peer.ID("foo"), gsm) require.NoError(t, err, "serialize to protobuf errored") selectorEncoded, err := ipldutil.EncodeNode(selector) require.NoError(t, err) pbRequest := pbMessage.Requests[0] - require.Equal(t, int32(id), pbRequest.Id) require.Equal(t, int32(priority), pbRequest.Priority) require.False(t, pbRequest.Cancel) require.False(t, pbRequest.Update) require.Equal(t, root.Bytes(), pbRequest.Root) require.Equal(t, selectorEncoded, pbRequest.Selector) - require.Equal(t, map[string][]byte{"graphsync/awesome": extension.Data}, pbRequest.Extensions) - - deserialized, err := newMessageFromProto(pbMessage) + require.Equal(t, 1, len(pbRequest.Extensions)) + byts, _ := extension.Data.AsBytes() + expectedByts := append([]byte{88, 100}, byts...) + actualByts, ok := pbRequest.Extensions["graphsync/awesome"] + require.True(t, ok) + require.Equal(t, expectedByts, actualByts) + + deserialized, err := mh.fromProto(peer.ID("foo"), pbMessage) require.NoError(t, err, "deserializing protobuf message errored") deserializedRequests := deserialized.Requests() require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") @@ -67,8 +76,7 @@ func TestAppendingRequests(t *testing.T) { deserializedRequest := deserializedRequests[0] extensionData, found = deserializedRequest.Extension(extensionName) require.Equal(t, id, deserializedRequest.ID()) - require.False(t, deserializedRequest.IsCancel()) - require.False(t, deserializedRequest.IsUpdate()) + require.Equal(t, deserializedRequest.Type(), graphsync.RequestTypeNew) require.Equal(t, priority, deserializedRequest.Priority()) require.Equal(t, root.String(), deserializedRequest.Root().String()) require.Equal(t, selector, deserializedRequest.Selector()) @@ -80,12 +88,14 @@ func TestAppendingResponses(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewString("test extension data"), } - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() + p := peer.ID("test peer") + mh := NewMessageHandler() status := graphsync.RequestAcknowledged - builder := NewBuilder() + builder := message.NewBuilder() builder.AddResponseCode(requestID, status) builder.AddExtensionData(requestID, extension) gsm, err := builder.Build() @@ -99,14 +109,14 @@ func TestAppendingResponses(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := gsm.ToProto() + pbMessage, err := mh.ToProto(p, gsm) require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] - require.Equal(t, int32(requestID), pbResponse.Id) + // no longer equal: require.Equal(t, requestID.Bytes(), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) - require.Equal(t, extension.Data, pbResponse.Extensions["graphsync/awesome"]) + require.Equal(t, []byte("stest extension data"), pbResponse.Extensions["graphsync/awesome"]) - deserialized, err := newMessageFromProto(pbMessage) + deserialized, err := mh.fromProto(p, pbMessage) require.NoError(t, err, "deserializing protobuf message errored") deserializedResponses := deserialized.Responses() require.Len(t, deserializedResponses, 1, "did not add response to deserialized message") @@ -124,7 +134,7 @@ func TestAppendBlock(t *testing.T) { strs = append(strs, "Celeritas") strs = append(strs, "Incendia") - builder := NewBuilder() + builder := message.NewBuilder() for _, str := range strs { block := blocks.NewBlock([]byte(str)) builder.AddBlock(block) @@ -132,7 +142,7 @@ func TestAppendBlock(t *testing.T) { m, err := builder.Build() require.NoError(t, err) - pbMessage, err := m.ToProto() + pbMessage, err := NewMessageHandler().ToProto(peer.ID("foo"), m) require.NoError(t, err, "serializing to protobuf errored") // assert strings are in proto message @@ -154,13 +164,13 @@ func contains(strs []string, x string) bool { func TestRequestCancel(t *testing.T) { ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) root := testutil.GenerateCids(1)[0] - builder := NewBuilder() - builder.AddRequest(NewRequest(id, root, selector, priority)) - builder.AddRequest(CancelRequest(id)) + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority)) + builder.AddRequest(message.NewCancelRequest(id)) gsm, err := builder.Build() require.NoError(t, err) @@ -168,31 +178,33 @@ func TestRequestCancel(t *testing.T) { require.Len(t, requests, 1, "did not add cancel request") request := requests[0] require.Equal(t, id, request.ID()) - require.True(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeCancel) + + mh := NewMessageHandler() buf := new(bytes.Buffer) - err = gsm.ToNet(buf) + err = mh.ToNet(peer.ID("foo"), gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := FromNet(buf) + deserialized, err := mh.FromNet(peer.ID("foo"), buf) require.NoError(t, err, "did not deserialize protobuf message") deserializedRequests := deserialized.Requests() require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") deserializedRequest := deserializedRequests[0] require.Equal(t, request.ID(), deserializedRequest.ID()) - require.Equal(t, request.IsCancel(), deserializedRequest.IsCancel()) + require.Equal(t, request.Type(), deserializedRequest.Type()) } func TestRequestUpdate(t *testing.T) { - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } - builder := NewBuilder() - builder.AddRequest(UpdateRequest(id, extension)) + builder := message.NewBuilder() + builder.AddRequest(message.NewUpdateRequest(id, extension)) gsm, err := builder.Build() require.NoError(t, err) @@ -200,16 +212,17 @@ func TestRequestUpdate(t *testing.T) { require.Len(t, requests, 1, "did not add cancel request") request := requests[0] require.Equal(t, id, request.ID()) - require.True(t, request.IsUpdate()) - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeUpdate) extensionData, found := request.Extension(extensionName) require.True(t, found) require.Equal(t, extension.Data, extensionData) + mh := NewMessageHandler() + buf := new(bytes.Buffer) - err = gsm.ToNet(buf) + err = mh.ToNet(peer.ID("foo"), gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := FromNet(buf) + deserialized, err := mh.FromNet(peer.ID("foo"), buf) require.NoError(t, err, "did not deserialize protobuf message") deserializedRequests := deserialized.Requests() @@ -217,8 +230,7 @@ func TestRequestUpdate(t *testing.T) { deserializedRequest := deserializedRequests[0] extensionData, found = deserializedRequest.Extension(extensionName) require.Equal(t, request.ID(), deserializedRequest.ID()) - require.Equal(t, request.IsCancel(), deserializedRequest.IsCancel()) - require.Equal(t, request.IsUpdate(), deserializedRequest.IsUpdate()) + require.Equal(t, request.Type(), deserializedRequest.Type()) require.Equal(t, request.Priority(), deserializedRequest.Priority()) require.Equal(t, request.Root().String(), deserializedRequest.Root().String()) require.Equal(t, request.Selector(), deserializedRequest.Selector()) @@ -233,14 +245,14 @@ func TestToNetFromNetEquivalency(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) status := graphsync.RequestAcknowledged - builder := NewBuilder() - builder.AddRequest(NewRequest(id, root, selector, priority, extension)) + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority, extension)) builder.AddResponseCode(id, status) builder.AddExtensionData(id, extension) builder.AddBlock(blocks.NewBlock([]byte("W"))) @@ -250,10 +262,12 @@ func TestToNetFromNetEquivalency(t *testing.T) { gsm, err := builder.Build() require.NoError(t, err) + mh := NewMessageHandler() + buf := new(bytes.Buffer) - err = gsm.ToNet(buf) + err = mh.ToNet(peer.ID("foo"), gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := FromNet(buf) + deserialized, err := mh.FromNet(peer.ID("foo"), buf) require.NoError(t, err, "did not deserialize protobuf message") requests := gsm.Requests() @@ -264,8 +278,7 @@ func TestToNetFromNetEquivalency(t *testing.T) { deserializedRequest := deserializedRequests[0] extensionData, found := deserializedRequest.Extension(extensionName) require.Equal(t, request.ID(), deserializedRequest.ID()) - require.False(t, deserializedRequest.IsCancel()) - require.False(t, deserializedRequest.IsUpdate()) + require.Equal(t, deserializedRequest.Type(), graphsync.RequestTypeNew) require.Equal(t, request.Priority(), deserializedRequest.Priority()) require.Equal(t, request.Root().String(), deserializedRequest.Root().String()) require.Equal(t, request.Selector(), deserializedRequest.Selector()) @@ -302,34 +315,42 @@ func TestMergeExtensions(t *testing.T) { initialExtensions := []graphsync.ExtensionData{ { Name: extensionName1, - Data: []byte("applesauce"), + Data: basicnode.NewString("applesauce"), }, { Name: extensionName2, - Data: []byte("hello"), + Data: basicnode.NewString("hello"), }, } replacementExtensions := []graphsync.ExtensionData{ { Name: extensionName2, - Data: []byte("world"), + Data: basicnode.NewString("world"), }, { Name: extensionName3, - Data: []byte("cheese"), + Data: basicnode.NewString("cheese"), }, } - defaultMergeFunc := func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { - return []byte(string(oldData) + " " + string(newData)), nil + defaultMergeFunc := func(name graphsync.ExtensionName, oldData datamodel.Node, newData datamodel.Node) (datamodel.Node, error) { + os, err := oldData.AsString() + if err != nil { + return nil, err + } + ns, err := newData.AsString() + if err != nil { + return nil, err + } + return basicnode.NewString(os + " " + ns), nil } root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) - defaultRequest := NewRequest(id, root, selector, priority, initialExtensions...) + defaultRequest := message.NewRequest(id, root, selector, priority, initialExtensions...) t.Run("when merging into empty", func(t *testing.T) { - emptyRequest := NewRequest(id, root, selector, priority) + emptyRequest := message.NewRequest(id, root, selector, priority) resultRequest, err := emptyRequest.MergeExtensions(replacementExtensions, defaultMergeFunc) require.NoError(t, err) require.Equal(t, emptyRequest.ID(), resultRequest.ID()) @@ -340,10 +361,10 @@ func TestMergeExtensions(t *testing.T) { require.False(t, has) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, []byte("world"), extData2) + require.Equal(t, basicnode.NewString("world"), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, []byte("cheese"), extData3) + require.Equal(t, basicnode.NewString("cheese"), extData3) }) t.Run("when merging two requests", func(t *testing.T) { resultRequest, err := defaultRequest.MergeExtensions(replacementExtensions, defaultMergeFunc) @@ -354,16 +375,16 @@ func TestMergeExtensions(t *testing.T) { require.Equal(t, defaultRequest.Selector(), resultRequest.Selector()) extData1, has := resultRequest.Extension(extensionName1) require.True(t, has) - require.Equal(t, []byte("applesauce"), extData1) + require.Equal(t, basicnode.NewString("applesauce"), extData1) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, []byte("hello world"), extData2) + require.Equal(t, basicnode.NewString("hello world"), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, []byte("cheese"), extData3) + require.Equal(t, basicnode.NewString("cheese"), extData3) }) t.Run("when merging errors", func(t *testing.T) { - errorMergeFunc := func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { + errorMergeFunc := func(name graphsync.ExtensionName, oldData datamodel.Node, newData datamodel.Node) (datamodel.Node, error) { return nil, errors.New("something went wrong") } _, err := defaultRequest.MergeExtensions(replacementExtensions, errorMergeFunc) @@ -377,13 +398,13 @@ func TestMergeExtensions(t *testing.T) { require.Equal(t, defaultRequest.Selector(), resultRequest.Selector()) extData1, has := resultRequest.Extension(extensionName1) require.True(t, has) - require.Equal(t, []byte("applesauce"), extData1) + require.Equal(t, basicnode.NewString("applesauce"), extData1) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, []byte("world"), extData2) + require.Equal(t, basicnode.NewString("world"), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, []byte("cheese"), extData3) + require.Equal(t, basicnode.NewString("cheese"), extData3) }) } @@ -401,18 +422,20 @@ func TestKnownFuzzIssues(t *testing.T) { " \n\v ", "\x0600\x1a\x02\x180", } + p := peer.ID("test peer") + mh := NewMessageHandler() for _, input := range inputs { //inputAsBytes, err := hex.DecodeString(input) ///require.NoError(t, err) - msg1, err := FromNet(bytes.NewReader([]byte(input))) + msg1, err := mh.FromNet(p, bytes.NewReader([]byte(input))) if err != nil { continue } buf2 := new(bytes.Buffer) - err = msg1.ToNet(buf2) + err = mh.ToNet(p, msg1, buf2) require.NoError(t, err) - msg2, err := FromNet(buf2) + msg2, err := mh.FromNet(p, buf2) require.NoError(t, err) require.Equal(t, msg1, msg2) diff --git a/message/v1/metadata/metadata.go b/message/v1/metadata/metadata.go new file mode 100644 index 00000000..2e0bf487 --- /dev/null +++ b/message/v1/metadata/metadata.go @@ -0,0 +1,54 @@ +package metadata + +import ( + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" +) + +// Item is a single link traversed in a repsonse +type Item struct { + Link cid.Cid + BlockPresent bool +} + +// Metadata is information about metadata contained in a response, which can be +// serialized back and forth to bytes +type Metadata []Item + +func (md Metadata) ToGraphSyncMetadata() []message.GraphSyncLinkMetadatum { + if len(md) == 0 { + return nil + } + gsm := make([]message.GraphSyncLinkMetadatum, 0, len(md)) + for _, ii := range md { + action := graphsync.LinkActionPresent + if !ii.BlockPresent { + action = graphsync.LinkActionMissing + } + gsm = append(gsm, message.GraphSyncLinkMetadatum{Link: ii.Link, Action: action}) + } + return gsm +} + +// DecodeMetadata assembles metadata from a raw byte array, first deserializing +// as a node and then assembling into a metadata struct. +func DecodeMetadata(data datamodel.Node) (Metadata, error) { + if data == nil { + return nil, nil + } + builder := Prototype.Metadata.Representation().NewBuilder() + err := builder.AssignNode(data) + if err != nil { + return nil, err + } + metadata := bindnode.Unwrap(builder.Build()).(*Metadata) + return *metadata, nil +} + +// EncodeMetadata encodes metadata to an IPLD node then serializes to raw bytes +func EncodeMetadata(entries Metadata) datamodel.Node { + return bindnode.Wrap(&entries, Prototype.Metadata.Type()) +} diff --git a/metadata/metadata_test.go b/message/v1/metadata/metadata_test.go similarity index 61% rename from metadata/metadata_test.go rename to message/v1/metadata/metadata_test.go index bc7fdb60..6a7abb38 100644 --- a/metadata/metadata_test.go +++ b/message/v1/metadata/metadata_test.go @@ -1,11 +1,9 @@ package metadata import ( - "bytes" "math/rand" "testing" - "github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/ipld/go-ipld-prime/fluent" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" @@ -29,29 +27,14 @@ func TestDecodeEncodeMetadata(t *testing.T) { }) // verify metadata matches - encoded, err := EncodeMetadata(initialMetadata) - require.NoError(t, err, "encode errored") + encoded := EncodeMetadata(initialMetadata) decodedMetadata, err := DecodeMetadata(encoded) require.NoError(t, err, "decode errored") require.Equal(t, initialMetadata, decodedMetadata, "metadata changed during encoding and decoding") // verify metadata is equivalent of IPLD node encoding - encodedNode := new(bytes.Buffer) - err = dagcbor.Encode(nd, encodedNode) - require.NoError(t, err) - decodedMetadataFromNode, err := DecodeMetadata(encodedNode.Bytes()) + decodedMetadataFromNode, err := DecodeMetadata(nd) require.NoError(t, err) require.Equal(t, decodedMetadata, decodedMetadataFromNode, "metadata not equal to IPLD encoding") - - nb := basicnode.Prototype.List.NewBuilder() - err = dagcbor.Decode(nb, encodedNode) - require.NoError(t, err) - decodedNode := nb.Build() - require.Equal(t, nd, decodedNode) - nb = basicnode.Prototype.List.NewBuilder() - err = dagcbor.Decode(nb, bytes.NewReader(encoded)) - require.NoError(t, err) - decodedNodeFromMetadata := nb.Build() - require.Equal(t, decodedNode, decodedNodeFromMetadata, "deserialzed metadata does not match deserialized node") } diff --git a/message/v1/metadata/schema.go b/message/v1/metadata/schema.go new file mode 100644 index 00000000..afdf8d45 --- /dev/null +++ b/message/v1/metadata/schema.go @@ -0,0 +1,25 @@ +package metadata + +import ( + _ "embed" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/schema" +) + +//go:embed schema.ipldsch +var embedSchema []byte + +var Prototype struct { + Metadata schema.TypedPrototype +} + +func init() { + ts, err := ipld.LoadSchemaBytes(embedSchema) + if err != nil { + panic(err) + } + + Prototype.Metadata = bindnode.Prototype((*Metadata)(nil), ts.TypeByName("Metadata")) +} diff --git a/message/v1/metadata/schema.ipldsch b/message/v1/metadata/schema.ipldsch new file mode 100644 index 00000000..20e6c688 --- /dev/null +++ b/message/v1/metadata/schema.ipldsch @@ -0,0 +1,6 @@ +type Item struct { + link Link + blockPresent Bool +} representation map + +type Metadata [Item] diff --git a/message/v1/pb_roundtrip_test.go b/message/v1/pb_roundtrip_test.go new file mode 100644 index 00000000..c38224e2 --- /dev/null +++ b/message/v1/pb_roundtrip_test.go @@ -0,0 +1,127 @@ +package v1 + +import ( + "testing" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + pb "github.com/ipfs/go-graphsync/message/pb" + "github.com/ipfs/go-graphsync/testutil" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" + selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" + "github.com/libp2p/go-libp2p-core/peer" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestIPLDRoundTrip(t *testing.T) { + id1 := graphsync.NewRequestID() + id2 := graphsync.NewRequestID() + root1, _ := cid.Decode("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") + root2, _ := cid.Decode("bafyreibdoxfay27gf4ye3t5a7aa5h4z2azw7hhhz36qrbf5qleldj76qfy") + extension1Data := basicnode.NewString("yee haw") + extension1Name := graphsync.ExtensionName("AppleSauce/McGee") + extension1 := graphsync.ExtensionData{ + Name: extension1Name, + Data: extension1Data, + } + extension2Data := basicnode.NewBytes(testutil.RandomBytes(100)) + extension2Name := graphsync.ExtensionName("Hippity+Hoppity") + extension2 := graphsync.ExtensionData{ + Name: extension2Name, + Data: extension2Data, + } + + requests := map[graphsync.RequestID]message.GraphSyncRequest{ + id1: message.NewRequest(id1, root1, selectorparse.CommonSelector_MatchAllRecursively, graphsync.Priority(101), extension1), + id2: message.NewRequest(id2, root2, selectorparse.CommonSelector_ExploreAllRecursively, graphsync.Priority(202)), + } + + metadata := message.GraphSyncLinkMetadatum{Link: root2, Action: graphsync.LinkActionMissing} + + responses := map[graphsync.RequestID]message.GraphSyncResponse{ + id1: message.NewResponse(id1, graphsync.RequestFailedContentNotFound, []message.GraphSyncLinkMetadatum{metadata}), + id2: message.NewResponse(id2, graphsync.PartialResponse, nil, extension2), + } + + blks := testutil.GenerateBlocksOfSize(2, 100) + + blocks := map[cid.Cid]blocks.Block{ + blks[0].Cid(): blks[0], + blks[1].Cid(): blks[1], + } + + mh := NewMessageHandler() + p := peer.ID("blip") + + // message format + gsm := message.NewMessage(requests, responses, blocks) + // proto internal format + pgsm, err := mh.ToProto(p, gsm) + require.NoError(t, err) + + // protobuf binary format + buf, err := proto.Marshal(pgsm) + require.NoError(t, err) + + // back to proto internal format + var rtpgsm pb.Message + err = proto.Unmarshal(buf, &rtpgsm) + require.NoError(t, err) + + // back to bindnode internal format + rtgsm, err := mh.fromProto(p, &rtpgsm) + require.NoError(t, err) + + rtreq := rtgsm.Requests() + require.Len(t, rtreq, 2) + rtreqmap := map[graphsync.RequestID]message.GraphSyncRequest{ + rtreq[0].ID(): rtreq[0], + rtreq[1].ID(): rtreq[1], + } + require.NotNil(t, rtreqmap[id1]) + require.NotNil(t, rtreqmap[id2]) + require.Equal(t, rtreqmap[id1].ID(), id1) + require.Equal(t, rtreqmap[id2].ID(), id2) + require.Equal(t, rtreqmap[id1].Root(), root1) + require.Equal(t, rtreqmap[id2].Root(), root2) + require.True(t, datamodel.DeepEqual(rtreqmap[id1].Selector(), selectorparse.CommonSelector_MatchAllRecursively)) + require.True(t, datamodel.DeepEqual(rtreqmap[id2].Selector(), selectorparse.CommonSelector_ExploreAllRecursively)) + require.Equal(t, rtreqmap[id1].Priority(), graphsync.Priority(101)) + require.Equal(t, rtreqmap[id2].Priority(), graphsync.Priority(202)) + require.Len(t, rtreqmap[id1].ExtensionNames(), 1) + require.Empty(t, rtreqmap[id2].ExtensionNames()) + rtext1, exists := rtreqmap[id1].Extension(extension1Name) + require.True(t, exists) + require.True(t, datamodel.DeepEqual(rtext1, extension1Data)) + + rtres := rtgsm.Responses() + require.Len(t, rtres, 2) + require.Len(t, rtreq, 2) + rtresmap := map[graphsync.RequestID]message.GraphSyncResponse{ + rtres[0].RequestID(): rtres[0], + rtres[1].RequestID(): rtres[1], + } + require.NotNil(t, rtresmap[id1]) + require.NotNil(t, rtresmap[id2]) + require.Equal(t, rtresmap[id1].Status(), graphsync.RequestFailedContentNotFound) + require.Equal(t, rtresmap[id2].Status(), graphsync.PartialResponse) + gslm1, ok := rtresmap[id1].Metadata().(message.GraphSyncLinkMetadata) + require.True(t, ok) + require.Len(t, gslm1.RawMetadata(), 1) + require.Equal(t, gslm1.RawMetadata()[0], metadata) + gslm2, ok := rtresmap[id2].Metadata().(message.GraphSyncLinkMetadata) + require.True(t, ok) + require.Empty(t, gslm2.RawMetadata()) + require.Empty(t, rtresmap[id1].ExtensionNames()) + require.Len(t, rtresmap[id2].ExtensionNames(), 1) + rtext2, exists := rtresmap[id2].Extension(extension2Name) + require.True(t, exists) + require.True(t, datamodel.DeepEqual(rtext2, extension2Data)) + + rtblks := rtgsm.Blocks() + require.Len(t, rtblks, 2) +} diff --git a/message/v2/ipld_roundtrip_test.go b/message/v2/ipld_roundtrip_test.go new file mode 100644 index 00000000..b8e0b921 --- /dev/null +++ b/message/v2/ipld_roundtrip_test.go @@ -0,0 +1,131 @@ +package v2 + +import ( + "bytes" + "testing" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + "github.com/ipfs/go-graphsync/message/ipldbind" + "github.com/ipfs/go-graphsync/testutil" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/bindnode" + selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" + "github.com/stretchr/testify/require" +) + +func TestIPLDRoundTrip(t *testing.T) { + id1 := graphsync.NewRequestID() + id2 := graphsync.NewRequestID() + root1, _ := cid.Decode("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") + root2, _ := cid.Decode("bafyreibdoxfay27gf4ye3t5a7aa5h4z2azw7hhhz36qrbf5qleldj76qfy") + extension1Data := basicnode.NewString("yee haw") + extension1Name := graphsync.ExtensionName("AppleSauce/McGee") + extension1 := graphsync.ExtensionData{ + Name: extension1Name, + Data: extension1Data, + } + extension2Data := basicnode.NewBytes(testutil.RandomBytes(100)) + extension2Name := graphsync.ExtensionName("Hippity+Hoppity") + extension2 := graphsync.ExtensionData{ + Name: extension2Name, + Data: extension2Data, + } + + requests := map[graphsync.RequestID]message.GraphSyncRequest{ + id1: message.NewRequest(id1, root1, selectorparse.CommonSelector_MatchAllRecursively, graphsync.Priority(101), extension1), + id2: message.NewRequest(id2, root2, selectorparse.CommonSelector_ExploreAllRecursively, graphsync.Priority(202)), + } + + metadata := message.GraphSyncLinkMetadatum{Link: root2, Action: graphsync.LinkActionMissing} + + responses := map[graphsync.RequestID]message.GraphSyncResponse{ + id1: message.NewResponse(id1, graphsync.RequestFailedContentNotFound, []message.GraphSyncLinkMetadatum{metadata}), + id2: message.NewResponse(id2, graphsync.PartialResponse, nil, extension2), + } + + blks := testutil.GenerateBlocksOfSize(2, 100) + + blocks := map[cid.Cid]blocks.Block{ + blks[0].Cid(): blks[0], + blks[1].Cid(): blks[1], + } + + // message format + gsm := message.NewMessage(requests, responses, blocks) + // bindnode internal format + igsm, err := NewMessageHandler().toIPLD(gsm) + require.NoError(t, err) + + // ipld TypedNode format + var buf bytes.Buffer + node := bindnode.Wrap(igsm, ipldbind.Prototype.Message.Type()) + + // dag-cbor binary format + err = dagcbor.Encode(node.Representation(), &buf) + require.NoError(t, err) + + // back to bindnode internal format + builder := ipldbind.Prototype.Message.Representation().NewBuilder() + err = dagcbor.Decode(builder, &buf) + require.NoError(t, err) + rtnode := builder.Build() + rtigsm := bindnode.Unwrap(rtnode).(*ipldbind.GraphSyncMessageRoot) + + // back to message format + rtgsm, err := NewMessageHandler().fromIPLD(rtigsm) + require.NoError(t, err) + + rtreq := rtgsm.Requests() + require.Len(t, rtreq, 2) + rtreqmap := map[graphsync.RequestID]message.GraphSyncRequest{ + rtreq[0].ID(): rtreq[0], + rtreq[1].ID(): rtreq[1], + } + require.NotNil(t, rtreqmap[id1]) + require.NotNil(t, rtreqmap[id2]) + require.Equal(t, rtreqmap[id1].ID(), id1) + require.Equal(t, rtreqmap[id2].ID(), id2) + require.Equal(t, rtreqmap[id1].Root(), root1) + require.Equal(t, rtreqmap[id2].Root(), root2) + require.True(t, datamodel.DeepEqual(rtreqmap[id1].Selector(), selectorparse.CommonSelector_MatchAllRecursively)) + require.True(t, datamodel.DeepEqual(rtreqmap[id2].Selector(), selectorparse.CommonSelector_ExploreAllRecursively)) + require.Equal(t, rtreqmap[id1].Priority(), graphsync.Priority(101)) + require.Equal(t, rtreqmap[id2].Priority(), graphsync.Priority(202)) + require.Len(t, rtreqmap[id1].ExtensionNames(), 1) + require.Empty(t, rtreqmap[id2].ExtensionNames()) + rtext1, exists := rtreqmap[id1].Extension(extension1Name) + require.True(t, exists) + require.True(t, datamodel.DeepEqual(rtext1, extension1Data)) + + rtres := rtgsm.Responses() + require.Len(t, rtres, 2) + require.Len(t, rtreq, 2) + rtresmap := map[graphsync.RequestID]message.GraphSyncResponse{ + rtres[0].RequestID(): rtres[0], + rtres[1].RequestID(): rtres[1], + } + require.NotNil(t, rtresmap[id1]) + require.NotNil(t, rtresmap[id2]) + require.Equal(t, rtresmap[id1].Status(), graphsync.RequestFailedContentNotFound) + require.Equal(t, rtresmap[id2].Status(), graphsync.PartialResponse) + gslm1, ok := rtresmap[id1].Metadata().(message.GraphSyncLinkMetadata) + require.True(t, ok) + require.Len(t, gslm1.RawMetadata(), 1) + require.Equal(t, gslm1.RawMetadata()[0], metadata) + gslm2, ok := rtresmap[id2].Metadata().(message.GraphSyncLinkMetadata) + require.True(t, ok) + require.Empty(t, gslm2.RawMetadata()) + require.Empty(t, rtresmap[id1].ExtensionNames()) + require.Len(t, rtresmap[id2].ExtensionNames(), 1) + rtext2, exists := rtresmap[id2].Extension(extension2Name) + require.True(t, exists) + require.True(t, datamodel.DeepEqual(rtext2, extension2Data)) + + rtblks := rtgsm.Blocks() + require.Len(t, rtblks, 2) +} diff --git a/message/v2/message.go b/message/v2/message.go new file mode 100644 index 00000000..8b47efb3 --- /dev/null +++ b/message/v2/message.go @@ -0,0 +1,251 @@ +package v2 + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/libp2p/go-libp2p-core/network" + "github.com/libp2p/go-libp2p-core/peer" + "github.com/libp2p/go-msgio" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + "github.com/ipfs/go-graphsync/message/ipldbind" +) + +// MessageHandler is used to hold per-peer state for each connection. There is +// no state to hold for the v2 protocol, so this exists to provide a consistent +// interface between the protocol versions. +type MessageHandler struct{} + +// NewMessageHandler creates a new MessageHandler +func NewMessageHandler() *MessageHandler { + return &MessageHandler{} +} + +// FromNet can read a network stream to deserialized a GraphSyncMessage +func (mh *MessageHandler) FromNet(p peer.ID, r io.Reader) (message.GraphSyncMessage, error) { + reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) + return mh.FromMsgReader(p, reader) +} + +// FromMsgReader can deserialize a DAG-CBOR message into a GraphySyncMessage +func (mh *MessageHandler) FromMsgReader(_ peer.ID, r msgio.Reader) (message.GraphSyncMessage, error) { + msg, err := r.ReadMsg() + if err != nil { + return message.GraphSyncMessage{}, err + } + + builder := ipldbind.Prototype.Message.Representation().NewBuilder() + err = dagcbor.Decode(builder, bytes.NewReader(msg)) + if err != nil { + return message.GraphSyncMessage{}, err + } + node := builder.Build() + ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessageRoot) + return mh.fromIPLD(ipldGSM) +} + +// ToProto converts a GraphSyncMessage to its ipldbind.GraphSyncMessageRoot equivalent +func (mh *MessageHandler) toIPLD(gsm message.GraphSyncMessage) (*ipldbind.GraphSyncMessageRoot, error) { + ibm := new(ipldbind.GraphSyncMessage) + requests := gsm.Requests() + if len(requests) > 0 { + ibmRequests := make([]ipldbind.GraphSyncRequest, 0, len(requests)) + for _, request := range requests { + req := ipldbind.GraphSyncRequest{ + Id: request.ID().Bytes(), + RequestType: request.Type(), + Extensions: ipldbind.NewGraphSyncExtensions(request), + } + + root := request.Root() + if root != cid.Undef { + req.Root = &root + } + + selector := request.Selector() + if selector != nil { + req.Selector = &selector + } + + priority := request.Priority() + if priority != 0 { + req.Priority = &priority + } + + ibmRequests = append(ibmRequests, req) + } + ibm.Requests = &ibmRequests + } + + responses := gsm.Responses() + if len(responses) > 0 { + ibmResponses := make([]ipldbind.GraphSyncResponse, 0, len(responses)) + for _, response := range responses { + glsm, ok := response.Metadata().(message.GraphSyncLinkMetadata) + if !ok { + return nil, fmt.Errorf("unexpected metadata type") + } + + res := ipldbind.GraphSyncResponse{ + Id: response.RequestID().Bytes(), + Status: response.Status(), + Extensions: ipldbind.NewGraphSyncExtensions(response), + } + + md := glsm.RawMetadata() + if len(md) > 0 { + res.Metadata = &md + } + + ibmResponses = append(ibmResponses, res) + } + ibm.Responses = &ibmResponses + } + + blocks := gsm.Blocks() + if len(blocks) > 0 { + ibmBlocks := make([]ipldbind.GraphSyncBlock, 0, len(blocks)) + for _, b := range blocks { + ibmBlocks = append(ibmBlocks, ipldbind.GraphSyncBlock{ + Data: b.RawData(), + Prefix: b.Cid().Prefix().Bytes(), + }) + } + ibm.Blocks = &ibmBlocks + } + + return &ipldbind.GraphSyncMessageRoot{Gs2: ibm}, nil +} + +// ToNet writes a GraphSyncMessage in its DAG-CBOR format to a writer, +// prefixed with a length uvar +func (mh *MessageHandler) ToNet(_ peer.ID, gsm message.GraphSyncMessage, w io.Writer) error { + msg, err := mh.toIPLD(gsm) + if err != nil { + return err + } + + lbuf := make([]byte, binary.MaxVarintLen64) + buf := new(bytes.Buffer) + buf.Write(lbuf) + + node := bindnode.Wrap(msg, ipldbind.Prototype.Message.Type()) + err = dagcbor.Encode(node.Representation(), buf) + if err != nil { + return err + } + + lbuflen := binary.PutUvarint(lbuf, uint64(buf.Len()-binary.MaxVarintLen64)) + out := buf.Bytes() + copy(out[binary.MaxVarintLen64-lbuflen:], lbuf[:lbuflen]) + _, err = w.Write(out[binary.MaxVarintLen64-lbuflen:]) + + return err +} + +// Mapping from a ipldbind.GraphSyncMessageRoot object to a GraphSyncMessage object +func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessageRoot) (message.GraphSyncMessage, error) { + if ibm.Gs2 == nil { + return message.GraphSyncMessage{}, fmt.Errorf("invalid GraphSyncMessageRoot, no inner message") + } + + var requests map[graphsync.RequestID]message.GraphSyncRequest + if ibm.Gs2.Requests != nil { + requests = make(map[graphsync.RequestID]message.GraphSyncRequest, len(*ibm.Gs2.Requests)) + for _, req := range *ibm.Gs2.Requests { + id, err := graphsync.ParseRequestID(req.Id) + if err != nil { + return message.GraphSyncMessage{}, err + } + + if req.RequestType == graphsync.RequestTypeCancel { + requests[id] = message.NewCancelRequest(id) + continue + } + + var ext []graphsync.ExtensionData + if req.Extensions != nil { + ext = req.Extensions.ToExtensionsList() + } + + if req.RequestType == graphsync.RequestTypeUpdate { + requests[id] = message.NewUpdateRequest(id, ext...) + continue + } + + root := cid.Undef + if req.Root != nil { + root = *req.Root + } + + var selector datamodel.Node + if req.Selector != nil { + selector = *req.Selector + } + + var priority graphsync.Priority + if req.Priority != nil { + priority = graphsync.Priority(*req.Priority) + } + + requests[id] = message.NewRequest(id, root, selector, priority, ext...) + } + } + + var responses map[graphsync.RequestID]message.GraphSyncResponse + if ibm.Gs2.Responses != nil { + responses = make(map[graphsync.RequestID]message.GraphSyncResponse, len(*ibm.Gs2.Responses)) + for _, res := range *ibm.Gs2.Responses { + id, err := graphsync.ParseRequestID(res.Id) + if err != nil { + return message.GraphSyncMessage{}, err + } + + var md []message.GraphSyncLinkMetadatum + if res.Metadata != nil { + md = *res.Metadata + } + + var ext []graphsync.ExtensionData + if res.Extensions != nil { + ext = res.Extensions.ToExtensionsList() + } + + responses[id] = message.NewResponse(id, graphsync.ResponseStatusCode(res.Status), md, ext...) + } + } + + var blks map[cid.Cid]blocks.Block + if ibm.Gs2.Blocks != nil { + blks = make(map[cid.Cid]blocks.Block, len(*ibm.Gs2.Blocks)) + for _, b := range *ibm.Gs2.Blocks { + pref, err := cid.PrefixFromBytes(b.Prefix) + if err != nil { + return message.GraphSyncMessage{}, err + } + + c, err := pref.Sum(b.Data) + if err != nil { + return message.GraphSyncMessage{}, err + } + + blk, err := blocks.NewBlockWithCid(b.Data, c) + if err != nil { + return message.GraphSyncMessage{}, err + } + + blks[blk.Cid()] = blk + } + } + + return message.NewMessage(requests, responses, blks), nil +} diff --git a/message/v2/message_test.go b/message/v2/message_test.go new file mode 100644 index 00000000..b0c25426 --- /dev/null +++ b/message/v2/message_test.go @@ -0,0 +1,403 @@ +package v2 + +import ( + "bytes" + "errors" + "math/rand" + "testing" + + blocks "github.com/ipfs/go-block-format" + cid "github.com/ipfs/go-cid" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/traversal/selector/builder" + "github.com/libp2p/go-libp2p-core/peer" + "github.com/stretchr/testify/require" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + "github.com/ipfs/go-graphsync/testutil" +) + +func TestAppendingRequests(t *testing.T) { + extensionName := graphsync.ExtensionName("graphsync/awesome") + extension := graphsync.ExtensionData{ + Name: extensionName, + Data: basicnode.NewBytes(testutil.RandomBytes(100)), + } + root := testutil.GenerateCids(1)[0] + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + selector := ssb.Matcher().Node() + id := graphsync.NewRequestID() + priority := graphsync.Priority(rand.Int31()) + + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority, extension)) + gsm, err := builder.Build() + require.NoError(t, err) + requests := gsm.Requests() + require.Len(t, requests, 1, "did not add request to message") + request := requests[0] + extensionData, found := request.Extension(extensionName) + require.Equal(t, id, request.ID()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) + require.Equal(t, priority, request.Priority()) + require.Equal(t, root.String(), request.Root().String()) + require.Equal(t, selector, request.Selector()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) + + mh := NewMessageHandler() + + gsmIpld, err := mh.toIPLD(gsm) + require.NoError(t, err, "serialize to dag-cbor errored") + require.NoError(t, err) + + gsrIpld := (*gsmIpld.Gs2.Requests)[0] + require.Equal(t, priority, *gsrIpld.Priority) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) + require.Equal(t, root, *gsrIpld.Root) + require.Equal(t, selector, *gsrIpld.Selector) + require.Equal(t, 1, len(gsrIpld.Extensions.Keys)) + actualData, ok := gsrIpld.Extensions.Values["graphsync/awesome"] + require.True(t, ok) + require.Equal(t, extension.Data, actualData) + + deserialized, err := mh.fromIPLD(gsmIpld) + require.NoError(t, err, "deserializing dag-cbor message errored") + deserializedRequests := deserialized.Requests() + require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") + + deserializedRequest := deserializedRequests[0] + extensionData, found = deserializedRequest.Extension(extensionName) + require.Equal(t, id, deserializedRequest.ID()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) + require.Equal(t, priority, deserializedRequest.Priority()) + require.Equal(t, root.String(), deserializedRequest.Root().String()) + require.Equal(t, selector, deserializedRequest.Selector()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) +} + +func TestAppendingResponses(t *testing.T) { + extensionName := graphsync.ExtensionName("graphsync/awesome") + extension := graphsync.ExtensionData{ + Name: extensionName, + Data: basicnode.NewString("test extension data"), + } + requestID := graphsync.NewRequestID() + mh := NewMessageHandler() + status := graphsync.RequestAcknowledged + + builder := message.NewBuilder() + builder.AddResponseCode(requestID, status) + builder.AddExtensionData(requestID, extension) + gsm, err := builder.Build() + require.NoError(t, err) + responses := gsm.Responses() + require.Len(t, responses, 1, "did not add response to message") + response := responses[0] + extensionData, found := response.Extension(extensionName) + require.Equal(t, requestID, response.RequestID()) + require.Equal(t, status, response.Status()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) + + gsmIpld, err := mh.toIPLD(gsm) + require.NoError(t, err, "serialize to dag-cbor errored") + gsr := (*gsmIpld.Gs2.Responses)[0] + // no longer equal: require.Equal(t, requestID.Bytes(), gsr.Id) + require.Equal(t, status, gsr.Status) + require.Equal(t, basicnode.NewString("test extension data"), gsr.Extensions.Values["graphsync/awesome"]) + + deserialized, err := mh.fromIPLD(gsmIpld) + require.NoError(t, err, "deserializing dag-cbor message errored") + deserializedResponses := deserialized.Responses() + require.Len(t, deserializedResponses, 1, "did not add response to deserialized message") + deserializedResponse := deserializedResponses[0] + extensionData, found = deserializedResponse.Extension(extensionName) + require.Equal(t, response.RequestID(), deserializedResponse.RequestID()) + require.Equal(t, response.Status(), deserializedResponse.Status()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) +} + +func TestAppendBlock(t *testing.T) { + + strs := make([]string, 2) + strs = append(strs, "Celeritas") + strs = append(strs, "Incendia") + + builder := message.NewBuilder() + for _, str := range strs { + block := blocks.NewBlock([]byte(str)) + builder.AddBlock(block) + } + m, err := builder.Build() + require.NoError(t, err) + + gsmIpld, err := NewMessageHandler().toIPLD(m) + require.NoError(t, err, "serializing to dag-cbor errored") + + // assert strings are in dag-cbor message + for _, block := range *gsmIpld.Gs2.Blocks { + s := bytes.NewBuffer(block.Data).String() + require.True(t, contains(strs, s)) + } +} + +func contains(strs []string, x string) bool { + for _, s := range strs { + if s == x { + return true + } + } + return false +} + +func TestRequestCancel(t *testing.T) { + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + selector := ssb.Matcher().Node() + id := graphsync.NewRequestID() + priority := graphsync.Priority(rand.Int31()) + root := testutil.GenerateCids(1)[0] + + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority)) + builder.AddRequest(message.NewCancelRequest(id)) + gsm, err := builder.Build() + require.NoError(t, err) + + requests := gsm.Requests() + require.Len(t, requests, 1, "did not add cancel request") + request := requests[0] + require.Equal(t, id, request.ID()) + require.Equal(t, request.Type(), graphsync.RequestTypeCancel) + + mh := NewMessageHandler() + + buf := new(bytes.Buffer) + err = mh.ToNet(peer.ID("foo"), gsm, buf) + require.NoError(t, err, "did not serialize dag-cbor message") + deserialized, err := mh.FromNet(peer.ID("foo"), buf) + require.NoError(t, err, "did not deserialize dag-cbor message") + deserializedRequests := deserialized.Requests() + require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") + deserializedRequest := deserializedRequests[0] + require.Equal(t, request.ID(), deserializedRequest.ID()) + require.Equal(t, request.Type(), deserializedRequest.Type()) +} + +func TestRequestUpdate(t *testing.T) { + + id := graphsync.NewRequestID() + extensionName := graphsync.ExtensionName("graphsync/awesome") + extension := graphsync.ExtensionData{ + Name: extensionName, + Data: basicnode.NewBytes(testutil.RandomBytes(100)), + } + + builder := message.NewBuilder() + builder.AddRequest(message.NewUpdateRequest(id, extension)) + gsm, err := builder.Build() + require.NoError(t, err) + + requests := gsm.Requests() + require.Len(t, requests, 1, "did not add cancel request") + request := requests[0] + require.Equal(t, id, request.ID()) + require.Equal(t, request.Type(), graphsync.RequestTypeUpdate) + extensionData, found := request.Extension(extensionName) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) + + mh := NewMessageHandler() + + buf := new(bytes.Buffer) + err = mh.ToNet(peer.ID("foo"), gsm, buf) + require.NoError(t, err, "did not serialize dag-cbor message") + deserialized, err := mh.FromNet(peer.ID("foo"), buf) + require.NoError(t, err, "did not deserialize dag-cbor message") + + deserializedRequests := deserialized.Requests() + require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") + deserializedRequest := deserializedRequests[0] + extensionData, found = deserializedRequest.Extension(extensionName) + require.Equal(t, request.ID(), deserializedRequest.ID()) + require.Equal(t, request.Type(), deserializedRequest.Type()) + require.Equal(t, request.Priority(), deserializedRequest.Priority()) + require.Equal(t, request.Root().String(), deserializedRequest.Root().String()) + require.Equal(t, request.Selector(), deserializedRequest.Selector()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) +} + +func TestToNetFromNetEquivalency(t *testing.T) { + root := testutil.GenerateCids(1)[0] + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + selector := ssb.Matcher().Node() + extensionName := graphsync.ExtensionName("graphsync/awesome") + extension := graphsync.ExtensionData{ + Name: extensionName, + Data: basicnode.NewBytes(testutil.RandomBytes(100)), + } + id := graphsync.NewRequestID() + priority := graphsync.Priority(rand.Int31()) + status := graphsync.RequestAcknowledged + + builder := message.NewBuilder() + builder.AddRequest(message.NewRequest(id, root, selector, priority, extension)) + builder.AddResponseCode(id, status) + builder.AddExtensionData(id, extension) + builder.AddBlock(blocks.NewBlock([]byte("W"))) + builder.AddBlock(blocks.NewBlock([]byte("E"))) + builder.AddBlock(blocks.NewBlock([]byte("F"))) + builder.AddBlock(blocks.NewBlock([]byte("M"))) + gsm, err := builder.Build() + require.NoError(t, err) + + mh := NewMessageHandler() + + buf := new(bytes.Buffer) + err = mh.ToNet(peer.ID("foo"), gsm, buf) + require.NoError(t, err, "did not serialize dag-cbor message") + deserialized, err := mh.FromNet(peer.ID("foo"), buf) + require.NoError(t, err, "did not deserialize dag-cbor message") + + requests := gsm.Requests() + require.Len(t, requests, 1, "did not add request to message") + request := requests[0] + deserializedRequests := deserialized.Requests() + require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") + deserializedRequest := deserializedRequests[0] + extensionData, found := deserializedRequest.Extension(extensionName) + require.Equal(t, request.ID(), deserializedRequest.ID()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) + require.Equal(t, request.Priority(), deserializedRequest.Priority()) + require.Equal(t, request.Root().String(), deserializedRequest.Root().String()) + require.Equal(t, request.Selector(), deserializedRequest.Selector()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) + + responses := gsm.Responses() + require.Len(t, responses, 1, "did not add response to message") + response := responses[0] + deserializedResponses := deserialized.Responses() + require.Len(t, deserializedResponses, 1, "did not add response to message") + deserializedResponse := deserializedResponses[0] + extensionData, found = deserializedResponse.Extension(extensionName) + require.Equal(t, response.RequestID(), deserializedResponse.RequestID()) + require.Equal(t, response.Status(), deserializedResponse.Status()) + require.True(t, found) + require.Equal(t, extension.Data, extensionData) + + keys := make(map[cid.Cid]bool) + for _, b := range deserialized.Blocks() { + keys[b.Cid()] = true + } + + for _, b := range gsm.Blocks() { + _, ok := keys[b.Cid()] + require.True(t, ok) + } +} + +func TestMergeExtensions(t *testing.T) { + extensionName1 := graphsync.ExtensionName("graphsync/1") + extensionName2 := graphsync.ExtensionName("graphsync/2") + extensionName3 := graphsync.ExtensionName("graphsync/3") + initialExtensions := []graphsync.ExtensionData{ + { + Name: extensionName1, + Data: basicnode.NewString("applesauce"), + }, + { + Name: extensionName2, + Data: basicnode.NewString("hello"), + }, + } + replacementExtensions := []graphsync.ExtensionData{ + { + Name: extensionName2, + Data: basicnode.NewString("world"), + }, + { + Name: extensionName3, + Data: basicnode.NewString("cheese"), + }, + } + defaultMergeFunc := func(name graphsync.ExtensionName, oldData datamodel.Node, newData datamodel.Node) (datamodel.Node, error) { + os, err := oldData.AsString() + if err != nil { + return nil, err + } + ns, err := newData.AsString() + if err != nil { + return nil, err + } + return basicnode.NewString(os + " " + ns), nil + } + root := testutil.GenerateCids(1)[0] + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + selector := ssb.Matcher().Node() + id := graphsync.NewRequestID() + priority := graphsync.Priority(rand.Int31()) + defaultRequest := message.NewRequest(id, root, selector, priority, initialExtensions...) + t.Run("when merging into empty", func(t *testing.T) { + emptyRequest := message.NewRequest(id, root, selector, priority) + resultRequest, err := emptyRequest.MergeExtensions(replacementExtensions, defaultMergeFunc) + require.NoError(t, err) + require.Equal(t, emptyRequest.ID(), resultRequest.ID()) + require.Equal(t, emptyRequest.Priority(), resultRequest.Priority()) + require.Equal(t, emptyRequest.Root().String(), resultRequest.Root().String()) + require.Equal(t, emptyRequest.Selector(), resultRequest.Selector()) + _, has := resultRequest.Extension(extensionName1) + require.False(t, has) + extData2, has := resultRequest.Extension(extensionName2) + require.True(t, has) + require.Equal(t, basicnode.NewString("world"), extData2) + extData3, has := resultRequest.Extension(extensionName3) + require.True(t, has) + require.Equal(t, basicnode.NewString("cheese"), extData3) + }) + t.Run("when merging two requests", func(t *testing.T) { + resultRequest, err := defaultRequest.MergeExtensions(replacementExtensions, defaultMergeFunc) + require.NoError(t, err) + require.Equal(t, defaultRequest.ID(), resultRequest.ID()) + require.Equal(t, defaultRequest.Priority(), resultRequest.Priority()) + require.Equal(t, defaultRequest.Root().String(), resultRequest.Root().String()) + require.Equal(t, defaultRequest.Selector(), resultRequest.Selector()) + extData1, has := resultRequest.Extension(extensionName1) + require.True(t, has) + require.Equal(t, basicnode.NewString("applesauce"), extData1) + extData2, has := resultRequest.Extension(extensionName2) + require.True(t, has) + require.Equal(t, basicnode.NewString("hello world"), extData2) + extData3, has := resultRequest.Extension(extensionName3) + require.True(t, has) + require.Equal(t, basicnode.NewString("cheese"), extData3) + }) + t.Run("when merging errors", func(t *testing.T) { + errorMergeFunc := func(name graphsync.ExtensionName, oldData datamodel.Node, newData datamodel.Node) (datamodel.Node, error) { + return nil, errors.New("something went wrong") + } + _, err := defaultRequest.MergeExtensions(replacementExtensions, errorMergeFunc) + require.Error(t, err) + }) + t.Run("when merging with replace", func(t *testing.T) { + resultRequest := defaultRequest.ReplaceExtensions(replacementExtensions) + require.Equal(t, defaultRequest.ID(), resultRequest.ID()) + require.Equal(t, defaultRequest.Priority(), resultRequest.Priority()) + require.Equal(t, defaultRequest.Root().String(), resultRequest.Root().String()) + require.Equal(t, defaultRequest.Selector(), resultRequest.Selector()) + extData1, has := resultRequest.Extension(extensionName1) + require.True(t, has) + require.Equal(t, basicnode.NewString("applesauce"), extData1) + extData2, has := resultRequest.Extension(extensionName2) + require.True(t, has) + require.Equal(t, basicnode.NewString("world"), extData2) + extData3, has := resultRequest.Extension(extensionName3) + require.True(t, has) + require.Equal(t, basicnode.NewString("cheese"), extData3) + }) +} diff --git a/messagequeue/messagequeue_test.go b/messagequeue/messagequeue_test.go index 60d5fb8b..bf5b8ec5 100644 --- a/messagequeue/messagequeue_test.go +++ b/messagequeue/messagequeue_test.go @@ -39,7 +39,7 @@ func TestStartupAndShutdown(t *testing.T) { messageQueue := New(ctx, peer, messageNetwork, allocator, messageSendRetries, sendMessageTimeout) messageQueue.Startup() - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() @@ -77,7 +77,7 @@ func TestShutdownDuringMessageSend(t *testing.T) { messageQueue := New(ctx, peer, messageNetwork, allocator, messageSendRetries, sendMessageTimeout) messageQueue.Startup() - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() @@ -128,11 +128,11 @@ func TestProcessingNotification(t *testing.T) { waitGroup.Add(1) blks := testutil.GenerateBlocksOfSize(3, 128) - responseID := graphsync.RequestID(rand.Int31()) + responseID := graphsync.NewRequestID() extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } status := graphsync.RequestCompletedFull blkData := testutil.NewFakeBlockData() @@ -199,7 +199,7 @@ func TestDedupingMessages(t *testing.T) { messageQueue := New(ctx, peer, messageNetwork, allocator, messageSendRetries, sendMessageTimeout) messageQueue.Startup() waitGroup.Add(1) - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() @@ -210,11 +210,11 @@ func TestDedupingMessages(t *testing.T) { }) // wait for send attempt waitGroup.Wait() - id2 := graphsync.RequestID(rand.Int31()) + id2 := graphsync.NewRequestID() priority2 := graphsync.Priority(rand.Int31()) selector2 := ssb.ExploreAll(ssb.Matcher()).Node() root2 := testutil.GenerateCids(1)[0] - id3 := graphsync.RequestID(rand.Int31()) + id3 := graphsync.NewRequestID() priority3 := graphsync.Priority(rand.Int31()) selector3 := ssb.ExploreIndex(0, ssb.Matcher()).Node() root3 := testutil.GenerateCids(1)[0] @@ -231,7 +231,7 @@ func TestDedupingMessages(t *testing.T) { require.Len(t, requests, 1, "number of requests in first message was not 1") request := requests[0] require.Equal(t, id, request.ID()) - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority, request.Priority()) require.Equal(t, selector, request.Selector()) @@ -241,11 +241,11 @@ func TestDedupingMessages(t *testing.T) { require.Len(t, requests, 2, "number of requests in second message was not 2") for _, request := range requests { if request.ID() == id2 { - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority2, request.Priority()) require.Equal(t, selector2, request.Selector()) } else if request.ID() == id3 { - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority3, request.Priority()) require.Equal(t, selector3, request.Selector()) } else { @@ -385,8 +385,8 @@ func TestNetworkErrorClearResponses(t *testing.T) { messagesSent := make(chan gsmsg.GraphSyncMessage) resetChan := make(chan struct{}, 1) fullClosedChan := make(chan struct{}, 1) - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() messageSender := &fakeMessageSender{nil, fullClosedChan, resetChan, messagesSent} var waitGroup sync.WaitGroup messageNetwork := &fakeMessageNetwork{nil, nil, messageSender, &waitGroup} @@ -403,7 +403,7 @@ func TestNetworkErrorClearResponses(t *testing.T) { messageQueue.AllocateAndBuildMessage(uint64(len(blks[0].RawData())), func(b *Builder) { b.AddBlock(blks[0]) - b.AddLink(requestID1, cidlink.Link{Cid: blks[0].Cid()}, true) + b.AddLink(requestID1, cidlink.Link{Cid: blks[0].Cid()}, graphsync.LinkActionPresent) b.SetSubscriber(requestID1, subscriber) }) waitGroup.Wait() @@ -431,16 +431,16 @@ func TestNetworkErrorClearResponses(t *testing.T) { messageQueue.AllocateAndBuildMessage(uint64(len(blks[1].RawData())), func(b *Builder) { b.AddBlock(blks[1]) b.SetResponseStream(requestID1, fc1) - b.AddLink(requestID1, cidlink.Link{Cid: blks[1].Cid()}, true) + b.AddLink(requestID1, cidlink.Link{Cid: blks[1].Cid()}, graphsync.LinkActionPresent) }) messageQueue.AllocateAndBuildMessage(uint64(len(blks[2].RawData())), func(b *Builder) { b.AddBlock(blks[2]) b.SetResponseStream(requestID1, fc1) - b.AddLink(requestID1, cidlink.Link{Cid: blks[2].Cid()}, true) + b.AddLink(requestID1, cidlink.Link{Cid: blks[2].Cid()}, graphsync.LinkActionPresent) }) messageQueue.AllocateAndBuildMessage(uint64(len(blks[3].RawData())), func(b *Builder) { b.SetResponseStream(requestID2, fc2) - b.AddLink(requestID2, cidlink.Link{Cid: blks[3].Cid()}, true) + b.AddLink(requestID2, cidlink.Link{Cid: blks[3].Cid()}, graphsync.LinkActionPresent) b.AddBlock(blks[3]) }) diff --git a/metadata/metadata.go b/metadata/metadata.go deleted file mode 100644 index 5e79f8fa..00000000 --- a/metadata/metadata.go +++ /dev/null @@ -1,77 +0,0 @@ -package metadata - -import ( - "bytes" - "fmt" - - "github.com/ipfs/go-cid" - cbg "github.com/whyrusleeping/cbor-gen" - xerrors "golang.org/x/xerrors" -) - -// Item is a single link traversed in a repsonse -type Item struct { - Link cid.Cid - BlockPresent bool -} - -// Metadata is information about metadata contained in a response, which can be -// serialized back and forth to bytes -type Metadata []Item - -// DecodeMetadata assembles metadata from a raw byte array, first deserializing -// as a node and then assembling into a metadata struct. -func DecodeMetadata(data []byte) (Metadata, error) { - var metadata Metadata - r := bytes.NewReader(data) - - br := cbg.GetPeeker(r) - scratch := make([]byte, 8) - - maj, extra, err := cbg.CborReadHeaderBuf(br, scratch) - if err != nil { - return nil, err - } - - if extra > cbg.MaxLength { - return nil, fmt.Errorf("t.Metadata: array too large (%d)", extra) - } - - if maj != cbg.MajArray { - return nil, fmt.Errorf("expected cbor array") - } - - if extra > 0 { - metadata = make(Metadata, extra) - } - - for i := 0; i < int(extra); i++ { - - var v Item - if err := v.UnmarshalCBOR(br); err != nil { - return nil, err - } - - metadata[i] = v - } - - return metadata, nil -} - -// EncodeMetadata encodes metadata to an IPLD node then serializes to raw bytes -func EncodeMetadata(entries Metadata) ([]byte, error) { - w := new(bytes.Buffer) - scratch := make([]byte, 9) - if len(entries) > cbg.MaxLength { - return nil, xerrors.Errorf("Slice value was too long") - } - if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajArray, uint64(len(entries))); err != nil { - return nil, err - } - for _, v := range entries { - if err := v.MarshalCBOR(w); err != nil { - return nil, err - } - } - return w.Bytes(), nil -} diff --git a/metadata/metadata_cbor_gen.go b/metadata/metadata_cbor_gen.go deleted file mode 100644 index 8712e2d4..00000000 --- a/metadata/metadata_cbor_gen.go +++ /dev/null @@ -1,129 +0,0 @@ -package metadata - -import ( - "fmt" - "io" - - cbg "github.com/whyrusleeping/cbor-gen" - xerrors "golang.org/x/xerrors" -) - -var _ = xerrors.Errorf - -func (t *Item) MarshalCBOR(w io.Writer) error { - if t == nil { - _, err := w.Write(cbg.CborNull) - return err - } - if _, err := w.Write([]byte{162}); err != nil { - return err - } - - scratch := make([]byte, 9) - - // t.Link (cid.Cid) (struct) - if len("link") > cbg.MaxLength { - return xerrors.Errorf("Value in field \"link\" was too long") - } - - if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("link"))); err != nil { - return err - } - if _, err := io.WriteString(w, "link"); err != nil { - return err - } - - if err := cbg.WriteCidBuf(scratch, w, t.Link); err != nil { - return xerrors.Errorf("failed to write cid field t.Link: %w", err) - } - - // t.BlockPresent (bool) (bool) - if len("blockPresent") > cbg.MaxLength { - return xerrors.Errorf("Value in field \"blockPresent\" was too long") - } - - if err := cbg.WriteMajorTypeHeaderBuf(scratch, w, cbg.MajTextString, uint64(len("blockPresent"))); err != nil { - return err - } - if _, err := io.WriteString(w, "blockPresent"); err != nil { - return err - } - - if err := cbg.WriteBool(w, t.BlockPresent); err != nil { - return err - } - return nil -} - -func (t *Item) UnmarshalCBOR(r io.Reader) error { - *t = Item{} - - br := cbg.GetPeeker(r) - scratch := make([]byte, 8) - - maj, extra, err := cbg.CborReadHeaderBuf(br, scratch) - if err != nil { - return err - } - if maj != cbg.MajMap { - return fmt.Errorf("cbor input should be of type map") - } - - if extra > cbg.MaxLength { - return fmt.Errorf("Item: map struct too large (%d)", extra) - } - - var name string - n := extra - - for i := uint64(0); i < n; i++ { - - { - sval, err := cbg.ReadStringBuf(br, scratch) - if err != nil { - return err - } - - name = string(sval) - } - - switch name { - // t.Link (cid.Cid) (struct) - case "link": - - { - - c, err := cbg.ReadCid(br) - if err != nil { - return xerrors.Errorf("failed to read cid field t.Link: %w", err) - } - - t.Link = c - - } - // t.BlockPresent (bool) (bool) - case "blockPresent": - - maj, extra, err = cbg.CborReadHeaderBuf(br, scratch) - if err != nil { - return err - } - if maj != cbg.MajOther { - return fmt.Errorf("booleans must be major type 7") - } - switch extra { - case 20: - t.BlockPresent = false - case 21: - t.BlockPresent = true - default: - return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) - } - - default: - return fmt.Errorf("unknown struct field %d: '%s'", i, name) - } - } - - return nil -} diff --git a/network/interface.go b/network/interface.go index 2277cc08..1f4e135c 100644 --- a/network/interface.go +++ b/network/interface.go @@ -12,7 +12,8 @@ import ( var ( // ProtocolGraphsync is the protocol identifier for graphsync messages - ProtocolGraphsync protocol.ID = "/ipfs/graphsync/1.0.0" + ProtocolGraphsync_1_0_0 protocol.ID = "/ipfs/graphsync/1.0.0" + ProtocolGraphsync_2_0_0 protocol.ID = "/ipfs/graphsync/2.0.0" ) // GraphSyncNetwork provides network connectivity for GraphSync. diff --git a/network/libp2p_impl.go b/network/libp2p_impl.go index 0b6a9a6c..a423ab54 100644 --- a/network/libp2p_impl.go +++ b/network/libp2p_impl.go @@ -10,36 +10,96 @@ import ( "github.com/libp2p/go-libp2p-core/host" "github.com/libp2p/go-libp2p-core/network" "github.com/libp2p/go-libp2p-core/peer" + "github.com/libp2p/go-libp2p-core/protocol" "github.com/libp2p/go-msgio" ma "github.com/multiformats/go-multiaddr" gsmsg "github.com/ipfs/go-graphsync/message" + gsmsgv1 "github.com/ipfs/go-graphsync/message/v1" + gsmsgv2 "github.com/ipfs/go-graphsync/message/v2" ) var log = logging.Logger("graphsync_network") var sendMessageTimeout = time.Minute * 10 +// Option is an option for configuring the libp2p storage market network +type Option func(*libp2pGraphSyncNetwork) + +// DataTransferProtocols OVERWRITES the default libp2p protocols we use for +// graphsync with the specified protocols +func GraphsyncProtocols(protocols []protocol.ID) Option { + return func(gsnet *libp2pGraphSyncNetwork) { + gsnet.setProtocols(protocols) + } +} + // NewFromLibp2pHost returns a GraphSyncNetwork supported by underlying Libp2p host. -func NewFromLibp2pHost(host host.Host) GraphSyncNetwork { +func NewFromLibp2pHost(host host.Host, options ...Option) GraphSyncNetwork { + messageHandlerSelector := messageHandlerSelector{ + v1MessageHandler: gsmsgv1.NewMessageHandler(), + v2MessageHandler: gsmsgv2.NewMessageHandler(), + } graphSyncNetwork := libp2pGraphSyncNetwork{ - host: host, + host: host, + messageHandlerSelector: &messageHandlerSelector, + protocols: []protocol.ID{ProtocolGraphsync_2_0_0, ProtocolGraphsync_1_0_0}, + } + + for _, option := range options { + option(&graphSyncNetwork) } return &graphSyncNetwork } +// a message.MessageHandler that simply returns an error for any of the calls, allows +// us to simplify erroring on bad protocol within the messageHandlerSelector#Select() +// call so we only have one place to be strict about allowed versions +type messageHandlerErrorer struct { + err error +} + +func (mhe messageHandlerErrorer) FromNet(peer.ID, io.Reader) (gsmsg.GraphSyncMessage, error) { + return gsmsg.GraphSyncMessage{}, mhe.err +} +func (mhe messageHandlerErrorer) FromMsgReader(peer.ID, msgio.Reader) (gsmsg.GraphSyncMessage, error) { + return gsmsg.GraphSyncMessage{}, mhe.err +} +func (mhe messageHandlerErrorer) ToNet(peer.ID, gsmsg.GraphSyncMessage, io.Writer) error { + return mhe.err +} + +type messageHandlerSelector struct { + v1MessageHandler gsmsg.MessageHandler + v2MessageHandler gsmsg.MessageHandler +} + +func (smh messageHandlerSelector) Select(protocol protocol.ID) gsmsg.MessageHandler { + switch protocol { + case ProtocolGraphsync_1_0_0: + return smh.v1MessageHandler + case ProtocolGraphsync_2_0_0: + return smh.v2MessageHandler + default: + return messageHandlerErrorer{fmt.Errorf("unrecognized protocol version: %s", protocol)} + } +} + // libp2pGraphSyncNetwork transforms the libp2p host interface, which sends and receives // NetMessage objects, into the graphsync network interface. type libp2pGraphSyncNetwork struct { host host.Host // inbound messages from the network are forwarded to the receiver - receiver Receiver + receiver Receiver + protocols []protocol.ID + messageHandlerSelector *messageHandlerSelector } type streamMessageSender struct { - s network.Stream - opts MessageSenderOpts + s network.Stream + opts MessageSenderOpts + messageHandlerSelector *messageHandlerSelector } func (s *streamMessageSender) Close() error { @@ -51,10 +111,10 @@ func (s *streamMessageSender) Reset() error { } func (s *streamMessageSender) SendMsg(ctx context.Context, msg gsmsg.GraphSyncMessage) error { - return msgToStream(ctx, s.s, msg, s.opts.SendTimeout) + return msgToStream(ctx, s.s, s.messageHandlerSelector, msg, s.opts.SendTimeout) } -func msgToStream(ctx context.Context, s network.Stream, msg gsmsg.GraphSyncMessage, timeout time.Duration) error { +func msgToStream(ctx context.Context, s network.Stream, mh *messageHandlerSelector, msg gsmsg.GraphSyncMessage, timeout time.Duration) error { log.Debugf("Outgoing message with %d requests, %d responses, and %d blocks", len(msg.Requests()), len(msg.Responses()), len(msg.Blocks())) @@ -66,14 +126,9 @@ func msgToStream(ctx context.Context, s network.Stream, msg gsmsg.GraphSyncMessa log.Warnf("error setting deadline: %s", err) } - switch s.Protocol() { - case ProtocolGraphsync: - if err := msg.ToNet(s); err != nil { - log.Debugf("error: %s", err) - return err - } - default: - return fmt.Errorf("unrecognized protocol on remote: %s", s.Protocol()) + if err := mh.Select(s.Protocol()).ToNet(s.Conn().RemotePeer(), msg, s); err != nil { + log.Debugf("error: %s", err) + return err } if err := s.SetWriteDeadline(time.Time{}); err != nil { @@ -88,11 +143,15 @@ func (gsnet *libp2pGraphSyncNetwork) NewMessageSender(ctx context.Context, p pee return nil, err } - return &streamMessageSender{s: s, opts: setDefaults(opts)}, nil + return &streamMessageSender{ + s: s, + opts: setDefaults(opts), + messageHandlerSelector: gsnet.messageHandlerSelector, + }, nil } func (gsnet *libp2pGraphSyncNetwork) newStreamToPeer(ctx context.Context, p peer.ID) (network.Stream, error) { - return gsnet.host.NewStream(ctx, p, ProtocolGraphsync) + return gsnet.host.NewStream(ctx, p, gsnet.protocols...) } func (gsnet *libp2pGraphSyncNetwork) SendMessage( @@ -105,7 +164,7 @@ func (gsnet *libp2pGraphSyncNetwork) SendMessage( return err } - if err = msgToStream(ctx, s, outgoing, sendMessageTimeout); err != nil { + if err = msgToStream(ctx, s, gsnet.messageHandlerSelector, outgoing, sendMessageTimeout); err != nil { _ = s.Reset() return err } @@ -115,7 +174,9 @@ func (gsnet *libp2pGraphSyncNetwork) SendMessage( func (gsnet *libp2pGraphSyncNetwork) SetDelegate(r Receiver) { gsnet.receiver = r - gsnet.host.SetStreamHandler(ProtocolGraphsync, gsnet.handleNewStream) + for _, p := range gsnet.protocols { + gsnet.host.SetStreamHandler(p, gsnet.handleNewStream) + } gsnet.host.Network().Notify((*libp2pGraphSyncNotifee)(gsnet)) } @@ -134,7 +195,7 @@ func (gsnet *libp2pGraphSyncNetwork) handleNewStream(s network.Stream) { reader := msgio.NewVarintReaderSize(s, network.MessageSizeMax) for { - received, err := gsmsg.FromMsgReader(reader) + received, err := gsnet.messageHandlerSelector.Select(s.Protocol()).FromMsgReader(s.Conn().RemotePeer(), reader) p := s.Conn().RemotePeer() if err != nil { @@ -148,6 +209,7 @@ func (gsnet *libp2pGraphSyncNetwork) handleNewStream(s network.Stream) { ctx := context.Background() log.Debugf("graphsync net handleNewStream from %s", s.Conn().RemotePeer()) + gsnet.receiver.ReceiveMessage(ctx, p, received) } } @@ -156,6 +218,16 @@ func (gsnet *libp2pGraphSyncNetwork) ConnectionManager() ConnManager { return gsnet.host.ConnManager() } +func (gsnet *libp2pGraphSyncNetwork) setProtocols(protocols []protocol.ID) { + gsnet.protocols = make([]protocol.ID, 0) + for _, proto := range protocols { + switch proto { + case ProtocolGraphsync_1_0_0, ProtocolGraphsync_2_0_0: + gsnet.protocols = append([]protocol.ID{}, proto) + } + } +} + type libp2pGraphSyncNotifee libp2pGraphSyncNetwork func (nn *libp2pGraphSyncNotifee) libp2pGraphSyncNetwork() *libp2pGraphSyncNetwork { diff --git a/network/libp2p_impl_test.go b/network/libp2p_impl_test.go index da7d2225..7a67dafe 100644 --- a/network/libp2p_impl_test.go +++ b/network/libp2p_impl_test.go @@ -75,9 +75,9 @@ func TestMessageSendAndReceive(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) status := graphsync.RequestAcknowledged @@ -107,8 +107,7 @@ func TestMessageSendAndReceive(t *testing.T) { require.Len(t, receivedRequests, 1, "did not add request to received message") receivedRequest := receivedRequests[0] require.Equal(t, sentRequest.ID(), receivedRequest.ID()) - require.Equal(t, sentRequest.IsCancel(), receivedRequest.IsCancel()) - require.Equal(t, sentRequest.Priority(), receivedRequest.Priority()) + require.Equal(t, sentRequest.Type(), receivedRequest.Type()) require.Equal(t, sentRequest.Root().String(), receivedRequest.Root().String()) require.Equal(t, sentRequest.Selector(), receivedRequest.Selector()) diff --git a/peermanager/peermessagemanager_test.go b/peermanager/peermessagemanager_test.go index 6d55c936..9a13c877 100644 --- a/peermanager/peermessagemanager_test.go +++ b/peermanager/peermessagemanager_test.go @@ -67,7 +67,7 @@ func TestSendingMessagesToPeers(t *testing.T) { tp := testutil.GeneratePeers(5) - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) @@ -82,7 +82,7 @@ func TestSendingMessagesToPeers(t *testing.T) { peerManager.AllocateAndBuildMessage(tp[1], 0, func(b *messagequeue.Builder) { b.AddRequest(request) }) - cancelRequest := gsmsg.CancelRequest(id) + cancelRequest := gsmsg.NewCancelRequest(id) peerManager.AllocateAndBuildMessage(tp[0], 0, func(b *messagequeue.Builder) { b.AddRequest(cancelRequest) }) @@ -92,7 +92,7 @@ func TestSendingMessagesToPeers(t *testing.T) { require.Equal(t, tp[0], firstMessage.p, "first message sent to incorrect peer") request = firstMessage.message.Requests()[0] require.Equal(t, id, request.ID()) - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority, request.Priority()) require.Equal(t, selector, request.Selector()) @@ -101,7 +101,7 @@ func TestSendingMessagesToPeers(t *testing.T) { require.Equal(t, tp[1], secondMessage.p, "second message sent to incorrect peer") request = secondMessage.message.Requests()[0] require.Equal(t, id, request.ID()) - require.False(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority, request.Priority()) require.Equal(t, selector, request.Selector()) @@ -111,7 +111,7 @@ func TestSendingMessagesToPeers(t *testing.T) { require.Equal(t, tp[0], thirdMessage.p, "third message sent to incorrect peer") request = thirdMessage.message.Requests()[0] require.Equal(t, id, request.ID()) - require.True(t, request.IsCancel()) + require.Equal(t, request.Type(), graphsync.RequestTypeCancel) connectedPeers := peerManager.ConnectedPeers() require.Len(t, connectedPeers, 2) diff --git a/peerstate/peerstate.go b/peerstate/peerstate.go index 17c4f9d7..36622f1e 100644 --- a/peerstate/peerstate.go +++ b/peerstate/peerstate.go @@ -30,10 +30,10 @@ func (ps PeerState) Diagnostics() map[graphsync.RequestID][]string { if ok { matchedActiveQueue[id] = struct{}{} if status != graphsync.Running { - diagnostics[id] = append(diagnostics[id], fmt.Sprintf("expected request with id %d in active task queue to be in running state, but was %s", id, status)) + diagnostics[id] = append(diagnostics[id], fmt.Sprintf("expected request with id %s in active task queue to be in running state, but was %s", id.String(), status)) } } else { - diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %d in active task queue but appears to have no tracked state", id)) + diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %s in active task queue but appears to have no tracked state", id.String())) } } for _, id := range ps.TaskQueueState.Pending { @@ -41,21 +41,21 @@ func (ps PeerState) Diagnostics() map[graphsync.RequestID][]string { if ok { matchedPendingQueue[id] = struct{}{} if status != graphsync.Queued { - diagnostics[id] = append(diagnostics[id], fmt.Sprintf("expected request with id %d in pending task queue to be in queued state, but was %s", id, status)) + diagnostics[id] = append(diagnostics[id], fmt.Sprintf("expected request with id %s in pending task queue to be in queued state, but was %s", id.String(), status)) } } else { - diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %d in pending task queue but appears to have no tracked state", id)) + diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %s in pending task queue but appears to have no tracked state", id.String())) } } for id, state := range ps.RequestStates { if state == graphsync.Running { if _, ok := matchedActiveQueue[id]; !ok { - diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %d in running state is not in the active task queue", id)) + diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %s in running state is not in the active task queue", id.String())) } } if state == graphsync.Queued { if _, ok := matchedPendingQueue[id]; !ok { - diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %d in queued state is not in the pending task queue", id)) + diagnostics[id] = append(diagnostics[id], fmt.Sprintf("request with id %s in queued state is not in the pending task queue", id.String())) } } } diff --git a/peerstate/peerstate_test.go b/peerstate/peerstate_test.go index 4519555f..46e8efd7 100644 --- a/peerstate/peerstate_test.go +++ b/peerstate/peerstate_test.go @@ -2,7 +2,6 @@ package peerstate_test import ( "fmt" - "math/rand" "testing" "github.com/stretchr/testify/require" @@ -14,7 +13,7 @@ import ( func TestDiagnostics(t *testing.T) { requestIDs := make([]graphsync.RequestID, 0, 5) for i := 0; i < 5; i++ { - requestIDs = append(requestIDs, graphsync.RequestID(rand.Int31())) + requestIDs = append(requestIDs, graphsync.NewRequestID()) } testCases := map[string]struct { requestStates graphsync.RequestStates @@ -48,8 +47,8 @@ func TestDiagnostics(t *testing.T) { Pending: []graphsync.RequestID{requestIDs[2], requestIDs[3]}, }, expectedDiagnostics: map[graphsync.RequestID][]string{ - requestIDs[1]: {fmt.Sprintf("expected request with id %d in active task queue to be in running state, but was queued", requestIDs[1]), fmt.Sprintf("request with id %d in queued state is not in the pending task queue", requestIDs[1])}, - requestIDs[4]: {fmt.Sprintf("expected request with id %d in active task queue to be in running state, but was paused", requestIDs[4])}, + requestIDs[1]: {fmt.Sprintf("expected request with id %s in active task queue to be in running state, but was queued", requestIDs[1].String()), fmt.Sprintf("request with id %s in queued state is not in the pending task queue", requestIDs[1].String())}, + requestIDs[4]: {fmt.Sprintf("expected request with id %s in active task queue to be in running state, but was paused", requestIDs[4].String())}, }, }, "active task with no state": { @@ -64,7 +63,7 @@ func TestDiagnostics(t *testing.T) { Pending: []graphsync.RequestID{requestIDs[2], requestIDs[3]}, }, expectedDiagnostics: map[graphsync.RequestID][]string{ - requestIDs[1]: {fmt.Sprintf("request with id %d in active task queue but appears to have no tracked state", requestIDs[1])}, + requestIDs[1]: {fmt.Sprintf("request with id %s in active task queue but appears to have no tracked state", requestIDs[1].String())}, }, }, "pending task with with incorrect state": { @@ -80,8 +79,8 @@ func TestDiagnostics(t *testing.T) { Pending: []graphsync.RequestID{requestIDs[2], requestIDs[3], requestIDs[4]}, }, expectedDiagnostics: map[graphsync.RequestID][]string{ - requestIDs[3]: {fmt.Sprintf("expected request with id %d in pending task queue to be in queued state, but was running", requestIDs[3]), fmt.Sprintf("request with id %d in running state is not in the active task queue", requestIDs[3])}, - requestIDs[4]: {fmt.Sprintf("expected request with id %d in pending task queue to be in queued state, but was paused", requestIDs[4])}, + requestIDs[3]: {fmt.Sprintf("expected request with id %s in pending task queue to be in queued state, but was running", requestIDs[3].String()), fmt.Sprintf("request with id %s in running state is not in the active task queue", requestIDs[3].String())}, + requestIDs[4]: {fmt.Sprintf("expected request with id %s in pending task queue to be in queued state, but was paused", requestIDs[4].String())}, }, }, "pending task with no state": { @@ -96,7 +95,7 @@ func TestDiagnostics(t *testing.T) { Pending: []graphsync.RequestID{requestIDs[2], requestIDs[3]}, }, expectedDiagnostics: map[graphsync.RequestID][]string{ - requestIDs[3]: {fmt.Sprintf("request with id %d in pending task queue but appears to have no tracked state", requestIDs[3])}, + requestIDs[3]: {fmt.Sprintf("request with id %s in pending task queue but appears to have no tracked state", requestIDs[3].String())}, }, }, "request state running with no active task": { @@ -112,7 +111,7 @@ func TestDiagnostics(t *testing.T) { Pending: []graphsync.RequestID{requestIDs[2], requestIDs[3]}, }, expectedDiagnostics: map[graphsync.RequestID][]string{ - requestIDs[1]: {fmt.Sprintf("request with id %d in running state is not in the active task queue", requestIDs[1])}, + requestIDs[1]: {fmt.Sprintf("request with id %s in running state is not in the active task queue", requestIDs[1].String())}, }, }, "request state queued with no pending task": { @@ -128,7 +127,7 @@ func TestDiagnostics(t *testing.T) { Pending: []graphsync.RequestID{requestIDs[2]}, }, expectedDiagnostics: map[graphsync.RequestID][]string{ - requestIDs[3]: {fmt.Sprintf("request with id %d in queued state is not in the pending task queue", requestIDs[3])}, + requestIDs[3]: {fmt.Sprintf("request with id %s in queued state is not in the pending task queue", requestIDs[3].String())}, }, }, } diff --git a/responsemanager/persistenceoptions/persistenceoptions.go b/persistenceoptions/persistenceoptions.go similarity index 100% rename from responsemanager/persistenceoptions/persistenceoptions.go rename to persistenceoptions/persistenceoptions.go diff --git a/requestmanager/asyncloader/asyncloader.go b/requestmanager/asyncloader/asyncloader.go deleted file mode 100644 index 5609b4e1..00000000 --- a/requestmanager/asyncloader/asyncloader.go +++ /dev/null @@ -1,217 +0,0 @@ -package asyncloader - -import ( - "context" - "errors" - "fmt" - "io/ioutil" - "sync" - - blocks "github.com/ipfs/go-block-format" - "github.com/ipld/go-ipld-prime" - peer "github.com/libp2p/go-libp2p-core/peer" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" - "github.com/ipfs/go-graphsync/requestmanager/asyncloader/loadattemptqueue" - "github.com/ipfs/go-graphsync/requestmanager/asyncloader/responsecache" - "github.com/ipfs/go-graphsync/requestmanager/asyncloader/unverifiedblockstore" - "github.com/ipfs/go-graphsync/requestmanager/types" -) - -type alternateQueue struct { - responseCache *responsecache.ResponseCache - loadAttemptQueue *loadattemptqueue.LoadAttemptQueue -} - -// AsyncLoader manages loading links asynchronously in as new responses -// come in from the network -type AsyncLoader struct { - ctx context.Context - cancel context.CancelFunc - - // this mutex protects access to the state of the async loader, which covers all data fields below below - stateLk sync.Mutex - activeRequests map[graphsync.RequestID]struct{} - requestQueues map[graphsync.RequestID]string - alternateQueues map[string]alternateQueue - responseCache *responsecache.ResponseCache - loadAttemptQueue *loadattemptqueue.LoadAttemptQueue -} - -// New initializes a new link loading manager for asynchronous loads from the given context -// and local store loading and storing function -func New(ctx context.Context, linkSystem ipld.LinkSystem) *AsyncLoader { - responseCache, loadAttemptQueue := setupAttemptQueue(linkSystem) - ctx, cancel := context.WithCancel(ctx) - return &AsyncLoader{ - ctx: ctx, - cancel: cancel, - activeRequests: make(map[graphsync.RequestID]struct{}), - requestQueues: make(map[graphsync.RequestID]string), - alternateQueues: make(map[string]alternateQueue), - responseCache: responseCache, - loadAttemptQueue: loadAttemptQueue, - } -} - -// RegisterPersistenceOption registers a new loader/storer option for processing requests -func (al *AsyncLoader) RegisterPersistenceOption(name string, lsys ipld.LinkSystem) error { - al.stateLk.Lock() - defer al.stateLk.Unlock() - _, existing := al.alternateQueues[name] - if existing { - return errors.New("already registerd a persistence option with this name") - } - responseCache, loadAttemptQueue := setupAttemptQueue(lsys) - al.alternateQueues[name] = alternateQueue{responseCache, loadAttemptQueue} - return nil -} - -// UnregisterPersistenceOption unregisters an existing loader/storer option for processing requests -func (al *AsyncLoader) UnregisterPersistenceOption(name string) error { - al.stateLk.Lock() - defer al.stateLk.Unlock() - _, ok := al.alternateQueues[name] - if !ok { - return fmt.Errorf("unknown persistence option: %s", name) - } - for _, requestQueue := range al.requestQueues { - if name == requestQueue { - return errors.New("cannot unregister while requests are in progress") - } - } - delete(al.alternateQueues, name) - return nil -} - -// StartRequest indicates the given request has started and the manager should -// continually attempt to load links for this request as new responses come in -func (al *AsyncLoader) StartRequest(requestID graphsync.RequestID, persistenceOption string) error { - al.stateLk.Lock() - defer al.stateLk.Unlock() - if persistenceOption != "" { - _, ok := al.alternateQueues[persistenceOption] - if !ok { - return errors.New("unknown persistence option") - } - al.requestQueues[requestID] = persistenceOption - } - al.activeRequests[requestID] = struct{}{} - return nil -} - -// ProcessResponse injests new responses and completes asynchronous loads as -// neccesary -func (al *AsyncLoader) ProcessResponse( - ctx context.Context, - responses map[graphsync.RequestID]metadata.Metadata, - blks []blocks.Block) { - - requestIds := make([]int, 0, len(responses)) - for requestID := range responses { - requestIds = append(requestIds, int(requestID)) - } - ctx, span := otel.Tracer("graphsync").Start(ctx, "loaderProcess", trace.WithAttributes( - attribute.IntSlice("requestIDs", requestIds), - )) - defer span.End() - - al.stateLk.Lock() - defer al.stateLk.Unlock() - byQueue := make(map[string][]graphsync.RequestID) - for requestID := range responses { - queue := al.requestQueues[requestID] - byQueue[queue] = append(byQueue[queue], requestID) - } - for queue, requestIDs := range byQueue { - loadAttemptQueue := al.getLoadAttemptQueue(queue) - responseCache := al.getResponseCache(queue) - queueResponses := make(map[graphsync.RequestID]metadata.Metadata, len(requestIDs)) - for _, requestID := range requestIDs { - queueResponses[requestID] = responses[requestID] - } - responseCache.ProcessResponse(ctx, queueResponses, blks) - loadAttemptQueue.RetryLoads() - } -} - -// AsyncLoad asynchronously loads the given link for the given request ID. It returns a channel for data and a channel -// for errors -- only one message will be sent over either. -func (al *AsyncLoader) AsyncLoad(p peer.ID, requestID graphsync.RequestID, link ipld.Link, linkContext ipld.LinkContext) <-chan types.AsyncLoadResult { - resultChan := make(chan types.AsyncLoadResult, 1) - lr := loadattemptqueue.NewLoadRequest(p, requestID, link, linkContext, resultChan) - al.stateLk.Lock() - defer al.stateLk.Unlock() - _, retry := al.activeRequests[requestID] - loadAttemptQueue := al.getLoadAttemptQueue(al.requestQueues[requestID]) - loadAttemptQueue.AttemptLoad(lr, retry) - return resultChan -} - -// CompleteResponsesFor indicates no further responses will come in for the given -// requestID, so if no responses are in the cache or local store, a link load -// should not retry -func (al *AsyncLoader) CompleteResponsesFor(requestID graphsync.RequestID) { - al.stateLk.Lock() - defer al.stateLk.Unlock() - delete(al.activeRequests, requestID) - loadAttemptQueue := al.getLoadAttemptQueue(al.requestQueues[requestID]) - loadAttemptQueue.ClearRequest(requestID) -} - -// CleanupRequest indicates the given request is complete on the client side, -// and no further attempts will be made to load links for this request, -// so any cached response data is invalid can be cleaned -func (al *AsyncLoader) CleanupRequest(p peer.ID, requestID graphsync.RequestID) { - al.stateLk.Lock() - defer al.stateLk.Unlock() - responseCache := al.responseCache - aq, ok := al.requestQueues[requestID] - if ok { - responseCache = al.alternateQueues[aq].responseCache - delete(al.requestQueues, requestID) - } - responseCache.FinishRequest(requestID) -} - -func (al *AsyncLoader) getLoadAttemptQueue(queue string) *loadattemptqueue.LoadAttemptQueue { - if queue == "" { - return al.loadAttemptQueue - } - return al.alternateQueues[queue].loadAttemptQueue -} - -func (al *AsyncLoader) getResponseCache(queue string) *responsecache.ResponseCache { - if queue == "" { - return al.responseCache - } - return al.alternateQueues[queue].responseCache -} - -func setupAttemptQueue(lsys ipld.LinkSystem) (*responsecache.ResponseCache, *loadattemptqueue.LoadAttemptQueue) { - unverifiedBlockStore := unverifiedblockstore.New(lsys.StorageWriteOpener) - responseCache := responsecache.New(unverifiedBlockStore) - loadAttemptQueue := loadattemptqueue.New(func(p peer.ID, requestID graphsync.RequestID, link ipld.Link, linkContext ipld.LinkContext) types.AsyncLoadResult { - // load from response cache - data, err := responseCache.AttemptLoad(requestID, link, linkContext) - if err != nil { - return types.AsyncLoadResult{Err: err, Local: false} - } - if data != nil { - return types.AsyncLoadResult{Data: data, Local: false} - } - // fall back to local store - if stream, err := lsys.StorageReadOpener(linkContext, link); stream != nil && err == nil { - if localData, err := ioutil.ReadAll(stream); err == nil && localData != nil { - return types.AsyncLoadResult{Data: localData, Local: true} - } - } - return types.AsyncLoadResult{Local: false} - }) - - return responseCache, loadAttemptQueue -} diff --git a/requestmanager/asyncloader/asyncloader_test.go b/requestmanager/asyncloader/asyncloader_test.go deleted file mode 100644 index 63255d53..00000000 --- a/requestmanager/asyncloader/asyncloader_test.go +++ /dev/null @@ -1,405 +0,0 @@ -package asyncloader - -import ( - "context" - "io" - "math/rand" - "testing" - "time" - - blocks "github.com/ipfs/go-block-format" - ipld "github.com/ipld/go-ipld-prime" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" - "github.com/stretchr/testify/require" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" - "github.com/ipfs/go-graphsync/requestmanager/types" - "github.com/ipfs/go-graphsync/testutil" -) - -func TestAsyncLoadInitialLoadSucceedsLocallyPresent(t *testing.T) { - block := testutil.GenerateBlocksOfSize(1, 100)[0] - st := newStore() - link := st.Store(t, block) - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) - p := testutil.GeneratePeers(1)[0] - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - assertSuccessResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 1) - }) -} - -func TestAsyncLoadInitialLoadSucceedsResponsePresent(t *testing.T) { - blocks := testutil.GenerateBlocksOfSize(1, 100) - block := blocks[0] - link := cidlink.Link{Cid: block.Cid()} - - st := newStore() - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, - } - p := testutil.GeneratePeers(1)[0] - asyncLoader.ProcessResponse(context.Background(), responses, blocks) - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - - assertSuccessResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 0) - st.AssertBlockStored(t, block) - }) -} - -func TestAsyncLoadInitialLoadFails(t *testing.T) { - st := newStore() - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) - - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.(cidlink.Link).Cid, - BlockPresent: false, - }, - }, - } - p := testutil.GeneratePeers(1)[0] - asyncLoader.ProcessResponse(context.Background(), responses, nil) - - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - assertFailResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 0) - }) -} - -func TestAsyncLoadInitialLoadIndeterminateWhenRequestNotInProgress(t *testing.T) { - st := newStore() - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) - p := testutil.GeneratePeers(1)[0] - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - assertFailResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 1) - }) -} - -func TestAsyncLoadInitialLoadIndeterminateThenSucceeds(t *testing.T) { - blocks := testutil.GenerateBlocksOfSize(1, 100) - block := blocks[0] - link := cidlink.Link{Cid: block.Cid()} - - st := newStore() - - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) - err := asyncLoader.StartRequest(requestID, "") - require.NoError(t, err) - p := testutil.GeneratePeers(1)[0] - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - - st.AssertAttemptLoadWithoutResult(ctx, t, resultChan) - - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, - } - asyncLoader.ProcessResponse(context.Background(), responses, blocks) - assertSuccessResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 1) - st.AssertBlockStored(t, block) - }) -} - -func TestAsyncLoadInitialLoadIndeterminateThenFails(t *testing.T) { - st := newStore() - - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) - err := asyncLoader.StartRequest(requestID, "") - require.NoError(t, err) - p := testutil.GeneratePeers(1)[0] - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - - st.AssertAttemptLoadWithoutResult(ctx, t, resultChan) - - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.(cidlink.Link).Cid, - BlockPresent: false, - }, - }, - } - asyncLoader.ProcessResponse(context.Background(), responses, nil) - assertFailResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 1) - }) -} - -func TestAsyncLoadInitialLoadIndeterminateThenRequestFinishes(t *testing.T) { - st := newStore() - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) - err := asyncLoader.StartRequest(requestID, "") - require.NoError(t, err) - p := testutil.GeneratePeers(1)[0] - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - st.AssertAttemptLoadWithoutResult(ctx, t, resultChan) - asyncLoader.CompleteResponsesFor(requestID) - assertFailResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 1) - }) -} - -func TestAsyncLoadTwiceLoadsLocallySecondTime(t *testing.T) { - blocks := testutil.GenerateBlocksOfSize(1, 100) - block := blocks[0] - link := cidlink.Link{Cid: block.Cid()} - st := newStore() - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, - } - p := testutil.GeneratePeers(1)[0] - asyncLoader.ProcessResponse(context.Background(), responses, blocks) - resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - - assertSuccessResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 0) - - resultChan = asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) - assertSuccessResponse(ctx, t, resultChan) - st.AssertLocalLoads(t, 1) - - st.AssertBlockStored(t, block) - }) -} - -func TestRegisterUnregister(t *testing.T) { - st := newStore() - otherSt := newStore() - blocks := testutil.GenerateBlocksOfSize(3, 100) - link1 := otherSt.Store(t, blocks[0]) - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - - requestID1 := graphsync.RequestID(rand.Int31()) - err := asyncLoader.StartRequest(requestID1, "other") - require.EqualError(t, err, "unknown persistence option") - - err = asyncLoader.RegisterPersistenceOption("other", otherSt.lsys) - require.NoError(t, err) - requestID2 := graphsync.RequestID(rand.Int31()) - err = asyncLoader.StartRequest(requestID2, "other") - require.NoError(t, err) - p := testutil.GeneratePeers(1)[0] - resultChan1 := asyncLoader.AsyncLoad(p, requestID2, link1, ipld.LinkContext{}) - assertSuccessResponse(ctx, t, resultChan1) - err = asyncLoader.UnregisterPersistenceOption("other") - require.EqualError(t, err, "cannot unregister while requests are in progress") - asyncLoader.CompleteResponsesFor(requestID2) - asyncLoader.CleanupRequest(p, requestID2) - err = asyncLoader.UnregisterPersistenceOption("other") - require.NoError(t, err) - - requestID3 := graphsync.RequestID(rand.Int31()) - err = asyncLoader.StartRequest(requestID3, "other") - require.EqualError(t, err, "unknown persistence option") - }) -} -func TestRequestSplittingLoadLocallyFromBlockstore(t *testing.T) { - st := newStore() - otherSt := newStore() - block := testutil.GenerateBlocksOfSize(1, 100)[0] - link := otherSt.Store(t, block) - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - err := asyncLoader.RegisterPersistenceOption("other", otherSt.lsys) - require.NoError(t, err) - requestID1 := graphsync.RequestID(rand.Int31()) - p := testutil.GeneratePeers(1)[0] - - resultChan1 := asyncLoader.AsyncLoad(p, requestID1, link, ipld.LinkContext{}) - requestID2 := graphsync.RequestID(rand.Int31()) - err = asyncLoader.StartRequest(requestID2, "other") - require.NoError(t, err) - resultChan2 := asyncLoader.AsyncLoad(p, requestID2, link, ipld.LinkContext{}) - - assertFailResponse(ctx, t, resultChan1) - assertSuccessResponse(ctx, t, resultChan2) - st.AssertLocalLoads(t, 1) - }) -} - -func TestRequestSplittingSameBlockTwoStores(t *testing.T) { - st := newStore() - otherSt := newStore() - blocks := testutil.GenerateBlocksOfSize(1, 100) - block := blocks[0] - link := cidlink.Link{Cid: block.Cid()} - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - err := asyncLoader.RegisterPersistenceOption("other", otherSt.lsys) - require.NoError(t, err) - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) - err = asyncLoader.StartRequest(requestID1, "") - require.NoError(t, err) - err = asyncLoader.StartRequest(requestID2, "other") - require.NoError(t, err) - p := testutil.GeneratePeers(1)[0] - resultChan1 := asyncLoader.AsyncLoad(p, requestID1, link, ipld.LinkContext{}) - resultChan2 := asyncLoader.AsyncLoad(p, requestID2, link, ipld.LinkContext{}) - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID1: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, - requestID2: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, - } - asyncLoader.ProcessResponse(context.Background(), responses, blocks) - - assertSuccessResponse(ctx, t, resultChan1) - assertSuccessResponse(ctx, t, resultChan2) - st.AssertBlockStored(t, block) - otherSt.AssertBlockStored(t, block) - }) -} - -func TestRequestSplittingSameBlockOnlyOneResponse(t *testing.T) { - st := newStore() - otherSt := newStore() - blocks := testutil.GenerateBlocksOfSize(1, 100) - block := blocks[0] - link := cidlink.Link{Cid: block.Cid()} - withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - err := asyncLoader.RegisterPersistenceOption("other", otherSt.lsys) - require.NoError(t, err) - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) - err = asyncLoader.StartRequest(requestID1, "") - require.NoError(t, err) - err = asyncLoader.StartRequest(requestID2, "other") - require.NoError(t, err) - p := testutil.GeneratePeers(1)[0] - resultChan1 := asyncLoader.AsyncLoad(p, requestID1, link, ipld.LinkContext{}) - resultChan2 := asyncLoader.AsyncLoad(p, requestID2, link, ipld.LinkContext{}) - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID2: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, - } - asyncLoader.ProcessResponse(context.Background(), responses, blocks) - asyncLoader.CompleteResponsesFor(requestID1) - - assertFailResponse(ctx, t, resultChan1) - assertSuccessResponse(ctx, t, resultChan2) - otherSt.AssertBlockStored(t, block) - }) -} - -type store struct { - internalLoader ipld.BlockReadOpener - lsys ipld.LinkSystem - blockstore map[ipld.Link][]byte - localLoads int - called chan struct{} -} - -func newStore() *store { - blockstore := make(map[ipld.Link][]byte) - st := &store{ - lsys: testutil.NewTestStore(blockstore), - blockstore: blockstore, - localLoads: 0, - called: make(chan struct{}), - } - st.internalLoader = st.lsys.StorageReadOpener - st.lsys.StorageReadOpener = st.loader - return st -} - -func (st *store) loader(lnkCtx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) { - select { - case <-st.called: - default: - close(st.called) - } - st.localLoads++ - return st.internalLoader(lnkCtx, lnk) -} - -func (st *store) AssertLocalLoads(t *testing.T, localLoads int) { - require.Equalf(t, localLoads, st.localLoads, "should have loaded locally %d times", localLoads) -} - -func (st *store) AssertBlockStored(t *testing.T, blk blocks.Block) { - require.Equal(t, blk.RawData(), st.blockstore[cidlink.Link{Cid: blk.Cid()}], "should store block") -} - -func (st *store) AssertAttemptLoadWithoutResult(ctx context.Context, t *testing.T, resultChan <-chan types.AsyncLoadResult) { - testutil.AssertDoesReceiveFirst(t, st.called, "should attempt load with no result", resultChan, ctx.Done()) -} - -func (st *store) Store(t *testing.T, blk blocks.Block) ipld.Link { - writer, commit, err := st.lsys.StorageWriteOpener(ipld.LinkContext{}) - require.NoError(t, err) - _, err = writer.Write(blk.RawData()) - require.NoError(t, err, "seeds block store") - link := cidlink.Link{Cid: blk.Cid()} - err = commit(link) - require.NoError(t, err, "seeds block store") - return link -} - -func withLoader(st *store, exec func(ctx context.Context, asyncLoader *AsyncLoader)) { - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - asyncLoader := New(ctx, st.lsys) - exec(ctx, asyncLoader) -} - -func assertSuccessResponse(ctx context.Context, t *testing.T, resultChan <-chan types.AsyncLoadResult) { - t.Helper() - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.NotNil(t, result.Data, "should send response") - require.Nil(t, result.Err, "should not send error") -} - -func assertFailResponse(ctx context.Context, t *testing.T, resultChan <-chan types.AsyncLoadResult) { - t.Helper() - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.Nil(t, result.Data, "should not send responses") - require.NotNil(t, result.Err, "should send an error") -} diff --git a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue.go b/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue.go deleted file mode 100644 index 618c889d..00000000 --- a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue.go +++ /dev/null @@ -1,96 +0,0 @@ -package loadattemptqueue - -import ( - "errors" - - "github.com/ipld/go-ipld-prime" - "github.com/libp2p/go-libp2p-core/peer" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/requestmanager/types" -) - -// LoadRequest is a request to load the given link for the given request id, -// with results returned to the given channel -type LoadRequest struct { - p peer.ID - requestID graphsync.RequestID - link ipld.Link - linkContext ipld.LinkContext - resultChan chan types.AsyncLoadResult -} - -// NewLoadRequest returns a new LoadRequest for the given request id, link, -// and results channel -func NewLoadRequest( - p peer.ID, - requestID graphsync.RequestID, - link ipld.Link, - linkContext ipld.LinkContext, - resultChan chan types.AsyncLoadResult) LoadRequest { - return LoadRequest{p, requestID, link, linkContext, resultChan} -} - -// LoadAttempter attempts to load a link to an array of bytes -// and returns an async load result -type LoadAttempter func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) types.AsyncLoadResult - -// LoadAttemptQueue attempts to load using the load attempter, and then can -// place requests on a retry queue -type LoadAttemptQueue struct { - loadAttempter LoadAttempter - pausedRequests []LoadRequest -} - -// New initializes a new AsyncLoader from loadAttempter function -func New(loadAttempter LoadAttempter) *LoadAttemptQueue { - return &LoadAttemptQueue{ - loadAttempter: loadAttempter, - } -} - -// AttemptLoad attempts to loads the given load request, and if retry is true -// it saves the loadrequest for retrying later -func (laq *LoadAttemptQueue) AttemptLoad(lr LoadRequest, retry bool) { - response := laq.loadAttempter(lr.p, lr.requestID, lr.link, lr.linkContext) - if response.Err != nil || response.Data != nil { - lr.resultChan <- response - close(lr.resultChan) - return - } - if !retry { - laq.terminateWithError("No active request", lr.resultChan) - return - } - laq.pausedRequests = append(laq.pausedRequests, lr) -} - -// ClearRequest purges the given request from the queue of load requests -// to retry -func (laq *LoadAttemptQueue) ClearRequest(requestID graphsync.RequestID) { - pausedRequests := laq.pausedRequests - laq.pausedRequests = nil - for _, lr := range pausedRequests { - if lr.requestID == requestID { - laq.terminateWithError("No active request", lr.resultChan) - } else { - laq.pausedRequests = append(laq.pausedRequests, lr) - } - } -} - -// RetryLoads attempts loads on all saved load requests that were loaded with -// retry = true -func (laq *LoadAttemptQueue) RetryLoads() { - // drain buffered - pausedRequests := laq.pausedRequests - laq.pausedRequests = nil - for _, lr := range pausedRequests { - laq.AttemptLoad(lr, true) - } -} - -func (laq *LoadAttemptQueue) terminateWithError(errMsg string, resultChan chan<- types.AsyncLoadResult) { - resultChan <- types.AsyncLoadResult{Data: nil, Err: errors.New(errMsg)} - close(resultChan) -} diff --git a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go b/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go deleted file mode 100644 index ae992711..00000000 --- a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package loadattemptqueue - -import ( - "context" - "fmt" - "math/rand" - "testing" - "time" - - ipld "github.com/ipld/go-ipld-prime" - "github.com/libp2p/go-libp2p-core/peer" - "github.com/stretchr/testify/require" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/requestmanager/types" - "github.com/ipfs/go-graphsync/testutil" -) - -func TestAsyncLoadInitialLoadSucceeds(t *testing.T) { - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - callCount := 0 - loadAttempter := func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) types.AsyncLoadResult { - callCount++ - return types.AsyncLoadResult{ - Data: testutil.RandomBytes(100), - } - } - loadAttemptQueue := New(loadAttempter) - - link := testutil.NewTestLink() - linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) - p := testutil.GeneratePeers(1)[0] - - resultChan := make(chan types.AsyncLoadResult, 1) - lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) - loadAttemptQueue.AttemptLoad(lr, false) - - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.NotNil(t, result.Data, "should send response") - require.Nil(t, result.Err, "should not send error") - - require.NotZero(t, callCount, "should attempt to load link from local store") -} - -func TestAsyncLoadInitialLoadFails(t *testing.T) { - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - callCount := 0 - loadAttempter := func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) types.AsyncLoadResult { - callCount++ - return types.AsyncLoadResult{ - Err: fmt.Errorf("something went wrong"), - } - } - loadAttemptQueue := New(loadAttempter) - - link := testutil.NewTestLink() - linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) - resultChan := make(chan types.AsyncLoadResult, 1) - p := testutil.GeneratePeers(1)[0] - - lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) - loadAttemptQueue.AttemptLoad(lr, false) - - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.Nil(t, result.Data, "should not send responses") - require.NotNil(t, result.Err, "should send an error") - require.NotZero(t, callCount, "should attempt to load link from local store") -} - -func TestAsyncLoadInitialLoadIndeterminateRetryFalse(t *testing.T) { - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - callCount := 0 - loadAttempter := func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) types.AsyncLoadResult { - var result []byte - if callCount > 0 { - result = testutil.RandomBytes(100) - } - callCount++ - return types.AsyncLoadResult{ - Data: result, - } - } - - loadAttemptQueue := New(loadAttempter) - - link := testutil.NewTestLink() - linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) - p := testutil.GeneratePeers(1)[0] - - resultChan := make(chan types.AsyncLoadResult, 1) - lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) - loadAttemptQueue.AttemptLoad(lr, false) - - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.Nil(t, result.Data, "should not send responses") - require.NotNil(t, result.Err, "should send an error") - require.Equal(t, 1, callCount, "should attempt to load once and then not retry") -} - -func TestAsyncLoadInitialLoadIndeterminateRetryTrueThenRetriedSuccess(t *testing.T) { - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - callCount := 0 - called := make(chan struct{}, 2) - loadAttempter := func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) types.AsyncLoadResult { - var result []byte - called <- struct{}{} - if callCount > 0 { - result = testutil.RandomBytes(100) - } - callCount++ - return types.AsyncLoadResult{ - Data: result, - } - } - loadAttemptQueue := New(loadAttempter) - - link := testutil.NewTestLink() - linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) - resultChan := make(chan types.AsyncLoadResult, 1) - p := testutil.GeneratePeers(1)[0] - lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) - loadAttemptQueue.AttemptLoad(lr, true) - - testutil.AssertDoesReceiveFirst(t, called, "should attempt load with no result", resultChan, ctx.Done()) - loadAttemptQueue.RetryLoads() - - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.NotNil(t, result.Data, "should send response") - require.Nil(t, result.Err, "should not send error") - require.Equal(t, 2, callCount, "should attempt to load multiple times till success") -} - -func TestAsyncLoadInitialLoadIndeterminateThenRequestFinishes(t *testing.T) { - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - callCount := 0 - called := make(chan struct{}, 2) - loadAttempter := func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) types.AsyncLoadResult { - var result []byte - called <- struct{}{} - if callCount > 0 { - result = testutil.RandomBytes(100) - } - callCount++ - return types.AsyncLoadResult{ - Data: result, - } - } - loadAttemptQueue := New(loadAttempter) - - link := testutil.NewTestLink() - linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) - resultChan := make(chan types.AsyncLoadResult, 1) - p := testutil.GeneratePeers(1)[0] - lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) - loadAttemptQueue.AttemptLoad(lr, true) - - testutil.AssertDoesReceiveFirst(t, called, "should attempt load with no result", resultChan, ctx.Done()) - loadAttemptQueue.ClearRequest(requestID) - loadAttemptQueue.RetryLoads() - - var result types.AsyncLoadResult - testutil.AssertReceive(ctx, t, resultChan, &result, "should close response channel with response") - require.Nil(t, result.Data, "should not send responses") - require.NotNil(t, result.Err, "should send an error") - require.Equal(t, 1, callCount, "should attempt to load only once because request is finised") -} diff --git a/requestmanager/asyncloader/responsecache/responsecache.go b/requestmanager/asyncloader/responsecache/responsecache.go deleted file mode 100644 index 69c3e902..00000000 --- a/requestmanager/asyncloader/responsecache/responsecache.go +++ /dev/null @@ -1,107 +0,0 @@ -package responsecache - -import ( - "context" - "sync" - - blocks "github.com/ipfs/go-block-format" - logging "github.com/ipfs/go-log/v2" - "github.com/ipld/go-ipld-prime" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/linktracker" - "github.com/ipfs/go-graphsync/metadata" -) - -var log = logging.Logger("graphsync") - -// UnverifiedBlockStore is an interface for storing blocks -// as they come in and removing them as they are verified -type UnverifiedBlockStore interface { - PruneBlocks(func(ipld.Link, uint64) bool) - PruneBlock(ipld.Link) - VerifyBlock(ipld.Link, ipld.LinkContext) ([]byte, error) - AddUnverifiedBlock(trace.Link, ipld.Link, []byte) -} - -// ResponseCache maintains a store of unverified blocks and response -// data about links for loading, and prunes blocks as needed. -type ResponseCache struct { - responseCacheLk sync.RWMutex - - linkTracker *linktracker.LinkTracker - unverifiedBlockStore UnverifiedBlockStore -} - -// New initializes a new ResponseCache using the given unverified block store. -func New(unverifiedBlockStore UnverifiedBlockStore) *ResponseCache { - return &ResponseCache{ - linkTracker: linktracker.New(), - unverifiedBlockStore: unverifiedBlockStore, - } -} - -// FinishRequest indicate there is no more need to track blocks tied to this -// response. It returns the total number of bytes in blocks that were being -// tracked but are no longer in memory -func (rc *ResponseCache) FinishRequest(requestID graphsync.RequestID) { - rc.responseCacheLk.Lock() - rc.linkTracker.FinishRequest(requestID) - - rc.unverifiedBlockStore.PruneBlocks(func(link ipld.Link, amt uint64) bool { - return rc.linkTracker.BlockRefCount(link) == 0 - }) - rc.responseCacheLk.Unlock() -} - -// AttemptLoad attempts to laod the given block from the cache -func (rc *ResponseCache) AttemptLoad(requestID graphsync.RequestID, link ipld.Link, linkContext ipld.LinkContext) ([]byte, error) { - rc.responseCacheLk.Lock() - defer rc.responseCacheLk.Unlock() - if rc.linkTracker.IsKnownMissingLink(requestID, link) { - return nil, graphsync.RemoteMissingBlockErr{Link: link} - } - data, _ := rc.unverifiedBlockStore.VerifyBlock(link, linkContext) - return data, nil -} - -// ProcessResponse processes incoming response data, adding unverified blocks, -// and tracking link metadata from a remote peer -func (rc *ResponseCache) ProcessResponse( - ctx context.Context, - responses map[graphsync.RequestID]metadata.Metadata, - blks []blocks.Block) { - - ctx, span := otel.Tracer("graphsync").Start(ctx, "cacheProcess", trace.WithAttributes( - attribute.Int("blockCount", len(blks)), - )) - traceLink := trace.LinkFromContext(ctx) - defer span.End() - - rc.responseCacheLk.Lock() - - for _, block := range blks { - log.Debugf("Received block from network: %s", block.Cid().String()) - rc.unverifiedBlockStore.AddUnverifiedBlock(traceLink, cidlink.Link{Cid: block.Cid()}, block.RawData()) - } - - for requestID, md := range responses { - for _, item := range md { - log.Debugf("Traverse link %s on request ID %d", item.Link.String(), requestID) - rc.linkTracker.RecordLinkTraversal(requestID, cidlink.Link{Cid: item.Link}, item.BlockPresent) - } - } - - // prune unused blocks right away - for _, block := range blks { - if rc.linkTracker.BlockRefCount(cidlink.Link{Cid: block.Cid()}) == 0 { - rc.unverifiedBlockStore.PruneBlock(cidlink.Link{Cid: block.Cid()}) - } - } - - rc.responseCacheLk.Unlock() -} diff --git a/requestmanager/asyncloader/responsecache/responsecache_test.go b/requestmanager/asyncloader/responsecache/responsecache_test.go deleted file mode 100644 index 438fdc30..00000000 --- a/requestmanager/asyncloader/responsecache/responsecache_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package responsecache - -import ( - "context" - "fmt" - "math/rand" - "testing" - - blocks "github.com/ipfs/go-block-format" - ipld "github.com/ipld/go-ipld-prime" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" - "github.com/ipfs/go-graphsync/testutil" -) - -type fakeUnverifiedBlockStore struct { - inMemoryBlocks map[ipld.Link][]byte -} - -func (ubs *fakeUnverifiedBlockStore) AddUnverifiedBlock(_ trace.Link, lnk ipld.Link, data []byte) { - ubs.inMemoryBlocks[lnk] = data -} - -func (ubs *fakeUnverifiedBlockStore) PruneBlocks(shouldPrune func(ipld.Link, uint64) bool) { - for link, data := range ubs.inMemoryBlocks { - if shouldPrune(link, uint64(len(data))) { - delete(ubs.inMemoryBlocks, link) - } - } -} - -func (ubs *fakeUnverifiedBlockStore) PruneBlock(link ipld.Link) { - delete(ubs.inMemoryBlocks, link) -} - -func (ubs *fakeUnverifiedBlockStore) VerifyBlock(lnk ipld.Link, linkCtx ipld.LinkContext) ([]byte, error) { - data, ok := ubs.inMemoryBlocks[lnk] - if !ok { - return nil, fmt.Errorf("Block not found") - } - delete(ubs.inMemoryBlocks, lnk) - return data, nil -} - -func (ubs *fakeUnverifiedBlockStore) blocks() []blocks.Block { - blks := make([]blocks.Block, 0, len(ubs.inMemoryBlocks)) - for link, data := range ubs.inMemoryBlocks { - blk, err := blocks.NewBlockWithCid(data, link.(cidlink.Link).Cid) - if err == nil { - blks = append(blks, blk) - } - } - return blks -} - -func TestResponseCacheManagingLinks(t *testing.T) { - blks := testutil.GenerateBlocksOfSize(5, 100) - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) - - request1Metadata := metadata.Metadata{ - metadata.Item{ - Link: blks[0].Cid(), - BlockPresent: true, - }, - metadata.Item{ - Link: blks[1].Cid(), - BlockPresent: false, - }, - metadata.Item{ - Link: blks[3].Cid(), - BlockPresent: true, - }, - } - - request2Metadata := metadata.Metadata{ - metadata.Item{ - Link: blks[1].Cid(), - BlockPresent: true, - }, - metadata.Item{ - Link: blks[3].Cid(), - BlockPresent: true, - }, - metadata.Item{ - Link: blks[4].Cid(), - BlockPresent: true, - }, - } - - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID1: request1Metadata, - requestID2: request2Metadata, - } - - fubs := &fakeUnverifiedBlockStore{ - inMemoryBlocks: make(map[ipld.Link][]byte), - } - responseCache := New(fubs) - - responseCache.ProcessResponse(context.Background(), responses, blks) - - require.Len(t, fubs.blocks(), len(blks)-1, "should prune block with no references") - testutil.RefuteContainsBlock(t, fubs.blocks(), blks[2]) - - lnkCtx := ipld.LinkContext{} - // should load block from unverified block store - data, err := responseCache.AttemptLoad(requestID2, cidlink.Link{Cid: blks[4].Cid()}, lnkCtx) - require.NoError(t, err) - require.Equal(t, blks[4].RawData(), data, "did not load correct block") - - // which will remove block - require.Len(t, fubs.blocks(), len(blks)-2, "should prune block once verified") - testutil.RefuteContainsBlock(t, fubs.blocks(), blks[4]) - - // fails as it is a known missing block - data, err = responseCache.AttemptLoad(requestID1, cidlink.Link{Cid: blks[1].Cid()}, lnkCtx) - require.Error(t, err) - require.Nil(t, data, "no data should be returned for missing block") - - // should succeed for request 2 where it's not a missing block - data, err = responseCache.AttemptLoad(requestID2, cidlink.Link{Cid: blks[1].Cid()}, lnkCtx) - require.NoError(t, err) - require.Equal(t, blks[1].RawData(), data) - - // which will remove block - require.Len(t, fubs.blocks(), len(blks)-3, "should prune block once verified") - testutil.RefuteContainsBlock(t, fubs.blocks(), blks[1]) - - // should be unknown result as block is not known missing or present in block store - data, err = responseCache.AttemptLoad(requestID1, cidlink.Link{Cid: blks[2].Cid()}, lnkCtx) - require.NoError(t, err) - require.Nil(t, data, "no data should be returned for unknown block") - - responseCache.FinishRequest(requestID1) - // should remove only block 0, since it now has no refering outstanding requests - require.Len(t, fubs.blocks(), len(blks)-4, "should prune block when it is orphaned") - testutil.RefuteContainsBlock(t, fubs.blocks(), blks[0]) - - responseCache.FinishRequest(requestID2) - // should remove last block since are no remaining references - require.Len(t, fubs.blocks(), 0, "should prune block when it is orphaned") - testutil.RefuteContainsBlock(t, fubs.blocks(), blks[3]) -} diff --git a/requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore.go b/requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore.go deleted file mode 100644 index 2478b0f5..00000000 --- a/requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore.go +++ /dev/null @@ -1,109 +0,0 @@ -package unverifiedblockstore - -import ( - "context" - "fmt" - - logging "github.com/ipfs/go-log/v2" - ipld "github.com/ipld/go-ipld-prime" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -var log = logging.Logger("gs-unverifiedbs") - -type settableWriter interface { - SetBytes([]byte) error -} - -// UnverifiedBlockStore holds an in memory cache of receied blocks from the network -// that have not been verified to be part of a traversal -type UnverifiedBlockStore struct { - inMemoryBlocks map[ipld.Link]tracedBlock - storer ipld.BlockWriteOpener - dataSize uint64 -} - -type tracedBlock struct { - block []byte - traceLink trace.Link -} - -// New initializes a new unverified store with the given storer function for writing -// to permaneant storage if the block is verified -func New(storer ipld.BlockWriteOpener) *UnverifiedBlockStore { - return &UnverifiedBlockStore{ - inMemoryBlocks: make(map[ipld.Link]tracedBlock), - storer: storer, - } -} - -// AddUnverifiedBlock adds a new unverified block to the in memory cache as it -// comes in as part of a traversal. -func (ubs *UnverifiedBlockStore) AddUnverifiedBlock(traceLink trace.Link, lnk ipld.Link, data []byte) { - ubs.inMemoryBlocks[lnk] = tracedBlock{data, traceLink} - ubs.dataSize = ubs.dataSize + uint64(len(data)) - log.Debugw("added in-memory block", "total_queued_bytes", ubs.dataSize) -} - -// PruneBlocks removes blocks from the unverified store without committing them, -// if the passed in function returns true for the given link -func (ubs *UnverifiedBlockStore) PruneBlocks(shouldPrune func(ipld.Link, uint64) bool) { - for link, data := range ubs.inMemoryBlocks { - if shouldPrune(link, uint64(len(data.block))) { - delete(ubs.inMemoryBlocks, link) - ubs.dataSize = ubs.dataSize - uint64(len(data.block)) - } - } - log.Debugw("finished pruning in-memory blocks", "total_queued_bytes", ubs.dataSize) -} - -// PruneBlock deletes an individual block from the store -func (ubs *UnverifiedBlockStore) PruneBlock(link ipld.Link) { - delete(ubs.inMemoryBlocks, link) - ubs.dataSize = ubs.dataSize - uint64(len(ubs.inMemoryBlocks[link].block)) - log.Debugw("pruned in-memory block", "total_queued_bytes", ubs.dataSize) -} - -// VerifyBlock verifies the data for the given link as being part of a traversal, -// removes it from the unverified store, and writes it to permaneant storage. -func (ubs *UnverifiedBlockStore) VerifyBlock(lnk ipld.Link, linkContext ipld.LinkContext) ([]byte, error) { - data, ok := ubs.inMemoryBlocks[lnk] - if !ok { - return nil, fmt.Errorf("block not found") - } - - ctx := linkContext.Ctx - if ctx == nil { - ctx = context.Background() - } - _, span := otel.Tracer("graphsync").Start( - ctx, - "verifyBlock", - trace.WithLinks(data.traceLink), - trace.WithAttributes(attribute.String("cid", lnk.String()))) - defer span.End() - - delete(ubs.inMemoryBlocks, lnk) - ubs.dataSize = ubs.dataSize - uint64(len(data.block)) - log.Debugw("verified block", "total_queued_bytes", ubs.dataSize) - - buffer, committer, err := ubs.storer(linkContext) - if err != nil { - return nil, err - } - if settable, ok := buffer.(settableWriter); ok { - err = settable.SetBytes(data.block) - } else { - _, err = buffer.Write(data.block) - } - if err != nil { - return nil, err - } - err = committer(lnk) - if err != nil { - return nil, err - } - return data.block, nil -} diff --git a/requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore_test.go b/requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore_test.go deleted file mode 100644 index 92bd66e5..00000000 --- a/requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package unverifiedblockstore - -import ( - "bytes" - "io" - "testing" - - "github.com/ipld/go-ipld-prime" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" - - "github.com/ipfs/go-graphsync/testutil" -) - -func TestVerifyBlockPresent(t *testing.T) { - blocksWritten := make(map[ipld.Link][]byte) - lsys := testutil.NewTestStore(blocksWritten) - unverifiedBlockStore := New(lsys.StorageWriteOpener) - block := testutil.GenerateBlocksOfSize(1, 100)[0] - reader, err := lsys.StorageReadOpener(ipld.LinkContext{}, cidlink.Link{Cid: block.Cid()}) - require.Nil(t, reader) - require.Error(t, err, "block should not be loadable till it's verified and stored") - - data, err := unverifiedBlockStore.VerifyBlock(cidlink.Link{Cid: block.Cid()}, ipld.LinkContext{}) - require.Nil(t, data) - require.Error(t, err, "block should not be verifiable till it's added as an unverifiable block") - - unverifiedBlockStore.AddUnverifiedBlock(trace.Link{}, cidlink.Link{Cid: block.Cid()}, block.RawData()) - reader, err = lsys.StorageReadOpener(ipld.LinkContext{}, cidlink.Link{Cid: block.Cid()}) - require.Nil(t, reader) - require.Error(t, err, "block should not be loadable till it's verified") - - data, err = unverifiedBlockStore.VerifyBlock(cidlink.Link{Cid: block.Cid()}, ipld.LinkContext{}) - require.NoError(t, err) - require.Equal(t, block.RawData(), data, "block should be returned on verification if added") - - reader, err = lsys.StorageReadOpener(ipld.LinkContext{}, cidlink.Link{Cid: block.Cid()}) - require.NoError(t, err) - var buffer bytes.Buffer - _, err = io.Copy(&buffer, reader) - require.NoError(t, err) - require.Equal(t, block.RawData(), buffer.Bytes(), "block should be stored and loadable after verification") - data, err = unverifiedBlockStore.VerifyBlock(cidlink.Link{Cid: block.Cid()}, ipld.LinkContext{}) - require.Nil(t, data) - require.Error(t, err, "block cannot be verified twice") -} diff --git a/requestmanager/client.go b/requestmanager/client.go index c58cad78..885f4f5f 100644 --- a/requestmanager/client.go +++ b/requestmanager/client.go @@ -23,13 +23,12 @@ import ( "github.com/ipfs/go-graphsync/listeners" gsmsg "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/messagequeue" - "github.com/ipfs/go-graphsync/metadata" "github.com/ipfs/go-graphsync/network" "github.com/ipfs/go-graphsync/notifications" "github.com/ipfs/go-graphsync/peerstate" "github.com/ipfs/go-graphsync/requestmanager/executor" "github.com/ipfs/go-graphsync/requestmanager/hooks" - "github.com/ipfs/go-graphsync/requestmanager/types" + "github.com/ipfs/go-graphsync/requestmanager/reconciledloader" "github.com/ipfs/go-graphsync/taskqueue" ) @@ -62,6 +61,8 @@ type inProgressRequestStatus struct { inProgressErr chan error traverser ipldutil.Traverser traverserCancel context.CancelFunc + lsys *ipld.LinkSystem + reconciledLoader *reconciledloader.ReconciledLoader } // PeerHandler is an interface that can send requests to peers @@ -69,34 +70,27 @@ type PeerHandler interface { AllocateAndBuildMessage(p peer.ID, blkSize uint64, buildMessageFn func(*messagequeue.Builder)) } -// AsyncLoader is an interface for loading links asynchronously, returning -// results as new responses are processed -type AsyncLoader interface { - StartRequest(graphsync.RequestID, string) error - ProcessResponse(ctx context.Context, responses map[graphsync.RequestID]metadata.Metadata, - blks []blocks.Block) - AsyncLoad(p peer.ID, requestID graphsync.RequestID, link ipld.Link, linkContext ipld.LinkContext) <-chan types.AsyncLoadResult - CompleteResponsesFor(requestID graphsync.RequestID) - CleanupRequest(p peer.ID, requestID graphsync.RequestID) +// PersistenceOptions is an interface for getting loaders by name +type PersistenceOptions interface { + GetLinkSystem(name string) (ipld.LinkSystem, bool) } // RequestManager tracks outgoing requests and processes incoming reponses // to them. type RequestManager struct { - ctx context.Context - cancel context.CancelFunc - messages chan requestManagerMessage - peerHandler PeerHandler - rc *responseCollector - asyncLoader AsyncLoader - disconnectNotif *pubsub.PubSub - linkSystem ipld.LinkSystem - connManager network.ConnManager + ctx context.Context + cancel context.CancelFunc + messages chan requestManagerMessage + peerHandler PeerHandler + rc *responseCollector + persistenceOptions PersistenceOptions + disconnectNotif *pubsub.PubSub + linkSystem ipld.LinkSystem + connManager network.ConnManager // maximum number of links to traverse per request. A value of zero = infinity, or no limit maxLinksPerRequest uint64 // dont touch out side of run loop - nextRequestID graphsync.RequestID inProgressRequestStatuses map[graphsync.RequestID]*inProgressRequestStatus requestHooks RequestHooks responseHooks ResponseHooks @@ -121,7 +115,7 @@ type ResponseHooks interface { // New generates a new request manager from a context, network, and selectorQuerier func New(ctx context.Context, - asyncLoader AsyncLoader, + persistenceOptions PersistenceOptions, linkSystem ipld.LinkSystem, requestHooks RequestHooks, responseHooks ResponseHooks, @@ -135,7 +129,7 @@ func New(ctx context.Context, return &RequestManager{ ctx: ctx, cancel: cancel, - asyncLoader: asyncLoader, + persistenceOptions: persistenceOptions, disconnectNotif: pubsub.New(disconnectDispatcher), linkSystem: linkSystem, rc: newResponseCollector(ctx), @@ -287,16 +281,18 @@ func (rm *RequestManager) CancelRequest(ctx context.Context, requestID graphsync // ProcessResponses ingests the given responses from the network and // and updates the in progress requests based on those responses. -func (rm *RequestManager) ProcessResponses(p peer.ID, responses []gsmsg.GraphSyncResponse, +func (rm *RequestManager) ProcessResponses(p peer.ID, + responses []gsmsg.GraphSyncResponse, blks []blocks.Block) { + rm.send(&processResponsesMessage{p, responses, blks}, nil) } // UnpauseRequest unpauses a request that was paused in a block hook based request ID // Can also send extensions with unpause -func (rm *RequestManager) UnpauseRequest(requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { +func (rm *RequestManager) UnpauseRequest(ctx context.Context, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { response := make(chan error, 1) - rm.send(&unpauseRequestMessage{requestID, extensions, response}, nil) + rm.send(&unpauseRequestMessage{requestID, extensions, response}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") @@ -306,9 +302,21 @@ func (rm *RequestManager) UnpauseRequest(requestID graphsync.RequestID, extensio } // PauseRequest pauses an in progress request (may take 1 or more blocks to process) -func (rm *RequestManager) PauseRequest(requestID graphsync.RequestID) error { +func (rm *RequestManager) PauseRequest(ctx context.Context, requestID graphsync.RequestID) error { + response := make(chan error, 1) + rm.send(&pauseRequestMessage{requestID, response}, ctx.Done()) + select { + case <-rm.ctx.Done(): + return errors.New("context cancelled") + case err := <-response: + return err + } +} + +// UpdateRequest updates an in progress request +func (rm *RequestManager) UpdateRequest(ctx context.Context, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { response := make(chan error, 1) - rm.send(&pauseRequestMessage{requestID, response}, nil) + rm.send(&updateRequestMessage{requestID, extensions, response}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") diff --git a/requestmanager/executor/executor.go b/requestmanager/executor/executor.go index 92516c1e..507cba02 100644 --- a/requestmanager/executor/executor.go +++ b/requestmanager/executor/executor.go @@ -7,7 +7,8 @@ import ( logging "github.com/ipfs/go-log/v2" "github.com/ipfs/go-peertaskqueue/peertask" - "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" "github.com/ipld/go-ipld-prime/traversal" "github.com/libp2p/go-libp2p-core/peer" "go.opentelemetry.io/otel" @@ -36,9 +37,12 @@ type BlockHooks interface { ProcessBlockHooks(p peer.ID, response graphsync.ResponseData, block graphsync.BlockData) hooks.UpdateResult } -// AsyncLoadFn is a function which given a request id and an ipld.Link, returns -// a channel which will eventually return data for the link or an err -type AsyncLoadFn func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) <-chan types.AsyncLoadResult +// ReconciledLoader is an interface that can be used to load blocks from a local store or a remote request +type ReconciledLoader interface { + SetRemoteOnline(online bool) + RetryLastLoad() types.AsyncLoadResult + BlockReadOpener(lctx linking.LinkContext, link datamodel.Link) types.AsyncLoadResult +} // Executor handles actually executing graphsync requests and verifying them. // It has control of requests when they are in the "running" state, while @@ -46,18 +50,15 @@ type AsyncLoadFn func(peer.ID, graphsync.RequestID, ipld.Link, ipld.LinkContext) type Executor struct { manager Manager blockHooks BlockHooks - loader AsyncLoadFn } // NewExecutor returns a new executor func NewExecutor( manager Manager, - blockHooks BlockHooks, - loader AsyncLoadFn) *Executor { + blockHooks BlockHooks) *Executor { return &Executor{ manager: manager, blockHooks: blockHooks, - loader: loader, } } @@ -83,7 +84,8 @@ func (e *Executor) ExecuteTask(ctx context.Context, pid peer.ID, task *peertask. if err != nil { span.RecordError(err) if !ipldutil.IsContextCancelErr(err) { - e.manager.SendRequest(requestTask.P, gsmsg.CancelRequest(requestTask.Request.ID())) + e.manager.SendRequest(requestTask.P, gsmsg.NewCancelRequest(requestTask.Request.ID())) + requestTask.ReconciledLoader.SetRemoteOnline(false) if !isPausedErr(err) { span.SetStatus(codes.Error, err.Error()) select { @@ -110,17 +112,12 @@ type RequestTask struct { P peer.ID InProgressErr chan error Empty bool - InitialRequest bool + ReconciledLoader ReconciledLoader } func (e *Executor) traverse(rt RequestTask) error { - onlyOnce := &onlyOnce{e, rt, false} + requestSent := false // for initial request, start remote right away - if rt.InitialRequest { - if err := onlyOnce.startRemoteRequest(); err != nil { - return err - } - } for { // check if traversal is complete isComplete, err := rt.Traverser.IsComplete() @@ -131,23 +128,20 @@ func (e *Executor) traverse(rt RequestTask) error { lnk, linkContext := rt.Traverser.CurrentRequest() // attempt to load log.Debugf("will load link=%s", lnk) - resultChan := e.loader(rt.P, rt.Request.ID(), lnk, linkContext) - var result types.AsyncLoadResult - // check for immediate result - select { - case result = <-resultChan: - default: - // if no immediate result - // initiate remote request if not already sent (we want to fill out the doNotSendCids on a resume) - if err := onlyOnce.startRemoteRequest(); err != nil { + result := rt.ReconciledLoader.BlockReadOpener(linkContext, lnk) + // if we've only loaded locally so far and hit a missing block + // initiate remote request and retry the load operation from remote + if _, ok := result.Err.(graphsync.RemoteMissingBlockErr); ok && !requestSent { + requestSent = true + + // tell the loader we're online now + rt.ReconciledLoader.SetRemoteOnline(true) + + if err := e.startRemoteRequest(rt); err != nil { return err } - // wait for block result - select { - case <-rt.Ctx.Done(): - return ipldutil.ContextCancelError{} - case result = <-resultChan: - } + // retry the load + result = rt.ReconciledLoader.RetryLastLoad() } log.Debugf("successfully loaded link=%s, nBlocksRead=%d", lnk, rt.Traverser.NBlocksTraversed()) // advance the traversal based on results @@ -161,14 +155,13 @@ func (e *Executor) traverse(rt RequestTask) error { if err != nil { return err } - } } func (e *Executor) processBlockHooks(p peer.ID, response graphsync.ResponseData, block graphsync.BlockData) error { result := e.blockHooks.ProcessBlockHooks(p, response, block) if len(result.Extensions) > 0 { - updateRequest := gsmsg.UpdateRequest(response.RequestID(), result.Extensions...) + updateRequest := gsmsg.NewUpdateRequest(response.RequestID(), result.Extensions...) e.manager.SendRequest(p, updateRequest) } return result.Err @@ -191,14 +184,18 @@ func (e *Executor) advanceTraversal(rt RequestTask, result types.AsyncLoadResult case <-rt.Ctx.Done(): return ipldutil.ContextCancelError{} case rt.InProgressErr <- result.Err: - rt.Traverser.Error(traversal.SkipMe{}) + if _, ok := result.Err.(graphsync.RemoteMissingBlockErr); ok { + rt.Traverser.Error(traversal.SkipMe{}) + } else { + rt.Traverser.Error(result.Err) + } return nil } } return rt.Traverser.Advance(bytes.NewBuffer(result.Data)) } -func (e *Executor) processResult(rt RequestTask, link ipld.Link, result types.AsyncLoadResult) error { +func (e *Executor) processResult(rt RequestTask, link datamodel.Link, result types.AsyncLoadResult) error { var err error if result.Err == nil { err = e.onNewBlock(rt, &blockData{link, result.Local, uint64(len(result.Data)), int64(rt.Traverser.NBlocksTraversed())}) @@ -220,10 +217,7 @@ func (e *Executor) startRemoteRequest(rt RequestTask) error { doNotSendFirstBlocks = int64(rt.Traverser.NBlocksTraversed()) } if doNotSendFirstBlocks > 0 { - doNotSendFirstBlocksData, err := donotsendfirstblocks.EncodeDoNotSendFirstBlocks(doNotSendFirstBlocks) - if err != nil { - return err - } + doNotSendFirstBlocksData := donotsendfirstblocks.EncodeDoNotSendFirstBlocks(doNotSendFirstBlocks) request = rt.Request.ReplaceExtensions([]graphsync.ExtensionData{{Name: graphsync.ExtensionsDoNotSendFirstBlocks, Data: doNotSendFirstBlocksData}}) } log.Debugw("starting remote request", "id", rt.Request.ID(), "peer", rt.P.String(), "root_cid", rt.Request.Root().String()) @@ -236,29 +230,15 @@ func isPausedErr(err error) bool { return isPaused } -type onlyOnce struct { - e *Executor - rt RequestTask - requestSent bool -} - -func (so *onlyOnce) startRemoteRequest() error { - if so.requestSent { - return nil - } - so.requestSent = true - return so.e.startRemoteRequest(so.rt) -} - type blockData struct { - link ipld.Link + link datamodel.Link local bool size uint64 index int64 } // Link is the link/cid for the block -func (bd *blockData) Link() ipld.Link { +func (bd *blockData) Link() datamodel.Link { return bd.link } diff --git a/requestmanager/executor/executor_test.go b/requestmanager/executor/executor_test.go index 1b181ad1..a3e8562b 100644 --- a/requestmanager/executor/executor_test.go +++ b/requestmanager/executor/executor_test.go @@ -4,13 +4,17 @@ import ( "context" "errors" "math/rand" + "sync" "sync/atomic" "testing" "time" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-peertaskqueue/peertask" - "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/traversal" "github.com/libp2p/go-libp2p-core/peer" "github.com/stretchr/testify/require" @@ -21,7 +25,6 @@ import ( gsmsg "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/requestmanager/executor" "github.com/ipfs/go-graphsync/requestmanager/hooks" - "github.com/ipfs/go-graphsync/requestmanager/testloader" "github.com/ipfs/go-graphsync/requestmanager/types" "github.com/ipfs/go-graphsync/testutil" ) @@ -44,15 +47,15 @@ func TestRequestExecutionBlockChain(t *testing.T) { configureRequestExecution: func(p peer.ID, requestID graphsync.RequestID, tbc *testutil.TestBlockChain, ree *requestExecutionEnv) { ree.customRemoteBehavior = func() { // pretend the remote sent five blocks before encountering a missing block - ree.fal.SuccessResponseOn(p, requestID, tbc.Blocks(0, 5)) + ree.reconciledLoader.successResponseOn(tbc.Blocks(0, 5)) missingCid := cidlink.Link{Cid: tbc.Blocks(5, 6)[0].Cid()} - ree.fal.ResponseOn(p, requestID, missingCid, types.AsyncLoadResult{Err: graphsync.RemoteMissingBlockErr{Link: missingCid}}) + ree.reconciledLoader.responseOn(missingCid, types.AsyncLoadResult{Err: graphsync.RemoteMissingBlockErr{Link: missingCid, Path: tbc.PathTipIndex(5)}}) } }, verifyResults: func(t *testing.T, tbc *testutil.TestBlockChain, ree *requestExecutionEnv, responses []graphsync.ResponseProgress, receivedErrors []error) { tbc.VerifyResponseRangeSync(responses, 0, 5) require.Len(t, receivedErrors, 1) - require.Equal(t, receivedErrors[0], graphsync.RemoteMissingBlockErr{Link: cidlink.Link{Cid: tbc.Blocks(5, 6)[0].Cid()}}) + require.Equal(t, receivedErrors[0], graphsync.RemoteMissingBlockErr{Link: cidlink.Link{Cid: tbc.Blocks(5, 6)[0].Cid()}, Path: tbc.PathTipIndex(5)}) require.Equal(t, []requestSent{{ree.p, ree.request}}, ree.requestsSent) // we should only call block hooks for blocks we actually received require.Len(t, ree.blookHooksCalled, 5) @@ -70,7 +73,7 @@ func TestRequestExecutionBlockChain(t *testing.T) { require.Regexp(t, "something went wrong", receivedErrors[0].Error()) require.Len(t, ree.requestsSent, 2) require.Equal(t, ree.request, ree.requestsSent[0].request) - require.True(t, ree.requestsSent[1].request.IsCancel()) + require.Equal(t, ree.requestsSent[1].request.Type(), graphsync.RequestTypeCancel) require.Len(t, ree.blookHooksCalled, 6) require.EqualError(t, ree.terminalError, "something went wrong") }, @@ -96,7 +99,7 @@ func TestRequestExecutionBlockChain(t *testing.T) { require.Empty(t, receivedErrors) require.Len(t, ree.requestsSent, 2) require.Equal(t, ree.request, ree.requestsSent[0].request) - require.True(t, ree.requestsSent[1].request.IsCancel()) + require.Equal(t, ree.requestsSent[1].request.Type(), graphsync.RequestTypeCancel) require.Len(t, ree.blookHooksCalled, 6) require.EqualError(t, ree.terminalError, hooks.ErrPaused{}.Error()) }, @@ -128,7 +131,7 @@ func TestRequestExecutionBlockChain(t *testing.T) { tbc.VerifyResponseRangeSync(responses, 0, 6) require.Empty(t, receivedErrors) require.Equal(t, ree.request, ree.requestsSent[0].request) - require.True(t, ree.requestsSent[1].request.IsCancel()) + require.Equal(t, ree.requestsSent[1].request.Type(), graphsync.RequestTypeCancel) require.Len(t, ree.blookHooksCalled, 6) require.EqualError(t, ree.terminalError, hooks.ErrPaused{}.Error()) }, @@ -164,24 +167,25 @@ func TestRequestExecutionBlockChain(t *testing.T) { require.Regexp(t, "something went wrong", receivedErrors[0].Error()) require.Len(t, ree.requestsSent, 2) require.Equal(t, ree.request, ree.requestsSent[0].request) - require.True(t, ree.requestsSent[1].request.IsCancel()) + require.Equal(t, ree.requestsSent[1].request.Type(), graphsync.RequestTypeCancel) require.Len(t, ree.blookHooksCalled, 6) require.EqualError(t, ree.terminalError, "something went wrong") }, }, "sending updates": { configureRequestExecution: func(p peer.ID, requestID graphsync.RequestID, tbc *testutil.TestBlockChain, ree *requestExecutionEnv) { - ree.blockHookResults[blockHookKey{p, requestID, tbc.LinkTipIndex(5)}] = hooks.UpdateResult{Extensions: []graphsync.ExtensionData{{Name: "something", Data: []byte("applesauce")}}} + ree.blockHookResults[blockHookKey{p, requestID, tbc.LinkTipIndex(5)}] = hooks.UpdateResult{Extensions: []graphsync.ExtensionData{{Name: "something", Data: basicnode.NewString("applesauce")}}} }, verifyResults: func(t *testing.T, tbc *testutil.TestBlockChain, ree *requestExecutionEnv, responses []graphsync.ResponseProgress, receivedErrors []error) { tbc.VerifyWholeChainSync(responses) require.Empty(t, receivedErrors) require.Len(t, ree.requestsSent, 2) require.Equal(t, ree.request, ree.requestsSent[0].request) - require.True(t, ree.requestsSent[1].request.IsUpdate()) + require.Equal(t, ree.requestsSent[1].request.Type(), graphsync.RequestTypeUpdate) data, has := ree.requestsSent[1].request.Extension("something") require.True(t, has) - require.Equal(t, string(data), "applesauce") + str, _ := data.AsString() + require.Equal(t, str, "applesauce") require.Len(t, ree.blookHooksCalled, 10) require.NoError(t, ree.terminalError) }, @@ -192,10 +196,12 @@ func TestRequestExecutionBlockChain(t *testing.T) { ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() - persistence := testutil.NewTestStore(make(map[ipld.Link][]byte)) + persistence := testutil.NewTestStore(make(map[datamodel.Link][]byte)) tbc := testutil.SetupBlockChain(ctx, t, persistence, 100, 10) - fal := testloader.NewFakeAsyncLoader() - requestID := graphsync.RequestID(rand.Int31()) + reconciledLoader := &fakeReconciledLoader{ + responses: make(map[datamodel.Link]chan types.AsyncLoadResult), + } + requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] requestCtx, requestCancel := context.WithCancel(ctx) defer requestCancel() @@ -207,14 +213,13 @@ func TestRequestExecutionBlockChain(t *testing.T) { blockHookResults: make(map[blockHookKey]hooks.UpdateResult), doNotSendFirstBlocks: 0, request: gsmsg.NewRequest(requestID, tbc.TipLink.(cidlink.Link).Cid, tbc.Selector(), graphsync.Priority(rand.Int31())), - fal: fal, tbc: tbc, initialRequest: true, inProgressErr: make(chan error, 1), traverser: ipldutil.TraversalBuilder{ Root: tbc.TipLink, Selector: tbc.Selector(), - Visitor: func(tp traversal.Progress, node ipld.Node, tr traversal.VisitReason) error { + Visitor: func(tp traversal.Progress, node datamodel.Node, tr traversal.VisitReason) error { responsesReceived = append(responsesReceived, graphsync.ResponseProgress{ Node: node, Path: tp.Path, @@ -223,12 +228,14 @@ func TestRequestExecutionBlockChain(t *testing.T) { return nil }, }.Start(requestCtx), + reconciledLoader: reconciledLoader, } - fal.OnAsyncLoad(ree.checkPause) + reconciledLoader.onAsyncLoad(ree.checkPause) if data.configureRequestExecution != nil { data.configureRequestExecution(p, requestID, tbc, ree) } - ree.fal.SuccessResponseOn(p, requestID, tbc.Blocks(0, ree.loadLocallyUntil)) + reconciledLoader.successResponseOn(tbc.Blocks(0, ree.loadLocallyUntil)) + reconciledLoader.responseOn(tbc.LinkTipIndex(ree.loadLocallyUntil), types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: tbc.LinkTipIndex(ree.loadLocallyUntil)}}) var errorsReceived []error errCollectionErr := make(chan error, 1) go func() { @@ -245,7 +252,7 @@ func TestRequestExecutionBlockChain(t *testing.T) { } } }() - executor.NewExecutor(ree, ree, fal.AsyncLoad).ExecuteTask(ctx, ree.p, &peertask.Task{}) + executor.NewExecutor(ree, ree).ExecuteTask(ctx, ree.p, &peertask.Task{}) require.NoError(t, <-errCollectionErr) ree.traverser.Shutdown(ctx) data.verifyResults(t, tbc, ree, responsesReceived, errorsReceived) @@ -261,12 +268,12 @@ type requestSent struct { type blockHookKey struct { p peer.ID requestID graphsync.RequestID - link ipld.Link + link datamodel.Link } type pauseKey struct { requestID graphsync.RequestID - link ipld.Link + link datamodel.Link } type requestExecutionEnv struct { @@ -280,6 +287,7 @@ type requestExecutionEnv struct { externalPause pauseKey loadLocallyUntil int traverser ipldutil.Traverser + reconciledLoader *fakeReconciledLoader inProgressErr chan error initialRequest bool customRemoteBehavior func() @@ -290,9 +298,62 @@ type requestExecutionEnv struct { // deps tbc *testutil.TestBlockChain - fal *testloader.FakeAsyncLoader } +type fakeReconciledLoader struct { + responsesLk sync.Mutex + responses map[datamodel.Link]chan types.AsyncLoadResult + lastLoad datamodel.Link + online bool + cb func(datamodel.Link) +} + +func (frl *fakeReconciledLoader) onAsyncLoad(cb func(datamodel.Link)) { + frl.cb = cb +} + +func (frl *fakeReconciledLoader) responseOn(link datamodel.Link, result types.AsyncLoadResult) { + response := frl.asyncLoad(link, true) + response <- result + close(response) +} + +func (frl *fakeReconciledLoader) successResponseOn(blks []blocks.Block) { + + for _, block := range blks { + frl.responseOn(cidlink.Link{Cid: block.Cid()}, types.AsyncLoadResult{Data: block.RawData(), Local: false, Err: nil}) + } +} + +func (frl *fakeReconciledLoader) asyncLoad(link datamodel.Link, force bool) chan types.AsyncLoadResult { + frl.responsesLk.Lock() + response, ok := frl.responses[link] + if !ok || force { + response = make(chan types.AsyncLoadResult, 1) + frl.responses[link] = response + } + frl.responsesLk.Unlock() + return response +} + +func (frl *fakeReconciledLoader) BlockReadOpener(_ linking.LinkContext, link datamodel.Link) types.AsyncLoadResult { + frl.lastLoad = link + if frl.cb != nil { + frl.cb(link) + } + return <-frl.asyncLoad(link, false) +} + +func (frl *fakeReconciledLoader) RetryLastLoad() types.AsyncLoadResult { + if frl.cb != nil { + frl.cb(frl.lastLoad) + } + return <-frl.asyncLoad(frl.lastLoad, false) +} + +func (frl *fakeReconciledLoader) SetRemoteOnline(online bool) { + frl.online = true +} func (ree *requestExecutionEnv) ReleaseRequestTask(_ peer.ID, _ *peertask.Task, err error) { ree.terminalError = err close(ree.inProgressErr) @@ -300,7 +361,7 @@ func (ree *requestExecutionEnv) ReleaseRequestTask(_ peer.ID, _ *peertask.Task, func (ree *requestExecutionEnv) GetRequestTask(_ peer.ID, _ *peertask.Task, requestExecutionChan chan executor.RequestTask) { var lastResponse atomic.Value - lastResponse.Store(gsmsg.NewResponse(ree.request.ID(), graphsync.RequestAcknowledged)) + lastResponse.Store(gsmsg.NewResponse(ree.request.ID(), graphsync.RequestAcknowledged, nil)) requestExecution := executor.RequestTask{ Ctx: ree.ctx, @@ -312,7 +373,7 @@ func (ree *requestExecutionEnv) GetRequestTask(_ peer.ID, _ *peertask.Task, requ P: ree.p, InProgressErr: ree.inProgressErr, Empty: false, - InitialRequest: ree.initialRequest, + ReconciledLoader: ree.reconciledLoader, } go func() { select { @@ -324,9 +385,9 @@ func (ree *requestExecutionEnv) GetRequestTask(_ peer.ID, _ *peertask.Task, requ func (ree *requestExecutionEnv) SendRequest(p peer.ID, request gsmsg.GraphSyncRequest) { ree.requestsSent = append(ree.requestsSent, requestSent{p, request}) - if !request.IsCancel() && !request.IsUpdate() { + if request.Type() == graphsync.RequestTypeNew { if ree.customRemoteBehavior == nil { - ree.fal.SuccessResponseOn(p, request.ID(), ree.tbc.Blocks(ree.loadLocallyUntil, len(ree.tbc.AllBlocks()))) + ree.reconciledLoader.successResponseOn(ree.tbc.Blocks(ree.loadLocallyUntil, len(ree.tbc.AllBlocks()))) } else { ree.customRemoteBehavior() } @@ -339,8 +400,8 @@ func (ree *requestExecutionEnv) ProcessBlockHooks(p peer.ID, response graphsync. return ree.blockHookResults[bhk] } -func (ree *requestExecutionEnv) checkPause(requestID graphsync.RequestID, link ipld.Link, result <-chan types.AsyncLoadResult) { - if ree.externalPause.link == link && ree.externalPause.requestID == requestID { +func (ree *requestExecutionEnv) checkPause(link datamodel.Link) { + if ree.externalPause.link == link { ree.externalPause = pauseKey{} ree.pauseMessages <- struct{}{} } diff --git a/requestmanager/hooks/hooks_test.go b/requestmanager/hooks/hooks_test.go index 4f008f09..0ee19aef 100644 --- a/requestmanager/hooks/hooks_test.go +++ b/requestmanager/hooks/hooks_test.go @@ -2,7 +2,6 @@ package hooks_test import ( "errors" - "math/rand" "testing" "github.com/ipld/go-ipld-prime" @@ -21,7 +20,7 @@ func TestRequestHookProcessing(t *testing.T) { fakeChooser := func(ipld.Link, ipld.LinkContext) (ipld.NodePrototype, error) { return basicnode.Prototype.Any, nil } - extensionData := testutil.RandomBytes(100) + extensionData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName := graphsync.ExtensionName("AppleSauce/McGee") extension := graphsync.ExtensionData{ Name: extensionName, @@ -29,7 +28,7 @@ func TestRequestHookProcessing(t *testing.T) { } root := testutil.GenerateCids(1)[0] - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) request := gsmsg.NewRequest(requestID, root, ssb.Matcher().Node(), graphsync.Priority(0), extension) p := testutil.GeneratePeers(1)[0] @@ -100,19 +99,19 @@ func TestRequestHookProcessing(t *testing.T) { func TestBlockHookProcessing(t *testing.T) { - extensionResponseData := testutil.RandomBytes(100) + extensionResponseData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName := graphsync.ExtensionName("AppleSauce/McGee") extensionResponse := graphsync.ExtensionData{ Name: extensionName, Data: extensionResponseData, } - extensionUpdateData := testutil.RandomBytes(100) + extensionUpdateData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionUpdate := graphsync.ExtensionData{ Name: extensionName, Data: extensionUpdateData, } - requestID := graphsync.RequestID(rand.Int31()) - response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, extensionResponse) + requestID := graphsync.NewRequestID() + response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, nil, extensionResponse) p := testutil.GeneratePeers(1)[0] blockData := testutil.NewFakeBlockData() @@ -197,19 +196,19 @@ func TestBlockHookProcessing(t *testing.T) { func TestResponseHookProcessing(t *testing.T) { - extensionResponseData := testutil.RandomBytes(100) + extensionResponseData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName := graphsync.ExtensionName("AppleSauce/McGee") extensionResponse := graphsync.ExtensionData{ Name: extensionName, Data: extensionResponseData, } - extensionUpdateData := testutil.RandomBytes(100) + extensionUpdateData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionUpdate := graphsync.ExtensionData{ Name: extensionName, Data: extensionUpdateData, } - requestID := graphsync.RequestID(rand.Int31()) - response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, extensionResponse) + requestID := graphsync.NewRequestID() + response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, nil, extensionResponse) p := testutil.GeneratePeers(1)[0] testCases := map[string]struct { diff --git a/requestmanager/messages.go b/requestmanager/messages.go index ce219f3a..46e2da4d 100644 --- a/requestmanager/messages.go +++ b/requestmanager/messages.go @@ -13,6 +13,20 @@ import ( "github.com/ipfs/go-graphsync/requestmanager/executor" ) +type updateRequestMessage struct { + id graphsync.RequestID + extensions []graphsync.ExtensionData + response chan<- error +} + +func (urm *updateRequestMessage) handle(rm *RequestManager) { + err := rm.update(urm.id, urm.extensions) + select { + case <-rm.ctx.Done(): + case urm.response <- err: + } +} + type pauseRequestMessage struct { id graphsync.RequestID response chan<- error diff --git a/requestmanager/reconciledloader/injest.go b/requestmanager/reconciledloader/injest.go new file mode 100644 index 00000000..2150a0e7 --- /dev/null +++ b/requestmanager/reconciledloader/injest.go @@ -0,0 +1,44 @@ +package reconciledloader + +import ( + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "go.opentelemetry.io/otel/trace" +) + +// IngestResponse ingests new remote items into the reconciled loader +func (rl *ReconciledLoader) IngestResponse(md graphsync.LinkMetadata, traceLink trace.Link, blocks map[cid.Cid][]byte) { + if md.Length() == 0 { + return + } + duplicates := make(map[cid.Cid]struct{}, md.Length()) + items := make([]*remotedLinkedItem, 0, md.Length()) + md.Iterate(func(link cid.Cid, action graphsync.LinkAction) { + newItem := newRemote() + newItem.link = link + newItem.action = action + if action == graphsync.LinkActionPresent { + if _, isDuplicate := duplicates[link]; !isDuplicate { + duplicates[link] = struct{}{} + newItem.block = blocks[link] + } + } + newItem.traceLink = traceLink + items = append(items, newItem) + }) + rl.lock.Lock() + + // refuse to queue items when the request is ofline + if !rl.open { + // don't hold block memory if we're dropping these + freeList(items) + rl.lock.Unlock() + return + } + + buffered := rl.remoteQueue.queue(items) + rl.signal.Signal() + rl.lock.Unlock() + + log.Debugw("injested blocks for new response", "request_id", rl.requestID, "total_queued_bytes", buffered) +} diff --git a/requestmanager/reconciledloader/load.go b/requestmanager/reconciledloader/load.go new file mode 100644 index 00000000..2f92f960 --- /dev/null +++ b/requestmanager/reconciledloader/load.go @@ -0,0 +1,184 @@ +package reconciledloader + +import ( + "context" + "io/ioutil" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/requestmanager/types" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// BlockReadOpener synchronously loads the next block result +// as long as the request is online, it will wait for more remote items until it can load this link definitively +// once the request is offline +func (rl *ReconciledLoader) BlockReadOpener(lctx linking.LinkContext, link datamodel.Link) types.AsyncLoadResult { + if !rl.mostRecentLoadAttempt.empty() { + // since we aren't retrying the most recent load, it's time to record it in the traversal record + rl.traversalRecord.RecordNextStep( + rl.mostRecentLoadAttempt.linkContext.LinkPath.Segments(), + rl.mostRecentLoadAttempt.link.(cidlink.Link).Cid, + rl.mostRecentLoadAttempt.successful, + ) + rl.mostRecentLoadAttempt = loadAttempt{} + } + + // the private method does the actual loading, while this wrapper simply does the record keeping + usedRemote, result := rl.blockReadOpener(lctx, link) + + // now, we cache to allow a retry if we're offline + rl.mostRecentLoadAttempt.link = link + rl.mostRecentLoadAttempt.linkContext = lctx + rl.mostRecentLoadAttempt.successful = result.Err == nil + rl.mostRecentLoadAttempt.usedRemote = usedRemote + return result +} + +func (rl *ReconciledLoader) blockReadOpener(lctx linking.LinkContext, link datamodel.Link) (usedRemote bool, result types.AsyncLoadResult) { + + // catch up the remore or determine that we are offline + hasRemoteData, err := rl.waitRemote() + if err != nil { + return false, types.AsyncLoadResult{Err: err, Local: !hasRemoteData} + } + + // if we're offline just load local + if !hasRemoteData { + return false, rl.loadLocal(lctx, link) + } + + // only attempt remote load if after reconciliation we're not on a missing path + if !rl.pathTracker.stillOnMissingRemotePath(lctx.LinkPath) { + data, err := rl.loadRemote(lctx, link) + if data != nil { + return true, types.AsyncLoadResult{Data: data, Local: false} + } + if err != nil { + return true, types.AsyncLoadResult{Err: err, Local: false} + } + } + // remote had missing or duplicate block, attempt load local + return true, rl.loadLocal(lctx, link) +} + +func (rl *ReconciledLoader) loadLocal(lctx linking.LinkContext, link datamodel.Link) types.AsyncLoadResult { + stream, err := rl.lsys.StorageReadOpener(lctx, link) + if err != nil { + return types.AsyncLoadResult{Err: graphsync.RemoteMissingBlockErr{Link: link, Path: lctx.LinkPath}, Local: true} + } + // skip a stream copy if it's not needed + if br, ok := stream.(byteReader); ok { + return types.AsyncLoadResult{Data: br.Bytes(), Local: true} + } + localData, err := ioutil.ReadAll(stream) + if err != nil { + return types.AsyncLoadResult{Err: graphsync.RemoteMissingBlockErr{Link: link, Path: lctx.LinkPath}, Local: true} + } + return types.AsyncLoadResult{Data: localData, Local: true} +} + +func (rl *ReconciledLoader) loadRemote(lctx linking.LinkContext, link datamodel.Link) ([]byte, error) { + rl.lock.Lock() + head := rl.remoteQueue.first() + buffered := rl.remoteQueue.consume() + rl.lock.Unlock() + + // verify it matches the expected next load + if !head.link.Equals(link.(cidlink.Link).Cid) { + return nil, graphsync.RemoteIncorrectResponseError{ + LocalLink: link, + RemoteLink: cidlink.Link{Cid: head.link}, + Path: lctx.LinkPath, + } + } + + // update path tracking + rl.pathTracker.recordRemoteLoadAttempt(lctx.LinkPath, head.action) + + // if block == nil, + // it can mean: + // - metadata had a Missing Action (Block is missing) + // - metadata had a Present Action but no block data in message + // - block appeared twice in metadata for a single message. During + // InjestResponse we decided to hold on to block data only for the + // first metadata instance + // Regardless, when block == nil, we need to simply try to load form local + // datastore + if head.block == nil { + return nil, nil + } + + // get a context + ctx := lctx.Ctx + if ctx == nil { + ctx = context.Background() + } + + // start a span + _, span := otel.Tracer("graphsync").Start( + ctx, + "verifyBlock", + trace.WithLinks(head.traceLink), + trace.WithAttributes(attribute.String("cid", link.String()))) + defer span.End() + + log.Debugw("verified block", "request_id", rl.requestID, "total_queued_bytes", buffered) + + // save the block + buffer, committer, err := rl.lsys.StorageWriteOpener(lctx) + if err != nil { + return nil, err + } + if settable, ok := buffer.(settableWriter); ok { + err = settable.SetBytes(head.block) + } else { + _, err = buffer.Write(head.block) + } + if err != nil { + return nil, err + } + err = committer(link) + if err != nil { + return nil, err + } + + // return the block + return head.block, nil +} + +func (rl *ReconciledLoader) waitRemote() (bool, error) { + rl.lock.Lock() + defer rl.lock.Unlock() + for { + // Case 1 item is waiting + if !rl.remoteQueue.empty() { + if rl.verifier == nil || rl.verifier.Done() { + rl.verifier = nil + return true, nil + } + path := rl.verifier.CurrentPath() + head := rl.remoteQueue.first() + rl.remoteQueue.consume() + err := rl.verifier.VerifyNext(head.link, head.action != graphsync.LinkActionMissing) + if err != nil { + return true, err + } + rl.pathTracker.recordRemoteLoadAttempt(path, head.action) + continue + + } + + // Case 2 no available item and channel is closed + if !rl.open { + return false, nil + } + + // Case 3 nothing available, wait for more items + rl.signal.Wait() + } +} diff --git a/requestmanager/reconciledloader/pathtracker.go b/requestmanager/reconciledloader/pathtracker.go new file mode 100644 index 00000000..85037afd --- /dev/null +++ b/requestmanager/reconciledloader/pathtracker.go @@ -0,0 +1,41 @@ +package reconciledloader + +import ( + "github.com/ipfs/go-graphsync" + "github.com/ipld/go-ipld-prime/datamodel" +) + +// pathTracker is just a simple utility to track whether we're on a missing +// path for the remote +type pathTracker struct { + lastMissingRemotePath datamodel.Path +} + +// stillOnMissingRemotePath determines whether the next link load will be from +// a path missing from the remote +// if it won't be, based on the linear nature of selector traversals, it wipes +// the last missing state +func (pt *pathTracker) stillOnMissingRemotePath(newPath datamodel.Path) bool { + // is there a known missing path? + if pt.lastMissingRemotePath.Len() == 0 { + return false + } + // are we still on it? + if newPath.Len() <= pt.lastMissingRemotePath.Len() { + // if not, reset to no known missing remote path + pt.lastMissingRemotePath = datamodel.NewPath(nil) + return false + } + // otherwise we're on a missing path + return true +} + +// recordRemoteLoadAttempt records the results of attempting to load from the remote +// at the given path +func (pt *pathTracker) recordRemoteLoadAttempt(currentPath datamodel.Path, action graphsync.LinkAction) { + // if the last remote link was missing + if action == graphsync.LinkActionMissing { + // record the last known missing path + pt.lastMissingRemotePath = currentPath + } +} diff --git a/requestmanager/reconciledloader/reconciledloader.go b/requestmanager/reconciledloader/reconciledloader.go new file mode 100644 index 00000000..e74d3ee9 --- /dev/null +++ b/requestmanager/reconciledloader/reconciledloader.go @@ -0,0 +1,124 @@ +/* +Package reconciledloader implements a block loader that can load from two different sources: +- a local store +- a series of remote responses for a given graphsync selector query + +It verifies the sequence of remote responses matches the sequence +of loads called from a local selector traversal. + +The reconciled loader also tracks whether or not there is a remote request in progress. + +When there is no request in progress, it loads from the local store only. + +When there is a request in progress, waits for remote responses before loading, and only calls +upon the local store for duplicate blocks and when traversing paths the remote was missing. + +The reconciled loader assumes: +1. A single thread is calling AsyncLoad to load blocks +2. When a request is online, a seperate thread may call IngestResponse +3. Either thread may call SetRemoteState or Cleanup +4. The remote sends metadata for all blocks it traverses in the query (per GraphSync protocol spec) - whether or not +the actual block is sent. +*/ +package reconciledloader + +import ( + "context" + "errors" + "sync" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/requestmanager/reconciledloader/traversalrecord" + "github.com/ipfs/go-graphsync/requestmanager/types" + logging "github.com/ipfs/go-log/v2" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/linking" +) + +var log = logging.Logger("gs-reconciledlaoder") + +type settableWriter interface { + SetBytes([]byte) error +} + +type byteReader interface { + Bytes() []byte +} + +type loadAttempt struct { + link datamodel.Link + linkContext linking.LinkContext + successful bool + usedRemote bool +} + +func (lr loadAttempt) empty() bool { + return lr.link == nil +} + +// ReconciledLoader is an instance of the reconciled loader +type ReconciledLoader struct { + requestID graphsync.RequestID + lsys *linking.LinkSystem + mostRecentLoadAttempt loadAttempt + traversalRecord *traversalrecord.TraversalRecord + pathTracker pathTracker + + lock *sync.Mutex + signal *sync.Cond + open bool + verifier *traversalrecord.Verifier + remoteQueue remoteQueue +} + +// NewReconciledLoader returns a new reconciled loader for the given requestID & localStore +func NewReconciledLoader(requestID graphsync.RequestID, localStore *linking.LinkSystem) *ReconciledLoader { + lock := &sync.Mutex{} + traversalRecord := traversalrecord.NewTraversalRecord() + return &ReconciledLoader{ + requestID: requestID, + lsys: localStore, + lock: lock, + signal: sync.NewCond(lock), + traversalRecord: traversalRecord, + } +} + +// SetRemoteState records whether or not the request is online +func (rl *ReconciledLoader) SetRemoteOnline(online bool) { + rl.lock.Lock() + defer rl.lock.Unlock() + wasOpen := rl.open + rl.open = online + if !rl.open && wasOpen { + // if the queue is closing, trigger any expecting new items + rl.signal.Signal() + return + } + if rl.open && !wasOpen { + // if we're opening a remote request, we need to reverify against what we've loaded so far + rl.verifier = traversalrecord.NewVerifier(rl.traversalRecord) + } +} + +// Cleanup frees up some memory resources for this loader prior to throwing it away +func (rl *ReconciledLoader) Cleanup(ctx context.Context) { + rl.lock.Lock() + rl.remoteQueue.clear() + rl.lock.Unlock() +} + +// RetryLastLoad retries the last offline load, assuming one is present +func (rl *ReconciledLoader) RetryLastLoad() types.AsyncLoadResult { + if rl.mostRecentLoadAttempt.link == nil { + return types.AsyncLoadResult{Err: errors.New("cannot retry offline load when none is present")} + } + retryLoadAttempt := rl.mostRecentLoadAttempt + rl.mostRecentLoadAttempt = loadAttempt{} + if retryLoadAttempt.usedRemote { + rl.lock.Lock() + rl.remoteQueue.retryLast() + rl.lock.Unlock() + } + return rl.BlockReadOpener(retryLoadAttempt.linkContext, retryLoadAttempt.link) +} diff --git a/requestmanager/reconciledloader/reconciledloader_test.go b/requestmanager/reconciledloader/reconciledloader_test.go new file mode 100644 index 00000000..a27ec70f --- /dev/null +++ b/requestmanager/reconciledloader/reconciledloader_test.go @@ -0,0 +1,628 @@ +package reconciledloader_test + +import ( + "bytes" + "context" + "testing" + "time" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/ipldutil" + "github.com/ipfs/go-graphsync/message" + "github.com/ipfs/go-graphsync/requestmanager/reconciledloader" + "github.com/ipfs/go-graphsync/requestmanager/types" + "github.com/ipfs/go-graphsync/testutil" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" +) + +func TestReconciledLoader(t *testing.T) { + ctx := context.Background() + testBCStorage := make(map[datamodel.Link][]byte) + bcLinkSys := testutil.NewTestStore(testBCStorage) + testChain := testutil.SetupBlockChain(ctx, t, bcLinkSys, 100, 100) + testTree := testutil.NewTestIPLDTree() + testCases := map[string]struct { + root cid.Cid + baseStore map[datamodel.Link][]byte + presentRemoteBlocks []blocks.Block + presentLocalBlocks []blocks.Block + remoteSeq []message.GraphSyncLinkMetadatum + steps []step + }{ + "load entirely from local store": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentLocalBlocks: testChain.AllBlocks(), + steps: syncLoadRange(testChain, 0, 100, true), + }, + "load entirely from remote store": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + remoteSeq: metadataRange(testChain, 0, 100, false), + steps: append([]step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 100}, + }, syncLoadRange(testChain, 0, 100, false)...), + }, + "load from local store, then go online": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentLocalBlocks: testChain.Blocks(0, 50), + presentRemoteBlocks: testChain.Blocks(50, 100), + remoteSeq: metadataRange(testChain, 0, 100, false), + steps: append(append( + // load first 50 locally + syncLoadRange(testChain, 0, 50, true), + []step{ + // should fail next because it's not stored locally + syncLoad{ + loadSeq: 50, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testChain.LinkTipIndex(50), Path: testChain.PathTipIndex(50)}}, + }, + // go online + goOnline{}, + // retry now that we're online -- note this won't return until we injest responses + asyncRetry{}, + // injest responses from remote peer + injest{ + metadataStart: 0, + metadataEnd: 100, + }, + // verify the retry worked + verifyAsyncResult{ + expectedResult: types.AsyncLoadResult{Local: false, Data: testChain.Blocks(50, 51)[0].RawData()}, + }, + }...), + // verify we can load the remaining items from the remote + syncLoadRange(testChain, 51, 100, false)...), + }, + "retry while offline": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentLocalBlocks: testChain.Blocks(0, 50), + steps: append( + // load first 50 locally + syncLoadRange(testChain, 0, 50, true), + []step{ + // should fail next because it's not stored locally + syncLoad{ + loadSeq: 50, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testChain.LinkTipIndex(50), Path: testChain.PathTipIndex(50)}}, + }, + retry{ + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testChain.LinkTipIndex(50), Path: testChain.PathTipIndex(50)}}, + }, + }...), + }, + "retry while online": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + remoteSeq: metadataRange(testChain, 0, 100, false), + steps: append(append([]step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 100}, + }, + syncLoadRange(testChain, 0, 50, false)...), + retry{ + expectedResult: types.AsyncLoadResult{Data: testChain.Blocks(49, 50)[0].RawData(), Local: true}, + }), + }, + "retry online load after going offline": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + remoteSeq: metadataRange(testChain, 0, 100, false), + steps: append(append([]step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 100}, + }, + syncLoadRange(testChain, 0, 50, false)...), + goOffline{}, + retry{ + expectedResult: types.AsyncLoadResult{Data: testChain.Blocks(49, 50)[0].RawData(), Local: true}, + }), + }, + "error reconciling local results": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentLocalBlocks: testChain.Blocks(0, 50), + presentRemoteBlocks: testChain.Blocks(50, 100), + remoteSeq: append(append(metadataRange(testChain, 0, 30, false), + message.GraphSyncLinkMetadatum{ + Link: testChain.LinkTipIndex(53).(cidlink.Link).Cid, + Action: graphsync.LinkActionPresent, + }), + metadataRange(testChain, 31, 100, false)...), + steps: append( + // load first 50 locally + syncLoadRange(testChain, 0, 50, true), + []step{ + // should fail next because it's not stored locally + syncLoad{ + loadSeq: 50, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testChain.LinkTipIndex(50), Path: testChain.PathTipIndex(50)}}, + }, + // go online + goOnline{}, + // retry now that we're online -- note this won't return until we injest responses + asyncRetry{}, + // injest responses from remote peer + injest{ + metadataStart: 0, + metadataEnd: 100, + }, + // we should get an error cause of issues reconciling against previous local log + verifyAsyncResult{ + expectedResult: types.AsyncLoadResult{Local: false, Err: graphsync.RemoteIncorrectResponseError{ + LocalLink: testChain.LinkTipIndex(30), + RemoteLink: testChain.LinkTipIndex(53), + Path: testChain.PathTipIndex(30), + }}, + }, + }...), + }, + "remote sends out of order block": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + remoteSeq: append(append(metadataRange(testChain, 0, 30, false), + message.GraphSyncLinkMetadatum{ + Link: testChain.LinkTipIndex(53).(cidlink.Link).Cid, + Action: graphsync.LinkActionPresent, + }), + metadataRange(testChain, 31, 100, false)...), + steps: append(append([]step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 100}, + }, syncLoadRange(testChain, 0, 30, false)...), + // we should get an error cause the remote sent and incorrect response + syncLoad{ + loadSeq: 30, + expectedResult: types.AsyncLoadResult{Local: false, Err: graphsync.RemoteIncorrectResponseError{ + LocalLink: testChain.LinkTipIndex(30), + RemoteLink: testChain.LinkTipIndex(53), + Path: testChain.PathTipIndex(30), + }}, + }, + ), + }, + "remote missing block": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + remoteSeq: append(metadataRange(testChain, 0, 30, false), + message.GraphSyncLinkMetadatum{ + Link: testChain.LinkTipIndex(30).(cidlink.Link).Cid, + Action: graphsync.LinkActionMissing, + }), + steps: append(append([]step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 31}, + }, syncLoadRange(testChain, 0, 30, false)...), + // we should get an error that we're missing a block for our response + syncLoad{ + loadSeq: 30, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{ + Link: testChain.LinkTipIndex(30), + Path: testChain.PathTipIndex(30), + }}, + }, + ), + }, + "remote missing chain that local has": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + presentLocalBlocks: testChain.Blocks(30, 100), + remoteSeq: append(metadataRange(testChain, 0, 30, false), + message.GraphSyncLinkMetadatum{ + Link: testChain.LinkTipIndex(30).(cidlink.Link).Cid, + Action: graphsync.LinkActionMissing, + }), + steps: append(append(append( + []step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 31}, + }, + // load the blocks the remote has + syncLoadRange(testChain, 0, 30, false)...), + []step{ + // load the block the remote missing says it's missing locally + syncLoadRange(testChain, 30, 31, true)[0], + asyncLoad{loadSeq: 31}, + // at this point we have no more remote responses, since it's a linear chain + verifyNoAsyncResult{}, + // we'd expect the remote would terminate here, since we've sent the last missing block + goOffline{}, + // this will cause us to start loading locally only again + verifyAsyncResult{ + expectedResult: types.AsyncLoadResult{Local: true, Data: testChain.Blocks(31, 32)[0].RawData()}, + }, + }...), + syncLoadRange(testChain, 30, 100, true)..., + ), + }, + "remote missing chain that local has partial": { + root: testChain.TipLink.(cidlink.Link).Cid, + baseStore: testBCStorage, + presentRemoteBlocks: testChain.AllBlocks(), + presentLocalBlocks: testChain.Blocks(30, 50), + remoteSeq: append(metadataRange(testChain, 0, 30, false), + message.GraphSyncLinkMetadatum{ + Link: testChain.LinkTipIndex(30).(cidlink.Link).Cid, + Action: graphsync.LinkActionMissing, + }), + steps: append(append(append(append([]step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 31}, + }, + // load the blocks the remote has + syncLoadRange(testChain, 0, 30, false)...), + []step{ + // load the block the remote missing says it's missing locally + syncLoadRange(testChain, 30, 31, true)[0], + asyncLoad{loadSeq: 31}, + // at this point we have no more remote responses, since it's a linear chain + verifyNoAsyncResult{}, + // we'd expect the remote would terminate here, since we've sent the last missing block + goOffline{}, + // this will cause us to start loading locally only + verifyAsyncResult{ + expectedResult: types.AsyncLoadResult{Local: true, Data: testChain.Blocks(31, 32)[0].RawData()}, + }, + }...), + // should follow up to the end of the local chain + syncLoadRange(testChain, 32, 50, true)...), + // but then it should return missing + syncLoad{ + loadSeq: 50, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{ + Link: testChain.LinkTipIndex(50), + Path: testChain.PathTipIndex(50), + }}, + }, + ), + }, + "remote duplicate blocks can load from local": { + root: testTree.RootBlock.Cid(), + baseStore: testTree.Storage, + presentRemoteBlocks: []blocks.Block{ + testTree.RootBlock, + testTree.MiddleListBlock, + testTree.MiddleMapBlock, + testTree.LeafAlphaBlock, + testTree.LeafBetaBlock, + }, + presentLocalBlocks: nil, + remoteSeq: []message.GraphSyncLinkMetadatum{ + {Link: testTree.RootBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.MiddleListBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafBetaBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.MiddleMapBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + }, + steps: []step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 8}, + syncLoad{loadSeq: 0, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.RootBlock.RawData()}}, + syncLoad{loadSeq: 1, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.MiddleListBlock.RawData()}}, + syncLoad{loadSeq: 2, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 3, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 4, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.LeafBetaBlock.RawData()}}, + syncLoad{loadSeq: 5, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 6, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.MiddleMapBlock.RawData()}}, + syncLoad{loadSeq: 7, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + }, + }, + "remote missing branch finishes to end": { + root: testTree.RootBlock.Cid(), + baseStore: testTree.Storage, + presentRemoteBlocks: []blocks.Block{ + testTree.RootBlock, + testTree.MiddleMapBlock, + testTree.LeafAlphaBlock, + }, + presentLocalBlocks: nil, + remoteSeq: []message.GraphSyncLinkMetadatum{ + {Link: testTree.RootBlock.Cid(), Action: graphsync.LinkActionPresent}, + // missing the whole list tree + {Link: testTree.MiddleListBlock.Cid(), Action: graphsync.LinkActionMissing}, + {Link: testTree.MiddleMapBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + }, + steps: []step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 4}, + syncLoad{loadSeq: 0, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.RootBlock.RawData()}}, + syncLoad{loadSeq: 1, expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testTree.MiddleListNodeLnk, Path: datamodel.ParsePath("linkedList")}}}, + syncLoad{loadSeq: 6, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.MiddleMapBlock.RawData()}}, + syncLoad{loadSeq: 7, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.LeafAlphaBlock.RawData()}}, + }, + }, + "remote missing branch with partial local": { + root: testTree.RootBlock.Cid(), + baseStore: testTree.Storage, + presentLocalBlocks: []blocks.Block{ + testTree.MiddleListBlock, + testTree.LeafAlphaBlock, + }, + presentRemoteBlocks: []blocks.Block{ + testTree.RootBlock, + testTree.MiddleMapBlock, + testTree.LeafAlphaBlock, + }, + remoteSeq: []message.GraphSyncLinkMetadatum{ + {Link: testTree.RootBlock.Cid(), Action: graphsync.LinkActionPresent}, + // missing the whole list tree + {Link: testTree.MiddleListBlock.Cid(), Action: graphsync.LinkActionMissing}, + {Link: testTree.MiddleMapBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + }, + steps: []step{ + goOnline{}, + injest{metadataStart: 0, metadataEnd: 4}, + syncLoad{loadSeq: 0, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.RootBlock.RawData()}}, + syncLoad{loadSeq: 1, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.MiddleListBlock.RawData()}}, + syncLoad{loadSeq: 2, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 3, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{ + loadSeq: 4, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testTree.LeafBetaLnk, Path: datamodel.NewPath([]datamodel.PathSegment{ + datamodel.PathSegmentOfString("linkedList"), + datamodel.PathSegmentOfInt(2), + })}}, + }, + syncLoad{loadSeq: 5, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 6, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.MiddleMapBlock.RawData()}}, + syncLoad{loadSeq: 7, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.LeafAlphaBlock.RawData()}}, + }, + }, + "remote missing branch during reconciliation": { + root: testTree.RootBlock.Cid(), + baseStore: testTree.Storage, + presentLocalBlocks: []blocks.Block{ + testTree.RootBlock, + testTree.MiddleListBlock, + testTree.LeafAlphaBlock, + }, + presentRemoteBlocks: []blocks.Block{ + testTree.RootBlock, + testTree.MiddleMapBlock, + testTree.LeafAlphaBlock, + }, + remoteSeq: []message.GraphSyncLinkMetadatum{ + {Link: testTree.RootBlock.Cid(), Action: graphsync.LinkActionPresent}, + // missing the whole list tree + {Link: testTree.MiddleListBlock.Cid(), Action: graphsync.LinkActionMissing}, + {Link: testTree.MiddleMapBlock.Cid(), Action: graphsync.LinkActionPresent}, + {Link: testTree.LeafAlphaBlock.Cid(), Action: graphsync.LinkActionPresent}, + }, + steps: []step{ + syncLoad{loadSeq: 0, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.RootBlock.RawData()}}, + syncLoad{loadSeq: 1, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.MiddleListBlock.RawData()}}, + syncLoad{loadSeq: 2, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 3, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + // here we have an offline load that is missing the local beta block + syncLoad{ + loadSeq: 4, + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testTree.LeafBetaLnk, Path: datamodel.NewPath([]datamodel.PathSegment{ + datamodel.PathSegmentOfString("linkedList"), + datamodel.PathSegmentOfInt(2), + })}}, + }, + goOnline{}, + injest{metadataStart: 0, metadataEnd: 4}, + // what we want to verify here is that when we retry loading, the reconciliation still works, + // even though the remote is missing a brnach that's farther up the tree + retry{ + expectedResult: types.AsyncLoadResult{Local: true, Err: graphsync.RemoteMissingBlockErr{Link: testTree.LeafBetaLnk, Path: datamodel.NewPath([]datamodel.PathSegment{ + datamodel.PathSegmentOfString("linkedList"), + datamodel.PathSegmentOfInt(2), + })}}, + }, + syncLoad{loadSeq: 5, expectedResult: types.AsyncLoadResult{Local: true, Data: testTree.LeafAlphaBlock.RawData()}}, + syncLoad{loadSeq: 6, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.MiddleMapBlock.RawData()}}, + syncLoad{loadSeq: 7, expectedResult: types.AsyncLoadResult{Local: false, Data: testTree.LeafAlphaBlock.RawData()}}, + }, + }, + } + + for testCase, data := range testCases { + t.Run(testCase, func(t *testing.T) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + localStorage := make(map[datamodel.Link][]byte) + for _, lb := range data.presentLocalBlocks { + localStorage[cidlink.Link{Cid: lb.Cid()}] = lb.RawData() + } + localLsys := testutil.NewTestStore(localStorage) + requestID := graphsync.NewRequestID() + + remoteStorage := make(map[cid.Cid][]byte) + for _, rb := range data.presentRemoteBlocks { + remoteStorage[rb.Cid()] = rb.RawData() + } + + // collect sequence of an explore all + var loadSeq []loadRequest + traverser := ipldutil.TraversalBuilder{ + Root: cidlink.Link{Cid: data.root}, + Selector: selectorparse.CommonSelector_ExploreAllRecursively, + }.Start(ctx) + for { + isComplete, err := traverser.IsComplete() + require.NoError(t, err) + if isComplete { + break + } + lnk, linkCtx := traverser.CurrentRequest() + loadSeq = append(loadSeq, loadRequest{linkCtx: linkCtx, link: lnk}) + traverser.Advance(bytes.NewReader(data.baseStore[lnk])) + } + ts := &testState{ + ctx: ctx, + remoteBlocks: remoteStorage, + remoteSeq: data.remoteSeq, + loadSeq: loadSeq, + asyncLoad: nil, + } + + rl := reconciledloader.NewReconciledLoader(requestID, &localLsys) + for _, step := range data.steps { + step.execute(t, ts, rl) + } + }) + } +} + +type loadRequest struct { + linkCtx ipld.LinkContext + link ipld.Link +} + +type testState struct { + ctx context.Context + remoteBlocks map[cid.Cid][]byte + remoteSeq []message.GraphSyncLinkMetadatum + loadSeq []loadRequest + asyncLoad <-chan types.AsyncLoadResult +} + +type step interface { + execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) +} + +type goOffline struct{} + +func (goOffline) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + rl.SetRemoteOnline(false) +} + +type goOnline struct{} + +func (goOnline) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + rl.SetRemoteOnline(true) +} + +type syncLoad struct { + loadSeq int + expectedResult types.AsyncLoadResult +} + +func (s syncLoad) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + require.Nil(t, ts.asyncLoad) + result := rl.BlockReadOpener(ts.loadSeq[s.loadSeq].linkCtx, ts.loadSeq[s.loadSeq].link) + require.Equal(t, s.expectedResult, result) +} + +type retry struct { + expectedResult types.AsyncLoadResult +} + +func (s retry) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + require.Nil(t, ts.asyncLoad) + result := rl.RetryLastLoad() + require.Equal(t, s.expectedResult, result) +} + +type asyncLoad struct { + loadSeq int +} + +func (s asyncLoad) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + require.Nil(t, ts.asyncLoad) + asyncLoad := make(chan types.AsyncLoadResult, 1) + ts.asyncLoad = asyncLoad + go func() { + result := rl.BlockReadOpener(ts.loadSeq[s.loadSeq].linkCtx, ts.loadSeq[s.loadSeq].link) + asyncLoad <- result + }() +} + +type asyncRetry struct { +} + +func (s asyncRetry) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + require.Nil(t, ts.asyncLoad) + asyncLoad := make(chan types.AsyncLoadResult, 1) + ts.asyncLoad = asyncLoad + go func() { + result := rl.RetryLastLoad() + asyncLoad <- result + }() +} + +type verifyNoAsyncResult struct{} + +func (verifyNoAsyncResult) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + require.NotNil(t, ts.asyncLoad) + time.Sleep(20 * time.Millisecond) + select { + case <-ts.asyncLoad: + require.FailNow(t, "should have no async load result but does") + default: + } +} + +type verifyAsyncResult struct { + expectedResult types.AsyncLoadResult +} + +func (v verifyAsyncResult) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + require.NotNil(t, ts.asyncLoad) + select { + case <-ts.ctx.Done(): + require.FailNow(t, "expected async load but failed") + case result := <-ts.asyncLoad: + ts.asyncLoad = nil + require.Equal(t, v.expectedResult, result) + } +} + +type injest struct { + metadataStart int + metadataEnd int + traceLink trace.Link +} + +func (i injest) execute(t *testing.T, ts *testState, rl *reconciledloader.ReconciledLoader) { + linkMetadata := ts.remoteSeq[i.metadataStart:i.metadataEnd] + rl.IngestResponse(message.NewLinkMetadata(linkMetadata), i.traceLink, ts.remoteBlocks) + // simulate no dub blocks + for _, lmd := range linkMetadata { + delete(ts.remoteBlocks, lmd.Link) + } +} + +func syncLoadRange(tbc *testutil.TestBlockChain, from int, to int, local bool) []step { + blocks := tbc.Blocks(from, to) + steps := make([]step, 0, len(blocks)) + for i := from; i < to; i++ { + steps = append(steps, syncLoad{i, types.AsyncLoadResult{Data: blocks[i-from].RawData(), Local: local}}) + } + return steps +} + +func metadataRange(tbc *testutil.TestBlockChain, from int, to int, missing bool) []message.GraphSyncLinkMetadatum { + tm := make([]message.GraphSyncLinkMetadatum, 0, to-from) + for i := from; i < to; i++ { + action := graphsync.LinkActionPresent + if missing { + action = graphsync.LinkActionMissing + } + tm = append(tm, message.GraphSyncLinkMetadatum{Link: tbc.LinkTipIndex(i).(cidlink.Link).Cid, Action: action}) + } + return tm +} diff --git a/requestmanager/reconciledloader/remotequeue.go b/requestmanager/reconciledloader/remotequeue.go new file mode 100644 index 00000000..8cf31f8b --- /dev/null +++ b/requestmanager/reconciledloader/remotequeue.go @@ -0,0 +1,121 @@ +package reconciledloader + +import ( + "sync" + + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "go.opentelemetry.io/otel/trace" +) + +var linkedRemoteItemPool = sync.Pool{ + New: func() interface{} { + return new(remotedLinkedItem) + }, +} + +type remoteItem struct { + link cid.Cid + action graphsync.LinkAction + block []byte + traceLink trace.Link +} + +type remotedLinkedItem struct { + remoteItem + next *remotedLinkedItem +} + +func newRemote() *remotedLinkedItem { + newItem := linkedRemoteItemPool.Get().(*remotedLinkedItem) + // need to reset next value to nil we're pulling out of a pool of potentially + // old objects + newItem.next = nil + return newItem +} + +func freeList(remoteItems []*remotedLinkedItem) { + for _, ri := range remoteItems { + ri.block = nil + linkedRemoteItemPool.Put(ri) + } +} + +type remoteQueue struct { + head *remotedLinkedItem + tail *remotedLinkedItem + dataSize uint64 + // we hold a reference to the last consumed item in order to + // allow us to retry while online + lastConsumed *remotedLinkedItem +} + +func (rq *remoteQueue) empty() bool { + return rq.head == nil +} + +func (rq *remoteQueue) first() remoteItem { + if rq.head == nil { + return remoteItem{} + } + + return rq.head.remoteItem +} + +// retry last will put the last consumed item back in the queue at the front +func (rq *remoteQueue) retryLast() { + if rq.lastConsumed != nil { + rq.head = rq.lastConsumed + } +} + +func (rq *remoteQueue) consume() uint64 { + // release and clear the previous last consumed item + if rq.lastConsumed != nil { + linkedRemoteItemPool.Put(rq.lastConsumed) + rq.lastConsumed = nil + } + // update our total data size buffered + rq.dataSize -= uint64(len(rq.head.block)) + // wipe the block reference -- if its been consumed, its saved + // to local store, and we don't need it - let the memory get freed + rq.head.block = nil + + // we hold the last consumed, minus the block, around so we can retry + rq.lastConsumed = rq.head + + // advance the queue + rq.head = rq.head.next + return rq.dataSize +} + +func (rq *remoteQueue) clear() { + for rq.head != nil { + rq.consume() + } + // clear any last consumed reference left over + if rq.lastConsumed != nil { + linkedRemoteItemPool.Put(rq.lastConsumed) + rq.lastConsumed = nil + } +} + +func (rq *remoteQueue) queue(newItems []*remotedLinkedItem) uint64 { + for _, newItem := range newItems { + // update total size buffered + + // TODO: this is a good place to hold off on accepting data + // to let the local traversal catch up + // a second enqueue/dequeue signal would allow us + // to make this call block until datasize dropped below a certain amount + rq.dataSize += uint64(len(newItem.block)) + if rq.head == nil { + rq.tail = newItem + rq.head = rq.tail + } else { + rq.tail.next = newItem + rq.tail = rq.tail.next + } + } + return rq.dataSize +} diff --git a/requestmanager/reconciledloader/traversalrecord/traversalrecord.go b/requestmanager/reconciledloader/traversalrecord/traversalrecord.go new file mode 100644 index 00000000..9caba96f --- /dev/null +++ b/requestmanager/reconciledloader/traversalrecord/traversalrecord.go @@ -0,0 +1,181 @@ +package traversalrecord + +import ( + "errors" + + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipld/go-ipld-prime/datamodel" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" +) + +// TraversalRecord records the links traversed by a selector and their paths in a space efficient manner +type TraversalRecord struct { + link *cid.Cid + successful bool + childSegments map[datamodel.PathSegment]int + children []*traversalLink +} + +type traversalLink struct { + segment datamodel.PathSegment + *TraversalRecord +} + +// NewTraversalRecord returns a new traversal record +func NewTraversalRecord() *TraversalRecord { + return &TraversalRecord{ + childSegments: make(map[datamodel.PathSegment]int), + } +} + +// RecordNextStep records the next step in the traversal into the tree +// based on its path, link, and whether the load was successful or not +func (tr *TraversalRecord) RecordNextStep(p []datamodel.PathSegment, link cid.Cid, successful bool) { + if len(p) == 0 { + tr.link = &link + tr.successful = successful + return + } + if _, ok := tr.childSegments[p[0]]; !ok { + child := traversalLink{ + TraversalRecord: NewTraversalRecord(), + segment: p[0], + } + tr.childSegments[p[0]] = len(tr.children) + tr.children = append(tr.children, &child) + } + tr.children[tr.childSegments[p[0]]].RecordNextStep(p[1:], link, successful) +} + +// AllLinks returns all links traversed for a given record +func (tr *TraversalRecord) AllLinks() []cid.Cid { + if len(tr.children) == 0 { + return []cid.Cid{*tr.link} + } + links := make([]cid.Cid, 0) + if tr.link != nil { + links = append(links, *tr.link) + } + for _, v := range tr.children { + links = append(links, v.AllLinks()...) + } + return links +} + +// GetLinks returns all links starting at the path in the tree rooted at 'root' +func (tr *TraversalRecord) GetLinks(root datamodel.Path) []cid.Cid { + segs := root.Segments() + switch len(segs) { + case 0: + if tr.link != nil { + return []cid.Cid{*tr.link} + } + return []cid.Cid{} + case 1: + // base case 1: get all paths below this child. + next := segs[0] + if childIndex, ok := tr.childSegments[next]; ok { + return tr.children[childIndex].AllLinks() + } + return []cid.Cid{} + default: + } + + next := segs[0] + if _, ok := tr.childSegments[next]; !ok { + // base case 2: not registered sub-path. + return []cid.Cid{} + } + return tr.children[tr.childSegments[next]].GetLinks(datamodel.NewPathNocopy(segs[1:])) +} + +// Verifier allows you to verify series of links loads matches a previous traversal +// order when those loads are successful +// At any point it can reconstruct the current path. +type Verifier struct { + stack []*traversalLink +} + +func NewVerifier(root *TraversalRecord) *Verifier { + v := &Verifier{ + stack: []*traversalLink{{TraversalRecord: root}}, + } + v.appendUntilLink() + return v +} + +func (v *Verifier) tip() *traversalLink { + if len(v.stack) == 0 { + return nil + } + return v.stack[len(v.stack)-1] +} + +func (v *Verifier) appendUntilLink() { + for v.tip().link == nil && len(v.tip().children) > 0 { + v.stack = append(v.stack, v.tip().children[0]) + } +} + +func (v *Verifier) nextLink(exploreChildren bool) { + last := v.tip() + if len(last.children) > 0 && exploreChildren { + v.stack = append(v.stack, last.children[0]) + v.appendUntilLink() + return + } + // pop the stack + v.stack = v.stack[:len(v.stack)-1] + if len(v.stack) == 0 { + return + } + parent := v.tip() + // find this segments index + childIndex := parent.childSegments[last.segment] + // if this is the last child, parents next sibling + if childIndex == len(parent.children)-1 { + v.nextLink(false) + return + } + // otherwise go to next sibling + v.stack = append(v.stack, parent.children[childIndex+1]) + v.appendUntilLink() +} + +func (v *Verifier) CurrentPath() datamodel.Path { + if v.Done() { + return datamodel.NewPathNocopy(nil) + } + segments := make([]datamodel.PathSegment, 0, len(v.stack)-1) + for i, seg := range v.stack { + if i == 0 { + continue + } + segments = append(segments, seg.segment) + } + return datamodel.NewPathNocopy(segments) +} + +func (v *Verifier) Done() bool { + return len(v.stack) == 0 || (len(v.stack) == 1 && v.stack[0].link == nil) +} + +func (v *Verifier) VerifyNext(link cid.Cid, successful bool) error { + if v.Done() { + return errors.New("nothing left to verify") + } + next := v.tip() + if !next.link.Equals(link) { + return graphsync.RemoteIncorrectResponseError{ + LocalLink: cidlink.Link{Cid: *next.link}, + RemoteLink: cidlink.Link{Cid: link}, + Path: v.CurrentPath(), + } + } + if !next.successful && successful { + return errors.New("verifying against tree with additional data not possible") + } + v.nextLink(successful) + return nil +} diff --git a/requestmanager/reconciledloader/traversalrecord/traversalrecord_test.go b/requestmanager/reconciledloader/traversalrecord/traversalrecord_test.go new file mode 100644 index 00000000..28453c44 --- /dev/null +++ b/requestmanager/reconciledloader/traversalrecord/traversalrecord_test.go @@ -0,0 +1,215 @@ +package traversalrecord_test + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/ipldutil" + "github.com/ipfs/go-graphsync/requestmanager/reconciledloader/traversalrecord" + "github.com/ipfs/go-graphsync/testutil" + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" + cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/traversal" + selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" + "github.com/stretchr/testify/require" +) + +func TestTraversalRecord(t *testing.T) { + testTree := testutil.NewTestIPLDTree() + + traversalRecord := buildTraversalRecord(t, testTree.Storage, testTree.RootNodeLnk) + + expectedAllLinks := []cid.Cid{ + testTree.RootBlock.Cid(), + testTree.MiddleListBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + testTree.LeafBetaBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + testTree.MiddleMapBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + } + require.Equal(t, expectedAllLinks, traversalRecord.AllLinks()) + + expectedListLinks := []cid.Cid{ + testTree.MiddleListBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + testTree.LeafBetaBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + } + + require.Equal(t, expectedListLinks, traversalRecord.GetLinks(datamodel.ParsePath("linkedList"))) + + expectedMapLinks := []cid.Cid{ + testTree.MiddleMapBlock.Cid(), + testTree.LeafAlphaBlock.Cid(), + } + + require.Equal(t, expectedMapLinks, traversalRecord.GetLinks(datamodel.ParsePath("linkedMap"))) + + require.Empty(t, traversalRecord.GetLinks(datamodel.ParsePath("apples"))) +} + +type linkStep struct { + link cid.Cid + successful bool +} + +func TestVerification(t *testing.T) { + testTree := testutil.NewTestIPLDTree() + // add a missing element to the original tree + delete(testTree.Storage, testTree.LeafBetaLnk) + traversalRecord := buildTraversalRecord(t, testTree.Storage, testTree.RootNodeLnk) + + testCases := map[string]struct { + linkSequence []linkStep + expectedError error + expectedPaths []string + }{ + "normal successful verification": { + linkSequence: []linkStep{ + {testTree.RootBlock.Cid(), true}, + {testTree.MiddleListBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafBetaBlock.Cid(), false}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.MiddleMapBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + }, + expectedPaths: []string{ + "", + "linkedList", + "linkedList/0", + "linkedList/1", + "linkedList/2", + "linkedList/3", + "linkedMap", + "linkedMap/nested/alink", + "linkedString", + }, + }, + "successful verification with missing items": { + linkSequence: []linkStep{ + {testTree.RootBlock.Cid(), true}, + {testTree.MiddleListBlock.Cid(), false}, + {testTree.MiddleMapBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + }, + expectedPaths: []string{ + "", + "linkedList", + "linkedMap", + "linkedMap/nested/alink", + "linkedString", + }, + }, + "mismatched verification": { + linkSequence: []linkStep{ + {testTree.RootBlock.Cid(), true}, + {testTree.MiddleListBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafBetaBlock.Cid(), false}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.MiddleMapBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + }, + expectedError: graphsync.RemoteIncorrectResponseError{ + LocalLink: testTree.LeafAlphaLnk, + RemoteLink: testTree.LeafBetaLnk, + Path: datamodel.NewPath([]datamodel.PathSegment{datamodel.PathSegmentOfString("linkedList"), datamodel.PathSegmentOfInt(1)}), + }, + expectedPaths: []string{ + "", + "linkedList", + "linkedList/0", + "linkedList/1", + }, + }, + "additional data on unsuccessful loads": { + linkSequence: []linkStep{ + {testTree.RootBlock.Cid(), true}, + {testTree.MiddleListBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafAlphaBlock.Cid(), true}, + {testTree.LeafBetaBlock.Cid(), true}, + }, + expectedError: errors.New("verifying against tree with additional data not possible"), + expectedPaths: []string{ + "", + "linkedList", + "linkedList/0", + "linkedList/1", + "linkedList/2", + }, + }, + } + + for testCase, data := range testCases { + t.Run(testCase, func(t *testing.T) { + verifier := traversalrecord.NewVerifier(traversalRecord) + var actualErr error + var actualPaths []datamodel.Path + for _, step := range data.linkSequence { + require.False(t, verifier.Done()) + actualPaths = append(actualPaths, verifier.CurrentPath()) + actualErr = verifier.VerifyNext(step.link, step.successful) + if actualErr != nil { + break + } + } + if data.expectedError == nil { + require.NoError(t, actualErr) + require.True(t, verifier.Done()) + } else { + require.EqualError(t, actualErr, data.expectedError.Error()) + require.False(t, verifier.Done()) + } + require.Equal(t, data.expectedPaths, toPathStrings(actualPaths)) + }) + } +} + +func buildTraversalRecord(t *testing.T, storage map[datamodel.Link][]byte, root ipld.Link) *traversalrecord.TraversalRecord { + ctx := context.Background() + traversalRecord := traversalrecord.NewTraversalRecord() + traverser := ipldutil.TraversalBuilder{ + Root: root, + Selector: selectorparse.CommonSelector_ExploreAllRecursively, + }.Start(ctx) + for { + isComplete, err := traverser.IsComplete() + require.NoError(t, err) + if isComplete { + break + } + lnk, linkCtx := traverser.CurrentRequest() + data, successful := storage[lnk] + traversalRecord.RecordNextStep(linkCtx.LinkPath.Segments(), lnk.(cidlink.Link).Cid, successful) + if successful { + traverser.Advance(bytes.NewReader(data)) + } else { + traverser.Error(traversal.SkipMe{}) + } + } + return traversalRecord +} + +func toPathStrings(paths []datamodel.Path) []string { + pathStrings := make([]string, 0, len(paths)) + for _, path := range paths { + pathStrings = append(pathStrings, path.String()) + } + return pathStrings +} diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index e107ae38..fe06304f 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -4,27 +4,26 @@ import ( "context" "errors" "fmt" - "sort" "testing" "time" blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/libp2p/go-libp2p-core/peer" "github.com/stretchr/testify/require" "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/dedupkey" - "github.com/ipfs/go-graphsync/donotsendfirstblocks" "github.com/ipfs/go-graphsync/listeners" gsmsg "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/messagequeue" - "github.com/ipfs/go-graphsync/metadata" + "github.com/ipfs/go-graphsync/persistenceoptions" "github.com/ipfs/go-graphsync/requestmanager/executor" "github.com/ipfs/go-graphsync/requestmanager/hooks" - "github.com/ipfs/go-graphsync/requestmanager/testloader" - "github.com/ipfs/go-graphsync/requestmanager/types" "github.com/ipfs/go-graphsync/taskqueue" "github.com/ipfs/go-graphsync/testutil" ) @@ -42,14 +41,13 @@ func TestNormalSimultaneousFetch(t *testing.T) { returnedResponseChan1, returnedErrorChan1 := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) returnedResponseChan2, returnedErrorChan2 := td.requestManager.NewRequest(requestCtx, peers[0], blockChain2.TipLink, blockChain2.Selector()) - requestRecords := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 2) + requestRecords := readNNetworkRequests(requestCtx, t, td, 2) td.tcm.AssertProtected(t, peers[0]) td.tcm.AssertProtectedWithTags(t, peers[0], requestRecords[0].gsr.ID().Tag(), requestRecords[1].gsr.ID().Tag()) require.Equal(t, peers[0], requestRecords[0].p) require.Equal(t, peers[0], requestRecords[1].p) - require.False(t, requestRecords[0].gsr.IsCancel()) - require.False(t, requestRecords[1].gsr.IsCancel()) + require.Equal(t, requestRecords[0].gsr.Type(), graphsync.RequestTypeNew) require.Equal(t, defaultPriority, requestRecords[0].gsr.Priority()) require.Equal(t, defaultPriority, requestRecords[1].gsr.Priority()) @@ -59,31 +57,14 @@ func TestNormalSimultaneousFetch(t *testing.T) { require.Equal(t, blockChain2.Selector(), requestRecords[1].gsr.Selector(), "did not encode selector properly") firstBlocks := append(td.blockChain.AllBlocks(), blockChain2.Blocks(0, 3)...) - firstMetadata1 := metadataForBlocks(td.blockChain.AllBlocks(), true) - firstMetadataEncoded1, err := metadata.EncodeMetadata(firstMetadata1) - require.NoError(t, err, "did not encode metadata") - firstMetadata2 := metadataForBlocks(blockChain2.Blocks(0, 3), true) - firstMetadataEncoded2, err := metadata.EncodeMetadata(firstMetadata2) - require.NoError(t, err, "did not encode metadata") + firstMetadata1 := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) + firstMetadata2 := metadataForBlocks(blockChain2.Blocks(0, 3), graphsync.LinkActionPresent) firstResponses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: firstMetadataEncoded1, - }), - gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.PartialResponse, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: firstMetadataEncoded2, - }), + gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, firstMetadata1), + gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.PartialResponse, firstMetadata2), } td.requestManager.ProcessResponses(peers[0], firstResponses, firstBlocks) - td.fal.VerifyLastProcessedBlocks(ctx, t, firstBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ - requestRecords[0].gsr.ID(): firstMetadata1, - requestRecords[1].gsr.ID(): firstMetadata2, - }) - td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), td.blockChain.AllBlocks()) - td.fal.SuccessResponseOn(peers[0], requestRecords[1].gsr.ID(), blockChain2.Blocks(0, 3)) td.blockChain.VerifyWholeChain(requestCtx, returnedResponseChan1) blockChain2.VerifyResponseRange(requestCtx, returnedResponseChan2, 0, 3) @@ -93,24 +74,12 @@ func TestNormalSimultaneousFetch(t *testing.T) { td.tcm.AssertProtectedWithTags(t, peers[0], requestRecords[1].gsr.ID().Tag()) moreBlocks := blockChain2.RemainderBlocks(3) - moreMetadata := metadataForBlocks(moreBlocks, true) - moreMetadataEncoded, err := metadata.EncodeMetadata(moreMetadata) - require.NoError(t, err, "did not encode metadata") + moreMetadata := metadataForBlocks(moreBlocks, graphsync.LinkActionPresent) moreResponses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: moreMetadataEncoded, - }), + gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.RequestCompletedFull, moreMetadata), } td.requestManager.ProcessResponses(peers[0], moreResponses, moreBlocks) - td.fal.VerifyLastProcessedBlocks(ctx, t, moreBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ - requestRecords[1].gsr.ID(): moreMetadata, - }) - - td.fal.SuccessResponseOn(peers[0], requestRecords[1].gsr.ID(), moreBlocks) - blockChain2.VerifyRemainder(requestCtx, returnedResponseChan2, 3) testutil.VerifyEmptyErrors(requestCtx, t, returnedErrorChan1) testutil.VerifyEmptyErrors(requestCtx, t, returnedErrorChan2) @@ -131,38 +100,33 @@ func TestCancelRequestInProgress(t *testing.T) { returnedResponseChan1, returnedErrorChan1 := td.requestManager.NewRequest(requestCtx1, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) returnedResponseChan2, returnedErrorChan2 := td.requestManager.NewRequest(requestCtx2, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - requestRecords := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 2) + requestRecords := readNNetworkRequests(requestCtx, t, td, 2) td.tcm.AssertProtected(t, peers[0]) td.tcm.AssertProtectedWithTags(t, peers[0], requestRecords[0].gsr.ID().Tag(), requestRecords[1].gsr.ID().Tag()) firstBlocks := td.blockChain.Blocks(0, 3) - firstMetadata := encodedMetadataForBlocks(t, firstBlocks, true) + firstMetadata := metadataForBlocks(firstBlocks, graphsync.LinkActionPresent) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.PartialResponse, firstMetadata), gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.PartialResponse, firstMetadata), } td.requestManager.ProcessResponses(peers[0], firstResponses, firstBlocks) - - td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), firstBlocks) - td.fal.SuccessResponseOn(peers[0], requestRecords[1].gsr.ID(), firstBlocks) td.blockChain.VerifyResponseRange(requestCtx1, returnedResponseChan1, 0, 3) cancel1() - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] - require.True(t, rr.gsr.IsCancel()) + require.Equal(t, rr.gsr.Type(), graphsync.RequestTypeCancel) require.Equal(t, requestRecords[0].gsr.ID(), rr.gsr.ID()) moreBlocks := td.blockChain.RemainderBlocks(3) - moreMetadata := encodedMetadataForBlocks(t, moreBlocks, true) + moreMetadata := metadataForBlocks(moreBlocks, graphsync.LinkActionPresent) moreResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, moreMetadata), gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.RequestCompletedFull, moreMetadata), } td.requestManager.ProcessResponses(peers[0], moreResponses, moreBlocks) - td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), moreBlocks) - td.fal.SuccessResponseOn(peers[0], requestRecords[1].gsr.ID(), moreBlocks) testutil.VerifyEmptyResponse(requestCtx, t, returnedResponseChan1) td.blockChain.VerifyWholeChain(requestCtx, returnedResponseChan2) @@ -183,42 +147,30 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { defer cancel() peers := testutil.GeneratePeers(1) - postCancel := make(chan struct{}, 1) - loadPostCancel := make(chan struct{}, 1) - td.fal.OnAsyncLoad(func(graphsync.RequestID, ipld.Link, <-chan types.AsyncLoadResult) { - select { - case <-postCancel: - loadPostCancel <- struct{}{} - default: - } - }) - _, returnedErrorChan1 := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - requestRecords := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1) + requestRecords := readNNetworkRequests(requestCtx, t, td, 1) td.tcm.AssertProtected(t, peers[0]) td.tcm.AssertProtectedWithTags(t, peers[0], requestRecords[0].gsr.ID().Tag()) go func() { firstBlocks := td.blockChain.Blocks(0, 3) - firstMetadata := encodedMetadataForBlocks(t, firstBlocks, true) + firstMetadata := metadataForBlocks(firstBlocks, graphsync.LinkActionPresent) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.PartialResponse, firstMetadata), } td.requestManager.ProcessResponses(peers[0], firstResponses, firstBlocks) - td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), firstBlocks) }() timeoutCtx, timeoutCancel := context.WithTimeout(ctx, time.Second) defer timeoutCancel() err := td.requestManager.CancelRequest(timeoutCtx, requestRecords[0].gsr.ID()) require.NoError(t, err) - postCancel <- struct{}{} - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] - require.True(t, rr.gsr.IsCancel()) + require.Equal(t, rr.gsr.Type(), graphsync.RequestTypeCancel) require.Equal(t, requestRecords[0].gsr.ID(), rr.gsr.ID()) td.tcm.RefuteProtected(t, peers[0]) @@ -227,11 +179,6 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { require.Len(t, errors, 1) _, ok := errors[0].(graphsync.RequestClientCancelledErr) require.True(t, ok) - select { - case <-loadPostCancel: - t.Fatalf("Loaded block after cancel") - case <-requestCtx.Done(): - } } func TestCancelManagerExitsGracefully(t *testing.T) { @@ -244,25 +191,23 @@ func TestCancelManagerExitsGracefully(t *testing.T) { returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] firstBlocks := td.blockChain.Blocks(0, 3) - firstMetadata := encodedMetadataForBlocks(t, firstBlocks, true) + firstMetadata := metadataForBlocks(firstBlocks, graphsync.LinkActionPresent) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.PartialResponse, firstMetadata), } td.requestManager.ProcessResponses(peers[0], firstResponses, firstBlocks) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), firstBlocks) td.blockChain.VerifyResponseRange(ctx, returnedResponseChan, 0, 3) managerCancel() moreBlocks := td.blockChain.RemainderBlocks(3) - moreMetadata := encodedMetadataForBlocks(t, moreBlocks, true) + moreMetadata := metadataForBlocks(moreBlocks, graphsync.LinkActionPresent) moreResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, moreMetadata), } td.requestManager.ProcessResponses(peers[0], moreResponses, moreBlocks) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), moreBlocks) testutil.VerifyEmptyResponse(requestCtx, t, returnedResponseChan) testutil.VerifyEmptyErrors(requestCtx, t, returnedErrorChan) } @@ -276,12 +221,12 @@ func TestFailedRequest(t *testing.T) { returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] td.tcm.AssertProtected(t, peers[0]) td.tcm.AssertProtectedWithTags(t, peers[0], rr.gsr.ID().Tag()) failedResponses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestFailedContentNotFound), + gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestFailedContentNotFound, nil), } td.requestManager.ProcessResponses(peers[0], failedResponses, nil) @@ -290,6 +235,13 @@ func TestFailedRequest(t *testing.T) { td.tcm.RefuteProtected(t, peers[0]) } +/* +TODO: Delete? These tests no longer seem relevant, or at minimum need a rearchitect +- the new architecture will simply never fire a graphsync request if all of the data is +preset + +Perhaps we should put this back in as a mode? Or make the "wait to fire" and exprimental feature? + func TestLocallyFulfilledFirstRequestFailsLater(t *testing.T) { ctx := context.Background() td := newTestData(ctx, t) @@ -348,6 +300,7 @@ func TestLocallyFulfilledFirstRequestSucceedsLater(t *testing.T) { testutil.VerifyEmptyErrors(ctx, t, returnedErrorChan) testutil.AssertDoesReceive(requestCtx, t, called, "response hooks called for response") } +*/ func TestRequestReturnsMissingBlocks(t *testing.T) { ctx := context.Background() @@ -359,16 +312,13 @@ func TestRequestReturnsMissingBlocks(t *testing.T) { returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] - md := encodedMetadataForBlocks(t, td.blockChain.AllBlocks(), false) + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionMissing) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedPartial, md), } td.requestManager.ProcessResponses(peers[0], firstResponses, nil) - for _, block := range td.blockChain.AllBlocks() { - td.fal.ResponseOn(peers[0], rr.gsr.ID(), cidlink.Link{Cid: block.Cid()}, types.AsyncLoadResult{Data: nil, Err: fmt.Errorf("Terrible Thing")}) - } testutil.VerifyEmptyResponse(ctx, t, returnedResponseChan) errs := testutil.CollectErrors(ctx, t, returnedErrorChan) require.NotEqual(t, len(errs), 0, "did not send errors") @@ -419,7 +369,7 @@ func TestEncodingExtensions(t *testing.T) { peers := testutil.GeneratePeers(1) expectedError := make(chan error, 2) - receivedExtensionData := make(chan []byte, 2) + receivedExtensionData := make(chan datamodel.Node, 2) expectedUpdateChan := make(chan []graphsync.ExtensionData, 2) hook := func(p peer.ID, responseData graphsync.ResponseData, hookActions graphsync.IncomingResponseHookActions) { data, has := responseData.Extension(td.extensionName1) @@ -437,7 +387,7 @@ func TestEncodingExtensions(t *testing.T) { td.responseHooks.Register(hook) returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector(), td.extension1, td.extension2) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] gsr := rr.gsr returnedData1, found := gsr.Extension(td.extensionName1) @@ -449,14 +399,12 @@ func TestEncodingExtensions(t *testing.T) { require.Equal(t, td.extensionData2, returnedData2, "did not encode second extension correctly") t.Run("responding to extensions", func(t *testing.T) { - expectedData := testutil.RandomBytes(100) - expectedUpdate := testutil.RandomBytes(100) + expectedData := basicnode.NewBytes(testutil.RandomBytes(100)) + expectedUpdate := basicnode.NewBytes(testutil.RandomBytes(100)) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(gsr.ID(), - graphsync.PartialResponse, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: nil, - }, + graphsync.PartialResponse, + nil, graphsync.ExtensionData{ Name: td.extensionName1, Data: expectedData, @@ -471,25 +419,23 @@ func TestEncodingExtensions(t *testing.T) { }, } td.requestManager.ProcessResponses(peers[0], firstResponses, nil) - var received []byte + var received datamodel.Node testutil.AssertReceive(ctx, t, receivedExtensionData, &received, "did not receive extension data") require.Equal(t, expectedData, received, "did not receive correct extension data from resposne") - rr = readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr = readNNetworkRequests(requestCtx, t, td, 1)[0] receivedUpdateData, has := rr.gsr.Extension(td.extensionName1) require.True(t, has) require.Equal(t, expectedUpdate, receivedUpdateData, "should have updated with correct extension") - nextExpectedData := testutil.RandomBytes(100) - nextExpectedUpdate1 := testutil.RandomBytes(100) - nextExpectedUpdate2 := testutil.RandomBytes(100) + nextExpectedData := basicnode.NewBytes(testutil.RandomBytes(100)) + nextExpectedUpdate1 := basicnode.NewBytes(testutil.RandomBytes(100)) + nextExpectedUpdate2 := basicnode.NewBytes(testutil.RandomBytes(100)) secondResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(gsr.ID(), - graphsync.PartialResponse, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: nil, - }, + graphsync.PartialResponse, + nil, graphsync.ExtensionData{ Name: td.extensionName1, Data: nextExpectedData, @@ -511,7 +457,7 @@ func TestEncodingExtensions(t *testing.T) { testutil.AssertReceive(ctx, t, receivedExtensionData, &received, "did not receive extension data") require.Equal(t, nextExpectedData, received, "did not receive correct extension data from resposne") - rr = readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr = readNNetworkRequests(requestCtx, t, td, 1)[0] receivedUpdateData, has = rr.gsr.Extension(td.extensionName1) require.True(t, has) require.Equal(t, nextExpectedUpdate1, receivedUpdateData, "should have updated with correct extension") @@ -551,7 +497,7 @@ func TestBlockHooks(t *testing.T) { td.blockHooks.Register(hook) returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector(), td.extension1, td.extension2) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] gsr := rr.gsr returnedData1, found := gsr.Extension(td.extensionName1) @@ -563,19 +509,15 @@ func TestBlockHooks(t *testing.T) { require.Equal(t, td.extensionData2, returnedData2, "did not encode second extension correctly") t.Run("responding to extensions", func(t *testing.T) { - expectedData := testutil.RandomBytes(100) - expectedUpdate := testutil.RandomBytes(100) + expectedData := basicnode.NewBytes(testutil.RandomBytes(100)) + expectedUpdate := basicnode.NewBytes(testutil.RandomBytes(100)) firstBlocks := td.blockChain.Blocks(0, 3) - firstMetadata := metadataForBlocks(firstBlocks, true) - firstMetadataEncoded, err := metadata.EncodeMetadata(firstMetadata) - require.NoError(t, err, "did not encode metadata") + firstMetadata := metadataForBlocks(firstBlocks, graphsync.LinkActionPresent) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(gsr.ID(), - graphsync.PartialResponse, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: firstMetadataEncoded, - }, + graphsync.PartialResponse, + firstMetadata, graphsync.ExtensionData{ Name: td.extensionName1, Data: expectedData, @@ -597,13 +539,8 @@ func TestBlockHooks(t *testing.T) { } td.requestManager.ProcessResponses(peers[0], firstResponses, firstBlocks) - td.fal.VerifyLastProcessedBlocks(ctx, t, firstBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ - rr.gsr.ID(): firstMetadata, - }) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), firstBlocks) - ur := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + ur := readNNetworkRequests(requestCtx, t, td, 1)[0] receivedUpdateData, has := ur.gsr.Extension(td.extensionName1) require.True(t, has) require.Equal(t, expectedUpdate, receivedUpdateData, "should have updated with correct extension") @@ -613,9 +550,12 @@ func TestBlockHooks(t *testing.T) { testutil.AssertReceive(ctx, t, receivedResponses, &receivedResponse, "did not receive response data") require.Equal(t, firstResponses[0].RequestID(), receivedResponse.RequestID(), "did not receive correct response ID") require.Equal(t, firstResponses[0].Status(), receivedResponse.Status(), "did not receive correct response status") - metadata, has := receivedResponse.Extension(graphsync.ExtensionMetadata) - require.True(t, has) - require.Equal(t, firstMetadataEncoded, metadata, "should receive correct metadata") + md := make([]gsmsg.GraphSyncLinkMetadatum, 0) + receivedResponse.Metadata().Iterate(func(c cid.Cid, la graphsync.LinkAction) { + md = append(md, gsmsg.GraphSyncLinkMetadatum{Link: c, Action: graphsync.LinkActionPresent}) + }) + require.Greater(t, len(md), 0) + require.Equal(t, firstMetadata, md, "should receive correct metadata") receivedExtensionData, _ := receivedResponse.Extension(td.extensionName1) require.Equal(t, expectedData, receivedExtensionData, "should receive correct response extension data") var receivedBlock graphsync.BlockData @@ -624,19 +564,15 @@ func TestBlockHooks(t *testing.T) { require.Equal(t, uint64(len(blk.RawData())), receivedBlock.BlockSize()) } - nextExpectedData := testutil.RandomBytes(100) - nextExpectedUpdate1 := testutil.RandomBytes(100) - nextExpectedUpdate2 := testutil.RandomBytes(100) + nextExpectedData := basicnode.NewBytes(testutil.RandomBytes(100)) + nextExpectedUpdate1 := basicnode.NewBytes(testutil.RandomBytes(100)) + nextExpectedUpdate2 := basicnode.NewBytes(testutil.RandomBytes(100)) nextBlocks := td.blockChain.RemainderBlocks(3) - nextMetadata := metadataForBlocks(nextBlocks, true) - nextMetadataEncoded, err := metadata.EncodeMetadata(nextMetadata) - require.NoError(t, err) + nextMetadata := metadataForBlocks(nextBlocks, graphsync.LinkActionPresent) secondResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(gsr.ID(), - graphsync.RequestCompletedFull, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: nextMetadataEncoded, - }, + graphsync.RequestCompletedFull, + nextMetadata, graphsync.ExtensionData{ Name: td.extensionName1, Data: nextExpectedData, @@ -661,13 +597,8 @@ func TestBlockHooks(t *testing.T) { expectedUpdateChan <- update } td.requestManager.ProcessResponses(peers[0], secondResponses, nextBlocks) - td.fal.VerifyLastProcessedBlocks(ctx, t, nextBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ - rr.gsr.ID(): nextMetadata, - }) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), nextBlocks) - ur = readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + ur = readNNetworkRequests(requestCtx, t, td, 1)[0] receivedUpdateData, has = ur.gsr.Extension(td.extensionName1) require.True(t, has) require.Equal(t, nextExpectedUpdate1, receivedUpdateData, "should have updated with correct extension") @@ -680,9 +611,12 @@ func TestBlockHooks(t *testing.T) { testutil.AssertReceive(ctx, t, receivedResponses, &receivedResponse, "did not receive response data") require.Equal(t, secondResponses[0].RequestID(), receivedResponse.RequestID(), "did not receive correct response ID") require.Equal(t, secondResponses[0].Status(), receivedResponse.Status(), "did not receive correct response status") - metadata, has := receivedResponse.Extension(graphsync.ExtensionMetadata) - require.True(t, has) - require.Equal(t, nextMetadataEncoded, metadata, "should receive correct metadata") + md := make([]gsmsg.GraphSyncLinkMetadatum, 0) + receivedResponse.Metadata().Iterate(func(c cid.Cid, la graphsync.LinkAction) { + md = append(md, gsmsg.GraphSyncLinkMetadatum{Link: c, Action: graphsync.LinkActionPresent}) + }) + require.Greater(t, len(md), 0) + require.Equal(t, nextMetadata, md, "should receive correct metadata") receivedExtensionData, _ := receivedResponse.Extension(td.extensionName1) require.Equal(t, nextExpectedData, receivedExtensionData, "should receive correct response extension data") var receivedBlock graphsync.BlockData @@ -704,6 +638,8 @@ func TestOutgoingRequestHooks(t *testing.T) { defer cancel() peers := testutil.GeneratePeers(1) + alternateStore := testutil.NewTestStore(make(map[datamodel.Link][]byte)) + td.persistenceOptions.Register("chainstore", alternateStore) hook := func(p peer.ID, r graphsync.RequestData, ha graphsync.OutgoingRequestHookActions) { _, has := r.Extension(td.extensionName1) if has { @@ -716,7 +652,7 @@ func TestOutgoingRequestHooks(t *testing.T) { returnedResponseChan1, returnedErrorChan1 := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector(), td.extension1) returnedResponseChan2, returnedErrorChan2 := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - requestRecords := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 2) + requestRecords := readNNetworkRequests(requestCtx, t, td, 2) dedupData, has := requestRecords[0].gsr.Extension(graphsync.ExtensionDeDupByKey) require.True(t, has) @@ -724,32 +660,17 @@ func TestOutgoingRequestHooks(t *testing.T) { require.NoError(t, err) require.Equal(t, "chainstore", key) - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) - mdExt := graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: mdEncoded, - } + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) responses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, mdExt), - gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.RequestCompletedFull, mdExt), + gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, md), + gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.RequestCompletedFull, md), } td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.AllBlocks()) - td.fal.VerifyLastProcessedBlocks(ctx, t, td.blockChain.AllBlocks()) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ - requestRecords[0].gsr.ID(): md, - requestRecords[1].gsr.ID(): md, - }) - td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), td.blockChain.AllBlocks()) - td.fal.SuccessResponseOn(peers[0], requestRecords[1].gsr.ID(), td.blockChain.AllBlocks()) td.blockChain.VerifyWholeChainWithTypes(requestCtx, returnedResponseChan1) td.blockChain.VerifyWholeChain(requestCtx, returnedResponseChan2) testutil.VerifyEmptyErrors(ctx, t, returnedErrorChan1) testutil.VerifyEmptyErrors(ctx, t, returnedErrorChan2) - td.fal.VerifyStoreUsed(t, requestRecords[0].gsr.ID(), "chainstore") - td.fal.VerifyStoreUsed(t, requestRecords[1].gsr.ID(), "") } type outgoingRequestProcessingEvent struct { @@ -774,7 +695,7 @@ func TestOutgoingRequestListeners(t *testing.T) { returnedResponseChan1, returnedErrorChan1 := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector(), td.extension1) - requestRecords := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1) + requestRecords := readNNetworkRequests(requestCtx, t, td, 1) // Should have fired by now select { @@ -787,22 +708,11 @@ func TestOutgoingRequestListeners(t *testing.T) { t.Fatal("should fire outgoing request listener") } - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) - mdExt := graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: mdEncoded, - } + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) responses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, mdExt), + gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, md), } td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.AllBlocks()) - td.fal.VerifyLastProcessedBlocks(ctx, t, td.blockChain.AllBlocks()) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ - requestRecords[0].gsr.ID(): md, - }) - td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), td.blockChain.AllBlocks()) td.blockChain.VerifyWholeChain(requestCtx, returnedResponseChan1) testutil.VerifyEmptyErrors(requestCtx, t, returnedErrorChan1) @@ -837,24 +747,18 @@ func TestPauseResume(t *testing.T) { // Start request returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] // Start processing responses - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) responses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: mdEncoded, - }), + gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, md), } td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.AllBlocks()) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) // attempt to unpause while request is not paused (note: hook on second block will keep it from // reaching pause point) - err = td.requestManager.UnpauseRequest(rr.gsr.ID()) + err := td.requestManager.UnpauseRequest(ctx, rr.gsr.ID()) require.EqualError(t, err, "request is not paused") close(holdForResumeAttempt) // verify responses sent read ONLY for blocks BEFORE the pause @@ -863,40 +767,43 @@ func TestPauseResume(t *testing.T) { <-holdForPause // read the outgoing cancel request - pauseCancel := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] - require.True(t, pauseCancel.gsr.IsCancel()) + pauseCancel := readNNetworkRequests(requestCtx, t, td, 1)[0] + require.Equal(t, pauseCancel.gsr.Type(), graphsync.RequestTypeCancel) // verify no further responses come through time.Sleep(100 * time.Millisecond) testutil.AssertChannelEmpty(t, returnedResponseChan, "no response should be sent request is paused") - td.fal.CleanupRequest(peers[0], rr.gsr.ID()) // unpause - err = td.requestManager.UnpauseRequest(rr.gsr.ID(), td.extension1, td.extension2) + err = td.requestManager.UnpauseRequest(ctx, rr.gsr.ID(), td.extension1, td.extension2) require.NoError(t, err) - // verify the correct new request with Do-no-send-cids & other extensions - resumedRequest := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] - doNotSendFirstBlocksData, has := resumedRequest.gsr.Extension(graphsync.ExtensionsDoNotSendFirstBlocks) - doNotSendFirstBlocks, err := donotsendfirstblocks.DecodeDoNotSendFirstBlocks(doNotSendFirstBlocksData) - require.NoError(t, err) - require.Equal(t, pauseAt, int(doNotSendFirstBlocks)) - require.True(t, has) - ext1Data, has := resumedRequest.gsr.Extension(td.extensionName1) - require.True(t, has) - require.Equal(t, td.extensionData1, ext1Data) - ext2Data, has := resumedRequest.gsr.Extension(td.extensionName2) - require.True(t, has) - require.Equal(t, td.extensionData2, ext2Data) + /* + TODO: these are no longer used as the old responses are consumed upon restart, to minimize + network utilization -- does this make sense? Maybe we should throw out these responses while paused? + + // verify the correct new request with Do-no-send-cids & other extensions + resumedRequest := readNNetworkRequests(requestCtx, t, td, 1)[0] + doNotSendFirstBlocksData, has := resumedRequest.gsr.Extension(graphsync.ExtensionsDoNotSendFirstBlocks) + doNotSendFirstBlocks, err := donotsendfirstblocks.DecodeDoNotSendFirstBlocks(doNotSendFirstBlocksData) + require.NoError(t, err) + require.Equal(t, pauseAt, int(doNotSendFirstBlocks)) + require.True(t, has) + ext1Data, has := resumedRequest.gsr.Extension(td.extensionName1) + require.True(t, has) + require.Equal(t, td.extensionData1, ext1Data) + ext2Data, has := resumedRequest.gsr.Extension(td.extensionName2) + require.True(t, has) + require.Equal(t, td.extensionData2, ext2Data) + */ // process responses td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.RemainderBlocks(pauseAt)) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) - // verify the correct results are returned, picking up after where there request was paused td.blockChain.VerifyRemainder(ctx, returnedResponseChan, pauseAt) testutil.VerifyEmptyErrors(ctx, t, returnedErrorChan) } + func TestPauseResumeExternal(t *testing.T) { ctx := context.Background() td := newTestData(ctx, t) @@ -913,7 +820,7 @@ func TestPauseResumeExternal(t *testing.T) { hook := func(p peer.ID, responseData graphsync.ResponseData, blockData graphsync.BlockData, hookActions graphsync.IncomingBlockHookActions) { blocksReceived++ if blocksReceived == pauseAt { - err := td.requestManager.PauseRequest(responseData.RequestID()) + err := td.requestManager.PauseRequest(ctx, responseData.RequestID()) require.NoError(t, err) close(holdForPause) } @@ -923,40 +830,36 @@ func TestPauseResumeExternal(t *testing.T) { // Start request returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] // Start processing responses - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) responses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: mdEncoded, - }), + gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, md), } td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.AllBlocks()) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) // verify responses sent read ONLY for blocks BEFORE the pause td.blockChain.VerifyResponseRange(ctx, returnedResponseChan, 0, pauseAt) // wait for the pause to occur <-holdForPause // read the outgoing cancel request - pauseCancel := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] - require.True(t, pauseCancel.gsr.IsCancel()) + pauseCancel := readNNetworkRequests(requestCtx, t, td, 1)[0] + require.Equal(t, pauseCancel.gsr.Type(), graphsync.RequestTypeCancel) // verify no further responses come through time.Sleep(100 * time.Millisecond) testutil.AssertChannelEmpty(t, returnedResponseChan, "no response should be sent request is paused") - td.fal.CleanupRequest(peers[0], rr.gsr.ID()) // unpause - err = td.requestManager.UnpauseRequest(rr.gsr.ID(), td.extension1, td.extension2) + err := td.requestManager.UnpauseRequest(ctx, rr.gsr.ID(), td.extension1, td.extension2) require.NoError(t, err) // verify the correct new request with Do-no-send-cids & other extensions - resumedRequest := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + /* TODO: these are no longer used as the old responses are consumed upon restart, to minimize + network utilization -- does this make sense? Maybe we should throw out these responses while paused? + + resumedRequest := readNNetworkRequests(requestCtx, t, td, 1)[0] doNotSendFirstBlocksData, has := resumedRequest.gsr.Extension(graphsync.ExtensionsDoNotSendFirstBlocks) doNotSendFirstBlocks, err := donotsendfirstblocks.DecodeDoNotSendFirstBlocks(doNotSendFirstBlocksData) require.NoError(t, err) @@ -967,17 +870,98 @@ func TestPauseResumeExternal(t *testing.T) { require.Equal(t, td.extensionData1, ext1Data) ext2Data, has := resumedRequest.gsr.Extension(td.extensionName2) require.True(t, has) - require.Equal(t, td.extensionData2, ext2Data) + require.Equal(t, td.extensionData2, ext2Data)*/ // process responses td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.RemainderBlocks(pauseAt)) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) // verify the correct results are returned, picking up after where there request was paused td.blockChain.VerifyRemainder(ctx, returnedResponseChan, pauseAt) testutil.VerifyEmptyErrors(ctx, t, returnedErrorChan) } +func TestUpdateRequest(t *testing.T) { + ctx := context.Background() + td := newTestData(ctx, t) + + requestCtx, cancel := context.WithCancel(ctx) + defer cancel() + requestCtx1, cancel1 := context.WithCancel(requestCtx) + + peers := testutil.GeneratePeers(1) + + blocksReceived := 0 + holdForPause := make(chan struct{}) + + // setup hook to pause when the blocks start flowing + hook := func(p peer.ID, responseData graphsync.ResponseData, blockData graphsync.BlockData, hookActions graphsync.IncomingBlockHookActions) { + blocksReceived++ + if blocksReceived == 1 { + err := td.requestManager.PauseRequest(ctx, responseData.RequestID()) + require.NoError(t, err) + close(holdForPause) + } + } + td.blockHooks.Register(hook) + + // Start request + returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx1, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) + + rr := readNNetworkRequests(requestCtx, t, td, 1)[0] + + // Start processing responses + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) + responses := []gsmsg.GraphSyncResponse{ + gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, md), + } + td.requestManager.ProcessResponses(peers[0], responses, td.blockChain.AllBlocks()) + td.blockChain.VerifyResponseRange(ctx, returnedResponseChan, 0, 1) + + // wait for the pause to occur + <-holdForPause + + // read the outgoing cancel request + readNNetworkRequests(requestCtx, t, td, 1) + + // verify no further responses come through + time.Sleep(100 * time.Millisecond) + testutil.AssertChannelEmpty(t, returnedResponseChan, "no response should be sent request is paused") + + // send an update with some custom extensions + ext1Name := graphsync.ExtensionName("grip grop") + ext1Data := "flim flam, blim blam" + ext2Name := graphsync.ExtensionName("Humpty/Dumpty") + var ext2Data int64 = 101 + + err := td.requestManager.UpdateRequest(ctx, rr.gsr.ID(), + graphsync.ExtensionData{Name: ext1Name, Data: basicnode.NewString(ext1Data)}, + graphsync.ExtensionData{Name: ext2Name, Data: basicnode.NewInt(ext2Data)}, + ) + require.NoError(t, err) + + // verify the correct new request and its extensions + updatedRequest := readNNetworkRequests(requestCtx, t, td, 1)[0] + + actualData, has := updatedRequest.gsr.Extension(ext1Name) + require.True(t, has, "has expected extension1") + actualString, err := actualData.AsString() + require.NoError(t, err) + require.Equal(t, ext1Data, actualString) + + actualData, has = updatedRequest.gsr.Extension(ext2Name) + require.True(t, has, "has expected extension2") + actualInt, err := actualData.AsInt() + require.NoError(t, err) + require.Equal(t, ext2Data, actualInt) + + // pack down + cancel1() + errors := testutil.CollectErrors(requestCtx, t, returnedErrorChan) + require.Len(t, errors, 1) + _, ok := errors[0].(graphsync.RequestClientCancelledErr) + require.True(t, ok) +} + func TestStats(t *testing.T) { ctx := context.Background() td := newTestData(ctx, t) @@ -992,7 +976,7 @@ func TestStats(t *testing.T) { _, _ = td.requestManager.NewRequest(requestCtx, peers[0], blockChain2.TipLink, blockChain2.Selector()) _, _ = td.requestManager.NewRequest(requestCtx, peers[1], td.blockChain.TipLink, td.blockChain.Selector()) - requestRecords := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 3) + requestRecords := readNNetworkRequests(requestCtx, t, td, 3) peerState := td.requestManager.PeerState(peers[0]) require.Len(t, peerState.RequestStates, 2) @@ -1027,50 +1011,39 @@ func (fph *fakePeerHandler) AllocateAndBuildMessage(p peer.ID, blkSize uint64, } } -func readNNetworkRequests(ctx context.Context, - t *testing.T, - requestRecordChan <-chan requestRecord, - count int) []requestRecord { - requestRecords := make([]requestRecord, 0, count) +func readNNetworkRequests(ctx context.Context, t *testing.T, td *testData, count int) []requestRecord { + requestRecords := make(map[graphsync.RequestID]requestRecord, count) for i := 0; i < count; i++ { var rr requestRecord - testutil.AssertReceive(ctx, t, requestRecordChan, &rr, fmt.Sprintf("did not receive request %d", i)) - requestRecords = append(requestRecords, rr) + testutil.AssertReceive(ctx, t, td.requestRecordChan, &rr, fmt.Sprintf("did not receive request %d", i)) + requestRecords[rr.gsr.ID()] = rr } // because of the simultaneous request queues it's possible for the requests to go to the network layer out of order // if the requests are queued at a near identical time - sort.Slice(requestRecords, func(i, j int) bool { - return requestRecords[i].gsr.ID() < requestRecords[j].gsr.ID() - }) - return requestRecords + sorted := make([]requestRecord, 0, len(requestRecords)) + for _, id := range td.requestIds { + if rr, ok := requestRecords[id]; ok { + sorted = append(sorted, rr) + } + } + return sorted } -func metadataForBlocks(blks []blocks.Block, present bool) metadata.Metadata { - md := make(metadata.Metadata, 0, len(blks)) +func metadataForBlocks(blks []blocks.Block, action graphsync.LinkAction) []gsmsg.GraphSyncLinkMetadatum { + md := make([]gsmsg.GraphSyncLinkMetadatum, 0, len(blks)) for _, block := range blks { - md = append(md, metadata.Item{ - Link: block.Cid(), - BlockPresent: present, + md = append(md, gsmsg.GraphSyncLinkMetadatum{ + Link: block.Cid(), + Action: action, }) } return md } -func encodedMetadataForBlocks(t *testing.T, blks []blocks.Block, present bool) graphsync.ExtensionData { - t.Helper() - md := metadataForBlocks(blks, present) - metadataEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err, "did not encode metadata") - return graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: metadataEncoded, - } -} - type testData struct { requestRecordChan chan requestRecord fph *fakePeerHandler - fal *testloader.FakeAsyncLoader + persistenceOptions *persistenceoptions.PersistenceOptions tcm *testutil.TestConnManager requestHooks *hooks.OutgoingRequestHooks responseHooks *hooks.IncomingResponseHooks @@ -1078,17 +1051,20 @@ type testData struct { requestManager *RequestManager blockStore map[ipld.Link][]byte persistence ipld.LinkSystem + localBlockStore map[ipld.Link][]byte + localPersistence ipld.LinkSystem blockChain *testutil.TestBlockChain extensionName1 graphsync.ExtensionName - extensionData1 []byte + extensionData1 datamodel.Node extension1 graphsync.ExtensionData extensionName2 graphsync.ExtensionName - extensionData2 []byte + extensionData2 datamodel.Node extension2 graphsync.ExtensionData networkErrorListeners *listeners.NetworkErrorListeners outgoingRequestProcessingListeners *listeners.OutgoingRequestProcessingListeners taskqueue *taskqueue.WorkerTaskQueue executor *executor.Executor + requestIds []graphsync.RequestID } func newTestData(ctx context.Context, t *testing.T) *testData { @@ -1096,7 +1072,7 @@ func newTestData(ctx context.Context, t *testing.T) *testData { td := &testData{} td.requestRecordChan = make(chan requestRecord, 3) td.fph = &fakePeerHandler{td.requestRecordChan} - td.fal = testloader.NewFakeAsyncLoader() + td.persistenceOptions = persistenceoptions.New() td.tcm = testutil.NewTestConnManager() td.requestHooks = hooks.NewRequestHooks() td.responseHooks = hooks.NewResponseHooks() @@ -1104,26 +1080,30 @@ func newTestData(ctx context.Context, t *testing.T) *testData { td.networkErrorListeners = listeners.NewNetworkErrorListeners() td.outgoingRequestProcessingListeners = listeners.NewOutgoingRequestProcessingListeners() td.taskqueue = taskqueue.NewTaskQueue(ctx) - lsys := cidlink.DefaultLinkSystem() - td.requestManager = New(ctx, td.fal, lsys, td.requestHooks, td.responseHooks, td.networkErrorListeners, td.outgoingRequestProcessingListeners, td.taskqueue, td.tcm, 0) - td.executor = executor.NewExecutor(td.requestManager, td.blockHooks, td.fal.AsyncLoad) + td.localBlockStore = make(map[ipld.Link][]byte) + td.localPersistence = testutil.NewTestStore(td.localBlockStore) + td.requestManager = New(ctx, td.persistenceOptions, td.localPersistence, td.requestHooks, td.responseHooks, td.networkErrorListeners, td.outgoingRequestProcessingListeners, td.taskqueue, td.tcm, 0) + td.executor = executor.NewExecutor(td.requestManager, td.blockHooks) td.requestManager.SetDelegate(td.fph) td.requestManager.Startup() td.taskqueue.Startup(6, td.executor) td.blockStore = make(map[ipld.Link][]byte) td.persistence = testutil.NewTestStore(td.blockStore) td.blockChain = testutil.SetupBlockChain(ctx, t, td.persistence, 100, 5) - td.extensionData1 = testutil.RandomBytes(100) + td.extensionData1 = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionName1 = graphsync.ExtensionName("AppleSauce/McGee") td.extension1 = graphsync.ExtensionData{ Name: td.extensionName1, Data: td.extensionData1, } - td.extensionData2 = testutil.RandomBytes(100) + td.extensionData2 = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionName2 = graphsync.ExtensionName("HappyLand/Happenstance") td.extension2 = graphsync.ExtensionData{ Name: td.extensionName2, Data: td.extensionData2, } + td.requestHooks.Register(func(p peer.ID, request graphsync.RequestData, hookActions graphsync.OutgoingRequestHookActions) { + td.requestIds = append(td.requestIds, request.ID()) + }) return td } diff --git a/requestmanager/server.go b/requestmanager/server.go index 63de7a91..0b76876d 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -8,9 +8,11 @@ import ( "time" blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" "github.com/ipfs/go-peertaskqueue/peertask" "github.com/ipfs/go-peertaskqueue/peertracker" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/linking" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/traversal" "github.com/ipld/go-ipld-prime/traversal/selector" @@ -28,6 +30,7 @@ import ( "github.com/ipfs/go-graphsync/peerstate" "github.com/ipfs/go-graphsync/requestmanager/executor" "github.com/ipfs/go-graphsync/requestmanager/hooks" + "github.com/ipfs/go-graphsync/requestmanager/reconciledloader" ) // The code in this file implements the internal thread for the request manager. @@ -55,16 +58,15 @@ func (rm *RequestManager) cleanupInProcessRequests() { } func (rm *RequestManager) newRequest(parentSpan trace.Span, p peer.ID, root ipld.Link, selector ipld.Node, extensions []graphsync.ExtensionData) (gsmsg.GraphSyncRequest, chan graphsync.ResponseProgress, chan error) { - requestID := rm.nextRequestID - rm.nextRequestID++ + requestID := graphsync.NewRequestID() - parentSpan.SetAttributes(attribute.Int("requestID", int(requestID))) + parentSpan.SetAttributes(attribute.String("requestID", requestID.String())) ctx, span := otel.Tracer("graphsync").Start(trace.ContextWithSpan(rm.ctx, parentSpan), "newRequest") defer span.End() - log.Infow("graphsync request initiated", "request id", requestID, "peer", p, "root", root) + log.Infow("graphsync request initiated", "request id", requestID.String(), "peer", p, "root", root) - request, hooksResult, err := rm.validateRequest(requestID, p, root, selector, extensions) + request, hooksResult, lsys, err := rm.validateRequest(requestID, p, root, selector, extensions) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -98,8 +100,9 @@ func (rm *RequestManager) newRequest(parentSpan trace.Span, p peer.ID, root ipld nodeStyleChooser: hooksResult.CustomChooser, inProgressChan: make(chan graphsync.ResponseProgress), inProgressErr: make(chan error), + lsys: lsys, } - requestStatus.lastResponse.Store(gsmsg.NewResponse(request.ID(), graphsync.RequestAcknowledged)) + requestStatus.lastResponse.Store(gsmsg.NewResponse(request.ID(), graphsync.RequestAcknowledged, nil)) rm.inProgressRequestStatuses[request.ID()] = requestStatus rm.connManager.Protect(p, requestID.Tag()) @@ -112,11 +115,9 @@ func (rm *RequestManager) requestTask(requestID graphsync.RequestID) executor.Re if !ok { return executor.RequestTask{Empty: true} } - log.Infow("graphsync request processing begins", "request id", requestID, "peer", ipr.p, "total time", time.Since(ipr.startTime)) + log.Infow("graphsync request processing begins", "request id", requestID.String(), "peer", ipr.p, "total time", time.Since(ipr.startTime)) - var initialRequest bool if ipr.traverser == nil { - initialRequest = true var budget *traversal.Budget if rm.maxLinksPerRequest > 0 { budget = &traversal.Budget{ @@ -148,6 +149,7 @@ func (rm *RequestManager) requestTask(requestID graphsync.RequestID) executor.Re Budget: budget, }.Start(ctx) + ipr.reconciledLoader = reconciledloader.NewReconciledLoader(ipr.request.ID(), ipr.lsys) inProgressCount := len(rm.inProgressRequestStatuses) rm.outgoingRequestProcessingListeners.NotifyOutgoingRequestProcessingListeners(ipr.p, ipr.request, inProgressCount) } @@ -163,7 +165,7 @@ func (rm *RequestManager) requestTask(requestID graphsync.RequestID) executor.Re Traverser: ipr.traverser, P: ipr.p, InProgressErr: ipr.inProgressErr, - InitialRequest: initialRequest, + ReconciledLoader: ipr.reconciledLoader, Empty: false, } } @@ -191,7 +193,9 @@ func (rm *RequestManager) terminateRequest(requestID graphsync.RequestID, ipr *i rm.connManager.Unprotect(ipr.p, requestID.Tag()) delete(rm.inProgressRequestStatuses, requestID) ipr.cancelFn() - rm.asyncLoader.CleanupRequest(ipr.p, requestID) + if ipr.reconciledLoader != nil { + ipr.reconciledLoader.Cleanup(rm.ctx) + } if ipr.traverser != nil { ipr.traverserCancel() ipr.traverser.Shutdown(rm.ctx) @@ -225,7 +229,7 @@ func (rm *RequestManager) releaseRequestTask(p peer.ID, task *peertask.Task, err ipr.state = graphsync.Paused return } - log.Infow("graphsync request complete", "request id", requestID, "peer", ipr.p, "total time", time.Since(ipr.startTime)) + log.Infow("graphsync request complete", "request id", requestID.String(), "peer", ipr.p, "total time", time.Since(ipr.startTime)) rm.terminateRequest(requestID, ipr) } @@ -244,7 +248,7 @@ func (rm *RequestManager) cancelRequest(requestID graphsync.RequestID, onTermina if onTerminated != nil { inProgressRequestStatus.onTerminated = append(inProgressRequestStatus.onTerminated, onTerminated) } - rm.SendRequest(inProgressRequestStatus.p, gsmsg.CancelRequest(requestID)) + rm.SendRequest(inProgressRequestStatus.p, gsmsg.NewCancelRequest(requestID)) rm.cancelOnError(requestID, inProgressRequestStatus, terminalError) } @@ -256,26 +260,38 @@ func (rm *RequestManager) cancelOnError(requestID graphsync.RequestID, ipr *inPr rm.terminateRequest(requestID, ipr) } else { ipr.cancelFn() - rm.asyncLoader.CompleteResponsesFor(requestID) + ipr.reconciledLoader.SetRemoteOnline(false) } } -func (rm *RequestManager) processResponses(p peer.ID, responses []gsmsg.GraphSyncResponse, blks []blocks.Block) { +func (rm *RequestManager) processResponses(p peer.ID, + responses []gsmsg.GraphSyncResponse, + blks []blocks.Block) { + log.Debugf("beginning processing responses for peer %s", p) - requestIds := make([]int, 0, len(responses)) + requestIds := make([]string, 0, len(responses)) for _, r := range responses { - requestIds = append(requestIds, int(r.RequestID())) + requestIds = append(requestIds, r.RequestID().String()) } ctx, span := otel.Tracer("graphsync").Start(rm.ctx, "processResponses", trace.WithAttributes( attribute.String("peerID", p.Pretty()), - attribute.IntSlice("requestIDs", requestIds), + attribute.StringSlice("requestIDs", requestIds), + attribute.Int("blockCount", len(blks)), )) defer span.End() filteredResponses := rm.processExtensions(responses, p) filteredResponses = rm.filterResponsesForPeer(filteredResponses, p) + blkMap := make(map[cid.Cid][]byte, len(blks)) + for _, blk := range blks { + blkMap[blk.Cid()] = blk.RawData() + } + for _, response := range filteredResponses { + reconciledLoader := rm.inProgressRequestStatuses[response.RequestID()].reconciledLoader + if reconciledLoader != nil { + reconciledLoader.IngestResponse(response.Metadata(), trace.LinkFromContext(ctx), blkMap) + } + } rm.updateLastResponses(filteredResponses) - responseMetadata := metadataForResponses(filteredResponses) - rm.asyncLoader.ProcessResponse(ctx, responseMetadata, blks) rm.processTerminations(filteredResponses) log.Debugf("end processing responses for peer %s", p) } @@ -312,7 +328,7 @@ func (rm *RequestManager) updateLastResponses(responses []gsmsg.GraphSyncRespons func (rm *RequestManager) processExtensionsForResponse(p peer.ID, response gsmsg.GraphSyncResponse) bool { result := rm.responseHooks.ProcessResponseHooks(p, response) if len(result.Extensions) > 0 { - updateRequest := gsmsg.UpdateRequest(response.RequestID(), result.Extensions...) + updateRequest := gsmsg.NewUpdateRequest(response.RequestID(), result.Extensions...) rm.SendRequest(p, updateRequest) } if result.Err != nil { @@ -320,7 +336,7 @@ func (rm *RequestManager) processExtensionsForResponse(p peer.ID, response gsmsg if !ok { return false } - rm.SendRequest(requestStatus.p, gsmsg.CancelRequest(response.RequestID())) + rm.SendRequest(requestStatus.p, gsmsg.NewCancelRequest(response.RequestID())) rm.cancelOnError(response.RequestID(), requestStatus, result.Err) return false } @@ -333,30 +349,33 @@ func (rm *RequestManager) processTerminations(responses []gsmsg.GraphSyncRespons if response.Status().IsFailure() { rm.cancelOnError(response.RequestID(), rm.inProgressRequestStatuses[response.RequestID()], response.Status().AsError()) } - rm.asyncLoader.CompleteResponsesFor(response.RequestID()) + ipr, ok := rm.inProgressRequestStatuses[response.RequestID()] + if ok && ipr.reconciledLoader != nil { + ipr.reconciledLoader.SetRemoteOnline(false) + } } } } -func (rm *RequestManager) validateRequest(requestID graphsync.RequestID, p peer.ID, root ipld.Link, selectorSpec ipld.Node, extensions []graphsync.ExtensionData) (gsmsg.GraphSyncRequest, hooks.RequestResult, error) { +func (rm *RequestManager) validateRequest(requestID graphsync.RequestID, p peer.ID, root ipld.Link, selectorSpec ipld.Node, extensions []graphsync.ExtensionData) (gsmsg.GraphSyncRequest, hooks.RequestResult, *linking.LinkSystem, error) { _, err := ipldutil.EncodeNode(selectorSpec) if err != nil { - return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, err + return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, nil, err } _, err = selector.ParseSelector(selectorSpec) if err != nil { - return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, err + return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, nil, err } asCidLink, ok := root.(cidlink.Link) if !ok { - return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, fmt.Errorf("request failed: link has no cid") + return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, nil, fmt.Errorf("request failed: link has no cid") } request := gsmsg.NewRequest(requestID, asCidLink.Cid, selectorSpec, defaultPriority, extensions...) hooksResult := rm.requestHooks.ProcessRequestHooks(p, request) if hooksResult.PersistenceOption != "" { dedupData, err := dedupkey.EncodeDedupKey(hooksResult.PersistenceOption) if err != nil { - return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, err + return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, nil, err } request = request.ReplaceExtensions([]graphsync.ExtensionData{ { @@ -365,11 +384,15 @@ func (rm *RequestManager) validateRequest(requestID graphsync.RequestID, p peer. }, }) } - err = rm.asyncLoader.StartRequest(requestID, hooksResult.PersistenceOption) - if err != nil { - return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, err + lsys := rm.linkSystem + if hooksResult.PersistenceOption != "" { + var has bool + lsys, has = rm.persistenceOptions.GetLinkSystem(hooksResult.PersistenceOption) + if !has { + return gsmsg.GraphSyncRequest{}, hooks.RequestResult{}, nil, errors.New("unknown persistence option") + } } - return request, hooksResult, nil + return request, hooksResult, &lsys, nil } func (rm *RequestManager) unpause(id graphsync.RequestID, extensions []graphsync.ExtensionData) error { @@ -401,6 +424,16 @@ func (rm *RequestManager) pause(id graphsync.RequestID) error { return nil } +func (rm *RequestManager) update(id graphsync.RequestID, extensions []graphsync.ExtensionData) error { + inProgressRequestStatus, ok := rm.inProgressRequestStatuses[id] + if !ok { + return graphsync.RequestNotFoundErr{} + } + updateRequest := gsmsg.NewUpdateRequest(id, extensions...) + rm.SendRequest(inProgressRequestStatus.p, updateRequest) + return nil +} + func (rm *RequestManager) peerStats(p peer.ID) peerstate.PeerState { var peerState peerstate.PeerState rm.requestQueue.WithPeerTopics(p, func(peerTopics *peertracker.PeerTrackerTopics) { diff --git a/requestmanager/testloader/asyncloader.go b/requestmanager/testloader/asyncloader.go deleted file mode 100644 index 0c52877c..00000000 --- a/requestmanager/testloader/asyncloader.go +++ /dev/null @@ -1,164 +0,0 @@ -package testloader - -import ( - "context" - "sync" - "testing" - - blocks "github.com/ipfs/go-block-format" - "github.com/ipld/go-ipld-prime" - cidlink "github.com/ipld/go-ipld-prime/linking/cid" - peer "github.com/libp2p/go-libp2p-core/peer" - "github.com/stretchr/testify/require" - - "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" - "github.com/ipfs/go-graphsync/requestmanager/types" - "github.com/ipfs/go-graphsync/testutil" -) - -type requestKey struct { - p peer.ID - requestID graphsync.RequestID - link ipld.Link -} - -type storeKey struct { - requestID graphsync.RequestID - storeName string -} - -// FakeAsyncLoader simultates the requestmanager.AsyncLoader interface -// with mocked responses and can also be used to simulate a -// executor.AsycLoadFn -- all responses are stubbed and no actual processing is -// done -type FakeAsyncLoader struct { - responseChannelsLk sync.RWMutex - responseChannels map[requestKey]chan types.AsyncLoadResult - responses chan map[graphsync.RequestID]metadata.Metadata - blks chan []blocks.Block - storesRequestedLk sync.RWMutex - storesRequested map[storeKey]struct{} - cb func(graphsync.RequestID, ipld.Link, <-chan types.AsyncLoadResult) -} - -// NewFakeAsyncLoader returns a new FakeAsyncLoader instance -func NewFakeAsyncLoader() *FakeAsyncLoader { - return &FakeAsyncLoader{ - responseChannels: make(map[requestKey]chan types.AsyncLoadResult), - responses: make(chan map[graphsync.RequestID]metadata.Metadata, 10), - blks: make(chan []blocks.Block, 10), - storesRequested: make(map[storeKey]struct{}), - } -} - -// StartRequest just requests what store was requested for a given requestID -func (fal *FakeAsyncLoader) StartRequest(requestID graphsync.RequestID, name string) error { - fal.storesRequestedLk.Lock() - fal.storesRequested[storeKey{requestID, name}] = struct{}{} - fal.storesRequestedLk.Unlock() - return nil -} - -// ProcessResponse just records values passed to verify expectations later -func (fal *FakeAsyncLoader) ProcessResponse(_ context.Context, responses map[graphsync.RequestID]metadata.Metadata, - blks []blocks.Block) { - fal.responses <- responses - fal.blks <- blks -} - -// VerifyLastProcessedBlocks verifies the blocks passed to the last call to ProcessResponse -// match the expected ones -func (fal *FakeAsyncLoader) VerifyLastProcessedBlocks(ctx context.Context, t *testing.T, expectedBlocks []blocks.Block) { - t.Helper() - var processedBlocks []blocks.Block - testutil.AssertReceive(ctx, t, fal.blks, &processedBlocks, "did not process blocks") - require.Equal(t, expectedBlocks, processedBlocks, "did not process correct blocks") -} - -// VerifyLastProcessedResponses verifies the responses passed to the last call to ProcessResponse -// match the expected ones -func (fal *FakeAsyncLoader) VerifyLastProcessedResponses(ctx context.Context, t *testing.T, - expectedResponses map[graphsync.RequestID]metadata.Metadata) { - t.Helper() - var responses map[graphsync.RequestID]metadata.Metadata - testutil.AssertReceive(ctx, t, fal.responses, &responses, "did not process responses") - require.Equal(t, expectedResponses, responses, "did not process correct responses") -} - -// VerifyNoRemainingData verifies no outstanding response channels are open for the given -// RequestID (CleanupRequest was called last) -func (fal *FakeAsyncLoader) VerifyNoRemainingData(t *testing.T, requestID graphsync.RequestID) { - t.Helper() - fal.responseChannelsLk.RLock() - for key := range fal.responseChannels { - require.NotEqual(t, key.requestID, requestID, "did not clean up request properly") - } - fal.responseChannelsLk.RUnlock() -} - -// VerifyStoreUsed verifies the given store was used for the given request -func (fal *FakeAsyncLoader) VerifyStoreUsed(t *testing.T, requestID graphsync.RequestID, storeName string) { - t.Helper() - fal.storesRequestedLk.RLock() - _, ok := fal.storesRequested[storeKey{requestID, storeName}] - require.True(t, ok, "request should load from correct store") - fal.storesRequestedLk.RUnlock() -} - -func (fal *FakeAsyncLoader) asyncLoad(p peer.ID, requestID graphsync.RequestID, link ipld.Link, linkContext ipld.LinkContext) chan types.AsyncLoadResult { - fal.responseChannelsLk.Lock() - responseChannel, ok := fal.responseChannels[requestKey{p, requestID, link}] - if !ok { - responseChannel = make(chan types.AsyncLoadResult, 1) - fal.responseChannels[requestKey{p, requestID, link}] = responseChannel - } - fal.responseChannelsLk.Unlock() - return responseChannel -} - -// OnAsyncLoad allows you to listen for load requests to the loader and perform other actions or tests -func (fal *FakeAsyncLoader) OnAsyncLoad(cb func(graphsync.RequestID, ipld.Link, <-chan types.AsyncLoadResult)) { - fal.cb = cb -} - -// AsyncLoad simulates an asynchronous load with responses stubbed by ResponseOn & SuccessResponseOn -func (fal *FakeAsyncLoader) AsyncLoad(p peer.ID, requestID graphsync.RequestID, link ipld.Link, linkContext ipld.LinkContext) <-chan types.AsyncLoadResult { - res := fal.asyncLoad(p, requestID, link, linkContext) - if fal.cb != nil { - fal.cb(requestID, link, res) - } - return res -} - -// CompleteResponsesFor in the case of the test loader does nothing -func (fal *FakeAsyncLoader) CompleteResponsesFor(requestID graphsync.RequestID) {} - -// CleanupRequest simulates the effect of cleaning up the request by removing any response channels -// for the request -func (fal *FakeAsyncLoader) CleanupRequest(p peer.ID, requestID graphsync.RequestID) { - fal.responseChannelsLk.Lock() - for key := range fal.responseChannels { - if key.requestID == requestID { - delete(fal.responseChannels, key) - } - } - fal.responseChannelsLk.Unlock() -} - -// ResponseOn sets the value returned when the given link is loaded for the given request. Because it's an -// "asynchronous" load, this can be called AFTER the attempt to load this link -- and the client will only get -// the response at that point -func (fal *FakeAsyncLoader) ResponseOn(p peer.ID, requestID graphsync.RequestID, link ipld.Link, result types.AsyncLoadResult) { - responseChannel := fal.asyncLoad(p, requestID, link, ipld.LinkContext{}) - responseChannel <- result - close(responseChannel) -} - -// SuccessResponseOn is convenience function for setting several asynchronous responses at once as all successes -// and returning the given blocks -func (fal *FakeAsyncLoader) SuccessResponseOn(p peer.ID, requestID graphsync.RequestID, blks []blocks.Block) { - for _, block := range blks { - fal.ResponseOn(p, requestID, cidlink.Link{Cid: block.Cid()}, types.AsyncLoadResult{Data: block.RawData(), Local: false, Err: nil}) - } -} diff --git a/requestmanager/utils.go b/requestmanager/utils.go deleted file mode 100644 index dc2a3102..00000000 --- a/requestmanager/utils.go +++ /dev/null @@ -1,32 +0,0 @@ -package requestmanager - -import ( - "github.com/ipfs/go-peertaskqueue/peertask" - - "github.com/ipfs/go-graphsync" - gsmsg "github.com/ipfs/go-graphsync/message" - "github.com/ipfs/go-graphsync/metadata" -) - -func metadataForResponses(responses []gsmsg.GraphSyncResponse) map[graphsync.RequestID]metadata.Metadata { - responseMetadata := make(map[graphsync.RequestID]metadata.Metadata, len(responses)) - for _, response := range responses { - mdRaw, found := response.Extension(graphsync.ExtensionMetadata) - if !found { - log.Warnf("Unable to decode metadata in response for request id: %d", response.RequestID()) - continue - } - md, err := metadata.DecodeMetadata(mdRaw) - if err != nil { - log.Warnf("Unable to decode metadata in response for request id: %d", response.RequestID()) - continue - } - responseMetadata[response.RequestID()] = md - } - return responseMetadata -} - -// RequestIDFromTaskTopic extracts a request ID from a given peer task topic -func RequestIDFromTaskTopic(topic peertask.Topic) graphsync.RequestID { - return topic.(graphsync.RequestID) -} diff --git a/responsemanager/client.go b/responsemanager/client.go index 08e4bfcd..9cbebc3c 100644 --- a/responsemanager/client.go +++ b/responsemanager/client.go @@ -33,6 +33,7 @@ type inProgressResponseStatus struct { ctx context.Context span trace.Span cancelFn func() + peer peer.ID request gsmsg.GraphSyncRequest loader ipld.BlockReadOpener traverser ipldutil.Traverser @@ -43,11 +44,6 @@ type inProgressResponseStatus struct { responseStream responseassembler.ResponseStream } -type responseKey struct { - p peer.ID - requestID graphsync.RequestID -} - // RequestHooks is an interface for processing request hooks type RequestHooks interface { ProcessRequestHooks(p peer.ID, request graphsync.RequestData) hooks.RequestResult @@ -107,7 +103,7 @@ type ResponseManager struct { blockSentListeners BlockSentListeners networkErrorListeners NetworkErrorListeners messages chan responseManagerMessage - inProgressResponses map[responseKey]*inProgressResponseStatus + inProgressResponses map[graphsync.RequestID]*inProgressResponseStatus connManager network.ConnManager // maximum number of links to traverse per request. A value of zero = infinity, or no limit maxLinksPerRequest uint64 @@ -144,7 +140,7 @@ func New(ctx context.Context, blockSentListeners: blockSentListeners, networkErrorListeners: networkErrorListeners, messages: messages, - inProgressResponses: make(map[responseKey]*inProgressResponseStatus), + inProgressResponses: make(map[graphsync.RequestID]*inProgressResponseStatus), connManager: connManager, maxLinksPerRequest: maxLinksPerRequest, responseQueue: responseQueue, @@ -158,9 +154,9 @@ func (rm *ResponseManager) ProcessRequests(ctx context.Context, p peer.ID, reque } // UnpauseResponse unpauses a response that was previously paused -func (rm *ResponseManager) UnpauseResponse(p peer.ID, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { +func (rm *ResponseManager) UnpauseResponse(ctx context.Context, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { response := make(chan error, 1) - rm.send(&unpauseRequestMessage{p, requestID, response, extensions}, nil) + rm.send(&unpauseRequestMessage{requestID, response, extensions}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") @@ -170,9 +166,9 @@ func (rm *ResponseManager) UnpauseResponse(p peer.ID, requestID graphsync.Reques } // PauseResponse pauses an in progress response (may take 1 or more blocks to process) -func (rm *ResponseManager) PauseResponse(p peer.ID, requestID graphsync.RequestID) error { +func (rm *ResponseManager) PauseResponse(ctx context.Context, requestID graphsync.RequestID) error { response := make(chan error, 1) - rm.send(&pauseRequestMessage{p, requestID, response}, nil) + rm.send(&pauseRequestMessage{requestID, response}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") @@ -182,9 +178,21 @@ func (rm *ResponseManager) PauseResponse(p peer.ID, requestID graphsync.RequestI } // CancelResponse cancels an in progress response -func (rm *ResponseManager) CancelResponse(p peer.ID, requestID graphsync.RequestID) error { +func (rm *ResponseManager) CancelResponse(ctx context.Context, requestID graphsync.RequestID) error { + response := make(chan error, 1) + rm.send(&errorRequestMessage{requestID, queryexecutor.ErrCancelledByCommand, response}, ctx.Done()) + select { + case <-rm.ctx.Done(): + return errors.New("context cancelled") + case err := <-response: + return err + } +} + +// UpdateRequest updates an in progress response +func (rm *ResponseManager) UpdateResponse(ctx context.Context, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { response := make(chan error, 1) - rm.send(&errorRequestMessage{p, requestID, queryexecutor.ErrCancelledByCommand, response}, nil) + rm.send(&updateRequestMessage{requestID, extensions, response}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") @@ -204,19 +212,19 @@ func (rm *ResponseManager) synchronize() { } // StartTask starts the given task from the peer task queue -func (rm *ResponseManager) StartTask(task *peertask.Task, responseTaskChan chan<- queryexecutor.ResponseTask) { - rm.send(&startTaskRequest{task, responseTaskChan}, nil) +func (rm *ResponseManager) StartTask(task *peertask.Task, p peer.ID, responseTaskChan chan<- queryexecutor.ResponseTask) { + rm.send(&startTaskRequest{task, p, responseTaskChan}, nil) } // GetUpdates is called to read pending updates for a task and clear them -func (rm *ResponseManager) GetUpdates(p peer.ID, requestID graphsync.RequestID, updatesChan chan<- []gsmsg.GraphSyncRequest) { - rm.send(&responseUpdateRequest{responseKey{p, requestID}, updatesChan}, nil) +func (rm *ResponseManager) GetUpdates(requestID graphsync.RequestID, updatesChan chan<- []gsmsg.GraphSyncRequest) { + rm.send(&responseUpdateRequest{requestID, updatesChan}, nil) } // FinishTask marks a task from the task queue as done -func (rm *ResponseManager) FinishTask(task *peertask.Task, err error) { +func (rm *ResponseManager) FinishTask(task *peertask.Task, p peer.ID, err error) { done := make(chan struct{}, 1) - rm.send(&finishTaskRequest{task, err, done}, nil) + rm.send(&finishTaskRequest{task, p, err, done}, nil) select { case <-rm.ctx.Done(): case <-done: @@ -224,9 +232,9 @@ func (rm *ResponseManager) FinishTask(task *peertask.Task, err error) { } // CloseWithNetworkError closes a request due to a network error -func (rm *ResponseManager) CloseWithNetworkError(p peer.ID, requestID graphsync.RequestID) { +func (rm *ResponseManager) CloseWithNetworkError(requestID graphsync.RequestID) { done := make(chan error, 1) - rm.send(&errorRequestMessage{p, requestID, queryexecutor.ErrNetworkError, done}, nil) + rm.send(&errorRequestMessage{requestID, queryexecutor.ErrNetworkError, done}, nil) select { case <-rm.ctx.Done(): case <-done: @@ -234,9 +242,9 @@ func (rm *ResponseManager) CloseWithNetworkError(p peer.ID, requestID graphsync. } // TerminateRequest indicates a request has finished sending data and should no longer be tracked -func (rm *ResponseManager) TerminateRequest(p peer.ID, requestID graphsync.RequestID) { +func (rm *ResponseManager) TerminateRequest(requestID graphsync.RequestID) { done := make(chan struct{}, 1) - rm.send(&terminateRequestMessage{p, requestID, done}, nil) + rm.send(&terminateRequestMessage{requestID, done}, nil) select { case <-rm.ctx.Done(): case <-done: diff --git a/responsemanager/hooks/hooks_test.go b/responsemanager/hooks/hooks_test.go index e8a56903..9fb2c6f0 100644 --- a/responsemanager/hooks/hooks_test.go +++ b/responsemanager/hooks/hooks_test.go @@ -3,7 +3,6 @@ package hooks_test import ( "errors" "io" - "math/rand" "testing" "github.com/ipld/go-ipld-prime" @@ -41,20 +40,20 @@ func TestRequestHookProcessing(t *testing.T) { "chainstore": fakeSystem, }, } - extensionData := testutil.RandomBytes(100) + extensionData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName := graphsync.ExtensionName("AppleSauce/McGee") extension := graphsync.ExtensionData{ Name: extensionName, Data: extensionData, } - extensionResponseData := testutil.RandomBytes(100) + extensionResponseData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionResponse := graphsync.ExtensionData{ Name: extensionName, Data: extensionResponseData, } root := testutil.GenerateCids(1)[0] - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) request := gsmsg.NewRequest(requestID, root, ssb.Matcher().Node(), graphsync.Priority(0), extension) p := testutil.GeneratePeers(1)[0] @@ -219,20 +218,20 @@ func TestRequestHookProcessing(t *testing.T) { } func TestBlockHookProcessing(t *testing.T) { - extensionData := testutil.RandomBytes(100) + extensionData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName := graphsync.ExtensionName("AppleSauce/McGee") extension := graphsync.ExtensionData{ Name: extensionName, Data: extensionData, } - extensionResponseData := testutil.RandomBytes(100) + extensionResponseData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionResponse := graphsync.ExtensionData{ Name: extensionName, Data: extensionResponseData, } root := testutil.GenerateCids(1)[0] - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) request := gsmsg.NewRequest(requestID, root, ssb.Matcher().Node(), graphsync.Priority(0), extension) p := testutil.GeneratePeers(1)[0] @@ -297,28 +296,28 @@ func TestBlockHookProcessing(t *testing.T) { } func TestUpdateHookProcessing(t *testing.T) { - extensionData := testutil.RandomBytes(100) + extensionData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName := graphsync.ExtensionName("AppleSauce/McGee") extension := graphsync.ExtensionData{ Name: extensionName, Data: extensionData, } - extensionUpdateData := testutil.RandomBytes(100) + extensionUpdateData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionUpdate := graphsync.ExtensionData{ Name: extensionName, Data: extensionUpdateData, } - extensionResponseData := testutil.RandomBytes(100) + extensionResponseData := basicnode.NewBytes(testutil.RandomBytes(100)) extensionResponse := graphsync.ExtensionData{ Name: extensionName, Data: extensionResponseData, } root := testutil.GenerateCids(1)[0] - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) request := gsmsg.NewRequest(requestID, root, ssb.Matcher().Node(), graphsync.Priority(0), extension) - update := gsmsg.UpdateRequest(requestID, extensionUpdate) + update := gsmsg.NewUpdateRequest(requestID, extensionUpdate) p := testutil.GeneratePeers(1)[0] testCases := map[string]struct { configure func(t *testing.T, updateHooks *hooks.RequestUpdatedHooks) diff --git a/responsemanager/messages.go b/responsemanager/messages.go index 917d70c1..416d9eb0 100644 --- a/responsemanager/messages.go +++ b/responsemanager/messages.go @@ -19,14 +19,27 @@ func (prm *processRequestsMessage) handle(rm *ResponseManager) { rm.processRequests(prm.p, prm.requests) } +type updateRequestMessage struct { + requestID graphsync.RequestID + extensions []graphsync.ExtensionData + response chan error +} + +func (urm *updateRequestMessage) handle(rm *ResponseManager) { + err := rm.updateRequest(urm.requestID, urm.extensions) + select { + case <-rm.ctx.Done(): + case urm.response <- err: + } +} + type pauseRequestMessage struct { - p peer.ID requestID graphsync.RequestID response chan error } func (prm *pauseRequestMessage) handle(rm *ResponseManager) { - err := rm.pauseRequest(prm.p, prm.requestID) + err := rm.pauseRequest(prm.requestID) select { case <-rm.ctx.Done(): case prm.response <- err: @@ -34,14 +47,13 @@ func (prm *pauseRequestMessage) handle(rm *ResponseManager) { } type errorRequestMessage struct { - p peer.ID requestID graphsync.RequestID err error response chan error } func (erm *errorRequestMessage) handle(rm *ResponseManager) { - err := rm.abortRequest(rm.ctx, erm.p, erm.requestID, erm.err) + err := rm.abortRequest(rm.ctx, erm.requestID, erm.err) select { case <-rm.ctx.Done(): case erm.response <- err: @@ -60,14 +72,13 @@ func (sm *synchronizeMessage) handle(rm *ResponseManager) { } type unpauseRequestMessage struct { - p peer.ID requestID graphsync.RequestID response chan error extensions []graphsync.ExtensionData } func (urm *unpauseRequestMessage) handle(rm *ResponseManager) { - err := rm.unpauseRequest(urm.p, urm.requestID, urm.extensions...) + err := rm.unpauseRequest(urm.requestID, urm.extensions...) select { case <-rm.ctx.Done(): case urm.response <- err: @@ -75,12 +86,12 @@ func (urm *unpauseRequestMessage) handle(rm *ResponseManager) { } type responseUpdateRequest struct { - key responseKey + requestID graphsync.RequestID updateChan chan<- []gsmsg.GraphSyncRequest } func (rur *responseUpdateRequest) handle(rm *ResponseManager) { - updates := rm.getUpdates(rur.key) + updates := rm.getUpdates(rur.requestID) select { case <-rm.ctx.Done(): case rur.updateChan <- updates: @@ -89,12 +100,13 @@ func (rur *responseUpdateRequest) handle(rm *ResponseManager) { type finishTaskRequest struct { task *peertask.Task + p peer.ID err error done chan struct{} } func (ftr *finishTaskRequest) handle(rm *ResponseManager) { - rm.finishTask(ftr.task, ftr.err) + rm.finishTask(ftr.task, ftr.p, ftr.err) select { case <-rm.ctx.Done(): case ftr.done <- struct{}{}: @@ -103,11 +115,12 @@ func (ftr *finishTaskRequest) handle(rm *ResponseManager) { type startTaskRequest struct { task *peertask.Task + p peer.ID taskDataChan chan<- queryexecutor.ResponseTask } func (str *startTaskRequest) handle(rm *ResponseManager) { - taskData := rm.startTask(str.task) + taskData := rm.startTask(str.task, str.p) select { case <-rm.ctx.Done(): @@ -129,13 +142,12 @@ func (psm *peerStateMessage) handle(rm *ResponseManager) { } type terminateRequestMessage struct { - p peer.ID requestID graphsync.RequestID done chan<- struct{} } func (trm *terminateRequestMessage) handle(rm *ResponseManager) { - rm.terminateRequest(responseKey{trm.p, trm.requestID}) + rm.terminateRequest(trm.requestID) select { case <-rm.ctx.Done(): case trm.done <- struct{}{}: diff --git a/responsemanager/queryexecutor/queryexecutor.go b/responsemanager/queryexecutor/queryexecutor.go index f43e19f9..563168ad 100644 --- a/responsemanager/queryexecutor/queryexecutor.go +++ b/responsemanager/queryexecutor/queryexecutor.go @@ -87,7 +87,7 @@ func (qe *QueryExecutor) ExecuteTask(_ context.Context, pid peer.ID, task *peert // StartTask lets us block until this task is at the top of the execution stack responseTaskChan := make(chan ResponseTask) var rt ResponseTask - qe.manager.StartTask(task, responseTaskChan) + qe.manager.StartTask(task, pid, responseTaskChan) select { case rt = <-responseTaskChan: case <-qe.ctx.Done(): @@ -109,7 +109,7 @@ func (qe *QueryExecutor) ExecuteTask(_ context.Context, pid peer.ID, task *peert span.SetStatus(codes.Error, err.Error()) } } - qe.manager.FinishTask(task, err) + qe.manager.FinishTask(task, pid, err) log.Debugw("finishing response execution", "id", rt.Request.ID(), "peer", pid.String(), "root_cid", rt.Request.Root().String()) return false } @@ -159,7 +159,7 @@ func (qe *QueryExecutor) checkForUpdates( return err case <-taskData.Signals.UpdateSignal: updateChan := make(chan []gsmsg.GraphSyncRequest) - qe.manager.GetUpdates(p, taskData.Request.ID(), updateChan) + qe.manager.GetUpdates(taskData.Request.ID(), updateChan) select { case updates := <-updateChan: for _, update := range updates { @@ -279,9 +279,9 @@ func (qe *QueryExecutor) sendResponse(ctx context.Context, p peer.ID, taskData R // Manager providers an interface to the response manager type Manager interface { - StartTask(task *peertask.Task, responseTaskChan chan<- ResponseTask) - GetUpdates(p peer.ID, requestID graphsync.RequestID, updatesChan chan<- []gsmsg.GraphSyncRequest) - FinishTask(task *peertask.Task, err error) + StartTask(task *peertask.Task, p peer.ID, responseTaskChan chan<- ResponseTask) + GetUpdates(requestID graphsync.RequestID, updatesChan chan<- []gsmsg.GraphSyncRequest) + FinishTask(task *peertask.Task, p peer.ID, err error) } // BlockHooks is an interface for processing block hooks diff --git a/responsemanager/queryexecutor/queryexecutor_test.go b/responsemanager/queryexecutor/queryexecutor_test.go index 3711b5b9..b3b33d4f 100644 --- a/responsemanager/queryexecutor/queryexecutor_test.go +++ b/responsemanager/queryexecutor/queryexecutor_test.go @@ -247,7 +247,7 @@ type testData struct { responseBuilder *fauxResponseBuilder blockHooks *hooks.OutgoingBlockHooks updateHooks *hooks.RequestUpdatedHooks - extensionData []byte + extensionData datamodel.Node extensionName graphsync.ExtensionName extension graphsync.ExtensionData requestID graphsync.RequestID @@ -268,19 +268,19 @@ func newTestData(t *testing.T, blockCount int, expectedTraverse int) (*testData, td := &testData{} td.t = t td.ctx, td.cancel = context.WithTimeout(ctx, 10*time.Second) + td.peer = testutil.GeneratePeers(1)[0] td.blockStore = make(map[ipld.Link][]byte) td.persistence = testutil.NewTestStore(td.blockStore) td.task = &peertask.Task{} - td.manager = &fauxManager{ctx: ctx, t: t, expectedStartTask: td.task} + td.manager = &fauxManager{ctx: ctx, t: t, expectedStartTask: td.task, expectedPeer: td.peer} td.blockHooks = hooks.NewBlockHooks() td.updateHooks = hooks.NewUpdateHooks() - td.requestID = graphsync.RequestID(rand.Int31()) + td.requestID = graphsync.NewRequestID() td.requestCid, _ = cid.Decode("bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi") td.requestSelector = basicnode.NewInt(rand.Int63()) - td.extensionData = testutil.RandomBytes(100) + td.extensionData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionName = graphsync.ExtensionName("AppleSauce/McGee") td.responseCode = graphsync.ResponseStatusCode(101) - td.peer = testutil.GeneratePeers(1)[0] td.extension = graphsync.ExtensionData{ Name: td.extensionName, @@ -367,10 +367,12 @@ type fauxManager struct { t *testing.T responseTask ResponseTask expectedStartTask *peertask.Task + expectedPeer peer.ID } -func (fm *fauxManager) StartTask(task *peertask.Task, responseTaskChan chan<- ResponseTask) { +func (fm *fauxManager) StartTask(task *peertask.Task, p peer.ID, responseTaskChan chan<- ResponseTask) { require.Same(fm.t, fm.expectedStartTask, task) + require.Equal(fm.t, fm.expectedPeer, p) go func() { select { case <-fm.ctx.Done(): @@ -379,10 +381,11 @@ func (fm *fauxManager) StartTask(task *peertask.Task, responseTaskChan chan<- Re }() } -func (fm *fauxManager) GetUpdates(p peer.ID, requestID graphsync.RequestID, updatesChan chan<- []gsmsg.GraphSyncRequest) { +func (fm *fauxManager) GetUpdates(requestID graphsync.RequestID, updatesChan chan<- []gsmsg.GraphSyncRequest) { } -func (fm *fauxManager) FinishTask(task *peertask.Task, err error) { +func (fm *fauxManager) FinishTask(task *peertask.Task, p peer.ID, err error) { + require.Equal(fm.t, fm.expectedPeer, p) } type fauxResponseStream struct { @@ -424,6 +427,9 @@ func (rb fauxResponseBuilder) SendResponse(link ipld.Link, data []byte) graphsyn func (rb fauxResponseBuilder) SendExtensionData(ed graphsync.ExtensionData) { } +func (rb fauxResponseBuilder) SendUpdates(ed []graphsync.ExtensionData) { +} + func (rb fauxResponseBuilder) FinishRequest() graphsync.ResponseStatusCode { return rb.finishRequest } diff --git a/responsemanager/responseassembler/responseBuilder.go b/responsemanager/responseassembler/responseBuilder.go index e395a871..4973c16a 100644 --- a/responsemanager/responseassembler/responseBuilder.go +++ b/responsemanager/responseassembler/responseBuilder.go @@ -6,6 +6,7 @@ import ( blocks "github.com/ipfs/go-block-format" logging "github.com/ipfs/go-log/v2" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipfs/go-graphsync" @@ -50,6 +51,14 @@ func (rb *responseBuilder) PauseRequest() { rb.operations = append(rb.operations, statusOperation{rb.requestID, graphsync.RequestPaused}) } +// SendUpdates sets up a PartialResponse with just the extension data provided +func (rb *responseBuilder) SendUpdates(extensions []graphsync.ExtensionData) { + for _, extension := range extensions { + rb.SendExtensionData(extension) + } + rb.operations = append(rb.operations, statusOperation{rb.requestID, graphsync.PartialResponse}) +} + func (rb *responseBuilder) Context() context.Context { return rb.ctx } @@ -102,7 +111,13 @@ func (eo extensionOperation) build(builder *messagequeue.Builder) { } func (eo extensionOperation) size() uint64 { - return uint64(len(eo.extension.Data)) + if eo.extension.Data == nil { + return 0 + } + // any erorr produced by this call will be picked up during actual encode, so + // we can defer handling till then and let it be zero for now + len, _ := dagcbor.EncodedLength(eo.extension.Data) + return uint64(len) } type blockOperation struct { @@ -122,7 +137,11 @@ func (bo blockOperation) build(builder *messagequeue.Builder) { } builder.AddBlock(block) } - builder.AddLink(bo.requestID, bo.link, bo.data != nil) + action := graphsync.LinkActionPresent + if bo.data == nil { + action = graphsync.LinkActionMissing + } + builder.AddLink(bo.requestID, bo.link, action) builder.AddBlockData(bo.requestID, bo.Block()) } diff --git a/responsemanager/responseassembler/responseassembler.go b/responsemanager/responseassembler/responseassembler.go index befa10a7..3d5b38c5 100644 --- a/responsemanager/responseassembler/responseassembler.go +++ b/responsemanager/responseassembler/responseassembler.go @@ -37,6 +37,9 @@ type ResponseBuilder interface { // SendExtensionData adds extension data to the transaction. SendExtensionData(graphsync.ExtensionData) + // SendUpdates sets up a PartialResponse with just the extension data provided + SendUpdates([]graphsync.ExtensionData) + // FinishRequest completes the response to a request. FinishRequest() graphsync.ResponseStatusCode diff --git a/responsemanager/responseassembler/responseassembler_test.go b/responsemanager/responseassembler/responseassembler_test.go index b9f26c6b..96845895 100644 --- a/responsemanager/responseassembler/responseassembler_test.go +++ b/responsemanager/responseassembler/responseassembler_test.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "io" - "math/rand" "testing" "time" blocks "github.com/ipfs/go-block-format" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" + "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/libp2p/go-libp2p-core/peer" "github.com/stretchr/testify/require" @@ -27,9 +27,9 @@ func TestResponseAssemblerSendsResponses(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) - requestID3 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() + requestID3 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) @@ -139,7 +139,7 @@ func TestResponseAssemblerCloseStream(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) for _, block := range blks { @@ -175,7 +175,7 @@ func TestResponseAssemblerSendsExtensionData(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) for _, block := range blks { @@ -194,13 +194,13 @@ func TestResponseAssemblerSendsExtensionData(t *testing.T) { fph.AssertBlocks(blks[0]) fph.AssertResponses(expectedResponses{requestID1: graphsync.PartialResponse}) - extensionData1 := testutil.RandomBytes(100) + extensionData1 := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName1 := graphsync.ExtensionName("AppleSauce/McGee") extension1 := graphsync.ExtensionData{ Name: extensionName1, Data: extensionData1, } - extensionData2 := testutil.RandomBytes(100) + extensionData2 := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName2 := graphsync.ExtensionName("HappyLand/Happenstance") extension2 := graphsync.ExtensionData{ Name: extensionName2, @@ -222,7 +222,7 @@ func TestResponseAssemblerSendsResponsesInTransaction(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) for _, block := range blks { @@ -261,8 +261,8 @@ func TestResponseAssemblerIgnoreBlocks(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) for _, block := range blks { @@ -336,8 +336,8 @@ func TestResponseAssemblerSkipFirstBlocks(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) for _, block := range blks { @@ -427,9 +427,9 @@ func TestResponseAssemblerDupKeys(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() p := testutil.GeneratePeers(1)[0] - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) - requestID3 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() + requestID3 := graphsync.NewRequestID() blks := testutil.GenerateBlocksOfSize(5, 100) links := make([]ipld.Link, 0, len(blks)) for _, block := range blks { diff --git a/responsemanager/responsemanager_test.go b/responsemanager/responsemanager_test.go index 443e2408..b7de9b5f 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -13,6 +13,7 @@ import ( "github.com/ipfs/go-peertaskqueue/peertask" "github.com/ipfs/go-peertaskqueue/peertracker" ipld "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/libp2p/go-libp2p-core/peer" @@ -28,8 +29,8 @@ import ( gsmsg "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/messagequeue" "github.com/ipfs/go-graphsync/notifications" + "github.com/ipfs/go-graphsync/persistenceoptions" "github.com/ipfs/go-graphsync/responsemanager/hooks" - "github.com/ipfs/go-graphsync/responsemanager/persistenceoptions" "github.com/ipfs/go-graphsync/responsemanager/queryexecutor" "github.com/ipfs/go-graphsync/responsemanager/responseassembler" "github.com/ipfs/go-graphsync/selectorvalidator" @@ -124,7 +125,7 @@ func TestCancellationQueryInProgress(t *testing.T) { // send a cancellation cancelRequests := []gsmsg.GraphSyncRequest{ - gsmsg.CancelRequest(td.requestID), + gsmsg.NewCancelRequest(td.requestID), } responseManager.ProcessRequests(td.ctx, td.p, cancelRequests) responseManager.synchronize() @@ -173,7 +174,7 @@ func TestCancellationViaCommand(t *testing.T) { td.assertSendBlock() // send a cancellation - err := responseManager.CancelResponse(td.p, td.requestID) + err := responseManager.CancelResponse(td.ctx, td.requestID) require.NoError(t, err) close(waitForCancel) @@ -197,7 +198,7 @@ func TestEarlyCancellation(t *testing.T) { // send a cancellation cancelRequests := []gsmsg.GraphSyncRequest{ - gsmsg.CancelRequest(td.requestID), + gsmsg.NewCancelRequest(td.requestID), } responseManager.ProcessRequests(td.ctx, td.p, cancelRequests) @@ -217,22 +218,33 @@ func TestStats(t *testing.T) { responseManager := td.nullTaskQueueResponseManager() td.requestHooks.Register(selectorvalidator.SelectorValidator(100)) responseManager.Startup() - responseManager.ProcessRequests(td.ctx, td.p, td.requests) + + p1 := td.p + reqid1 := td.requestID + req1 := td.requests + p2 := testutil.GeneratePeers(1)[0] - responseManager.ProcessRequests(td.ctx, p2, td.requests) - peerState := responseManager.PeerState(td.p) + reqid2 := graphsync.NewRequestID() + req2 := []gsmsg.GraphSyncRequest{ + gsmsg.NewRequest(reqid2, td.blockChain.TipLink.(cidlink.Link).Cid, td.blockChain.Selector(), graphsync.Priority(0), td.extension), + } + + responseManager.ProcessRequests(td.ctx, p1, req1) + responseManager.ProcessRequests(td.ctx, p2, req2) + + peerState := responseManager.PeerState(p1) require.Len(t, peerState.RequestStates, 1) - require.Equal(t, peerState.RequestStates[td.requestID], graphsync.Queued) + require.Equal(t, peerState.RequestStates[reqid1], graphsync.Queued) require.Len(t, peerState.Pending, 1) - require.Equal(t, peerState.Pending[0], td.requestID) + require.Equal(t, peerState.Pending[0], reqid1) require.Len(t, peerState.Active, 0) // no inconsistencies require.Len(t, peerState.Diagnostics(), 0) peerState = responseManager.PeerState(p2) require.Len(t, peerState.RequestStates, 1) - require.Equal(t, peerState.RequestStates[td.requestID], graphsync.Queued) + require.Equal(t, peerState.RequestStates[reqid2], graphsync.Queued) require.Len(t, peerState.Pending, 1) - require.Equal(t, peerState.Pending[0], td.requestID) + require.Equal(t, peerState.Pending[0], reqid2) require.Len(t, peerState.Active, 0) // no inconsistencies require.Len(t, peerState.Diagnostics(), 0) @@ -252,6 +264,7 @@ func TestMissingContent(t *testing.T) { responseManager.ProcessRequests(td.ctx, td.p, td.requests) td.assertCompleteRequestWith(graphsync.RequestFailedContentNotFound) }) + t.Run("missing other block", func(t *testing.T) { td := newTestData(t) defer td.cancel() @@ -433,8 +446,7 @@ func TestValidationAndExtensions(t *testing.T) { for _, blk := range blks { set.Add(blk.Cid()) } - data, err := cidset.EncodeCidSet(set) - require.NoError(t, err) + data := cidset.EncodeCidSet(set) requests := []gsmsg.GraphSyncRequest{ gsmsg.NewRequest(td.requestID, td.blockChain.TipLink.(cidlink.Link).Cid, td.blockChain.Selector(), graphsync.Priority(0), graphsync.ExtensionData{ @@ -455,8 +467,7 @@ func TestValidationAndExtensions(t *testing.T) { td.requestHooks.Register(func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) { hookActions.ValidateRequest() }) - data, err := donotsendfirstblocks.EncodeDoNotSendFirstBlocks(4) - require.NoError(t, err) + data := donotsendfirstblocks.EncodeDoNotSendFirstBlocks(4) requests := []gsmsg.GraphSyncRequest{ gsmsg.NewRequest(td.requestID, td.blockChain.TipLink.(cidlink.Link).Cid, td.blockChain.Selector(), graphsync.Priority(0), graphsync.ExtensionData{ @@ -490,6 +501,7 @@ func TestValidationAndExtensions(t *testing.T) { td.assertCompleteRequestWith(graphsync.RequestCompletedFull) td.assertDedupKey("applesauce") }) + t.Run("test pause/resume", func(t *testing.T) { td := newTestData(t) defer td.cancel() @@ -503,10 +515,11 @@ func TestValidationAndExtensions(t *testing.T) { td.assertPausedRequest() td.assertRequestDoesNotCompleteWhilePaused() testutil.AssertChannelEmpty(t, td.sentResponses, "should not send more blocks") - err := responseManager.UnpauseResponse(td.p, td.requestID) + err := responseManager.UnpauseResponse(td.ctx, td.requestID) require.NoError(t, err) td.assertCompleteRequestWith(graphsync.RequestCompletedFull) }) + t.Run("test block hook processing", func(t *testing.T) { t.Run("can send extension data", func(t *testing.T) { td := newTestData(t) @@ -561,7 +574,7 @@ func TestValidationAndExtensions(t *testing.T) { td.assertRequestDoesNotCompleteWhilePaused() td.verifyNResponses(blockCount) td.assertPausedRequest() - err := responseManager.UnpauseResponse(td.p, td.requestID, td.extensionResponse) + err := responseManager.UnpauseResponse(td.ctx, td.requestID, td.extensionResponse) require.NoError(t, err) td.assertReceiveExtensionResponse() td.assertCompleteRequestWith(graphsync.RequestCompletedFull) @@ -580,7 +593,7 @@ func TestValidationAndExtensions(t *testing.T) { td.blockHooks.Register(func(p peer.ID, requestData graphsync.RequestData, blockData graphsync.BlockData, hookActions graphsync.OutgoingBlockHookActions) { blkIndex++ if blkIndex == blockCount { - err := responseManager.PauseResponse(p, requestData.ID()) + err := responseManager.PauseResponse(td.ctx, requestData.ID()) require.NoError(t, err) } }) @@ -588,7 +601,7 @@ func TestValidationAndExtensions(t *testing.T) { td.assertRequestDoesNotCompleteWhilePaused() td.verifyNResponses(blockCount + 1) td.assertPausedRequest() - err := responseManager.UnpauseResponse(td.p, td.requestID) + err := responseManager.UnpauseResponse(td.ctx, td.requestID) require.NoError(t, err) td.verifyNResponses(td.blockChainLength - (blockCount + 1)) td.assertCompleteRequestWith(graphsync.RequestCompletedFull) @@ -607,7 +620,7 @@ func TestValidationAndExtensions(t *testing.T) { }) go func() { <-advance - err := responseManager.UnpauseResponse(td.p, td.requestID) + err := responseManager.UnpauseResponse(td.ctx, td.requestID) require.NoError(t, err) }() responseManager.ProcessRequests(td.ctx, td.p, td.requests) @@ -781,7 +794,7 @@ func TestValidationAndExtensions(t *testing.T) { td.assertCompleteRequestWith(graphsync.RequestFailedUnknown) // cannot unpause - err := responseManager.UnpauseResponse(td.p, td.requestID) + err := responseManager.UnpauseResponse(td.ctx, td.requestID) require.Error(t, err) }) }) @@ -806,6 +819,7 @@ func TestNetworkErrors(t *testing.T) { td.assertNetworkErrors(err, 1) td.assertNoCompletedResponseStatuses() }) + t.Run("network error final status - failure", func(t *testing.T) { td := newTestData(t) defer td.cancel() @@ -818,6 +832,7 @@ func TestNetworkErrors(t *testing.T) { td.assertNetworkErrors(err, 1) td.assertNoCompletedResponseStatuses() }) + t.Run("network error block send", func(t *testing.T) { td := newTestData(t) defer td.cancel() @@ -833,6 +848,7 @@ func TestNetworkErrors(t *testing.T) { td.assertHasNetworkErrors(err) td.assertNoCompletedResponseStatuses() }) + t.Run("network error while paused", func(t *testing.T) { td := newTestData(t) defer td.cancel() @@ -857,11 +873,75 @@ func TestNetworkErrors(t *testing.T) { td.notifyBlockSendsNetworkError(err) td.assertNetworkErrors(err, 1) td.assertRequestCleared() - err = responseManager.UnpauseResponse(td.p, td.requestID, td.extensionResponse) + err = responseManager.UnpauseResponse(td.ctx, td.requestID, td.extensionResponse) require.Error(t, err) }) } +func TestUpdateResponse(t *testing.T) { + t.Run("while unpaused", func(t *testing.T) { + td := newTestData(t) + defer td.cancel() + responseManager := td.newResponseManager() + responseManager.Startup() + td.requestHooks.Register(func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) { + hookActions.ValidateRequest() + }) + responseManager.ProcessRequests(td.ctx, td.p, td.requests) + td.assertSendBlock() + responseManager.synchronize() + + // send an update with some custom extensions + ext1 := graphsync.ExtensionData{Name: graphsync.ExtensionName("grip grop"), Data: basicnode.NewString("flim flam, blim blam")} + ext2 := graphsync.ExtensionData{Name: graphsync.ExtensionName("Humpty/Dumpty"), Data: basicnode.NewInt(101)} + + responseManager.UpdateResponse(td.ctx, td.requestID, ext1, ext2) + + var receivedExtension sentExtension + testutil.AssertReceive(td.ctx, td.t, td.sentExtensions, &receivedExtension, "should send first extension response") + require.Equal(td.t, ext1, receivedExtension.extension, "incorrect first extension response sent") + testutil.AssertReceive(td.ctx, td.t, td.sentExtensions, &receivedExtension, "should send second extension response") + require.Equal(td.t, ext2, receivedExtension.extension, "incorrect second extension response sent") + td.assertNoCompletedResponseStatuses() + }) + + t.Run("while paused", func(t *testing.T) { + td := newTestData(t) + defer td.cancel() + responseManager := td.newResponseManager() + responseManager.Startup() + td.requestHooks.Register(func(p peer.ID, requestData graphsync.RequestData, hookActions graphsync.IncomingRequestHookActions) { + hookActions.ValidateRequest() + }) + blkIndex := 0 + blockCount := 3 + td.blockHooks.Register(func(p peer.ID, requestData graphsync.RequestData, blockData graphsync.BlockData, hookActions graphsync.OutgoingBlockHookActions) { + blkIndex++ + if blkIndex == blockCount { + hookActions.PauseResponse() + } + }) + responseManager.ProcessRequests(td.ctx, td.p, td.requests) + td.assertRequestDoesNotCompleteWhilePaused() + td.verifyNResponsesOnlyProcessing(blockCount) + td.assertPausedRequest() + + // send an update with some custom extensions + ext1 := graphsync.ExtensionData{Name: graphsync.ExtensionName("grip grop"), Data: basicnode.NewString("flim flam, blim blam")} + ext2 := graphsync.ExtensionData{Name: graphsync.ExtensionName("Humpty/Dumpty"), Data: basicnode.NewInt(101)} + + responseManager.UpdateResponse(td.ctx, td.requestID, ext1, ext2) + responseManager.synchronize() + + var receivedExtension sentExtension + testutil.AssertReceive(td.ctx, td.t, td.sentExtensions, &receivedExtension, "should send first extension response") + require.Equal(td.t, ext1, receivedExtension.extension, "incorrect first extension response sent") + testutil.AssertReceive(td.ctx, td.t, td.sentExtensions, &receivedExtension, "should send second extension response") + require.Equal(td.t, ext2, receivedExtension.extension, "incorrect second extension response sent") + td.assertNoCompletedResponseStatuses() + }) +} + type fakeResponseAssembler struct { transactionLk *sync.Mutex sentResponses chan sentResponse @@ -1017,6 +1097,12 @@ func (frb *fakeResponseBuilder) SendExtensionData(extension graphsync.ExtensionD frb.fra.sendExtensionData(frb.requestID, extension) } +func (frb *fakeResponseBuilder) SendUpdates(extensions []graphsync.ExtensionData) { + for _, ext := range extensions { + frb.fra.sendExtensionData(frb.requestID, ext) + } +} + func (frb *fakeResponseBuilder) FinishRequest() graphsync.ResponseStatusCode { return frb.fra.finishRequest(frb.requestID) } @@ -1052,12 +1138,12 @@ type testData struct { skippedFirstBlocks chan int64 dedupKeys chan string responseAssembler *fakeResponseAssembler - extensionData []byte + extensionData datamodel.Node extensionName graphsync.ExtensionName extension graphsync.ExtensionData - extensionResponseData []byte + extensionResponseData datamodel.Node extensionResponse graphsync.ExtensionData - extensionUpdateData []byte + extensionUpdateData datamodel.Node extensionUpdate graphsync.ExtensionData requestID graphsync.RequestID requests []gsmsg.GraphSyncRequest @@ -1126,28 +1212,28 @@ func newTestData(t *testing.T) testData { completedNotifications: td.completedNotifications, } - td.extensionData = testutil.RandomBytes(100) + td.extensionData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionName = graphsync.ExtensionName("AppleSauce/McGee") td.extension = graphsync.ExtensionData{ Name: td.extensionName, Data: td.extensionData, } - td.extensionResponseData = testutil.RandomBytes(100) + td.extensionResponseData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionResponse = graphsync.ExtensionData{ Name: td.extensionName, Data: td.extensionResponseData, } - td.extensionUpdateData = testutil.RandomBytes(100) + td.extensionUpdateData = basicnode.NewBytes(testutil.RandomBytes(100)) td.extensionUpdate = graphsync.ExtensionData{ Name: td.extensionName, Data: td.extensionUpdateData, } - td.requestID = graphsync.RequestID(rand.Int31()) + td.requestID = graphsync.NewRequestID() td.requests = []gsmsg.GraphSyncRequest{ gsmsg.NewRequest(td.requestID, td.blockChain.TipLink.(cidlink.Link).Cid, td.blockChain.Selector(), graphsync.Priority(0), td.extension), } td.updateRequests = []gsmsg.GraphSyncRequest{ - gsmsg.UpdateRequest(td.requestID, td.extensionUpdate), + gsmsg.NewUpdateRequest(td.requestID, td.extensionUpdate), } td.p = testutil.GeneratePeers(1)[0] td.peristenceOptions = persistenceoptions.New() @@ -1321,7 +1407,7 @@ func (td *testData) notifyStatusMessagesSent() { func (td *testData) notifyBlockSendsSent() { td.transactionLk.Lock() - td.notifeePublisher.PublishEvents(notifications.Topic(rand.Int31), []notifications.Event{ + td.notifeePublisher.PublishEvents(notifications.Topic(graphsync.NewRequestID), []notifications.Event{ messagequeue.Event{Name: messagequeue.Sent, Metadata: messagequeue.Metadata{BlockData: td.blkNotifications}}, }) td.blkNotifications = make(map[graphsync.RequestID][]graphsync.BlockData) diff --git a/responsemanager/server.go b/responsemanager/server.go index 16b8c889..7927b26a 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -45,21 +45,21 @@ func (rm *ResponseManager) run() { } } -func (rm *ResponseManager) terminateRequest(key responseKey) { - ipr, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) terminateRequest(requestID graphsync.RequestID) { + ipr, ok := rm.inProgressResponses[requestID] if !ok { return } - rm.connManager.Unprotect(key.p, key.requestID.Tag()) - delete(rm.inProgressResponses, key) + rm.connManager.Unprotect(ipr.peer, requestID.Tag()) + delete(rm.inProgressResponses, requestID) ipr.cancelFn() ipr.span.End() } -func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, update gsmsg.GraphSyncRequest) { - response, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) processUpdate(ctx context.Context, requestID graphsync.RequestID, update gsmsg.GraphSyncRequest) { + response, ok := rm.inProgressResponses[requestID] if !ok || response.state == graphsync.CompletingSend { - log.Warnf("received update for non existent request, peer %s, request ID %d", key.p.Pretty(), key.requestID) + log.Warnf("received update for non existent request ID %s", requestID.String()) return } @@ -68,8 +68,15 @@ func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, u "processUpdate", trace.WithLinks(trace.LinkFromContext(ctx)), trace.WithAttributes( - attribute.Int("id", int(update.ID())), - attribute.StringSlice("extensions", update.ExtensionNames()), + attribute.String("id", update.ID().String()), + attribute.StringSlice("extensions", func() []string { + names := update.ExtensionNames() + st := make([]string, 0, len(names)) + for _, n := range names { + st = append(st, string(n)) + } + return st + }()), )) defer span.End() @@ -82,7 +89,7 @@ func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, u } return } // else this is a paused response, so the update needs to be handled here and not in the executor - result := rm.updateHooks.ProcessUpdateHooks(key.p, response.request, update) + result := rm.updateHooks.ProcessUpdateHooks(response.peer, response.request, update) _ = response.responseStream.Transaction(func(rb responseassembler.ResponseBuilder) error { for _, extension := range result.Extensions { rb.SendExtensionData(extension) @@ -99,7 +106,7 @@ func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, u return } if result.Unpause { - err := rm.unpauseRequest(key.p, key.requestID) + err := rm.unpauseRequest(requestID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, result.Err.Error()) @@ -108,11 +115,10 @@ func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, u } } -func (rm *ResponseManager) unpauseRequest(p peer.ID, requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { - key := responseKey{p, requestID} - inProgressResponse, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) unpauseRequest(requestID graphsync.RequestID, extensions ...graphsync.ExtensionData) error { + inProgressResponse, ok := rm.inProgressResponses[requestID] if !ok { - return errors.New("could not find request") + return graphsync.RequestNotFoundErr{} } if inProgressResponse.state != graphsync.Paused { return errors.New("request is not paused") @@ -126,16 +132,17 @@ func (rm *ResponseManager) unpauseRequest(p peer.ID, requestID graphsync.Request return nil }) } - rm.responseQueue.PushTask(p, peertask.Task{Topic: key, Priority: math.MaxInt32, Work: 1}) + rm.responseQueue.PushTask(inProgressResponse.peer, peertask.Task{Topic: requestID, Priority: math.MaxInt32, Work: 1}) return nil } -func (rm *ResponseManager) abortRequest(ctx context.Context, p peer.ID, requestID graphsync.RequestID, err error) error { - key := responseKey{p, requestID} - rm.responseQueue.Remove(key, key.p) - response, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) abortRequest(ctx context.Context, requestID graphsync.RequestID, err error) error { + response, ok := rm.inProgressResponses[requestID] + if ok { + rm.responseQueue.Remove(requestID, response.peer) + } if !ok || response.state == graphsync.CompletingSend { - return errors.New("could not find request") + return graphsync.RequestNotFoundErr{} } _, span := otel.Tracer("graphsync").Start(trace.ContextWithSpan(ctx, response.span), @@ -151,13 +158,13 @@ func (rm *ResponseManager) abortRequest(ctx context.Context, p peer.ID, requestI if response.state != graphsync.Running { if ipldutil.IsContextCancelErr(err) { response.responseStream.ClearRequest() - rm.terminateRequest(key) - rm.cancelledListeners.NotifyCancelledListeners(p, response.request) + rm.terminateRequest(requestID) + rm.cancelledListeners.NotifyCancelledListeners(response.peer, response.request) return nil } if err == queryexecutor.ErrNetworkError { response.responseStream.ClearRequest() - rm.terminateRequest(key) + rm.terminateRequest(requestID) return nil } response.state = graphsync.CompletingSend @@ -182,14 +189,14 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync defer messageSpan.End() for _, request := range requests { - key := responseKey{p: p, requestID: request.ID()} - if request.IsCancel() { - _ = rm.abortRequest(ctx, p, request.ID(), ipldutil.ContextCancelError{}) + switch request.Type() { + case graphsync.RequestTypeCancel: + _ = rm.abortRequest(ctx, request.ID(), ipldutil.ContextCancelError{}) continue - } - if request.IsUpdate() { - rm.processUpdate(ctx, key, request) + case graphsync.RequestTypeUpdate: + rm.processUpdate(ctx, request.ID(), request) continue + default: } rm.connManager.Protect(p, request.ID().Tag()) // don't use `ctx` which has the "message" trace, but rm.ctx for a fresh trace which allows @@ -200,14 +207,21 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync "response", trace.WithLinks(trace.LinkFromContext(ctx)), trace.WithAttributes( - attribute.Int("id", int(request.ID())), + attribute.String("id", request.ID().String()), attribute.Int("priority", int(request.Priority())), attribute.String("root", request.Root().String()), - attribute.StringSlice("extensions", request.ExtensionNames()), + attribute.StringSlice("extensions", func() []string { + names := request.ExtensionNames() + st := make([]string, 0, len(names)) + for _, n := range names { + st = append(st, string(n)) + } + return st + }()), )) rctx, cancelFn := context.WithCancel(rctx) sub := &subscriber{ - p: key.p, + p: p, request: request, requestCloser: rm, blockSentListeners: rm.blockSentListeners, @@ -215,17 +229,18 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync networkErrorListeners: rm.networkErrorListeners, connManager: rm.connManager, } - log.Infow("graphsync request initiated", "request id", request.ID(), "peer", p, "root", request.Root()) - ipr, ok := rm.inProgressResponses[key] + log.Infow("graphsync request initiated", "request id", request.ID().String(), "peer", p, "root", request.Root()) + ipr, ok := rm.inProgressResponses[request.ID()] if ok && ipr.state == graphsync.Running { - log.Warnf("there is an identical request already in progress", "request id", request.ID(), "peer", p) + log.Warnf("there is an identical request already in progress", "request id", request.ID().String(), "peer", p) } - rm.inProgressResponses[key] = + rm.inProgressResponses[request.ID()] = &inProgressResponseStatus{ ctx: rctx, span: responseSpan, cancelFn: cancelFn, + peer: p, request: request, signals: queryexecutor.ResponseSignals{ PauseSignal: make(chan struct{}, 1), @@ -234,23 +249,23 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync }, state: graphsync.Queued, startTime: time.Now(), - responseStream: rm.responseAssembler.NewStream(ctx, key.p, key.requestID, sub), + responseStream: rm.responseAssembler.NewStream(ctx, p, request.ID(), sub), } // TODO: Use a better work estimation metric. - rm.responseQueue.PushTask(p, peertask.Task{Topic: key, Priority: int(request.Priority()), Work: 1}) + rm.responseQueue.PushTask(p, peertask.Task{Topic: request.ID(), Priority: int(request.Priority()), Work: 1}) } } -func (rm *ResponseManager) taskDataForKey(key responseKey) queryexecutor.ResponseTask { - response, hasResponse := rm.inProgressResponses[key] +func (rm *ResponseManager) taskDataForKey(requestID graphsync.RequestID) queryexecutor.ResponseTask { + response, hasResponse := rm.inProgressResponses[requestID] if !hasResponse || response.state == graphsync.CompletingSend { return queryexecutor.ResponseTask{Empty: true} } - log.Infow("graphsync response processing begins", "request id", key.requestID, "peer", key.p, "total time", time.Since(response.startTime)) + log.Infow("graphsync response processing begins", "request id", requestID.String(), "peer", response.peer, "total time", time.Since(response.startTime)) if response.loader == nil || response.traverser == nil { - loader, traverser, isPaused, err := (&queryPreparer{rm.requestHooks, rm.linkSystem, rm.maxLinksPerRequest}).prepareQuery(response.ctx, key.p, response.request, response.responseStream, response.signals) + loader, traverser, isPaused, err := (&queryPreparer{rm.requestHooks, rm.linkSystem, rm.maxLinksPerRequest}).prepareQuery(response.ctx, response.peer, response.request, response.responseStream, response.signals) if err != nil { response.state = graphsync.CompletingSend response.span.RecordError(err) @@ -277,20 +292,20 @@ func (rm *ResponseManager) taskDataForKey(key responseKey) queryexecutor.Respons } } -func (rm *ResponseManager) startTask(task *peertask.Task) queryexecutor.ResponseTask { - key := task.Topic.(responseKey) - taskData := rm.taskDataForKey(key) +func (rm *ResponseManager) startTask(task *peertask.Task, p peer.ID) queryexecutor.ResponseTask { + requestID := task.Topic.(graphsync.RequestID) + taskData := rm.taskDataForKey(requestID) if taskData.Empty { - rm.responseQueue.TaskDone(key.p, task) + rm.responseQueue.TaskDone(p, task) } return taskData } -func (rm *ResponseManager) finishTask(task *peertask.Task, err error) { - key := task.Topic.(responseKey) - rm.responseQueue.TaskDone(key.p, task) - response, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) finishTask(task *peertask.Task, p peer.ID, err error) { + requestID := task.Topic.(graphsync.RequestID) + rm.responseQueue.TaskDone(p, task) + response, ok := rm.inProgressResponses[requestID] if !ok { return } @@ -298,7 +313,7 @@ func (rm *ResponseManager) finishTask(task *peertask.Task, err error) { response.state = graphsync.Paused return } - log.Infow("graphsync response processing complete (messages stil sending)", "request id", key.requestID, "peer", key.p, "total time", time.Since(response.startTime)) + log.Infow("graphsync response processing complete (messages stil sending)", "request id", requestID.String(), "peer", p, "total time", time.Since(response.startTime)) if err != nil { response.span.RecordError(err) @@ -307,21 +322,21 @@ func (rm *ResponseManager) finishTask(task *peertask.Task, err error) { } if ipldutil.IsContextCancelErr(err) { - rm.cancelledListeners.NotifyCancelledListeners(key.p, response.request) - rm.terminateRequest(key) + rm.cancelledListeners.NotifyCancelledListeners(p, response.request) + rm.terminateRequest(requestID) return } if err == queryexecutor.ErrNetworkError { - rm.terminateRequest(key) + rm.terminateRequest(requestID) return } response.state = graphsync.CompletingSend } -func (rm *ResponseManager) getUpdates(key responseKey) []gsmsg.GraphSyncRequest { - response, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) getUpdates(requestID graphsync.RequestID) []gsmsg.GraphSyncRequest { + response, ok := rm.inProgressResponses[requestID] if !ok { return nil } @@ -330,11 +345,10 @@ func (rm *ResponseManager) getUpdates(key responseKey) []gsmsg.GraphSyncRequest return updates } -func (rm *ResponseManager) pauseRequest(p peer.ID, requestID graphsync.RequestID) error { - key := responseKey{p, requestID} - inProgressResponse, ok := rm.inProgressResponses[key] +func (rm *ResponseManager) pauseRequest(requestID graphsync.RequestID) error { + inProgressResponse, ok := rm.inProgressResponses[requestID] if !ok || inProgressResponse.state == graphsync.CompletingSend { - return errors.New("could not find request") + return graphsync.RequestNotFoundErr{} } if inProgressResponse.state == graphsync.Paused { return errors.New("request is already paused") @@ -346,13 +360,26 @@ func (rm *ResponseManager) pauseRequest(p peer.ID, requestID graphsync.RequestID return nil } +func (rm *ResponseManager) updateRequest(requestID graphsync.RequestID, extensions []graphsync.ExtensionData) error { + inProgressResponse, ok := rm.inProgressResponses[requestID] + if !ok { + return graphsync.RequestNotFoundErr{} + } + _ = inProgressResponse.responseStream.Transaction(func(rb responseassembler.ResponseBuilder) error { + rb.SendUpdates(extensions) + return nil + }) + + return nil +} + func (rm *ResponseManager) peerState(p peer.ID) peerstate.PeerState { var peerState peerstate.PeerState rm.responseQueue.WithPeerTopics(p, func(peerTopics *peertracker.PeerTrackerTopics) { requestStates := make(graphsync.RequestStates) for key, ipr := range rm.inProgressResponses { - if key.p == p { - requestStates[key.requestID] = ipr.state + if ipr.peer == p { + requestStates[key] = ipr.state } } peerState = peerstate.PeerState{RequestStates: requestStates, TaskQueueState: fromPeerTopics(peerTopics)} @@ -366,11 +393,11 @@ func fromPeerTopics(pt *peertracker.PeerTrackerTopics) peerstate.TaskQueueState } active := make([]graphsync.RequestID, 0, len(pt.Active)) for _, topic := range pt.Active { - active = append(active, topic.(responseKey).requestID) + active = append(active, topic.(graphsync.RequestID)) } pending := make([]graphsync.RequestID, 0, len(pt.Pending)) for _, topic := range pt.Pending { - pending = append(pending, topic.(responseKey).requestID) + pending = append(pending, topic.(graphsync.RequestID)) } return peerstate.TaskQueueState{ Active: active, diff --git a/responsemanager/subscriber.go b/responsemanager/subscriber.go index 8e3992e7..a5ef20a2 100644 --- a/responsemanager/subscriber.go +++ b/responsemanager/subscriber.go @@ -12,8 +12,8 @@ import ( // RequestCloser can cancel request on a network error type RequestCloser interface { - TerminateRequest(p peer.ID, requestID graphsync.RequestID) - CloseWithNetworkError(p peer.ID, requestID graphsync.RequestID) + TerminateRequest(requestID graphsync.RequestID) + CloseWithNetworkError(requestID graphsync.RequestID) } type subscriber struct { @@ -33,10 +33,10 @@ func (s *subscriber) OnNext(_ notifications.Topic, event notifications.Event) { } switch responseEvent.Name { case messagequeue.Error: - s.requestCloser.CloseWithNetworkError(s.p, s.request.ID()) + s.requestCloser.CloseWithNetworkError(s.request.ID()) responseCode := responseEvent.Metadata.ResponseCodes[s.request.ID()] if responseCode.IsTerminal() { - s.requestCloser.TerminateRequest(s.p, s.request.ID()) + s.requestCloser.TerminateRequest(s.request.ID()) } s.networkErrorListeners.NotifyNetworkErrorListeners(s.p, s.request, responseEvent.Err) case messagequeue.Sent: @@ -46,7 +46,7 @@ func (s *subscriber) OnNext(_ notifications.Topic, event notifications.Event) { } responseCode := responseEvent.Metadata.ResponseCodes[s.request.ID()] if responseCode.IsTerminal() { - s.requestCloser.TerminateRequest(s.p, s.request.ID()) + s.requestCloser.TerminateRequest(s.request.ID()) s.completedListeners.NotifyCompletedListeners(s.p, s.request, responseCode) } } diff --git a/testutil/testchain.go b/testutil/testchain.go index f6e15deb..01142537 100644 --- a/testutil/testchain.go +++ b/testutil/testchain.go @@ -8,6 +8,7 @@ import ( blocks "github.com/ipfs/go-block-format" cid "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/datamodel" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/traversal/selector" @@ -149,16 +150,19 @@ func (tbc *TestBlockChain) NodeTipIndex(fromTip int) ipld.Node { return tbc.MiddleNodes[height-1] } } + +// PathTipIndex returns the path to the block at the given index from the tip +func (tbc *TestBlockChain) PathTipIndex(fromTip int) ipld.Path { + expectedPath := make([]datamodel.PathSegment, 0, 2*fromTip) + for i := 0; i < fromTip; i++ { + expectedPath = append(expectedPath, datamodel.PathSegmentOfString("Parents"), datamodel.PathSegmentOfInt(0)) + } + return datamodel.NewPath(expectedPath) +} + func (tbc *TestBlockChain) checkResponses(responses []graphsync.ResponseProgress, start int, end int, verifyTypes bool) { require.Len(tbc.t, responses, (end-start)*blockChainTraversedNodesPerBlock, "traverses all nodes") - expectedPath := "" - for i := 0; i < start; i++ { - if expectedPath == "" { - expectedPath = "Parents/0" - } else { - expectedPath = expectedPath + "/Parents/0" - } - } + expectedPath := tbc.PathTipIndex(start).String() for i, response := range responses { require.Equal(tbc.t, expectedPath, response.Path.String(), "response has correct path") if i%2 == 0 {