From f330fe83d82a77e676d8642b19aa05382ee195fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Fri, 17 Dec 2021 18:27:41 +0000 Subject: [PATCH 01/32] feat(net): initial dag-cbor protocol support also added first roundtrip benchmark --- go.mod | 2 + message/bench_test.go | 81 +++++++++ message/builder.go | 30 ++-- message/builder_test.go | 73 +++++--- message/message.go | 385 ++++++++++++++++++++-------------------- message/message_test.go | 189 ++++++++++---------- message/schema.go | 28 +++ message/schema.ipldsch | 63 +++++++ 8 files changed, 534 insertions(+), 317 deletions(-) create mode 100644 message/bench_test.go create mode 100644 message/schema.go create mode 100644 message/schema.ipldsch diff --git a/go.mod b/go.mod index 8b7e7830..00dac4d1 100644 --- a/go.mod +++ b/go.mod @@ -47,3 +47,5 @@ require ( golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/protobuf v1.27.1 ) + +replace github.com/ipld/go-ipld-prime => ../../src/ipld diff --git a/message/bench_test.go b/message/bench_test.go new file mode 100644 index 00000000..0c2d34cf --- /dev/null +++ b/message/bench_test.go @@ -0,0 +1,81 @@ +package message_test + +import ( + "bytes" + "math/rand" + "testing" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message" + "github.com/ipfs/go-graphsync/testutil" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/node/basicnode" + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/ipld/go-ipld-prime/traversal/selector/builder" + "github.com/stretchr/testify/require" +) + +func BenchmarkMessageEncodingRoundtrip(b *testing.B) { + root := testutil.GenerateCids(1)[0] + ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) + selector := ssb.Matcher().Node() + extensionName := graphsync.ExtensionName("graphsync/awesome") + extension := message.NamedExtension{ + Name: extensionName, + Data: basicnode.NewBytes(testutil.RandomBytes(100)), + } + id := graphsync.RequestID(rand.Int31()) + 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(b, err) + + b.Run("Protobuf", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + buf := new(bytes.Buffer) + for pb.Next() { + buf.Reset() + + err := gsm.ToNet(buf) + require.NoError(b, err) + + gsm2, err := message.FromNet(buf) + require.NoError(b, err) + require.Equal(b, gsm, gsm2) + } + }) + }) + + b.Run("DagCbor", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + buf := new(bytes.Buffer) + for pb.Next() { + buf.Reset() + + node := bindnode.Wrap(&gsm, message.Prototype.Message.Type()) + err := dagcbor.Encode(node.Representation(), buf) + require.NoError(b, err) + + builder := message.Prototype.Message.Representation().NewBuilder() + err = dagcbor.Decode(builder, buf) + require.NoError(b, err) + node2 := builder.Build() + gsm2 := *bindnode.Unwrap(node2).(*message.GraphSyncMessage) + require.Equal(b, gsm, gsm2) + } + }) + }) +} diff --git a/message/builder.go b/message/builder.go index 15017198..8c92f72a 100644 --- a/message/builder.go +++ b/message/builder.go @@ -2,9 +2,10 @@ package message import ( blocks "github.com/ipfs/go-block-format" - "github.com/ipfs/go-cid" + cid "github.com/ipfs/go-cid" "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/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/metadata" @@ -18,7 +19,7 @@ type Builder struct { blkSize uint64 completedResponses map[graphsync.RequestID]graphsync.ResponseStatusCode outgoingResponses map[graphsync.RequestID]metadata.Metadata - extensions map[graphsync.RequestID][]graphsync.ExtensionData + extensions map[graphsync.RequestID][]NamedExtension requests map[graphsync.RequestID]GraphSyncRequest } @@ -29,13 +30,13 @@ func NewBuilder() *Builder { outgoingBlocks: make(map[cid.Cid]blocks.Block), completedResponses: make(map[graphsync.RequestID]graphsync.ResponseStatusCode), outgoingResponses: make(map[graphsync.RequestID]metadata.Metadata), - extensions: make(map[graphsync.RequestID][]graphsync.ExtensionData), + extensions: make(map[graphsync.RequestID][]NamedExtension), } } // AddRequest registers a new request to be added to the message. func (b *Builder) AddRequest(request GraphSyncRequest) { - b.requests[request.ID()] = request + b.requests[request.ID] = request } // AddBlock adds the given block to the message. @@ -45,7 +46,7 @@ func (b *Builder) AddBlock(block blocks.Block) { } // AddExtensionData adds the given extension data to to the message -func (b *Builder) AddExtensionData(requestID graphsync.RequestID, extension graphsync.ExtensionData) { +func (b *Builder) AddExtensionData(requestID graphsync.RequestID, extension NamedExtension) { b.extensions[requestID] = append(b.extensions[requestID], extension) // make sure this extension goes out in next response even if no links are sent _, ok := b.outgoingResponses[requestID] @@ -109,21 +110,30 @@ func (b *Builder) ScrubResponses(requestIDs []graphsync.RequestID) uint64 { // Build assembles and encodes message data from the added requests, links, and blocks. func (b *Builder) Build() (GraphSyncMessage, error) { - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(b.outgoingResponses)) + requests := make([]GraphSyncRequest, 0, len(b.requests)) + for _, request := range b.requests { + requests = append(requests, request) + } + responses := make([]GraphSyncResponse, 0, 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{ + b.extensions[requestID] = append(b.extensions[requestID], NamedExtension{ Name: graphsync.ExtensionMetadata, - Data: mdRaw, + Data: basicnode.NewBytes(mdRaw), // TODO: likely wrong }) status, isComplete := b.completedResponses[requestID] - responses[requestID] = NewResponse(requestID, responseCode(status, isComplete), b.extensions[requestID]...) + responses = append(responses, NewResponse(requestID, responseCode(status, isComplete), b.extensions[requestID]...)) + } + blocks := make([]GraphSyncBlock, 0, len(b.outgoingBlocks)) + for _, block := range b.outgoingBlocks { + blocks = append(blocks, FromBlockFormat(block)) } + // TODO: sort requests, responses, and blocks? map order is randomized return GraphSyncMessage{ - b.requests, responses, b.outgoingBlocks, + requests, responses, blocks, }, nil } diff --git a/message/builder_test.go b/message/builder_test.go index b9722c84..4dac2132 100644 --- a/message/builder_test.go +++ b/message/builder_test.go @@ -1,12 +1,14 @@ package message import ( + "bytes" "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" @@ -14,21 +16,42 @@ import ( "github.com/ipfs/go-graphsync/testutil" ) +// Like the funcs in testutil above, but using blocks at the protocol level. +// We can't put them there right away, due to import cycles. +// We need to refactor these tests to be external, i.e. "package message_test". + +func ContainsGraphSyncBlock(blks []GraphSyncBlock, block GraphSyncBlock) bool { + for _, blk := range blks { + if bytes.Equal(blk.Prefix, block.Prefix) && bytes.Equal(blk.Data, block.Data) { + return true + } + } + return false +} +func AssertContainsGraphSyncBlock(t testing.TB, blks []GraphSyncBlock, block GraphSyncBlock) { + t.Helper() + require.True(t, ContainsGraphSyncBlock(blks, block), "given block should be in list") +} +func RefuteContainsGraphSyncBlock(t testing.TB, blks []GraphSyncBlock, block GraphSyncBlock) { + t.Helper() + require.False(t, ContainsGraphSyncBlock(blks, block), "given block should not be in list") +} + func TestMessageBuilding(t *testing.T) { blocks := testutil.GenerateBlocksOfSize(3, 100) links := make([]ipld.Link, 0, len(blocks)) for _, block := range blocks { links = append(links, cidlink.Link{Cid: block.Cid()}) } - extensionData1 := testutil.RandomBytes(100) + extensionData1 := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName1 := graphsync.ExtensionName("AppleSauce/McGee") - extension1 := graphsync.ExtensionData{ + extension1 := NamedExtension{ Name: extensionName1, Data: extensionData1, } - extensionData2 := testutil.RandomBytes(100) + extensionData2 := basicnode.NewBytes(testutil.RandomBytes(100)) extensionName2 := graphsync.ExtensionName("HappyLand/Happenstance") - extension2 := graphsync.ExtensionData{ + extension2 := NamedExtension{ Name: extensionName2, Data: extensionData2, } @@ -75,12 +98,12 @@ func TestMessageBuilding(t *testing.T) { }, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses() - sentBlocks := message.Blocks() + responses := message.Responses + sentBlocks := BlockFormatSlice(message.Blocks) require.Len(t, responses, 4, "did not assemble correct number of responses") response1 := findResponseForRequestID(t, responses, requestID1) - require.Equal(t, graphsync.RequestCompletedPartial, response1.Status(), "did not generate completed partial response") + 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}, @@ -89,7 +112,7 @@ func TestMessageBuilding(t *testing.T) { assertExtension(t, response1, extension1) response2 := findResponseForRequestID(t, responses, requestID2) - require.Equal(t, graphsync.RequestCompletedFull, response2.Status(), "did not generate completed full response") + 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}, @@ -97,7 +120,7 @@ func TestMessageBuilding(t *testing.T) { }) response3 := findResponseForRequestID(t, responses, requestID3) - require.Equal(t, graphsync.PartialResponse, response3.Status(), "did not generate partial response") + 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}, @@ -105,7 +128,7 @@ func TestMessageBuilding(t *testing.T) { assertExtension(t, response3, extension2) response4 := findResponseForRequestID(t, responses, requestID4) - require.Equal(t, graphsync.RequestCompletedFull, response4.Status(), "did not generate completed full response") + require.Equal(t, graphsync.RequestCompletedFull, response4.Status, "did not generate completed full response") require.Equal(t, len(blocks), len(sentBlocks), "did not send all blocks") @@ -121,15 +144,15 @@ func TestMessageBuilding(t *testing.T) { }, expectedSize: 0, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses() + responses := message.Responses response1 := findResponseForRequestID(t, responses, requestID1) - require.Equal(t, graphsync.PartialResponse, response1.Status(), "did not generate partial response") + require.Equal(t, graphsync.PartialResponse, response1.Status, "did not generate partial response") assertMetadata(t, response1, nil) assertExtension(t, response1, extension1) response2 := findResponseForRequestID(t, responses, requestID2) - require.Equal(t, graphsync.PartialResponse, response2.Status(), "did not generate partial response") + require.Equal(t, graphsync.PartialResponse, response2.Status, "did not generate partial response") assertMetadata(t, response2, nil) assertExtension(t, response2, extension2) }, @@ -162,12 +185,12 @@ func TestMessageBuilding(t *testing.T) { expectedSize: 200, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses() - sentBlocks := message.Blocks() + responses := message.Responses + sentBlocks := BlockFormatSlice(message.Blocks) require.Len(t, responses, 3, "did not assemble correct number of responses") response2 := findResponseForRequestID(t, responses, requestID2) - require.Equal(t, graphsync.RequestCompletedFull, response2.Status(), "did not generate completed full response") + 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}, @@ -175,14 +198,14 @@ func TestMessageBuilding(t *testing.T) { }) response3 := findResponseForRequestID(t, responses, requestID3) - require.Equal(t, graphsync.PartialResponse, response3.Status(), "did not generate partial response") + 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}, }) assertExtension(t, response3, extension2) response4 := findResponseForRequestID(t, responses, requestID4) - require.Equal(t, graphsync.RequestCompletedFull, response4.Status(), "did not generate completed full response") + require.Equal(t, graphsync.RequestCompletedFull, response4.Status, "did not generate completed full response") require.Equal(t, len(blocks)-1, len(sentBlocks), "did not send all blocks") @@ -220,12 +243,12 @@ func TestMessageBuilding(t *testing.T) { expectedSize: 100, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses() - sentBlocks := message.Blocks() + responses := message.Responses + sentBlocks := BlockFormatSlice(message.Blocks) require.Len(t, responses, 1, "did not assemble correct number of responses") response3 := findResponseForRequestID(t, responses, requestID3) - require.Equal(t, graphsync.PartialResponse, response3.Status(), "did not generate partial response") + 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}, }) @@ -251,7 +274,7 @@ func TestMessageBuilding(t *testing.T) { func findResponseForRequestID(t *testing.T, responses []GraphSyncResponse, requestID graphsync.RequestID) GraphSyncResponse { for _, response := range responses { - if response.RequestID() == requestID { + if response.ID == requestID { return response } } @@ -259,15 +282,17 @@ func findResponseForRequestID(t *testing.T, responses []GraphSyncResponse, reque return GraphSyncResponse{} } -func assertExtension(t *testing.T, response GraphSyncResponse, extension graphsync.ExtensionData) { +func assertExtension(t *testing.T, response GraphSyncResponse, extension NamedExtension) { returnedExtensionData, found := response.Extension(extension.Name) require.True(t, found) 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) + responseMetadataNode, found := response.Extension(graphsync.ExtensionMetadata) require.True(t, found, "Metadata should be included in response") + responseMetadataRaw, err := responseMetadataNode.AsBytes() + require.NoError(t, err) responseMetadata, err := metadata.DecodeMetadata(responseMetadataRaw) require.NoError(t, err) require.Equal(t, expectedMetadata, responseMetadata, "incorrect metadata included in response") diff --git a/message/message.go b/message/message.go index 6e818847..d1e7428b 100644 --- a/message/message.go +++ b/message/message.go @@ -5,10 +5,13 @@ import ( "errors" "fmt" "io" + "sort" 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" + "github.com/ipld/go-ipld-prime/node/basicnode" pool "github.com/libp2p/go-buffer-pool" "github.com/libp2p/go-libp2p-core/network" "github.com/libp2p/go-msgio" @@ -46,30 +49,81 @@ type Exportable interface { ToNet(w io.Writer) error } +type GraphSyncExtensions struct { + Keys []string + Values map[string]datamodel.Node +} + // 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 + ID graphsync.RequestID + + Root cid.Cid + Selector ipld.Node + Extensions GraphSyncExtensions + Priority graphsync.Priority + Cancel bool + Update bool +} + +type GraphSyncMetadatum struct { + Link datamodel.Link + BlockPresent bool } // GraphSyncResponse is an struct to capture data on a response sent back // in a GraphSyncMessage. type GraphSyncResponse struct { - requestID graphsync.RequestID - status graphsync.ResponseStatusCode - extensions map[string][]byte + ID graphsync.RequestID + + Status graphsync.ResponseStatusCode + Metadata []GraphSyncMetadatum + Extensions GraphSyncExtensions +} + +type GraphSyncBlock struct { + Prefix []byte + Data []byte +} + +func FromBlockFormat(block blocks.Block) GraphSyncBlock { + return GraphSyncBlock{ + Prefix: block.Cid().Prefix().Bytes(), + Data: block.RawData(), + } +} + +func (b GraphSyncBlock) BlockFormat() *blocks.BasicBlock { + pref, err := cid.PrefixFromBytes(b.Prefix) + if err != nil { + panic(err) // should never happen + } + + c, err := pref.Sum(b.Data) + if err != nil { + panic(err) // should never happen + } + + block, err := blocks.NewBlockWithCid(b.Data, c) + if err != nil { + panic(err) // should never happen + } + return block +} + +func BlockFormatSlice(bs []GraphSyncBlock) []blocks.Block { + blks := make([]blocks.Block, len(bs)) + for i, b := range bs { + blks[i] = b.BlockFormat() + } + return blks } type GraphSyncMessage struct { - requests map[graphsync.RequestID]GraphSyncRequest - responses map[graphsync.RequestID]GraphSyncResponse - blocks map[cid.Cid]blocks.Block + Requests []GraphSyncRequest + Responses []GraphSyncResponse + Blocks []GraphSyncBlock } // NewRequest builds a new Graphsync request @@ -77,29 +131,48 @@ func NewRequest(id graphsync.RequestID, root cid.Cid, selector ipld.Node, priority graphsync.Priority, - extensions ...graphsync.ExtensionData) GraphSyncRequest { + extensions ...NamedExtension) GraphSyncRequest { return newRequest(id, root, selector, priority, false, false, toExtensionsMap(extensions)) } // 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) + return newRequest(id, cid.Cid{}, nil, 0, true, false, GraphSyncExtensions{}) } // UpdateRequest generates a new request to update an in progress request with the given extensions -func UpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionData) GraphSyncRequest { +func UpdateRequest(id graphsync.RequestID, extensions ...NamedExtension) GraphSyncRequest { return newRequest(id, cid.Cid{}, nil, 0, false, true, toExtensionsMap(extensions)) } -func toExtensionsMap(extensions []graphsync.ExtensionData) (extensionsMap map[string][]byte) { +// NamedExtension exists just for the purpose of the constructors. +type NamedExtension struct { + Name graphsync.ExtensionName + Data ipld.Node +} + +func toExtensionsMap(extensions []NamedExtension) (m GraphSyncExtensions) { if len(extensions) > 0 { - extensionsMap = make(map[string][]byte, len(extensions)) - for _, extension := range extensions { - extensionsMap[string(extension.Name)] = extension.Data + m.Keys = make([]string, len(extensions)) + m.Values = make(map[string]ipld.Node, len(extensions)) + for i, ext := range extensions { + m.Keys[i] = string(ext.Name) + m.Values[string(ext.Name)] = ext.Data } } - return + return m +} + +func fromProtoExtensions(protoExts map[string][]byte) GraphSyncExtensions { + var exts []NamedExtension + for name, data := range protoExts { + exts = append(exts, NamedExtension{graphsync.ExtensionName(name), basicnode.NewBytes(data)}) + } + // Iterating over the map above is non-deterministic, + // so sort by the unique names to ensure determinism. + sort.Slice(exts, func(i, j int) bool { return exts[i].Name < exts[j].Name }) + return toExtensionsMap(exts) } func newRequest(id graphsync.RequestID, @@ -108,37 +181,37 @@ func newRequest(id graphsync.RequestID, priority graphsync.Priority, isCancel bool, isUpdate bool, - extensions map[string][]byte) GraphSyncRequest { + extensions GraphSyncExtensions) 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, + Cancel: isCancel, + Update: isUpdate, + Extensions: extensions, } } // NewResponse builds a new Graphsync response func NewResponse(requestID graphsync.RequestID, status graphsync.ResponseStatusCode, - extensions ...graphsync.ExtensionData) GraphSyncResponse { + extensions ...NamedExtension) GraphSyncResponse { return newResponse(requestID, status, toExtensionsMap(extensions)) } func newResponse(requestID graphsync.RequestID, - status graphsync.ResponseStatusCode, extensions map[string][]byte) GraphSyncResponse { + status graphsync.ResponseStatusCode, extensions GraphSyncExtensions) GraphSyncResponse { return GraphSyncResponse{ - requestID: requestID, - status: status, - extensions: extensions, + ID: requestID, + Status: status, + Extensions: extensions, } } func newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.GetRequests())) - for _, req := range pbm.Requests { + requests := make([]GraphSyncRequest, len(pbm.GetRequests())) + for i, req := range pbm.Requests { if req == nil { return GraphSyncMessage{}, errors.New("request is nil") } @@ -158,47 +231,29 @@ func newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { 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) + // TODO: we likely need to turn some "core" extensions to fields, + // as some of those got moved to proper fields in the new protocol. + // Same for responses above, as well as the "to proto" funcs. + requests[i] = newRequest(graphsync.RequestID(req.Id), root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, fromProtoExtensions(req.GetExtensions())) } - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.GetResponses())) - for _, res := range pbm.Responses { + responses := make([]GraphSyncResponse, len(pbm.GetResponses())) + for i, 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) + responses[i] = newResponse(graphsync.RequestID(res.Id), graphsync.ResponseStatusCode(res.Status), fromProtoExtensions(res.GetExtensions())) } - blks := make(map[cid.Cid]blocks.Block, len(pbm.GetData())) - for _, b := range pbm.GetData() { + blks := make([]GraphSyncBlock, len(pbm.GetData())) + for i, 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 + blks[i] = GraphSyncBlock{ + Prefix: b.GetPrefix(), + Data: b.GetData(), } - - 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{ @@ -207,41 +262,17 @@ func newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { } func (gsm GraphSyncMessage) Empty() bool { - return len(gsm.blocks) == 0 && len(gsm.requests) == 0 && len(gsm.responses) == 0 -} - -func (gsm GraphSyncMessage) Requests() []GraphSyncRequest { - requests := make([]GraphSyncRequest, 0, len(gsm.requests)) - for _, request := range gsm.requests { - requests = append(requests, request) - } - return requests + return len(gsm.Blocks) == 0 && len(gsm.Requests) == 0 && len(gsm.Responses) == 0 } 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 { - codes[id] = response.Status() + codes := make(map[graphsync.RequestID]graphsync.ResponseStatusCode, len(gsm.Responses)) + for _, response := range gsm.Responses { + codes[response.ID] = response.Status } return codes } -func (gsm GraphSyncMessage) Responses() []GraphSyncResponse { - responses := make([]GraphSyncResponse, 0, len(gsm.responses)) - for _, response := range gsm.responses { - responses = append(responses, response) - } - return responses -} - -func (gsm GraphSyncMessage) Blocks() []blocks.Block { - bs := make([]blocks.Block, 0, len(gsm.blocks)) - for _, block := range gsm.blocks { - bs = append(bs, 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) @@ -265,44 +296,60 @@ func FromMsgReader(r msgio.Reader) (GraphSyncMessage, error) { return newMessageFromProto(&pb) } +func toProtoExtensions(m GraphSyncExtensions) map[string][]byte { + protoExts := make(map[string][]byte, len(m.Values)) + for name, node := range m.Values { + // Only keep those which are plain bytes, + // as those are the only ones that the older protocol clients understand. + if node.Kind() != ipld.Kind_Bytes { + continue + } + raw, err := node.AsBytes() + if err != nil { + panic(err) // shouldn't happen + } + protoExts[name] = raw + } + return protoExts +} + 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 { + 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 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(), + Id: int32(request.ID), + Root: request.Root.Bytes(), Selector: selector, - Priority: int32(request.priority), - Cancel: request.isCancel, - Update: request.isUpdate, - Extensions: request.extensions, + Priority: int32(request.Priority), + Cancel: request.Cancel, + Update: request.Update, + Extensions: toProtoExtensions(request.Extensions), }) } - pbm.Responses = make([]*pb.Message_Response, 0, len(gsm.responses)) - for _, response := range gsm.responses { + 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, + Id: int32(response.ID), + Status: int32(response.Status), + Extensions: toProtoExtensions(response.Extensions), }) } - blocks := gsm.Blocks() - pbm.Data = make([]*pb.Message_Block, 0, len(blocks)) - for _, b := range blocks { + pbm.Data = make([]*pb.Message_Block, 0, len(gsm.Blocks)) + for _, b := range gsm.Blocks { pbm.Data = append(pbm.Data, &pb.Message_Block{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), + Prefix: b.Prefix, + Data: b.Data, }) } return pbm, nil @@ -328,13 +375,13 @@ func (gsm GraphSyncMessage) ToNet(w io.Writer) error { } 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)) + 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)) + responses := make([]string, 0, len(gsm.Responses)) + for _, response := range gsm.Responses { + responses = append(responses, fmt.Sprintf("%d", response.ID)) } return map[string]interface{}{ "requests": requests, @@ -343,40 +390,16 @@ func (gsm GraphSyncMessage) Loggable() map[string]interface{} { } func (gsm GraphSyncMessage) Clone() GraphSyncMessage { - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(gsm.requests)) - for id, request := range gsm.requests { - requests[id] = request - } - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(gsm.responses)) - for id, response := range gsm.responses { - responses[id] = response - } - blocks := make(map[cid.Cid]blocks.Block, len(gsm.blocks)) - for cid, block := range gsm.blocks { - blocks[cid] = block - } + requests := append([]GraphSyncRequest{}, gsm.Requests...) + responses := append([]GraphSyncResponse{}, gsm.Responses...) + blocks := append([]GraphSyncBlock{}, gsm.Blocks...) return GraphSyncMessage{requests, responses, blocks} } -// ID Returns the request ID for this Request -func (gsr GraphSyncRequest) ID() graphsync.RequestID { return gsr.id } - -// Root returns the CID to the root block of this request -func (gsr GraphSyncRequest) Root() cid.Cid { return gsr.root } - -// Selector returns the byte representation of the selector for this request -func (gsr GraphSyncRequest) Selector() ipld.Node { return gsr.selector } - -// Priority returns the priority of this request -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) { - if gsr.extensions == nil { - return nil, false - } - val, ok := gsr.extensions[string(name)] +func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) (ipld.Node, bool) { + val, ok := gsr.Extensions.Values[string(name)] if !ok { return nil, false } @@ -385,32 +408,13 @@ 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 - for ext := range gsr.extensions { - extNames = append(extNames, ext) - } - return extNames + return gsr.Extensions.Keys } -// 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 } - -// RequestID returns the request ID for this response -func (gsr GraphSyncResponse) RequestID() graphsync.RequestID { return gsr.requestID } - -// Status returns the status for a response -func (gsr GraphSyncResponse) Status() graphsync.ResponseStatusCode { return gsr.status } - // 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) { - if gsr.extensions == nil { - return nil, false - } - val, ok := gsr.extensions[string(name)] +func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) (ipld.Node, bool) { + val, ok := gsr.Extensions.Values[string(name)] if !ok { return nil, false } @@ -419,18 +423,14 @@ 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 - for ext := range gsr.extensions { - extNames = append(extNames, ext) - } - return extNames + return gsr.Extensions.Keys } // 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) { - return newData, nil +func (gsr GraphSyncRequest) ReplaceExtensions(extensions []NamedExtension) GraphSyncRequest { + req, _ := gsr.MergeExtensions(extensions, func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error) { + return newNode, nil }) return req } @@ -438,31 +438,32 @@ 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) { - if gsr.extensions == nil { - return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.isCancel, gsr.isUpdate, toExtensionsMap(extensions)), nil +func (gsr GraphSyncRequest) MergeExtensions(extensions []NamedExtension, mergeFunc func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error)) (GraphSyncRequest, error) { + if len(gsr.Extensions.Keys) == 0 { + return newRequest(gsr.ID, gsr.Root, gsr.Selector, gsr.Priority, gsr.Cancel, gsr.Update, toExtensionsMap(extensions)), nil } - newExtensionMap := toExtensionsMap(extensions) - combinedExtensions := make(map[string][]byte) - for name, newData := range newExtensionMap { - oldData, ok := gsr.extensions[name] + combinedExtensions := make(map[string]ipld.Node) + for _, newExt := range extensions { + oldNode, ok := gsr.Extensions.Values[string(newExt.Name)] if !ok { - combinedExtensions[name] = newData + combinedExtensions[string(newExt.Name)] = newExt.Data continue } - resultData, err := mergeFunc(graphsync.ExtensionName(name), oldData, newData) + resultNode, err := mergeFunc(graphsync.ExtensionName(newExt.Name), oldNode, newExt.Data) if err != nil { return GraphSyncRequest{}, err } - combinedExtensions[name] = resultData + combinedExtensions[string(newExt.Name)] = resultNode } - for name, oldData := range gsr.extensions { + for name, oldNode := range gsr.Extensions.Values { _, ok := combinedExtensions[name] if ok { continue } - combinedExtensions[name] = oldData + combinedExtensions[name] = oldNode } - return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.isCancel, gsr.isUpdate, combinedExtensions), nil + extNames := make([]string, len(combinedExtensions)) + sort.Strings(extNames) // for reproducibility + return newRequest(gsr.ID, gsr.Root, gsr.Selector, gsr.Priority, gsr.Cancel, gsr.Update, GraphSyncExtensions{extNames, combinedExtensions}), nil } diff --git a/message/message_test.go b/message/message_test.go index 135342d3..052950f9 100644 --- a/message/message_test.go +++ b/message/message_test.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/node/basicnode" "github.com/ipld/go-ipld-prime/traversal/selector/builder" "github.com/stretchr/testify/require" @@ -19,9 +20,10 @@ import ( func TestAppendingRequests(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") - extension := graphsync.ExtensionData{ + extensionBytes := testutil.RandomBytes(100) + extension := NamedExtension{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(extensionBytes), } root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) @@ -33,15 +35,15 @@ func TestAppendingRequests(t *testing.T) { builder.AddRequest(NewRequest(id, root, selector, priority, extension)) gsm, err := builder.Build() require.NoError(t, err) - requests := gsm.Requests() + 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.False(t, request.IsCancel()) - require.Equal(t, priority, request.Priority()) - require.Equal(t, root.String(), request.Root().String()) - require.Equal(t, selector, request.Selector()) + require.Equal(t, id, request.ID) + require.False(t, request.Cancel) + 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) @@ -57,30 +59,31 @@ func TestAppendingRequests(t *testing.T) { 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) + require.Equal(t, map[string][]byte{"graphsync/awesome": extensionBytes}, pbRequest.Extensions) deserialized, err := newMessageFromProto(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") - deserializedRequests := deserialized.Requests() + 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.False(t, deserializedRequest.IsCancel()) - require.False(t, deserializedRequest.IsUpdate()) - require.Equal(t, priority, deserializedRequest.Priority()) - require.Equal(t, root.String(), deserializedRequest.Root().String()) - require.Equal(t, selector, deserializedRequest.Selector()) + require.Equal(t, id, deserializedRequest.ID) + require.False(t, deserializedRequest.Cancel) + require.False(t, deserializedRequest.Update) + 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{ + extensionBytes := testutil.RandomBytes(100) + extension := NamedExtension{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(extensionBytes), } requestID := graphsync.RequestID(rand.Int31()) status := graphsync.RequestAcknowledged @@ -90,12 +93,12 @@ func TestAppendingResponses(t *testing.T) { builder.AddExtensionData(requestID, extension) gsm, err := builder.Build() require.NoError(t, err) - responses := gsm.Responses() + 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.Equal(t, requestID, response.ID) + require.Equal(t, status, response.Status) require.True(t, found) require.Equal(t, extension.Data, extensionData) @@ -104,16 +107,16 @@ func TestAppendingResponses(t *testing.T) { pbResponse := pbMessage.Responses[0] require.Equal(t, int32(requestID), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) - require.Equal(t, extension.Data, pbResponse.Extensions["graphsync/awesome"]) + require.Equal(t, extensionBytes, pbResponse.Extensions["graphsync/awesome"]) deserialized, err := newMessageFromProto(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") - deserializedResponses := deserialized.Responses() + 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.Equal(t, response.ID, deserializedResponse.ID) + require.Equal(t, response.Status, deserializedResponse.Status) require.True(t, found) require.Equal(t, extension.Data, extensionData) } @@ -164,31 +167,31 @@ func TestRequestCancel(t *testing.T) { gsm, err := builder.Build() require.NoError(t, err) - requests := gsm.Requests() + requests := gsm.Requests 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, id, request.ID) + require.True(t, request.Cancel) buf := new(bytes.Buffer) err = gsm.ToNet(buf) require.NoError(t, err, "did not serialize protobuf message") deserialized, err := FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") - deserializedRequests := deserialized.Requests() + 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.ID, deserializedRequest.ID) + require.Equal(t, request.Cancel, deserializedRequest.Cancel) } func TestRequestUpdate(t *testing.T) { id := graphsync.RequestID(rand.Int31()) extensionName := graphsync.ExtensionName("graphsync/awesome") - extension := graphsync.ExtensionData{ + extension := NamedExtension{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } builder := NewBuilder() @@ -196,12 +199,12 @@ func TestRequestUpdate(t *testing.T) { gsm, err := builder.Build() require.NoError(t, err) - requests := gsm.Requests() + requests := gsm.Requests 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, id, request.ID) + require.True(t, request.Update) + require.False(t, request.Cancel) extensionData, found := request.Extension(extensionName) require.True(t, found) require.Equal(t, extension.Data, extensionData) @@ -212,16 +215,16 @@ func TestRequestUpdate(t *testing.T) { deserialized, err := FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") - deserializedRequests := deserialized.Requests() + 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.IsCancel(), deserializedRequest.IsCancel()) - require.Equal(t, request.IsUpdate(), deserializedRequest.IsUpdate()) - require.Equal(t, request.Priority(), deserializedRequest.Priority()) - require.Equal(t, request.Root().String(), deserializedRequest.Root().String()) - require.Equal(t, request.Selector(), deserializedRequest.Selector()) + require.Equal(t, request.ID, deserializedRequest.ID) + require.Equal(t, request.Cancel, deserializedRequest.Cancel) + require.Equal(t, request.Update, deserializedRequest.Update) + 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) } @@ -231,9 +234,9 @@ func TestToNetFromNetEquivalency(t *testing.T) { ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() extensionName := graphsync.ExtensionName("graphsync/awesome") - extension := graphsync.ExtensionData{ + extension := NamedExtension{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } id := graphsync.RequestID(rand.Int31()) priority := graphsync.Priority(rand.Int31()) @@ -256,41 +259,41 @@ func TestToNetFromNetEquivalency(t *testing.T) { deserialized, err := FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") - requests := gsm.Requests() + requests := gsm.Requests require.Len(t, requests, 1, "did not add request to message") request := requests[0] - deserializedRequests := deserialized.Requests() + 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.False(t, deserializedRequest.IsCancel()) - require.False(t, deserializedRequest.IsUpdate()) - require.Equal(t, request.Priority(), deserializedRequest.Priority()) - require.Equal(t, request.Root().String(), deserializedRequest.Root().String()) - require.Equal(t, request.Selector(), deserializedRequest.Selector()) + require.Equal(t, request.ID, deserializedRequest.ID) + require.False(t, deserializedRequest.Cancel) + require.False(t, deserializedRequest.Update) + 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() + responses := gsm.Responses require.Len(t, responses, 1, "did not add response to message") response := responses[0] - deserializedResponses := deserialized.Responses() + 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.Equal(t, response.ID, deserializedResponse.ID) + 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 deserialized.Blocks { + keys[b.BlockFormat().Cid()] = true } - for _, b := range gsm.Blocks() { - _, ok := keys[b.Cid()] + for _, b := range gsm.Blocks { + _, ok := keys[b.BlockFormat().Cid()] require.True(t, ok) } } @@ -299,28 +302,32 @@ func TestMergeExtensions(t *testing.T) { extensionName1 := graphsync.ExtensionName("graphsync/1") extensionName2 := graphsync.ExtensionName("graphsync/2") extensionName3 := graphsync.ExtensionName("graphsync/3") - initialExtensions := []graphsync.ExtensionData{ + initialExtensions := []NamedExtension{ { Name: extensionName1, - Data: []byte("applesauce"), + Data: basicnode.NewBytes([]byte("applesauce")), }, { Name: extensionName2, - Data: []byte("hello"), + Data: basicnode.NewBytes([]byte("hello")), }, } - replacementExtensions := []graphsync.ExtensionData{ + replacementExtensions := []NamedExtension{ { Name: extensionName2, - Data: []byte("world"), + Data: basicnode.NewBytes([]byte("world")), }, { Name: extensionName3, - Data: []byte("cheese"), + Data: basicnode.NewBytes([]byte("cheese")), }, } - defaultMergeFunc := func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { - return []byte(string(oldData) + " " + string(newData)), nil + defaultMergeFunc := func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error) { + oldData, err := oldNode.AsBytes() + require.NoError(t, err) + newData, err := newNode.AsBytes() + require.NoError(t, err) + return basicnode.NewBytes([]byte(string(oldData) + " " + string(newData))), nil } root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) @@ -332,38 +339,38 @@ func TestMergeExtensions(t *testing.T) { emptyRequest := 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()) + 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, []byte("world"), extData2) + require.Equal(t, basicnode.NewBytes([]byte("world")), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, []byte("cheese"), extData3) + require.Equal(t, basicnode.NewBytes([]byte("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()) + 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, []byte("applesauce"), extData1) + require.Equal(t, basicnode.NewBytes([]byte("applesauce")), extData1) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, []byte("hello world"), extData2) + require.Equal(t, basicnode.NewBytes([]byte("hello world")), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, []byte("cheese"), extData3) + require.Equal(t, basicnode.NewBytes([]byte("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, oldNode, newNode ipld.Node) (ipld.Node, error) { return nil, errors.New("something went wrong") } _, err := defaultRequest.MergeExtensions(replacementExtensions, errorMergeFunc) @@ -371,19 +378,19 @@ func TestMergeExtensions(t *testing.T) { }) 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()) + 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, []byte("applesauce"), extData1) + require.Equal(t, basicnode.NewBytes([]byte("applesauce")), extData1) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, []byte("world"), extData2) + require.Equal(t, basicnode.NewBytes([]byte("world")), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, []byte("cheese"), extData3) + require.Equal(t, basicnode.NewBytes([]byte("cheese")), extData3) }) } diff --git a/message/schema.go b/message/schema.go new file mode 100644 index 00000000..970b801e --- /dev/null +++ b/message/schema.go @@ -0,0 +1,28 @@ +package message + +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 schemaTypeSystem *schema.TypeSystem + +var Prototype struct { + Message schema.TypedPrototype +} + +func init() { + ts, err := ipld.LoadSchemaBytes(embedSchema) + if err != nil { + panic(err) + } + schemaTypeSystem = ts + + Prototype.Message = bindnode.Prototype((*GraphSyncMessage)(nil), ts.TypeByName("GraphSyncMessage")) +} diff --git a/message/schema.ipldsch b/message/schema.ipldsch new file mode 100644 index 00000000..89fc5a3d --- /dev/null +++ b/message/schema.ipldsch @@ -0,0 +1,63 @@ +type GraphSyncExtensions {String:Any} +type GraphSyncRequestID int +type GraphSyncPriority int + +type GraphSyncMetadatum struct { + link Link + blockPresent Bool +} 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 GraphSyncRequest struct { + id GraphSyncRequestID (rename "ID") # unique id set on the requester side + root Link (rename "Root") # a CID for the root node in the query + selector Any (rename "Sel") # see https://github.com/ipld/specs/blob/master/selectors/selectors.md + extensions GraphSyncExtensions (rename "Ext") # side channel information + priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 + cancel Bool (rename "Canc") # whether this cancels a request + update Bool (rename "Updt") # whether this is an update to an in progress request +} representation map + +type GraphSyncResponse struct { + id GraphSyncRequestID (rename "ID") # the request id we are responding to + status GraphSyncResponseStatusCode (rename "Stat") # a status code. + metadata GraphSyncMetadata (rename "Meta") # metadata about response + extensions GraphSyncExtensions (rename "Ext") # side channel information +} representation map + +type GraphSyncBlock struct { + prefix Bytes (rename "Pre") # CID prefix (cid version, multicodec and multihash prefix (type + length) + data Bytes (rename "Data") +} representation map + +type GraphSyncMessage struct { + requests [GraphSyncRequest] (rename "Reqs") + responses [GraphSyncResponse] (rename "Rsps") + blocks [GraphSyncBlock] (rename "Blks") +} representation map From 480fcf501d3b9de68f083d45358f1506ad6ca86e Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 13 Dec 2021 22:36:01 +1100 Subject: [PATCH 02/32] feat(requestid): use uuids for requestids Ref: https://github.com/ipfs/go-graphsync/issues/278 Closes: https://github.com/ipfs/go-graphsync/issues/279 Closes: https://github.com/ipfs/go-graphsync/issues/281 --- go.mod | 1 + graphsync.go | 15 ++++++-- impl/graphsync_test.go | 3 +- linktracker/linktracker_test.go | 7 ++-- message/builder_test.go | 9 +++-- message/message.go | 5 +-- message/message_test.go | 16 ++++----- message/pb/message.pb.go | 18 +++++----- message/pb/message.proto | 4 +-- messagequeue/messagequeue_test.go | 16 ++++----- network/libp2p_impl_test.go | 2 +- peermanager/peermessagemanager_test.go | 2 +- peerstate/peerstate_test.go | 3 +- .../asyncloader/asyncloader_test.go | 35 +++++++++---------- .../loadattemptqueue/loadattemptqueue_test.go | 11 +++--- .../responsecache/responsecache_test.go | 5 ++- requestmanager/client.go | 1 - requestmanager/executor/executor_test.go | 2 +- requestmanager/hooks/hooks_test.go | 7 ++-- requestmanager/requestmanager_test.go | 10 +++--- requestmanager/server.go | 5 ++- responsemanager/hooks/hooks_test.go | 7 ++-- .../queryexecutor/queryexecutor_test.go | 2 +- .../responseassembler_test.go | 27 +++++++------- responsemanager/responsemanager_test.go | 2 +- responsemanager/server.go | 4 +-- 26 files changed, 111 insertions(+), 108 deletions(-) diff --git a/go.mod b/go.mod index 00dac4d1..492fd421 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect + 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 diff --git a/graphsync.go b/graphsync.go index 75756c88..90d53de7 100644 --- a/graphsync.go +++ b/graphsync.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "github.com/google/uuid" "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/traversal" @@ -12,11 +13,21 @@ import ( ) // RequestID is a unique identifier for a GraphSync request. -type RequestID int32 +type RequestID uuid.UUID // 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.UUID(r).String() +} + +// Create a new, random RequestID (should be a UUIDv4) +func NewRequestID() RequestID { + return RequestID(uuid.New()) } // Priority a priority for a GraphSync request. diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 83b11140..9ae71554 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -8,7 +8,6 @@ import ( "io" "io/ioutil" "math" - "math/rand" "os" "path/filepath" "testing" @@ -136,7 +135,7 @@ func TestSendResponseToIncomingRequest(t *testing.T) { blockChainLength := 100 blockChain := testutil.SetupBlockChain(ctx, t, td.persistence2, 100, blockChainLength) - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() builder := gsmsg.NewBuilder() builder.AddRequest(gsmsg.NewRequest(requestID, blockChain.TipLink.(cidlink.Link).Cid, blockChain.Selector(), graphsync.Priority(math.MaxInt32), td.extension)) 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/builder_test.go b/message/builder_test.go index 4dac2132..b7fba622 100644 --- a/message/builder_test.go +++ b/message/builder_test.go @@ -3,7 +3,6 @@ package message import ( "bytes" "io" - "math/rand" "testing" "github.com/ipld/go-ipld-prime" @@ -55,10 +54,10 @@ func TestMessageBuilding(t *testing.T) { Name: extensionName2, Data: extensionData2, } - 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) diff --git a/message/message.go b/message/message.go index d1e7428b..227047ec 100644 --- a/message/message.go +++ b/message/message.go @@ -7,6 +7,7 @@ import ( "io" "sort" + "github.com/google/uuid" blocks "github.com/ipfs/go-block-format" cid "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" @@ -326,7 +327,7 @@ func (gsm GraphSyncMessage) ToProto() (*pb.Message, error) { } } pbm.Requests = append(pbm.Requests, &pb.Message_Request{ - Id: int32(request.ID), + Id: request.ID[:], Root: request.Root.Bytes(), Selector: selector, Priority: int32(request.Priority), @@ -339,7 +340,7 @@ func (gsm GraphSyncMessage) ToProto() (*pb.Message, error) { 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.ID), + Id: response.ID[:], Status: int32(response.Status), Extensions: toProtoExtensions(response.Extensions), }) diff --git a/message/message_test.go b/message/message_test.go index 052950f9..c866bcc4 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -28,7 +28,7 @@ func TestAppendingRequests(t *testing.T) { 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() @@ -53,7 +53,7 @@ func TestAppendingRequests(t *testing.T) { require.NoError(t, err) pbRequest := pbMessage.Requests[0] - require.Equal(t, int32(id), pbRequest.Id) + require.Equal(t, id[:], pbRequest.Id) require.Equal(t, int32(priority), pbRequest.Priority) require.False(t, pbRequest.Cancel) require.False(t, pbRequest.Update) @@ -85,7 +85,7 @@ func TestAppendingResponses(t *testing.T) { Name: extensionName, Data: basicnode.NewBytes(extensionBytes), } - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() status := graphsync.RequestAcknowledged builder := NewBuilder() @@ -105,7 +105,7 @@ func TestAppendingResponses(t *testing.T) { pbMessage, err := gsm.ToProto() require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] - require.Equal(t, int32(requestID), pbResponse.Id) + require.Equal(t, requestID[:], pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, extensionBytes, pbResponse.Extensions["graphsync/awesome"]) @@ -157,7 +157,7 @@ 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] @@ -187,7 +187,7 @@ func TestRequestCancel(t *testing.T) { func TestRequestUpdate(t *testing.T) { - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() extensionName := graphsync.ExtensionName("graphsync/awesome") extension := NamedExtension{ Name: extensionName, @@ -238,7 +238,7 @@ func TestToNetFromNetEquivalency(t *testing.T) { Name: extensionName, Data: basicnode.NewBytes(testutil.RandomBytes(100)), } - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) status := graphsync.RequestAcknowledged @@ -332,7 +332,7 @@ func TestMergeExtensions(t *testing.T) { 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...) t.Run("when merging into empty", func(t *testing.T) { diff --git a/message/pb/message.pb.go b/message/pb/message.pb.go index 7110c0b1..897027eb 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 @@ -98,7 +98,7 @@ type Message_Request struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // unique id set on the requester side + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique id set on the requester side Root []byte `protobuf:"bytes,2,opt,name=root,proto3" json:"root,omitempty"` // a CID for the root node in the query Selector []byte `protobuf:"bytes,3,opt,name=selector,proto3" json:"selector,omitempty"` // ipld selector to retrieve Extensions map[string][]byte `protobuf:"bytes,4,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // aux information. useful for other protocols @@ -139,11 +139,11 @@ func (*Message_Request) Descriptor() ([]byte, []int) { return file_message_proto_rawDescGZIP(), []int{0, 0} } -func (x *Message_Request) GetId() int32 { +func (x *Message_Request) GetId() []byte { if x != nil { return x.Id } - return 0 + return nil } func (x *Message_Request) GetRoot() []byte { @@ -193,7 +193,7 @@ type Message_Response struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // the request id + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // the request id Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"` // a status code. Extensions map[string][]byte `protobuf:"bytes,3,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // additional data } @@ -230,11 +230,11 @@ func (*Message_Response) Descriptor() ([]byte, []int) { return file_message_proto_rawDescGZIP(), []int{0, 1} } -func (x *Message_Response) GetId() int32 { +func (x *Message_Response) GetId() []byte { if x != nil { return x.Id } - return 0 + return nil } func (x *Message_Response) GetStatus() int32 { @@ -328,7 +328,7 @@ var file_message_proto_rawDesc = []byte{ 0x70, 0x68, 0x73, 0x79, 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xab, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, @@ -347,7 +347,7 @@ var file_message_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xc9, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x56, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x67, diff --git a/message/pb/message.proto b/message/pb/message.proto index ed6d561d..0703e50a 100644 --- a/message/pb/message.proto +++ b/message/pb/message.proto @@ -7,7 +7,7 @@ option go_package = ".;graphsync_message_pb"; message Message { message Request { - int32 id = 1; // unique id set on the requester side + bytes id = 1; // unique id set on the requester side bytes root = 2; // a CID for the root node in the query bytes selector = 3; // ipld selector to retrieve map extensions = 4; // aux information. useful for other protocols @@ -17,7 +17,7 @@ message Message { } message Response { - int32 id = 1; // the request id + bytes id = 1; // the request id int32 status = 2; // a status code. map extensions = 3; // additional data } diff --git a/messagequeue/messagequeue_test.go b/messagequeue/messagequeue_test.go index 60d5fb8b..219db099 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,7 +128,7 @@ 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, @@ -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] @@ -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} diff --git a/network/libp2p_impl_test.go b/network/libp2p_impl_test.go index da7d2225..2cdf0938 100644 --- a/network/libp2p_impl_test.go +++ b/network/libp2p_impl_test.go @@ -77,7 +77,7 @@ func TestMessageSendAndReceive(t *testing.T) { Name: extensionName, Data: testutil.RandomBytes(100), } - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) status := graphsync.RequestAcknowledged diff --git a/peermanager/peermessagemanager_test.go b/peermanager/peermessagemanager_test.go index 6d55c936..5b1223dd 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) diff --git a/peerstate/peerstate_test.go b/peerstate/peerstate_test.go index 4519555f..aa4e860a 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 diff --git a/requestmanager/asyncloader/asyncloader_test.go b/requestmanager/asyncloader/asyncloader_test.go index 63255d53..8b6717fa 100644 --- a/requestmanager/asyncloader/asyncloader_test.go +++ b/requestmanager/asyncloader/asyncloader_test.go @@ -3,7 +3,6 @@ package asyncloader import ( "context" "io" - "math/rand" "testing" "time" @@ -23,7 +22,7 @@ func TestAsyncLoadInitialLoadSucceedsLocallyPresent(t *testing.T) { st := newStore() link := st.Store(t, block) withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) assertSuccessResponse(ctx, t, resultChan) @@ -38,7 +37,7 @@ func TestAsyncLoadInitialLoadSucceedsResponsePresent(t *testing.T) { st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() responses := map[graphsync.RequestID]metadata.Metadata{ requestID: { metadata.Item{ @@ -61,7 +60,7 @@ func TestAsyncLoadInitialLoadFails(t *testing.T) { st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() responses := map[graphsync.RequestID]metadata.Metadata{ requestID: { @@ -84,7 +83,7 @@ func TestAsyncLoadInitialLoadIndeterminateWhenRequestNotInProgress(t *testing.T) st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] resultChan := asyncLoader.AsyncLoad(p, requestID, link, ipld.LinkContext{}) assertFailResponse(ctx, t, resultChan) @@ -100,7 +99,7 @@ func TestAsyncLoadInitialLoadIndeterminateThenSucceeds(t *testing.T) { st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() err := asyncLoader.StartRequest(requestID, "") require.NoError(t, err) p := testutil.GeneratePeers(1)[0] @@ -128,7 +127,7 @@ func TestAsyncLoadInitialLoadIndeterminateThenFails(t *testing.T) { withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() err := asyncLoader.StartRequest(requestID, "") require.NoError(t, err) p := testutil.GeneratePeers(1)[0] @@ -154,7 +153,7 @@ func TestAsyncLoadInitialLoadIndeterminateThenRequestFinishes(t *testing.T) { st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { link := testutil.NewTestLink() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() err := asyncLoader.StartRequest(requestID, "") require.NoError(t, err) p := testutil.GeneratePeers(1)[0] @@ -172,7 +171,7 @@ func TestAsyncLoadTwiceLoadsLocallySecondTime(t *testing.T) { link := cidlink.Link{Cid: block.Cid()} st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() responses := map[graphsync.RequestID]metadata.Metadata{ requestID: { metadata.Item{ @@ -203,13 +202,13 @@ func TestRegisterUnregister(t *testing.T) { link1 := otherSt.Store(t, blocks[0]) withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { - requestID1 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() 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()) + requestID2 := graphsync.NewRequestID() err = asyncLoader.StartRequest(requestID2, "other") require.NoError(t, err) p := testutil.GeneratePeers(1)[0] @@ -222,7 +221,7 @@ func TestRegisterUnregister(t *testing.T) { err = asyncLoader.UnregisterPersistenceOption("other") require.NoError(t, err) - requestID3 := graphsync.RequestID(rand.Int31()) + requestID3 := graphsync.NewRequestID() err = asyncLoader.StartRequest(requestID3, "other") require.EqualError(t, err, "unknown persistence option") }) @@ -235,11 +234,11 @@ func TestRequestSplittingLoadLocallyFromBlockstore(t *testing.T) { withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { err := asyncLoader.RegisterPersistenceOption("other", otherSt.lsys) require.NoError(t, err) - requestID1 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] resultChan1 := asyncLoader.AsyncLoad(p, requestID1, link, ipld.LinkContext{}) - requestID2 := graphsync.RequestID(rand.Int31()) + requestID2 := graphsync.NewRequestID() err = asyncLoader.StartRequest(requestID2, "other") require.NoError(t, err) resultChan2 := asyncLoader.AsyncLoad(p, requestID2, link, ipld.LinkContext{}) @@ -259,8 +258,8 @@ func TestRequestSplittingSameBlockTwoStores(t *testing.T) { 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()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() err = asyncLoader.StartRequest(requestID1, "") require.NoError(t, err) err = asyncLoader.StartRequest(requestID2, "other") @@ -300,8 +299,8 @@ func TestRequestSplittingSameBlockOnlyOneResponse(t *testing.T) { 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()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() err = asyncLoader.StartRequest(requestID1, "") require.NoError(t, err) err = asyncLoader.StartRequest(requestID2, "other") diff --git a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go b/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go index ae992711..9c83a426 100644 --- a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go +++ b/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go @@ -3,7 +3,6 @@ package loadattemptqueue import ( "context" "fmt" - "math/rand" "testing" "time" @@ -31,7 +30,7 @@ func TestAsyncLoadInitialLoadSucceeds(t *testing.T) { link := testutil.NewTestLink() linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] resultChan := make(chan types.AsyncLoadResult, 1) @@ -61,7 +60,7 @@ func TestAsyncLoadInitialLoadFails(t *testing.T) { link := testutil.NewTestLink() linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() resultChan := make(chan types.AsyncLoadResult, 1) p := testutil.GeneratePeers(1)[0] @@ -95,7 +94,7 @@ func TestAsyncLoadInitialLoadIndeterminateRetryFalse(t *testing.T) { link := testutil.NewTestLink() linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] resultChan := make(chan types.AsyncLoadResult, 1) @@ -130,7 +129,7 @@ func TestAsyncLoadInitialLoadIndeterminateRetryTrueThenRetriedSuccess(t *testing link := testutil.NewTestLink() linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() resultChan := make(chan types.AsyncLoadResult, 1) p := testutil.GeneratePeers(1)[0] lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) @@ -167,7 +166,7 @@ func TestAsyncLoadInitialLoadIndeterminateThenRequestFinishes(t *testing.T) { link := testutil.NewTestLink() linkContext := ipld.LinkContext{} - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() resultChan := make(chan types.AsyncLoadResult, 1) p := testutil.GeneratePeers(1)[0] lr := NewLoadRequest(p, requestID, link, linkContext, resultChan) diff --git a/requestmanager/asyncloader/responsecache/responsecache_test.go b/requestmanager/asyncloader/responsecache/responsecache_test.go index 438fdc30..7034c13a 100644 --- a/requestmanager/asyncloader/responsecache/responsecache_test.go +++ b/requestmanager/asyncloader/responsecache/responsecache_test.go @@ -3,7 +3,6 @@ package responsecache import ( "context" "fmt" - "math/rand" "testing" blocks "github.com/ipfs/go-block-format" @@ -59,8 +58,8 @@ func (ubs *fakeUnverifiedBlockStore) blocks() []blocks.Block { func TestResponseCacheManagingLinks(t *testing.T) { blks := testutil.GenerateBlocksOfSize(5, 100) - requestID1 := graphsync.RequestID(rand.Int31()) - requestID2 := graphsync.RequestID(rand.Int31()) + requestID1 := graphsync.NewRequestID() + requestID2 := graphsync.NewRequestID() request1Metadata := metadata.Metadata{ metadata.Item{ diff --git a/requestmanager/client.go b/requestmanager/client.go index c58cad78..f579842f 100644 --- a/requestmanager/client.go +++ b/requestmanager/client.go @@ -96,7 +96,6 @@ type RequestManager struct { maxLinksPerRequest uint64 // dont touch out side of run loop - nextRequestID graphsync.RequestID inProgressRequestStatuses map[graphsync.RequestID]*inProgressRequestStatus requestHooks RequestHooks responseHooks ResponseHooks diff --git a/requestmanager/executor/executor_test.go b/requestmanager/executor/executor_test.go index 1b181ad1..2346ddc8 100644 --- a/requestmanager/executor/executor_test.go +++ b/requestmanager/executor/executor_test.go @@ -195,7 +195,7 @@ func TestRequestExecutionBlockChain(t *testing.T) { persistence := testutil.NewTestStore(make(map[ipld.Link][]byte)) tbc := testutil.SetupBlockChain(ctx, t, persistence, 100, 10) fal := testloader.NewFakeAsyncLoader() - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] requestCtx, requestCancel := context.WithCancel(ctx) defer requestCancel() diff --git a/requestmanager/hooks/hooks_test.go b/requestmanager/hooks/hooks_test.go index 4f008f09..2df87541 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" @@ -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] @@ -111,7 +110,7 @@ func TestBlockHookProcessing(t *testing.T) { Name: extensionName, Data: extensionUpdateData, } - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, extensionResponse) p := testutil.GeneratePeers(1)[0] @@ -208,7 +207,7 @@ func TestResponseHookProcessing(t *testing.T) { Name: extensionName, Data: extensionUpdateData, } - requestID := graphsync.RequestID(rand.Int31()) + requestID := graphsync.NewRequestID() response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, extensionResponse) p := testutil.GeneratePeers(1)[0] diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index e107ae38..1ede3592 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "sort" "testing" "time" @@ -1039,9 +1038,12 @@ func readNNetworkRequests(ctx context.Context, } // 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() - }) + // TODO: howdo? + /* + sort.Slice(requestRecords, func(i, j int) bool { + return requestRecords[i].gsr.ID() < requestRecords[j].gsr.ID() + }) + */ return requestRecords } diff --git a/requestmanager/server.go b/requestmanager/server.go index 63de7a91..ebd72f94 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -55,10 +55,9 @@ 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() diff --git a/responsemanager/hooks/hooks_test.go b/responsemanager/hooks/hooks_test.go index e8a56903..3d0e7812 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" @@ -54,7 +53,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] @@ -232,7 +231,7 @@ func TestBlockHookProcessing(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] @@ -315,7 +314,7 @@ func TestUpdateHookProcessing(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) update := gsmsg.UpdateRequest(requestID, extensionUpdate) diff --git a/responsemanager/queryexecutor/queryexecutor_test.go b/responsemanager/queryexecutor/queryexecutor_test.go index 3711b5b9..937bc79c 100644 --- a/responsemanager/queryexecutor/queryexecutor_test.go +++ b/responsemanager/queryexecutor/queryexecutor_test.go @@ -274,7 +274,7 @@ func newTestData(t *testing.T, blockCount int, expectedTraverse int) (*testData, td.manager = &fauxManager{ctx: ctx, t: t, expectedStartTask: td.task} 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) diff --git a/responsemanager/responseassembler/responseassembler_test.go b/responsemanager/responseassembler/responseassembler_test.go index b9f26c6b..e8cc1f03 100644 --- a/responsemanager/responseassembler/responseassembler_test.go +++ b/responsemanager/responseassembler/responseassembler_test.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "math/rand" "testing" "time" @@ -27,9 +26,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 +138,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 +174,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 { @@ -222,7 +221,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 +260,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 +335,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 +426,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..9328bea4 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -1142,7 +1142,7 @@ func newTestData(t *testing.T) testData { 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), } diff --git a/responsemanager/server.go b/responsemanager/server.go index 16b8c889..56456866 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -68,7 +68,7 @@ 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.String("id", update.ID().String()), attribute.StringSlice("extensions", update.ExtensionNames()), )) @@ -200,7 +200,7 @@ 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()), From cf6009ac0759813d5f9206eb28342e809b394bef Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 14 Dec 2021 15:40:43 +1100 Subject: [PATCH 03/32] fix(requestmanager): make collect test requests with uuids sortable --- requestmanager/requestmanager_test.go | 79 ++++++++++++++------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index 1ede3592..f63c2b09 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -41,7 +41,7 @@ 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()) @@ -130,7 +130,7 @@ 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()) @@ -148,7 +148,7 @@ func TestCancelRequestInProgress(t *testing.T) { 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, requestRecords[0].gsr.ID(), rr.gsr.ID()) @@ -194,7 +194,7 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { _, 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()) @@ -215,7 +215,7 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { 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, requestRecords[0].gsr.ID(), rr.gsr.ID()) @@ -243,7 +243,7 @@ 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) @@ -275,7 +275,7 @@ 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()) @@ -299,7 +299,7 @@ func TestLocallyFulfilledFirstRequestFailsLater(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] // async loaded response responds immediately td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) @@ -330,7 +330,7 @@ func TestLocallyFulfilledFirstRequestSucceedsLater(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] // async loaded response responds immediately td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) @@ -358,7 +358,7 @@ 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) firstResponses := []gsmsg.GraphSyncResponse{ @@ -436,7 +436,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) @@ -474,7 +474,7 @@ func TestEncodingExtensions(t *testing.T) { 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") @@ -510,7 +510,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") @@ -550,7 +550,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) @@ -602,7 +602,7 @@ func TestBlockHooks(t *testing.T) { }) 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") @@ -666,7 +666,7 @@ func TestBlockHooks(t *testing.T) { }) 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") @@ -715,7 +715,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) @@ -773,7 +773,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 { @@ -836,7 +836,7 @@ 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) @@ -862,7 +862,7 @@ func TestPauseResume(t *testing.T) { <-holdForPause // read the outgoing cancel request - pauseCancel := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + pauseCancel := readNNetworkRequests(requestCtx, t, td, 1)[0] require.True(t, pauseCancel.gsr.IsCancel()) // verify no further responses come through @@ -875,7 +875,7 @@ func TestPauseResume(t *testing.T) { require.NoError(t, err) // verify the correct new request with Do-no-send-cids & other extensions - resumedRequest := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + resumedRequest := readNNetworkRequests(requestCtx, t, td, 1)[0] doNotSendFirstBlocksData, has := resumedRequest.gsr.Extension(graphsync.ExtensionsDoNotSendFirstBlocks) doNotSendFirstBlocks, err := donotsendfirstblocks.DecodeDoNotSendFirstBlocks(doNotSendFirstBlocksData) require.NoError(t, err) @@ -922,7 +922,7 @@ 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) @@ -942,7 +942,7 @@ func TestPauseResumeExternal(t *testing.T) { <-holdForPause // read the outgoing cancel request - pauseCancel := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + pauseCancel := readNNetworkRequests(requestCtx, t, td, 1)[0] require.True(t, pauseCancel.gsr.IsCancel()) // verify no further responses come through @@ -955,7 +955,7 @@ func TestPauseResumeExternal(t *testing.T) { require.NoError(t, err) // verify the correct new request with Do-no-send-cids & other extensions - resumedRequest := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] + resumedRequest := readNNetworkRequests(requestCtx, t, td, 1)[0] doNotSendFirstBlocksData, has := resumedRequest.gsr.Extension(graphsync.ExtensionsDoNotSendFirstBlocks) doNotSendFirstBlocks, err := donotsendfirstblocks.DecodeDoNotSendFirstBlocks(doNotSendFirstBlocksData) require.NoError(t, err) @@ -991,7 +991,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) @@ -1026,25 +1026,22 @@ 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 - // TODO: howdo? - /* - 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 { @@ -1091,6 +1088,7 @@ type testData struct { outgoingRequestProcessingListeners *listeners.OutgoingRequestProcessingListeners taskqueue *taskqueue.WorkerTaskQueue executor *executor.Executor + requestIds []graphsync.RequestID } func newTestData(ctx context.Context, t *testing.T) *testData { @@ -1127,5 +1125,8 @@ func newTestData(ctx context.Context, t *testing.T) *testData { 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 } From af9dd524b922f3453b6320860143e157d690f5d3 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 14 Dec 2021 17:16:19 +1100 Subject: [PATCH 04/32] fix(requestid): print requestids as string uuids in logs --- impl/graphsync_test.go | 4 ++-- requestmanager/asyncloader/asyncloader.go | 6 +++--- .../asyncloader/responsecache/responsecache.go | 2 +- requestmanager/server.go | 12 ++++++------ requestmanager/utils.go | 4 ++-- responsemanager/server.go | 10 +++++----- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 9ae71554..003de85b 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -1846,8 +1846,8 @@ func processResponsesTraces(t *testing.T, tracing *testutil.Collector, responseC 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)) + if len(testutil.AttributeValueInTraceSpan(t, *finalStub, "requestIDs").AsStringSlice()) == 0 { + return append(traces, fmt.Sprintf("responseMessage(%d)->loaderProcess(0)", responseCount-1)) } return append(traces, fmt.Sprintf("processResponses(%d)->loaderProcess(0)->cacheProcess(0)", responseCount-1)) } diff --git a/requestmanager/asyncloader/asyncloader.go b/requestmanager/asyncloader/asyncloader.go index 5609b4e1..ea478099 100644 --- a/requestmanager/asyncloader/asyncloader.go +++ b/requestmanager/asyncloader/asyncloader.go @@ -111,12 +111,12 @@ func (al *AsyncLoader) ProcessResponse( responses map[graphsync.RequestID]metadata.Metadata, blks []blocks.Block) { - requestIds := make([]int, 0, len(responses)) + requestIds := make([]string, 0, len(responses)) for requestID := range responses { - requestIds = append(requestIds, int(requestID)) + requestIds = append(requestIds, requestID.String()) } ctx, span := otel.Tracer("graphsync").Start(ctx, "loaderProcess", trace.WithAttributes( - attribute.IntSlice("requestIDs", requestIds), + attribute.StringSlice("requestIDs", requestIds), )) defer span.End() diff --git a/requestmanager/asyncloader/responsecache/responsecache.go b/requestmanager/asyncloader/responsecache/responsecache.go index 69c3e902..a490178b 100644 --- a/requestmanager/asyncloader/responsecache/responsecache.go +++ b/requestmanager/asyncloader/responsecache/responsecache.go @@ -91,7 +91,7 @@ func (rc *ResponseCache) ProcessResponse( for requestID, md := range responses { for _, item := range md { - log.Debugf("Traverse link %s on request ID %d", item.Link.String(), requestID) + log.Debugf("Traverse link %s on request ID %s", item.Link.String(), requestID.String()) rc.linkTracker.RecordLinkTraversal(requestID, cidlink.Link{Cid: item.Link}, item.BlockPresent) } } diff --git a/requestmanager/server.go b/requestmanager/server.go index ebd72f94..d579a5e6 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -61,7 +61,7 @@ func (rm *RequestManager) newRequest(parentSpan trace.Span, p peer.ID, root ipld 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) if err != nil { @@ -111,7 +111,7 @@ 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 { @@ -224,7 +224,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) } @@ -261,13 +261,13 @@ func (rm *RequestManager) cancelOnError(requestID graphsync.RequestID, ipr *inPr 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), )) defer span.End() filteredResponses := rm.processExtensions(responses, p) diff --git a/requestmanager/utils.go b/requestmanager/utils.go index dc2a3102..60a94190 100644 --- a/requestmanager/utils.go +++ b/requestmanager/utils.go @@ -13,12 +13,12 @@ func metadataForResponses(responses []gsmsg.GraphSyncResponse) map[graphsync.Req 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()) + log.Warnf("Unable to decode metadata in response for request id: %s", response.RequestID().String()) continue } md, err := metadata.DecodeMetadata(mdRaw) if err != nil { - log.Warnf("Unable to decode metadata in response for request id: %d", response.RequestID()) + log.Warnf("Unable to decode metadata in response for request id: %s", response.RequestID().String()) continue } responseMetadata[response.RequestID()] = md diff --git a/responsemanager/server.go b/responsemanager/server.go index 56456866..1c0b45c2 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -59,7 +59,7 @@ func (rm *ResponseManager) terminateRequest(key responseKey) { func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, update gsmsg.GraphSyncRequest) { response, ok := rm.inProgressResponses[key] 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, peer %s, request ID %s", key.p.Pretty(), key.requestID.String()) return } @@ -215,10 +215,10 @@ 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()) + log.Infow("graphsync request initiated", "request id", request.ID().String(), "peer", p, "root", request.Root()) ipr, ok := rm.inProgressResponses[key] 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] = @@ -247,7 +247,7 @@ func (rm *ResponseManager) taskDataForKey(key responseKey) queryexecutor.Respons 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", key.requestID.String(), "peer", key.p, "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) @@ -298,7 +298,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", key.requestID.String(), "peer", key.p, "total time", time.Since(response.startTime)) if err != nil { response.span.RecordError(err) From a2a87feb3b7919551ab405d3137346c3c61dc7b5 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 15 Dec 2021 17:11:02 +1100 Subject: [PATCH 05/32] fix(requestid): use string as base type for RequestId --- graphsync.go | 7 ++++--- message/message.go | 1 - message/message_test.go | 4 ++-- peerstate/peerstate.go | 12 ++++++------ peerstate/peerstate_test.go | 16 ++++++++-------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/graphsync.go b/graphsync.go index 90d53de7..3e1f2ff1 100644 --- a/graphsync.go +++ b/graphsync.go @@ -13,7 +13,7 @@ import ( ) // RequestID is a unique identifier for a GraphSync request. -type RequestID uuid.UUID +type RequestID string // Tag returns an easy way to identify this request id as a graphsync request (for libp2p connections) func (r RequestID) Tag() string { @@ -22,12 +22,13 @@ func (r RequestID) Tag() string { // String form of a RequestID (should be a well-formed UUIDv4 string) func (r RequestID) String() string { - return uuid.UUID(r).String() + return uuid.Must(uuid.FromBytes([]byte(r))).String() } // Create a new, random RequestID (should be a UUIDv4) func NewRequestID() RequestID { - return RequestID(uuid.New()) + u := uuid.New() + return RequestID(u[:]) } // Priority a priority for a GraphSync request. diff --git a/message/message.go b/message/message.go index 227047ec..3bca0b38 100644 --- a/message/message.go +++ b/message/message.go @@ -3,7 +3,6 @@ package message import ( "encoding/binary" "errors" - "fmt" "io" "sort" diff --git a/message/message_test.go b/message/message_test.go index c866bcc4..2b87a810 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -53,7 +53,7 @@ func TestAppendingRequests(t *testing.T) { require.NoError(t, err) pbRequest := pbMessage.Requests[0] - require.Equal(t, id[:], pbRequest.Id) + require.Equal(t, []byte(id), pbRequest.Id) require.Equal(t, int32(priority), pbRequest.Priority) require.False(t, pbRequest.Cancel) require.False(t, pbRequest.Update) @@ -105,7 +105,7 @@ func TestAppendingResponses(t *testing.T) { pbMessage, err := gsm.ToProto() require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] - require.Equal(t, requestID[:], pbResponse.Id) + require.Equal(t, []byte(requestID), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, extensionBytes, pbResponse.Extensions["graphsync/awesome"]) 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 aa4e860a..46e8efd7 100644 --- a/peerstate/peerstate_test.go +++ b/peerstate/peerstate_test.go @@ -47,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": { @@ -63,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": { @@ -79,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": { @@ -95,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": { @@ -111,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": { @@ -127,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())}, }, }, } From d25c6ef245817d21a2020981367c2a18de02695b Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 16 Dec 2021 12:27:39 +1100 Subject: [PATCH 06/32] chore(requestid): wrap requestid string in a struct --- graphsync.go | 20 +++++++++++++++++--- message/message.go | 1 - message/message_test.go | 4 ++-- responsemanager/responsemanager_test.go | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/graphsync.go b/graphsync.go index 3e1f2ff1..f79463f6 100644 --- a/graphsync.go +++ b/graphsync.go @@ -13,7 +13,7 @@ import ( ) // RequestID is a unique identifier for a GraphSync request. -type RequestID string +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 { @@ -22,13 +22,27 @@ func (r RequestID) Tag() 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() + 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(u[:]) + 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. diff --git a/message/message.go b/message/message.go index 3bca0b38..3be35fe4 100644 --- a/message/message.go +++ b/message/message.go @@ -6,7 +6,6 @@ import ( "io" "sort" - "github.com/google/uuid" blocks "github.com/ipfs/go-block-format" cid "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" diff --git a/message/message_test.go b/message/message_test.go index 2b87a810..6163fc05 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -53,7 +53,7 @@ func TestAppendingRequests(t *testing.T) { require.NoError(t, err) pbRequest := pbMessage.Requests[0] - require.Equal(t, []byte(id), pbRequest.Id) + require.Equal(t, id.Bytes(), pbRequest.Id) require.Equal(t, int32(priority), pbRequest.Priority) require.False(t, pbRequest.Cancel) require.False(t, pbRequest.Update) @@ -105,7 +105,7 @@ func TestAppendingResponses(t *testing.T) { pbMessage, err := gsm.ToProto() require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] - require.Equal(t, []byte(requestID), pbResponse.Id) + require.Equal(t, requestID.Bytes(), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, extensionBytes, pbResponse.Extensions["graphsync/awesome"]) diff --git a/responsemanager/responsemanager_test.go b/responsemanager/responsemanager_test.go index 9328bea4..045512a6 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -1321,7 +1321,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) From 901b2c0f03da6832310d5f94f895b10718852ec4 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 11 Jan 2022 13:31:00 +1100 Subject: [PATCH 07/32] feat(libp2p): add v1.0.0 network compatibility --- benchmarks/testnet/virtual.go | 2 +- impl/graphsync_test.go | 243 +++++++++------- message/message.go | 230 ++++++--------- message/message_test.go | 28 +- message/messagehandler.go | 431 ++++++++++++++++++++++++++++ message/pb/message.pb.go | 5 +- message/pb/message_v1_0_0.pb.go | 479 ++++++++++++++++++++++++++++++++ message/pb/message_v1_0_0.proto | 36 +++ network/interface.go | 3 +- network/libp2p_impl.go | 80 +++++- 10 files changed, 1248 insertions(+), 289 deletions(-) create mode 100644 message/messagehandler.go create mode 100644 message/pb/message_v1_0_0.pb.go create mode 100644 message/pb/message_v1_0_0.proto diff --git a/benchmarks/testnet/virtual.go b/benchmarks/testnet/virtual.go index 024f4dc9..063feca8 100644 --- a/benchmarks/testnet/virtual.go +++ b/benchmarks/testnet/virtual.go @@ -137,7 +137,7 @@ func (n *network) SendMessage( rateLimiters[to] = rateLimiter } - pbMsg, err := mes.ToProto() + pbMsg, err := gsmsg.NewMessageHandler().ToProto(mes) if err != nil { return err } diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 003de85b..8a81ea89 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -36,6 +36,7 @@ import ( "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" @@ -51,6 +52,22 @@ import ( "github.com/ipfs/go-graphsync/testutil" ) +// 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 +}{ + "(v1.1 -> v1.1)": {nil, nil}, + "(v1.0 -> v1.1)": {[]protocol.ID{gsnet.ProtocolGraphsync_1_0_0}, nil}, + "(v1.1 -> 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 TestMakeRequestToNetwork(t *testing.T) { // create network @@ -271,7 +288,6 @@ func TestGraphsyncRoundTripRequestBudgetRequestor(t *testing.T) { } func TestGraphsyncRoundTripRequestBudgetResponder(t *testing.T) { - // create network ctx := context.Background() ctx, collectTracing := testutil.SetupTracing(ctx) @@ -319,112 +335,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 []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 + } + }) + + 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 + + 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 + } + } + 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) - } - }) + // each cacheProcess span should be linked to one verifyBlock span per block it stored - 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 - - 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 _, 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") } - } - 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") + }) } } @@ -1764,6 +1783,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) @@ -1776,8 +1799,16 @@ 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) @@ -1847,7 +1878,7 @@ func processResponsesTraces(t *testing.T, tracing *testutil.Collector, responseC finalStub := tracing.FindSpanByTraceString(fmt.Sprintf("processResponses(%d)->loaderProcess(0)", responseCount-1)) require.NotNil(t, finalStub) if len(testutil.AttributeValueInTraceSpan(t, *finalStub, "requestIDs").AsStringSlice()) == 0 { - return append(traces, fmt.Sprintf("responseMessage(%d)->loaderProcess(0)", responseCount-1)) + return append(traces, fmt.Sprintf("processResponses(%d)->loaderProcess(0)", responseCount-1)) } return append(traces, fmt.Sprintf("processResponses(%d)->loaderProcess(0)->cacheProcess(0)", responseCount-1)) } diff --git a/message/message.go b/message/message.go index 3be35fe4..8c6c9218 100644 --- a/message/message.go +++ b/message/message.go @@ -1,8 +1,8 @@ package message import ( - "encoding/binary" - "errors" + "bytes" + "fmt" "io" "sort" @@ -17,7 +17,6 @@ import ( "google.golang.org/protobuf/proto" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/ipldutil" pb "github.com/ipfs/go-graphsync/message/pb" ) @@ -71,6 +70,25 @@ type GraphSyncMetadatum struct { BlockPresent bool } +// String returns a human-readable form of a GraphSyncRequest +func (gsr GraphSyncRequest) String() string { + var buf bytes.Buffer + dagjson.Encode(gsr.selector, &buf) + ext := make([]string, 0) + for s := range gsr.extensions { + ext = append(ext, s) + } + return fmt.Sprintf("GraphSyncRequest", + gsr.root.String(), + buf.String(), + gsr.priority, + gsr.id.String(), + gsr.isCancel, + gsr.isUpdate, + strings.Join(ext, "|"), + ) +} + // GraphSyncResponse is an struct to capture data on a response sent back // in a GraphSyncMessage. type GraphSyncResponse struct { @@ -119,12 +137,43 @@ func BlockFormatSlice(bs []GraphSyncBlock) []blocks.Block { return blks } +// String returns a human-readable form of a GraphSyncResponse +func (gsr GraphSyncResponse) String() string { + ext := make([]string, 0) + for s := range gsr.extensions { + ext = append(ext, s) + } + return fmt.Sprintf("GraphSyncResponse", + gsr.requestID.String(), + gsr.status, + strings.Join(ext, "|"), + ) +} + +// GraphSyncMessage is the internal representation form of a message sent or +// received over the wire type GraphSyncMessage struct { Requests []GraphSyncRequest Responses []GraphSyncResponse Blocks []GraphSyncBlock } +// 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 Graphsync request func NewRequest(id graphsync.RequestID, root cid.Cid, @@ -208,62 +257,23 @@ func newResponse(requestID graphsync.RequestID, } } -func newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { - requests := make([]GraphSyncRequest, len(pbm.GetRequests())) - for i, 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 - } - } - // TODO: we likely need to turn some "core" extensions to fields, - // as some of those got moved to proper fields in the new protocol. - // Same for responses above, as well as the "to proto" funcs. - requests[i] = newRequest(graphsync.RequestID(req.Id), root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, fromProtoExtensions(req.GetExtensions())) - } - - responses := make([]GraphSyncResponse, len(pbm.GetResponses())) - for i, res := range pbm.Responses { - if res == nil { - return GraphSyncMessage{}, errors.New("response is nil") - } - responses[i] = newResponse(graphsync.RequestID(res.Id), graphsync.ResponseStatusCode(res.Status), fromProtoExtensions(res.GetExtensions())) - } - - blks := make([]GraphSyncBlock, len(pbm.GetData())) - for i, b := range pbm.GetData() { - if b == nil { - return GraphSyncMessage{}, errors.New("block is nil") - } - blks[i] = GraphSyncBlock{ - Prefix: b.GetPrefix(), - Data: b.GetData(), - } - } - - return GraphSyncMessage{ - requests, responses, blks, - }, nil -} - +// Empty returns true if this message has no actionable content func (gsm GraphSyncMessage) Empty() bool { return len(gsm.Blocks) == 0 && len(gsm.Requests) == 0 && len(gsm.Responses) == 0 } +// Requests returns a copy of the list of GraphSyncRequests in this +// GraphSyncMessage +func (gsm GraphSyncMessage) Requests() []GraphSyncRequest { + requests := make([]GraphSyncRequest, 0, len(gsm.requests)) + for _, request := range gsm.requests { + requests = append(requests, request) + } + 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 _, response := range gsm.Responses { @@ -272,107 +282,26 @@ func (gsm GraphSyncMessage) ResponseCodes() map[graphsync.RequestID]graphsync.Re return codes } -// 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 +// Responses returns a copy of the list of GraphSyncResponses in this +// GraphSyncMessage +func (gsm GraphSyncMessage) Responses() []GraphSyncResponse { + responses := make([]GraphSyncResponse, 0, len(gsm.responses)) + for _, response := range gsm.responses { + responses = append(responses, response) } - - return newMessageFromProto(&pb) + return responses } -func toProtoExtensions(m GraphSyncExtensions) map[string][]byte { - protoExts := make(map[string][]byte, len(m.Values)) - for name, node := range m.Values { - // Only keep those which are plain bytes, - // as those are the only ones that the older protocol clients understand. - if node.Kind() != ipld.Kind_Bytes { - continue - } - raw, err := node.AsBytes() - if err != nil { - panic(err) // shouldn't happen - } - protoExts[name] = raw - } - return protoExts -} - -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: request.ID[:], - Root: request.Root.Bytes(), - Selector: selector, - Priority: int32(request.Priority), - Cancel: request.Cancel, - Update: request.Update, - Extensions: toProtoExtensions(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: response.ID[:], - Status: int32(response.Status), - Extensions: toProtoExtensions(response.Extensions), - }) - } - - pbm.Data = make([]*pb.Message_Block, 0, len(gsm.Blocks)) - for _, b := range gsm.Blocks { - pbm.Data = append(pbm.Data, &pb.Message_Block{ - Prefix: b.Prefix, - Data: b.Data, - }) - } - 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 +// Blocks returns a copy of the list of Blocks in this GraphSyncMessage +func (gsm GraphSyncMessage) Blocks() []blocks.Block { + bs := make([]blocks.Block, 0, len(gsm.blocks)) + for _, block := range gsm.blocks { + bs = append(bs, block) } - _, err = w.Write(out) - return err + return bs } +// Loggable returns a simplified, single-line log form of this GraphSyncMessage func (gsm GraphSyncMessage) Loggable() map[string]interface{} { requests := make([]string, 0, len(gsm.Requests)) for _, request := range gsm.Requests { @@ -388,6 +317,7 @@ func (gsm GraphSyncMessage) Loggable() map[string]interface{} { } } +// Clone returns a shallow copy of this GraphSyncMessage func (gsm GraphSyncMessage) Clone() GraphSyncMessage { requests := append([]GraphSyncRequest{}, gsm.Requests...) responses := append([]GraphSyncResponse{}, gsm.Responses...) diff --git a/message/message_test.go b/message/message_test.go index 6163fc05..130767ef 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -47,7 +47,7 @@ func TestAppendingRequests(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := gsm.ToProto() + pbMessage, err := NewMessageHandler().ToProto(gsm) require.NoError(t, err, "serialize to protobuf errored") selectorEncoded, err := ipldutil.EncodeNode(selector) require.NoError(t, err) @@ -61,7 +61,7 @@ func TestAppendingRequests(t *testing.T) { require.Equal(t, selectorEncoded, pbRequest.Selector) require.Equal(t, map[string][]byte{"graphsync/awesome": extensionBytes}, pbRequest.Extensions) - deserialized, err := newMessageFromProto(pbMessage) + deserialized, err := NewMessageHandler().newMessageFromProto(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") deserializedRequests := deserialized.Requests require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") @@ -102,14 +102,14 @@ func TestAppendingResponses(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := gsm.ToProto() + pbMessage, err := NewMessageHandler().ToProto(gsm) require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] require.Equal(t, requestID.Bytes(), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, extensionBytes, pbResponse.Extensions["graphsync/awesome"]) - deserialized, err := newMessageFromProto(pbMessage) + deserialized, err := NewMessageHandler().newMessageFromProto(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") deserializedResponses := deserialized.Responses require.Len(t, deserializedResponses, 1, "did not add response to deserialized message") @@ -135,7 +135,7 @@ func TestAppendBlock(t *testing.T) { m, err := builder.Build() require.NoError(t, err) - pbMessage, err := m.ToProto() + pbMessage, err := NewMessageHandler().ToProto(m) require.NoError(t, err, "serializing to protobuf errored") // assert strings are in proto message @@ -174,9 +174,9 @@ func TestRequestCancel(t *testing.T) { require.True(t, request.Cancel) buf := new(bytes.Buffer) - err = gsm.ToNet(buf) + err = NewMessageHandler().ToNet(gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := FromNet(buf) + deserialized, err := NewMessageHandler().FromNet(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") @@ -210,9 +210,9 @@ func TestRequestUpdate(t *testing.T) { require.Equal(t, extension.Data, extensionData) buf := new(bytes.Buffer) - err = gsm.ToNet(buf) + err = NewMessageHandler().ToNet(gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := FromNet(buf) + deserialized, err := NewMessageHandler().FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") deserializedRequests := deserialized.Requests @@ -254,9 +254,9 @@ func TestToNetFromNetEquivalency(t *testing.T) { require.NoError(t, err) buf := new(bytes.Buffer) - err = gsm.ToNet(buf) + err = NewMessageHandler().ToNet(gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := FromNet(buf) + deserialized, err := NewMessageHandler().FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") requests := gsm.Requests @@ -411,15 +411,15 @@ func TestKnownFuzzIssues(t *testing.T) { for _, input := range inputs { //inputAsBytes, err := hex.DecodeString(input) ///require.NoError(t, err) - msg1, err := FromNet(bytes.NewReader([]byte(input))) + msg1, err := NewMessageHandler().FromNet(bytes.NewReader([]byte(input))) if err != nil { continue } buf2 := new(bytes.Buffer) - err = msg1.ToNet(buf2) + err = NewMessageHandler().ToNet(msg1, buf2) require.NoError(t, err) - msg2, err := FromNet(buf2) + msg2, err := NewMessageHandler().FromNet(buf2) require.NoError(t, err) require.Equal(t, msg1, msg2) diff --git a/message/messagehandler.go b/message/messagehandler.go new file mode 100644 index 00000000..ea15e32d --- /dev/null +++ b/message/messagehandler.go @@ -0,0 +1,431 @@ +package message + +import ( + "encoding/binary" + "errors" + "io" + "sync" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/ipldutil" + pb "github.com/ipfs/go-graphsync/message/pb" + "github.com/ipld/go-ipld-prime" + 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" +) + +type v1RequestKey struct { + p peer.ID + id int32 +} + +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 network stream to deserialized a GraphSyncMessage +func (mh *MessageHandler) FromNet(r io.Reader) (GraphSyncMessage, error) { + reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) + return mh.FromMsgReader(reader) +} + +// FromMsgReader can deserialize a protobuf message into a GraphySyncMessage. +func (mh *MessageHandler) 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 mh.newMessageFromProto(&pb) +} + +// FromMsgReaderV1 can deserialize a v1.0.0 protobuf message into a GraphySyncMessage. +func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncMessage, error) { + msg, err := r.ReadMsg() + if err != nil { + return GraphSyncMessage{}, err + } + + var pb pb.Message_V1_0_0 + err = proto.Unmarshal(msg, &pb) + r.ReleaseMsg(msg) + if err != nil { + return GraphSyncMessage{}, err + } + + return mh.newMessageFromProtoV1(p, &pb) +} + +// ToProto converts a GraphSyncMessage to its pb.Message equivalent +func (mh *MessageHandler) ToProto(gsm GraphSyncMessage) (*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: request.id.Bytes(), + Root: request.root.Bytes(), + Selector: selector, + Priority: int32(request.priority), + Cancel: request.isCancel, + Update: request.isUpdate, + Extensions: toProtoExtensions(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: response.requestID.Bytes(), + Status: int32(response.status), + Extensions: toProtoExtensions(request.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 +} + +// ToProtoV1 converts a GraphSyncMessage to its pb.Message_V1_0_0 equivalent +func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Message_V1_0_0, error) { + mh.mapLock.Lock() + defer mh.mapLock.Unlock() + + pbm := new(pb.Message_V1_0_0) + pbm.Requests = make([]*pb.Message_V1_0_0_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 + } + } + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, request.id.Bytes()) + if err != nil { + return nil, err + } + pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ + Id: rid, + Root: request.root.Bytes(), + Selector: selector, + Priority: int32(request.priority), + Cancel: request.isCancel, + Update: request.isUpdate, + Extensions: request.extensions, + }) + } + + pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(gsm.responses)) + for _, response := range gsm.responses { + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.requestID.Bytes()) + if err != nil { + return nil, err + } + pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ + Id: rid, + Status: int32(response.status), + Extensions: response.extensions, + }) + } + + blocks := gsm.Blocks() + pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(blocks)) + for _, b := range blocks { + pbm.Data = append(pbm.Data, &pb.Message_V1_0_0_Block{ + Data: b.RawData(), + Prefix: b.Cid().Prefix().Bytes(), + }) + } + return pbm, nil +} + +// ToNet writes a GraphSyncMessage in its protobuf format to a writer +func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { + msg, err := mh.ToProto(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 +} + +// ToNet writes a GraphSyncMessage in its v1.0.0 protobuf format to a writer +func (mh *MessageHandler) ToNetV1(p peer.ID, gsm GraphSyncMessage, w io.Writer) error { + msg, err := mh.ToProtoV1(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 +} + +func toProtoExtensions(m GraphSyncExtensions) map[string][]byte { + protoExts := make(map[string][]byte, len(m.Values)) + for name, node := range m.Values { + // Only keep those which are plain bytes, + // as those are the only ones that the older protocol clients understand. + if node.Kind() != ipld.Kind_Bytes { + continue + } + raw, err := node.AsBytes() + if err != nil { + panic(err) // shouldn't happen + } + protoExts[name] = raw + } + return protoExts +} + + +// 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 +} + +// Mapping from a pb.Message object to a GraphSyncMessage object +func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.Requests)) + 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.Extensions + if exts == nil { + exts = make(map[string][]byte) + } + id, err := graphsync.ParseRequestID(req.Id) + if err != nil { + return GraphSyncMessage{}, err + } + requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) + } + + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.Responses)) + for _, res := range pbm.Responses { + if res == nil { + return GraphSyncMessage{}, errors.New("response is nil") + } + exts := res.Extensions + if exts == nil { + exts = make(map[string][]byte) + } + id, err := graphsync.ParseRequestID(res.Id) + if err != nil { + return GraphSyncMessage{}, err + } + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) + } + + blks := make(map[cid.Cid]blocks.Block, len(pbm.Data)) + for _, b := range pbm.Data { + 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 +} + +// Mapping from a pb.Message_V1_0_0 object to a GraphSyncMessage object, including +// RequestID (int / uuid) mapping. +func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_0) (GraphSyncMessage, error) { + mh.mapLock.Lock() + defer mh.mapLock.Unlock() + + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.Requests)) + 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.Extensions + if exts == nil { + exts = make(map[string][]byte) + } + id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, req.Id) + if err != nil { + return GraphSyncMessage{}, err + } + requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) + } + + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.Responses)) + for _, res := range pbm.Responses { + if res == nil { + return GraphSyncMessage{}, errors.New("response is nil") + } + exts := res.Extensions + if exts == nil { + exts = make(map[string][]byte) + } + id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, res.Id) + if err != nil { + return GraphSyncMessage{}, err + } + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) + } + + blks := make(map[cid.Cid]blocks.Block, len(pbm.Data)) + for _, b := range pbm.Data { + 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 +} diff --git a/message/pb/message.pb.go b/message/pb/message.pb.go index 897027eb..7a55b06f 100644 --- a/message/pb/message.pb.go +++ b/message/pb/message.pb.go @@ -7,11 +7,10 @@ package graphsync_message_pb import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/message/pb/message_v1_0_0.pb.go b/message/pb/message_v1_0_0.pb.go new file mode 100644 index 00000000..6cacd3ec --- /dev/null +++ b/message/pb/message_v1_0_0.pb.go @@ -0,0 +1,479 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.19.1 +// source: message_V1_0_0.proto + +package graphsync_message_pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Message_V1_0_0 struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // the actual data included in this message + CompleteRequestList bool `protobuf:"varint,1,opt,name=completeRequestList,proto3" json:"completeRequestList,omitempty"` // This request list includes *all* requests, replacing outstanding requests. + Requests []*Message_V1_0_0_Request `protobuf:"bytes,2,rep,name=requests,proto3" json:"requests,omitempty"` // The list of requests. + Responses []*Message_V1_0_0_Response `protobuf:"bytes,3,rep,name=responses,proto3" json:"responses,omitempty"` // The list of responses. + Data []*Message_V1_0_0_Block `protobuf:"bytes,4,rep,name=data,proto3" json:"data,omitempty"` // Blocks related to the responses +} + +func (x *Message_V1_0_0) Reset() { + *x = Message_V1_0_0{} + if protoimpl.UnsafeEnabled { + mi := &file_message_V1_0_0_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message_V1_0_0) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message_V1_0_0) ProtoMessage() {} + +func (x *Message_V1_0_0) ProtoReflect() protoreflect.Message { + mi := &file_message_V1_0_0_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message_V1_0_0.ProtoReflect.Descriptor instead. +func (*Message_V1_0_0) Descriptor() ([]byte, []int) { + return file_message_V1_0_0_proto_rawDescGZIP(), []int{0} +} + +func (x *Message_V1_0_0) GetCompleteRequestList() bool { + if x != nil { + return x.CompleteRequestList + } + return false +} + +func (x *Message_V1_0_0) GetRequests() []*Message_V1_0_0_Request { + if x != nil { + return x.Requests + } + return nil +} + +func (x *Message_V1_0_0) GetResponses() []*Message_V1_0_0_Response { + if x != nil { + return x.Responses + } + return nil +} + +func (x *Message_V1_0_0) GetData() []*Message_V1_0_0_Block { + if x != nil { + return x.Data + } + return nil +} + +type Message_V1_0_0_Request struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // unique id set on the requester side + Root []byte `protobuf:"bytes,2,opt,name=root,proto3" json:"root,omitempty"` // a CID for the root node in the query + Selector []byte `protobuf:"bytes,3,opt,name=selector,proto3" json:"selector,omitempty"` // ipld selector to retrieve + Extensions map[string][]byte `protobuf:"bytes,4,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // aux information. useful for other protocols + Priority int32 `protobuf:"varint,5,opt,name=priority,proto3" json:"priority,omitempty"` // the priority (normalized). default to 1 + Cancel bool `protobuf:"varint,6,opt,name=cancel,proto3" json:"cancel,omitempty"` // whether this cancels a request + Update bool `protobuf:"varint,7,opt,name=update,proto3" json:"update,omitempty"` // whether this requests resumes a previous request +} + +func (x *Message_V1_0_0_Request) Reset() { + *x = Message_V1_0_0_Request{} + if protoimpl.UnsafeEnabled { + mi := &file_message_V1_0_0_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message_V1_0_0_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message_V1_0_0_Request) ProtoMessage() {} + +func (x *Message_V1_0_0_Request) ProtoReflect() protoreflect.Message { + mi := &file_message_V1_0_0_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message_V1_0_0_Request.ProtoReflect.Descriptor instead. +func (*Message_V1_0_0_Request) Descriptor() ([]byte, []int) { + return file_message_V1_0_0_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *Message_V1_0_0_Request) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Message_V1_0_0_Request) GetRoot() []byte { + if x != nil { + return x.Root + } + return nil +} + +func (x *Message_V1_0_0_Request) GetSelector() []byte { + if x != nil { + return x.Selector + } + return nil +} + +func (x *Message_V1_0_0_Request) GetExtensions() map[string][]byte { + if x != nil { + return x.Extensions + } + return nil +} + +func (x *Message_V1_0_0_Request) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *Message_V1_0_0_Request) GetCancel() bool { + if x != nil { + return x.Cancel + } + return false +} + +func (x *Message_V1_0_0_Request) GetUpdate() bool { + if x != nil { + return x.Update + } + return false +} + +type Message_V1_0_0_Response struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // the request id + Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"` // a status code. + Extensions map[string][]byte `protobuf:"bytes,3,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // additional data +} + +func (x *Message_V1_0_0_Response) Reset() { + *x = Message_V1_0_0_Response{} + if protoimpl.UnsafeEnabled { + mi := &file_message_V1_0_0_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message_V1_0_0_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message_V1_0_0_Response) ProtoMessage() {} + +func (x *Message_V1_0_0_Response) ProtoReflect() protoreflect.Message { + mi := &file_message_V1_0_0_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message_V1_0_0_Response.ProtoReflect.Descriptor instead. +func (*Message_V1_0_0_Response) Descriptor() ([]byte, []int) { + return file_message_V1_0_0_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *Message_V1_0_0_Response) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *Message_V1_0_0_Response) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *Message_V1_0_0_Response) GetExtensions() map[string][]byte { + if x != nil { + return x.Extensions + } + return nil +} + +type Message_V1_0_0_Block struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Prefix []byte `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` // CID prefix (cid version, multicodec and multihash prefix (type + length) + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *Message_V1_0_0_Block) Reset() { + *x = Message_V1_0_0_Block{} + if protoimpl.UnsafeEnabled { + mi := &file_message_V1_0_0_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message_V1_0_0_Block) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message_V1_0_0_Block) ProtoMessage() {} + +func (x *Message_V1_0_0_Block) ProtoReflect() protoreflect.Message { + mi := &file_message_V1_0_0_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message_V1_0_0_Block.ProtoReflect.Descriptor instead. +func (*Message_V1_0_0_Block) Descriptor() ([]byte, []int) { + return file_message_V1_0_0_proto_rawDescGZIP(), []int{0, 2} +} + +func (x *Message_V1_0_0_Block) GetPrefix() []byte { + if x != nil { + return x.Prefix + } + return nil +} + +func (x *Message_V1_0_0_Block) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +var File_message_V1_0_0_proto protoreflect.FileDescriptor + +var file_message_V1_0_0_proto_rawDesc = []byte{ + 0x0a, 0x14, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, 0x6e, + 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x22, 0xd6, 0x06, 0x0a, + 0x0e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x12, + 0x30, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x63, 0x6f, + 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x73, + 0x74, 0x12, 0x48, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, 0x6e, 0x63, 0x2e, + 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x4b, 0x0a, 0x09, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, + 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, + 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x09, 0x72, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, + 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xb2, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, + 0x73, 0x79, 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, + 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x3d, + 0x0a, 0x0f, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xd0, 0x01, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x5d, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, + 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, + 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x1a, 0x33, 0x0a, 0x05, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x42, 0x18, 0x5a, 0x16, 0x2e, 0x3b, 0x67, 0x72, 0x61, 0x70, 0x68, + 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x62, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_message_V1_0_0_proto_rawDescOnce sync.Once + file_message_V1_0_0_proto_rawDescData = file_message_V1_0_0_proto_rawDesc +) + +func file_message_V1_0_0_proto_rawDescGZIP() []byte { + file_message_V1_0_0_proto_rawDescOnce.Do(func() { + file_message_V1_0_0_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_V1_0_0_proto_rawDescData) + }) + return file_message_V1_0_0_proto_rawDescData +} + +var file_message_V1_0_0_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_message_V1_0_0_proto_goTypes = []interface{}{ + (*Message_V1_0_0)(nil), // 0: graphsync.message.pb.Message_V1_0_0 + (*Message_V1_0_0_Request)(nil), // 1: graphsync.message.pb.Message_V1_0_0.Request + (*Message_V1_0_0_Response)(nil), // 2: graphsync.message.pb.Message_V1_0_0.Response + (*Message_V1_0_0_Block)(nil), // 3: graphsync.message.pb.Message_V1_0_0.Block + nil, // 4: graphsync.message.pb.Message_V1_0_0.Request.ExtensionsEntry + nil, // 5: graphsync.message.pb.Message_V1_0_0.Response.ExtensionsEntry +} +var file_message_V1_0_0_proto_depIdxs = []int32{ + 1, // 0: graphsync.message.pb.Message_V1_0_0.requests:type_name -> graphsync.message.pb.Message_V1_0_0.Request + 2, // 1: graphsync.message.pb.Message_V1_0_0.responses:type_name -> graphsync.message.pb.Message_V1_0_0.Response + 3, // 2: graphsync.message.pb.Message_V1_0_0.data:type_name -> graphsync.message.pb.Message_V1_0_0.Block + 4, // 3: graphsync.message.pb.Message_V1_0_0.Request.extensions:type_name -> graphsync.message.pb.Message_V1_0_0.Request.ExtensionsEntry + 5, // 4: graphsync.message.pb.Message_V1_0_0.Response.extensions:type_name -> graphsync.message.pb.Message_V1_0_0.Response.ExtensionsEntry + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_message_V1_0_0_proto_init() } +func file_message_V1_0_0_proto_init() { + if File_message_V1_0_0_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_message_V1_0_0_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message_V1_0_0); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_message_V1_0_0_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message_V1_0_0_Request); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_message_V1_0_0_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message_V1_0_0_Response); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_message_V1_0_0_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message_V1_0_0_Block); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_message_V1_0_0_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_message_V1_0_0_proto_goTypes, + DependencyIndexes: file_message_V1_0_0_proto_depIdxs, + MessageInfos: file_message_V1_0_0_proto_msgTypes, + }.Build() + File_message_V1_0_0_proto = out.File + file_message_V1_0_0_proto_rawDesc = nil + file_message_V1_0_0_proto_goTypes = nil + file_message_V1_0_0_proto_depIdxs = nil +} diff --git a/message/pb/message_v1_0_0.proto b/message/pb/message_v1_0_0.proto new file mode 100644 index 00000000..81f26699 --- /dev/null +++ b/message/pb/message_v1_0_0.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package graphsync.message.pb; + +option go_package = ".;graphsync_message_pb"; + +message Message_V1_0_0 { + + message Request { + int32 id = 1; // unique id set on the requester side + bytes root = 2; // a CID for the root node in the query + bytes selector = 3; // ipld selector to retrieve + map extensions = 4; // aux information. useful for other protocols + int32 priority = 5; // the priority (normalized). default to 1 + bool cancel = 6; // whether this cancels a request + bool update = 7; // whether this requests resumes a previous request + } + + message Response { + int32 id = 1; // the request id + int32 status = 2; // a status code. + map extensions = 3; // additional data + } + + message Block { + bytes prefix = 1; // CID prefix (cid version, multicodec and multihash prefix (type + length) + bytes data = 2; + } + + // the actual data included in this message + bool completeRequestList = 1; // This request list includes *all* requests, replacing outstanding requests. + repeated Request requests = 2; // The list of requests. + repeated Response responses = 3; // The list of responses. + repeated Block data = 4; // Blocks related to the responses + +} diff --git a/network/interface.go b/network/interface.go index 2277cc08..413050d7 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_1_1_0 protocol.ID = "/ipfs/graphsync/1.1.0" ) // GraphSyncNetwork provides network connectivity for GraphSync. diff --git a/network/libp2p_impl.go b/network/libp2p_impl.go index 0b6a9a6c..12e7b891 100644 --- a/network/libp2p_impl.go +++ b/network/libp2p_impl.go @@ -10,6 +10,7 @@ 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" @@ -20,10 +21,27 @@ 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 { graphSyncNetwork := libp2pGraphSyncNetwork{ - host: host, + host: host, + messageHandler: gsmsg.NewMessageHandler(), + protocols: []protocol.ID{ProtocolGraphsync_1_1_0, ProtocolGraphsync_1_0_0}, + } + + for _, option := range options { + option(&graphSyncNetwork) } return &graphSyncNetwork @@ -34,12 +52,15 @@ func NewFromLibp2pHost(host host.Host) GraphSyncNetwork { type libp2pGraphSyncNetwork struct { host host.Host // inbound messages from the network are forwarded to the receiver - receiver Receiver + receiver Receiver + messageHandler *gsmsg.MessageHandler + protocols []protocol.ID } type streamMessageSender struct { - s network.Stream - opts MessageSenderOpts + s network.Stream + opts MessageSenderOpts + messageHandler *gsmsg.MessageHandler } func (s *streamMessageSender) Close() error { @@ -51,10 +72,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.messageHandler, 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 *gsmsg.MessageHandler, 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())) @@ -67,8 +88,13 @@ func msgToStream(ctx context.Context, s network.Stream, msg gsmsg.GraphSyncMessa } switch s.Protocol() { - case ProtocolGraphsync: - if err := msg.ToNet(s); err != nil { + case ProtocolGraphsync_1_0_0: + if err := mh.ToNetV1(s.Conn().RemotePeer(), msg, s); err != nil { + log.Debugf("error: %s", err) + return err + } + case ProtocolGraphsync_1_1_0: + if err := mh.ToNet(msg, s); err != nil { log.Debugf("error: %s", err) return err } @@ -88,11 +114,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), + messageHandler: gsnet.messageHandler, + }, 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 +135,7 @@ func (gsnet *libp2pGraphSyncNetwork) SendMessage( return err } - if err = msgToStream(ctx, s, outgoing, sendMessageTimeout); err != nil { + if err = msgToStream(ctx, s, gsnet.messageHandler, outgoing, sendMessageTimeout); err != nil { _ = s.Reset() return err } @@ -115,7 +145,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 +166,16 @@ func (gsnet *libp2pGraphSyncNetwork) handleNewStream(s network.Stream) { reader := msgio.NewVarintReaderSize(s, network.MessageSizeMax) for { - received, err := gsmsg.FromMsgReader(reader) + var received gsmsg.GraphSyncMessage + var err error + switch s.Protocol() { + case ProtocolGraphsync_1_0_0: + received, err = gsnet.messageHandler.FromMsgReaderV1(s.Conn().RemotePeer(), reader) + case ProtocolGraphsync_1_1_0: + received, err = gsnet.messageHandler.FromMsgReader(reader) + default: + err = fmt.Errorf("unexpected protocol version %s", s.Protocol()) + } p := s.Conn().RemotePeer() if err != nil { @@ -148,6 +189,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 +198,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_1_1_0: + gsnet.protocols = append([]protocol.ID{}, proto) + } + } +} + type libp2pGraphSyncNotifee libp2pGraphSyncNetwork func (nn *libp2pGraphSyncNotifee) libp2pGraphSyncNetwork() *libp2pGraphSyncNetwork { From caa4b58825e8a541e0990ba1a43877e9df8292f4 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 13 Jan 2022 15:45:43 +1100 Subject: [PATCH 08/32] chore(net): resolve most cbor + uuid merge problems --- go.mod | 2 +- message/bench_test.go | 6 +- message/message.go | 75 ++++------------- message/messagehandler.go | 168 +++++++++++++++----------------------- message/schema.ipldsch | 2 +- 5 files changed, 88 insertions(+), 165 deletions(-) diff --git a/go.mod b/go.mod index 492fd421..e7362e50 100644 --- a/go.mod +++ b/go.mod @@ -49,4 +49,4 @@ require ( google.golang.org/protobuf v1.27.1 ) -replace github.com/ipld/go-ipld-prime => ../../src/ipld +replace github.com/ipld/go-ipld-prime => ../../ipld/go-ipld-prime diff --git a/message/bench_test.go b/message/bench_test.go index 0c2d34cf..0b309914 100644 --- a/message/bench_test.go +++ b/message/bench_test.go @@ -25,7 +25,7 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { Name: extensionName, Data: basicnode.NewBytes(testutil.RandomBytes(100)), } - id := graphsync.RequestID(rand.Int31()) + id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) status := graphsync.RequestAcknowledged @@ -48,10 +48,10 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { for pb.Next() { buf.Reset() - err := gsm.ToNet(buf) + err := message.NewMessageHandler().ToNet(gsm, buf) require.NoError(b, err) - gsm2, err := message.FromNet(buf) + gsm2, err := message.NewMessageHandler().FromNet(buf) require.NoError(b, err) require.Equal(b, gsm, gsm2) } diff --git a/message/message.go b/message/message.go index 8c6c9218..cc38fb80 100644 --- a/message/message.go +++ b/message/message.go @@ -5,16 +5,14 @@ import ( "fmt" "io" "sort" + "strings" 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/codec/dagjson" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" - pool "github.com/libp2p/go-buffer-pool" - "github.com/libp2p/go-libp2p-core/network" - "github.com/libp2p/go-msgio" - "google.golang.org/protobuf/proto" "github.com/ipfs/go-graphsync" pb "github.com/ipfs/go-graphsync/message/pb" @@ -73,19 +71,15 @@ type GraphSyncMetadatum struct { // String returns a human-readable form of a GraphSyncRequest func (gsr GraphSyncRequest) String() string { var buf bytes.Buffer - dagjson.Encode(gsr.selector, &buf) - ext := make([]string, 0) - for s := range gsr.extensions { - ext = append(ext, s) - } + dagjson.Encode(gsr.Selector, &buf) return fmt.Sprintf("GraphSyncRequest", - gsr.root.String(), + gsr.Root.String(), buf.String(), - gsr.priority, - gsr.id.String(), - gsr.isCancel, - gsr.isUpdate, - strings.Join(ext, "|"), + gsr.Priority, + gsr.ID.String(), + gsr.Cancel, + gsr.Update, + strings.Join(gsr.ExtensionNames(), "|"), ) } @@ -139,14 +133,10 @@ func BlockFormatSlice(bs []GraphSyncBlock) []blocks.Block { // String returns a human-readable form of a GraphSyncResponse func (gsr GraphSyncResponse) String() string { - ext := make([]string, 0) - for s := range gsr.extensions { - ext = append(ext, s) - } return fmt.Sprintf("GraphSyncResponse", - gsr.requestID.String(), - gsr.status, - strings.Join(ext, "|"), + gsr.ID.String(), + gsr.Status, + strings.Join(gsr.ExtensionNames(), "|"), ) } @@ -162,14 +152,14 @@ type GraphSyncMessage struct { // its contents func (gsm GraphSyncMessage) String() string { cts := make([]string, 0) - for _, req := range gsm.requests { + for _, req := range gsm.Requests { cts = append(cts, req.String()) } - for _, resp := range gsm.responses { + for _, resp := range gsm.Responses { cts = append(cts, resp.String()) } - for c := range gsm.blocks { - cts = append(cts, fmt.Sprintf("Block<%s>", c.String())) + for _, c := range gsm.Blocks { + cts = append(cts, fmt.Sprintf("Block<%s>", c.BlockFormat().Cid().String())) } return fmt.Sprintf("GraphSyncMessage<\n\t%s\n>", strings.Join(cts, ",\n\t")) } @@ -262,16 +252,6 @@ func (gsm GraphSyncMessage) Empty() bool { return len(gsm.Blocks) == 0 && len(gsm.Requests) == 0 && len(gsm.Responses) == 0 } -// Requests returns a copy of the list of GraphSyncRequests in this -// GraphSyncMessage -func (gsm GraphSyncMessage) Requests() []GraphSyncRequest { - requests := make([]GraphSyncRequest, 0, len(gsm.requests)) - for _, request := range gsm.requests { - requests = append(requests, request) - } - return requests -} - // ResponseCodes returns a list of ResponseStatusCodes contained in the // responses in this GraphSyncMessage func (gsm GraphSyncMessage) ResponseCodes() map[graphsync.RequestID]graphsync.ResponseStatusCode { @@ -282,34 +262,15 @@ func (gsm GraphSyncMessage) ResponseCodes() map[graphsync.RequestID]graphsync.Re return codes } -// Responses returns a copy of the list of GraphSyncResponses in this -// GraphSyncMessage -func (gsm GraphSyncMessage) Responses() []GraphSyncResponse { - responses := make([]GraphSyncResponse, 0, len(gsm.responses)) - for _, response := range gsm.responses { - responses = append(responses, response) - } - return responses -} - -// Blocks returns a copy of the list of Blocks in this GraphSyncMessage -func (gsm GraphSyncMessage) Blocks() []blocks.Block { - bs := make([]blocks.Block, 0, len(gsm.blocks)) - for _, block := range gsm.blocks { - bs = append(bs, block) - } - return bs -} - // Loggable returns a simplified, single-line log form of this GraphSyncMessage 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)) + requests = append(requests, request.ID.String()) } responses := make([]string, 0, len(gsm.Responses)) for _, response := range gsm.Responses { - responses = append(responses, fmt.Sprintf("%d", response.ID)) + responses = append(responses, response.ID.String()) } return map[string]interface{}{ "requests": requests, diff --git a/message/messagehandler.go b/message/messagehandler.go index ea15e32d..48262951 100644 --- a/message/messagehandler.go +++ b/message/messagehandler.go @@ -6,7 +6,6 @@ import ( "io" "sync" - blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/ipldutil" @@ -84,42 +83,41 @@ func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncM // ToProto converts a GraphSyncMessage to its pb.Message equivalent func (mh *MessageHandler) ToProto(gsm GraphSyncMessage) (*pb.Message, error) { pbm := new(pb.Message) - pbm.Requests = make([]*pb.Message_Request, 0, len(gsm.requests)) - for _, request := range gsm.requests { + 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 request.Selector != nil { + selector, err = ipldutil.EncodeNode(request.Selector) if err != nil { return nil, err } } pbm.Requests = append(pbm.Requests, &pb.Message_Request{ - Id: request.id.Bytes(), - Root: request.root.Bytes(), + Id: request.ID.Bytes(), + Root: request.Root.Bytes(), Selector: selector, - Priority: int32(request.priority), - Cancel: request.isCancel, - Update: request.isUpdate, + Priority: int32(request.Priority), + Cancel: request.Cancel, + Update: request.Update, Extensions: toProtoExtensions(request.Extensions), }) } - pbm.Responses = make([]*pb.Message_Response, 0, len(gsm.responses)) - for _, response := range gsm.responses { + pbm.Responses = make([]*pb.Message_Response, 0, len(gsm.Responses)) + for _, response := range gsm.Responses { pbm.Responses = append(pbm.Responses, &pb.Message_Response{ - Id: response.requestID.Bytes(), - Status: int32(response.status), - Extensions: toProtoExtensions(request.Extensions), + Id: response.ID.Bytes(), + Status: int32(response.Status), + Extensions: toProtoExtensions(response.Extensions), }) } - blocks := gsm.Blocks() - pbm.Data = make([]*pb.Message_Block, 0, len(blocks)) - for _, b := range blocks { + pbm.Data = make([]*pb.Message_Block, 0, len(gsm.Blocks)) + for _, b := range gsm.Blocks { pbm.Data = append(pbm.Data, &pb.Message_Block{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), + Prefix: b.Prefix, + Data: b.Data, }) } return pbm, nil @@ -131,50 +129,49 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag defer mh.mapLock.Unlock() pbm := new(pb.Message_V1_0_0) - pbm.Requests = make([]*pb.Message_V1_0_0_Request, 0, len(gsm.requests)) - for _, request := range gsm.requests { + pbm.Requests = make([]*pb.Message_V1_0_0_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 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()) + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, request.ID.Bytes()) if err != nil { return nil, err } pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ Id: rid, - Root: request.root.Bytes(), + Root: request.Root.Bytes(), Selector: selector, - Priority: int32(request.priority), - Cancel: request.isCancel, - Update: request.isUpdate, - Extensions: request.extensions, + Priority: int32(request.Priority), + Cancel: request.Cancel, + Update: request.Update, + Extensions: toProtoExtensions(request.Extensions), }) } - pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(gsm.responses)) - for _, response := range gsm.responses { - rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.requestID.Bytes()) + pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(gsm.Responses)) + for _, response := range gsm.Responses { + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.ID.Bytes()) if err != nil { return nil, err } pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ Id: rid, - Status: int32(response.status), - Extensions: response.extensions, + Status: int32(response.Status), + Extensions: toProtoExtensions(response.Extensions), }) } - blocks := gsm.Blocks() - pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(blocks)) - for _, b := range blocks { + pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(gsm.Blocks)) + for _, b := range gsm.Blocks { pbm.Data = append(pbm.Data, &pb.Message_V1_0_0_Block{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), + Prefix: b.Prefix, + Data: b.Data, }) } return pbm, nil @@ -237,7 +234,6 @@ func toProtoExtensions(m GraphSyncExtensions) map[string][]byte { return protoExts } - // 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) { @@ -270,8 +266,8 @@ func intIdToRequestId(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, // Mapping from a pb.Message object to a GraphSyncMessage object func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.Requests)) - for _, req := range pbm.Requests { + requests := make([]GraphSyncRequest, len(pbm.GetRequests())) + for i, req := range pbm.Requests { if req == nil { return GraphSyncMessage{}, errors.New("request is nil") } @@ -291,55 +287,38 @@ func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage return GraphSyncMessage{}, err } } - exts := req.Extensions - if exts == nil { - exts = make(map[string][]byte) - } id, err := graphsync.ParseRequestID(req.Id) if err != nil { return GraphSyncMessage{}, err } - requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) + // TODO: we likely need to turn some "core" extensions to fields, + // as some of those got moved to proper fields in the new protocol. + // Same for responses above, as well as the "to proto" funcs. + requests[i] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, fromProtoExtensions(req.GetExtensions())) } - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.Responses)) - for _, res := range pbm.Responses { + responses := make([]GraphSyncResponse, len(pbm.GetResponses())) + for i, res := range pbm.Responses { if res == nil { return GraphSyncMessage{}, errors.New("response is nil") } - exts := res.Extensions - if exts == nil { - exts = make(map[string][]byte) - } id, err := graphsync.ParseRequestID(res.Id) if err != nil { return GraphSyncMessage{}, err } - responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) + responses[i] = newResponse(id, graphsync.ResponseStatusCode(res.Status), fromProtoExtensions(res.GetExtensions())) } - blks := make(map[cid.Cid]blocks.Block, len(pbm.Data)) - for _, b := range pbm.Data { + blks := make([]GraphSyncBlock, len(pbm.GetData())) + for i, b := range pbm.Data { 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[i] = GraphSyncBlock{ + Prefix: b.GetPrefix(), + Data: b.GetData(), } - - blks[blk.Cid()] = blk } return GraphSyncMessage{ @@ -353,8 +332,8 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ mh.mapLock.Lock() defer mh.mapLock.Unlock() - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.Requests)) - for _, req := range pbm.Requests { + requests := make([]GraphSyncRequest, len(pbm.GetRequests())) + for i, req := range pbm.Requests { if req == nil { return GraphSyncMessage{}, errors.New("request is nil") } @@ -374,55 +353,38 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ return GraphSyncMessage{}, err } } - exts := req.Extensions - if exts == nil { - exts = make(map[string][]byte) - } id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, req.Id) if err != nil { return GraphSyncMessage{}, err } - requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) + // TODO: we likely need to turn some "core" extensions to fields, + // as some of those got moved to proper fields in the new protocol. + // Same for responses above, as well as the "to proto" funcs. + requests[i] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, fromProtoExtensions(req.GetExtensions())) } - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.Responses)) - for _, res := range pbm.Responses { + responses := make([]GraphSyncResponse, len(pbm.GetResponses())) + for i, res := range pbm.Responses { if res == nil { return GraphSyncMessage{}, errors.New("response is nil") } - exts := res.Extensions - if exts == nil { - exts = make(map[string][]byte) - } id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, res.Id) if err != nil { return GraphSyncMessage{}, err } - responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) + responses[i] = newResponse(id, graphsync.ResponseStatusCode(res.Status), fromProtoExtensions(res.GetExtensions())) } - blks := make(map[cid.Cid]blocks.Block, len(pbm.Data)) - for _, b := range pbm.Data { + blks := make([]GraphSyncBlock, len(pbm.GetData())) + for i, b := range pbm.Data { 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 + blks[i] = GraphSyncBlock{ + Prefix: b.GetPrefix(), + Data: b.GetData(), } - - blk, err := blocks.NewBlockWithCid(b.GetData(), c) - if err != nil { - return GraphSyncMessage{}, err - } - - blks[blk.Cid()] = blk } return GraphSyncMessage{ diff --git a/message/schema.ipldsch b/message/schema.ipldsch index 89fc5a3d..060ac50d 100644 --- a/message/schema.ipldsch +++ b/message/schema.ipldsch @@ -1,5 +1,5 @@ type GraphSyncExtensions {String:Any} -type GraphSyncRequestID int +type GraphSyncRequestID bytes type GraphSyncPriority int type GraphSyncMetadatum struct { From 8ef200897ece667e6d3496667c5ff242bc6dbc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Thu, 13 Jan 2022 15:04:41 +0000 Subject: [PATCH 09/32] feat(net): to/from ipld bindnode types, more cbor protoc improvements --- go.mod | 5 +- message/bench_test.go | 44 ++- message/builder.go | 30 +- message/builder_test.go | 73 ++--- message/ipldbind/message.go | 123 ++++++++ message/{ => ipldbind}/schema.go | 2 +- message/{ => ipldbind}/schema.ipldsch | 0 message/message.go | 391 +++++++++++++++----------- message/message_test.go | 189 ++++++------- message/messagehandler.go | 172 +++++------ 10 files changed, 604 insertions(+), 425 deletions(-) create mode 100644 message/ipldbind/message.go rename message/{ => ipldbind}/schema.go (96%) rename message/{ => ipldbind}/schema.ipldsch (100%) diff --git a/go.mod b/go.mod index e7362e50..3ab0f941 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,9 @@ 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.6 github.com/google/uuid v1.3.0 + github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect 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 @@ -48,5 +49,3 @@ require ( golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/protobuf v1.27.1 ) - -replace github.com/ipld/go-ipld-prime => ../../ipld/go-ipld-prime diff --git a/message/bench_test.go b/message/bench_test.go index 0b309914..f2565eae 100644 --- a/message/bench_test.go +++ b/message/bench_test.go @@ -1,13 +1,15 @@ -package message_test +package message import ( "bytes" "math/rand" + "reflect" "testing" + "github.com/google/go-cmp/cmp" blocks "github.com/ipfs/go-block-format" "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/node/basicnode" @@ -21,16 +23,17 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() extensionName := graphsync.ExtensionName("graphsync/awesome") - extension := message.NamedExtension{ + extension := graphsync.ExtensionData{ Name: extensionName, - Data: basicnode.NewBytes(testutil.RandomBytes(100)), + Data: 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 := NewBuilder() + builder.AddRequest(NewRequest(id, root, selector, priority, extension)) + builder.AddRequest(NewRequest(id, root, selector, priority)) builder.AddResponseCode(id, status) builder.AddExtensionData(id, extension) builder.AddBlock(blocks.NewBlock([]byte("W"))) @@ -48,12 +51,17 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { for pb.Next() { buf.Reset() - err := message.NewMessageHandler().ToNet(gsm, buf) + err := NewMessageHandler().ToNet(gsm, buf) require.NoError(b, err) - gsm2, err := message.NewMessageHandler().FromNet(buf) + gsm2, err := NewMessageHandler().FromNet(buf) require.NoError(b, err) - require.Equal(b, gsm, gsm2) + + // 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 })); diff != "" { + b.Fatal(diff) + } } }) }) @@ -65,16 +73,24 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { for pb.Next() { buf.Reset() - node := bindnode.Wrap(&gsm, message.Prototype.Message.Type()) - err := dagcbor.Encode(node.Representation(), buf) + ipldGSM, err := gsm.ToIPLD() + require.NoError(b, err) + node := bindnode.Wrap(ipldGSM, ipldbind.Prototype.Message.Type()) + err = dagcbor.Encode(node.Representation(), buf) require.NoError(b, err) - builder := message.Prototype.Message.Representation().NewBuilder() + builder := ipldbind.Prototype.Message.Representation().NewBuilder() err = dagcbor.Decode(builder, buf) require.NoError(b, err) node2 := builder.Build() - gsm2 := *bindnode.Unwrap(node2).(*message.GraphSyncMessage) - require.Equal(b, gsm, gsm2) + ipldGSM2 := bindnode.Unwrap(node2).(*ipldbind.GraphSyncMessage) + gsm2, err := messageFromIPLD(ipldGSM2) + require.NoError(b, err) + + // same as above. + if diff := cmp.Diff(gsm, gsm2, cmp.Exporter(func(reflect.Type) bool { return true })); diff != "" { + b.Fatal(diff) + } } }) }) diff --git a/message/builder.go b/message/builder.go index 8c92f72a..15017198 100644 --- a/message/builder.go +++ b/message/builder.go @@ -2,10 +2,9 @@ package message import ( blocks "github.com/ipfs/go-block-format" - cid "github.com/ipfs/go-cid" + "github.com/ipfs/go-cid" "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/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/metadata" @@ -19,7 +18,7 @@ type Builder struct { blkSize uint64 completedResponses map[graphsync.RequestID]graphsync.ResponseStatusCode outgoingResponses map[graphsync.RequestID]metadata.Metadata - extensions map[graphsync.RequestID][]NamedExtension + extensions map[graphsync.RequestID][]graphsync.ExtensionData requests map[graphsync.RequestID]GraphSyncRequest } @@ -30,13 +29,13 @@ func NewBuilder() *Builder { outgoingBlocks: make(map[cid.Cid]blocks.Block), completedResponses: make(map[graphsync.RequestID]graphsync.ResponseStatusCode), outgoingResponses: make(map[graphsync.RequestID]metadata.Metadata), - extensions: make(map[graphsync.RequestID][]NamedExtension), + extensions: make(map[graphsync.RequestID][]graphsync.ExtensionData), } } // AddRequest registers a new request to be added to the message. func (b *Builder) AddRequest(request GraphSyncRequest) { - b.requests[request.ID] = request + b.requests[request.ID()] = request } // AddBlock adds the given block to the message. @@ -46,7 +45,7 @@ func (b *Builder) AddBlock(block blocks.Block) { } // AddExtensionData adds the given extension data to to the message -func (b *Builder) AddExtensionData(requestID graphsync.RequestID, extension NamedExtension) { +func (b *Builder) AddExtensionData(requestID graphsync.RequestID, extension graphsync.ExtensionData) { b.extensions[requestID] = append(b.extensions[requestID], extension) // make sure this extension goes out in next response even if no links are sent _, ok := b.outgoingResponses[requestID] @@ -110,30 +109,21 @@ func (b *Builder) ScrubResponses(requestIDs []graphsync.RequestID) uint64 { // Build assembles and encodes message data from the added requests, links, and blocks. func (b *Builder) Build() (GraphSyncMessage, error) { - requests := make([]GraphSyncRequest, 0, len(b.requests)) - for _, request := range b.requests { - requests = append(requests, request) - } - responses := make([]GraphSyncResponse, 0, len(b.outgoingResponses)) + 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], NamedExtension{ + b.extensions[requestID] = append(b.extensions[requestID], graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, - Data: basicnode.NewBytes(mdRaw), // TODO: likely wrong + Data: mdRaw, }) status, isComplete := b.completedResponses[requestID] - responses = append(responses, NewResponse(requestID, responseCode(status, isComplete), b.extensions[requestID]...)) - } - blocks := make([]GraphSyncBlock, 0, len(b.outgoingBlocks)) - for _, block := range b.outgoingBlocks { - blocks = append(blocks, FromBlockFormat(block)) + responses[requestID] = NewResponse(requestID, responseCode(status, isComplete), b.extensions[requestID]...) } - // TODO: sort requests, responses, and blocks? map order is randomized return GraphSyncMessage{ - requests, responses, blocks, + b.requests, responses, b.outgoingBlocks, }, nil } diff --git a/message/builder_test.go b/message/builder_test.go index b7fba622..841197f3 100644 --- a/message/builder_test.go +++ b/message/builder_test.go @@ -1,13 +1,11 @@ package message import ( - "bytes" "io" "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" @@ -15,42 +13,21 @@ import ( "github.com/ipfs/go-graphsync/testutil" ) -// Like the funcs in testutil above, but using blocks at the protocol level. -// We can't put them there right away, due to import cycles. -// We need to refactor these tests to be external, i.e. "package message_test". - -func ContainsGraphSyncBlock(blks []GraphSyncBlock, block GraphSyncBlock) bool { - for _, blk := range blks { - if bytes.Equal(blk.Prefix, block.Prefix) && bytes.Equal(blk.Data, block.Data) { - return true - } - } - return false -} -func AssertContainsGraphSyncBlock(t testing.TB, blks []GraphSyncBlock, block GraphSyncBlock) { - t.Helper() - require.True(t, ContainsGraphSyncBlock(blks, block), "given block should be in list") -} -func RefuteContainsGraphSyncBlock(t testing.TB, blks []GraphSyncBlock, block GraphSyncBlock) { - t.Helper() - require.False(t, ContainsGraphSyncBlock(blks, block), "given block should not be in list") -} - func TestMessageBuilding(t *testing.T) { blocks := testutil.GenerateBlocksOfSize(3, 100) links := make([]ipld.Link, 0, len(blocks)) for _, block := range blocks { links = append(links, cidlink.Link{Cid: block.Cid()}) } - extensionData1 := basicnode.NewBytes(testutil.RandomBytes(100)) + extensionData1 := testutil.RandomBytes(100) extensionName1 := graphsync.ExtensionName("AppleSauce/McGee") - extension1 := NamedExtension{ + extension1 := graphsync.ExtensionData{ Name: extensionName1, Data: extensionData1, } - extensionData2 := basicnode.NewBytes(testutil.RandomBytes(100)) + extensionData2 := testutil.RandomBytes(100) extensionName2 := graphsync.ExtensionName("HappyLand/Happenstance") - extension2 := NamedExtension{ + extension2 := graphsync.ExtensionData{ Name: extensionName2, Data: extensionData2, } @@ -97,12 +74,12 @@ func TestMessageBuilding(t *testing.T) { }, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses - sentBlocks := BlockFormatSlice(message.Blocks) + responses := message.Responses() + sentBlocks := message.Blocks() require.Len(t, responses, 4, "did not assemble correct number of responses") response1 := findResponseForRequestID(t, responses, requestID1) - require.Equal(t, graphsync.RequestCompletedPartial, response1.Status, "did not generate completed partial response") + 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}, @@ -111,7 +88,7 @@ func TestMessageBuilding(t *testing.T) { assertExtension(t, response1, extension1) response2 := findResponseForRequestID(t, responses, requestID2) - require.Equal(t, graphsync.RequestCompletedFull, response2.Status, "did not generate completed full response") + 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}, @@ -119,7 +96,7 @@ func TestMessageBuilding(t *testing.T) { }) response3 := findResponseForRequestID(t, responses, requestID3) - require.Equal(t, graphsync.PartialResponse, response3.Status, "did not generate partial response") + 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}, @@ -127,7 +104,7 @@ func TestMessageBuilding(t *testing.T) { assertExtension(t, response3, extension2) response4 := findResponseForRequestID(t, responses, requestID4) - require.Equal(t, graphsync.RequestCompletedFull, response4.Status, "did not generate completed full response") + require.Equal(t, graphsync.RequestCompletedFull, response4.Status(), "did not generate completed full response") require.Equal(t, len(blocks), len(sentBlocks), "did not send all blocks") @@ -143,15 +120,15 @@ func TestMessageBuilding(t *testing.T) { }, expectedSize: 0, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses + responses := message.Responses() response1 := findResponseForRequestID(t, responses, requestID1) - require.Equal(t, graphsync.PartialResponse, response1.Status, "did not generate partial response") + require.Equal(t, graphsync.PartialResponse, response1.Status(), "did not generate partial response") assertMetadata(t, response1, nil) assertExtension(t, response1, extension1) response2 := findResponseForRequestID(t, responses, requestID2) - require.Equal(t, graphsync.PartialResponse, response2.Status, "did not generate partial response") + require.Equal(t, graphsync.PartialResponse, response2.Status(), "did not generate partial response") assertMetadata(t, response2, nil) assertExtension(t, response2, extension2) }, @@ -184,12 +161,12 @@ func TestMessageBuilding(t *testing.T) { expectedSize: 200, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses - sentBlocks := BlockFormatSlice(message.Blocks) + responses := message.Responses() + sentBlocks := message.Blocks() require.Len(t, responses, 3, "did not assemble correct number of responses") response2 := findResponseForRequestID(t, responses, requestID2) - require.Equal(t, graphsync.RequestCompletedFull, response2.Status, "did not generate completed full response") + 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}, @@ -197,14 +174,14 @@ func TestMessageBuilding(t *testing.T) { }) response3 := findResponseForRequestID(t, responses, requestID3) - require.Equal(t, graphsync.PartialResponse, response3.Status, "did not generate partial response") + 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}, }) assertExtension(t, response3, extension2) response4 := findResponseForRequestID(t, responses, requestID4) - require.Equal(t, graphsync.RequestCompletedFull, response4.Status, "did not generate completed full response") + require.Equal(t, graphsync.RequestCompletedFull, response4.Status(), "did not generate completed full response") require.Equal(t, len(blocks)-1, len(sentBlocks), "did not send all blocks") @@ -242,12 +219,12 @@ func TestMessageBuilding(t *testing.T) { expectedSize: 100, checkMsg: func(t *testing.T, message GraphSyncMessage) { - responses := message.Responses - sentBlocks := BlockFormatSlice(message.Blocks) + responses := message.Responses() + sentBlocks := message.Blocks() require.Len(t, responses, 1, "did not assemble correct number of responses") response3 := findResponseForRequestID(t, responses, requestID3) - require.Equal(t, graphsync.PartialResponse, response3.Status, "did not generate partial response") + 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}, }) @@ -273,7 +250,7 @@ func TestMessageBuilding(t *testing.T) { func findResponseForRequestID(t *testing.T, responses []GraphSyncResponse, requestID graphsync.RequestID) GraphSyncResponse { for _, response := range responses { - if response.ID == requestID { + if response.RequestID() == requestID { return response } } @@ -281,17 +258,15 @@ func findResponseForRequestID(t *testing.T, responses []GraphSyncResponse, reque return GraphSyncResponse{} } -func assertExtension(t *testing.T, response GraphSyncResponse, extension NamedExtension) { +func assertExtension(t *testing.T, response GraphSyncResponse, extension graphsync.ExtensionData) { returnedExtensionData, found := response.Extension(extension.Name) require.True(t, found) require.Equal(t, extension.Data, returnedExtensionData, "did not encode extension") } func assertMetadata(t *testing.T, response GraphSyncResponse, expectedMetadata metadata.Metadata) { - responseMetadataNode, found := response.Extension(graphsync.ExtensionMetadata) + responseMetadataRaw, found := response.Extension(graphsync.ExtensionMetadata) require.True(t, found, "Metadata should be included in response") - responseMetadataRaw, err := responseMetadataNode.AsBytes() - require.NoError(t, err) responseMetadata, err := metadata.DecodeMetadata(responseMetadataRaw) require.NoError(t, err) require.Equal(t, expectedMetadata, responseMetadata, "incorrect metadata included in response") diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go new file mode 100644 index 00000000..fcc71f95 --- /dev/null +++ b/message/ipldbind/message.go @@ -0,0 +1,123 @@ +package ipldbind + +import ( + "io" + + 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" + + "github.com/ipfs/go-graphsync" + 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() +} + +// 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 +} + +type GraphSyncExtensions struct { + Keys []string + Values map[string]datamodel.Node +} + +// GraphSyncRequest is a struct to capture data on a request contained in a +// GraphSyncMessage. +type GraphSyncRequest struct { + Id []byte + + Root cid.Cid + Selector ipld.Node + Extensions GraphSyncExtensions + Priority graphsync.Priority + Cancel bool + Update bool +} + +type GraphSyncMetadatum struct { + Link datamodel.Link + BlockPresent bool +} + +// GraphSyncResponse is an struct to capture data on a response sent back +// in a GraphSyncMessage. +type GraphSyncResponse struct { + Id []byte + + Status graphsync.ResponseStatusCode + Metadata []GraphSyncMetadatum + Extensions GraphSyncExtensions +} + +type GraphSyncBlock struct { + Prefix []byte + Data []byte +} + +func FromBlockFormat(block blocks.Block) GraphSyncBlock { + return GraphSyncBlock{ + Prefix: block.Cid().Prefix().Bytes(), + Data: block.RawData(), + } +} + +func (b GraphSyncBlock) BlockFormat() *blocks.BasicBlock { + pref, err := cid.PrefixFromBytes(b.Prefix) + if err != nil { + panic(err) // should never happen + } + + c, err := pref.Sum(b.Data) + if err != nil { + panic(err) // should never happen + } + + block, err := blocks.NewBlockWithCid(b.Data, c) + if err != nil { + panic(err) // should never happen + } + return block +} + +func BlockFormatSlice(bs []GraphSyncBlock) []blocks.Block { + blks := make([]blocks.Block, len(bs)) + for i, b := range bs { + blks[i] = b.BlockFormat() + } + return blks +} + +type GraphSyncMessage struct { + Requests []GraphSyncRequest + Responses []GraphSyncResponse + Blocks []GraphSyncBlock +} + +// NamedExtension exists just for the purpose of the constructors. +type NamedExtension struct { + Name graphsync.ExtensionName + Data ipld.Node +} diff --git a/message/schema.go b/message/ipldbind/schema.go similarity index 96% rename from message/schema.go rename to message/ipldbind/schema.go index 970b801e..ec7be58a 100644 --- a/message/schema.go +++ b/message/ipldbind/schema.go @@ -1,4 +1,4 @@ -package message +package ipldbind import ( _ "embed" diff --git a/message/schema.ipldsch b/message/ipldbind/schema.ipldsch similarity index 100% rename from message/schema.ipldsch rename to message/ipldbind/schema.ipldsch diff --git a/message/message.go b/message/message.go index cc38fb80..ace17d7e 100644 --- a/message/message.go +++ b/message/message.go @@ -4,17 +4,15 @@ import ( "bytes" "fmt" "io" - "sort" "strings" 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/codec/dagjson" - "github.com/ipld/go-ipld-prime/datamodel" - "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message/ipldbind" pb "github.com/ipfs/go-graphsync/message/pb" ) @@ -45,40 +43,29 @@ type Exportable interface { ToNet(w io.Writer) error } -type GraphSyncExtensions struct { - Keys []string - Values map[string]datamodel.Node -} - // GraphSyncRequest is a struct to capture data on a request contained in a // GraphSyncMessage. type GraphSyncRequest struct { - ID graphsync.RequestID - - Root cid.Cid - Selector ipld.Node - Extensions GraphSyncExtensions - Priority graphsync.Priority - Cancel bool - Update bool -} - -type GraphSyncMetadatum struct { - Link datamodel.Link - BlockPresent bool + root cid.Cid + selector ipld.Node + priority graphsync.Priority + id graphsync.RequestID + extensions map[string][]byte + isCancel bool + isUpdate bool } // String returns a human-readable form of a GraphSyncRequest func (gsr GraphSyncRequest) String() string { var buf bytes.Buffer - dagjson.Encode(gsr.Selector, &buf) + dagjson.Encode(gsr.selector, &buf) return fmt.Sprintf("GraphSyncRequest", - gsr.Root.String(), + gsr.root.String(), buf.String(), - gsr.Priority, - gsr.ID.String(), - gsr.Cancel, - gsr.Update, + gsr.priority, + gsr.id.String(), + gsr.isCancel, + gsr.isUpdate, strings.Join(gsr.ExtensionNames(), "|"), ) } @@ -86,56 +73,16 @@ func (gsr GraphSyncRequest) String() string { // GraphSyncResponse is an struct to capture data on a response sent back // in a GraphSyncMessage. type GraphSyncResponse struct { - ID graphsync.RequestID - - Status graphsync.ResponseStatusCode - Metadata []GraphSyncMetadatum - Extensions GraphSyncExtensions -} - -type GraphSyncBlock struct { - Prefix []byte - Data []byte -} - -func FromBlockFormat(block blocks.Block) GraphSyncBlock { - return GraphSyncBlock{ - Prefix: block.Cid().Prefix().Bytes(), - Data: block.RawData(), - } -} - -func (b GraphSyncBlock) BlockFormat() *blocks.BasicBlock { - pref, err := cid.PrefixFromBytes(b.Prefix) - if err != nil { - panic(err) // should never happen - } - - c, err := pref.Sum(b.Data) - if err != nil { - panic(err) // should never happen - } - - block, err := blocks.NewBlockWithCid(b.Data, c) - if err != nil { - panic(err) // should never happen - } - return block -} - -func BlockFormatSlice(bs []GraphSyncBlock) []blocks.Block { - blks := make([]blocks.Block, len(bs)) - for i, b := range bs { - blks[i] = b.BlockFormat() - } - return blks + requestID graphsync.RequestID + status graphsync.ResponseStatusCode + extensions map[string][]byte } // String returns a human-readable form of a GraphSyncResponse func (gsr GraphSyncResponse) String() string { return fmt.Sprintf("GraphSyncResponse", - gsr.ID.String(), - gsr.Status, + gsr.requestID.String(), + gsr.status, strings.Join(gsr.ExtensionNames(), "|"), ) } @@ -143,23 +90,23 @@ func (gsr GraphSyncResponse) String() string { // GraphSyncMessage is the internal representation form of a message sent or // received over the wire type GraphSyncMessage struct { - Requests []GraphSyncRequest - Responses []GraphSyncResponse - Blocks []GraphSyncBlock + requests map[graphsync.RequestID]GraphSyncRequest + responses map[graphsync.RequestID]GraphSyncResponse + blocks map[cid.Cid]blocks.Block } // 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 { + for _, req := range gsm.requests { cts = append(cts, req.String()) } - for _, resp := range gsm.Responses { + for _, resp := range gsm.responses { cts = append(cts, resp.String()) } - for _, c := range gsm.Blocks { - cts = append(cts, fmt.Sprintf("Block<%s>", c.BlockFormat().Cid().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")) } @@ -169,48 +116,29 @@ func NewRequest(id graphsync.RequestID, root cid.Cid, selector ipld.Node, priority graphsync.Priority, - extensions ...NamedExtension) GraphSyncRequest { + extensions ...graphsync.ExtensionData) GraphSyncRequest { return newRequest(id, root, selector, priority, false, false, toExtensionsMap(extensions)) } // 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, GraphSyncExtensions{}) + return newRequest(id, cid.Cid{}, nil, 0, true, false, nil) } // UpdateRequest generates a new request to update an in progress request with the given extensions -func UpdateRequest(id graphsync.RequestID, extensions ...NamedExtension) GraphSyncRequest { +func UpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionData) GraphSyncRequest { return newRequest(id, cid.Cid{}, nil, 0, false, true, toExtensionsMap(extensions)) } -// NamedExtension exists just for the purpose of the constructors. -type NamedExtension struct { - Name graphsync.ExtensionName - Data ipld.Node -} - -func toExtensionsMap(extensions []NamedExtension) (m GraphSyncExtensions) { +func toExtensionsMap(extensions []graphsync.ExtensionData) (extensionsMap map[string][]byte) { if len(extensions) > 0 { - m.Keys = make([]string, len(extensions)) - m.Values = make(map[string]ipld.Node, len(extensions)) - for i, ext := range extensions { - m.Keys[i] = string(ext.Name) - m.Values[string(ext.Name)] = ext.Data + extensionsMap = make(map[string][]byte, len(extensions)) + for _, extension := range extensions { + extensionsMap[string(extension.Name)] = extension.Data } } - return m -} - -func fromProtoExtensions(protoExts map[string][]byte) GraphSyncExtensions { - var exts []NamedExtension - for name, data := range protoExts { - exts = append(exts, NamedExtension{graphsync.ExtensionName(name), basicnode.NewBytes(data)}) - } - // Iterating over the map above is non-deterministic, - // so sort by the unique names to ensure determinism. - sort.Slice(exts, func(i, j int) bool { return exts[i].Name < exts[j].Name }) - return toExtensionsMap(exts) + return } func newRequest(id graphsync.RequestID, @@ -219,58 +147,161 @@ func newRequest(id graphsync.RequestID, priority graphsync.Priority, isCancel bool, isUpdate bool, - extensions GraphSyncExtensions) GraphSyncRequest { + extensions map[string][]byte) GraphSyncRequest { return GraphSyncRequest{ - ID: id, - Root: root, - Selector: selector, - Priority: priority, - Cancel: isCancel, - Update: isUpdate, - Extensions: extensions, + id: id, + root: root, + selector: selector, + priority: priority, + isCancel: isCancel, + isUpdate: isUpdate, + extensions: extensions, } } // NewResponse builds a new Graphsync response func NewResponse(requestID graphsync.RequestID, status graphsync.ResponseStatusCode, - extensions ...NamedExtension) GraphSyncResponse { + extensions ...graphsync.ExtensionData) GraphSyncResponse { return newResponse(requestID, status, toExtensionsMap(extensions)) } func newResponse(requestID graphsync.RequestID, - status graphsync.ResponseStatusCode, extensions GraphSyncExtensions) GraphSyncResponse { + status graphsync.ResponseStatusCode, extensions map[string][]byte) GraphSyncResponse { return GraphSyncResponse{ - ID: requestID, - Status: status, - Extensions: extensions, + requestID: requestID, + status: status, + extensions: extensions, } } -// Empty returns true if this message has no actionable content func (gsm GraphSyncMessage) Empty() bool { - return len(gsm.Blocks) == 0 && len(gsm.Requests) == 0 && len(gsm.Responses) == 0 + return len(gsm.blocks) == 0 && len(gsm.requests) == 0 && len(gsm.responses) == 0 +} + +func (gsm GraphSyncMessage) Requests() []GraphSyncRequest { + requests := make([]GraphSyncRequest, 0, len(gsm.requests)) + for _, request := range gsm.requests { + requests = append(requests, request) + } + 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 _, response := range gsm.Responses { - codes[response.ID] = response.Status + codes := make(map[graphsync.RequestID]graphsync.ResponseStatusCode, len(gsm.responses)) + for id, response := range gsm.responses { + codes[id] = response.Status() } return codes } -// Loggable returns a simplified, single-line log form of this GraphSyncMessage +func (gsm GraphSyncMessage) Responses() []GraphSyncResponse { + responses := make([]GraphSyncResponse, 0, len(gsm.responses)) + for _, response := range gsm.responses { + responses = append(responses, response) + } + return responses +} + +func (gsm GraphSyncMessage) Blocks() []blocks.Block { + bs := make([]blocks.Block, 0, len(gsm.blocks)) + for _, block := range gsm.blocks { + bs = append(bs, block) + } + return bs +} + +func (gsm GraphSyncMessage) ToIPLD() (*ipldbind.GraphSyncMessage, error) { + ibm := new(ipldbind.GraphSyncMessage) + ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(gsm.requests)) + for _, request := range gsm.requests { + ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ + Id: request.id.Bytes(), + Root: request.root, + Selector: request.selector, + Priority: request.priority, + Cancel: request.isCancel, + Update: request.isUpdate, + // Extensions: request.extensions, + }) + } + + ibm.Responses = make([]ipldbind.GraphSyncResponse, 0, len(gsm.responses)) + for _, response := range gsm.responses { + ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ + Id: response.requestID.Bytes(), + Status: response.status, + // Extensions: response.extensions, + }) + } + + blocks := gsm.Blocks() + ibm.Blocks = make([]ipldbind.GraphSyncBlock, 0, len(blocks)) + for _, b := range blocks { + ibm.Blocks = append(ibm.Blocks, ipldbind.GraphSyncBlock{ + Data: b.RawData(), + Prefix: b.Cid().Prefix().Bytes(), + }) + } + return ibm, nil +} + +func messageFromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(ibm.Requests)) + for _, req := range ibm.Requests { + // exts := req.Extensions + id, err := graphsync.ParseRequestID(req.Id) + if err != nil { + return GraphSyncMessage{}, err + } + requests[id] = newRequest(id, req.Root, req.Selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, nil) + } + + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(ibm.Responses)) + for _, res := range ibm.Responses { + // exts := res.Extensions + id, err := graphsync.ParseRequestID(res.Id) + if err != nil { + return GraphSyncMessage{}, err + } + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), nil) + } + + blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) + for _, b := range ibm.Blocks { + pref, err := cid.PrefixFromBytes(b.Prefix) + if err != nil { + return GraphSyncMessage{}, err + } + + c, err := pref.Sum(b.Data) + if err != nil { + return GraphSyncMessage{}, err + } + + blk, err := blocks.NewBlockWithCid(b.Data, c) + if err != nil { + return GraphSyncMessage{}, err + } + + blks[blk.Cid()] = blk + } + + return GraphSyncMessage{ + requests, responses, blks, + }, nil +} + func (gsm GraphSyncMessage) Loggable() map[string]interface{} { - requests := make([]string, 0, len(gsm.Requests)) - for _, request := range gsm.Requests { - requests = append(requests, request.ID.String()) + requests := make([]string, 0, len(gsm.requests)) + for _, request := range gsm.requests { + requests = append(requests, request.id.String()) } - responses := make([]string, 0, len(gsm.Responses)) - for _, response := range gsm.Responses { - responses = append(responses, response.ID.String()) + responses := make([]string, 0, len(gsm.responses)) + for _, response := range gsm.responses { + responses = append(responses, response.requestID.String()) } return map[string]interface{}{ "requests": requests, @@ -280,16 +311,40 @@ func (gsm GraphSyncMessage) Loggable() map[string]interface{} { // Clone returns a shallow copy of this GraphSyncMessage func (gsm GraphSyncMessage) Clone() GraphSyncMessage { - requests := append([]GraphSyncRequest{}, gsm.Requests...) - responses := append([]GraphSyncResponse{}, gsm.Responses...) - blocks := append([]GraphSyncBlock{}, gsm.Blocks...) + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(gsm.requests)) + for id, request := range gsm.requests { + requests[id] = request + } + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(gsm.responses)) + for id, response := range gsm.responses { + responses[id] = response + } + blocks := make(map[cid.Cid]blocks.Block, len(gsm.blocks)) + for cid, block := range gsm.blocks { + blocks[cid] = block + } return GraphSyncMessage{requests, responses, blocks} } +// ID Returns the request ID for this Request +func (gsr GraphSyncRequest) ID() graphsync.RequestID { return gsr.id } + +// Root returns the CID to the root block of this request +func (gsr GraphSyncRequest) Root() cid.Cid { return gsr.root } + +// Selector returns the byte representation of the selector for this request +func (gsr GraphSyncRequest) Selector() ipld.Node { return gsr.selector } + +// Priority returns the priority of this request +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) (ipld.Node, bool) { - val, ok := gsr.Extensions.Values[string(name)] +func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) ([]byte, bool) { + if gsr.extensions == nil { + return nil, false + } + val, ok := gsr.extensions[string(name)] if !ok { return nil, false } @@ -298,13 +353,32 @@ func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) (ipld.Node, // ExtensionNames returns the names of the extensions included in this request func (gsr GraphSyncRequest) ExtensionNames() []string { - return gsr.Extensions.Keys + var extNames []string + for ext := range gsr.extensions { + extNames = append(extNames, 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 } + +// RequestID returns the request ID for this response +func (gsr GraphSyncResponse) RequestID() graphsync.RequestID { return gsr.requestID } + +// Status returns the status for a response +func (gsr GraphSyncResponse) Status() graphsync.ResponseStatusCode { return gsr.status } + // Extension returns the content for an extension on a response, or errors // if extension is not present -func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) (ipld.Node, bool) { - val, ok := gsr.Extensions.Values[string(name)] +func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) ([]byte, bool) { + if gsr.extensions == nil { + return nil, false + } + val, ok := gsr.extensions[string(name)] if !ok { return nil, false } @@ -313,14 +387,18 @@ func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) (ipld.Node, // ExtensionNames returns the names of the extensions included in this request func (gsr GraphSyncResponse) ExtensionNames() []string { - return gsr.Extensions.Keys + var extNames []string + for ext := range gsr.extensions { + extNames = append(extNames, ext) + } + return extNames } // ReplaceExtensions merges the extensions given extensions into the request to create a new request, // but always uses new data -func (gsr GraphSyncRequest) ReplaceExtensions(extensions []NamedExtension) GraphSyncRequest { - req, _ := gsr.MergeExtensions(extensions, func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error) { - return newNode, nil +func (gsr GraphSyncRequest) ReplaceExtensions(extensions []graphsync.ExtensionData) GraphSyncRequest { + req, _ := gsr.MergeExtensions(extensions, func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { + return newData, nil }) return req } @@ -328,32 +406,31 @@ func (gsr GraphSyncRequest) ReplaceExtensions(extensions []NamedExtension) Graph // 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 []NamedExtension, mergeFunc func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error)) (GraphSyncRequest, error) { - if len(gsr.Extensions.Keys) == 0 { - return newRequest(gsr.ID, gsr.Root, gsr.Selector, gsr.Priority, gsr.Cancel, gsr.Update, toExtensionsMap(extensions)), nil +func (gsr GraphSyncRequest) MergeExtensions(extensions []graphsync.ExtensionData, mergeFunc func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error)) (GraphSyncRequest, error) { + if gsr.extensions == nil { + return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.isCancel, gsr.isUpdate, toExtensionsMap(extensions)), nil } - combinedExtensions := make(map[string]ipld.Node) - for _, newExt := range extensions { - oldNode, ok := gsr.Extensions.Values[string(newExt.Name)] + newExtensionMap := toExtensionsMap(extensions) + combinedExtensions := make(map[string][]byte) + for name, newData := range newExtensionMap { + oldData, ok := gsr.extensions[name] if !ok { - combinedExtensions[string(newExt.Name)] = newExt.Data + combinedExtensions[name] = newData continue } - resultNode, err := mergeFunc(graphsync.ExtensionName(newExt.Name), oldNode, newExt.Data) + resultData, err := mergeFunc(graphsync.ExtensionName(name), oldData, newData) if err != nil { return GraphSyncRequest{}, err } - combinedExtensions[string(newExt.Name)] = resultNode + combinedExtensions[name] = resultData } - for name, oldNode := range gsr.Extensions.Values { + for name, oldData := range gsr.extensions { _, ok := combinedExtensions[name] if ok { continue } - combinedExtensions[name] = oldNode + combinedExtensions[name] = oldData } - extNames := make([]string, len(combinedExtensions)) - sort.Strings(extNames) // for reproducibility - return newRequest(gsr.ID, gsr.Root, gsr.Selector, gsr.Priority, gsr.Cancel, gsr.Update, GraphSyncExtensions{extNames, combinedExtensions}), nil + return newRequest(gsr.id, gsr.root, gsr.selector, gsr.priority, gsr.isCancel, gsr.isUpdate, combinedExtensions), nil } diff --git a/message/message_test.go b/message/message_test.go index 130767ef..20f73aee 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -8,7 +8,6 @@ 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/node/basicnode" "github.com/ipld/go-ipld-prime/traversal/selector/builder" "github.com/stretchr/testify/require" @@ -20,10 +19,9 @@ import ( func TestAppendingRequests(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") - extensionBytes := testutil.RandomBytes(100) - extension := NamedExtension{ + extension := graphsync.ExtensionData{ Name: extensionName, - Data: basicnode.NewBytes(extensionBytes), + Data: testutil.RandomBytes(100), } root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) @@ -35,15 +33,15 @@ func TestAppendingRequests(t *testing.T) { builder.AddRequest(NewRequest(id, root, selector, priority, extension)) gsm, err := builder.Build() require.NoError(t, err) - requests := gsm.Requests + 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.False(t, request.Cancel) - require.Equal(t, priority, request.Priority) - require.Equal(t, root.String(), request.Root.String()) - require.Equal(t, selector, request.Selector) + require.Equal(t, id, request.ID()) + require.False(t, request.IsCancel()) + 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) @@ -59,31 +57,30 @@ func TestAppendingRequests(t *testing.T) { 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": extensionBytes}, pbRequest.Extensions) + require.Equal(t, map[string][]byte{"graphsync/awesome": extension.Data}, pbRequest.Extensions) deserialized, err := NewMessageHandler().newMessageFromProto(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") - deserializedRequests := deserialized.Requests + 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.False(t, deserializedRequest.Cancel) - require.False(t, deserializedRequest.Update) - require.Equal(t, priority, deserializedRequest.Priority) - require.Equal(t, root.String(), deserializedRequest.Root.String()) - require.Equal(t, selector, deserializedRequest.Selector) + require.Equal(t, id, deserializedRequest.ID()) + require.False(t, deserializedRequest.IsCancel()) + require.False(t, deserializedRequest.IsUpdate()) + 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") - extensionBytes := testutil.RandomBytes(100) - extension := NamedExtension{ + extension := graphsync.ExtensionData{ Name: extensionName, - Data: basicnode.NewBytes(extensionBytes), + Data: testutil.RandomBytes(100), } requestID := graphsync.NewRequestID() status := graphsync.RequestAcknowledged @@ -93,12 +90,12 @@ func TestAppendingResponses(t *testing.T) { builder.AddExtensionData(requestID, extension) gsm, err := builder.Build() require.NoError(t, err) - responses := gsm.Responses + 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.ID) - require.Equal(t, status, response.Status) + require.Equal(t, requestID, response.RequestID()) + require.Equal(t, status, response.Status()) require.True(t, found) require.Equal(t, extension.Data, extensionData) @@ -107,16 +104,16 @@ func TestAppendingResponses(t *testing.T) { pbResponse := pbMessage.Responses[0] require.Equal(t, requestID.Bytes(), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) - require.Equal(t, extensionBytes, pbResponse.Extensions["graphsync/awesome"]) + require.Equal(t, extension.Data, pbResponse.Extensions["graphsync/awesome"]) deserialized, err := NewMessageHandler().newMessageFromProto(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") - deserializedResponses := deserialized.Responses + 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.ID, deserializedResponse.ID) - require.Equal(t, response.Status, deserializedResponse.Status) + require.Equal(t, response.RequestID(), deserializedResponse.RequestID()) + require.Equal(t, response.Status(), deserializedResponse.Status()) require.True(t, found) require.Equal(t, extension.Data, extensionData) } @@ -167,31 +164,31 @@ func TestRequestCancel(t *testing.T) { gsm, err := builder.Build() require.NoError(t, err) - requests := gsm.Requests + requests := gsm.Requests() require.Len(t, requests, 1, "did not add cancel request") request := requests[0] - require.Equal(t, id, request.ID) - require.True(t, request.Cancel) + require.Equal(t, id, request.ID()) + require.True(t, request.IsCancel()) buf := new(bytes.Buffer) err = NewMessageHandler().ToNet(gsm, buf) require.NoError(t, err, "did not serialize protobuf message") deserialized, err := NewMessageHandler().FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") - deserializedRequests := deserialized.Requests + 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.Cancel, deserializedRequest.Cancel) + require.Equal(t, request.ID(), deserializedRequest.ID()) + require.Equal(t, request.IsCancel(), deserializedRequest.IsCancel()) } func TestRequestUpdate(t *testing.T) { id := graphsync.NewRequestID() extensionName := graphsync.ExtensionName("graphsync/awesome") - extension := NamedExtension{ + extension := graphsync.ExtensionData{ Name: extensionName, - Data: basicnode.NewBytes(testutil.RandomBytes(100)), + Data: testutil.RandomBytes(100), } builder := NewBuilder() @@ -199,12 +196,12 @@ func TestRequestUpdate(t *testing.T) { gsm, err := builder.Build() require.NoError(t, err) - requests := gsm.Requests + requests := gsm.Requests() require.Len(t, requests, 1, "did not add cancel request") request := requests[0] - require.Equal(t, id, request.ID) - require.True(t, request.Update) - require.False(t, request.Cancel) + require.Equal(t, id, request.ID()) + require.True(t, request.IsUpdate()) + require.False(t, request.IsCancel()) extensionData, found := request.Extension(extensionName) require.True(t, found) require.Equal(t, extension.Data, extensionData) @@ -215,16 +212,16 @@ func TestRequestUpdate(t *testing.T) { deserialized, err := NewMessageHandler().FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") - deserializedRequests := deserialized.Requests + 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.Cancel, deserializedRequest.Cancel) - require.Equal(t, request.Update, deserializedRequest.Update) - require.Equal(t, request.Priority, deserializedRequest.Priority) - require.Equal(t, request.Root.String(), deserializedRequest.Root.String()) - require.Equal(t, request.Selector, deserializedRequest.Selector) + 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.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) } @@ -234,9 +231,9 @@ func TestToNetFromNetEquivalency(t *testing.T) { ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) selector := ssb.Matcher().Node() extensionName := graphsync.ExtensionName("graphsync/awesome") - extension := NamedExtension{ + extension := graphsync.ExtensionData{ Name: extensionName, - Data: basicnode.NewBytes(testutil.RandomBytes(100)), + Data: testutil.RandomBytes(100), } id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) @@ -259,41 +256,41 @@ func TestToNetFromNetEquivalency(t *testing.T) { deserialized, err := NewMessageHandler().FromNet(buf) require.NoError(t, err, "did not deserialize protobuf message") - requests := gsm.Requests + requests := gsm.Requests() require.Len(t, requests, 1, "did not add request to message") request := requests[0] - deserializedRequests := deserialized.Requests + 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.False(t, deserializedRequest.Cancel) - require.False(t, deserializedRequest.Update) - require.Equal(t, request.Priority, deserializedRequest.Priority) - require.Equal(t, request.Root.String(), deserializedRequest.Root.String()) - require.Equal(t, request.Selector, deserializedRequest.Selector) + require.Equal(t, request.ID(), deserializedRequest.ID()) + require.False(t, deserializedRequest.IsCancel()) + require.False(t, deserializedRequest.IsUpdate()) + 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 + responses := gsm.Responses() require.Len(t, responses, 1, "did not add response to message") response := responses[0] - deserializedResponses := deserialized.Responses + 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.ID, deserializedResponse.ID) - require.Equal(t, response.Status, deserializedResponse.Status) + 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.BlockFormat().Cid()] = true + for _, b := range deserialized.Blocks() { + keys[b.Cid()] = true } - for _, b := range gsm.Blocks { - _, ok := keys[b.BlockFormat().Cid()] + for _, b := range gsm.Blocks() { + _, ok := keys[b.Cid()] require.True(t, ok) } } @@ -302,32 +299,28 @@ func TestMergeExtensions(t *testing.T) { extensionName1 := graphsync.ExtensionName("graphsync/1") extensionName2 := graphsync.ExtensionName("graphsync/2") extensionName3 := graphsync.ExtensionName("graphsync/3") - initialExtensions := []NamedExtension{ + initialExtensions := []graphsync.ExtensionData{ { Name: extensionName1, - Data: basicnode.NewBytes([]byte("applesauce")), + Data: []byte("applesauce"), }, { Name: extensionName2, - Data: basicnode.NewBytes([]byte("hello")), + Data: []byte("hello"), }, } - replacementExtensions := []NamedExtension{ + replacementExtensions := []graphsync.ExtensionData{ { Name: extensionName2, - Data: basicnode.NewBytes([]byte("world")), + Data: []byte("world"), }, { Name: extensionName3, - Data: basicnode.NewBytes([]byte("cheese")), + Data: []byte("cheese"), }, } - defaultMergeFunc := func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error) { - oldData, err := oldNode.AsBytes() - require.NoError(t, err) - newData, err := newNode.AsBytes() - require.NoError(t, err) - return basicnode.NewBytes([]byte(string(oldData) + " " + string(newData))), nil + defaultMergeFunc := func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { + return []byte(string(oldData) + " " + string(newData)), nil } root := testutil.GenerateCids(1)[0] ssb := builder.NewSelectorSpecBuilder(basicnode.Prototype.Any) @@ -339,38 +332,38 @@ func TestMergeExtensions(t *testing.T) { emptyRequest := 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) + 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.NewBytes([]byte("world")), extData2) + require.Equal(t, []byte("world"), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, basicnode.NewBytes([]byte("cheese")), extData3) + require.Equal(t, []byte("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) + 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.NewBytes([]byte("applesauce")), extData1) + require.Equal(t, []byte("applesauce"), extData1) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, basicnode.NewBytes([]byte("hello world")), extData2) + require.Equal(t, []byte("hello world"), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, basicnode.NewBytes([]byte("cheese")), extData3) + require.Equal(t, []byte("cheese"), extData3) }) t.Run("when merging errors", func(t *testing.T) { - errorMergeFunc := func(name graphsync.ExtensionName, oldNode, newNode ipld.Node) (ipld.Node, error) { + errorMergeFunc := func(name graphsync.ExtensionName, oldData []byte, newData []byte) ([]byte, error) { return nil, errors.New("something went wrong") } _, err := defaultRequest.MergeExtensions(replacementExtensions, errorMergeFunc) @@ -378,19 +371,19 @@ func TestMergeExtensions(t *testing.T) { }) 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) + 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.NewBytes([]byte("applesauce")), extData1) + require.Equal(t, []byte("applesauce"), extData1) extData2, has := resultRequest.Extension(extensionName2) require.True(t, has) - require.Equal(t, basicnode.NewBytes([]byte("world")), extData2) + require.Equal(t, []byte("world"), extData2) extData3, has := resultRequest.Extension(extensionName3) require.True(t, has) - require.Equal(t, basicnode.NewBytes([]byte("cheese")), extData3) + require.Equal(t, []byte("cheese"), extData3) }) } diff --git a/message/messagehandler.go b/message/messagehandler.go index 48262951..0f3355fa 100644 --- a/message/messagehandler.go +++ b/message/messagehandler.go @@ -6,6 +6,7 @@ import ( "io" "sync" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/ipldutil" @@ -83,41 +84,41 @@ func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncM // ToProto converts a GraphSyncMessage to its pb.Message equivalent func (mh *MessageHandler) ToProto(gsm GraphSyncMessage) (*pb.Message, error) { pbm := new(pb.Message) - pbm.Requests = make([]*pb.Message_Request, 0, len(gsm.Requests)) - for _, request := range gsm.Requests { + 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 request.selector != nil { + selector, err = ipldutil.EncodeNode(request.selector) if err != nil { return nil, err } } pbm.Requests = append(pbm.Requests, &pb.Message_Request{ - Id: request.ID.Bytes(), - Root: request.Root.Bytes(), + Id: request.id.Bytes(), + Root: request.root.Bytes(), Selector: selector, - Priority: int32(request.Priority), - Cancel: request.Cancel, - Update: request.Update, - Extensions: toProtoExtensions(request.Extensions), + 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 = make([]*pb.Message_Response, 0, len(gsm.responses)) + for _, response := range gsm.responses { pbm.Responses = append(pbm.Responses, &pb.Message_Response{ - Id: response.ID.Bytes(), - Status: int32(response.Status), - Extensions: toProtoExtensions(response.Extensions), + Id: response.requestID.Bytes(), + Status: int32(response.status), + Extensions: response.extensions, }) } - pbm.Data = make([]*pb.Message_Block, 0, len(gsm.Blocks)) - for _, b := range gsm.Blocks { + pbm.Data = make([]*pb.Message_Block, 0, len(gsm.blocks)) + for _, b := range gsm.blocks { pbm.Data = append(pbm.Data, &pb.Message_Block{ - Prefix: b.Prefix, - Data: b.Data, + Prefix: b.Cid().Prefix().Bytes(), + Data: b.RawData(), }) } return pbm, nil @@ -129,49 +130,49 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag defer mh.mapLock.Unlock() pbm := new(pb.Message_V1_0_0) - pbm.Requests = make([]*pb.Message_V1_0_0_Request, 0, len(gsm.Requests)) - for _, request := range gsm.Requests { + pbm.Requests = make([]*pb.Message_V1_0_0_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 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()) + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, request.id.Bytes()) if err != nil { return nil, err } pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ Id: rid, - Root: request.Root.Bytes(), + Root: request.root.Bytes(), Selector: selector, - Priority: int32(request.Priority), - Cancel: request.Cancel, - Update: request.Update, - Extensions: toProtoExtensions(request.Extensions), + Priority: int32(request.priority), + Cancel: request.isCancel, + Update: request.isUpdate, + Extensions: request.extensions, }) } - pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(gsm.Responses)) - for _, response := range gsm.Responses { - rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.ID.Bytes()) + pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(gsm.responses)) + for _, response := range gsm.responses { + rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.requestID.Bytes()) if err != nil { return nil, err } pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ Id: rid, - Status: int32(response.Status), - Extensions: toProtoExtensions(response.Extensions), + Status: int32(response.status), + Extensions: response.extensions, }) } - pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(gsm.Blocks)) - for _, b := range gsm.Blocks { + pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(gsm.blocks)) + for _, b := range gsm.blocks { pbm.Data = append(pbm.Data, &pb.Message_V1_0_0_Block{ - Prefix: b.Prefix, - Data: b.Data, + Prefix: b.Cid().Prefix().Bytes(), + Data: b.RawData(), }) } return pbm, nil @@ -217,23 +218,6 @@ func (mh *MessageHandler) ToNetV1(p peer.ID, gsm GraphSyncMessage, w io.Writer) return err } -func toProtoExtensions(m GraphSyncExtensions) map[string][]byte { - protoExts := make(map[string][]byte, len(m.Values)) - for name, node := range m.Values { - // Only keep those which are plain bytes, - // as those are the only ones that the older protocol clients understand. - if node.Kind() != ipld.Kind_Bytes { - continue - } - raw, err := node.AsBytes() - if err != nil { - panic(err) // shouldn't happen - } - protoExts[name] = raw - } - return protoExts -} - // 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) { @@ -266,8 +250,8 @@ func intIdToRequestId(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, // Mapping from a pb.Message object to a GraphSyncMessage object func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { - requests := make([]GraphSyncRequest, len(pbm.GetRequests())) - for i, req := range pbm.Requests { + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.GetRequests())) + for _, req := range pbm.Requests { if req == nil { return GraphSyncMessage{}, errors.New("request is nil") } @@ -291,14 +275,12 @@ func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage if err != nil { return GraphSyncMessage{}, err } - // TODO: we likely need to turn some "core" extensions to fields, - // as some of those got moved to proper fields in the new protocol. - // Same for responses above, as well as the "to proto" funcs. - requests[i] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, fromProtoExtensions(req.GetExtensions())) + exts := req.GetExtensions() + requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) } - responses := make([]GraphSyncResponse, len(pbm.GetResponses())) - for i, res := range pbm.Responses { + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.GetResponses())) + for _, res := range pbm.Responses { if res == nil { return GraphSyncMessage{}, errors.New("response is nil") } @@ -306,19 +288,32 @@ func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage if err != nil { return GraphSyncMessage{}, err } - responses[i] = newResponse(id, graphsync.ResponseStatusCode(res.Status), fromProtoExtensions(res.GetExtensions())) + exts := res.GetExtensions() + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) } - blks := make([]GraphSyncBlock, len(pbm.GetData())) - for i, b := range pbm.Data { + 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") } - blks[i] = GraphSyncBlock{ - Prefix: b.GetPrefix(), - Data: b.GetData(), + 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{ @@ -332,8 +327,8 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ mh.mapLock.Lock() defer mh.mapLock.Unlock() - requests := make([]GraphSyncRequest, len(pbm.GetRequests())) - for i, req := range pbm.Requests { + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.GetRequests())) + for _, req := range pbm.Requests { if req == nil { return GraphSyncMessage{}, errors.New("request is nil") } @@ -357,14 +352,12 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ if err != nil { return GraphSyncMessage{}, err } - // TODO: we likely need to turn some "core" extensions to fields, - // as some of those got moved to proper fields in the new protocol. - // Same for responses above, as well as the "to proto" funcs. - requests[i] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, fromProtoExtensions(req.GetExtensions())) + exts := req.GetExtensions() + requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) } - responses := make([]GraphSyncResponse, len(pbm.GetResponses())) - for i, res := range pbm.Responses { + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(pbm.GetResponses())) + for _, res := range pbm.Responses { if res == nil { return GraphSyncMessage{}, errors.New("response is nil") } @@ -372,19 +365,32 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ if err != nil { return GraphSyncMessage{}, err } - responses[i] = newResponse(id, graphsync.ResponseStatusCode(res.Status), fromProtoExtensions(res.GetExtensions())) + exts := res.GetExtensions() + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) } - blks := make([]GraphSyncBlock, len(pbm.GetData())) - for i, b := range pbm.Data { + 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") } - blks[i] = GraphSyncBlock{ - Prefix: b.GetPrefix(), - Data: b.GetData(), + 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{ From ea4a06ad90a6ae5f12114dbe280d09aa9b9265a4 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 14 Jan 2022 14:38:08 +1100 Subject: [PATCH 10/32] feat(net): introduce 2.0.0 protocol for dag-cbor --- benchmarks/testnet/virtual.go | 2 +- message/bench_test.go | 4 +- message/ipldbind/message.go | 2 +- message/ipldbind/schema.ipldsch | 2 +- message/message.go | 124 ++----------------------- message/message_test.go | 10 +-- message/messagehandler.go | 154 ++++++++++++++++++++++++++++++-- network/interface.go | 1 + network/libp2p_impl.go | 11 ++- 9 files changed, 174 insertions(+), 136 deletions(-) diff --git a/benchmarks/testnet/virtual.go b/benchmarks/testnet/virtual.go index 063feca8..01d26de4 100644 --- a/benchmarks/testnet/virtual.go +++ b/benchmarks/testnet/virtual.go @@ -137,7 +137,7 @@ func (n *network) SendMessage( rateLimiters[to] = rateLimiter } - pbMsg, err := gsmsg.NewMessageHandler().ToProto(mes) + pbMsg, err := gsmsg.NewMessageHandler().ToProtoV11(mes) if err != nil { return err } diff --git a/message/bench_test.go b/message/bench_test.go index f2565eae..f91e227e 100644 --- a/message/bench_test.go +++ b/message/bench_test.go @@ -73,7 +73,7 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { for pb.Next() { buf.Reset() - ipldGSM, err := gsm.ToIPLD() + ipldGSM, err := NewMessageHandler().ToIPLD(gsm) require.NoError(b, err) node := bindnode.Wrap(ipldGSM, ipldbind.Prototype.Message.Type()) err = dagcbor.Encode(node.Representation(), buf) @@ -84,7 +84,7 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { require.NoError(b, err) node2 := builder.Build() ipldGSM2 := bindnode.Unwrap(node2).(*ipldbind.GraphSyncMessage) - gsm2, err := messageFromIPLD(ipldGSM2) + gsm2, err := NewMessageHandler().messageFromIPLD(ipldGSM2) require.NoError(b, err) // same as above. diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go index fcc71f95..5966199d 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -50,7 +50,7 @@ type GraphSyncRequest struct { Id []byte Root cid.Cid - Selector ipld.Node + Selector *ipld.Node Extensions GraphSyncExtensions Priority graphsync.Priority Cancel bool diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch index 060ac50d..53e68046 100644 --- a/message/ipldbind/schema.ipldsch +++ b/message/ipldbind/schema.ipldsch @@ -37,7 +37,7 @@ type GraphSyncResponseStatusCode enum { type GraphSyncRequest struct { id GraphSyncRequestID (rename "ID") # unique id set on the requester side root Link (rename "Root") # a CID for the root node in the query - selector Any (rename "Sel") # see https://github.com/ipld/specs/blob/master/selectors/selectors.md + selector nullable Any (rename "Sel") # see https://github.com/ipld/specs/blob/master/selectors/selectors.md extensions GraphSyncExtensions (rename "Ext") # side channel information priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 cancel Bool (rename "Canc") # whether this cancels a request diff --git a/message/message.go b/message/message.go index ace17d7e..55965d5c 100644 --- a/message/message.go +++ b/message/message.go @@ -3,7 +3,6 @@ package message import ( "bytes" "fmt" - "io" "strings" blocks "github.com/ipfs/go-block-format" @@ -12,37 +11,8 @@ import ( "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/message/ipldbind" - 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() -} - -// 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 -} - // GraphSyncRequest is a struct to capture data on a request contained in a // GraphSyncMessage. type GraphSyncRequest struct { @@ -57,11 +27,15 @@ type GraphSyncRequest struct { // String returns a human-readable form of a GraphSyncRequest func (gsr GraphSyncRequest) String() string { - var buf bytes.Buffer - dagjson.Encode(gsr.selector, &buf) + sel := "nil" + if gsr.selector != nil { + var buf bytes.Buffer + dagjson.Encode(gsr.selector, &buf) + sel = buf.String() + } return fmt.Sprintf("GraphSyncRequest", gsr.root.String(), - buf.String(), + sel, gsr.priority, gsr.id.String(), gsr.isCancel, @@ -99,7 +73,8 @@ type GraphSyncMessage struct { // its contents func (gsm GraphSyncMessage) String() string { cts := make([]string, 0) - for _, req := range gsm.requests { + for i, req := range gsm.requests { + fmt.Printf("req.String(%v)\n", i) cts = append(cts, req.String()) } for _, resp := range gsm.responses { @@ -213,87 +188,6 @@ func (gsm GraphSyncMessage) Blocks() []blocks.Block { return bs } -func (gsm GraphSyncMessage) ToIPLD() (*ipldbind.GraphSyncMessage, error) { - ibm := new(ipldbind.GraphSyncMessage) - ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(gsm.requests)) - for _, request := range gsm.requests { - ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ - Id: request.id.Bytes(), - Root: request.root, - Selector: request.selector, - Priority: request.priority, - Cancel: request.isCancel, - Update: request.isUpdate, - // Extensions: request.extensions, - }) - } - - ibm.Responses = make([]ipldbind.GraphSyncResponse, 0, len(gsm.responses)) - for _, response := range gsm.responses { - ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ - Id: response.requestID.Bytes(), - Status: response.status, - // Extensions: response.extensions, - }) - } - - blocks := gsm.Blocks() - ibm.Blocks = make([]ipldbind.GraphSyncBlock, 0, len(blocks)) - for _, b := range blocks { - ibm.Blocks = append(ibm.Blocks, ipldbind.GraphSyncBlock{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), - }) - } - return ibm, nil -} - -func messageFromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(ibm.Requests)) - for _, req := range ibm.Requests { - // exts := req.Extensions - id, err := graphsync.ParseRequestID(req.Id) - if err != nil { - return GraphSyncMessage{}, err - } - requests[id] = newRequest(id, req.Root, req.Selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, nil) - } - - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(ibm.Responses)) - for _, res := range ibm.Responses { - // exts := res.Extensions - id, err := graphsync.ParseRequestID(res.Id) - if err != nil { - return GraphSyncMessage{}, err - } - responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), nil) - } - - blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) - for _, b := range ibm.Blocks { - pref, err := cid.PrefixFromBytes(b.Prefix) - if err != nil { - return GraphSyncMessage{}, err - } - - c, err := pref.Sum(b.Data) - if err != nil { - return GraphSyncMessage{}, err - } - - blk, err := blocks.NewBlockWithCid(b.Data, c) - if err != nil { - return GraphSyncMessage{}, err - } - - blks[blk.Cid()] = blk - } - - return GraphSyncMessage{ - requests, responses, blks, - }, nil -} - func (gsm GraphSyncMessage) Loggable() map[string]interface{} { requests := make([]string, 0, len(gsm.requests)) for _, request := range gsm.requests { diff --git a/message/message_test.go b/message/message_test.go index 20f73aee..2ffbcba3 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -45,7 +45,7 @@ func TestAppendingRequests(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := NewMessageHandler().ToProto(gsm) + pbMessage, err := NewMessageHandler().ToProtoV11(gsm) require.NoError(t, err, "serialize to protobuf errored") selectorEncoded, err := ipldutil.EncodeNode(selector) require.NoError(t, err) @@ -59,7 +59,7 @@ func TestAppendingRequests(t *testing.T) { require.Equal(t, selectorEncoded, pbRequest.Selector) require.Equal(t, map[string][]byte{"graphsync/awesome": extension.Data}, pbRequest.Extensions) - deserialized, err := NewMessageHandler().newMessageFromProto(pbMessage) + deserialized, err := NewMessageHandler().newMessageFromProtoV11(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") deserializedRequests := deserialized.Requests() require.Len(t, deserializedRequests, 1, "did not add request to deserialized message") @@ -99,14 +99,14 @@ func TestAppendingResponses(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := NewMessageHandler().ToProto(gsm) + pbMessage, err := NewMessageHandler().ToProtoV11(gsm) require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] require.Equal(t, requestID.Bytes(), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, extension.Data, pbResponse.Extensions["graphsync/awesome"]) - deserialized, err := NewMessageHandler().newMessageFromProto(pbMessage) + deserialized, err := NewMessageHandler().newMessageFromProtoV11(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") deserializedResponses := deserialized.Responses() require.Len(t, deserializedResponses, 1, "did not add response to deserialized message") @@ -132,7 +132,7 @@ func TestAppendBlock(t *testing.T) { m, err := builder.Build() require.NoError(t, err) - pbMessage, err := NewMessageHandler().ToProto(m) + pbMessage, err := NewMessageHandler().ToProtoV11(m) require.NoError(t, err, "serializing to protobuf errored") // assert strings are in proto message diff --git a/message/messagehandler.go b/message/messagehandler.go index 0f3355fa..4fde2478 100644 --- a/message/messagehandler.go +++ b/message/messagehandler.go @@ -1,8 +1,10 @@ package message import ( + "bytes" "encoding/binary" "errors" + "fmt" "io" "sync" @@ -10,8 +12,11 @@ import ( "github.com/ipfs/go-cid" "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/ipldutil" + "github.com/ipfs/go-graphsync/message/ipldbind" pb "github.com/ipfs/go-graphsync/message/pb" "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/node/bindnode" pool "github.com/libp2p/go-buffer-pool" "github.com/libp2p/go-libp2p-core/network" "github.com/libp2p/go-libp2p-core/peer" @@ -44,16 +49,34 @@ func NewMessageHandler() *MessageHandler { // FromNet can read a network stream to deserialized a GraphSyncMessage func (mh *MessageHandler) FromNet(r io.Reader) (GraphSyncMessage, error) { reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) - return mh.FromMsgReader(reader) + return mh.FromMsgReaderV11(reader) } -// FromMsgReader can deserialize a protobuf message into a GraphySyncMessage. +// FromMsgReader can deserialize a DAG-CBOR message into a GraphySyncMessage func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error) { msg, err := r.ReadMsg() if err != nil { return GraphSyncMessage{}, err } + builder := ipldbind.Prototype.Message.Representation().NewBuilder() + err = dagcbor.Decode(builder, bytes.NewReader(msg)) + if err != nil { + return GraphSyncMessage{}, err + } + + node := builder.Build() + ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessage) + return mh.messageFromIPLD(ipldGSM) +} + +// FromMsgReaderV11 can deserialize a protobuf message into a GraphySyncMessage +func (mh *MessageHandler) FromMsgReaderV11(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) @@ -61,10 +84,10 @@ func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error return GraphSyncMessage{}, err } - return mh.newMessageFromProto(&pb) + return mh.newMessageFromProtoV11(&pb) } -// FromMsgReaderV1 can deserialize a v1.0.0 protobuf message into a GraphySyncMessage. +// FromMsgReaderV1 can deserialize a v1.0.0 protobuf message into a GraphySyncMessage func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncMessage, error) { msg, err := r.ReadMsg() if err != nil { @@ -81,8 +104,45 @@ func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncM return mh.newMessageFromProtoV1(p, &pb) } +// ToProto converts a GraphSyncMessage to its ipldbind.GraphSyncMessage equivalent +func (mh *MessageHandler) ToIPLD(gsm GraphSyncMessage) (*ipldbind.GraphSyncMessage, error) { + ibm := new(ipldbind.GraphSyncMessage) + ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(gsm.requests)) + for _, request := range gsm.requests { + ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ + Id: request.id.Bytes(), + Root: request.root, + Selector: &request.selector, + Priority: request.priority, + Cancel: request.isCancel, + Update: request.isUpdate, + // Extensions: request.extensions, + }) + } + + ibm.Responses = make([]ipldbind.GraphSyncResponse, 0, len(gsm.responses)) + for _, response := range gsm.responses { + ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ + Id: response.requestID.Bytes(), + Status: response.status, + // Extensions: response.extensions, + }) + } + + blocks := gsm.Blocks() + ibm.Blocks = make([]ipldbind.GraphSyncBlock, 0, len(blocks)) + for _, b := range blocks { + ibm.Blocks = append(ibm.Blocks, ipldbind.GraphSyncBlock{ + Data: b.RawData(), + Prefix: b.Cid().Prefix().Bytes(), + }) + } + + return ibm, nil +} + // ToProto converts a GraphSyncMessage to its pb.Message equivalent -func (mh *MessageHandler) ToProto(gsm GraphSyncMessage) (*pb.Message, error) { +func (mh *MessageHandler) ToProtoV11(gsm GraphSyncMessage) (*pb.Message, error) { pbm := new(pb.Message) pbm.Requests = make([]*pb.Message_Request, 0, len(gsm.requests)) for _, request := range gsm.requests { @@ -178,9 +238,38 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag return pbm, nil } -// ToNet writes a GraphSyncMessage in its protobuf format to a writer +// ToNet writes a GraphSyncMessage in its DAG-CBOR format to a writer, +// prefixed with a length uvar func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { - msg, err := mh.ToProto(gsm) + fmt.Printf("gsm: %v\n", gsm.String()) + msg, err := mh.ToIPLD(gsm) + if err != nil { + return err + } + + fmt.Printf("ipldgsm: %v\n", msg) + + lbuf := make([]byte, binary.MaxVarintLen32) + 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.MaxVarintLen32)) + out := buf.Bytes() + copy(lbuf[:lbuflen], out[lbuflen:]) + + _, err = w.Write(out[lbuflen:]) + return err +} + +// ToNetV11 writes a GraphSyncMessage in its v1.1.0 protobuf format to a writer +func (mh *MessageHandler) ToNetV11(gsm GraphSyncMessage, w io.Writer) error { + msg, err := mh.ToProtoV11(gsm) if err != nil { return err } @@ -198,7 +287,7 @@ func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { return err } -// ToNet writes a GraphSyncMessage in its v1.0.0 protobuf format to a writer +// ToNetV1 writes a GraphSyncMessage in its v1.0.0 protobuf format to a writer func (mh *MessageHandler) ToNetV1(p peer.ID, gsm GraphSyncMessage, w io.Writer) error { msg, err := mh.ToProtoV1(p, gsm) if err != nil { @@ -248,8 +337,55 @@ func intIdToRequestId(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, return rid, nil } +// Mapping from a ipldbind.GraphSyncMessage object to a GraphSyncMessage object +func (mh *MessageHandler) messageFromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { + requests := make(map[graphsync.RequestID]GraphSyncRequest, len(ibm.Requests)) + for _, req := range ibm.Requests { + // exts := req.Extensions + id, err := graphsync.ParseRequestID(req.Id) + if err != nil { + return GraphSyncMessage{}, err + } + requests[id] = newRequest(id, req.Root, *req.Selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, nil) + } + + responses := make(map[graphsync.RequestID]GraphSyncResponse, len(ibm.Responses)) + for _, res := range ibm.Responses { + // exts := res.Extensions + id, err := graphsync.ParseRequestID(res.Id) + if err != nil { + return GraphSyncMessage{}, err + } + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), nil) + } + + blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) + for _, b := range ibm.Blocks { + pref, err := cid.PrefixFromBytes(b.Prefix) + if err != nil { + return GraphSyncMessage{}, err + } + + c, err := pref.Sum(b.Data) + if err != nil { + return GraphSyncMessage{}, err + } + + blk, err := blocks.NewBlockWithCid(b.Data, c) + if err != nil { + return GraphSyncMessage{}, err + } + + blks[blk.Cid()] = blk + } + + return GraphSyncMessage{ + requests, responses, blks, + }, nil +} + // Mapping from a pb.Message object to a GraphSyncMessage object -func (mh *MessageHandler) newMessageFromProto(pbm *pb.Message) (GraphSyncMessage, error) { +func (mh *MessageHandler) newMessageFromProtoV11(pbm *pb.Message) (GraphSyncMessage, error) { requests := make(map[graphsync.RequestID]GraphSyncRequest, len(pbm.GetRequests())) for _, req := range pbm.Requests { if req == nil { diff --git a/network/interface.go b/network/interface.go index 413050d7..ab65b150 100644 --- a/network/interface.go +++ b/network/interface.go @@ -14,6 +14,7 @@ var ( // ProtocolGraphsync is the protocol identifier for graphsync messages ProtocolGraphsync_1_0_0 protocol.ID = "/ipfs/graphsync/1.0.0" ProtocolGraphsync_1_1_0 protocol.ID = "/ipfs/graphsync/1.1.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 12e7b891..a295dbeb 100644 --- a/network/libp2p_impl.go +++ b/network/libp2p_impl.go @@ -37,7 +37,7 @@ func NewFromLibp2pHost(host host.Host, options ...Option) GraphSyncNetwork { graphSyncNetwork := libp2pGraphSyncNetwork{ host: host, messageHandler: gsmsg.NewMessageHandler(), - protocols: []protocol.ID{ProtocolGraphsync_1_1_0, ProtocolGraphsync_1_0_0}, + protocols: []protocol.ID{ProtocolGraphsync_1_1_0, ProtocolGraphsync_2_0_0, ProtocolGraphsync_1_0_0}, } for _, option := range options { @@ -94,6 +94,11 @@ func msgToStream(ctx context.Context, s network.Stream, mh *gsmsg.MessageHandler return err } case ProtocolGraphsync_1_1_0: + if err := mh.ToNetV11(msg, s); err != nil { + log.Debugf("error: %s", err) + return err + } + case ProtocolGraphsync_2_0_0: if err := mh.ToNet(msg, s); err != nil { log.Debugf("error: %s", err) return err @@ -172,6 +177,8 @@ func (gsnet *libp2pGraphSyncNetwork) handleNewStream(s network.Stream) { case ProtocolGraphsync_1_0_0: received, err = gsnet.messageHandler.FromMsgReaderV1(s.Conn().RemotePeer(), reader) case ProtocolGraphsync_1_1_0: + received, err = gsnet.messageHandler.FromMsgReaderV11(reader) + case ProtocolGraphsync_2_0_0: received, err = gsnet.messageHandler.FromMsgReader(reader) default: err = fmt.Errorf("unexpected protocol version %s", s.Protocol()) @@ -202,7 +209,7 @@ 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_1_1_0: + case ProtocolGraphsync_1_0_0, ProtocolGraphsync_1_1_0, ProtocolGraphsync_2_0_0: gsnet.protocols = append([]protocol.ID{}, proto) } } From 45b87ff3424f21c47ad43e89b593095c8b3663d3 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 14 Jan 2022 16:43:13 +1100 Subject: [PATCH 11/32] fix(net): more bindnode dag-cbor protocol fixes Not quite working yet, still need some upstream fixes and no extensions work has been attempted yet. --- go.mod | 2 ++ message/bench_test.go | 4 +-- message/ipldbind/message.go | 2 +- message/ipldbind/schema.ipldsch | 12 ++++---- message/messagehandler.go | 53 +++++++++++++++++++++++---------- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 3ab0f941..d587cb59 100644 --- a/go.mod +++ b/go.mod @@ -49,3 +49,5 @@ require ( golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/protobuf v1.27.1 ) + +replace github.com/ipld/go-ipld-prime => ../../ipld/go-ipld-prime diff --git a/message/bench_test.go b/message/bench_test.go index f91e227e..6d54eda6 100644 --- a/message/bench_test.go +++ b/message/bench_test.go @@ -73,7 +73,7 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { for pb.Next() { buf.Reset() - ipldGSM, err := NewMessageHandler().ToIPLD(gsm) + ipldGSM, err := NewMessageHandler().toIPLD(gsm) require.NoError(b, err) node := bindnode.Wrap(ipldGSM, ipldbind.Prototype.Message.Type()) err = dagcbor.Encode(node.Representation(), buf) @@ -84,7 +84,7 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { require.NoError(b, err) node2 := builder.Build() ipldGSM2 := bindnode.Unwrap(node2).(*ipldbind.GraphSyncMessage) - gsm2, err := NewMessageHandler().messageFromIPLD(ipldGSM2) + gsm2, err := NewMessageHandler().fromIPLD(ipldGSM2) require.NoError(b, err) // same as above. diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go index 5966199d..3dd3cb82 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -49,7 +49,7 @@ type GraphSyncExtensions struct { type GraphSyncRequest struct { Id []byte - Root cid.Cid + Root *cid.Cid Selector *ipld.Node Extensions GraphSyncExtensions Priority graphsync.Priority diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch index 53e68046..ce7598ac 100644 --- a/message/ipldbind/schema.ipldsch +++ b/message/ipldbind/schema.ipldsch @@ -35,13 +35,13 @@ type GraphSyncResponseStatusCode enum { } representation int type GraphSyncRequest struct { - id GraphSyncRequestID (rename "ID") # unique id set on the requester side - root Link (rename "Root") # a CID for the root node in the query + id GraphSyncRequestID (rename "ID") # unique id set on the requester side + root nullable Link (rename "Root") # a CID for the root node in the query selector nullable Any (rename "Sel") # see https://github.com/ipld/specs/blob/master/selectors/selectors.md - extensions GraphSyncExtensions (rename "Ext") # side channel information - priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 - cancel Bool (rename "Canc") # whether this cancels a request - update Bool (rename "Updt") # whether this is an update to an in progress request + extensions GraphSyncExtensions (rename "Ext") # side channel information + priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 + cancel Bool (rename "Canc") # whether this cancels a request + update Bool (rename "Updt") # whether this is an update to an in progress request } representation map type GraphSyncResponse struct { diff --git a/message/messagehandler.go b/message/messagehandler.go index 4fde2478..aab33ec8 100644 --- a/message/messagehandler.go +++ b/message/messagehandler.go @@ -3,6 +3,7 @@ package message import ( "bytes" "encoding/binary" + "encoding/hex" "errors" "fmt" "io" @@ -14,8 +15,8 @@ import ( "github.com/ipfs/go-graphsync/ipldutil" "github.com/ipfs/go-graphsync/message/ipldbind" pb "github.com/ipfs/go-graphsync/message/pb" - "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/bindnode" pool "github.com/libp2p/go-buffer-pool" "github.com/libp2p/go-libp2p-core/network" @@ -49,7 +50,7 @@ func NewMessageHandler() *MessageHandler { // FromNet can read a network stream to deserialized a GraphSyncMessage func (mh *MessageHandler) FromNet(r io.Reader) (GraphSyncMessage, error) { reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) - return mh.FromMsgReaderV11(reader) + return mh.FromMsgReader(reader) } // FromMsgReader can deserialize a DAG-CBOR message into a GraphySyncMessage @@ -60,14 +61,16 @@ func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error } builder := ipldbind.Prototype.Message.Representation().NewBuilder() + fmt.Println(hex.EncodeToString(msg)) err = dagcbor.Decode(builder, bytes.NewReader(msg)) if err != nil { + fmt.Printf("dagcbor decode error %v", err) return GraphSyncMessage{}, err } node := builder.Build() ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessage) - return mh.messageFromIPLD(ipldGSM) + return mh.fromIPLD(ipldGSM) } // FromMsgReaderV11 can deserialize a protobuf message into a GraphySyncMessage @@ -105,14 +108,22 @@ func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncM } // ToProto converts a GraphSyncMessage to its ipldbind.GraphSyncMessage equivalent -func (mh *MessageHandler) ToIPLD(gsm GraphSyncMessage) (*ipldbind.GraphSyncMessage, error) { +func (mh *MessageHandler) toIPLD(gsm GraphSyncMessage) (*ipldbind.GraphSyncMessage, error) { ibm := new(ipldbind.GraphSyncMessage) ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(gsm.requests)) for _, request := range gsm.requests { + sel := &request.selector + if request.selector == nil { + sel = nil + } + root := &request.root + if request.root == cid.Undef { + root = nil + } ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ Id: request.id.Bytes(), - Root: request.root, - Selector: &request.selector, + Root: root, + Selector: sel, Priority: request.priority, Cancel: request.isCancel, Update: request.isUpdate, @@ -242,14 +253,14 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag // prefixed with a length uvar func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { fmt.Printf("gsm: %v\n", gsm.String()) - msg, err := mh.ToIPLD(gsm) + msg, err := mh.toIPLD(gsm) if err != nil { return err } fmt.Printf("ipldgsm: %v\n", msg) - lbuf := make([]byte, binary.MaxVarintLen32) + lbuf := make([]byte, binary.MaxVarintLen64) buf := new(bytes.Buffer) buf.Write(lbuf) @@ -258,12 +269,16 @@ func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { if err != nil { return err } + //_, err = buf.WriteTo(w) - lbuflen := binary.PutUvarint(lbuf, uint64(buf.Len()-binary.MaxVarintLen32)) + lbuflen := binary.PutUvarint(lbuf, uint64(buf.Len()-binary.MaxVarintLen64)) out := buf.Bytes() - copy(lbuf[:lbuflen], out[lbuflen:]) + // fmt.Printf("%v = %v - %v\n", uint64(buf.Len()-binary.MaxVarintLen64), hex.EncodeToString(lbuf), lbuf[:lbuflen]) + copy(out[binary.MaxVarintLen64-lbuflen:], lbuf[:lbuflen]) + + fmt.Println(hex.EncodeToString(out)) + _, err = w.Write(out[binary.MaxVarintLen64-lbuflen:]) - _, err = w.Write(out[lbuflen:]) return err } @@ -338,7 +353,7 @@ func intIdToRequestId(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, } // Mapping from a ipldbind.GraphSyncMessage object to a GraphSyncMessage object -func (mh *MessageHandler) messageFromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { +func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { requests := make(map[graphsync.RequestID]GraphSyncRequest, len(ibm.Requests)) for _, req := range ibm.Requests { // exts := req.Extensions @@ -346,7 +361,15 @@ func (mh *MessageHandler) messageFromIPLD(ibm *ipldbind.GraphSyncMessage) (Graph if err != nil { return GraphSyncMessage{}, err } - requests[id] = newRequest(id, req.Root, *req.Selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, nil) + root := cid.Undef + if req.Root != nil { + root = *req.Root + } + var selector datamodel.Node + if req.Selector != nil { + selector = *req.Selector + } + requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, nil) } responses := make(map[graphsync.RequestID]GraphSyncResponse, len(ibm.Responses)) @@ -400,7 +423,7 @@ func (mh *MessageHandler) newMessageFromProtoV11(pbm *pb.Message) (GraphSyncMess } } - var selector ipld.Node + var selector datamodel.Node if !req.Cancel && !req.Update { selector, err = ipldutil.DecodeNode(req.Selector) if err != nil { @@ -477,7 +500,7 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ } } - var selector ipld.Node + var selector datamodel.Node if !req.Cancel && !req.Update { selector, err = ipldutil.DecodeNode(req.Selector) if err != nil { From c3649f3eb18a547a5404f02c207a54b9709a68dc Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 24 Jan 2022 17:16:45 +1100 Subject: [PATCH 12/32] chore(metadata): convert metadata to bindnode --- metadata/metadata.go | 61 +++------------- metadata/metadata_cbor_gen.go | 129 ---------------------------------- metadata/metadata_test.go | 23 ++---- metadata/schema.go | 25 +++++++ metadata/schema.ipldsch | 6 ++ 5 files changed, 45 insertions(+), 199 deletions(-) delete mode 100644 metadata/metadata_cbor_gen.go create mode 100644 metadata/schema.go create mode 100644 metadata/schema.ipldsch diff --git a/metadata/metadata.go b/metadata/metadata.go index 5e79f8fa..072b8e76 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -1,12 +1,9 @@ package metadata import ( - "bytes" - "fmt" - "github.com/ipfs/go-cid" - cbg "github.com/whyrusleeping/cbor-gen" - xerrors "golang.org/x/xerrors" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" ) // Item is a single link traversed in a repsonse @@ -21,57 +18,17 @@ 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) +func DecodeMetadata(data datamodel.Node) (Metadata, error) { + builder := Prototype.Metadata.Representation().NewBuilder() + err := builder.AssignNode(data) 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 + 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) ([]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 +func EncodeMetadata(entries Metadata) datamodel.Node { + return bindnode.Wrap(&entries, Prototype.Metadata.Type()) } 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/metadata/metadata_test.go b/metadata/metadata_test.go index bc7fdb60..55488000 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -1,11 +1,11 @@ package metadata import ( - "bytes" "math/rand" + "os" "testing" - "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime/codec/dagjson" "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 +29,16 @@ 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") + dagjson.Encode(nd, os.Stdout) } diff --git a/metadata/schema.go b/metadata/schema.go new file mode 100644 index 00000000..afdf8d45 --- /dev/null +++ b/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/metadata/schema.ipldsch b/metadata/schema.ipldsch new file mode 100644 index 00000000..20e6c688 --- /dev/null +++ b/metadata/schema.ipldsch @@ -0,0 +1,6 @@ +type Item struct { + link Link + blockPresent Bool +} representation map + +type Metadata [Item] From cb458335b51f2ea0e4f06a985b7079628b41433a Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 25 Jan 2022 15:35:40 +1100 Subject: [PATCH 13/32] chore(net,extensions): wire up IPLD extensions, expose as Node instead of []byte * Extensions now working with new dag-cbor network protocol * dag-cbor network protocol still not default, most tests are still exercising the existing v1 protocol * Metadata now using bindnode instead of cbor-gen * []byte for deferred extensions decoding is now replaced with datamodel.Node everywhere. Internal extensions now using some form of go-ipld-prime decode to convert them to local types (metadata using bindnode, others using direct inspection). * V1 protocol also using dag-cbor decode of extensions data and exporting the bytes - this may be a breaking change for exising extensions - need to check whether this should be done differently. Maybe a try-decode and if it fails export a wrapped Bytes Node? --- cidset/cidset.go | 16 +-- cidset/cidset_test.go | 3 +- dedupkey/dedupkey.go | 16 +-- donotsendfirstblocks/donotsendfirstblocks.go | 16 +-- go.mod | 4 +- go.sum | 3 +- graphsync.go | 7 +- impl/graphsync_test.go | 39 +++--- message/bench_test.go | 4 +- message/builder.go | 5 +- message/builder_test.go | 11 +- message/ipldbind/message.go | 13 +- message/ipldbind/schema.ipldsch | 6 +- message/message.go | 23 ++-- message/message_test.go | 73 ++++++---- message/messagehandler.go | 128 +++++++++++++----- messagequeue/messagequeue_test.go | 2 +- metadata/metadata.go | 3 + network/libp2p_impl_test.go | 2 +- requestmanager/executor/executor.go | 5 +- requestmanager/executor/executor_test.go | 6 +- requestmanager/hooks/hooks_test.go | 10 +- requestmanager/requestmanager_test.go | 68 ++++------ requestmanager/utils.go | 1 - responsemanager/hooks/hooks_test.go | 14 +- .../queryexecutor/queryexecutor_test.go | 4 +- .../responseassembler/responseBuilder.go | 11 +- .../responseassembler_test.go | 5 +- responsemanager/responsemanager_test.go | 19 ++- 29 files changed, 297 insertions(+), 220 deletions(-) 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/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 d587cb59..fd950ec1 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,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.20220121142026-257b06219831 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 @@ -49,5 +49,3 @@ require ( golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 google.golang.org/protobuf v1.27.1 ) - -replace github.com/ipld/go-ipld-prime => ../../ipld/go-ipld-prime diff --git a/go.sum b/go.sum index 7a2c37f9..ca11c864 100644 --- a/go.sum +++ b/go.sum @@ -454,8 +454,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.20220121142026-257b06219831 h1:hHLYeedwqakiOMaGI6HWF84geJu2VL6OZ1DrrhyY70s= +github.com/ipld/go-ipld-prime v0.14.5-0.20220121142026-257b06219831/go.mod h1:QcE4Y9n/ZZr8Ijg5bGPT0GqYWgZ1704nH0RDcQtgTP0= 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= diff --git a/graphsync.go b/graphsync.go index f79463f6..8c75e778 100644 --- a/graphsync.go +++ b/graphsync.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -54,7 +55,7 @@ type ExtensionName string // ExtensionData is a name/data pair for a graphsync extension type ExtensionData struct { Name ExtensionName - Data []byte + Data datamodel.Node } const ( @@ -172,7 +173,7 @@ 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 @@ -188,7 +189,7 @@ 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) } // BlockData gives information about a block included in a graphsync response diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 8a81ea89..9a50fb4f 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -30,6 +30,7 @@ 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" @@ -137,7 +138,7 @@ func TestSendResponseToIncomingRequest(t *testing.T) { } td.gsnet1.SetDelegate(r) - var receivedRequestData []byte + var receivedRequestData datamodel.Node // initialize graphsync on second node to response to requests gsnet := td.GraphSyncHost2() gsnet.RegisterIncomingRequestHook( @@ -164,7 +165,7 @@ func TestSendResponseToIncomingRequest(t *testing.T) { // read the values sent back to requestor var received gsmsg.GraphSyncMessage var receivedBlocks []blocks.Block - var receivedExtensions [][]byte + var receivedExtensions []datamodel.Node for { var message receivedMessage testutil.AssertReceive(ctx, t, r.messageReceived, &message, "did not receive complete response") @@ -355,8 +356,8 @@ func TestGraphsyncRoundTrip(t *testing.T) { responder := td.GraphSyncHost2() assertComplete := assertCompletionFunction(responder, 1) - 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) { @@ -537,8 +538,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, @@ -609,8 +609,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, @@ -844,8 +843,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) @@ -945,8 +944,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() @@ -1639,8 +1638,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) { @@ -1718,12 +1717,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 } @@ -1814,18 +1813,18 @@ func newOptionalGsTestData(ctx context.Context, t *testing.T, network1Protocols 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, diff --git a/message/bench_test.go b/message/bench_test.go index 6d54eda6..221c0cd4 100644 --- a/message/bench_test.go +++ b/message/bench_test.go @@ -22,10 +22,12 @@ 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: testutil.RandomBytes(100), + Data: bb.Build(), } id := graphsync.NewRequestID() priority := graphsync.Priority(rand.Int31()) diff --git a/message/builder.go b/message/builder.go index 15017198..5ea86f92 100644 --- a/message/builder.go +++ b/message/builder.go @@ -111,10 +111,7 @@ 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 - } + mdRaw := metadata.EncodeMetadata(linkMap) b.extensions[requestID] = append(b.extensions[requestID], graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, Data: mdRaw, diff --git a/message/builder_test.go b/message/builder_test.go index 841197f3..bb3545e4 100644 --- a/message/builder_test.go +++ b/message/builder_test.go @@ -6,6 +6,7 @@ import ( "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" @@ -19,17 +20,19 @@ 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.NewRequestID() requestID2 := graphsync.NewRequestID() diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go index 3dd3cb82..cfb4d959 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -5,7 +5,6 @@ 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" "github.com/ipfs/go-graphsync" @@ -44,13 +43,21 @@ type GraphSyncExtensions struct { Values map[string]datamodel.Node } +func NewGraphSyncExtensions(values map[string]datamodel.Node) GraphSyncExtensions { + keys := make([]string, 0, len(values)) + for k := range values { + keys = append(keys, k) + } + return GraphSyncExtensions{keys, values} +} + // GraphSyncRequest is a struct to capture data on a request contained in a // GraphSyncMessage. type GraphSyncRequest struct { Id []byte Root *cid.Cid - Selector *ipld.Node + Selector *datamodel.Node Extensions GraphSyncExtensions Priority graphsync.Priority Cancel bool @@ -119,5 +126,5 @@ type GraphSyncMessage struct { // NamedExtension exists just for the purpose of the constructors. type NamedExtension struct { Name graphsync.ExtensionName - Data ipld.Node + Data datamodel.Node } diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch index ce7598ac..db89db93 100644 --- a/message/ipldbind/schema.ipldsch +++ b/message/ipldbind/schema.ipldsch @@ -1,4 +1,4 @@ -type GraphSyncExtensions {String:Any} +type GraphSyncExtensions {String:nullable Any} type GraphSyncRequestID bytes type GraphSyncPriority int @@ -36,8 +36,8 @@ type GraphSyncResponseStatusCode enum { type GraphSyncRequest struct { id GraphSyncRequestID (rename "ID") # unique id set on the requester side - root nullable Link (rename "Root") # a CID for the root node in the query - selector nullable Any (rename "Sel") # see https://github.com/ipld/specs/blob/master/selectors/selectors.md + 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 GraphSyncExtensions (rename "Ext") # side channel information priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 cancel Bool (rename "Canc") # whether this cancels a request diff --git a/message/message.go b/message/message.go index 55965d5c..586bb01c 100644 --- a/message/message.go +++ b/message/message.go @@ -9,6 +9,7 @@ import ( cid "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" + "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipfs/go-graphsync" ) @@ -20,7 +21,7 @@ type GraphSyncRequest struct { selector ipld.Node priority graphsync.Priority id graphsync.RequestID - extensions map[string][]byte + extensions map[string]datamodel.Node isCancel bool isUpdate bool } @@ -49,7 +50,7 @@ func (gsr GraphSyncRequest) String() string { type GraphSyncResponse struct { requestID graphsync.RequestID status graphsync.ResponseStatusCode - extensions map[string][]byte + extensions map[string]datamodel.Node } // String returns a human-readable form of a GraphSyncResponse @@ -106,9 +107,9 @@ func UpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionData return newRequest(id, cid.Cid{}, nil, 0, false, true, toExtensionsMap(extensions)) } -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 } @@ -122,7 +123,7 @@ func newRequest(id graphsync.RequestID, priority graphsync.Priority, isCancel bool, isUpdate bool, - extensions map[string][]byte) GraphSyncRequest { + extensions map[string]datamodel.Node) GraphSyncRequest { return GraphSyncRequest{ id: id, root: root, @@ -142,7 +143,7 @@ func NewResponse(requestID graphsync.RequestID, } func newResponse(requestID graphsync.RequestID, - status graphsync.ResponseStatusCode, extensions map[string][]byte) GraphSyncResponse { + status graphsync.ResponseStatusCode, extensions map[string]datamodel.Node) GraphSyncResponse { return GraphSyncResponse{ requestID: requestID, status: status, @@ -234,7 +235,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 } @@ -268,7 +269,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 } @@ -291,7 +292,7 @@ func (gsr GraphSyncResponse) ExtensionNames() []string { // 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 @@ -300,12 +301,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 } 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 { diff --git a/message/message_test.go b/message/message_test.go index 2ffbcba3..62c7d34e 100644 --- a/message/message_test.go +++ b/message/message_test.go @@ -8,8 +8,10 @@ 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" @@ -21,7 +23,7 @@ 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) @@ -57,7 +59,12 @@ func TestAppendingRequests(t *testing.T) { 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) + 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 := NewMessageHandler().newMessageFromProtoV11(pbMessage) require.NoError(t, err, "deserializing protobuf message errored") @@ -80,9 +87,11 @@ 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.NewRequestID() + p := peer.ID("test peer") + mh := NewMessageHandler() status := graphsync.RequestAcknowledged builder := NewBuilder() @@ -99,14 +108,14 @@ func TestAppendingResponses(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := NewMessageHandler().ToProtoV11(gsm) + pbMessage, err := mh.ToProtoV1(p, gsm) require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] - require.Equal(t, requestID.Bytes(), 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 := NewMessageHandler().newMessageFromProtoV11(pbMessage) + deserialized, err := mh.newMessageFromProtoV1(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") @@ -188,7 +197,7 @@ func TestRequestUpdate(t *testing.T) { extensionName := graphsync.ExtensionName("graphsync/awesome") extension := graphsync.ExtensionData{ Name: extensionName, - Data: testutil.RandomBytes(100), + Data: basicnode.NewBytes(testutil.RandomBytes(100)), } builder := NewBuilder() @@ -233,7 +242,7 @@ 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.NewRequestID() priority := graphsync.Priority(rand.Int31()) @@ -302,25 +311,33 @@ 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) @@ -340,10 +357,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 +371,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 +394,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 +418,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 := NewMessageHandler().FromNet(bytes.NewReader([]byte(input))) + msg1, err := mh.FromNetV1(p, bytes.NewReader([]byte(input))) if err != nil { continue } buf2 := new(bytes.Buffer) - err = NewMessageHandler().ToNet(msg1, buf2) + err = mh.ToNetV1(p, msg1, buf2) require.NoError(t, err) - msg2, err := NewMessageHandler().FromNet(buf2) + msg2, err := mh.FromNetV1(p, buf2) require.NoError(t, err) require.Equal(t, msg1, msg2) diff --git a/message/messagehandler.go b/message/messagehandler.go index aab33ec8..9360b749 100644 --- a/message/messagehandler.go +++ b/message/messagehandler.go @@ -3,9 +3,7 @@ package message import ( "bytes" "encoding/binary" - "encoding/hex" "errors" - "fmt" "io" "sync" @@ -17,6 +15,7 @@ import ( pb "github.com/ipfs/go-graphsync/message/pb" "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" pool "github.com/libp2p/go-buffer-pool" "github.com/libp2p/go-libp2p-core/network" @@ -53,6 +52,12 @@ func (mh *MessageHandler) FromNet(r io.Reader) (GraphSyncMessage, error) { return mh.FromMsgReader(reader) } +// FromNetV1 can read a v1.0.0 network stream to deserialized a GraphSyncMessage +func (mh *MessageHandler) FromNetV1(p peer.ID, r io.Reader) (GraphSyncMessage, error) { + reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) + return mh.FromMsgReaderV1(p, reader) +} + // FromMsgReader can deserialize a DAG-CBOR message into a GraphySyncMessage func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error) { msg, err := r.ReadMsg() @@ -61,10 +66,8 @@ func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error } builder := ipldbind.Prototype.Message.Representation().NewBuilder() - fmt.Println(hex.EncodeToString(msg)) err = dagcbor.Decode(builder, bytes.NewReader(msg)) if err != nil { - fmt.Printf("dagcbor decode error %v", err) return GraphSyncMessage{}, err } @@ -73,7 +76,7 @@ func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error return mh.fromIPLD(ipldGSM) } -// FromMsgReaderV11 can deserialize a protobuf message into a GraphySyncMessage +// FromMsgReaderV11 can deserialize a v1.1.0 protobuf message into a GraphySyncMessage func (mh *MessageHandler) FromMsgReaderV11(r msgio.Reader) (GraphSyncMessage, error) { msg, err := r.ReadMsg() if err != nil { @@ -121,22 +124,22 @@ func (mh *MessageHandler) toIPLD(gsm GraphSyncMessage) (*ipldbind.GraphSyncMessa root = nil } ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ - Id: request.id.Bytes(), - Root: root, - Selector: sel, - Priority: request.priority, - Cancel: request.isCancel, - Update: request.isUpdate, - // Extensions: request.extensions, + Id: request.id.Bytes(), + Root: root, + Selector: sel, + Priority: request.priority, + Cancel: request.isCancel, + Update: request.isUpdate, + Extensions: ipldbind.NewGraphSyncExtensions(request.extensions), }) } ibm.Responses = make([]ipldbind.GraphSyncResponse, 0, len(gsm.responses)) for _, response := range gsm.responses { ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ - Id: response.requestID.Bytes(), - Status: response.status, - // Extensions: response.extensions, + Id: response.requestID.Bytes(), + Status: response.status, + Extensions: ipldbind.NewGraphSyncExtensions(response.extensions), }) } @@ -165,6 +168,10 @@ func (mh *MessageHandler) ToProtoV11(gsm GraphSyncMessage) (*pb.Message, error) return nil, err } } + ext, err := toEncodedExtensions(request.extensions) + if err != nil { + return nil, err + } pbm.Requests = append(pbm.Requests, &pb.Message_Request{ Id: request.id.Bytes(), Root: request.root.Bytes(), @@ -172,16 +179,21 @@ func (mh *MessageHandler) ToProtoV11(gsm GraphSyncMessage) (*pb.Message, error) Priority: int32(request.priority), Cancel: request.isCancel, Update: request.isUpdate, - Extensions: request.extensions, + Extensions: ext, }) } pbm.Responses = make([]*pb.Message_Response, 0, len(gsm.responses)) for _, response := range gsm.responses { + ext, err := toEncodedExtensions(response.extensions) + if err != nil { + return nil, err + } + pbm.Responses = append(pbm.Responses, &pb.Message_Response{ Id: response.requestID.Bytes(), Status: int32(response.status), - Extensions: response.extensions, + Extensions: ext, }) } @@ -192,6 +204,7 @@ func (mh *MessageHandler) ToProtoV11(gsm GraphSyncMessage) (*pb.Message, error) Data: b.RawData(), }) } + return pbm, nil } @@ -215,6 +228,10 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag if err != nil { return nil, err } + ext, err := toEncodedExtensions(request.extensions) + if err != nil { + return nil, err + } pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ Id: rid, Root: request.root.Bytes(), @@ -222,7 +239,7 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag Priority: int32(request.priority), Cancel: request.isCancel, Update: request.isUpdate, - Extensions: request.extensions, + Extensions: ext, }) } @@ -232,10 +249,14 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag if err != nil { return nil, err } + ext, err := toEncodedExtensions(response.extensions) + if err != nil { + return nil, err + } pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ Id: rid, Status: int32(response.status), - Extensions: response.extensions, + Extensions: ext, }) } @@ -252,14 +273,11 @@ func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Messag // ToNet writes a GraphSyncMessage in its DAG-CBOR format to a writer, // prefixed with a length uvar func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { - fmt.Printf("gsm: %v\n", gsm.String()) msg, err := mh.toIPLD(gsm) if err != nil { return err } - fmt.Printf("ipldgsm: %v\n", msg) - lbuf := make([]byte, binary.MaxVarintLen64) buf := new(bytes.Buffer) buf.Write(lbuf) @@ -273,10 +291,7 @@ func (mh *MessageHandler) ToNet(gsm GraphSyncMessage, w io.Writer) error { lbuflen := binary.PutUvarint(lbuf, uint64(buf.Len()-binary.MaxVarintLen64)) out := buf.Bytes() - // fmt.Printf("%v = %v - %v\n", uint64(buf.Len()-binary.MaxVarintLen64), hex.EncodeToString(lbuf), lbuf[:lbuflen]) copy(out[binary.MaxVarintLen64-lbuflen:], lbuf[:lbuflen]) - - fmt.Println(hex.EncodeToString(out)) _, err = w.Write(out[binary.MaxVarintLen64-lbuflen:]) return err @@ -356,7 +371,6 @@ func intIdToRequestId(p peer.ID, fromV1Map map[v1RequestKey]graphsync.RequestID, func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { requests := make(map[graphsync.RequestID]GraphSyncRequest, len(ibm.Requests)) for _, req := range ibm.Requests { - // exts := req.Extensions id, err := graphsync.ParseRequestID(req.Id) if err != nil { return GraphSyncMessage{}, err @@ -369,7 +383,7 @@ func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMes if req.Selector != nil { selector = *req.Selector } - requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, nil) + requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, req.Extensions.Values) } responses := make(map[graphsync.RequestID]GraphSyncResponse, len(ibm.Responses)) @@ -379,7 +393,7 @@ func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMes if err != nil { return GraphSyncMessage{}, err } - responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), nil) + responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), res.Extensions.Values) } blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) @@ -434,10 +448,12 @@ func (mh *MessageHandler) newMessageFromProtoV11(pbm *pb.Message) (GraphSyncMess if err != nil { return GraphSyncMessage{}, err } - exts := req.GetExtensions() + exts, err := fromEncodedExtensions(req.GetExtensions()) + if err != nil { + return GraphSyncMessage{}, err + } requests[id] = newRequest(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 { @@ -447,7 +463,10 @@ func (mh *MessageHandler) newMessageFromProtoV11(pbm *pb.Message) (GraphSyncMess if err != nil { return GraphSyncMessage{}, err } - exts := res.GetExtensions() + exts, err := fromEncodedExtensions(res.GetExtensions()) + if err != nil { + return GraphSyncMessage{}, err + } responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) } @@ -511,7 +530,10 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ if err != nil { return GraphSyncMessage{}, err } - exts := req.GetExtensions() + exts, err := fromEncodedExtensions(req.GetExtensions()) + if err != nil { + return GraphSyncMessage{}, err + } requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, exts) } @@ -524,7 +546,10 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ if err != nil { return GraphSyncMessage{}, err } - exts := res.GetExtensions() + exts, err := fromEncodedExtensions(res.GetExtensions()) + if err != nil { + return GraphSyncMessage{}, err + } responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), exts) } @@ -556,3 +581,40 @@ func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_ requests, responses, blks, }, nil } + +func toEncodedExtensions(in map[string]datamodel.Node) (map[string][]byte, error) { + out := make(map[string][]byte, len(in)) + for name, data := range in { + if data == nil { + out[name] = nil + } else { + var buf bytes.Buffer + err := dagcbor.Encode(data, &buf) + if err != nil { + return nil, err + } + out[name] = buf.Bytes() + } + } + return out, nil +} + +func fromEncodedExtensions(in map[string][]byte) (map[string]datamodel.Node, error) { + if in == nil { + return make(map[string]datamodel.Node), nil + } + out := make(map[string]datamodel.Node, len(in)) + for name, data := range in { + if len(data) == 0 { + out[name] = nil + } else { + nb := basicnode.Prototype.Any.NewBuilder() + err := dagcbor.Decode(nb, bytes.NewReader(data)) + if err != nil { + return nil, err + } + out[name] = nb.Build() + } + } + return out, nil +} diff --git a/messagequeue/messagequeue_test.go b/messagequeue/messagequeue_test.go index 219db099..f1c38a97 100644 --- a/messagequeue/messagequeue_test.go +++ b/messagequeue/messagequeue_test.go @@ -132,7 +132,7 @@ func TestProcessingNotification(t *testing.T) { 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() diff --git a/metadata/metadata.go b/metadata/metadata.go index 072b8e76..ede29be6 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -19,6 +19,9 @@ 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 datamodel.Node) (Metadata, error) { + if data == nil { + return nil, nil + } builder := Prototype.Metadata.Representation().NewBuilder() err := builder.AssignNode(data) if err != nil { diff --git a/network/libp2p_impl_test.go b/network/libp2p_impl_test.go index 2cdf0938..a7cdebc3 100644 --- a/network/libp2p_impl_test.go +++ b/network/libp2p_impl_test.go @@ -75,7 +75,7 @@ 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.NewRequestID() priority := graphsync.Priority(rand.Int31()) diff --git a/requestmanager/executor/executor.go b/requestmanager/executor/executor.go index 92516c1e..672090bd 100644 --- a/requestmanager/executor/executor.go +++ b/requestmanager/executor/executor.go @@ -220,10 +220,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()) diff --git a/requestmanager/executor/executor_test.go b/requestmanager/executor/executor_test.go index 2346ddc8..501cbedb 100644 --- a/requestmanager/executor/executor_test.go +++ b/requestmanager/executor/executor_test.go @@ -11,6 +11,7 @@ import ( "github.com/ipfs/go-peertaskqueue/peertask" "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/ipld/go-ipld-prime/traversal" "github.com/libp2p/go-libp2p-core/peer" "github.com/stretchr/testify/require" @@ -171,7 +172,7 @@ func TestRequestExecutionBlockChain(t *testing.T) { }, "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) @@ -181,7 +182,8 @@ func TestRequestExecutionBlockChain(t *testing.T) { require.True(t, ree.requestsSent[1].request.IsUpdate()) 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) }, diff --git a/requestmanager/hooks/hooks_test.go b/requestmanager/hooks/hooks_test.go index 2df87541..d55c612b 100644 --- a/requestmanager/hooks/hooks_test.go +++ b/requestmanager/hooks/hooks_test.go @@ -20,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, @@ -99,13 +99,13 @@ 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, @@ -196,13 +196,13 @@ 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, diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index f63c2b09..d8431b3d 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -9,7 +9,9 @@ import ( blocks "github.com/ipfs/go-block-format" "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" @@ -59,11 +61,9 @@ func TestNormalSimultaneousFetch(t *testing.T) { 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") + firstMetadataEncoded1 := metadata.EncodeMetadata(firstMetadata1) firstMetadata2 := metadataForBlocks(blockChain2.Blocks(0, 3), true) - firstMetadataEncoded2, err := metadata.EncodeMetadata(firstMetadata2) - require.NoError(t, err, "did not encode metadata") + firstMetadataEncoded2 := metadata.EncodeMetadata(firstMetadata2) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(requestRecords[0].gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, @@ -93,8 +93,7 @@ func TestNormalSimultaneousFetch(t *testing.T) { moreBlocks := blockChain2.RemainderBlocks(3) moreMetadata := metadataForBlocks(moreBlocks, true) - moreMetadataEncoded, err := metadata.EncodeMetadata(moreMetadata) - require.NoError(t, err, "did not encode metadata") + moreMetadataEncoded := metadata.EncodeMetadata(moreMetadata) moreResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(requestRecords[1].gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, @@ -418,7 +417,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) @@ -448,8 +447,8 @@ 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{ @@ -470,7 +469,7 @@ 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") @@ -479,9 +478,9 @@ func TestEncodingExtensions(t *testing.T) { 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(), @@ -562,13 +561,12 @@ 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") + firstMetadataEncoded := metadata.EncodeMetadata(firstMetadata) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(gsr.ID(), graphsync.PartialResponse, graphsync.ExtensionData{ @@ -623,13 +621,12 @@ 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) + nextMetadataEncoded := metadata.EncodeMetadata(nextMetadata) secondResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ @@ -724,8 +721,7 @@ func TestOutgoingRequestHooks(t *testing.T) { require.Equal(t, "chainstore", key) md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) + mdEncoded := metadata.EncodeMetadata(md) mdExt := graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, Data: mdEncoded, @@ -787,8 +783,7 @@ func TestOutgoingRequestListeners(t *testing.T) { } md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) + mdEncoded := metadata.EncodeMetadata(md) mdExt := graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, Data: mdEncoded, @@ -840,8 +835,7 @@ func TestPauseResume(t *testing.T) { // Start processing responses md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) + mdEncoded := metadata.EncodeMetadata(md) responses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, @@ -853,7 +847,7 @@ func TestPauseResume(t *testing.T) { // 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(rr.gsr.ID()) require.EqualError(t, err, "request is not paused") close(holdForResumeAttempt) // verify responses sent read ONLY for blocks BEFORE the pause @@ -926,8 +920,7 @@ func TestPauseResumeExternal(t *testing.T) { // Start processing responses md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded, err := metadata.EncodeMetadata(md) - require.NoError(t, err) + mdEncoded := metadata.EncodeMetadata(md) responses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, @@ -951,7 +944,7 @@ func TestPauseResumeExternal(t *testing.T) { td.fal.CleanupRequest(peers[0], rr.gsr.ID()) // unpause - err = td.requestManager.UnpauseRequest(rr.gsr.ID(), td.extension1, td.extension2) + err := td.requestManager.UnpauseRequest(rr.gsr.ID(), td.extension1, td.extension2) require.NoError(t, err) // verify the correct new request with Do-no-send-cids & other extensions @@ -1058,8 +1051,7 @@ func metadataForBlocks(blks []blocks.Block, present bool) metadata.Metadata { 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") + metadataEncoded := metadata.EncodeMetadata(md) return graphsync.ExtensionData{ Name: graphsync.ExtensionMetadata, Data: metadataEncoded, @@ -1079,10 +1071,10 @@ type testData struct { persistence 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 @@ -1113,13 +1105,13 @@ func newTestData(ctx context.Context, t *testing.T) *testData { 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, diff --git a/requestmanager/utils.go b/requestmanager/utils.go index 60a94190..384f2e0c 100644 --- a/requestmanager/utils.go +++ b/requestmanager/utils.go @@ -18,7 +18,6 @@ func metadataForResponses(responses []gsmsg.GraphSyncResponse) map[graphsync.Req } md, err := metadata.DecodeMetadata(mdRaw) if err != nil { - log.Warnf("Unable to decode metadata in response for request id: %s", response.RequestID().String()) continue } responseMetadata[response.RequestID()] = md diff --git a/responsemanager/hooks/hooks_test.go b/responsemanager/hooks/hooks_test.go index 3d0e7812..e2e8aad4 100644 --- a/responsemanager/hooks/hooks_test.go +++ b/responsemanager/hooks/hooks_test.go @@ -40,13 +40,13 @@ 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, @@ -218,13 +218,13 @@ 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, @@ -296,18 +296,18 @@ 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, diff --git a/responsemanager/queryexecutor/queryexecutor_test.go b/responsemanager/queryexecutor/queryexecutor_test.go index 937bc79c..15fa66bd 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 @@ -277,7 +277,7 @@ func newTestData(t *testing.T, blockCount int, expectedTraverse int) (*testData, 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] diff --git a/responsemanager/responseassembler/responseBuilder.go b/responsemanager/responseassembler/responseBuilder.go index e395a871..25af0ec5 100644 --- a/responsemanager/responseassembler/responseBuilder.go +++ b/responsemanager/responseassembler/responseBuilder.go @@ -1,11 +1,13 @@ package responseassembler import ( + "bytes" "context" 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" @@ -102,7 +104,14 @@ func (eo extensionOperation) build(builder *messagequeue.Builder) { } func (eo extensionOperation) size() uint64 { - return uint64(len(eo.extension.Data)) + // TODO: this incurs a double-encode, this first one is just to get the expected length; + // can we avoid this? + if eo.extension.Data == nil { + return 0 + } + var buf bytes.Buffer + dagcbor.Encode(eo.extension.Data, &buf) + return uint64(buf.Len()) } type blockOperation struct { diff --git a/responsemanager/responseassembler/responseassembler_test.go b/responsemanager/responseassembler/responseassembler_test.go index e8cc1f03..96845895 100644 --- a/responsemanager/responseassembler/responseassembler_test.go +++ b/responsemanager/responseassembler/responseassembler_test.go @@ -10,6 +10,7 @@ import ( 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" @@ -193,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, diff --git a/responsemanager/responsemanager_test.go b/responsemanager/responsemanager_test.go index 045512a6..bd547727 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" @@ -433,8 +434,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 +455,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{ @@ -1052,12 +1051,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,18 +1125,18 @@ 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, From 9c08dd03869836283493e0de60116003df2b2376 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 25 Jan 2022 15:49:38 +1100 Subject: [PATCH 14/32] fix(src): fix imports --- message/bench_test.go | 7 ++++--- message/messagehandler.go | 9 +++++---- message/pb/message.pb.go | 5 +++-- message/pb/message_v1_0_0.pb.go | 5 +++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/message/bench_test.go b/message/bench_test.go index 221c0cd4..67cfcd92 100644 --- a/message/bench_test.go +++ b/message/bench_test.go @@ -8,14 +8,15 @@ import ( "github.com/google/go-cmp/cmp" blocks "github.com/ipfs/go-block-format" - "github.com/ipfs/go-graphsync" - "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/node/basicnode" "github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/traversal/selector/builder" "github.com/stretchr/testify/require" + + "github.com/ipfs/go-graphsync" + "github.com/ipfs/go-graphsync/message/ipldbind" + "github.com/ipfs/go-graphsync/testutil" ) func BenchmarkMessageEncodingRoundtrip(b *testing.B) { diff --git a/message/messagehandler.go b/message/messagehandler.go index 9360b749..7eac108b 100644 --- a/message/messagehandler.go +++ b/message/messagehandler.go @@ -9,10 +9,6 @@ import ( 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/ipldbind" - pb "github.com/ipfs/go-graphsync/message/pb" "github.com/ipld/go-ipld-prime/codec/dagcbor" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipld/go-ipld-prime/node/basicnode" @@ -22,6 +18,11 @@ import ( "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/ipldbind" + pb "github.com/ipfs/go-graphsync/message/pb" ) type v1RequestKey struct { diff --git a/message/pb/message.pb.go b/message/pb/message.pb.go index 7a55b06f..897027eb 100644 --- a/message/pb/message.pb.go +++ b/message/pb/message.pb.go @@ -7,10 +7,11 @@ package graphsync_message_pb import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/message/pb/message_v1_0_0.pb.go b/message/pb/message_v1_0_0.pb.go index 6cacd3ec..e567758e 100644 --- a/message/pb/message_v1_0_0.pb.go +++ b/message/pb/message_v1_0_0.pb.go @@ -7,10 +7,11 @@ package graphsync_message_pb import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( From 989cf66fdc571a5cd927d9c0f5e5e3a69aa8e6d4 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 25 Jan 2022 15:53:09 +1100 Subject: [PATCH 15/32] fix(mod): clean up go.mod --- go.mod | 4 ---- go.sum | 9 +++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index fd950ec1..dd5fd85f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.16 require ( github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.3.0 - github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect 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 @@ -39,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 ca11c864..1a3fdd32 100644 --- a/go.sum +++ b/go.sum @@ -290,9 +290,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= @@ -1024,9 +1023,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= @@ -1081,9 +1079,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= From fc58e2348a20d1c0965e48889922c8eb325dec88 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 25 Jan 2022 20:54:50 +1100 Subject: [PATCH 16/32] fix(net): refactor message version format code to separate packages --- benchmarks/testnet/virtual.go | 3 +- message/{ => bench}/bench_test.go | 43 +- message/ipldbind/message.go | 85 +--- message/ipldbind/schema.go | 3 - message/message.go | 53 +- message/messagehandler.go | 621 ------------------------ message/v1/message.go | 314 ++++++++++++ message/{ => v1}/message_test.go | 66 +-- message/v2/message.go | 190 ++++++++ message/v2/message_test.go | 408 ++++++++++++++++ network/interface.go | 1 - network/libp2p_impl.go | 28 +- network/libp2p_impl_test.go | 6 +- peermanager/peermessagemanager_test.go | 2 +- requestmanager/executor/executor.go | 4 +- requestmanager/server.go | 6 +- responsemanager/hooks/hooks_test.go | 2 +- responsemanager/responsemanager_test.go | 6 +- responsemanager/server.go | 18 +- 19 files changed, 1074 insertions(+), 785 deletions(-) rename message/{ => bench}/bench_test.go (63%) delete mode 100644 message/messagehandler.go create mode 100644 message/v1/message.go rename message/{ => v1}/message_test.go (90%) create mode 100644 message/v2/message.go create mode 100644 message/v2/message_test.go diff --git a/benchmarks/testnet/virtual.go b/benchmarks/testnet/virtual.go index 01d26de4..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 := gsmsg.NewMessageHandler().ToProtoV11(mes) + pbMsg, err := gsmsgv1.NewMessageHandler().ToProto(peer.ID("foo"), mes) if err != nil { return err } diff --git a/message/bench_test.go b/message/bench/bench_test.go similarity index 63% rename from message/bench_test.go rename to message/bench/bench_test.go index 67cfcd92..3d2b4033 100644 --- a/message/bench_test.go +++ b/message/bench/bench_test.go @@ -8,14 +8,16 @@ import ( "github.com/google/go-cmp/cmp" blocks "github.com/ipfs/go-block-format" - "github.com/ipld/go-ipld-prime/codec/dagcbor" + "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/node/basicnode" - "github.com/ipld/go-ipld-prime/node/bindnode" "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/ipldbind" + "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" ) @@ -34,9 +36,9 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { priority := graphsync.Priority(rand.Int31()) status := graphsync.RequestAcknowledged - builder := NewBuilder() - builder.AddRequest(NewRequest(id, root, selector, priority, extension)) - builder.AddRequest(NewRequest(id, root, selector, priority)) + 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"))) @@ -44,6 +46,7 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { 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) @@ -51,18 +54,22 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { buf := new(bytes.Buffer) + mh := v1.NewMessageHandler() for pb.Next() { buf.Reset() - err := NewMessageHandler().ToNet(gsm, buf) + err := mh.ToNet(p, gsm, buf) require.NoError(b, err) - gsm2, err := NewMessageHandler().FromNet(buf) + 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 })); diff != "" { + if diff := cmp.Diff(gsm, gsm2, + cmp.Exporter(func(reflect.Type) bool { return true }), + cmp.Comparer(ipld.DeepEqual), + ); diff != "" { b.Fatal(diff) } } @@ -73,25 +80,21 @@ func BenchmarkMessageEncodingRoundtrip(b *testing.B) { b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { buf := new(bytes.Buffer) + mh := v2.NewMessageHandler() for pb.Next() { buf.Reset() - ipldGSM, err := NewMessageHandler().toIPLD(gsm) - require.NoError(b, err) - node := bindnode.Wrap(ipldGSM, ipldbind.Prototype.Message.Type()) - err = dagcbor.Encode(node.Representation(), buf) + err := mh.ToNet(p, gsm, buf) require.NoError(b, err) - builder := ipldbind.Prototype.Message.Representation().NewBuilder() - err = dagcbor.Decode(builder, buf) - require.NoError(b, err) - node2 := builder.Build() - ipldGSM2 := bindnode.Unwrap(node2).(*ipldbind.GraphSyncMessage) - gsm2, err := NewMessageHandler().fromIPLD(ipldGSM2) + 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 })); diff != "" { + 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/ipldbind/message.go b/message/ipldbind/message.go index cfb4d959..7199524b 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -1,41 +1,15 @@ package ipldbind import ( - "io" - - blocks "github.com/ipfs/go-block-format" cid "github.com/ipfs/go-cid" "github.com/ipld/go-ipld-prime/datamodel" "github.com/ipfs/go-graphsync" - 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() -} - -// 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 +type MessagePartWithExtensions interface { + ExtensionNames() []graphsync.ExtensionName + Extension(name graphsync.ExtensionName) (datamodel.Node, bool) } type GraphSyncExtensions struct { @@ -43,14 +17,26 @@ type GraphSyncExtensions struct { Values map[string]datamodel.Node } -func NewGraphSyncExtensions(values map[string]datamodel.Node) GraphSyncExtensions { - keys := make([]string, 0, len(values)) - for k := range values { - keys = append(keys, k) +func NewGraphSyncExtensions(part MessagePartWithExtensions) GraphSyncExtensions { + names := part.ExtensionNames() + 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} } +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 { @@ -84,39 +70,6 @@ type GraphSyncBlock struct { Data []byte } -func FromBlockFormat(block blocks.Block) GraphSyncBlock { - return GraphSyncBlock{ - Prefix: block.Cid().Prefix().Bytes(), - Data: block.RawData(), - } -} - -func (b GraphSyncBlock) BlockFormat() *blocks.BasicBlock { - pref, err := cid.PrefixFromBytes(b.Prefix) - if err != nil { - panic(err) // should never happen - } - - c, err := pref.Sum(b.Data) - if err != nil { - panic(err) // should never happen - } - - block, err := blocks.NewBlockWithCid(b.Data, c) - if err != nil { - panic(err) // should never happen - } - return block -} - -func BlockFormatSlice(bs []GraphSyncBlock) []blocks.Block { - blks := make([]blocks.Block, len(bs)) - for i, b := range bs { - blks[i] = b.BlockFormat() - } - return blks -} - type GraphSyncMessage struct { Requests []GraphSyncRequest Responses []GraphSyncResponse diff --git a/message/ipldbind/schema.go b/message/ipldbind/schema.go index ec7be58a..2db86c9b 100644 --- a/message/ipldbind/schema.go +++ b/message/ipldbind/schema.go @@ -11,8 +11,6 @@ import ( //go:embed schema.ipldsch var embedSchema []byte -var schemaTypeSystem *schema.TypeSystem - var Prototype struct { Message schema.TypedPrototype } @@ -22,7 +20,6 @@ func init() { if err != nil { panic(err) } - schemaTypeSystem = ts Prototype.Message = bindnode.Prototype((*GraphSyncMessage)(nil), ts.TypeByName("GraphSyncMessage")) } diff --git a/message/message.go b/message/message.go index 586bb01c..3247b8ca 100644 --- a/message/message.go +++ b/message/message.go @@ -3,6 +3,7 @@ package message import ( "bytes" "fmt" + "io" "strings" blocks "github.com/ipfs/go-block-format" @@ -10,10 +11,18 @@ import ( "github.com/ipld/go-ipld-prime" "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" "github.com/ipfs/go-graphsync" ) +type MessageHandler interface { + FromNet(peer.ID, io.Reader) (GraphSyncMessage, error) + FromMsgReader(peer.ID, msgio.Reader) (GraphSyncMessage, error) + ToNet(peer.ID, GraphSyncMessage, io.Writer) error +} + // GraphSyncRequest is a struct to capture data on a request contained in a // GraphSyncMessage. type GraphSyncRequest struct { @@ -34,6 +43,11 @@ func (gsr GraphSyncRequest) String() string { 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, @@ -41,7 +55,7 @@ func (gsr GraphSyncRequest) String() string { gsr.id.String(), gsr.isCancel, gsr.isUpdate, - strings.Join(gsr.ExtensionNames(), "|"), + extStr.String(), ) } @@ -55,10 +69,15 @@ type GraphSyncResponse struct { // 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, - strings.Join(gsr.ExtensionNames(), "|"), + extStr.String(), ) } @@ -70,6 +89,14 @@ type GraphSyncMessage struct { blocks map[cid.Cid]blocks.Block } +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 { @@ -87,7 +114,7 @@ func (gsm GraphSyncMessage) String() string { return fmt.Sprintf("GraphSyncMessage<\n\t%s\n>", strings.Join(cts, ",\n\t")) } -// NewRequest builds a new Graphsync request +// NewRequest builds a new GraphSyncRequest func NewRequest(id graphsync.RequestID, root cid.Cid, selector ipld.Node, @@ -97,13 +124,13 @@ func NewRequest(id graphsync.RequestID, return newRequest(id, root, selector, priority, false, false, toExtensionsMap(extensions)) } -// CancelRequest request generates a request to cancel an in progress request -func CancelRequest(id graphsync.RequestID) GraphSyncRequest { +// NewCancelRequest request generates a request to cancel an in progress request +func NewCancelRequest(id graphsync.RequestID) GraphSyncRequest { return newRequest(id, cid.Cid{}, nil, 0, true, false, nil) } -// UpdateRequest generates a new request to update an in progress request with the given extensions -func UpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionData) GraphSyncRequest { +// 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, false, true, toExtensionsMap(extensions)) } @@ -247,10 +274,10 @@ func (gsr GraphSyncRequest) Extension(name graphsync.ExtensionName) (datamodel.N } // 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 } @@ -281,10 +308,10 @@ func (gsr GraphSyncResponse) Extension(name graphsync.ExtensionName) (datamodel. } // 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 } diff --git a/message/messagehandler.go b/message/messagehandler.go deleted file mode 100644 index 7eac108b..00000000 --- a/message/messagehandler.go +++ /dev/null @@ -1,621 +0,0 @@ -package message - -import ( - "bytes" - "encoding/binary" - "errors" - "io" - "sync" - - 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/basicnode" - "github.com/ipld/go-ipld-prime/node/bindnode" - 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/ipldbind" - pb "github.com/ipfs/go-graphsync/message/pb" -) - -type v1RequestKey struct { - p peer.ID - id int32 -} - -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 network stream to deserialized a GraphSyncMessage -func (mh *MessageHandler) FromNet(r io.Reader) (GraphSyncMessage, error) { - reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) - return mh.FromMsgReader(reader) -} - -// FromNetV1 can read a v1.0.0 network stream to deserialized a GraphSyncMessage -func (mh *MessageHandler) FromNetV1(p peer.ID, r io.Reader) (GraphSyncMessage, error) { - reader := msgio.NewVarintReaderSize(r, network.MessageSizeMax) - return mh.FromMsgReaderV1(p, reader) -} - -// FromMsgReader can deserialize a DAG-CBOR message into a GraphySyncMessage -func (mh *MessageHandler) FromMsgReader(r msgio.Reader) (GraphSyncMessage, error) { - msg, err := r.ReadMsg() - if err != nil { - return GraphSyncMessage{}, err - } - - builder := ipldbind.Prototype.Message.Representation().NewBuilder() - err = dagcbor.Decode(builder, bytes.NewReader(msg)) - if err != nil { - return GraphSyncMessage{}, err - } - - node := builder.Build() - ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessage) - return mh.fromIPLD(ipldGSM) -} - -// FromMsgReaderV11 can deserialize a v1.1.0 protobuf message into a GraphySyncMessage -func (mh *MessageHandler) FromMsgReaderV11(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 mh.newMessageFromProtoV11(&pb) -} - -// FromMsgReaderV1 can deserialize a v1.0.0 protobuf message into a GraphySyncMessage -func (mh *MessageHandler) FromMsgReaderV1(p peer.ID, r msgio.Reader) (GraphSyncMessage, error) { - msg, err := r.ReadMsg() - if err != nil { - return GraphSyncMessage{}, err - } - - var pb pb.Message_V1_0_0 - err = proto.Unmarshal(msg, &pb) - r.ReleaseMsg(msg) - if err != nil { - return GraphSyncMessage{}, err - } - - return mh.newMessageFromProtoV1(p, &pb) -} - -// ToProto converts a GraphSyncMessage to its ipldbind.GraphSyncMessage equivalent -func (mh *MessageHandler) toIPLD(gsm GraphSyncMessage) (*ipldbind.GraphSyncMessage, error) { - ibm := new(ipldbind.GraphSyncMessage) - ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(gsm.requests)) - for _, request := range gsm.requests { - sel := &request.selector - if request.selector == nil { - sel = nil - } - root := &request.root - if request.root == cid.Undef { - root = nil - } - ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ - Id: request.id.Bytes(), - Root: root, - Selector: sel, - Priority: request.priority, - Cancel: request.isCancel, - Update: request.isUpdate, - Extensions: ipldbind.NewGraphSyncExtensions(request.extensions), - }) - } - - ibm.Responses = make([]ipldbind.GraphSyncResponse, 0, len(gsm.responses)) - for _, response := range gsm.responses { - ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ - Id: response.requestID.Bytes(), - Status: response.status, - Extensions: ipldbind.NewGraphSyncExtensions(response.extensions), - }) - } - - blocks := gsm.Blocks() - ibm.Blocks = make([]ipldbind.GraphSyncBlock, 0, len(blocks)) - for _, b := range blocks { - ibm.Blocks = append(ibm.Blocks, ipldbind.GraphSyncBlock{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), - }) - } - - return ibm, nil -} - -// ToProto converts a GraphSyncMessage to its pb.Message equivalent -func (mh *MessageHandler) ToProtoV11(gsm GraphSyncMessage) (*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 - } - } - ext, err := toEncodedExtensions(request.extensions) - if err != nil { - return nil, err - } - pbm.Requests = append(pbm.Requests, &pb.Message_Request{ - Id: request.id.Bytes(), - Root: request.root.Bytes(), - Selector: selector, - Priority: int32(request.priority), - Cancel: request.isCancel, - Update: request.isUpdate, - Extensions: ext, - }) - } - - pbm.Responses = make([]*pb.Message_Response, 0, len(gsm.responses)) - for _, response := range gsm.responses { - ext, err := toEncodedExtensions(response.extensions) - if err != nil { - return nil, err - } - - pbm.Responses = append(pbm.Responses, &pb.Message_Response{ - Id: response.requestID.Bytes(), - Status: int32(response.status), - Extensions: ext, - }) - } - - pbm.Data = make([]*pb.Message_Block, 0, len(gsm.blocks)) - for _, b := range gsm.blocks { - pbm.Data = append(pbm.Data, &pb.Message_Block{ - Prefix: b.Cid().Prefix().Bytes(), - Data: b.RawData(), - }) - } - - return pbm, nil -} - -// ToProtoV1 converts a GraphSyncMessage to its pb.Message_V1_0_0 equivalent -func (mh *MessageHandler) ToProtoV1(p peer.ID, gsm GraphSyncMessage) (*pb.Message_V1_0_0, error) { - mh.mapLock.Lock() - defer mh.mapLock.Unlock() - - pbm := new(pb.Message_V1_0_0) - pbm.Requests = make([]*pb.Message_V1_0_0_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 - } - } - rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, request.id.Bytes()) - if err != nil { - return nil, err - } - ext, err := toEncodedExtensions(request.extensions) - if err != nil { - return nil, err - } - pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ - Id: rid, - Root: request.root.Bytes(), - Selector: selector, - Priority: int32(request.priority), - Cancel: request.isCancel, - Update: request.isUpdate, - Extensions: ext, - }) - } - - pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(gsm.responses)) - for _, response := range gsm.responses { - rid, err := bytesIdToInt(p, mh.fromV1Map, mh.toV1Map, &mh.nextIntId, response.requestID.Bytes()) - if err != nil { - return nil, err - } - ext, err := toEncodedExtensions(response.extensions) - if err != nil { - return nil, err - } - pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ - Id: rid, - Status: int32(response.status), - Extensions: ext, - }) - } - - pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(gsm.blocks)) - for _, b := range gsm.blocks { - pbm.Data = append(pbm.Data, &pb.Message_V1_0_0_Block{ - Prefix: b.Cid().Prefix().Bytes(), - Data: b.RawData(), - }) - } - return pbm, nil -} - -// ToNet writes a GraphSyncMessage in its DAG-CBOR format to a writer, -// prefixed with a length uvar -func (mh *MessageHandler) ToNet(gsm 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 - } - //_, err = buf.WriteTo(w) - - 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 -} - -// ToNetV11 writes a GraphSyncMessage in its v1.1.0 protobuf format to a writer -func (mh *MessageHandler) ToNetV11(gsm GraphSyncMessage, w io.Writer) error { - msg, err := mh.ToProtoV11(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 -} - -// ToNetV1 writes a GraphSyncMessage in its v1.0.0 protobuf format to a writer -func (mh *MessageHandler) ToNetV1(p peer.ID, gsm GraphSyncMessage, w io.Writer) error { - msg, err := mh.ToProtoV1(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 -} - -// 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 -} - -// Mapping from a ipldbind.GraphSyncMessage object to a GraphSyncMessage object -func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (GraphSyncMessage, error) { - requests := make(map[graphsync.RequestID]GraphSyncRequest, len(ibm.Requests)) - for _, req := range ibm.Requests { - id, err := graphsync.ParseRequestID(req.Id) - if err != nil { - return GraphSyncMessage{}, err - } - root := cid.Undef - if req.Root != nil { - root = *req.Root - } - var selector datamodel.Node - if req.Selector != nil { - selector = *req.Selector - } - requests[id] = newRequest(id, root, selector, graphsync.Priority(req.Priority), req.Cancel, req.Update, req.Extensions.Values) - } - - responses := make(map[graphsync.RequestID]GraphSyncResponse, len(ibm.Responses)) - for _, res := range ibm.Responses { - // exts := res.Extensions - id, err := graphsync.ParseRequestID(res.Id) - if err != nil { - return GraphSyncMessage{}, err - } - responses[id] = newResponse(id, graphsync.ResponseStatusCode(res.Status), res.Extensions.Values) - } - - blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) - for _, b := range ibm.Blocks { - pref, err := cid.PrefixFromBytes(b.Prefix) - if err != nil { - return GraphSyncMessage{}, err - } - - c, err := pref.Sum(b.Data) - if err != nil { - return GraphSyncMessage{}, err - } - - blk, err := blocks.NewBlockWithCid(b.Data, c) - if err != nil { - return GraphSyncMessage{}, err - } - - blks[blk.Cid()] = blk - } - - return GraphSyncMessage{ - requests, responses, blks, - }, nil -} - -// Mapping from a pb.Message object to a GraphSyncMessage object -func (mh *MessageHandler) newMessageFromProtoV11(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 datamodel.Node - if !req.Cancel && !req.Update { - selector, err = ipldutil.DecodeNode(req.Selector) - if err != nil { - return GraphSyncMessage{}, err - } - } - id, err := graphsync.ParseRequestID(req.Id) - if err != nil { - return GraphSyncMessage{}, err - } - exts, err := fromEncodedExtensions(req.GetExtensions()) - if err != nil { - return GraphSyncMessage{}, err - } - requests[id] = newRequest(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") - } - id, err := graphsync.ParseRequestID(res.Id) - if err != nil { - return GraphSyncMessage{}, err - } - exts, err := fromEncodedExtensions(res.GetExtensions()) - if err != nil { - return GraphSyncMessage{}, err - } - responses[id] = newResponse(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 -} - -// Mapping from a pb.Message_V1_0_0 object to a GraphSyncMessage object, including -// RequestID (int / uuid) mapping. -func (mh *MessageHandler) newMessageFromProtoV1(p peer.ID, pbm *pb.Message_V1_0_0) (GraphSyncMessage, error) { - mh.mapLock.Lock() - defer mh.mapLock.Unlock() - - 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 datamodel.Node - if !req.Cancel && !req.Update { - selector, err = ipldutil.DecodeNode(req.Selector) - if err != nil { - return GraphSyncMessage{}, err - } - } - id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, req.Id) - if err != nil { - return GraphSyncMessage{}, err - } - exts, err := fromEncodedExtensions(req.GetExtensions()) - if err != nil { - return GraphSyncMessage{}, err - } - requests[id] = newRequest(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") - } - id, err := intIdToRequestId(p, mh.fromV1Map, mh.toV1Map, res.Id) - if err != nil { - return GraphSyncMessage{}, err - } - exts, err := fromEncodedExtensions(res.GetExtensions()) - if err != nil { - return GraphSyncMessage{}, err - } - responses[id] = newResponse(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 -} - -func toEncodedExtensions(in map[string]datamodel.Node) (map[string][]byte, error) { - out := make(map[string][]byte, len(in)) - for name, data := range in { - if data == nil { - out[name] = nil - } else { - var buf bytes.Buffer - err := dagcbor.Encode(data, &buf) - if err != nil { - return nil, err - } - out[name] = buf.Bytes() - } - } - return out, nil -} - -func fromEncodedExtensions(in map[string][]byte) (map[string]datamodel.Node, error) { - if in == nil { - return make(map[string]datamodel.Node), nil - } - out := make(map[string]datamodel.Node, len(in)) - for name, data := range in { - if len(data) == 0 { - out[name] = nil - } else { - nb := basicnode.Prototype.Any.NewBuilder() - err := dagcbor.Decode(nb, bytes.NewReader(data)) - if err != nil { - return nil, err - } - out[name] = nb.Build() - } - } - return out, nil -} diff --git a/message/v1/message.go b/message/v1/message.go new file mode 100644 index 00000000..c554e281 --- /dev/null +++ b/message/v1/message.go @@ -0,0 +1,314 @@ +package v1 + +import ( + "encoding/binary" + "errors" + "io" + "sync" + + 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" + pb "github.com/ipfs/go-graphsync/message/pb" + "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" +) + +type MessagePartWithExtensions interface { + ExtensionNames() []graphsync.ExtensionName + Extension(name graphsync.ExtensionName) (datamodel.Node, bool) +} + +type v1RequestKey struct { + p peer.ID + id int32 +} + +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_V1_0_0 + err = proto.Unmarshal(msg, &pb) + r.ReleaseMsg(msg) + if err != nil { + return message.GraphSyncMessage{}, err + } + + return mh.newMessageFromProto(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_V1_0_0 equivalent +func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb.Message_V1_0_0, error) { + mh.mapLock.Lock() + defer mh.mapLock.Unlock() + + pbm := new(pb.Message_V1_0_0) + requests := gsm.Requests() + pbm.Requests = make([]*pb.Message_V1_0_0_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) + if err != nil { + return nil, err + } + pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ + Id: rid, + Root: request.Root().Bytes(), + Selector: selector, + Priority: int32(request.Priority()), + Cancel: request.IsCancel(), + Update: request.IsUpdate(), + Extensions: ext, + }) + } + + responses := gsm.Responses() + pbm.Responses = make([]*pb.Message_V1_0_0_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) + if err != nil { + return nil, err + } + pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ + Id: rid, + Status: int32(response.Status()), + Extensions: ext, + }) + } + + blocks := gsm.Blocks() + pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(blocks)) + for _, b := range blocks { + pbm.Data = append(pbm.Data, &pb.Message_V1_0_0_Block{ + Prefix: b.Cid().Prefix().Bytes(), + Data: b.RawData(), + }) + } + return pbm, nil +} + +// Mapping from a pb.Message_V1_0_0 object to a GraphSyncMessage object, including +// RequestID (int / uuid) mapping. +func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message_V1_0_0) (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, err := fromEncodedExtensions(req.GetExtensions()) + if err != nil { + return message.GraphSyncMessage{}, err + } + + 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, err := fromEncodedExtensions(res.GetExtensions()) + if err != nil { + return message.GraphSyncMessage{}, err + } + responses[id] = message.NewResponse(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 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 +} + +// TODO: is this a breaking protocol change? force all extension data into dag-cbor? +func toEncodedExtensions(part MessagePartWithExtensions) (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 + } + } + return out, nil +} + +// TODO: is this a breaking protocol change? force all extension data into dag-cbor? +func fromEncodedExtensions(in map[string][]byte) ([]graphsync.ExtensionData, error) { + if in == nil { + return []graphsync.ExtensionData{}, nil + } + out := make([]graphsync.ExtensionData, 0, len(in)) + for name, data := range in { + if len(data) == 0 { + out = append(out, graphsync.ExtensionData{Name: graphsync.ExtensionName(name), Data: nil}) + } else { + data, err := ipldutil.DecodeNode(data) + if err != nil { + return nil, err + } + out = append(out, graphsync.ExtensionData{Name: graphsync.ExtensionName(name), Data: data}) + } + } + return out, 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 90% rename from message/message_test.go rename to message/v1/message_test.go index 62c7d34e..1655d415 100644 --- a/message/message_test.go +++ b/message/v1/message_test.go @@ -1,4 +1,4 @@ -package message +package v1 import ( "bytes" @@ -16,6 +16,7 @@ import ( "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/ipldutil" + "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/testutil" ) @@ -31,8 +32,8 @@ func TestAppendingRequests(t *testing.T) { 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() @@ -47,13 +48,14 @@ func TestAppendingRequests(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := NewMessageHandler().ToProtoV11(gsm) + 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, id.Bytes(), pbRequest.Id) require.Equal(t, int32(priority), pbRequest.Priority) require.False(t, pbRequest.Cancel) require.False(t, pbRequest.Update) @@ -66,7 +68,7 @@ func TestAppendingRequests(t *testing.T) { require.True(t, ok) require.Equal(t, expectedByts, actualByts) - deserialized, err := NewMessageHandler().newMessageFromProtoV11(pbMessage) + deserialized, err := mh.newMessageFromProto(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") @@ -94,7 +96,7 @@ func TestAppendingResponses(t *testing.T) { mh := NewMessageHandler() status := graphsync.RequestAcknowledged - builder := NewBuilder() + builder := message.NewBuilder() builder.AddResponseCode(requestID, status) builder.AddExtensionData(requestID, extension) gsm, err := builder.Build() @@ -108,14 +110,14 @@ func TestAppendingResponses(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) - pbMessage, err := mh.ToProtoV1(p, gsm) + pbMessage, err := mh.ToProto(p, gsm) require.NoError(t, err, "serialize to protobuf errored") pbResponse := pbMessage.Responses[0] // no longer equal: require.Equal(t, requestID.Bytes(), pbResponse.Id) require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, []byte("stest extension data"), pbResponse.Extensions["graphsync/awesome"]) - deserialized, err := mh.newMessageFromProtoV1(p, pbMessage) + deserialized, err := mh.newMessageFromProto(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") @@ -133,7 +135,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) @@ -141,7 +143,7 @@ func TestAppendBlock(t *testing.T) { m, err := builder.Build() require.NoError(t, err) - pbMessage, err := NewMessageHandler().ToProtoV11(m) + pbMessage, err := NewMessageHandler().ToProto(peer.ID("foo"), m) require.NoError(t, err, "serializing to protobuf errored") // assert strings are in proto message @@ -167,9 +169,9 @@ func TestRequestCancel(t *testing.T) { 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) @@ -179,10 +181,12 @@ func TestRequestCancel(t *testing.T) { require.Equal(t, id, request.ID()) require.True(t, request.IsCancel()) + mh := NewMessageHandler() + buf := new(bytes.Buffer) - err = NewMessageHandler().ToNet(gsm, buf) + err = mh.ToNet(peer.ID("foo"), gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := NewMessageHandler().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") @@ -200,8 +204,8 @@ func TestRequestUpdate(t *testing.T) { 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) @@ -215,10 +219,12 @@ func TestRequestUpdate(t *testing.T) { require.True(t, found) require.Equal(t, extension.Data, extensionData) + mh := NewMessageHandler() + buf := new(bytes.Buffer) - err = NewMessageHandler().ToNet(gsm, buf) + err = mh.ToNet(peer.ID("foo"), gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := NewMessageHandler().FromNet(buf) + deserialized, err := mh.FromNet(peer.ID("foo"), buf) require.NoError(t, err, "did not deserialize protobuf message") deserializedRequests := deserialized.Requests() @@ -248,8 +254,8 @@ func TestToNetFromNetEquivalency(t *testing.T) { 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"))) @@ -259,10 +265,12 @@ func TestToNetFromNetEquivalency(t *testing.T) { gsm, err := builder.Build() require.NoError(t, err) + mh := NewMessageHandler() + buf := new(bytes.Buffer) - err = NewMessageHandler().ToNet(gsm, buf) + err = mh.ToNet(peer.ID("foo"), gsm, buf) require.NoError(t, err, "did not serialize protobuf message") - deserialized, err := NewMessageHandler().FromNet(buf) + deserialized, err := mh.FromNet(peer.ID("foo"), buf) require.NoError(t, err, "did not deserialize protobuf message") requests := gsm.Requests() @@ -344,9 +352,9 @@ func TestMergeExtensions(t *testing.T) { selector := ssb.Matcher().Node() 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()) @@ -423,15 +431,15 @@ func TestKnownFuzzIssues(t *testing.T) { for _, input := range inputs { //inputAsBytes, err := hex.DecodeString(input) ///require.NoError(t, err) - msg1, err := mh.FromNetV1(p, bytes.NewReader([]byte(input))) + msg1, err := mh.FromNet(p, bytes.NewReader([]byte(input))) if err != nil { continue } buf2 := new(bytes.Buffer) - err = mh.ToNetV1(p, msg1, buf2) + err = mh.ToNet(p, msg1, buf2) require.NoError(t, err) - msg2, err := mh.FromNetV1(p, buf2) + msg2, err := mh.FromNet(p, buf2) require.NoError(t, err) require.Equal(t, msg1, msg2) diff --git a/message/v2/message.go b/message/v2/message.go new file mode 100644 index 00000000..0e41f503 --- /dev/null +++ b/message/v2/message.go @@ -0,0 +1,190 @@ +package v2 + +import ( + "bytes" + "encoding/binary" + "io" + + 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/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" +) + +type MessageHandler struct{} + +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.GraphSyncMessage) + return mh.fromIPLD(ipldGSM) +} + +// ToProto converts a GraphSyncMessage to its ipldbind.GraphSyncMessage equivalent +func (mh *MessageHandler) toIPLD(gsm message.GraphSyncMessage) (*ipldbind.GraphSyncMessage, error) { + ibm := new(ipldbind.GraphSyncMessage) + requests := gsm.Requests() + ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(requests)) + for _, request := range requests { + selector := request.Selector() + selPtr := &selector + if selector == nil { + selPtr = nil + } + root := request.Root() + rootPtr := &root + if root == cid.Undef { + rootPtr = nil + } + ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ + Id: request.ID().Bytes(), + Root: rootPtr, + Selector: selPtr, + Priority: request.Priority(), + Cancel: request.IsCancel(), + Update: request.IsUpdate(), + Extensions: ipldbind.NewGraphSyncExtensions(request), + }) + } + + responses := gsm.Responses() + ibm.Responses = make([]ipldbind.GraphSyncResponse, 0, len(responses)) + for _, response := range responses { + ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ + Id: response.RequestID().Bytes(), + Status: response.Status(), + Extensions: ipldbind.NewGraphSyncExtensions(response), + }) + } + + blocks := gsm.Blocks() + ibm.Blocks = make([]ipldbind.GraphSyncBlock, 0, len(blocks)) + for _, b := range blocks { + ibm.Blocks = append(ibm.Blocks, ipldbind.GraphSyncBlock{ + Data: b.RawData(), + Prefix: b.Cid().Prefix().Bytes(), + }) + } + + return 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 + } + //_, err = buf.WriteTo(w) + + 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.GraphSyncMessage object to a GraphSyncMessage object +func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (message.GraphSyncMessage, error) { + requests := make(map[graphsync.RequestID]message.GraphSyncRequest, len(ibm.Requests)) + for _, req := range ibm.Requests { + id, err := graphsync.ParseRequestID(req.Id) + if err != nil { + return message.GraphSyncMessage{}, err + } + + if req.Cancel { + requests[id] = message.NewCancelRequest(id) + continue + } + + if req.Update { + requests[id] = message.NewUpdateRequest(id, req.Extensions.ToExtensionsList()...) + continue + } + + root := cid.Undef + if req.Root != nil { + root = *req.Root + } + + var selector datamodel.Node + if req.Selector != nil { + selector = *req.Selector + } + + requests[id] = message.NewRequest(id, root, selector, graphsync.Priority(req.Priority), req.Extensions.ToExtensionsList()...) + } + + responses := make(map[graphsync.RequestID]message.GraphSyncResponse, len(ibm.Responses)) + for _, res := range ibm.Responses { + // exts := res.Extensions + id, err := graphsync.ParseRequestID(res.Id) + if err != nil { + return message.GraphSyncMessage{}, err + } + responses[id] = message.NewResponse(id, graphsync.ResponseStatusCode(res.Status), res.Extensions.ToExtensionsList()...) + } + + blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) + for _, b := range ibm.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..b9c3c740 --- /dev/null +++ b/message/v2/message_test.go @@ -0,0 +1,408 @@ +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.False(t, request.IsCancel()) + 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.Requests[0] + require.Equal(t, priority, gsrIpld.Priority) + require.False(t, gsrIpld.Cancel) + require.False(t, gsrIpld.Update) + 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.False(t, deserializedRequest.IsCancel()) + require.False(t, deserializedRequest.IsUpdate()) + 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.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.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.True(t, request.IsCancel()) + + 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.IsCancel(), deserializedRequest.IsCancel()) +} + +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.True(t, request.IsUpdate()) + require.False(t, request.IsCancel()) + 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.IsCancel(), deserializedRequest.IsCancel()) + require.Equal(t, request.IsUpdate(), deserializedRequest.IsUpdate()) + 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.False(t, deserializedRequest.IsCancel()) + require.False(t, deserializedRequest.IsUpdate()) + 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/network/interface.go b/network/interface.go index ab65b150..1f4e135c 100644 --- a/network/interface.go +++ b/network/interface.go @@ -13,7 +13,6 @@ import ( var ( // ProtocolGraphsync is the protocol identifier for graphsync messages ProtocolGraphsync_1_0_0 protocol.ID = "/ipfs/graphsync/1.0.0" - ProtocolGraphsync_1_1_0 protocol.ID = "/ipfs/graphsync/1.1.0" ProtocolGraphsync_2_0_0 protocol.ID = "/ipfs/graphsync/2.0.0" ) diff --git a/network/libp2p_impl.go b/network/libp2p_impl.go index a295dbeb..bb04456e 100644 --- a/network/libp2p_impl.go +++ b/network/libp2p_impl.go @@ -15,6 +15,7 @@ import ( ma "github.com/multiformats/go-multiaddr" gsmsg "github.com/ipfs/go-graphsync/message" + gsmsgv1 "github.com/ipfs/go-graphsync/message/v1" ) var log = logging.Logger("graphsync_network") @@ -36,8 +37,8 @@ func GraphsyncProtocols(protocols []protocol.ID) Option { func NewFromLibp2pHost(host host.Host, options ...Option) GraphSyncNetwork { graphSyncNetwork := libp2pGraphSyncNetwork{ host: host, - messageHandler: gsmsg.NewMessageHandler(), - protocols: []protocol.ID{ProtocolGraphsync_1_1_0, ProtocolGraphsync_2_0_0, ProtocolGraphsync_1_0_0}, + messageHandler: gsmsgv1.NewMessageHandler(), + protocols: []protocol.ID{ProtocolGraphsync_1_0_0, ProtocolGraphsync_2_0_0}, } for _, option := range options { @@ -53,14 +54,14 @@ type libp2pGraphSyncNetwork struct { host host.Host // inbound messages from the network are forwarded to the receiver receiver Receiver - messageHandler *gsmsg.MessageHandler + messageHandler gsmsg.MessageHandler protocols []protocol.ID } type streamMessageSender struct { s network.Stream opts MessageSenderOpts - messageHandler *gsmsg.MessageHandler + messageHandler gsmsg.MessageHandler } func (s *streamMessageSender) Close() error { @@ -75,7 +76,7 @@ func (s *streamMessageSender) SendMsg(ctx context.Context, msg gsmsg.GraphSyncMe return msgToStream(ctx, s.s, s.messageHandler, msg, s.opts.SendTimeout) } -func msgToStream(ctx context.Context, s network.Stream, mh *gsmsg.MessageHandler, msg gsmsg.GraphSyncMessage, timeout time.Duration) error { +func msgToStream(ctx context.Context, s network.Stream, mh gsmsg.MessageHandler, 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())) @@ -89,17 +90,12 @@ func msgToStream(ctx context.Context, s network.Stream, mh *gsmsg.MessageHandler switch s.Protocol() { case ProtocolGraphsync_1_0_0: - if err := mh.ToNetV1(s.Conn().RemotePeer(), msg, s); err != nil { - log.Debugf("error: %s", err) - return err - } - case ProtocolGraphsync_1_1_0: - if err := mh.ToNetV11(msg, s); err != nil { + if err := mh.ToNet(s.Conn().RemotePeer(), msg, s); err != nil { log.Debugf("error: %s", err) return err } case ProtocolGraphsync_2_0_0: - if err := mh.ToNet(msg, s); err != nil { + if err := mh.ToNet(s.Conn().RemotePeer(), msg, s); err != nil { log.Debugf("error: %s", err) return err } @@ -175,11 +171,9 @@ func (gsnet *libp2pGraphSyncNetwork) handleNewStream(s network.Stream) { var err error switch s.Protocol() { case ProtocolGraphsync_1_0_0: - received, err = gsnet.messageHandler.FromMsgReaderV1(s.Conn().RemotePeer(), reader) - case ProtocolGraphsync_1_1_0: - received, err = gsnet.messageHandler.FromMsgReaderV11(reader) + received, err = gsnet.messageHandler.FromMsgReader(s.Conn().RemotePeer(), reader) case ProtocolGraphsync_2_0_0: - received, err = gsnet.messageHandler.FromMsgReader(reader) + received, err = gsnet.messageHandler.FromMsgReader(s.Conn().RemotePeer(), reader) default: err = fmt.Errorf("unexpected protocol version %s", s.Protocol()) } @@ -209,7 +203,7 @@ 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_1_1_0, ProtocolGraphsync_2_0_0: + case ProtocolGraphsync_1_0_0, ProtocolGraphsync_2_0_0: gsnet.protocols = append([]protocol.ID{}, proto) } } diff --git a/network/libp2p_impl_test.go b/network/libp2p_impl_test.go index a7cdebc3..b8811e80 100644 --- a/network/libp2p_impl_test.go +++ b/network/libp2p_impl_test.go @@ -106,7 +106,8 @@ func TestMessageSendAndReceive(t *testing.T) { receivedRequests := received.Requests() require.Len(t, receivedRequests, 1, "did not add request to received message") receivedRequest := receivedRequests[0] - require.Equal(t, sentRequest.ID(), receivedRequest.ID()) + // TODO: for protocol v1 this shouldn't match, but for v2 it should + // 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.Root().String(), receivedRequest.Root().String()) @@ -119,7 +120,8 @@ func TestMessageSendAndReceive(t *testing.T) { require.Len(t, receivedResponses, 1, "did not add response to received message") receivedResponse := receivedResponses[0] extensionData, found := receivedResponse.Extension(extensionName) - require.Equal(t, sentResponse.RequestID(), receivedResponse.RequestID()) + // TODO: for protocol v1 this shouldn't match, but for v2 it should + // require.Equal(t, sentResponse.RequestID(), receivedResponse.RequestID()) require.Equal(t, sentResponse.Status(), receivedResponse.Status()) require.True(t, found) require.Equal(t, extension.Data, extensionData) diff --git a/peermanager/peermessagemanager_test.go b/peermanager/peermessagemanager_test.go index 5b1223dd..d08144c1 100644 --- a/peermanager/peermessagemanager_test.go +++ b/peermanager/peermessagemanager_test.go @@ -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) }) diff --git a/requestmanager/executor/executor.go b/requestmanager/executor/executor.go index 672090bd..4659a70d 100644 --- a/requestmanager/executor/executor.go +++ b/requestmanager/executor/executor.go @@ -83,7 +83,7 @@ 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())) if !isPausedErr(err) { span.SetStatus(codes.Error, err.Error()) select { @@ -168,7 +168,7 @@ func (e *Executor) traverse(rt RequestTask) error { 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 diff --git a/requestmanager/server.go b/requestmanager/server.go index d579a5e6..bd94154b 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -243,7 +243,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) } @@ -311,7 +311,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 { @@ -319,7 +319,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 } diff --git a/responsemanager/hooks/hooks_test.go b/responsemanager/hooks/hooks_test.go index e2e8aad4..9fb2c6f0 100644 --- a/responsemanager/hooks/hooks_test.go +++ b/responsemanager/hooks/hooks_test.go @@ -317,7 +317,7 @@ func TestUpdateHookProcessing(t *testing.T) { 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/responsemanager_test.go b/responsemanager/responsemanager_test.go index bd547727..09ea3731 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -125,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() @@ -198,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) @@ -1146,7 +1146,7 @@ func newTestData(t *testing.T) testData { 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() diff --git a/responsemanager/server.go b/responsemanager/server.go index 1c0b45c2..f71808bb 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -69,7 +69,14 @@ func (rm *ResponseManager) processUpdate(ctx context.Context, key responseKey, u trace.WithLinks(trace.LinkFromContext(ctx)), trace.WithAttributes( attribute.String("id", update.ID().String()), - attribute.StringSlice("extensions", update.ExtensionNames()), + 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() @@ -203,7 +210,14 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync 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{ From 161a577f7a6cd22f9681abb702e82d2266ae7ccc Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 25 Jan 2022 21:13:54 +1100 Subject: [PATCH 17/32] feat(net): activate v2 network as default --- impl/graphsync_test.go | 6 +-- message/v1/message.go | 9 ++-- message/v2/message.go | 7 +-- network/libp2p_impl.go | 95 ++++++++++++++++++++++--------------- network/libp2p_impl_test.go | 6 +-- 5 files changed, 71 insertions(+), 52 deletions(-) diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 9a50fb4f..921f4215 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -63,9 +63,9 @@ var protocolsForTest = map[string]struct { host1Protocols []protocol.ID host2Protocols []protocol.ID }{ - "(v1.1 -> v1.1)": {nil, nil}, - "(v1.0 -> v1.1)": {[]protocol.ID{gsnet.ProtocolGraphsync_1_0_0}, nil}, - "(v1.1 -> v1.0)": {nil, []protocol.ID{gsnet.ProtocolGraphsync_1_0_0}}, + "(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}}, } diff --git a/message/v1/message.go b/message/v1/message.go index c554e281..e8b458a4 100644 --- a/message/v1/message.go +++ b/message/v1/message.go @@ -8,16 +8,17 @@ import ( 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" - pb "github.com/ipfs/go-graphsync/message/pb" "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" ) type MessagePartWithExtensions interface { diff --git a/message/v2/message.go b/message/v2/message.go index 0e41f503..7c63a3a7 100644 --- a/message/v2/message.go +++ b/message/v2/message.go @@ -7,15 +7,16 @@ import ( 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/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" ) type MessageHandler struct{} diff --git a/network/libp2p_impl.go b/network/libp2p_impl.go index bb04456e..a423ab54 100644 --- a/network/libp2p_impl.go +++ b/network/libp2p_impl.go @@ -16,6 +16,7 @@ import ( 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") @@ -35,10 +36,14 @@ func GraphsyncProtocols(protocols []protocol.ID) Option { // NewFromLibp2pHost returns a GraphSyncNetwork supported by underlying Libp2p host. func NewFromLibp2pHost(host host.Host, options ...Option) GraphSyncNetwork { + messageHandlerSelector := messageHandlerSelector{ + v1MessageHandler: gsmsgv1.NewMessageHandler(), + v2MessageHandler: gsmsgv2.NewMessageHandler(), + } graphSyncNetwork := libp2pGraphSyncNetwork{ - host: host, - messageHandler: gsmsgv1.NewMessageHandler(), - protocols: []protocol.ID{ProtocolGraphsync_1_0_0, ProtocolGraphsync_2_0_0}, + host: host, + messageHandlerSelector: &messageHandlerSelector, + protocols: []protocol.ID{ProtocolGraphsync_2_0_0, ProtocolGraphsync_1_0_0}, } for _, option := range options { @@ -48,20 +53,53 @@ func NewFromLibp2pHost(host host.Host, 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 - messageHandler gsmsg.MessageHandler - protocols []protocol.ID + receiver Receiver + protocols []protocol.ID + messageHandlerSelector *messageHandlerSelector } type streamMessageSender struct { - s network.Stream - opts MessageSenderOpts - messageHandler gsmsg.MessageHandler + s network.Stream + opts MessageSenderOpts + messageHandlerSelector *messageHandlerSelector } func (s *streamMessageSender) Close() error { @@ -73,10 +111,10 @@ func (s *streamMessageSender) Reset() error { } func (s *streamMessageSender) SendMsg(ctx context.Context, msg gsmsg.GraphSyncMessage) error { - return msgToStream(ctx, s.s, s.messageHandler, msg, s.opts.SendTimeout) + return msgToStream(ctx, s.s, s.messageHandlerSelector, msg, s.opts.SendTimeout) } -func msgToStream(ctx context.Context, s network.Stream, mh gsmsg.MessageHandler, 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())) @@ -88,19 +126,9 @@ func msgToStream(ctx context.Context, s network.Stream, mh gsmsg.MessageHandler, log.Warnf("error setting deadline: %s", err) } - switch s.Protocol() { - case ProtocolGraphsync_1_0_0: - if err := mh.ToNet(s.Conn().RemotePeer(), msg, s); err != nil { - log.Debugf("error: %s", err) - return err - } - case ProtocolGraphsync_2_0_0: - if err := mh.ToNet(s.Conn().RemotePeer(), msg, 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 { @@ -116,9 +144,9 @@ func (gsnet *libp2pGraphSyncNetwork) NewMessageSender(ctx context.Context, p pee } return &streamMessageSender{ - s: s, - opts: setDefaults(opts), - messageHandler: gsnet.messageHandler, + s: s, + opts: setDefaults(opts), + messageHandlerSelector: gsnet.messageHandlerSelector, }, nil } @@ -136,7 +164,7 @@ func (gsnet *libp2pGraphSyncNetwork) SendMessage( return err } - if err = msgToStream(ctx, s, gsnet.messageHandler, outgoing, sendMessageTimeout); err != nil { + if err = msgToStream(ctx, s, gsnet.messageHandlerSelector, outgoing, sendMessageTimeout); err != nil { _ = s.Reset() return err } @@ -167,16 +195,7 @@ func (gsnet *libp2pGraphSyncNetwork) handleNewStream(s network.Stream) { reader := msgio.NewVarintReaderSize(s, network.MessageSizeMax) for { - var received gsmsg.GraphSyncMessage - var err error - switch s.Protocol() { - case ProtocolGraphsync_1_0_0: - received, err = gsnet.messageHandler.FromMsgReader(s.Conn().RemotePeer(), reader) - case ProtocolGraphsync_2_0_0: - received, err = gsnet.messageHandler.FromMsgReader(s.Conn().RemotePeer(), reader) - default: - err = fmt.Errorf("unexpected protocol version %s", s.Protocol()) - } + received, err := gsnet.messageHandlerSelector.Select(s.Protocol()).FromMsgReader(s.Conn().RemotePeer(), reader) p := s.Conn().RemotePeer() if err != nil { diff --git a/network/libp2p_impl_test.go b/network/libp2p_impl_test.go index b8811e80..a7cdebc3 100644 --- a/network/libp2p_impl_test.go +++ b/network/libp2p_impl_test.go @@ -106,8 +106,7 @@ func TestMessageSendAndReceive(t *testing.T) { receivedRequests := received.Requests() require.Len(t, receivedRequests, 1, "did not add request to received message") receivedRequest := receivedRequests[0] - // TODO: for protocol v1 this shouldn't match, but for v2 it should - // require.Equal(t, sentRequest.ID(), receivedRequest.ID()) + 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.Root().String(), receivedRequest.Root().String()) @@ -120,8 +119,7 @@ func TestMessageSendAndReceive(t *testing.T) { require.Len(t, receivedResponses, 1, "did not add response to received message") receivedResponse := receivedResponses[0] extensionData, found := receivedResponse.Extension(extensionName) - // TODO: for protocol v1 this shouldn't match, but for v2 it should - // require.Equal(t, sentResponse.RequestID(), receivedResponse.RequestID()) + require.Equal(t, sentResponse.RequestID(), receivedResponse.RequestID()) require.Equal(t, sentResponse.Status(), receivedResponse.Status()) require.True(t, found) require.Equal(t, extension.Data, extensionData) From e997ff32277150a4c47f8237895534c04ccf7bfe Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Tue, 25 Jan 2022 21:29:38 +1100 Subject: [PATCH 18/32] fix(src): build error --- message/bench/bench_test.go | 2 +- message/bench/empty.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 message/bench/empty.go diff --git a/message/bench/bench_test.go b/message/bench/bench_test.go index 3d2b4033..d7e003b0 100644 --- a/message/bench/bench_test.go +++ b/message/bench/bench_test.go @@ -1,4 +1,4 @@ -package message +package bench import ( "bytes" 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 From 4e57d92eae3810173862ad602cdf28a1ab047d82 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 31 Jan 2022 15:13:43 +1100 Subject: [PATCH 19/32] chore: remove GraphSyncMessage#Loggable Ref: https://github.com/ipfs/go-graphsync/pull/332#discussion_r792169770 --- message/message.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/message/message.go b/message/message.go index 3247b8ca..f88d335b 100644 --- a/message/message.go +++ b/message/message.go @@ -216,21 +216,6 @@ func (gsm GraphSyncMessage) Blocks() []blocks.Block { return bs } -func (gsm GraphSyncMessage) Loggable() map[string]interface{} { - requests := make([]string, 0, len(gsm.requests)) - for _, request := range gsm.requests { - requests = append(requests, request.id.String()) - } - responses := make([]string, 0, len(gsm.responses)) - for _, response := range gsm.responses { - responses = append(responses, response.requestID.String()) - } - 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)) From 6b86c3cb2e30ae29bd2f3935e6f76b40c9453f9f Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 31 Jan 2022 15:05:44 +1100 Subject: [PATCH 20/32] chore: remove intermediate v1.1 pb protocol message type v1.1.0 was introduced to start the transition to UUID RequestIDs. That change has since been combined with the switch to DAG-CBOR messaging format for a v2.0.0 protocol. Thus, this interim v1.1.0 format is no longer needed and has not been used at all in a released version of go-graphsync. Fixes: https://github.com/filecoin-project/lightning-planning/issues/14 --- message/pb/message.pb.go | 16 +- message/pb/message.proto | 4 +- message/pb/message_v1_0_0.pb.go | 480 -------------------------------- message/pb/message_v1_0_0.proto | 36 --- message/v1/message.go | 24 +- 5 files changed, 22 insertions(+), 538 deletions(-) delete mode 100644 message/pb/message_v1_0_0.pb.go delete mode 100644 message/pb/message_v1_0_0.proto diff --git a/message/pb/message.pb.go b/message/pb/message.pb.go index 897027eb..cdab7196 100644 --- a/message/pb/message.pb.go +++ b/message/pb/message.pb.go @@ -98,7 +98,7 @@ type Message_Request struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // unique id set on the requester side + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // unique id set on the requester side Root []byte `protobuf:"bytes,2,opt,name=root,proto3" json:"root,omitempty"` // a CID for the root node in the query Selector []byte `protobuf:"bytes,3,opt,name=selector,proto3" json:"selector,omitempty"` // ipld selector to retrieve Extensions map[string][]byte `protobuf:"bytes,4,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // aux information. useful for other protocols @@ -139,11 +139,11 @@ func (*Message_Request) Descriptor() ([]byte, []int) { return file_message_proto_rawDescGZIP(), []int{0, 0} } -func (x *Message_Request) GetId() []byte { +func (x *Message_Request) GetId() int32 { if x != nil { return x.Id } - return nil + return 0 } func (x *Message_Request) GetRoot() []byte { @@ -193,7 +193,7 @@ type Message_Response struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // the request id + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // the request id Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"` // a status code. Extensions map[string][]byte `protobuf:"bytes,3,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // additional data } @@ -230,11 +230,11 @@ func (*Message_Response) Descriptor() ([]byte, []int) { return file_message_proto_rawDescGZIP(), []int{0, 1} } -func (x *Message_Response) GetId() []byte { +func (x *Message_Response) GetId() int32 { if x != nil { return x.Id } - return nil + return 0 } func (x *Message_Response) GetStatus() int32 { @@ -328,7 +328,7 @@ var file_message_proto_rawDesc = []byte{ 0x70, 0x68, 0x73, 0x79, 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xab, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, + 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, @@ -347,7 +347,7 @@ var file_message_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xc9, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x56, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x67, diff --git a/message/pb/message.proto b/message/pb/message.proto index 0703e50a..ed6d561d 100644 --- a/message/pb/message.proto +++ b/message/pb/message.proto @@ -7,7 +7,7 @@ option go_package = ".;graphsync_message_pb"; message Message { message Request { - bytes id = 1; // unique id set on the requester side + int32 id = 1; // unique id set on the requester side bytes root = 2; // a CID for the root node in the query bytes selector = 3; // ipld selector to retrieve map extensions = 4; // aux information. useful for other protocols @@ -17,7 +17,7 @@ message Message { } message Response { - bytes id = 1; // the request id + int32 id = 1; // the request id int32 status = 2; // a status code. map extensions = 3; // additional data } diff --git a/message/pb/message_v1_0_0.pb.go b/message/pb/message_v1_0_0.pb.go deleted file mode 100644 index e567758e..00000000 --- a/message/pb/message_v1_0_0.pb.go +++ /dev/null @@ -1,480 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.19.1 -// source: message_V1_0_0.proto - -package graphsync_message_pb - -import ( - reflect "reflect" - sync "sync" - - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Message_V1_0_0 struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // the actual data included in this message - CompleteRequestList bool `protobuf:"varint,1,opt,name=completeRequestList,proto3" json:"completeRequestList,omitempty"` // This request list includes *all* requests, replacing outstanding requests. - Requests []*Message_V1_0_0_Request `protobuf:"bytes,2,rep,name=requests,proto3" json:"requests,omitempty"` // The list of requests. - Responses []*Message_V1_0_0_Response `protobuf:"bytes,3,rep,name=responses,proto3" json:"responses,omitempty"` // The list of responses. - Data []*Message_V1_0_0_Block `protobuf:"bytes,4,rep,name=data,proto3" json:"data,omitempty"` // Blocks related to the responses -} - -func (x *Message_V1_0_0) Reset() { - *x = Message_V1_0_0{} - if protoimpl.UnsafeEnabled { - mi := &file_message_V1_0_0_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Message_V1_0_0) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message_V1_0_0) ProtoMessage() {} - -func (x *Message_V1_0_0) ProtoReflect() protoreflect.Message { - mi := &file_message_V1_0_0_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message_V1_0_0.ProtoReflect.Descriptor instead. -func (*Message_V1_0_0) Descriptor() ([]byte, []int) { - return file_message_V1_0_0_proto_rawDescGZIP(), []int{0} -} - -func (x *Message_V1_0_0) GetCompleteRequestList() bool { - if x != nil { - return x.CompleteRequestList - } - return false -} - -func (x *Message_V1_0_0) GetRequests() []*Message_V1_0_0_Request { - if x != nil { - return x.Requests - } - return nil -} - -func (x *Message_V1_0_0) GetResponses() []*Message_V1_0_0_Response { - if x != nil { - return x.Responses - } - return nil -} - -func (x *Message_V1_0_0) GetData() []*Message_V1_0_0_Block { - if x != nil { - return x.Data - } - return nil -} - -type Message_V1_0_0_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // unique id set on the requester side - Root []byte `protobuf:"bytes,2,opt,name=root,proto3" json:"root,omitempty"` // a CID for the root node in the query - Selector []byte `protobuf:"bytes,3,opt,name=selector,proto3" json:"selector,omitempty"` // ipld selector to retrieve - Extensions map[string][]byte `protobuf:"bytes,4,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // aux information. useful for other protocols - Priority int32 `protobuf:"varint,5,opt,name=priority,proto3" json:"priority,omitempty"` // the priority (normalized). default to 1 - Cancel bool `protobuf:"varint,6,opt,name=cancel,proto3" json:"cancel,omitempty"` // whether this cancels a request - Update bool `protobuf:"varint,7,opt,name=update,proto3" json:"update,omitempty"` // whether this requests resumes a previous request -} - -func (x *Message_V1_0_0_Request) Reset() { - *x = Message_V1_0_0_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_message_V1_0_0_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Message_V1_0_0_Request) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message_V1_0_0_Request) ProtoMessage() {} - -func (x *Message_V1_0_0_Request) ProtoReflect() protoreflect.Message { - mi := &file_message_V1_0_0_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message_V1_0_0_Request.ProtoReflect.Descriptor instead. -func (*Message_V1_0_0_Request) Descriptor() ([]byte, []int) { - return file_message_V1_0_0_proto_rawDescGZIP(), []int{0, 0} -} - -func (x *Message_V1_0_0_Request) GetId() int32 { - if x != nil { - return x.Id - } - return 0 -} - -func (x *Message_V1_0_0_Request) GetRoot() []byte { - if x != nil { - return x.Root - } - return nil -} - -func (x *Message_V1_0_0_Request) GetSelector() []byte { - if x != nil { - return x.Selector - } - return nil -} - -func (x *Message_V1_0_0_Request) GetExtensions() map[string][]byte { - if x != nil { - return x.Extensions - } - return nil -} - -func (x *Message_V1_0_0_Request) GetPriority() int32 { - if x != nil { - return x.Priority - } - return 0 -} - -func (x *Message_V1_0_0_Request) GetCancel() bool { - if x != nil { - return x.Cancel - } - return false -} - -func (x *Message_V1_0_0_Request) GetUpdate() bool { - if x != nil { - return x.Update - } - return false -} - -type Message_V1_0_0_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` // the request id - Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"` // a status code. - Extensions map[string][]byte `protobuf:"bytes,3,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // additional data -} - -func (x *Message_V1_0_0_Response) Reset() { - *x = Message_V1_0_0_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_message_V1_0_0_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Message_V1_0_0_Response) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message_V1_0_0_Response) ProtoMessage() {} - -func (x *Message_V1_0_0_Response) ProtoReflect() protoreflect.Message { - mi := &file_message_V1_0_0_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message_V1_0_0_Response.ProtoReflect.Descriptor instead. -func (*Message_V1_0_0_Response) Descriptor() ([]byte, []int) { - return file_message_V1_0_0_proto_rawDescGZIP(), []int{0, 1} -} - -func (x *Message_V1_0_0_Response) GetId() int32 { - if x != nil { - return x.Id - } - return 0 -} - -func (x *Message_V1_0_0_Response) GetStatus() int32 { - if x != nil { - return x.Status - } - return 0 -} - -func (x *Message_V1_0_0_Response) GetExtensions() map[string][]byte { - if x != nil { - return x.Extensions - } - return nil -} - -type Message_V1_0_0_Block struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Prefix []byte `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` // CID prefix (cid version, multicodec and multihash prefix (type + length) - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` -} - -func (x *Message_V1_0_0_Block) Reset() { - *x = Message_V1_0_0_Block{} - if protoimpl.UnsafeEnabled { - mi := &file_message_V1_0_0_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Message_V1_0_0_Block) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Message_V1_0_0_Block) ProtoMessage() {} - -func (x *Message_V1_0_0_Block) ProtoReflect() protoreflect.Message { - mi := &file_message_V1_0_0_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Message_V1_0_0_Block.ProtoReflect.Descriptor instead. -func (*Message_V1_0_0_Block) Descriptor() ([]byte, []int) { - return file_message_V1_0_0_proto_rawDescGZIP(), []int{0, 2} -} - -func (x *Message_V1_0_0_Block) GetPrefix() []byte { - if x != nil { - return x.Prefix - } - return nil -} - -func (x *Message_V1_0_0_Block) GetData() []byte { - if x != nil { - return x.Data - } - return nil -} - -var File_message_V1_0_0_proto protoreflect.FileDescriptor - -var file_message_V1_0_0_proto_rawDesc = []byte{ - 0x0a, 0x14, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x14, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, 0x6e, - 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x22, 0xd6, 0x06, 0x0a, - 0x0e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x12, - 0x30, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x63, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x69, 0x73, - 0x74, 0x12, 0x48, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, 0x6e, 0x63, 0x2e, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x12, 0x4b, 0x0a, 0x09, 0x72, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, - 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, - 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x09, 0x72, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, - 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0xb2, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x04, 0x72, 0x6f, 0x6f, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x12, 0x5c, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, - 0x73, 0x79, 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, - 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x12, 0x16, - 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, - 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x3d, - 0x0a, 0x0f, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xd0, 0x01, - 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x5d, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x67, 0x72, 0x61, 0x70, 0x68, 0x73, 0x79, - 0x6e, 0x63, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x56, 0x31, 0x5f, 0x30, 0x5f, 0x30, 0x2e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, - 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x1a, 0x33, 0x0a, 0x05, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, - 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, - 0x78, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x04, 0x64, 0x61, 0x74, 0x61, 0x42, 0x18, 0x5a, 0x16, 0x2e, 0x3b, 0x67, 0x72, 0x61, 0x70, 0x68, - 0x73, 0x79, 0x6e, 0x63, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x62, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_message_V1_0_0_proto_rawDescOnce sync.Once - file_message_V1_0_0_proto_rawDescData = file_message_V1_0_0_proto_rawDesc -) - -func file_message_V1_0_0_proto_rawDescGZIP() []byte { - file_message_V1_0_0_proto_rawDescOnce.Do(func() { - file_message_V1_0_0_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_V1_0_0_proto_rawDescData) - }) - return file_message_V1_0_0_proto_rawDescData -} - -var file_message_V1_0_0_proto_msgTypes = make([]protoimpl.MessageInfo, 6) -var file_message_V1_0_0_proto_goTypes = []interface{}{ - (*Message_V1_0_0)(nil), // 0: graphsync.message.pb.Message_V1_0_0 - (*Message_V1_0_0_Request)(nil), // 1: graphsync.message.pb.Message_V1_0_0.Request - (*Message_V1_0_0_Response)(nil), // 2: graphsync.message.pb.Message_V1_0_0.Response - (*Message_V1_0_0_Block)(nil), // 3: graphsync.message.pb.Message_V1_0_0.Block - nil, // 4: graphsync.message.pb.Message_V1_0_0.Request.ExtensionsEntry - nil, // 5: graphsync.message.pb.Message_V1_0_0.Response.ExtensionsEntry -} -var file_message_V1_0_0_proto_depIdxs = []int32{ - 1, // 0: graphsync.message.pb.Message_V1_0_0.requests:type_name -> graphsync.message.pb.Message_V1_0_0.Request - 2, // 1: graphsync.message.pb.Message_V1_0_0.responses:type_name -> graphsync.message.pb.Message_V1_0_0.Response - 3, // 2: graphsync.message.pb.Message_V1_0_0.data:type_name -> graphsync.message.pb.Message_V1_0_0.Block - 4, // 3: graphsync.message.pb.Message_V1_0_0.Request.extensions:type_name -> graphsync.message.pb.Message_V1_0_0.Request.ExtensionsEntry - 5, // 4: graphsync.message.pb.Message_V1_0_0.Response.extensions:type_name -> graphsync.message.pb.Message_V1_0_0.Response.ExtensionsEntry - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name -} - -func init() { file_message_V1_0_0_proto_init() } -func file_message_V1_0_0_proto_init() { - if File_message_V1_0_0_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_message_V1_0_0_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message_V1_0_0); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_message_V1_0_0_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message_V1_0_0_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_message_V1_0_0_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message_V1_0_0_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_message_V1_0_0_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Message_V1_0_0_Block); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_message_V1_0_0_proto_rawDesc, - NumEnums: 0, - NumMessages: 6, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_message_V1_0_0_proto_goTypes, - DependencyIndexes: file_message_V1_0_0_proto_depIdxs, - MessageInfos: file_message_V1_0_0_proto_msgTypes, - }.Build() - File_message_V1_0_0_proto = out.File - file_message_V1_0_0_proto_rawDesc = nil - file_message_V1_0_0_proto_goTypes = nil - file_message_V1_0_0_proto_depIdxs = nil -} diff --git a/message/pb/message_v1_0_0.proto b/message/pb/message_v1_0_0.proto deleted file mode 100644 index 81f26699..00000000 --- a/message/pb/message_v1_0_0.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto3"; - -package graphsync.message.pb; - -option go_package = ".;graphsync_message_pb"; - -message Message_V1_0_0 { - - message Request { - int32 id = 1; // unique id set on the requester side - bytes root = 2; // a CID for the root node in the query - bytes selector = 3; // ipld selector to retrieve - map extensions = 4; // aux information. useful for other protocols - int32 priority = 5; // the priority (normalized). default to 1 - bool cancel = 6; // whether this cancels a request - bool update = 7; // whether this requests resumes a previous request - } - - message Response { - int32 id = 1; // the request id - int32 status = 2; // a status code. - map extensions = 3; // additional data - } - - message Block { - bytes prefix = 1; // CID prefix (cid version, multicodec and multihash prefix (type + length) - bytes data = 2; - } - - // the actual data included in this message - bool completeRequestList = 1; // This request list includes *all* requests, replacing outstanding requests. - repeated Request requests = 2; // The list of requests. - repeated Response responses = 3; // The list of responses. - repeated Block data = 4; // Blocks related to the responses - -} diff --git a/message/v1/message.go b/message/v1/message.go index e8b458a4..d6cb4515 100644 --- a/message/v1/message.go +++ b/message/v1/message.go @@ -61,7 +61,7 @@ func (mh *MessageHandler) FromMsgReader(p peer.ID, r msgio.Reader) (message.Grap return message.GraphSyncMessage{}, err } - var pb pb.Message_V1_0_0 + var pb pb.Message err = proto.Unmarshal(msg, &pb) r.ReleaseMsg(msg) if err != nil { @@ -91,14 +91,14 @@ func (mh *MessageHandler) ToNet(p peer.ID, gsm message.GraphSyncMessage, w io.Wr return err } -// toProto converts a GraphSyncMessage to its pb.Message_V1_0_0 equivalent -func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb.Message_V1_0_0, error) { +// 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_V1_0_0) + pbm := new(pb.Message) requests := gsm.Requests() - pbm.Requests = make([]*pb.Message_V1_0_0_Request, 0, len(requests)) + pbm.Requests = make([]*pb.Message_Request, 0, len(requests)) for _, request := range requests { var selector []byte var err error @@ -116,7 +116,7 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. if err != nil { return nil, err } - pbm.Requests = append(pbm.Requests, &pb.Message_V1_0_0_Request{ + pbm.Requests = append(pbm.Requests, &pb.Message_Request{ Id: rid, Root: request.Root().Bytes(), Selector: selector, @@ -128,7 +128,7 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. } responses := gsm.Responses() - pbm.Responses = make([]*pb.Message_V1_0_0_Response, 0, len(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 { @@ -138,7 +138,7 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. if err != nil { return nil, err } - pbm.Responses = append(pbm.Responses, &pb.Message_V1_0_0_Response{ + pbm.Responses = append(pbm.Responses, &pb.Message_Response{ Id: rid, Status: int32(response.Status()), Extensions: ext, @@ -146,9 +146,9 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. } blocks := gsm.Blocks() - pbm.Data = make([]*pb.Message_V1_0_0_Block, 0, len(blocks)) + pbm.Data = make([]*pb.Message_Block, 0, len(blocks)) for _, b := range blocks { - pbm.Data = append(pbm.Data, &pb.Message_V1_0_0_Block{ + pbm.Data = append(pbm.Data, &pb.Message_Block{ Prefix: b.Cid().Prefix().Bytes(), Data: b.RawData(), }) @@ -156,9 +156,9 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. return pbm, nil } -// Mapping from a pb.Message_V1_0_0 object to a GraphSyncMessage object, including +// Mapping from a pb.Message object to a GraphSyncMessage object, including // RequestID (int / uuid) mapping. -func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message_V1_0_0) (message.GraphSyncMessage, error) { +func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message) (message.GraphSyncMessage, error) { mh.mapLock.Lock() defer mh.mapLock.Unlock() From eb16b2746c06c21d0cd03362d620428d48dd0c08 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 2 Feb 2022 16:50:53 +1100 Subject: [PATCH 21/32] fix: clarify comments re dag-cbor extension data As per dission in https://github.com/ipfs/go-graphsync/pull/338, we are going to be erroring on extension data that is not properly dag-cbor encoded from now on --- message/v1/message.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/message/v1/message.go b/message/v1/message.go index d6cb4515..969d7784 100644 --- a/message/v1/message.go +++ b/message/v1/message.go @@ -245,7 +245,8 @@ func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message) (messa return message.NewMessage(requests, responses, blks), nil } -// TODO: is this a breaking protocol change? force all extension data into dag-cbor? +// 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 MessagePartWithExtensions) (map[string][]byte, error) { names := part.ExtensionNames() out := make(map[string][]byte, len(names)) @@ -264,7 +265,6 @@ func toEncodedExtensions(part MessagePartWithExtensions) (map[string][]byte, err return out, nil } -// TODO: is this a breaking protocol change? force all extension data into dag-cbor? func fromEncodedExtensions(in map[string][]byte) ([]graphsync.ExtensionData, error) { if in == nil { return []graphsync.ExtensionData{}, nil From 83c3860142cfbed18b8d87a6b3d375d017ef4fc8 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 4 Feb 2022 14:16:54 +1100 Subject: [PATCH 22/32] feat: new LinkMetadata iface, integrate metadata into Response type (#342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(metadata): new LinkMetadata iface, integrate metadata into Response type * LinkMetadata wrapper around existing metadata type to allow for easier backward-compat upgrade path * integrate metadata directly into GraphSyncResponse type, moving it from an optional extension * still deal with metadata as an extension for now—further work for v2 protocol will move it into the core message schema Ref: https://github.com/ipfs/go-graphsync/issues/335 * feat(metadata): move metadata to core protocol, only use extension in v1 proto * fix(metadata): bindnode expects Go enum strings to be at the type level * fix(metadata): minor fixes, tidy up naming * fix(metadata): make gofmt and staticcheck happy * fix(metadata): docs and minor tweaks after review Co-authored-by: Daniel Martí --- go.mod | 4 +- go.sum | 10 +- graphsync.go | 33 +++- message/builder.go | 18 +- message/builder_test.go | 91 +++++----- message/ipldbind/message.go | 28 +-- message/ipldbind/schema.ipldsch | 46 +++-- message/message.go | 72 +++++++- message/v1/message.go | 70 +++++--- message/v1/message_test.go | 4 +- {metadata => message/v1/metadata}/metadata.go | 17 ++ .../v1/metadata}/metadata_test.go | 4 - {metadata => message/v1/metadata}/schema.go | 0 .../v1/metadata}/schema.ipldsch | 0 message/v1/pb_roundtrip_test.go | 111 ++++++++++++ message/v2/ipld_roundtrip_test.go | 108 ++++++++++++ message/v2/message.go | 18 +- messagequeue/messagequeue_test.go | 8 +- requestmanager/asyncloader/asyncloader.go | 5 +- .../asyncloader/asyncloader_test.go | 104 ++++++------ .../responsecache/responsecache.go | 12 +- .../responsecache/responsecache_test.go | 48 +++--- requestmanager/client.go | 7 +- requestmanager/executor/executor_test.go | 2 +- requestmanager/hooks/hooks_test.go | 4 +- requestmanager/requestmanager_test.go | 160 +++++++----------- requestmanager/server.go | 12 +- requestmanager/testloader/asyncloader.go | 23 ++- requestmanager/utils.go | 31 ---- .../responseassembler/responseBuilder.go | 6 +- 30 files changed, 678 insertions(+), 378 deletions(-) rename {metadata => message/v1/metadata}/metadata.go (68%) rename {metadata => message/v1/metadata}/metadata_test.go (94%) rename {metadata => message/v1/metadata}/schema.go (100%) rename {metadata => message/v1/metadata}/schema.ipldsch (100%) create mode 100644 message/v1/pb_roundtrip_test.go create mode 100644 message/v2/ipld_roundtrip_test.go delete mode 100644 requestmanager/utils.go diff --git a/go.mod b/go.mod index dd5fd85f..f7a910ea 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ipfs/go-graphsync go 1.16 require ( - github.com/google/go-cmp v0.5.6 + 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 @@ -28,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.5-0.20220121142026-257b06219831 + github.com/ipld/go-ipld-prime v0.14.5-0.20220202110753-c322674203f0 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 diff --git a/go.sum b/go.sum index 1a3fdd32..9754b3ab 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= @@ -454,8 +456,8 @@ github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvB 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/go.mod h1:QcE4Y9n/ZZr8Ijg5bGPT0GqYWgZ1704nH0RDcQtgTP0= -github.com/ipld/go-ipld-prime v0.14.5-0.20220121142026-257b06219831 h1:hHLYeedwqakiOMaGI6HWF84geJu2VL6OZ1DrrhyY70s= -github.com/ipld/go-ipld-prime v0.14.5-0.20220121142026-257b06219831/go.mod h1:QcE4Y9n/ZZr8Ijg5bGPT0GqYWgZ1704nH0RDcQtgTP0= +github.com/ipld/go-ipld-prime v0.14.5-0.20220202110753-c322674203f0 h1:7X6qWhXCRVVe+eeL2XZtSgVgqKCQNwuuDAjOTE/97kg= +github.com/ipld/go-ipld-prime v0.14.5-0.20220202110753-c322674203f0/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= diff --git a/graphsync.go b/graphsync.go index 8c75e778..17b8e934 100644 --- a/graphsync.go +++ b/graphsync.go @@ -62,11 +62,6 @@ 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 @@ -190,6 +185,34 @@ type ResponseData interface { // Extension returns the content for an extension on a response, or errors // if extension is not present 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 { + // 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 diff --git a/message/builder.go b/message/builder.go index 5ea86f92..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,13 +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 := metadata.EncodeMetadata(linkMap) - 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 bb3545e4..7cb325cf 100644 --- a/message/builder_test.go +++ b/message/builder_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" "github.com/ipfs/go-graphsync/testutil" ) @@ -48,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) @@ -83,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) @@ -139,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) @@ -170,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) @@ -197,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) @@ -228,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) @@ -267,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 index 7199524b..849100af 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -2,22 +2,23 @@ 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" ) -type MessagePartWithExtensions interface { - ExtensionNames() []graphsync.ExtensionName - Extension(name graphsync.ExtensionName) (datamodel.Node, bool) -} - +// 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 } -func NewGraphSyncExtensions(part MessagePartWithExtensions) GraphSyncExtensions { +// NewGraphSyncExtensions creates GraphSyncExtensions from either a request or +// response object +func NewGraphSyncExtensions(part message.MessagePartWithExtensions) GraphSyncExtensions { names := part.ExtensionNames() keys := make([]string, 0, len(names)) values := make(map[string]datamodel.Node, len(names)) @@ -29,6 +30,8 @@ func NewGraphSyncExtensions(part MessagePartWithExtensions) GraphSyncExtensions 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 { @@ -50,33 +53,32 @@ type GraphSyncRequest struct { Update bool } -type GraphSyncMetadatum struct { - Link datamodel.Link - BlockPresent bool -} - // GraphSyncResponse is an struct to capture data on a response sent back // in a GraphSyncMessage. type GraphSyncResponse struct { Id []byte Status graphsync.ResponseStatusCode - Metadata []GraphSyncMetadatum + 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 } -// NamedExtension exists just for the purpose of the constructors. +// NamedExtension exists just for the purpose of the constructors type NamedExtension struct { Name graphsync.ExtensionName Data datamodel.Node diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch index db89db93..0cbdaebe 100644 --- a/message/ipldbind/schema.ipldsch +++ b/message/ipldbind/schema.ipldsch @@ -2,9 +2,25 @@ type GraphSyncExtensions {String:nullable Any} type GraphSyncRequestID bytes type GraphSyncPriority int +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 + type GraphSyncMetadatum struct { link Link - blockPresent Bool + action GraphSyncLinkAction } representation tuple type GraphSyncMetadata [GraphSyncMetadatum] @@ -12,26 +28,26 @@ type GraphSyncMetadata [GraphSyncMetadatum] type GraphSyncResponseStatusCode enum { # Informational Codes (request in progress) - | RequestAcknowledged ("10") - | AdditionalPeers ("11") - | NotEnoughGas ("12") - | OtherProtocol ("13") - | PartialResponse ("14") - | RequestPaused ("15") + | RequestAcknowledged ("10") + | AdditionalPeers ("11") + | NotEnoughGas ("12") + | OtherProtocol ("13") + | PartialResponse ("14") + | RequestPaused ("15") # Success Response Codes (request terminated) - | RequestCompletedFull ("20") - | RequestCompletedPartial ("21") + | RequestCompletedFull ("20") + | RequestCompletedPartial ("21") # Error Response Codes (request terminated) - | RequestRejected ("30") - | RequestFailedBusy ("31") - | RequestFailedUnknown ("32") - | RequestFailedLegal ("33") - | RequestFailedContentNotFound ("34") - | RequestCancelled ("35") + | RequestRejected ("30") + | RequestFailedBusy ("31") + | RequestFailedUnknown ("32") + | RequestFailedLegal ("33") + | RequestFailedContentNotFound ("34") + | RequestCancelled ("35") } representation int type GraphSyncRequest struct { diff --git a/message/message.go b/message/message.go index f88d335b..c28f946c 100644 --- a/message/message.go +++ b/message/message.go @@ -17,12 +17,21 @@ import ( "github.com/ipfs/go-graphsync" ) +// 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 } +// 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 { @@ -64,9 +73,24 @@ func (gsr GraphSyncRequest) String() string { type GraphSyncResponse struct { requestID graphsync.RequestID status graphsync.ResponseStatusCode + 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{} @@ -89,6 +113,8 @@ type GraphSyncMessage struct { blocks map[cid.Cid]blocks.Block } +// NewMessage generates a new message containing the provided requests, +// responses and blocks func NewMessage( requests map[graphsync.RequestID]GraphSyncRequest, responses map[graphsync.RequestID]GraphSyncResponse, @@ -101,8 +127,7 @@ func NewMessage( // its contents func (gsm GraphSyncMessage) String() string { cts := make([]string, 0) - for i, req := range gsm.requests { - fmt.Printf("req.String(%v)\n", i) + for _, req := range gsm.requests { cts = append(cts, req.String()) } for _, resp := range gsm.responses { @@ -134,6 +159,12 @@ func NewUpdateRequest(id graphsync.RequestID, extensions ...graphsync.ExtensionD 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]datamodel.Node) { if len(extensions) > 0 { extensionsMap = make(map[string]datamodel.Node, len(extensions)) @@ -151,6 +182,7 @@ func newRequest(id graphsync.RequestID, isCancel bool, isUpdate bool, extensions map[string]datamodel.Node) GraphSyncRequest { + return GraphSyncRequest{ id: id, root: root, @@ -165,23 +197,32 @@ func newRequest(id graphsync.RequestID, // 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]datamodel.Node) GraphSyncResponse { + status graphsync.ResponseStatusCode, + responseMetadata []GraphSyncLinkMetadatum, + extensions map[string]datamodel.Node) GraphSyncResponse { + return GraphSyncResponse{ requestID: requestID, status: status, + metadata: responseMetadata, extensions: extensions, } } +// 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 { @@ -200,6 +241,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 { @@ -208,6 +250,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 { @@ -301,6 +344,27 @@ func (gsr GraphSyncResponse) ExtensionNames() []graphsync.ExtensionName { 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) + } +} + +// 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 { diff --git a/message/v1/message.go b/message/v1/message.go index 969d7784..713b4580 100644 --- a/message/v1/message.go +++ b/message/v1/message.go @@ -3,6 +3,7 @@ package v1 import ( "encoding/binary" "errors" + "fmt" "io" "sync" @@ -19,18 +20,20 @@ import ( "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" ) -type MessagePartWithExtensions interface { - ExtensionNames() []graphsync.ExtensionName - Extension(name graphsync.ExtensionName) (datamodel.Node, bool) -} +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 @@ -68,7 +71,7 @@ func (mh *MessageHandler) FromMsgReader(p peer.ID, r msgio.Reader) (message.Grap return message.GraphSyncMessage{}, err } - return mh.newMessageFromProto(p, &pb) + return mh.fromProto(p, &pb) } // ToNet writes a GraphSyncMessage in its v1.0.0 protobuf format to a writer @@ -112,7 +115,7 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. if err != nil { return nil, err } - ext, err := toEncodedExtensions(request) + ext, err := toEncodedExtensions(request, nil) if err != nil { return nil, err } @@ -134,7 +137,7 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. if err != nil { return nil, err } - ext, err := toEncodedExtensions(response) + ext, err := toEncodedExtensions(response, response.Metadata()) if err != nil { return nil, err } @@ -158,7 +161,7 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. // Mapping from a pb.Message object to a GraphSyncMessage object, including // RequestID (int / uuid) mapping. -func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message) (message.GraphSyncMessage, error) { +func (mh *MessageHandler) fromProto(p peer.ID, pbm *pb.Message) (message.GraphSyncMessage, error) { mh.mapLock.Lock() defer mh.mapLock.Unlock() @@ -179,11 +182,15 @@ func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message) (messa continue } - exts, err := fromEncodedExtensions(req.GetExtensions()) + 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 @@ -211,11 +218,11 @@ func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message) (messa if err != nil { return message.GraphSyncMessage{}, err } - exts, err := fromEncodedExtensions(res.GetExtensions()) + exts, metadata, err := fromEncodedExtensions(res.GetExtensions()) if err != nil { return message.GraphSyncMessage{}, err } - responses[id] = message.NewResponse(id, graphsync.ResponseStatusCode(res.Status), exts...) + responses[id] = message.NewResponse(id, graphsync.ResponseStatusCode(res.Status), metadata, exts...) } blks := make(map[cid.Cid]blocks.Block, len(pbm.GetData())) @@ -247,7 +254,7 @@ func (mh *MessageHandler) newMessageFromProto(p peer.ID, pbm *pb.Message) (messa // 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 MessagePartWithExtensions) (map[string][]byte, 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 { @@ -262,26 +269,47 @@ func toEncodedExtensions(part MessagePartWithExtensions) (map[string][]byte, 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, error) { +func fromEncodedExtensions(in map[string][]byte) ([]graphsync.ExtensionData, []message.GraphSyncLinkMetadatum, error) { if in == nil { - return []graphsync.ExtensionData{}, nil + return []graphsync.ExtensionData{}, nil, nil } out := make([]graphsync.ExtensionData, 0, len(in)) + var md []message.GraphSyncLinkMetadatum for name, data := range in { - if len(data) == 0 { - out = append(out, graphsync.ExtensionData{Name: graphsync.ExtensionName(name), Data: nil}) - } else { - data, err := ipldutil.DecodeNode(data) + var node datamodel.Node + var err error + if len(data) > 0 { + node, err = ipldutil.DecodeNode(data) if err != nil { - return nil, err + 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}) } - out = append(out, graphsync.ExtensionData{Name: graphsync.ExtensionName(name), Data: data}) } } - return out, nil + return out, md, nil } // Maps a []byte slice form of a RequestID (uuid) to an integer format as used diff --git a/message/v1/message_test.go b/message/v1/message_test.go index 1655d415..e19acf5b 100644 --- a/message/v1/message_test.go +++ b/message/v1/message_test.go @@ -68,7 +68,7 @@ func TestAppendingRequests(t *testing.T) { require.True(t, ok) require.Equal(t, expectedByts, actualByts) - deserialized, err := mh.newMessageFromProto(peer.ID("foo"), pbMessage) + 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") @@ -117,7 +117,7 @@ func TestAppendingResponses(t *testing.T) { require.Equal(t, int32(status), pbResponse.Status) require.Equal(t, []byte("stest extension data"), pbResponse.Extensions["graphsync/awesome"]) - deserialized, err := mh.newMessageFromProto(p, 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") diff --git a/metadata/metadata.go b/message/v1/metadata/metadata.go similarity index 68% rename from metadata/metadata.go rename to message/v1/metadata/metadata.go index ede29be6..2e0bf487 100644 --- a/metadata/metadata.go +++ b/message/v1/metadata/metadata.go @@ -2,6 +2,8 @@ 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" ) @@ -16,6 +18,21 @@ type Item struct { // 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) { diff --git a/metadata/metadata_test.go b/message/v1/metadata/metadata_test.go similarity index 94% rename from metadata/metadata_test.go rename to message/v1/metadata/metadata_test.go index 55488000..6a7abb38 100644 --- a/metadata/metadata_test.go +++ b/message/v1/metadata/metadata_test.go @@ -2,10 +2,8 @@ package metadata import ( "math/rand" - "os" "testing" - "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/fluent" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" @@ -39,6 +37,4 @@ func TestDecodeEncodeMetadata(t *testing.T) { decodedMetadataFromNode, err := DecodeMetadata(nd) require.NoError(t, err) require.Equal(t, decodedMetadata, decodedMetadataFromNode, "metadata not equal to IPLD encoding") - - dagjson.Encode(nd, os.Stdout) } diff --git a/metadata/schema.go b/message/v1/metadata/schema.go similarity index 100% rename from metadata/schema.go rename to message/v1/metadata/schema.go diff --git a/metadata/schema.ipldsch b/message/v1/metadata/schema.ipldsch similarity index 100% rename from metadata/schema.ipldsch rename to message/v1/metadata/schema.ipldsch diff --git a/message/v1/pb_roundtrip_test.go b/message/v1/pb_roundtrip_test.go new file mode 100644 index 00000000..79215741 --- /dev/null +++ b/message/v1/pb_roundtrip_test.go @@ -0,0 +1,111 @@ +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" + "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" +) + +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") + gsm := message.NewMessage(requests, responses, blocks) + pgsm, err := mh.ToProto(p, gsm) + require.NoError(t, err) + rtgsm, err := mh.fromProto(p, pgsm) + 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..61a07e0e --- /dev/null +++ b/message/v2/ipld_roundtrip_test.go @@ -0,0 +1,108 @@ +package v2 + +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" + "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/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], + } + + gsm := message.NewMessage(requests, responses, blocks) + igsm, err := NewMessageHandler().toIPLD(gsm) + require.NoError(t, err) + rtgsm, err := NewMessageHandler().fromIPLD(igsm) + 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 index 7c63a3a7..29098d24 100644 --- a/message/v2/message.go +++ b/message/v2/message.go @@ -3,6 +3,7 @@ package v2 import ( "bytes" "encoding/binary" + "fmt" "io" blocks "github.com/ipfs/go-block-format" @@ -19,8 +20,12 @@ import ( "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{} } @@ -43,7 +48,6 @@ func (mh *MessageHandler) FromMsgReader(_ peer.ID, r msgio.Reader) (message.Grap if err != nil { return message.GraphSyncMessage{}, err } - node := builder.Build() ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessage) return mh.fromIPLD(ipldGSM) @@ -79,9 +83,14 @@ func (mh *MessageHandler) toIPLD(gsm message.GraphSyncMessage) (*ipldbind.GraphS responses := gsm.Responses() ibm.Responses = 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") + } ibm.Responses = append(ibm.Responses, ipldbind.GraphSyncResponse{ Id: response.RequestID().Bytes(), Status: response.Status(), + Metadata: glsm.RawMetadata(), Extensions: ipldbind.NewGraphSyncExtensions(response), }) } @@ -115,7 +124,6 @@ func (mh *MessageHandler) ToNet(_ peer.ID, gsm message.GraphSyncMessage, w io.Wr if err != nil { return err } - //_, err = buf.WriteTo(w) lbuflen := binary.PutUvarint(lbuf, uint64(buf.Len()-binary.MaxVarintLen64)) out := buf.Bytes() @@ -159,12 +167,14 @@ func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (message.Grap responses := make(map[graphsync.RequestID]message.GraphSyncResponse, len(ibm.Responses)) for _, res := range ibm.Responses { - // exts := res.Extensions id, err := graphsync.ParseRequestID(res.Id) if err != nil { return message.GraphSyncMessage{}, err } - responses[id] = message.NewResponse(id, graphsync.ResponseStatusCode(res.Status), res.Extensions.ToExtensionsList()...) + responses[id] = message.NewResponse(id, + graphsync.ResponseStatusCode(res.Status), + res.Metadata, + res.Extensions.ToExtensionsList()...) } blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) diff --git a/messagequeue/messagequeue_test.go b/messagequeue/messagequeue_test.go index f1c38a97..f182f594 100644 --- a/messagequeue/messagequeue_test.go +++ b/messagequeue/messagequeue_test.go @@ -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/requestmanager/asyncloader/asyncloader.go b/requestmanager/asyncloader/asyncloader.go index ea478099..b97c1576 100644 --- a/requestmanager/asyncloader/asyncloader.go +++ b/requestmanager/asyncloader/asyncloader.go @@ -15,7 +15,6 @@ import ( "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" @@ -108,7 +107,7 @@ func (al *AsyncLoader) StartRequest(requestID graphsync.RequestID, persistenceOp // neccesary func (al *AsyncLoader) ProcessResponse( ctx context.Context, - responses map[graphsync.RequestID]metadata.Metadata, + responses map[graphsync.RequestID]graphsync.LinkMetadata, blks []blocks.Block) { requestIds := make([]string, 0, len(responses)) @@ -130,7 +129,7 @@ func (al *AsyncLoader) ProcessResponse( for queue, requestIDs := range byQueue { loadAttemptQueue := al.getLoadAttemptQueue(queue) responseCache := al.getResponseCache(queue) - queueResponses := make(map[graphsync.RequestID]metadata.Metadata, len(requestIDs)) + queueResponses := make(map[graphsync.RequestID]graphsync.LinkMetadata, len(requestIDs)) for _, requestID := range requestIDs { queueResponses[requestID] = responses[requestID] } diff --git a/requestmanager/asyncloader/asyncloader_test.go b/requestmanager/asyncloader/asyncloader_test.go index 8b6717fa..5845e260 100644 --- a/requestmanager/asyncloader/asyncloader_test.go +++ b/requestmanager/asyncloader/asyncloader_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" + "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/requestmanager/types" "github.com/ipfs/go-graphsync/testutil" ) @@ -38,13 +38,12 @@ func TestAsyncLoadInitialLoadSucceedsResponsePresent(t *testing.T) { st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { requestID := graphsync.NewRequestID() - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.Cid, + Action: graphsync.LinkActionPresent, + }}), } p := testutil.GeneratePeers(1)[0] asyncLoader.ProcessResponse(context.Background(), responses, blocks) @@ -62,13 +61,12 @@ func TestAsyncLoadInitialLoadFails(t *testing.T) { link := testutil.NewTestLink() requestID := graphsync.NewRequestID() - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.(cidlink.Link).Cid, - BlockPresent: false, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.(cidlink.Link).Cid, + Action: graphsync.LinkActionMissing, + }}), } p := testutil.GeneratePeers(1)[0] asyncLoader.ProcessResponse(context.Background(), responses, nil) @@ -107,13 +105,12 @@ func TestAsyncLoadInitialLoadIndeterminateThenSucceeds(t *testing.T) { st.AssertAttemptLoadWithoutResult(ctx, t, resultChan) - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.Cid, + Action: graphsync.LinkActionPresent, + }}), } asyncLoader.ProcessResponse(context.Background(), responses, blocks) assertSuccessResponse(ctx, t, resultChan) @@ -135,13 +132,12 @@ func TestAsyncLoadInitialLoadIndeterminateThenFails(t *testing.T) { st.AssertAttemptLoadWithoutResult(ctx, t, resultChan) - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.(cidlink.Link).Cid, - BlockPresent: false, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.(cidlink.Link).Cid, + Action: graphsync.LinkActionMissing, + }}), } asyncLoader.ProcessResponse(context.Background(), responses, nil) assertFailResponse(ctx, t, resultChan) @@ -172,13 +168,12 @@ func TestAsyncLoadTwiceLoadsLocallySecondTime(t *testing.T) { st := newStore() withLoader(st, func(ctx context.Context, asyncLoader *AsyncLoader) { requestID := graphsync.NewRequestID() - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID: { - metadata.Item{ - Link: link.Cid, - BlockPresent: true, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.Cid, + Action: graphsync.LinkActionPresent, + }}), } p := testutil.GeneratePeers(1)[0] asyncLoader.ProcessResponse(context.Background(), responses, blocks) @@ -267,19 +262,17 @@ func TestRequestSplittingSameBlockTwoStores(t *testing.T) { 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, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID1: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.Cid, + Action: graphsync.LinkActionPresent, + }}), + requestID2: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.Cid, + Action: graphsync.LinkActionPresent, + }}), } asyncLoader.ProcessResponse(context.Background(), responses, blocks) @@ -308,13 +301,12 @@ func TestRequestSplittingSameBlockOnlyOneResponse(t *testing.T) { 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, - }, - }, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID2: message.NewLinkMetadata( + []message.GraphSyncLinkMetadatum{{ + Link: link.Cid, + Action: graphsync.LinkActionPresent, + }}), } asyncLoader.ProcessResponse(context.Background(), responses, blocks) asyncLoader.CompleteResponsesFor(requestID1) diff --git a/requestmanager/asyncloader/responsecache/responsecache.go b/requestmanager/asyncloader/responsecache/responsecache.go index a490178b..ffaab02e 100644 --- a/requestmanager/asyncloader/responsecache/responsecache.go +++ b/requestmanager/asyncloader/responsecache/responsecache.go @@ -5,6 +5,7 @@ import ( "sync" blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "github.com/ipld/go-ipld-prime" cidlink "github.com/ipld/go-ipld-prime/linking/cid" @@ -14,7 +15,6 @@ import ( "github.com/ipfs/go-graphsync" "github.com/ipfs/go-graphsync/linktracker" - "github.com/ipfs/go-graphsync/metadata" ) var log = logging.Logger("graphsync") @@ -73,7 +73,7 @@ func (rc *ResponseCache) AttemptLoad(requestID graphsync.RequestID, link ipld.Li // and tracking link metadata from a remote peer func (rc *ResponseCache) ProcessResponse( ctx context.Context, - responses map[graphsync.RequestID]metadata.Metadata, + responses map[graphsync.RequestID]graphsync.LinkMetadata, blks []blocks.Block) { ctx, span := otel.Tracer("graphsync").Start(ctx, "cacheProcess", trace.WithAttributes( @@ -90,10 +90,10 @@ func (rc *ResponseCache) ProcessResponse( } for requestID, md := range responses { - for _, item := range md { - log.Debugf("Traverse link %s on request ID %s", item.Link.String(), requestID.String()) - rc.linkTracker.RecordLinkTraversal(requestID, cidlink.Link{Cid: item.Link}, item.BlockPresent) - } + md.Iterate(func(c cid.Cid, la graphsync.LinkAction) { + log.Debugf("Traverse link %s on request ID %s", c.String(), requestID.String()) + rc.linkTracker.RecordLinkTraversal(requestID, cidlink.Link{Cid: c}, la == graphsync.LinkActionPresent) + }) } // prune unused blocks right away diff --git a/requestmanager/asyncloader/responsecache/responsecache_test.go b/requestmanager/asyncloader/responsecache/responsecache_test.go index 7034c13a..7cc8497f 100644 --- a/requestmanager/asyncloader/responsecache/responsecache_test.go +++ b/requestmanager/asyncloader/responsecache/responsecache_test.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/ipfs/go-graphsync" - "github.com/ipfs/go-graphsync/metadata" + "github.com/ipfs/go-graphsync/message" "github.com/ipfs/go-graphsync/testutil" ) @@ -61,39 +61,39 @@ func TestResponseCacheManagingLinks(t *testing.T) { requestID1 := graphsync.NewRequestID() requestID2 := graphsync.NewRequestID() - request1Metadata := metadata.Metadata{ - metadata.Item{ - Link: blks[0].Cid(), - BlockPresent: true, + request1Metadata := []message.GraphSyncLinkMetadatum{ + { + Link: blks[0].Cid(), + Action: graphsync.LinkActionPresent, }, - metadata.Item{ - Link: blks[1].Cid(), - BlockPresent: false, + { + Link: blks[1].Cid(), + Action: graphsync.LinkActionMissing, }, - metadata.Item{ - Link: blks[3].Cid(), - BlockPresent: true, + { + Link: blks[3].Cid(), + Action: graphsync.LinkActionPresent, }, } - request2Metadata := metadata.Metadata{ - metadata.Item{ - Link: blks[1].Cid(), - BlockPresent: true, + request2Metadata := []message.GraphSyncLinkMetadatum{ + { + Link: blks[1].Cid(), + Action: graphsync.LinkActionPresent, }, - metadata.Item{ - Link: blks[3].Cid(), - BlockPresent: true, + { + Link: blks[3].Cid(), + Action: graphsync.LinkActionPresent, }, - metadata.Item{ - Link: blks[4].Cid(), - BlockPresent: true, + { + Link: blks[4].Cid(), + Action: graphsync.LinkActionPresent, }, } - responses := map[graphsync.RequestID]metadata.Metadata{ - requestID1: request1Metadata, - requestID2: request2Metadata, + responses := map[graphsync.RequestID]graphsync.LinkMetadata{ + requestID1: message.NewLinkMetadata(request1Metadata), + requestID2: message.NewLinkMetadata(request2Metadata), } fubs := &fakeUnverifiedBlockStore{ diff --git a/requestmanager/client.go b/requestmanager/client.go index f579842f..5002f516 100644 --- a/requestmanager/client.go +++ b/requestmanager/client.go @@ -23,7 +23,6 @@ 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" @@ -73,8 +72,7 @@ type PeerHandler interface { // 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) + ProcessResponse(ctx context.Context, responses map[graphsync.RequestID]graphsync.LinkMetadata, 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) @@ -286,7 +284,8 @@ 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) } diff --git a/requestmanager/executor/executor_test.go b/requestmanager/executor/executor_test.go index 501cbedb..fc0c57f3 100644 --- a/requestmanager/executor/executor_test.go +++ b/requestmanager/executor/executor_test.go @@ -302,7 +302,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, diff --git a/requestmanager/hooks/hooks_test.go b/requestmanager/hooks/hooks_test.go index d55c612b..0ee19aef 100644 --- a/requestmanager/hooks/hooks_test.go +++ b/requestmanager/hooks/hooks_test.go @@ -111,7 +111,7 @@ func TestBlockHookProcessing(t *testing.T) { Data: extensionUpdateData, } requestID := graphsync.NewRequestID() - response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, extensionResponse) + response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, nil, extensionResponse) p := testutil.GeneratePeers(1)[0] blockData := testutil.NewFakeBlockData() @@ -208,7 +208,7 @@ func TestResponseHookProcessing(t *testing.T) { Data: extensionUpdateData, } requestID := graphsync.NewRequestID() - response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, extensionResponse) + response := gsmsg.NewResponse(requestID, graphsync.PartialResponse, nil, extensionResponse) p := testutil.GeneratePeers(1)[0] testCases := map[string]struct { diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index d8431b3d..e69f3ed7 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -21,7 +22,6 @@ 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/requestmanager/executor" "github.com/ipfs/go-graphsync/requestmanager/hooks" "github.com/ipfs/go-graphsync/requestmanager/testloader" @@ -60,24 +60,16 @@ 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 := metadata.EncodeMetadata(firstMetadata1) - firstMetadata2 := metadataForBlocks(blockChain2.Blocks(0, 3), true) - firstMetadataEncoded2 := metadata.EncodeMetadata(firstMetadata2) + 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{ + td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ requestRecords[0].gsr.ID(): firstMetadata1, requestRecords[1].gsr.ID(): firstMetadata2, }) @@ -92,18 +84,14 @@ 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 := metadata.EncodeMetadata(moreMetadata) + 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{ + td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ requestRecords[1].gsr.ID(): moreMetadata, }) @@ -135,7 +123,7 @@ func TestCancelRequestInProgress(t *testing.T) { 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), @@ -153,7 +141,7 @@ func TestCancelRequestInProgress(t *testing.T) { 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), @@ -200,7 +188,7 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { 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), } @@ -245,7 +233,7 @@ func TestCancelManagerExitsGracefully(t *testing.T) { 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), } @@ -255,7 +243,7 @@ func TestCancelManagerExitsGracefully(t *testing.T) { 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), } @@ -279,7 +267,7 @@ func TestFailedRequest(t *testing.T) { 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) @@ -307,7 +295,7 @@ func TestLocallyFulfilledFirstRequestFailsLater(t *testing.T) { // failure comes in later over network 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) @@ -336,7 +324,7 @@ func TestLocallyFulfilledFirstRequestSucceedsLater(t *testing.T) { td.blockChain.VerifyWholeChain(requestCtx, returnedResponseChan) - md := encodedMetadataForBlocks(t, td.blockChain.AllBlocks(), true) + md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, md), } @@ -359,7 +347,7 @@ func TestRequestReturnsMissingBlocks(t *testing.T) { 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), } @@ -451,10 +439,8 @@ func TestEncodingExtensions(t *testing.T) { 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, @@ -484,10 +470,8 @@ func TestEncodingExtensions(t *testing.T) { 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, @@ -565,14 +549,11 @@ func TestBlockHooks(t *testing.T) { expectedUpdate := basicnode.NewBytes(testutil.RandomBytes(100)) firstBlocks := td.blockChain.Blocks(0, 3) - firstMetadata := metadataForBlocks(firstBlocks, true) - firstMetadataEncoded := metadata.EncodeMetadata(firstMetadata) + 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, @@ -595,7 +576,7 @@ 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{ + td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ rr.gsr.ID(): firstMetadata, }) td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), firstBlocks) @@ -610,9 +591,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 @@ -625,14 +609,11 @@ func TestBlockHooks(t *testing.T) { nextExpectedUpdate1 := basicnode.NewBytes(testutil.RandomBytes(100)) nextExpectedUpdate2 := basicnode.NewBytes(testutil.RandomBytes(100)) nextBlocks := td.blockChain.RemainderBlocks(3) - nextMetadata := metadataForBlocks(nextBlocks, true) - nextMetadataEncoded := metadata.EncodeMetadata(nextMetadata) + 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, @@ -658,7 +639,7 @@ func TestBlockHooks(t *testing.T) { } td.requestManager.ProcessResponses(peers[0], secondResponses, nextBlocks) td.fal.VerifyLastProcessedBlocks(ctx, t, nextBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID]metadata.Metadata{ + td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ rr.gsr.ID(): nextMetadata, }) td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), nextBlocks) @@ -676,9 +657,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 @@ -720,19 +704,14 @@ func TestOutgoingRequestHooks(t *testing.T) { require.NoError(t, err) require.Equal(t, "chainstore", key) - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded := metadata.EncodeMetadata(md) - 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{ + td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ requestRecords[0].gsr.ID(): md, requestRecords[1].gsr.ID(): md, }) @@ -782,18 +761,13 @@ func TestOutgoingRequestListeners(t *testing.T) { t.Fatal("should fire outgoing request listener") } - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded := metadata.EncodeMetadata(md) - 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{ + td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ requestRecords[0].gsr.ID(): md, }) td.fal.SuccessResponseOn(peers[0], requestRecords[0].gsr.ID(), td.blockChain.AllBlocks()) @@ -834,13 +808,9 @@ func TestPauseResume(t *testing.T) { rr := readNNetworkRequests(requestCtx, t, td, 1)[0] // Start processing responses - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded := metadata.EncodeMetadata(md) + 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()) @@ -919,13 +889,9 @@ func TestPauseResumeExternal(t *testing.T) { rr := readNNetworkRequests(requestCtx, t, td, 1)[0] // Start processing responses - md := metadataForBlocks(td.blockChain.AllBlocks(), true) - mdEncoded := metadata.EncodeMetadata(md) + 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()) @@ -1037,27 +1003,17 @@ func readNNetworkRequests(ctx context.Context, t *testing.T, td *testData, count 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 := metadata.EncodeMetadata(md) - return graphsync.ExtensionData{ - Name: graphsync.ExtensionMetadata, - Data: metadataEncoded, - } -} - type testData struct { requestRecordChan chan requestRecord fph *fakePeerHandler diff --git a/requestmanager/server.go b/requestmanager/server.go index bd94154b..796a3fbd 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -98,7 +98,7 @@ func (rm *RequestManager) newRequest(parentSpan trace.Span, p peer.ID, root ipld inProgressChan: make(chan graphsync.ResponseProgress), inProgressErr: make(chan error), } - 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()) @@ -259,7 +259,10 @@ func (rm *RequestManager) cancelOnError(requestID graphsync.RequestID, ipr *inPr } } -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([]string, 0, len(responses)) for _, r := range responses { @@ -272,8 +275,11 @@ func (rm *RequestManager) processResponses(p peer.ID, responses []gsmsg.GraphSyn defer span.End() filteredResponses := rm.processExtensions(responses, p) filteredResponses = rm.filterResponsesForPeer(filteredResponses, p) + responseMetadata := make(map[graphsync.RequestID]graphsync.LinkMetadata, len(responses)) + for _, response := range responses { + responseMetadata[response.RequestID()] = response.Metadata() + } rm.updateLastResponses(filteredResponses) - responseMetadata := metadataForResponses(filteredResponses) rm.asyncLoader.ProcessResponse(ctx, responseMetadata, blks) rm.processTerminations(filteredResponses) log.Debugf("end processing responses for peer %s", p) diff --git a/requestmanager/testloader/asyncloader.go b/requestmanager/testloader/asyncloader.go index 0c52877c..624cda25 100644 --- a/requestmanager/testloader/asyncloader.go +++ b/requestmanager/testloader/asyncloader.go @@ -6,13 +6,14 @@ import ( "testing" blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" "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/message" "github.com/ipfs/go-graphsync/requestmanager/types" "github.com/ipfs/go-graphsync/testutil" ) @@ -35,7 +36,7 @@ type storeKey struct { type FakeAsyncLoader struct { responseChannelsLk sync.RWMutex responseChannels map[requestKey]chan types.AsyncLoadResult - responses chan map[graphsync.RequestID]metadata.Metadata + responses chan map[graphsync.RequestID]graphsync.LinkMetadata blks chan []blocks.Block storesRequestedLk sync.RWMutex storesRequested map[storeKey]struct{} @@ -46,7 +47,7 @@ type FakeAsyncLoader struct { func NewFakeAsyncLoader() *FakeAsyncLoader { return &FakeAsyncLoader{ responseChannels: make(map[requestKey]chan types.AsyncLoadResult), - responses: make(chan map[graphsync.RequestID]metadata.Metadata, 10), + responses: make(chan map[graphsync.RequestID]graphsync.LinkMetadata, 10), blks: make(chan []blocks.Block, 10), storesRequested: make(map[storeKey]struct{}), } @@ -61,7 +62,7 @@ func (fal *FakeAsyncLoader) StartRequest(requestID graphsync.RequestID, name str } // ProcessResponse just records values passed to verify expectations later -func (fal *FakeAsyncLoader) ProcessResponse(_ context.Context, responses map[graphsync.RequestID]metadata.Metadata, +func (fal *FakeAsyncLoader) ProcessResponse(_ context.Context, responses map[graphsync.RequestID]graphsync.LinkMetadata, blks []blocks.Block) { fal.responses <- responses fal.blks <- blks @@ -79,11 +80,19 @@ func (fal *FakeAsyncLoader) VerifyLastProcessedBlocks(ctx context.Context, t *te // 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) { + expectedResponses map[graphsync.RequestID][]message.GraphSyncLinkMetadatum) { t.Helper() - var responses map[graphsync.RequestID]metadata.Metadata + var responses map[graphsync.RequestID]graphsync.LinkMetadata testutil.AssertReceive(ctx, t, fal.responses, &responses, "did not process responses") - require.Equal(t, expectedResponses, responses, "did not process correct responses") + actualResponses := make(map[graphsync.RequestID][]message.GraphSyncLinkMetadatum) + for rid, lm := range responses { + actualResponses[rid] = make([]message.GraphSyncLinkMetadatum, 0) + lm.Iterate(func(c cid.Cid, la graphsync.LinkAction) { + actualResponses[rid] = append(actualResponses[rid], + message.GraphSyncLinkMetadatum{Link: c, Action: la}) + }) + } + require.Equal(t, expectedResponses, actualResponses, "did not process correct responses") } // VerifyNoRemainingData verifies no outstanding response channels are open for the given diff --git a/requestmanager/utils.go b/requestmanager/utils.go deleted file mode 100644 index 384f2e0c..00000000 --- a/requestmanager/utils.go +++ /dev/null @@ -1,31 +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: %s", response.RequestID().String()) - continue - } - md, err := metadata.DecodeMetadata(mdRaw) - if err != nil { - 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/responseassembler/responseBuilder.go b/responsemanager/responseassembler/responseBuilder.go index 25af0ec5..089318bf 100644 --- a/responsemanager/responseassembler/responseBuilder.go +++ b/responsemanager/responseassembler/responseBuilder.go @@ -131,7 +131,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()) } From 21a4546cdfd2e53d132c4015c24c487e166c9103 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Wed, 2 Feb 2022 16:45:57 +1100 Subject: [PATCH 23/32] fix: avoid double-encode for extension size estimation Closes: https://github.com/filecoin-project/lightning-planning/issues/15 --- go.mod | 2 +- go.sum | 4 ++-- responsemanager/responseassembler/responseBuilder.go | 10 ++++------ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index f7a910ea..6ef70cec 100644 --- a/go.mod +++ b/go.mod @@ -28,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.5-0.20220202110753-c322674203f0 + 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 diff --git a/go.sum b/go.sum index 9754b3ab..b923b801 100644 --- a/go.sum +++ b/go.sum @@ -456,8 +456,8 @@ github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvB 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/go.mod h1:QcE4Y9n/ZZr8Ijg5bGPT0GqYWgZ1704nH0RDcQtgTP0= -github.com/ipld/go-ipld-prime v0.14.5-0.20220202110753-c322674203f0 h1:7X6qWhXCRVVe+eeL2XZtSgVgqKCQNwuuDAjOTE/97kg= -github.com/ipld/go-ipld-prime v0.14.5-0.20220202110753-c322674203f0/go.mod h1:f5ls+uUY8Slf1NN6YUOeEyYe3TA/J02Rn7zw1NQTeSk= +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= diff --git a/responsemanager/responseassembler/responseBuilder.go b/responsemanager/responseassembler/responseBuilder.go index 089318bf..950d6cfd 100644 --- a/responsemanager/responseassembler/responseBuilder.go +++ b/responsemanager/responseassembler/responseBuilder.go @@ -1,7 +1,6 @@ package responseassembler import ( - "bytes" "context" blocks "github.com/ipfs/go-block-format" @@ -104,14 +103,13 @@ func (eo extensionOperation) build(builder *messagequeue.Builder) { } func (eo extensionOperation) size() uint64 { - // TODO: this incurs a double-encode, this first one is just to get the expected length; - // can we avoid this? if eo.extension.Data == nil { return 0 } - var buf bytes.Buffer - dagcbor.Encode(eo.extension.Data, &buf) - return uint64(buf.Len()) + // 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 { From ff4d7adf5a3f2a9a0f8240f7838f0fedd8bbbafb Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Sat, 5 Feb 2022 08:07:55 +1100 Subject: [PATCH 24/32] feat(requesttype): introduce RequestType enum to replace cancel&update bools (#352) Closes: https://github.com/ipfs/go-graphsync/issues/345 --- graphsync.go | 15 ++++++- message/ipldbind/message.go | 11 +++-- message/ipldbind/schema.ipldsch | 25 ++++++++---- message/message.go | 51 ++++++++++-------------- message/v1/message.go | 5 ++- message/v1/message_test.go | 18 ++++----- message/v2/message.go | 17 ++++---- message/v2/message_test.go | 21 ++++------ messagequeue/messagequeue_test.go | 6 +-- network/libp2p_impl_test.go | 3 +- peermanager/peermessagemanager_test.go | 6 +-- requestmanager/executor/executor_test.go | 12 +++--- requestmanager/requestmanager_test.go | 11 +++-- responsemanager/server.go | 7 ++-- 14 files changed, 107 insertions(+), 101 deletions(-) diff --git a/graphsync.go b/graphsync.go index 17b8e934..bae45ed8 100644 --- a/graphsync.go +++ b/graphsync.go @@ -171,9 +171,22 @@ type RequestData interface { 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 diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go index 849100af..4c273609 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -45,12 +45,11 @@ func (gse GraphSyncExtensions) ToExtensionsList() []graphsync.ExtensionData { type GraphSyncRequest struct { Id []byte - Root *cid.Cid - Selector *datamodel.Node - Extensions GraphSyncExtensions - Priority graphsync.Priority - Cancel bool - Update bool + Root *cid.Cid + Selector *datamodel.Node + Extensions GraphSyncExtensions + Priority graphsync.Priority + RequestType graphsync.RequestType } // GraphSyncResponse is an struct to capture data on a response sent back diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch index 0cbdaebe..7d49bf81 100644 --- a/message/ipldbind/schema.ipldsch +++ b/message/ipldbind/schema.ipldsch @@ -50,14 +50,25 @@ type GraphSyncResponseStatusCode enum { | 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 - 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 GraphSyncExtensions (rename "Ext") # side channel information - priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 - cancel Bool (rename "Canc") # whether this cancels a request - update Bool (rename "Updt") # whether this is an update to an in progress request + id GraphSyncRequestID (rename "ID") # unique id set on the requester side + 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 GraphSyncExtensions (rename "Ext") # side channel information + priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 + requestType GraphSyncRequestType (rename "Typ") # the request type } representation map type GraphSyncResponse struct { diff --git a/message/message.go b/message/message.go index c28f946c..9fa471e9 100644 --- a/message/message.go +++ b/message/message.go @@ -35,13 +35,12 @@ type MessagePartWithExtensions interface { // 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]datamodel.Node - 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 @@ -57,13 +56,12 @@ func (gsr GraphSyncRequest) String() string { extStr.WriteString(string(name)) extStr.WriteString("|") } - return fmt.Sprintf("GraphSyncRequest", + return fmt.Sprintf("GraphSyncRequest", gsr.root.String(), sel, gsr.priority, gsr.id.String(), - gsr.isCancel, - gsr.isUpdate, + gsr.requestType, extStr.String(), ) } @@ -146,17 +144,17 @@ func NewRequest(id graphsync.RequestID, 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, true, false, nil) + return newRequest(id, cid.Cid{}, nil, 0, graphsync.RequestTypeCancel, 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, false, true, toExtensionsMap(extensions)) + return newRequest(id, cid.Cid{}, nil, 0, graphsync.RequestTypeUpdate, toExtensionsMap(extensions)) } // NewLinkMetadata generates a new graphsync.LinkMetadata compatible object, @@ -179,18 +177,16 @@ func newRequest(id graphsync.RequestID, root cid.Cid, selector ipld.Node, priority graphsync.Priority, - isCancel bool, - isUpdate bool, + 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, } } @@ -310,11 +306,8 @@ func (gsr GraphSyncRequest) ExtensionNames() []graphsync.ExtensionName { 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 } @@ -379,7 +372,7 @@ func (gsr GraphSyncRequest) ReplaceExtensions(extensions []graphsync.ExtensionDa // the result 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]datamodel.Node) @@ -403,5 +396,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/v1/message.go b/message/v1/message.go index 713b4580..0554c355 100644 --- a/message/v1/message.go +++ b/message/v1/message.go @@ -119,13 +119,14 @@ func (mh *MessageHandler) ToProto(p peer.ID, gsm message.GraphSyncMessage) (*pb. 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.IsCancel(), - Update: request.IsUpdate(), + Cancel: request.Type() == graphsync.RequestTypeCancel, + Update: request.Type() == graphsync.RequestTypeUpdate, Extensions: ext, }) } diff --git a/message/v1/message_test.go b/message/v1/message_test.go index e19acf5b..22399b8a 100644 --- a/message/v1/message_test.go +++ b/message/v1/message_test.go @@ -41,7 +41,7 @@ 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()) @@ -76,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()) @@ -179,7 +178,7 @@ 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() @@ -192,7 +191,7 @@ func TestRequestCancel(t *testing.T) { 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) { @@ -213,8 +212,7 @@ 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) @@ -232,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()) @@ -281,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()) diff --git a/message/v2/message.go b/message/v2/message.go index 29098d24..fa04d755 100644 --- a/message/v2/message.go +++ b/message/v2/message.go @@ -70,13 +70,12 @@ func (mh *MessageHandler) toIPLD(gsm message.GraphSyncMessage) (*ipldbind.GraphS rootPtr = nil } ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ - Id: request.ID().Bytes(), - Root: rootPtr, - Selector: selPtr, - Priority: request.Priority(), - Cancel: request.IsCancel(), - Update: request.IsUpdate(), - Extensions: ipldbind.NewGraphSyncExtensions(request), + Id: request.ID().Bytes(), + Root: rootPtr, + Selector: selPtr, + Priority: request.Priority(), + RequestType: request.Type(), + Extensions: ipldbind.NewGraphSyncExtensions(request), }) } @@ -142,12 +141,12 @@ func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (message.Grap return message.GraphSyncMessage{}, err } - if req.Cancel { + if req.RequestType == graphsync.RequestTypeCancel { requests[id] = message.NewCancelRequest(id) continue } - if req.Update { + if req.RequestType == graphsync.RequestTypeUpdate { requests[id] = message.NewUpdateRequest(id, req.Extensions.ToExtensionsList()...) continue } diff --git a/message/v2/message_test.go b/message/v2/message_test.go index b9c3c740..7e9acad8 100644 --- a/message/v2/message_test.go +++ b/message/v2/message_test.go @@ -40,7 +40,7 @@ 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()) @@ -55,8 +55,7 @@ func TestAppendingRequests(t *testing.T) { gsrIpld := gsmIpld.Requests[0] require.Equal(t, priority, gsrIpld.Priority) - require.False(t, gsrIpld.Cancel) - require.False(t, gsrIpld.Update) + 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)) @@ -72,8 +71,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, request.Type(), graphsync.RequestTypeNew) require.Equal(t, priority, deserializedRequest.Priority()) require.Equal(t, root.String(), deserializedRequest.Root().String()) require.Equal(t, selector, deserializedRequest.Selector()) @@ -174,7 +172,7 @@ 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() @@ -187,7 +185,7 @@ func TestRequestCancel(t *testing.T) { 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) { @@ -208,8 +206,7 @@ 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) @@ -227,8 +224,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()) @@ -276,8 +272,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, 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()) diff --git a/messagequeue/messagequeue_test.go b/messagequeue/messagequeue_test.go index f182f594..bf5b8ec5 100644 --- a/messagequeue/messagequeue_test.go +++ b/messagequeue/messagequeue_test.go @@ -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 { diff --git a/network/libp2p_impl_test.go b/network/libp2p_impl_test.go index a7cdebc3..7a67dafe 100644 --- a/network/libp2p_impl_test.go +++ b/network/libp2p_impl_test.go @@ -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 d08144c1..9a13c877 100644 --- a/peermanager/peermessagemanager_test.go +++ b/peermanager/peermessagemanager_test.go @@ -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/requestmanager/executor/executor_test.go b/requestmanager/executor/executor_test.go index fc0c57f3..b25adb98 100644 --- a/requestmanager/executor/executor_test.go +++ b/requestmanager/executor/executor_test.go @@ -71,7 +71,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") }, @@ -97,7 +97,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()) }, @@ -129,7 +129,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()) }, @@ -165,7 +165,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") }, @@ -179,7 +179,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.IsUpdate()) + require.Equal(t, ree.requestsSent[1].request.Type(), graphsync.RequestTypeUpdate) data, has := ree.requestsSent[1].request.Extension("something") require.True(t, has) str, _ := data.AsString() @@ -326,7 +326,7 @@ 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()))) } else { diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index e69f3ed7..6e5fce3c 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -49,8 +49,7 @@ func TestNormalSimultaneousFetch(t *testing.T) { 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()) @@ -137,7 +136,7 @@ func TestCancelRequestInProgress(t *testing.T) { cancel1() 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) @@ -204,7 +203,7 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { 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]) @@ -827,7 +826,7 @@ func TestPauseResume(t *testing.T) { // read the outgoing cancel request pauseCancel := readNNetworkRequests(requestCtx, t, td, 1)[0] - require.True(t, pauseCancel.gsr.IsCancel()) + require.Equal(t, pauseCancel.gsr.Type(), graphsync.RequestTypeCancel) // verify no further responses come through time.Sleep(100 * time.Millisecond) @@ -902,7 +901,7 @@ func TestPauseResumeExternal(t *testing.T) { // read the outgoing cancel request pauseCancel := readNNetworkRequests(requestCtx, t, td, 1)[0] - require.True(t, pauseCancel.gsr.IsCancel()) + require.Equal(t, pauseCancel.gsr.Type(), graphsync.RequestTypeCancel) // verify no further responses come through time.Sleep(100 * time.Millisecond) diff --git a/responsemanager/server.go b/responsemanager/server.go index f71808bb..80758d04 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -190,13 +190,14 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync for _, request := range requests { key := responseKey{p: p, requestID: request.ID()} - if request.IsCancel() { + switch request.Type() { + case graphsync.RequestTypeCancel: _ = rm.abortRequest(ctx, p, request.ID(), ipldutil.ContextCancelError{}) continue - } - if request.IsUpdate() { + case graphsync.RequestTypeUpdate: rm.processUpdate(ctx, key, 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 From 586931b48f7147ade1d467b17843bff7cde30b4e Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Sat, 5 Feb 2022 08:08:45 +1100 Subject: [PATCH 25/32] fix(metadata): extend round-trip tests to byte representation (#350) --- message/v1/pb_roundtrip_test.go | 18 +++++++++++++++++- message/v2/ipld_roundtrip_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/message/v1/pb_roundtrip_test.go b/message/v1/pb_roundtrip_test.go index 79215741..c38224e2 100644 --- a/message/v1/pb_roundtrip_test.go +++ b/message/v1/pb_roundtrip_test.go @@ -7,12 +7,14 @@ import ( "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) { @@ -54,10 +56,24 @@ func TestIPLDRoundTrip(t *testing.T) { 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) - rtgsm, err := mh.fromProto(p, pgsm) + + // 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() diff --git a/message/v2/ipld_roundtrip_test.go b/message/v2/ipld_roundtrip_test.go index 61a07e0e..af0085c2 100644 --- a/message/v2/ipld_roundtrip_test.go +++ b/message/v2/ipld_roundtrip_test.go @@ -1,15 +1,19 @@ 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" ) @@ -51,10 +55,29 @@ func TestIPLDRoundTrip(t *testing.T) { 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) - rtgsm, err := NewMessageHandler().fromIPLD(igsm) + + // 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.GraphSyncMessage) + + // back to message format + rtgsm, err := NewMessageHandler().fromIPLD(rtigsm) require.NoError(t, err) rtreq := rtgsm.Requests() From 259905ab06a4df257a25ef4dda530410752a8517 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 11 Feb 2022 12:27:08 +1100 Subject: [PATCH 26/32] feat!(messagev2): tweak dag-cbor message schema (#354) * feat!(messagev2): tweak dag-cbor message schema For: 1. Efficiency: compacting the noisy structures into tuples representations and making top-level components of a message optional. 2. Migrations: providing a secondary mechanism to lean on for versioning if we want a gentler upgrade path than libp2p protocol versioning. Closes: https://github.com/ipfs/go-graphsync/issues/351 * fix(messagev2): adjust schema per feedback --- message/ipldbind/message.go | 33 +++-- message/ipldbind/schema.go | 2 +- message/ipldbind/schema.ipldsch | 62 +++++--- message/v2/ipld_roundtrip_test.go | 2 +- message/v2/message.go | 239 ++++++++++++++++++------------ message/v2/message_test.go | 8 +- 6 files changed, 213 insertions(+), 133 deletions(-) diff --git a/message/ipldbind/message.go b/message/ipldbind/message.go index 4c273609..e6e920ce 100644 --- a/message/ipldbind/message.go +++ b/message/ipldbind/message.go @@ -18,8 +18,11 @@ type GraphSyncExtensions struct { // NewGraphSyncExtensions creates GraphSyncExtensions from either a request or // response object -func NewGraphSyncExtensions(part message.MessagePartWithExtensions) GraphSyncExtensions { +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 { @@ -27,7 +30,7 @@ func NewGraphSyncExtensions(part message.MessagePartWithExtensions) GraphSyncExt data, _ := part.Extension(graphsync.ExtensionName(name)) values[string(name)] = data } - return GraphSyncExtensions{keys, values} + return &GraphSyncExtensions{keys, values} } // ToExtensionsList creates a list of graphsync.ExtensionData objects from a @@ -43,23 +46,21 @@ func (gse GraphSyncExtensions) ToExtensionsList() []graphsync.ExtensionData { // GraphSyncRequest is a struct to capture data on a request contained in a // GraphSyncMessage. type GraphSyncRequest struct { - Id []byte - + Id []byte + RequestType graphsync.RequestType + Priority *graphsync.Priority Root *cid.Cid Selector *datamodel.Node - Extensions GraphSyncExtensions - Priority graphsync.Priority - RequestType graphsync.RequestType + Extensions *GraphSyncExtensions } // GraphSyncResponse is an struct to capture data on a response sent back // in a GraphSyncMessage. type GraphSyncResponse struct { - Id []byte - + Id []byte Status graphsync.ResponseStatusCode - Metadata []message.GraphSyncLinkMetadatum - Extensions GraphSyncExtensions + Metadata *[]message.GraphSyncLinkMetadatum + Extensions *GraphSyncExtensions } // GraphSyncBlock is a container for representing extension data for bindnode, @@ -72,9 +73,13 @@ type GraphSyncBlock struct { // 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 + Requests *[]GraphSyncRequest + Responses *[]GraphSyncResponse + Blocks *[]GraphSyncBlock +} + +type GraphSyncMessageRoot struct { + Gs2 *GraphSyncMessage } // NamedExtension exists just for the purpose of the constructors diff --git a/message/ipldbind/schema.go b/message/ipldbind/schema.go index 2db86c9b..8e570fda 100644 --- a/message/ipldbind/schema.go +++ b/message/ipldbind/schema.go @@ -21,5 +21,5 @@ func init() { panic(err) } - Prototype.Message = bindnode.Prototype((*GraphSyncMessage)(nil), ts.TypeByName("GraphSyncMessage")) + Prototype.Message = bindnode.Prototype((*GraphSyncMessageRoot)(nil), ts.TypeByName("GraphSyncMessageRoot")) } diff --git a/message/ipldbind/schema.ipldsch b/message/ipldbind/schema.ipldsch index 7d49bf81..5f20390b 100644 --- a/message/ipldbind/schema.ipldsch +++ b/message/ipldbind/schema.ipldsch @@ -1,7 +1,15 @@ -type GraphSyncExtensions {String:nullable Any} +################################################################################ +### 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 @@ -18,6 +26,9 @@ type GraphSyncLinkAction enum { # 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 @@ -57,34 +68,47 @@ type GraphSyncRequestType enum { | 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 + # 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 - 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 GraphSyncExtensions (rename "Ext") # side channel information - priority GraphSyncPriority (rename "Pri") # the priority (normalized). default to 1 - requestType GraphSyncRequestType (rename "Typ") # the request type + 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 "ID") # the request id we are responding to - status GraphSyncResponseStatusCode (rename "Stat") # a status code. - metadata GraphSyncMetadata (rename "Meta") # metadata about response - extensions GraphSyncExtensions (rename "Ext") # side channel information + 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 (rename "Pre") # CID prefix (cid version, multicodec and multihash prefix (type + length) - data Bytes (rename "Data") -} representation map + 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 [GraphSyncRequest] (rename "Reqs") - responses [GraphSyncResponse] (rename "Rsps") - blocks [GraphSyncBlock] (rename "Blks") + 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/v2/ipld_roundtrip_test.go b/message/v2/ipld_roundtrip_test.go index af0085c2..b8e0b921 100644 --- a/message/v2/ipld_roundtrip_test.go +++ b/message/v2/ipld_roundtrip_test.go @@ -74,7 +74,7 @@ func TestIPLDRoundTrip(t *testing.T) { err = dagcbor.Decode(builder, &buf) require.NoError(t, err) rtnode := builder.Build() - rtigsm := bindnode.Unwrap(rtnode).(*ipldbind.GraphSyncMessage) + rtigsm := bindnode.Unwrap(rtnode).(*ipldbind.GraphSyncMessageRoot) // back to message format rtgsm, err := NewMessageHandler().fromIPLD(rtigsm) diff --git a/message/v2/message.go b/message/v2/message.go index fa04d755..8b47efb3 100644 --- a/message/v2/message.go +++ b/message/v2/message.go @@ -49,61 +49,81 @@ func (mh *MessageHandler) FromMsgReader(_ peer.ID, r msgio.Reader) (message.Grap return message.GraphSyncMessage{}, err } node := builder.Build() - ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessage) + ipldGSM := bindnode.Unwrap(node).(*ipldbind.GraphSyncMessageRoot) return mh.fromIPLD(ipldGSM) } -// ToProto converts a GraphSyncMessage to its ipldbind.GraphSyncMessage equivalent -func (mh *MessageHandler) toIPLD(gsm message.GraphSyncMessage) (*ipldbind.GraphSyncMessage, error) { +// 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() - ibm.Requests = make([]ipldbind.GraphSyncRequest, 0, len(requests)) - for _, request := range requests { - selector := request.Selector() - selPtr := &selector - if selector == nil { - selPtr = nil + 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) } - root := request.Root() - rootPtr := &root - if root == cid.Undef { - rootPtr = nil - } - ibm.Requests = append(ibm.Requests, ipldbind.GraphSyncRequest{ - Id: request.ID().Bytes(), - Root: rootPtr, - Selector: selPtr, - Priority: request.Priority(), - RequestType: request.Type(), - Extensions: ipldbind.NewGraphSyncExtensions(request), - }) + ibm.Requests = &ibmRequests } responses := gsm.Responses() - ibm.Responses = 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") + 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 = append(ibm.Responses, ipldbind.GraphSyncResponse{ - Id: response.RequestID().Bytes(), - Status: response.Status(), - Metadata: glsm.RawMetadata(), - Extensions: ipldbind.NewGraphSyncExtensions(response), - }) + ibm.Responses = &ibmResponses } blocks := gsm.Blocks() - ibm.Blocks = make([]ipldbind.GraphSyncBlock, 0, len(blocks)) - for _, b := range blocks { - ibm.Blocks = append(ibm.Blocks, ipldbind.GraphSyncBlock{ - Data: b.RawData(), - Prefix: b.Cid().Prefix().Bytes(), - }) + 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 ibm, nil + return &ipldbind.GraphSyncMessageRoot{Gs2: ibm}, nil } // ToNet writes a GraphSyncMessage in its DAG-CBOR format to a writer, @@ -132,68 +152,99 @@ func (mh *MessageHandler) ToNet(_ peer.ID, gsm message.GraphSyncMessage, w io.Wr return err } -// Mapping from a ipldbind.GraphSyncMessage object to a GraphSyncMessage object -func (mh *MessageHandler) fromIPLD(ibm *ipldbind.GraphSyncMessage) (message.GraphSyncMessage, error) { - requests := make(map[graphsync.RequestID]message.GraphSyncRequest, len(ibm.Requests)) - for _, req := range ibm.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 - } - - if req.RequestType == graphsync.RequestTypeUpdate { - requests[id] = message.NewUpdateRequest(id, req.Extensions.ToExtensionsList()...) - continue - } - - root := cid.Undef - if req.Root != nil { - root = *req.Root - } - - var selector datamodel.Node - if req.Selector != nil { - selector = *req.Selector - } - - requests[id] = message.NewRequest(id, root, selector, graphsync.Priority(req.Priority), req.Extensions.ToExtensionsList()...) +// 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") } - responses := make(map[graphsync.RequestID]message.GraphSyncResponse, len(ibm.Responses)) - for _, res := range ibm.Responses { - id, err := graphsync.ParseRequestID(res.Id) - if err != nil { - return message.GraphSyncMessage{}, err + 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...) } - responses[id] = message.NewResponse(id, - graphsync.ResponseStatusCode(res.Status), - res.Metadata, - res.Extensions.ToExtensionsList()...) } - blks := make(map[cid.Cid]blocks.Block, len(ibm.Blocks)) - for _, b := range ibm.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 + 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...) } + } - blk, err := blocks.NewBlockWithCid(b.Data, c) - if err != nil { - return message.GraphSyncMessage{}, err + 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 } - - 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 index 7e9acad8..b0c25426 100644 --- a/message/v2/message_test.go +++ b/message/v2/message_test.go @@ -53,8 +53,8 @@ func TestAppendingRequests(t *testing.T) { require.NoError(t, err, "serialize to dag-cbor errored") require.NoError(t, err) - gsrIpld := gsmIpld.Requests[0] - require.Equal(t, priority, gsrIpld.Priority) + 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) @@ -105,7 +105,7 @@ func TestAppendingResponses(t *testing.T) { gsmIpld, err := mh.toIPLD(gsm) require.NoError(t, err, "serialize to dag-cbor errored") - gsr := gsmIpld.Responses[0] + 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"]) @@ -140,7 +140,7 @@ func TestAppendBlock(t *testing.T) { require.NoError(t, err, "serializing to dag-cbor errored") // assert strings are in dag-cbor message - for _, block := range gsmIpld.Blocks { + for _, block := range *gsmIpld.Gs2.Blocks { s := bytes.NewBuffer(block.Data).String() require.True(t, contains(strs, s)) } From e66b39d441e9bdee01074a62980a85b41629fc46 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 17 Feb 2022 07:32:42 +1100 Subject: [PATCH 27/32] feat(graphsync): unify req & resp Pause, Unpause & Cancel by RequestID (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(graphsync): unify req & resp Pause, Unpause & Cancel by RequestID Closes: https://github.com/ipfs/go-graphsync/issues/349 * fixup! feat(graphsync): unify req & resp Pause, Unpause & Cancel by RequestID * fixup! feat(graphsync): unify req & resp Pause, Unpause & Cancel by RequestID when using error type T, use *T with As, rather than **T * fixup! feat(graphsync): unify req & resp Pause, Unpause & Cancel by RequestID * fixup! feat(graphsync): unify req & resp Pause, Unpause & Cancel by RequestID Co-authored-by: Daniel Martí --- graphsync.go | 22 +--- impl/graphsync.go | 49 ++++---- impl/graphsync_test.go | 6 +- requestmanager/client.go | 8 +- requestmanager/requestmanager_test.go | 8 +- requestmanager/server.go | 2 +- responsemanager/client.go | 42 +++---- responsemanager/messages.go | 22 ++-- .../queryexecutor/queryexecutor.go | 12 +- .../queryexecutor/queryexecutor_test.go | 13 +- responsemanager/responsemanager_test.go | 41 ++++--- responsemanager/server.go | 111 +++++++++--------- responsemanager/subscriber.go | 10 +- 13 files changed, 171 insertions(+), 175 deletions(-) diff --git a/graphsync.go b/graphsync.go index bae45ed8..80102c10 100644 --- a/graphsync.go +++ b/graphsync.go @@ -486,25 +486,15 @@ 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 - - // CancelResponse cancels an in progress response - CancelResponse(peer.ID, RequestID) error + Unpause(context.Context, RequestID, ...ExtensionData) error - // CancelRequest cancels an in progress request - CancelRequest(context.Context, RequestID) error + // Cancel cancels an in progress request or response + Cancel(context.Context, RequestID) 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..be70f32c 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" @@ -296,6 +297,7 @@ func New(parent context.Context, network gsnet.GraphSyncNetwork, responseManager.Startup() responseQueue.Startup(gsConfig.maxInProgressIncomingRequests, queryExecutor) network.SetDelegate((*graphSyncReceiver)(graphSync)) + return graphSync } @@ -402,35 +404,32 @@ 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...) -} - -// 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) +// 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) } -// CancelResponse cancels an in progress response -func (gs *GraphSync) CancelResponse(p peer.ID, requestID graphsync.RequestID) error { - return gs.responseManager.CancelResponse(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...) } -// CancelRequest cancels an in progress request -func (gs *GraphSync) CancelRequest(ctx context.Context, requestID graphsync.RequestID) error { - return gs.requestManager.CancelRequest(ctx, 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) } // Stats produces insight on the current state of a graphsync exchange diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 921f4215..524ac3b8 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -723,7 +723,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) @@ -793,7 +793,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) @@ -1092,7 +1092,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") diff --git a/requestmanager/client.go b/requestmanager/client.go index 5002f516..79453d0b 100644 --- a/requestmanager/client.go +++ b/requestmanager/client.go @@ -292,9 +292,9 @@ func (rm *RequestManager) ProcessResponses(p peer.ID, // 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") @@ -304,9 +304,9 @@ 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}, nil) + rm.send(&pauseRequestMessage{requestID, response}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index 6e5fce3c..5446f32d 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -816,7 +816,7 @@ func TestPauseResume(t *testing.T) { // 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 @@ -834,7 +834,7 @@ func TestPauseResume(t *testing.T) { 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 @@ -875,7 +875,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) } @@ -909,7 +909,7 @@ func TestPauseResumeExternal(t *testing.T) { 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 diff --git a/requestmanager/server.go b/requestmanager/server.go index 796a3fbd..9660ab8e 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -233,7 +233,7 @@ func (rm *RequestManager) cancelRequest(requestID graphsync.RequestID, onTermina if !ok { if onTerminated != nil { select { - case onTerminated <- graphsync.RequestNotFoundErr{}: + case onTerminated <- &graphsync.RequestNotFoundErr{}: case <-rm.ctx.Done(): } } diff --git a/responsemanager/client.go b/responsemanager/client.go index 08e4bfcd..38531a4c 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,9 @@ 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{p, requestID, queryexecutor.ErrCancelledByCommand, response}, nil) + rm.send(&errorRequestMessage{requestID, queryexecutor.ErrCancelledByCommand, response}, ctx.Done()) select { case <-rm.ctx.Done(): return errors.New("context cancelled") @@ -204,19 +200,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 +220,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 +230,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/messages.go b/responsemanager/messages.go index 917d70c1..cb052652 100644 --- a/responsemanager/messages.go +++ b/responsemanager/messages.go @@ -20,13 +20,12 @@ func (prm *processRequestsMessage) handle(rm *ResponseManager) { } 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 +33,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 +58,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 +72,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 +86,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 +101,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 +128,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 15fa66bd..af17cace 100644 --- a/responsemanager/queryexecutor/queryexecutor_test.go +++ b/responsemanager/queryexecutor/queryexecutor_test.go @@ -268,10 +268,11 @@ 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.NewRequestID() @@ -280,7 +281,6 @@ func newTestData(t *testing.T, blockCount int, expectedTraverse int) (*testData, 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 { diff --git a/responsemanager/responsemanager_test.go b/responsemanager/responsemanager_test.go index 09ea3731..6d321bd5 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -174,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) @@ -218,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) @@ -502,7 +513,7 @@ 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) }) @@ -560,7 +571,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) @@ -579,7 +590,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) } }) @@ -587,7 +598,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) @@ -606,7 +617,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) @@ -780,7 +791,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) }) }) @@ -856,7 +867,7 @@ 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) }) } diff --git a/responsemanager/server.go b/responsemanager/server.go index 80758d04..faf277f6 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 %s", key.p.Pretty(), key.requestID.String()) + log.Warnf("received update for non existent request ID %s", requestID.String()) return } @@ -89,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) @@ -106,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()) @@ -115,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") @@ -133,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), @@ -158,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 @@ -189,13 +189,12 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync defer messageSpan.End() for _, request := range requests { - key := responseKey{p: p, requestID: request.ID()} switch request.Type() { case graphsync.RequestTypeCancel: - _ = rm.abortRequest(ctx, p, request.ID(), ipldutil.ContextCancelError{}) + _ = rm.abortRequest(ctx, request.ID(), ipldutil.ContextCancelError{}) continue case graphsync.RequestTypeUpdate: - rm.processUpdate(ctx, key, request) + rm.processUpdate(ctx, request.ID(), request) continue default: } @@ -222,7 +221,7 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync )) rctx, cancelFn := context.WithCancel(rctx) sub := &subscriber{ - p: key.p, + p: p, request: request, requestCloser: rm, blockSentListeners: rm.blockSentListeners, @@ -231,16 +230,17 @@ func (rm *ResponseManager) processRequests(p peer.ID, requests []gsmsg.GraphSync connManager: rm.connManager, } log.Infow("graphsync request initiated", "request id", request.ID().String(), "peer", p, "root", request.Root()) - ipr, ok := rm.inProgressResponses[key] + 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().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), @@ -249,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.String(), "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) @@ -292,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 } @@ -313,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.String(), "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) @@ -322,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 } @@ -345,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") @@ -366,8 +365,8 @@ func (rm *ResponseManager) peerState(p peer.ID) 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)} @@ -381,11 +380,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) } } From e27c827812f5a891d074f6bde1e075b59e611ccf Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Mon, 14 Feb 2022 18:19:41 +1100 Subject: [PATCH 28/32] feat: SendUpdates() API to send only extension data to via existing request --- graphsync.go | 3 + impl/graphsync.go | 10 ++ impl/graphsync_test.go | 124 ++++++++++++++++++ requestmanager/client.go | 13 ++ requestmanager/messages.go | 14 ++ requestmanager/requestmanager_test.go | 85 ++++++++++++ requestmanager/server.go | 10 ++ responsemanager/client.go | 12 ++ responsemanager/messages.go | 14 ++ .../queryexecutor/queryexecutor_test.go | 3 + .../responseassembler/responseBuilder.go | 8 ++ .../responseassembler/responseassembler.go | 3 + responsemanager/responsemanager_test.go | 76 +++++++++++ responsemanager/server.go | 13 ++ 14 files changed, 388 insertions(+) diff --git a/graphsync.go b/graphsync.go index 80102c10..fd617c0e 100644 --- a/graphsync.go +++ b/graphsync.go @@ -496,6 +496,9 @@ type GraphExchange interface { // Cancel cancels an in progress request or response Cancel(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 be70f32c..540a88bf 100644 --- a/impl/graphsync.go +++ b/impl/graphsync.go @@ -432,6 +432,16 @@ func (gs *GraphSync) Cancel(ctx context.Context, requestID graphsync.RequestID) return gs.responseManager.CancelResponse(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 func (gs *GraphSync) Stats() graphsync.Stats { outgoingRequestStats := gs.requestQueue.Stats() diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 524ac3b8..5df1ad65 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -1708,6 +1708,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 diff --git a/requestmanager/client.go b/requestmanager/client.go index 79453d0b..88729907 100644 --- a/requestmanager/client.go +++ b/requestmanager/client.go @@ -287,6 +287,7 @@ func (rm *RequestManager) CancelRequest(ctx context.Context, requestID graphsync func (rm *RequestManager) ProcessResponses(p peer.ID, responses []gsmsg.GraphSyncResponse, blks []blocks.Block) { + rm.send(&processResponsesMessage{p, responses, blks}, nil) } @@ -315,6 +316,18 @@ func (rm *RequestManager) PauseRequest(ctx context.Context, requestID graphsync. } } +// 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(&updateRequestMessage{requestID, extensions, response}, ctx.Done()) + select { + case <-rm.ctx.Done(): + return errors.New("context cancelled") + case err := <-response: + return err + } +} + // GetRequestTask gets data for the given task in the request queue func (rm *RequestManager) GetRequestTask(p peer.ID, task *peertask.Task, requestExecutionChan chan executor.RequestTask) { rm.send(&getRequestTaskMessage{p, task, requestExecutionChan}, nil) 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/requestmanager_test.go b/requestmanager/requestmanager_test.go index 5446f32d..78ead6fd 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -859,6 +859,7 @@ func TestPauseResume(t *testing.T) { td.blockChain.VerifyRemainder(ctx, returnedResponseChan, pauseAt) testutil.VerifyEmptyErrors(ctx, t, returnedErrorChan) } + func TestPauseResumeExternal(t *testing.T) { ctx := context.Background() td := newTestData(ctx, t) @@ -935,6 +936,90 @@ func TestPauseResumeExternal(t *testing.T) { 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.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), 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") + td.fal.CleanupRequest(peers[0], rr.gsr.ID()) + + // 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) diff --git a/requestmanager/server.go b/requestmanager/server.go index 9660ab8e..cc542169 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -406,6 +406,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/responsemanager/client.go b/responsemanager/client.go index 38531a4c..9cbebc3c 100644 --- a/responsemanager/client.go +++ b/responsemanager/client.go @@ -189,6 +189,18 @@ func (rm *ResponseManager) CancelResponse(ctx context.Context, requestID graphsy } } +// 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(&updateRequestMessage{requestID, extensions, response}, ctx.Done()) + select { + case <-rm.ctx.Done(): + return errors.New("context cancelled") + case err := <-response: + return err + } +} + // Synchronize is a utility method that blocks until all current messages are processed func (rm *ResponseManager) synchronize() { sync := make(chan error) diff --git a/responsemanager/messages.go b/responsemanager/messages.go index cb052652..416d9eb0 100644 --- a/responsemanager/messages.go +++ b/responsemanager/messages.go @@ -19,6 +19,20 @@ 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 { requestID graphsync.RequestID response chan error diff --git a/responsemanager/queryexecutor/queryexecutor_test.go b/responsemanager/queryexecutor/queryexecutor_test.go index af17cace..b3b33d4f 100644 --- a/responsemanager/queryexecutor/queryexecutor_test.go +++ b/responsemanager/queryexecutor/queryexecutor_test.go @@ -427,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 950d6cfd..4973c16a 100644 --- a/responsemanager/responseassembler/responseBuilder.go +++ b/responsemanager/responseassembler/responseBuilder.go @@ -51,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 } 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/responsemanager_test.go b/responsemanager/responsemanager_test.go index 6d321bd5..f30d998a 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -264,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() @@ -500,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() @@ -517,6 +519,7 @@ func TestValidationAndExtensions(t *testing.T) { 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) @@ -816,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() @@ -828,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() @@ -843,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() @@ -872,6 +878,70 @@ func TestNetworkErrors(t *testing.T) { }) } +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 @@ -1027,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) } diff --git a/responsemanager/server.go b/responsemanager/server.go index faf277f6..97a1b16a 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -360,6 +360,19 @@ func (rm *ResponseManager) pauseRequest(requestID graphsync.RequestID) error { return nil } +func (rm *ResponseManager) updateRequest(requestID graphsync.RequestID, extensions []graphsync.ExtensionData) error { + inProgressResponse, ok := rm.inProgressResponses[requestID] + if !ok || inProgressResponse.state == graphsync.CompletingSend { + 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) { From 8106555390b93191ff717b3588eb2f752ad8c370 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Wed, 16 Feb 2022 14:25:27 -0800 Subject: [PATCH 29/32] fix(responsemanager): send update while completing If request has finished selector traversal but is still sending blocks, I think it should be possible to send updates. As a side effect, this fixes our race. Logically, this makes sense, cause our external indicator that we're done (completed response listener) has not been called. --- responsemanager/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responsemanager/server.go b/responsemanager/server.go index 97a1b16a..7927b26a 100644 --- a/responsemanager/server.go +++ b/responsemanager/server.go @@ -362,7 +362,7 @@ func (rm *ResponseManager) pauseRequest(requestID graphsync.RequestID) error { func (rm *ResponseManager) updateRequest(requestID graphsync.RequestID, extensions []graphsync.ExtensionData) error { inProgressResponse, ok := rm.inProgressResponses[requestID] - if !ok || inProgressResponse.state == graphsync.CompletingSend { + if !ok { return graphsync.RequestNotFoundErr{} } _ = inProgressResponse.responseStream.Transaction(func(rb responseassembler.ResponseBuilder) error { From db297c1c4fe336d1edbf0699d22847bc8fbb4191 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Thu, 17 Feb 2022 08:43:34 -0800 Subject: [PATCH 30/32] fix(requestmanager): revert change to pointer type --- requestmanager/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requestmanager/server.go b/requestmanager/server.go index cc542169..2c536e1d 100644 --- a/requestmanager/server.go +++ b/requestmanager/server.go @@ -233,7 +233,7 @@ func (rm *RequestManager) cancelRequest(requestID graphsync.RequestID, onTermina if !ok { if onTerminated != nil { select { - case onTerminated <- &graphsync.RequestNotFoundErr{}: + case onTerminated <- graphsync.RequestNotFoundErr{}: case <-rm.ctx.Done(): } } From f6a08b5ccaaf7cff572aad84186480d6adc05027 Mon Sep 17 00:00:00 2001 From: Hannah Howard Date: Fri, 18 Feb 2022 12:54:58 -0800 Subject: [PATCH 31/32] Refactor async loading for simplicity and correctness (#356) * feat(reconciledloader): first working version of reconciled loader * feat(traversalrecorder): add better recorder for traversals * feat(reconciledloader): pipe reconciled loader through code style(lint): fix static checks * Update requestmanager/reconciledloader/injest.go Co-authored-by: Rod Vagg * feat(reconciledloader): respond to PR comments Co-authored-by: Rod Vagg --- docs/async-loading.png | Bin 119522 -> 0 bytes docs/async-loading.puml | 75 --- docs/processes.png | Bin 86948 -> 84544 bytes docs/processes.puml | 21 +- docs/request-execution.png | Bin 0 -> 170616 bytes docs/request-execution.puml | 102 +++ docs/responder-sequence.puml | 36 +- graphsync.go | 21 +- impl/graphsync.go | 18 +- impl/graphsync_test.go | 226 +------ message/message.go | 5 + .../persistenceoptions.go | 0 requestmanager/asyncloader/asyncloader.go | 216 ------ .../asyncloader/asyncloader_test.go | 396 ----------- .../loadattemptqueue/loadattemptqueue.go | 96 --- .../loadattemptqueue/loadattemptqueue_test.go | 184 ----- .../responsecache/responsecache.go | 107 --- .../responsecache/responsecache_test.go | 147 ---- .../unverifiedblockstore.go | 109 --- .../unverifiedblockstore_test.go | 47 -- requestmanager/client.go | 37 +- requestmanager/executor/executor.go | 81 +-- requestmanager/executor/executor_test.go | 97 ++- requestmanager/reconciledloader/injest.go | 44 ++ requestmanager/reconciledloader/load.go | 184 +++++ .../reconciledloader/pathtracker.go | 41 ++ .../reconciledloader/reconciledloader.go | 124 ++++ .../reconciledloader/reconciledloader_test.go | 628 ++++++++++++++++++ .../reconciledloader/remotequeue.go | 121 ++++ .../traversalrecord/traversalrecord.go | 181 +++++ .../traversalrecord/traversalrecord_test.go | 215 ++++++ requestmanager/requestmanager_test.go | 143 ++-- requestmanager/server.go | 58 +- requestmanager/testloader/asyncloader.go | 173 ----- responsemanager/responsemanager_test.go | 2 +- testutil/testchain.go | 20 +- 36 files changed, 1945 insertions(+), 2010 deletions(-) delete mode 100644 docs/async-loading.png delete mode 100644 docs/async-loading.puml create mode 100644 docs/request-execution.png create mode 100644 docs/request-execution.puml rename {responsemanager/persistenceoptions => persistenceoptions}/persistenceoptions.go (100%) delete mode 100644 requestmanager/asyncloader/asyncloader.go delete mode 100644 requestmanager/asyncloader/asyncloader_test.go delete mode 100644 requestmanager/asyncloader/loadattemptqueue/loadattemptqueue.go delete mode 100644 requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go delete mode 100644 requestmanager/asyncloader/responsecache/responsecache.go delete mode 100644 requestmanager/asyncloader/responsecache/responsecache_test.go delete mode 100644 requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore.go delete mode 100644 requestmanager/asyncloader/unverifiedblockstore/unverifiedblockstore_test.go create mode 100644 requestmanager/reconciledloader/injest.go create mode 100644 requestmanager/reconciledloader/load.go create mode 100644 requestmanager/reconciledloader/pathtracker.go create mode 100644 requestmanager/reconciledloader/reconciledloader.go create mode 100644 requestmanager/reconciledloader/reconciledloader_test.go create mode 100644 requestmanager/reconciledloader/remotequeue.go create mode 100644 requestmanager/reconciledloader/traversalrecord/traversalrecord.go create mode 100644 requestmanager/reconciledloader/traversalrecord/traversalrecord_test.go delete mode 100644 requestmanager/testloader/asyncloader.go diff --git a/docs/async-loading.png b/docs/async-loading.png deleted file mode 100644 index 4b856ce2092f3332a9649141dd9328ccdc91e338..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119522 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

Ihm-#L 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 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 964dffd323c3b6fb19fb282921df6a5f25c10403..ddd3289cbadcd0ef53c4a301bc394a244d938948 100644 GIT binary patch literal 84544 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 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 0000000000000000000000000000000000000000..8487c5102f7f71805a89a0ae3cfad7f33f0d91d2 GIT binary patch literal 170616 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 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/graphsync.go b/graphsync.go index fd617c0e..0e0536d6 100644 --- a/graphsync.go +++ b/graphsync.go @@ -127,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 ( @@ -223,6 +238,8 @@ 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) diff --git a/impl/graphsync.go b/impl/graphsync.go index 540a88bf..36ecd9ba 100644 --- a/impl/graphsync.go +++ b/impl/graphsync.go @@ -21,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" @@ -51,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 @@ -231,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 { @@ -267,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, @@ -341,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) } diff --git a/impl/graphsync_test.go b/impl/graphsync_test.go index 5df1ad65..66d0cad7 100644 --- a/impl/graphsync_test.go +++ b/impl/graphsync_test.go @@ -7,13 +7,11 @@ import ( "fmt" "io" "io/ioutil" - "math" "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" @@ -45,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" @@ -69,130 +66,6 @@ var protocolsForTest = map[string]struct { "(v1.0 -> v1.0)": {[]protocol.ID{gsnet.ProtocolGraphsync_1_0_0}, []protocol.ID{gsnet.ProtocolGraphsync_1_0_0}}, } -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 datamodel.Node - // 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.NewRequestID() - - 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 []datamodel.Node - 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") -} - func TestRejectRequestsByDefault(t *testing.T) { // create network @@ -224,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)", @@ -278,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) @@ -327,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 @@ -411,8 +284,8 @@ func TestGraphsyncRoundTrip(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 processUpdateSpan := tracing.FindSpanByTraceString("response(0)") require.Equal(t, int64(0), testutil.AttributeValueInTraceSpan(t, *processUpdateSpan, "priority").AsInt64()) @@ -420,18 +293,18 @@ func TestGraphsyncRoundTrip(t *testing.T) { // each verifyBlock span should link to a cacheProcess span that stored it - cacheProcessSpans := tracing.FindSpans("cacheProcess") - cacheProcessLinks := make(map[string]int64) + 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 _, cacheProcessSpan := range cacheProcessSpans { - sid := cacheProcessSpan.SpanContext.SpanID().String() + for _, prcessResponseSpan := range processResponsesSpans { + sid := prcessResponseSpan.SpanContext.SpanID().String() if verifyBlockSpan.Links[0].SpanContext.SpanID().String() == sid { found = true - cacheProcessLinks[sid] = cacheProcessLinks[sid] + 1 + processResponsesLinks[sid] = processResponsesLinks[sid] + 1 break } } @@ -440,9 +313,9 @@ func TestGraphsyncRoundTrip(t *testing.T) { // 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") + 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") } }) } @@ -487,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]) @@ -510,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) { @@ -744,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) @@ -827,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) @@ -1115,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) @@ -1256,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) @@ -1345,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 @@ -1600,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) { @@ -1965,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").AsStringSlice()) == 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/message/message.go b/message/message.go index 9fa471e9..354cc236 100644 --- a/message/message.go +++ b/message/message.go @@ -351,6 +351,11 @@ func (gslm GraphSyncLinkMetadata) Iterate(iter graphsync.LinkMetadataIterator) { } } +// 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 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 b97c1576..00000000 --- a/requestmanager/asyncloader/asyncloader.go +++ /dev/null @@ -1,216 +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/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]graphsync.LinkMetadata, - blks []blocks.Block) { - - requestIds := make([]string, 0, len(responses)) - for requestID := range responses { - requestIds = append(requestIds, requestID.String()) - } - ctx, span := otel.Tracer("graphsync").Start(ctx, "loaderProcess", trace.WithAttributes( - attribute.StringSlice("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]graphsync.LinkMetadata, 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 5845e260..00000000 --- a/requestmanager/asyncloader/asyncloader_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package asyncloader - -import ( - "context" - "io" - "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/message" - "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.NewRequestID() - 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.NewRequestID() - responses := map[graphsync.RequestID]graphsync.LinkMetadata{ - requestID: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.Cid, - Action: graphsync.LinkActionPresent, - }}), - } - 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.NewRequestID() - - responses := map[graphsync.RequestID]graphsync.LinkMetadata{ - requestID: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.(cidlink.Link).Cid, - Action: graphsync.LinkActionMissing, - }}), - } - 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.NewRequestID() - 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.NewRequestID() - 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]graphsync.LinkMetadata{ - requestID: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.Cid, - Action: graphsync.LinkActionPresent, - }}), - } - 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.NewRequestID() - 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]graphsync.LinkMetadata{ - requestID: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.(cidlink.Link).Cid, - Action: graphsync.LinkActionMissing, - }}), - } - 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.NewRequestID() - 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.NewRequestID() - responses := map[graphsync.RequestID]graphsync.LinkMetadata{ - requestID: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.Cid, - Action: graphsync.LinkActionPresent, - }}), - } - 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.NewRequestID() - err := asyncLoader.StartRequest(requestID1, "other") - require.EqualError(t, err, "unknown persistence option") - - err = asyncLoader.RegisterPersistenceOption("other", otherSt.lsys) - require.NoError(t, err) - requestID2 := graphsync.NewRequestID() - 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.NewRequestID() - 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.NewRequestID() - p := testutil.GeneratePeers(1)[0] - - resultChan1 := asyncLoader.AsyncLoad(p, requestID1, link, ipld.LinkContext{}) - requestID2 := graphsync.NewRequestID() - 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.NewRequestID() - requestID2 := graphsync.NewRequestID() - 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]graphsync.LinkMetadata{ - requestID1: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.Cid, - Action: graphsync.LinkActionPresent, - }}), - requestID2: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.Cid, - Action: graphsync.LinkActionPresent, - }}), - } - 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.NewRequestID() - requestID2 := graphsync.NewRequestID() - 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]graphsync.LinkMetadata{ - requestID2: message.NewLinkMetadata( - []message.GraphSyncLinkMetadatum{{ - Link: link.Cid, - Action: graphsync.LinkActionPresent, - }}), - } - 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 9c83a426..00000000 --- a/requestmanager/asyncloader/loadattemptqueue/loadattemptqueue_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package loadattemptqueue - -import ( - "context" - "fmt" - "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.NewRequestID() - 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.NewRequestID() - 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.NewRequestID() - 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.NewRequestID() - 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.NewRequestID() - 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 ffaab02e..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" - "github.com/ipfs/go-cid" - 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" -) - -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]graphsync.LinkMetadata, - 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 { - md.Iterate(func(c cid.Cid, la graphsync.LinkAction) { - log.Debugf("Traverse link %s on request ID %s", c.String(), requestID.String()) - rc.linkTracker.RecordLinkTraversal(requestID, cidlink.Link{Cid: c}, la == graphsync.LinkActionPresent) - }) - } - - // 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 7cc8497f..00000000 --- a/requestmanager/asyncloader/responsecache/responsecache_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package responsecache - -import ( - "context" - "fmt" - "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/message" - "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.NewRequestID() - requestID2 := graphsync.NewRequestID() - - request1Metadata := []message.GraphSyncLinkMetadatum{ - { - Link: blks[0].Cid(), - Action: graphsync.LinkActionPresent, - }, - { - Link: blks[1].Cid(), - Action: graphsync.LinkActionMissing, - }, - { - Link: blks[3].Cid(), - Action: graphsync.LinkActionPresent, - }, - } - - request2Metadata := []message.GraphSyncLinkMetadatum{ - { - Link: blks[1].Cid(), - Action: graphsync.LinkActionPresent, - }, - { - Link: blks[3].Cid(), - Action: graphsync.LinkActionPresent, - }, - { - Link: blks[4].Cid(), - Action: graphsync.LinkActionPresent, - }, - } - - responses := map[graphsync.RequestID]graphsync.LinkMetadata{ - requestID1: message.NewLinkMetadata(request1Metadata), - requestID2: message.NewLinkMetadata(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 88729907..885f4f5f 100644 --- a/requestmanager/client.go +++ b/requestmanager/client.go @@ -28,7 +28,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/types" + "github.com/ipfs/go-graphsync/requestmanager/reconciledloader" "github.com/ipfs/go-graphsync/taskqueue" ) @@ -61,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 @@ -68,28 +70,23 @@ 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]graphsync.LinkMetadata, 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 @@ -118,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, @@ -132,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), diff --git a/requestmanager/executor/executor.go b/requestmanager/executor/executor.go index 4659a70d..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, } } @@ -84,6 +85,7 @@ func (e *Executor) ExecuteTask(ctx context.Context, pid peer.ID, task *peertask. span.RecordError(err) if !ipldutil.IsContextCancelErr(err) { 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,7 +155,6 @@ func (e *Executor) traverse(rt RequestTask) error { if err != nil { return 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())}) @@ -233,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 b25adb98..a3e8562b 100644 --- a/requestmanager/executor/executor_test.go +++ b/requestmanager/executor/executor_test.go @@ -4,12 +4,15 @@ 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" @@ -22,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" ) @@ -45,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) @@ -194,9 +196,11 @@ 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() + reconciledLoader := &fakeReconciledLoader{ + responses: make(map[datamodel.Link]chan types.AsyncLoadResult), + } requestID := graphsync.NewRequestID() p := testutil.GeneratePeers(1)[0] requestCtx, requestCancel := context.WithCancel(ctx) @@ -209,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, @@ -225,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() { @@ -247,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) @@ -263,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 { @@ -282,6 +287,7 @@ type requestExecutionEnv struct { externalPause pauseKey loadLocallyUntil int traverser ipldutil.Traverser + reconciledLoader *fakeReconciledLoader inProgressErr chan error initialRequest bool customRemoteBehavior func() @@ -292,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) @@ -314,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 { @@ -328,7 +387,7 @@ func (ree *requestExecutionEnv) SendRequest(p peer.ID, request gsmsg.GraphSyncRe ree.requestsSent = append(ree.requestsSent, requestSent{p, request}) 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() } @@ -341,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/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 78ead6fd..23e21473 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -18,14 +18,12 @@ import ( "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/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" ) @@ -67,13 +65,6 @@ func TestNormalSimultaneousFetch(t *testing.T) { } td.requestManager.ProcessResponses(peers[0], firstResponses, firstBlocks) - td.fal.VerifyLastProcessedBlocks(ctx, t, firstBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ - 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) @@ -89,13 +80,6 @@ func TestNormalSimultaneousFetch(t *testing.T) { } td.requestManager.ProcessResponses(peers[0], moreResponses, moreBlocks) - td.fal.VerifyLastProcessedBlocks(ctx, t, moreBlocks) - td.fal.VerifyLastProcessedResponses(ctx, t, map[graphsync.RequestID][]gsmsg.GraphSyncLinkMetadatum{ - 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) @@ -129,9 +113,6 @@ func TestCancelRequestInProgress(t *testing.T) { } 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, 1)[0] @@ -146,8 +127,6 @@ func TestCancelRequestInProgress(t *testing.T) { 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) @@ -168,16 +147,6 @@ 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, 1) @@ -192,14 +161,12 @@ func TestCancelRequestImperativeNoMoreBlocks(t *testing.T) { 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, 1)[0] @@ -212,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) { @@ -237,7 +199,6 @@ func TestCancelManagerExitsGracefully(t *testing.T) { 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() @@ -247,7 +208,6 @@ func TestCancelManagerExitsGracefully(t *testing.T) { 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) } @@ -275,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) @@ -285,7 +252,7 @@ func TestLocallyFulfilledFirstRequestFailsLater(t *testing.T) { returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] // async loaded response responds immediately td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) @@ -294,7 +261,7 @@ func TestLocallyFulfilledFirstRequestFailsLater(t *testing.T) { // failure comes in later over network failedResponses := []gsmsg.GraphSyncResponse{ - gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestFailedContentNotFound, nil), + gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestFailedContentNotFound), } td.requestManager.ProcessResponses(peers[0], failedResponses, nil) @@ -316,14 +283,14 @@ func TestLocallyFulfilledFirstRequestSucceedsLater(t *testing.T) { }) returnedResponseChan, returnedErrorChan := td.requestManager.NewRequest(requestCtx, peers[0], td.blockChain.TipLink, td.blockChain.Selector()) - rr := readNNetworkRequests(requestCtx, t, td, 1)[0] + rr := readNNetworkRequests(requestCtx, t, td.requestRecordChan, 1)[0] // async loaded response responds immediately td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), td.blockChain.AllBlocks()) td.blockChain.VerifyWholeChain(requestCtx, returnedResponseChan) - md := metadataForBlocks(td.blockChain.AllBlocks(), graphsync.LinkActionPresent) + md := encodedMetadataForBlocks(t, td.blockChain.AllBlocks(), true) firstResponses := []gsmsg.GraphSyncResponse{ gsmsg.NewResponse(rr.gsr.ID(), graphsync.RequestCompletedFull, md), } @@ -333,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() @@ -351,9 +319,6 @@ func TestRequestReturnsMissingBlocks(t *testing.T) { 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") @@ -574,11 +539,6 @@ 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][]gsmsg.GraphSyncLinkMetadatum{ - rr.gsr.ID(): firstMetadata, - }) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), firstBlocks) ur := readNNetworkRequests(requestCtx, t, td, 1)[0] receivedUpdateData, has := ur.gsr.Extension(td.extensionName1) @@ -637,11 +597,6 @@ 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][]gsmsg.GraphSyncLinkMetadatum{ - rr.gsr.ID(): nextMetadata, - }) - td.fal.SuccessResponseOn(peers[0], rr.gsr.ID(), nextBlocks) ur = readNNetworkRequests(requestCtx, t, td, 1)[0] receivedUpdateData, has = ur.gsr.Extension(td.extensionName1) @@ -683,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 { @@ -709,20 +666,11 @@ func TestOutgoingRequestHooks(t *testing.T) { 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][]gsmsg.GraphSyncLinkMetadatum{ - 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 { @@ -765,11 +713,6 @@ func TestOutgoingRequestListeners(t *testing.T) { 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][]gsmsg.GraphSyncLinkMetadatum{ - 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) @@ -812,7 +755,6 @@ func TestPauseResume(t *testing.T) { 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) @@ -831,30 +773,32 @@ func TestPauseResume(t *testing.T) { // 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(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, 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) @@ -894,7 +838,6 @@ func TestPauseResumeExternal(t *testing.T) { 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 @@ -907,13 +850,15 @@ func TestPauseResumeExternal(t *testing.T) { // 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(ctx, rr.gsr.ID(), td.extension1, td.extension2) require.NoError(t, err) // verify the correct new request with Do-no-send-cids & other extensions + /* 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) @@ -925,11 +870,10 @@ 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) @@ -1101,7 +1045,7 @@ func metadataForBlocks(blks []blocks.Block, action graphsync.LinkAction) []gsmsg type testData struct { requestRecordChan chan requestRecord fph *fakePeerHandler - fal *testloader.FakeAsyncLoader + persistenceOptions *persistenceoptions.PersistenceOptions tcm *testutil.TestConnManager requestHooks *hooks.OutgoingRequestHooks responseHooks *hooks.IncomingResponseHooks @@ -1109,6 +1053,8 @@ 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 datamodel.Node @@ -1128,7 +1074,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() @@ -1136,9 +1082,10 @@ 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) diff --git a/requestmanager/server.go b/requestmanager/server.go index 2c536e1d..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. @@ -63,7 +66,7 @@ func (rm *RequestManager) newRequest(parentSpan trace.Span, p peer.ID, root ipld 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()) @@ -97,6 +100,7 @@ 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, nil)) rm.inProgressRequestStatuses[request.ID()] = requestStatus @@ -113,9 +117,7 @@ func (rm *RequestManager) requestTask(requestID graphsync.RequestID) executor.Re } 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{ @@ -147,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) } @@ -162,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, } } @@ -190,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) @@ -255,7 +260,7 @@ func (rm *RequestManager) cancelOnError(requestID graphsync.RequestID, ipr *inPr rm.terminateRequest(requestID, ipr) } else { ipr.cancelFn() - rm.asyncLoader.CompleteResponsesFor(requestID) + ipr.reconciledLoader.SetRemoteOnline(false) } } @@ -271,16 +276,22 @@ func (rm *RequestManager) processResponses(p peer.ID, ctx, span := otel.Tracer("graphsync").Start(rm.ctx, "processResponses", trace.WithAttributes( attribute.String("peerID", p.Pretty()), attribute.StringSlice("requestIDs", requestIds), + attribute.Int("blockCount", len(blks)), )) defer span.End() filteredResponses := rm.processExtensions(responses, p) filteredResponses = rm.filterResponsesForPeer(filteredResponses, p) - responseMetadata := make(map[graphsync.RequestID]graphsync.LinkMetadata, len(responses)) - for _, response := range responses { - responseMetadata[response.RequestID()] = response.Metadata() + 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) - rm.asyncLoader.ProcessResponse(ctx, responseMetadata, blks) rm.processTerminations(filteredResponses) log.Debugf("end processing responses for peer %s", p) } @@ -338,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{ { @@ -370,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 { diff --git a/requestmanager/testloader/asyncloader.go b/requestmanager/testloader/asyncloader.go deleted file mode 100644 index 624cda25..00000000 --- a/requestmanager/testloader/asyncloader.go +++ /dev/null @@ -1,173 +0,0 @@ -package testloader - -import ( - "context" - "sync" - "testing" - - blocks "github.com/ipfs/go-block-format" - "github.com/ipfs/go-cid" - "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/message" - "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]graphsync.LinkMetadata - 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]graphsync.LinkMetadata, 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]graphsync.LinkMetadata, - 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][]message.GraphSyncLinkMetadatum) { - t.Helper() - var responses map[graphsync.RequestID]graphsync.LinkMetadata - testutil.AssertReceive(ctx, t, fal.responses, &responses, "did not process responses") - actualResponses := make(map[graphsync.RequestID][]message.GraphSyncLinkMetadatum) - for rid, lm := range responses { - actualResponses[rid] = make([]message.GraphSyncLinkMetadatum, 0) - lm.Iterate(func(c cid.Cid, la graphsync.LinkAction) { - actualResponses[rid] = append(actualResponses[rid], - message.GraphSyncLinkMetadatum{Link: c, Action: la}) - }) - } - require.Equal(t, expectedResponses, actualResponses, "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/responsemanager/responsemanager_test.go b/responsemanager/responsemanager_test.go index f30d998a..b7de9b5f 100644 --- a/responsemanager/responsemanager_test.go +++ b/responsemanager/responsemanager_test.go @@ -29,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" 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 { From c938c6be98d7c26cd4c7e96b74e537bb9231f036 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Fri, 18 Feb 2022 12:59:33 -0800 Subject: [PATCH 32/32] fix(requestmanager): update test for rebase --- requestmanager/requestmanager_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/requestmanager/requestmanager_test.go b/requestmanager/requestmanager_test.go index 23e21473..fe06304f 100644 --- a/requestmanager/requestmanager_test.go +++ b/requestmanager/requestmanager_test.go @@ -915,7 +915,6 @@ func TestUpdateRequest(t *testing.T) { 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()) td.blockChain.VerifyResponseRange(ctx, returnedResponseChan, 0, 1) // wait for the pause to occur @@ -927,7 +926,6 @@ func TestUpdateRequest(t *testing.T) { // 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()) // send an update with some custom extensions ext1Name := graphsync.ExtensionName("grip grop")